Dynamic Logging With Ammonite and JBang
It was fun putting together a proof of concept, so I've been trying to see what the smallest possible dynamic logging demo can be.
Ammonite and JBang are tools that internalize the work of dependency management and build options so that you can run a script without going through a build tool. This is really useful if you want to just download and play with something, because you can just copy and paste text into a file and be done.
Here's the github repo.
Ammonite
First the Ammonite script, you can run this using amm script.sc
:
import $ivy.{
`com.tersesystems.echopraxia.plusscala::logger:1.1.2`,
`com.tersesystems.echopraxia:scripting:2.2.4`,
`com.tersesystems.echopraxia:logstash:2.2.4`,
`com.tersesystems.logback:logback-classic:1.2.0`,
`com.lihaoyi::os-lib:0.9.1`
}
import com.tersesystems.echopraxia.plusscala._
import com.tersesystems.echopraxia.plusscala.api._
import com.tersesystems.echopraxia.scripting._
import com.tersesystems.logback.classic.ChangeLogLevel
case class ScriptService(dir: os.Path) {
private val sws = new ScriptWatchService(dir.toNIO);
def condition(path: os.Path) = {
val scriptHandle = sws.watchScript(path.toNIO, _.printStackTrace)
ScriptCondition.create(scriptHandle).asScala
}
}
object TweakFlow {
val default = """
|library echopraxia {
| function evaluate: (string level, dict ctx) ->
| let {
| find_string: ctx[:find_string];
| }
| find_string("$.foo") == "bar";
|}
""".stripMargin
}
@main
def main() = {
// No logback.xml, we're doing it live
val changer = new ChangeLogLevel
changer.changeLogLevel("ROOT", "INFO")
val logger = LoggerFactory.getLogger
changer.changeLogLevel(logger.name, "DEBUG")
// Ensure a script exists and is watched
val dir = os.pwd
val service = ScriptService(dir)
val tweakflowFile = dir / "tweakflow.tf"
if (! os.isFile(tweakflowFile)) {
os.write(tweakflowFile, TweakFlow.default)
}
// now we're sure the file exists, set up a condition and run in a loop.
val condition = service.condition(tweakflowFile)
while (true) {
try {
logger.debug(condition, "{}", fb => fb.keyValue("foo" -> "bar"));
} finally {
Thread.sleep(2000L);
}
}
}
JBang
And now JBang. Be sure you have something in JAVA_HOME
or else it will automatically download and install a JDK itself. I usually point it at JDK 17 using the jbang jdk feature:
jbang jdk install 17 `sdk home java 17.0.4.1-tem`
Here's the script: this works on a JDK 13 or above JVM because it uses text blocks:
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS com.tersesystems.echopraxia:logger:2.2.4
//DEPS com.tersesystems.echopraxia:logstash:2.2.4
//DEPS com.tersesystems.echopraxia:scripting:2.2.4
//DEPS com.tersesystems.logback:logback-classic:1.2.0
import com.tersesystems.echopraxia.*;
import com.tersesystems.echopraxia.api.*;
import com.tersesystems.echopraxia.scripting.*;
import com.tersesystems.logback.classic.ChangeLogLevel;
import java.nio.*;
import java.nio.file.*;
public class Script {
private static final Logger<?> logger = LoggerFactory.getLogger(Script.class);
private static final String defaultScript = """
import * as std from "std";
alias std.strings as str;
library echopraxia {
function evaluate: (string level, dict ctx) ->
let {
find_string: ctx[:find_string];
}
str.lower_case(find_string("$.foo")) == "bar";
}
""";
public static void main(String... args) throws java.io.IOException {
ChangeLogLevel changer = new ChangeLogLevel();
changer.changeLogLevel("ROOT", "INFO");
changer.changeLogLevel(logger.getName(), "DEBUG");
Path watchedDir = Paths.get(".");
ScriptWatchService watchService = new ScriptWatchService(watchedDir);
Path filePath = watchedDir.resolve("tweakflow.tf");
if (! Files.exists(filePath)) {
Files.writeString(filePath, defaultScript);
}
ScriptHandle watchedHandle = watchService.watchScript(filePath, e -> logger.error("Script compilation error", e));
Condition condition = ScriptCondition.create(watchedHandle);
logger.info(condition, "{}", fb -> fb.string("foo", "BAR"));
}
}
And that's it!