#3080 - Including a timestamp in LogEvent and in the MDC

This commit is contained in:
Viktor Klang 2013-04-21 00:40:47 -07:00
parent 75c9d1d5ad
commit 7f84dbf448
7 changed files with 95 additions and 19 deletions

View file

@ -7,10 +7,12 @@ import akka.testkit._
import scala.concurrent.duration._ import scala.concurrent.duration._
import com.typesafe.config.{ Config, ConfigFactory } import com.typesafe.config.{ Config, ConfigFactory }
import akka.actor.{ ActorRef, Actor, ActorSystem } import akka.actor.{ ActorRef, Actor, ActorSystem }
import java.util.{ Date, GregorianCalendar, TimeZone, Calendar }
import org.scalatest.WordSpec import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers import org.scalatest.matchers.MustMatchers
import akka.serialization.SerializationExtension import akka.serialization.SerializationExtension
import akka.event.Logging.{ Warning, LogEvent, LoggerInitialized, InitializeLogger } import akka.event.Logging.{ Warning, LogEvent, LoggerInitialized, InitializeLogger }
import akka.util.Helpers
object LoggerSpec { object LoggerSpec {
@ -144,6 +146,19 @@ class LoggerSpec extends WordSpec with MustMatchers {
} }
} }
"Ticket 3080" must {
"format currentTimeMillis to a valid UTC String" in {
val timestamp = System.currentTimeMillis
val c = new GregorianCalendar(TimeZone.getTimeZone("UTC"))
c.setTime(new Date(timestamp))
val hours = c.get(Calendar.HOUR_OF_DAY)
val minutes = c.get(Calendar.MINUTE)
val seconds = c.get(Calendar.SECOND)
val ms = c.get(Calendar.MILLISECOND)
Helpers.currentTimeMillisToUTCString(timestamp) must be(f"$hours%02d:$minutes%02d:$seconds%02d.$ms%03dUTC")
}
}
"Ticket 3165 - serialize-messages and dual-entry serialization of LogEvent" must { "Ticket 3165 - serialize-messages and dual-entry serialization of LogEvent" must {
"not cause StackOverflowError" in { "not cause StackOverflowError" in {
implicit val s = ActorSystem("foo", ticket3165Config) implicit val s = ActorSystem("foo", ticket3165Config)

View file

@ -577,6 +577,11 @@ object Logging {
@transient @transient
val thread: Thread = Thread.currentThread val thread: Thread = Thread.currentThread
/**
* When this LogEvent was created according to System.currentTimeMillis
*/
val timestamp: Long = System.currentTimeMillis
/** /**
* The LogLevel of this LogEvent * The LogLevel of this LogEvent
*/ */
@ -675,6 +680,7 @@ object Logging {
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
private val date = new Date()
private val dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss.SSS") private val dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss.SSS")
private val errorFormat = "[ERROR] [%s] [%s] [%s] %s%s".intern private val errorFormat = "[ERROR] [%s] [%s] [%s] %s%s".intern
private val errorFormatWithoutCause = "[ERROR] [%s] [%s] [%s] %s".intern private val errorFormatWithoutCause = "[ERROR] [%s] [%s] [%s] %s".intern
@ -682,7 +688,10 @@ object Logging {
private val infoFormat = "[INFO] [%s] [%s] [%s] %s".intern private val infoFormat = "[INFO] [%s] [%s] [%s] %s".intern
private val debugFormat = "[DEBUG] [%s] [%s] [%s] %s".intern private val debugFormat = "[DEBUG] [%s] [%s] [%s] %s".intern
def timestamp(): String = synchronized { dateFormat.format(new Date) } // SDF isn't threadsafe def timestamp(event: LogEvent): String = synchronized {
date.setTime(event.timestamp)
dateFormat.format(date)
} // SDF isn't threadsafe
def print(event: Any): Unit = event match { def print(event: Any): Unit = event match {
case e: Error error(e) case e: Error error(e)
@ -695,7 +704,7 @@ object Logging {
def error(event: Error): Unit = { def error(event: Error): Unit = {
val f = if (event.cause == Error.NoCause) errorFormatWithoutCause else errorFormat val f = if (event.cause == Error.NoCause) errorFormatWithoutCause else errorFormat
println(f.format( println(f.format(
timestamp, timestamp(event),
event.thread.getName, event.thread.getName,
event.logSource, event.logSource,
event.message, event.message,
@ -704,21 +713,21 @@ object Logging {
def warning(event: Warning): Unit = def warning(event: Warning): Unit =
println(warningFormat.format( println(warningFormat.format(
timestamp, timestamp(event),
event.thread.getName, event.thread.getName,
event.logSource, event.logSource,
event.message)) event.message))
def info(event: Info): Unit = def info(event: Info): Unit =
println(infoFormat.format( println(infoFormat.format(
timestamp, timestamp(event),
event.thread.getName, event.thread.getName,
event.logSource, event.logSource,
event.message)) event.message))
def debug(event: Debug): Unit = def debug(event: Debug): Unit =
println(debugFormat.format( println(debugFormat.format(
timestamp, timestamp(event),
event.thread.getName, event.thread.getName,
event.logSource, event.logSource,
event.message)) event.message))
@ -735,10 +744,9 @@ object Logging {
val path: ActorPath = new RootActorPath(Address("akka", "all-systems"), "/StandardOutLogger") val path: ActorPath = new RootActorPath(Address("akka", "all-systems"), "/StandardOutLogger")
def provider: ActorRefProvider = throw new UnsupportedOperationException("StandardOutLogger does not provide") def provider: ActorRefProvider = throw new UnsupportedOperationException("StandardOutLogger does not provide")
override val toString = "StandardOutLogger" override val toString = "StandardOutLogger"
override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit = { override def !(message: Any)(implicit sender: ActorRef = Actor.noSender): Unit =
if (message == null) throw new InvalidMessageException("Message is null") if (message == null) throw new InvalidMessageException("Message is null")
print(message) else print(message)
}
} }
val StandardOutLogger = new StandardOutLogger val StandardOutLogger = new StandardOutLogger

View file

@ -37,6 +37,21 @@ object Helpers {
} }
} }
/**
* Converts a "currentTimeMillis"-obtained timestamp accordingly:
* "$hours%02d:$minutes%02d:$seconds%02d.$ms%03dUTC"
* @param timestamp a "currentTimeMillis"-obtained timestamp
* @return A String formatted like: $hours%02d:$minutes%02d:$seconds%02d.$ms%03dUTC
*/
def currentTimeMillisToUTCString(timestamp: Long): String = {
val timeOfDay = timestamp % 86400000L
val hours = timeOfDay / 3600000L
val minutes = timeOfDay / 60000L % 60
val seconds = timeOfDay / 1000L % 60
val ms = timeOfDay % 1000
f"$hours%02d:$minutes%02d:$seconds%02d.$ms%03dUTC"
}
final val base64chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~" final val base64chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~"
@tailrec @tailrec

View file

@ -284,3 +284,16 @@ information is available in the MDC with attribute name ``akkaSource``::
For more details on what this attribute contains—also for non-actors—please see For more details on what this attribute contains—also for non-actors—please see
`How to Log`_. `How to Log`_.
More accurate timestamps for log output in MDC
------------------------------------------------
Akka's logging is asynchronous which means that the timestamp of a log entry is taken from
when the underlying logger implementation is called, which can be surprising at first.
If you want to more accurately output the timestamp, use the MDC attribute ``akkaTimestamp``::
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
</appender>

View file

@ -322,3 +322,17 @@ information is available in the MDC with attribute name ``akkaSource``::
For more details on what this attribute contains—also for non-actors—please see For more details on what this attribute contains—also for non-actors—please see
`How to Log`_. `How to Log`_.
More accurate timestamps for log output in MDC
------------------------------------------------
Akka's logging is asynchronous which means that the timestamp of a log entry is taken from
when the underlying logger implementation is called, which can be surprising at first.
If you want to more accurately output the timestamp, use the MDC attribute ``akkaTimestamp``::
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%X{akkaTimestamp} %-5level %logger{36} %X{akkaSource} - %msg%n</pattern>
</encoder>
</appender>

View file

@ -69,8 +69,10 @@ class DefaultOSGiLogger extends DefaultLogger {
*/ */
def logMessage(logService: LogService, event: LogEvent) { def logMessage(logService: LogService, event: LogEvent) {
event match { event match {
case error: Logging.Error if error.cause != NoCause logService.log(event.level.asInt, messageFormat.format(timestamp, event.thread.getName, event.logSource, event.message), error.cause) case error: Logging.Error if error.cause != NoCause
case _ logService.log(event.level.asInt, messageFormat.format(timestamp, event.thread.getName, event.logSource, event.message)) logService.log(event.level.asInt, messageFormat.format(timestamp(event), event.thread.getName, event.logSource, event.message), error.cause)
case _
logService.log(event.level.asInt, messageFormat.format(timestamp(event), event.thread.getName, event.logSource, event.message))
} }
} }

View file

@ -9,6 +9,7 @@ import org.slf4j.MDC
import akka.event.Logging._ import akka.event.Logging._
import akka.actor._ import akka.actor._
import akka.event.DummyClassForStringSources import akka.event.DummyClassForStringSources
import akka.util.Helpers
/** /**
* Base trait for all classes that wants to be able use the SLF4J logging infrastructure. * Base trait for all classes that wants to be able use the SLF4J logging infrastructure.
@ -54,11 +55,12 @@ class Slf4jLogger extends Actor with SLF4JLogging {
val mdcThreadAttributeName = "sourceThread" val mdcThreadAttributeName = "sourceThread"
val mdcAkkaSourceAttributeName = "akkaSource" val mdcAkkaSourceAttributeName = "akkaSource"
val mdcAkkaTimestamp = "akkaTimestamp"
def receive = { def receive = {
case event @ Error(cause, logSource, logClass, message) case event @ Error(cause, logSource, logClass, message)
withMdc(logSource, event.thread.getName) { withMdc(logSource, event) {
cause match { cause match {
case Error.NoCause | null Logger(logClass, logSource).error(if (message != null) message.toString else null) case Error.NoCause | null Logger(logClass, logSource).error(if (message != null) message.toString else null)
case cause Logger(logClass, logSource).error(if (message != null) message.toString else cause.getLocalizedMessage, cause) case cause Logger(logClass, logSource).error(if (message != null) message.toString else cause.getLocalizedMessage, cause)
@ -66,13 +68,13 @@ class Slf4jLogger extends Actor with SLF4JLogging {
} }
case event @ Warning(logSource, logClass, message) case event @ Warning(logSource, logClass, message)
withMdc(logSource, event.thread.getName) { Logger(logClass, logSource).warn("{}", message.asInstanceOf[AnyRef]) } withMdc(logSource, event) { Logger(logClass, logSource).warn("{}", message.asInstanceOf[AnyRef]) }
case event @ Info(logSource, logClass, message) case event @ Info(logSource, logClass, message)
withMdc(logSource, event.thread.getName) { Logger(logClass, logSource).info("{}", message.asInstanceOf[AnyRef]) } withMdc(logSource, event) { Logger(logClass, logSource).info("{}", message.asInstanceOf[AnyRef]) }
case event @ Debug(logSource, logClass, message) case event @ Debug(logSource, logClass, message)
withMdc(logSource, event.thread.getName) { Logger(logClass, logSource).debug("{}", message.asInstanceOf[AnyRef]) } withMdc(logSource, event) { Logger(logClass, logSource).debug("{}", message.asInstanceOf[AnyRef]) }
case InitializeLogger(_) case InitializeLogger(_)
log.info("Slf4jLogger started") log.info("Slf4jLogger started")
@ -80,16 +82,23 @@ class Slf4jLogger extends Actor with SLF4JLogging {
} }
@inline @inline
final def withMdc(logSource: String, thread: String)(logStatement: Unit) { final def withMdc(logSource: String, logEvent: LogEvent)(logStatement: Unit) {
MDC.put(mdcAkkaSourceAttributeName, logSource) MDC.put(mdcAkkaSourceAttributeName, logSource)
MDC.put(mdcThreadAttributeName, thread) MDC.put(mdcThreadAttributeName, logEvent.thread.getName)
try { MDC.put(mdcAkkaTimestamp, formatTimestamp(logEvent.timestamp))
logStatement try logStatement finally {
} finally {
MDC.remove(mdcAkkaSourceAttributeName) MDC.remove(mdcAkkaSourceAttributeName)
MDC.remove(mdcThreadAttributeName) MDC.remove(mdcThreadAttributeName)
MDC.remove(mdcAkkaTimestamp)
} }
} }
/**
* Override this method to provide a differently formatted timestamp
* @param timestamp a "currentTimeMillis"-obtained timestamp
* @return the given timestamp as a UTC String
*/
protected def formatTimestamp(timestamp: Long): String =
Helpers.currentTimeMillisToUTCString(timestamp)
} }