Overview

This usage page will give a quick overview of Blindsight.

Logger Resolvers

The simplest possible Logger is created from a LoggerFactory:

import com.tersesystems.blindsight.LoggerFactory
val logger = LoggerFactory.getLogger(getClass)

There is also a macro based version which finds the enclosing class name and hands it to you:

val loggerFromEnclosing = LoggerFactory.getLogger

The LoggerFactory LoggerResolver type class under the hood, which means you have the option of creating your own logger resolver. This is useful when you want to get away from class based logging, and use a naming strategy based on a correlation id. For example:

trait LoggerResolver[T] {
  def resolveLogger(instance: T): org.slf4j.Logger
}

means you can resolve a logger directly from a request:

implicit val requestToResolver: LoggerResolver[Request] = LoggerResolver { (instance: Request) => 
  org.slf4j.LoggerFactory.getLoggerFactory.getLogger("requests." + instance.requestId())
}

And from then on, you can do:

val myRequest: Request = ...
val logger = LoggerFactory.getLogger(myRequest)

See Logger Resolvers for more details.

SLF4J API

The default logger provides an SLF4J-like API:

logger.info("I am an SLF4J-like logger")

This converts to a Message class, and logs only if the level of the SLF4J logger is set to at least INFO. Very roughly:

val infoMethod: InfoMethod = logger.info
infoMethod.apply(Message("I am an SLF4J-like logger"))

// rough implementation
class InfoMethod {
  def apply(msg: Message) {
    if (slf4jLogger.isInfoEnabled()) {
       slf4jLogger.info(msg.toString)
    }
  }
}

First, let’s explain Message and its compatriots.

A single logging statement in SLF4J consists of a set of parameters in combination:

Marker marker = ...
String message = ...
Object[] arguments = ...
logger.info(marker, message, arguments);

All of these together make a logging statement.

Blindsight keeps the same concept these parameters, but creates specific types; Markers, Message, and Argument, with a Statement that encompasses all the above.

val markers: Markers = Markers(marker1, marker2)
val message: Message = Message("some message")
val argument1: Argument = Argument("arg1")

logger.info(markers, message, argument1);

Where possible, Blindsight provides type class mappings to automatically convert to the appropriate type. So Markers has a ToMarkers type class, Message has ToMessage, and Argument has ToArgument.

There is an implicit conversion from String to Message:

logger.info("this is a message");

And the API takes ToArgument for automatic conversion:

logger.info("this is a message", "arg1", "arg2");

The Arguments class aggregates multiple arguments together when there are more than two arguments. The Arguments() method takes a varadic list of arguments that can be hetrogeneous.

val arguments: Arguments = Arguments("arg1", 42, true)
logger.info(markers, message, arguments);

Exceptions must be at the end of the statement, and are not aggregated with arguments. This is to encourage type safety and make it impossible to include an exception as an argument by accident.

logger.info("Message with arguments and exceptions", arguments, exception);

You can use type class instances to extend Blindsight’s functionality. For example, you can pass a feature flag into isDebugEnabled and it will convert it into a Markers:

object Slf4jMain {
  final case class FeatureFlag(flagName: String)
  object FeatureFlag {
    implicit val toMarkers: ToMarkers[FeatureFlag] = ToMarkers { instance =>
      Markers(MarkerFactory.getDetachedMarker(instance.flagName))
    }
  }

  def main(args: Array[String]): Unit = {
    val logger: Logger = LoggerFactory.getLogger(getClass)

    val featureFlag = FeatureFlag("flag.enabled")
    // this is not a marker, but is converted via type class.
    if (logger.isDebugEnabled(featureFlag)) {
      logger.debug("this is a test")
    }
  }
}

See the SLF4J API page for more details.

Fluent API

The Logger instance provides access to a fluent builder logger API. A FluentLogger is accessible through logger.fluent:

import com.tersesystems.blindsight.fluent.FluentLogger

val fluentLogger: FluentLogger = logger.fluent
fluentLogger.info.message("I am a fluent logger").log()

See Fluent API for details.

Semantic API

The Logger instance provides access to a semantic, strongly typed logging API. A SemanticLogger is accessible through logger.semantic:


import com.tersesystems.blindsight.semantic.SemanticLogger val semanticLogger: SemanticLogger[UserEvent] = logger.semantic[UserEvent] val userLoggedInEvent = UserLoggedInEvent(name = "steve") semanticLogger.info(userEvent)

The semantic API takes a single strongly typed argument, and the logger is instantiated with the type that is acceptable. This makes it very useful for event logging and domain oriented observability.

See Semantic API for details.

Flow API

The Logger instance provides access to a control flow based logging wrapper. A FlowLogger is accessible through logger.flow:

import com.tersesystems.blindsight.ToArgument

import com.tersesystems.blindsight.flow._

