app/com/gu/contentapi/sanity/support/ElkFriendlyReporter.scala (79 lines of code) (raw):

package com.gu.contentapi.sanity.support import org.scalatest.Reporter import org.scalatest.events.{AlertProvided, Event, LineInFile, Location, TestFailed, TestIgnored, TestStarting, TestSucceeded, TopOfClass, TopOfMethod} import org.slf4j.{LoggerFactory, MDC} import scala.util.Try /** * This is a custom Reporter class for Scalatest which aims to give nicely parseable logs in central ELK */ class ElkFriendlyReporter extends Reporter { private val logger = LoggerFactory.getLogger(getClass) //Saves a copy of the logger context, calls the block safely and resets the context afterward private def withIsolatedContext(f: =>Unit) = { val backup = MDC.getCopyOfContextMap try { f } finally { MDC.setContextMap(backup) } } private def renderLocationMaybe(loc: Option[Location]) = loc match { case Some(LineInFile(lineNumber, fileName, filePathname))=> s"$fileName line $lineNumber" case Some(TopOfClass(className))=> s"Top of $className" case Some(TopOfMethod(className, methodId))=> s"Top of ${className}.$methodId" case None=> "(not provided)" } override def apply(event: Event): Unit = withIsolatedContext { event match { case AlertProvided(_, message, nameInfo, throwable, _, location, _, threadName, _) => MDC.put("threadName", threadName) nameInfo.foreach(names=>{ MDC.put("suite", names.suiteName) MDC.put("testName", names.testName.getOrElse("(not provided)")) }) MDC.put("location", renderLocationMaybe(location)) logger.error(message, throwable) case TestSucceeded(_, suiteName, _, suiteClassName, testName, testText, _, duration, _, location, _, _, threadName, _)=> MDC.put("threadName", threadName) MDC.put("suiteName", suiteName) MDC.put("status", "success") MDC.put("logger_name", suiteClassName.getOrElse("(not provided)")) MDC.put("testName", testName) MDC.put("testText", testText) MDC.put("location", renderLocationMaybe(location)) logger.info(s"'$testName' succeeded in ${duration.map(_.toString).getOrElse("(not provided)")}ms") case TestFailed(_, message, suiteName, _, suiteClassName, testName, testText, _, analysis, throwable, duration, _, location, _, _, threadName, _)=> MDC.put("threadName", threadName) MDC.put("suiteName", suiteName) MDC.put("status", "Failed") MDC.put("logger_name", suiteClassName.getOrElse("(not provided)")) MDC.put("testName", testName) MDC.put("testText", testText) MDC.put("failureMessage", message) MDC.put("exceptionMsg", throwable.flatMap(t=>Option(t.getMessage)).getOrElse("(not present)")) MDC.put("location", renderLocationMaybe(location)) analysis.foreach[Unit](a=>{ logger.info(a) }) logger.warn(s"'$testName' failed in ${duration.map(_.toString).getOrElse("(not provided)")}ms: $message") case TestIgnored(_, suiteName, _, suiteClassName, testName, testText, _, location, _, threadName, _)=> MDC.put("threadName", threadName) MDC.put("suiteName", suiteName) MDC.put("status", "ignored") MDC.put("logger_name", suiteClassName.getOrElse("(not provided)")) MDC.put("testName", testName) MDC.put("testText", testText) MDC.put("location", renderLocationMaybe(location)) logger.info(s"'$testName' was ignored") case TestStarting(_, suiteName, _, suiteClassName, testName, testText, _, location, _, _, threadName, _)=> MDC.put("threadName", threadName) MDC.put("suiteName", suiteName) MDC.put("logger_name", suiteClassName.getOrElse("(not provided)")) MDC.put("testName", testName) MDC.put("testText", testText) MDC.put("location", renderLocationMaybe(location)) logger.debug(s"'$testName' starting") case _=> //there are loads of other events we don't need to concern ourselves with right now } } }