Skip to content

API Concepts

Core Types

Scribe models logging with two event shapes:

  • Note: a single standalone event
  • SealedScroll: a sealed snapshot result of a multi-step Scroll

Both implement the sealed Entry interface, which is what EntrySaver receives.

Terminology

  • note(...): emits a single log entry through the active runtime
  • newScroll(...): starts a contextual logging session
  • seal(scribe, ...): applies the supplied runtime's footer, snapshots the current scroll data, and emits a SealedScroll
  • extend(scroll): copies missing keys from another scroll into this one
  • append(key, scroll): nests a scroll as a JSON object under the given key
  • Margin: hook for writing fields at open/close boundaries
  • hire(channel = ..., scope = ..., onSaver = ...): starts delivery over your channel configuration

Scribe

Scribe is an abstract runtime base class. A user creates one or more objects that extend it. Each object owns:

  • one or more savers (shelves)
  • an optional shared imprint
  • optional lifecycle hooks through Margin
  • optional uncaught exception wiring through onIgnition (the installed platform hook itself is global)

Define runtime configuration with overridden properties:

object CheckoutScribe : Scribe() {
    override val shelves: List<Saver<*>> = listOf(entrySaver)
    override val imprint = mapOf("service" to JsonPrimitive("checkout"))
    override val margins = timingMargin
}

Delivery is started with CheckoutScribe.hire(...) and stopped with CheckoutScribe.retire(). Different objects can run concurrently without sharing queues, savers, or lifecycle.

Scroll

Scroll is a typealias for MutableMap<String, JsonElement>. Calling newScroll(...) initializes it with the ID, imprint, and header margin from that Scribe, but the map does not retain a runtime reference. Supply the runtime that should apply its footer and deliver the snapshot to seal(...):

val scroll: Scroll = CheckoutScribe.newScroll(id = "checkout-42")
scroll["gateway"] = JsonPrimitive("stripe")
scroll.seal(CheckoutScribe) // applies/delivers through CheckoutScribe

It delegates normal mutable map operations, so you write JSON-safe values directly into it.

val scroll = CheckoutScribe.newScroll(id = "checkout-42")
scroll["gateway"] = JsonPrimitive("stripe")
scroll["attempt"] = JsonPrimitive(1)
scroll["retry"] = JsonPrimitive(false)

You can read/remove fields with normal map operations:

val phase = scroll["phase"]
val removed = scroll.remove("retryable")

scroll.id reads the generated/custom scroll_id field:

val scroll = CheckoutScribe.newScroll(id = "checkout-42")
println(scroll.id) // "checkout-42"

Calling seal(...) more than once is allowed. Each call emits a separate SealedScroll through the Scribe passed to that call, with the current success value and a snapshot of the data at that point.

Scroll Operations

Beyond direct map writes, Scroll has two convenience operations:

extend(scroll)

Copies only missing keys from another scroll into this one:

val base = CheckoutScribe.newScroll(id = "base")
base["gateway"] = JsonPrimitive("stripe")

val checkout = CheckoutScribe.newScroll(id = "checkout-42")
checkout["attempt"] = JsonPrimitive(1)
checkout.extend(base) // only copies "gateway" if not already present

append(key, scroll)

Nests another scroll as a JsonObject under the given key:

val meta = CheckoutScribe.newScroll(id = "cart-meta")
meta["item_count"] = JsonPrimitive(3)
checkout.append("cart", meta)
// Result: checkout["cart"] = {"item_count": 3}

Margin

Margin enriches a scroll at beginning and end.

val timingMargin = object : Margin {
    override fun header(scroll: Scroll) {
        scroll["started_at"] = JsonPrimitive(1000)
    }

    override fun footer(scroll: Scroll) {
        scroll["sealed_at"] = JsonPrimitive(2000)
    }
}

Delivery Configuration

Configure queue behavior through the Channel<Entry> passed to an instance's hire(...).

CheckoutScribe.hire(
    channel = Channel(
        capacity = 256,
        onBufferOverflow = BufferOverflow.DROP_OLDEST,
    ),
    onSaver = { saver, entry, error ->
        println("Saver $saver failed for $entry: $error")
    },
)

You can optionally provide a custom CoroutineScope to control the lifecycle of the delivery coroutine:

val customScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

CheckoutScribe.hire(
    scope = customScope,
    channel = Channel(capacity = 256),
)

Event Shapes

Note(
    tag = "payments",
    message = "starting checkout",
    level = Urgency.INFO,
    timestamp = 1710000000000L,
)
SealedScroll(
    success = true,
    data = mapOf(
        "scroll_id" to JsonPrimitive("checkout-42"),
        "gateway" to JsonPrimitive("stripe"),
    ),
)

Urgency Levels

Urgency is used by Note to indicate severity:

enum class Urgency {
    VERBOSE,
    DEBUG,
    INFO,
    WARN,
    ERROR
}

Failure Handling

object ApplicationScribe : Scribe() {
    override val shelves: List<Saver<*>> = listOf(entrySaver)
    override val onIgnition: ((Throwable) -> Unit)? = { throwable ->
        println("Uncaught exception: ${throwable.message}")
    }
}

ApplicationScribe.hire(
    channel = Channel(capacity = 256),
    onSaver = { saver, entry, error ->
        println("Saver $saver failed for $entry: ${error.message}")
    },
)

onIgnition is read when that runtime is first hired, but handles uncaught exceptions at the platform level. Multiple runtimes should not independently claim this application-global hook. Saver failures are reported by the onSaver callback passed to hire(...).