Downloading Jenkins Logs
a way to analyze Jenkins logs offline

Recently, I encountered a problem on one of the integration test run by Jenkins. This particular test was failing “sometimes”. The problem was that sometimes, the Selenium integration was timing out because a page was too slow, but it was hard to find which part of the test was the one failing. I needed some statistical information on how the test was running, but the one we have on Jenkins didn’t expose that information. As an alternative to change Jenkins configuration, I could analyze Jenkins test logs. Jenkins provides an RSS feed with all the run information, including a URL to a gzip-ed file containing the logs I need. In this article, I will describe the code I create to download this logs files to do further analysis locally.

First, I need the simplest framework I could find just to make a GET request to get the RSS feed, and then to get the gzip-ed log file. In the past, I used scalaj-http for simple REST-full service consumption on scripts. It is a simple-blocking-wrapping of the good-old-java HttpUrlConnection. That will do for this problem. The following is the build.sbt I created for the project:

name := "jenkins-rss"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.0.4"
libraryDependencies += "org.scalaj" %% "scalaj-http" % "1.1.5"
libraryDependencies += "com.github.nscala-time" %% "nscala-time" % "2.0.0"
libraryDependencies += "com.typesafe" % "config" % "1.3.0"
mainClass in (Compile, run) := Some("JekinsReader")

The last line on the build.sbt is using assembly plugin. This plugin needs to be configured by adding the file /project/assembly.sbt as described here.

To facilitate the HTTP communication, a http.Util object was created containing functions for Basic authentication and GET operations:

package http
import scala.util.{Success, Failure, Try}
import scalaj.http.{HttpResponse, HttpRequest, Base64, Http}
object Utils {
def authenticatedGetString(username: String, password: String)(url: String) : Try[String] =
(addAuthentication(username, password) _ andThen getString)(Http(url))
def authenticatedGetBytes(username: String, password: String)(url: String) : Try[Array[Byte]] =
(addAuthentication(username, password) _ andThen getBytes)(Http(url))
def addAuthentication(username: String, password: String)(req: HttpRequest) : HttpRequest =
req.header("Authorization", "Basic " + Base64.encodeString(username + ":" + password))
def getString = get[HttpResponse[String],String](req => req.asString)(_)
def getBytes = get[HttpResponse[Array[Byte]],Array[Byte]](req => req.asBytes)(_)
def get[T <: HttpResponse[U], U]( requester: HttpRequest => T )( req: HttpRequest ) : Try[U] =
Try( requester( req ) ) match {
case Success( res ) => {
val body = res.body
if ( res.isError ) {
Failure( new Exception( "code : " + res.code + ", body : " + body ))
} else {
Success(body)
}
}
case Failure(e) => Failure(e)
}
}

Using this functions, the main object JenkinsReader is created:

import java.io.{BufferedWriter, ByteArrayInputStream, FileWriter}
import java.nio.file.{Paths, Files}
import java.util.zip.GZIPInputStream
import com.typesafe.config.ConfigFactory
import http.Utils
import scala.io.Source
import scala.xml.XML
object JenkinsReader {
def main(args: Array[String]) = {
val conf = ConfigFactory.load()
val url = conf.getString("jenkins-reader.jenkins.rss-url")
val fileUri = conf.getString("jenkins-reader.jenkins.file-uri")
val runIdPattern = conf.getString("jenkins-reader.jenkins.run-id-extraction-pattern").r
val username = args(0)
val password = args(1)
val outputDir = args(2)
println(s"Jenkins username: $username, password: $password" )
println(s"Jenkins rss url: $url")
println(s"Jenkins log file uri: $fileUri")
println(s"Run id and fail/success extraction runIdPattern: $runIdPattern")
println(s"Output directory: $outputDir")
println()
val myGet = Utils.authenticatedGetString(username, password)(_)
val myGetBytes = Utils.authenticatedGetBytes(username, password)(_)
myGet(url)
.map ( data => {
val xml = XML.loadString(data)
val entries = xml \\ "feed" \\ "entry"
entries.map( node => ( (node \\ "title").text, (node \\ "link").\@("href") ) )
} )
.getOrElse(Seq())
.map( entry => {
val ( title, url ) = entry
title match {
case runIdPattern(entryRun, rest) => ( entryRun, rest.contains("fail"), url)
}
})
.filter( s => s._2)
.map( entry => {
val ( entryNumber, isFail, url ) = entry
val runState = if ( isFail ) "fail" else "stable"
val fileName = s"$outputDir/$entryNumber-log-$runState.log"
( fileName, url + fileUri)
})
.filter( file => !Files.exists(Paths.get(file._1)) )
.foreach( fileUrl => {
val ( file, url ) = fileUrl
myGetBytes(url) map( data => {
val text = Source
.fromInputStream( new GZIPInputStream(new ByteArrayInputStream(data)))
.getLines.toList
println( s"Writing file: $file" )
val writer = new BufferedWriter(new FileWriter(file))
text.foreach( line => {
writer.write(line)
writer.newLine()
})
writer.close()
})
})
}
}

The code expects to be executed passing Jenkin’s username/password and the output directory as parameters (lines 17 - 19). This code use Lightbend(Typesafe) Config to configure the following data:

The code is very straightforward, here is some description of it:

This code could be better as some errors could be handled (Ex. the code could not write to a particular path), but I hope it will be useful to somebody as it was for me.

Happy coding!

*****
Written by Darien Martinez Torres on 03 April 2016