Source Code
SLF4J can give access to the line and file of source code, but this is done at runtime and is very expensive. Blindsight provides this information for free, at compile time, through sourcecode macros. Internally, the LogstashLoggerFactory
adds extra markers to logging statements based on the macros.
Note that this only provides source code info where Blindsight is used, not generally across Logback. If you want source code information out of Logback generally with the compile time overhead of caller info, check out instrumentation.
Usage
To enable this, use blindsight-logstash
and add a blindsight.source.enabled
property to the Logback context with the value of true
:
<configuration>
<property name="blindsight.source.enabled" value="true" scope="context"/>
<!-- ... -->
</configuration>
This adds source.line
, source.file
and source.enclosing
to the JSON logs:
{
"@timestamp": "2020-04-12T17:58:45.410Z",
"@version": "1",
"message": "this is a test",
"logger_name": "example.slf4j.Slf4jMain$",
"thread_name": "run-main-0",
"level": "DEBUG",
"level_value": 10000,
"source.line": 39,
"source.file": "/home/wsargent/work/blindsight/example/src/main/scala/example/slf4j/Slf4jMain.scala",
"source.enclosing": "example.slf4j.Slf4jMain.main"
}
Changing Property Names
This is the default behavior. You can change the name of the labels used in the event that you have a conflict.
For example, Filebeat 6.8 and later uses source.* internally and will not collect logs if a field with that prefix already exists.
To change the source labels to fit Elastic Common Schema, you can set the properties in logback.xml
:
<configuration>
<property name="blindsight.source.file" value="log.origin.file.name" scope="context"/>
<property name="blindsight.source.line" value="log.origin.file.line" scope="context"/>
<property name="blindsight.source.enclosing" value="log.origin.function" scope="context"/>
<!-- ... -->
</configuration>
Customizing LoggerFactory
If you want to customize the source code behavior, you can provide your own LoggerFactory
implementation. The LoggerFactory
is provided from a service loader implementation, so the easiest thing to do is install the generic logger and implement META-INF/services/com.tersesystems.blindsight.LoggerFactory
with your own LoggerFactory
.
Writing your own logger factory is not too hard – here’s the LogstashLoggerFactory
for reference.
class LogstashLoggerFactory extends LoggerFactory {
override def getLogger[T: LoggerResolver](instance: T): Logger = {
val underlying = implicitly[LoggerResolver[T]].resolveLogger(instance)
new Logger.Impl(CoreLogger(underlying, sourceInfoBehavior(underlying)))
}
protected def sourceInfoBehavior(underlying: org.slf4j.Logger): Option[SourceInfoBehavior] = {
if (sourceInfoEnabled(underlying)) {
Some(sourceInfoAsMarker(underlying))
} else {
None
}
}
protected def sourceInfoEnabled(underlying: org.slf4j.Logger): Boolean = {
val enabled = property(underlying, LogstashLoggerFactory.SourceEnabledProperty)
java.lang.Boolean.parseBoolean(enabled.getOrElse(java.lang.Boolean.FALSE.toString))
}
protected def property(underlying: org.slf4j.Logger, propertyName: String): Option[String] = {
val logbackLogger = underlying.asInstanceOf[ch.qos.logback.classic.Logger]
Option(logbackLogger.getLoggerContext.getProperty(propertyName))
}
protected def sourceInfoAsMarker(underlying: org.slf4j.Logger): SourceInfoBehavior = {
import LogstashLoggerFactory._
val fileLabel = property(underlying, SourceFileProperty).getOrElse("source.file")
val lineLabel = property(underlying, SourceLineProperty).getOrElse("source.line")
val enclosingLabel = property(underlying, SourceEnclosingProperty).getOrElse("source.enclosing")
new SourceInfoBehavior.Impl(fileLabel, lineLabel, enclosingLabel)
}
}
object LogstashLoggerFactory {
val SourceEnabledProperty = "blindsight.source.enabled"
val SourceFileProperty = "blindsight.source.file"
val SourceLineProperty = "blindsight.source.line"
val SourceEnclosingProperty = "blindsight.source.enclosing"
}