Koka's effects: modify the log level of an HTTP handler at runtime

Date: 2022-04-24

Sometimes, when I'm debugging an HTTP handler, I wish I could set the log level to DEBUG for just one call to the handler.

This would solve some problems:

  1. The application doesn't have to be restarted to change the log level. Changing arguments and restarting can be challenging in production environments or when the software is deployed in a user-controlled environment.
  2. I won't be flooded with DEBUG-level logs from parts of the application I probably don't care about

With the Koka language's effects, this seems possible. I'm new to the language and certainly won't be doing all of its merits justice, but let's see what changing the log level for a call stack/context (like an HTTP handler) at runtime might look like!

The code

First, we'll define our effect, log, which takes a level and a string and does… something.

effect fun log( level : int, msg : string ) : ()

Let's write some mock business logic for our "HTTP handler": some calls to log. This highlights Koka's effect system. The logic calls an effect, log, whose handler hasn't been defined yet — all it knows is the effect's type signature.

fun http-handler-logic( foo : string , bar : string )
  log(2, "FOO:" ++ foo)
  log(3, "BAR:" ++ bar)

Next, we'll create a function that sets up an effect handler for log: log-leveled, which has different behavior depending on the log level. If the log level is within the target level's range, print the message. Otherwise, do nothing.

fun level-to-string( level : int ) : string
  match level
    0 -> "ERROR"
    1 -> "WARNING"
    2 -> "INFO"
    3 -> "DEBUG"
    _ -> "UNKNOWN LEVEL"

fun log-leveled( target-level : int, action )
  with fun log( level, msg ) {
    if level <= target-level then
      println(level-to-string(level) ++ ":" ++ msg)


Finally, our "HTTP handler." We'll set up the handler for log in this context by using log-leveled: depending on the value of verbose, calls to log will be set up to log at either the DEBUG level or the INFO level. A simple block of code, yet a fundamental difference from the global logger situation I'm so used to!

fun http-handler( verbose : bool )
  var log-level := 2
  if verbose then {
    log-level := 3
  with log-leveled(log-level)

  return http-handler-logic("testfoo", "testbar")

All we need is a main() to tie it all together. Please forgive the <<>> blocks, they're just tags to pull the earlier code in.


fun main()
  println("++Simulating a handler call with verbose=false")


  println("++Simulating a handler call with verbose=true")

After koka -e logtest.kk:

++Simulating a handler call with verbose=false

++Simulating a handler call with verbose=true

Just like that, we've changed the log level for a call stack, dynamically, at run time!


I'm fairly certain this technique can be generalized to a proper library for logging if given the proper care. Just think: using the effect system at startup time to set the log destination (stdout, stderr, a file) and default level, then using flags like our verbose to make an update for a given call! Awesome stuff.

I'm uncertain what the performance overheads might be. An idea for another day, perhaps.

On Koka

Koka is an interesting language. I'm new to the concept of effects, so the documentation is slow reading and the code often takes a few tries to wrap my head around. This mini-project happened because I was trying to explain what effects would actually be useful for to someone; I'm not familiar with any canonical use cases for effects, so I came up with this contrived one.

The documentation could do with some improvement. For example, the "Basics" section skips past fundamental language constructs (like if) and focuses instead on some of Koka's more unique features. It'd help to start with more "basics" first.