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"/>
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.