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!