implicit def flowBehavior[B: ToArgument]: FlowBehavior[B] = new SimpleFlowBehavior 

def flowMethod(arg1: Int, arg2: Int): Int = logger.flow.trace {
  arg1 + arg2
}

The flow API is used to render the entry and exit of a given method. It is tied together with a flow behavior which provides the relevant Statement on entry and exit. This can also be used for timers and hierarchical tracing.

See Flow API for more details.

Structured Logging

Blindsight provides structured logging using a DSL which converts to Logstash Markers and StructuredArguments. This makes structured logging easy and intuitive, and provides structured output in both line based and JSON based formats.

logger.info("some message", bobj("a" -> "b"))

In line format:

2020-04-05T23:09:08.436Z example.Main$ some message a=b

And in JSON:

{
  "@timestamp": "2020-04-05T23:09:08.436Z",
  "@version": "1",
  "message": "some message",
  "logger_name": "example.Main$",
  "a": "b"
}

See Structured Logging for more details.

Conditional Logging

No matter how fast your logging is, it’s always faster not to log a statement at all. Blindsight does its best to allow the system to not log as much as it makes it possible to log, allowing you to dynamically manage the CPU and memory pressure demands of logging.

Most of the loggers have an withCondition method that returns a conditional logger of the same type.

This logger will only log if the condition is true:

def booleanCondition: Boolean = ...
val conditionalLogger = logger.withCondition(booleanCondition)
conditionalLogger.info("Only logs when condition is true")

By contrast, when is used on methods, rather than the logger, and provides a block that is executed only when the condition is true:

logger.info.when(booleanCondition) { info =>
  info("log")
}

See Conditional Logging for more details.

Contextual Logging

Blindsight builds up context with markers. You can use withMarkers to add markers that don’t have to explicitly added to a statement.

import net.logstash.logback.marker.{Markers => LogstashMarkers}
val loggerWithMarkers = logger.withMarkers(LogstashMarkers.append("user", "will"))

See Context for more details.

Entry Transformation

After a statement passes through predicates and just before it is sent off to SLF4J, there is an opportunity to change the Entry from a function using Entry Transformation. Entry transformation allows for hooks into the logging system for debugging, testing, and auditing.

val logger = LoggerFactory.getLogger
               .withEntryTransform(e => e.copy(message = e.message + " IN BED"))

logger.info("You will discover your hidden talents")

Event Buffer

An Event Buffer is provided to an entry transformation that stores the Entry along with the timestamp, logging level, logger name, so that logging events are available to the application directly. Blindsight provides a prepackaged in-memory ring buffer implementation that is thread safe and performant.

val queueBuffer = EventBuffer(50000)
val logger      = LoggerFactory.getLogger.withEventBuffer(queueBuffer)

logger.info("Hello world")

val event = queueBuffer.head

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, using the SourceInfoMixin on the logger.

When using blindsight-generic, this returns Markers.empty, but when using blindsight-logstash, this adds source.line, source.file and source.enclosing to the JSON logs automatically:

{
  "@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"
}

This is the default behavior, and you can override sourceInfoMarker in your own implementation to return whatever you like using a custom logger factory.

See Source Code for more details.

Scripting

There are times when you want to reconfigure logging behavior. You can do that at the macro level with logging levels, but Blindsight gives you far more control, allowing you to change logging by individual method or even line number, in conjunction with Tweakflow Scripts that can be modified while the JVM is running.

Here’s an example Tweakflow script that will only enable logging that are in given methods and default to a level:

library blindsight {
  # level: the result of org.slf4j.event.Level.toInt()
  # enc: <class>.<method> i.e. com.tersesystems.blindsight.groovy.Main.logDebugSpecial
  # line: line number of the source code where condition was created
  # file: absolute path of the file containing the condition
  #
  doc 'Evaluates a condition'
  function evaluate: (long level, string enc, long line, string file) ->
    if (enc == "exampleapp.MyClass.logDebugSpecial") then true
    else (level >= 20); # info_int = 20
}

In this case, the script will return true if the logging statement is in the logDebugSpecial method of exampleapp.MyClass:

package exampleapp
class MyClass {
  def logDebugSpecial(): Unit = {
    logger.debug.when(location.here) { log => log("This will log!")}
  }
}

Otherwise, the script will return true iff the level is above or equal to 20 (the int value of INFO).

See Scripting for more details.

Inspections

Inspections are debugging focused methods and macros, intended to ease the experience of “printf debugging” and provide an experience closer to an interactive debugger.

For example if you want to debug the statements in a block, you would use decorateVals:

import com.tersesystems.blindsight.inspection.InspectionMacros._

decorateVals(dval => logger.debug(s"${dval.name} = ${dval.value}")) {
  val a = 5
  val b = 15
  a + b
}

See Inspections for more details.

The source code for this page can be found here.