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.

Installation

This library is in blindsight-scripting and depends on blindsight-api:

sbt
libraryDependencies += "com.tersesystems.blindsight" %% "blindsight-scripting" % "1.5.2"
Maven
<properties>
  <scala.binary.version>2.13</scala.binary.version>
</properties>
<dependencies>
  <dependency>
    <groupId>com.tersesystems.blindsight</groupId>
    <artifactId>blindsight-scripting_${scala.binary.version}</artifactId>
    <version>1.5.2</version>
  </dependency>
</dependencies>
Gradle
def versions = [
  ScalaBinary: "2.13"
]
dependencies {
  implementation "com.tersesystems.blindsight:blindsight-scripting_${versions.ScalaBinary}:1.5.2"
}

Usage

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 integer value of INFO).

Tweakflow has its own reference documentation, but it does not cover the standard library functions which include string matching. The test suite is a good place to start to show the standard library’s capabilities.

Configuration

Script-driven logging is most useful when it replaces level based logging, so in logback.xml you should set the level to ALL for the package you want:

<logger name="exampleapp" value="ALL"/>
Warning

You should not set the root logger level to ALL.

Since Blindsight can only add source level information to your own code, packages based on libraries, i.e. play.api and akka will still use the SLF4J API directly and will not go through scripting.

You can integrate tweakflow scripts through ScriptHandle and ScriptManager instances. When the handle’s isInvalid method returns true, the script is re-evaluated by the ScriptManager on the fly.

An example FileScriptHandle that compares the file’s last modified date to determine validity. A verifier function is provided, which can be leveraged to check the script with a message authentication code. Please see the SignatureBuilder in the test cases on Github for examples.

Script Aware Logging

You can leverage scripting from a ScriptAwareLogger. All calls to the logger will pass through the script automatically.

You can create ScriptAwareLogger directly with a CoreLogger:

import com.tersesystems.blindsight.scripting._
val slf4jLogger = org.slf4j.LoggerFactory.getLogger(getClass)
val scriptManager: ScriptManager = ???
val logger = new ScriptAwareLogger(CoreLogger(slf4jLogger), scriptManager)

Or you can register all logging using a custom logger factory.

Start by installing blindsight-generic which does not have a service provider already exposed.

Create a factory class:

sourceclass ScriptingLoggerFactory extends LoggerFactory {

  private val logger = org.slf4j.LoggerFactory.getLogger(getClass)

  val scriptHandle = new ScriptHandle {
    override def isInvalid: Boolean = false

    override val script: String =
      """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) ->
        |    level >= 20; # info_int = 20
        |}
        |""".stripMargin

    override def report(e: Throwable): Unit = e match {
      case lang: LangException =>
        val info = lang.getSourceInfo
        if (info != null) {
          logger.error("Cannot evaluate script {}", info: Any, e: Any)
        } else {
          logger.error("Cannot evaluate script", e)
        }
      case other: Throwable =>
        logger.error("Cannot evaluate script", other)
    }
  }
  private val cm = new ScriptManager(scriptHandle)

  override def getLogger[T: LoggerResolver](instance: T): Logger = {
    val underlying = implicitly[LoggerResolver[T]].resolveLogger(instance)
    new ScriptAwareLogger(CoreLogger(underlying), cm)
  }
}

To activate the ScriptingLoggerFactory, you must register it with the service loader by creating a service provider configuration file. In a resources directory, create a META-INF/services directory, and create a com.tersesystems.blindsight.LoggerFactory file containing the following:

# com.tersesystems.blindsight.LoggerFactory
com.tersesystems.blindsight.scripting.ScriptingLoggerFactory

Script Conditions

If you only want some logging statements to be source aware, you can use a ScriptBasedLocation, which returns a condition containing the source code information used by a script.

sourceobject ConditionExample {
  def main(args: Array[String]): Unit = {
    // or use FileScriptHandle
    val scriptHandle = new ScriptHandle {
      override def isInvalid: Boolean = false
      override val script: String =
        """import strings as s from 'std.tf';
          |alias s.ends_with? as ends_with?;
          |
          |library blindsight {
          |  function evaluate: (long level, string enc, long line, string file) ->
          |    if (ends_with?(enc, "main")) then true
          |    else false;
          |}
          |""".stripMargin
      override def report(e: Throwable): Unit = e.printStackTrace()
    }
    val sm     = new ScriptManager(scriptHandle)
    val logger = LoggerFactory.getLogger

    val location = new ScriptBasedLocation(sm, true)
    logger.debug.when(location.here) { log => // line 37 :-)
      log("Hello world!")
    }
  }
}

Other Scripting Languages

You can also integrate Blindsight with more powerful scripting languages like Groovy using JSR-223, but they do allow arbitrary execution and can pose a security risk. Please see the blog post for details.

The source code for this page can be found here.