Skip to content
Kavachकवच

Logging

● Intermediate5 minBefore this: RecordsSpec 14.12

A log event is a record you define, not a string. log.info, warn, error and debug write it, and the program's owner decides where it goes.

use std.log

type OrderPlaced: LogEvent {
    order_id: Int
    amount: Decimal
    secret card: String
}

fn main() {
    log.info(OrderPlaced(order_id: 7, amount: 499.00, card: "4242 4242 4242 4242"))
}

The program ran and printed:

kavach run log.kav

INFO OrderPlaced(order_id: 7, amount: 499.00, card: <secret>)
log.kav: Kavach code, then what running it prints
  • type OrderPlaced: LogEvent makes the record an event. Only events can be logged: log.info("order placed") is a compile error (below).
  • Each event is one line: the level, then the event as it prints. Every entry of a kind has the same fields, so logs can be searched and counted.
  • A secret field is written as <secret>.

Logging plain text out of habit is refused, and the message shows the record to declare:

use std.log

fn main() {
    log.info("order placed")
}

The compiler refused the program:

kavach run log-text.kav

error: `String` does not implement `LogEvent`: log events are records, so a log line can be searched by its fields. Declare one, as `type OrderPlaced: LogEvent { order_id: Int }`, and log that: `log.info(OrderPlaced(order_id: 7))` (spec 14.12)
log-text.kav: Kavach code, then what running it prints

Levels

FunctionLevelFor
log.debug(event)debugDetails for finding a problem
log.info(event)infoWhat the program did
log.warn(event)warnSomething unexpected that the program handled
log.error(event)errorSomething that failed

Events below the lowest level written are left out. kavach.toml sets it; here only warnings and errors are written:

[package]
name = "shop"
version = "0.1.0"

[log]
level = "warn"
kavach.toml: TOML code
use std.log

type Started: LogEvent {
    port: Int
}

type Retrying: LogEvent {
    attempt: Int
}

type DiskFull: LogEvent {
    free_mb: Int
}

fn main() {
    log.debug(Started(port: 8080))
    log.info(Started(port: 8080))
    log.warn(Retrying(attempt: 2))
    log.error(DiskFull(free_mb: 12))
}

The program ran and printed:

kavach run

WARN Retrying(attempt: 2)ERROR DiskFull(free_mb: 12)
src/main.kav: Kavach code, then what running it prints

The same settings send lines to a file, which starts a new one at a size you choose, or to an OpenTelemetry collector. See Settings.

No capability needed

use std.log

type Refunded: LogEvent {
    order_id: Int
}

pub fn refund(order_id: Int) -> Int {
    log.info(Refunded(order_id: order_id))
    order_id
}

fn main() {
    print(refund(42))
}

The program ran and printed:

kavach run no-uses.kav

INFO Refunded(order_id: 42)42
no-uses.kav: Kavach code, then what running it prints

refund is pub and logs, yet has no uses: where events go is the owner's choice, not the code's, and the code can't read them back.

Traces

A trace follows one piece of work, such as a request, through the program and the services it calls. It is made of spans: log.span(name) { … } runs its block as one span, timed from its start to its end. The block is a plain block: see Blocks Given to a Call.

use std.log

fn main() ! Error {
    print(log.trace_id() ?? "no trace outside a span")
    log.span("request") {
        const id = log.trace_id() ?? ""
        print("trace id: {id.len()} hex digits")
        const header = log.traceparent() ?? ""
        print("traceparent: {header.len()} characters, starts {header.slice(0, 3)}")
        const task = spawn log.trace_id()
        print("a task keeps it: {(try task.wait()) == id}")
    }
    print(log.trace_id() ?? "no trace after it")

    const continued = log.span("next", from: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") {
        log.trace_id() ?? ""
    }
    print(continued)
}

The program ran and printed:

kavach run span.kav

no trace outside a spantrace id: 32 hex digitstraceparent: 55 characters, starts 00-a task keeps it: trueno trace after it4bf92f3577b34da6a3ce929d0e0e4736
span.kav: Kavach code, then what running it prints
  • log.trace_id() is the trace's id, 32 hexadecimal digits, or none outside a span: for an error page or a header in a reply.
  • log.traceparent() is the span's W3C traceparent header, and from: continues the trace one names. Together they carry a trace over any connection: a queue, gRPC metadata, a file.
  • A span inside another is its child. A task keeps the trace of the code that spawned it.
  • Each event written inside a span carries the span's trace and span ids. A span whose block fails is marked as an error.
  • HTTP joins traces with no code: http.serve runs each handler in a span that continues the caller's trace, and http.request sends it. The span is named by the method (GET), never the path, which can hold ids and tokens. See Serving.
  • log.name_span(name) renames the current span, once the code knows what it is doing; outside a span it does nothing. Give it one of a few names, such as a route's pattern ("GET /orders/\{id}"), not text with ids in it.
  • With [log.otlp] set, each span goes to the collector when it ends. Without it, spans are sent nowhere.

Metrics

Numbers a program counts or measures go to the collector every 10 seconds as OpenTelemetry metrics, when [log.otlp] is set. Without it, they are kept nowhere and cost almost nothing:

log.count("orders_placed", 1)
log.histogram("checkout_ms", Float(elapsed))
from spec 14.12: Kavach code
FunctionSends
log.count(name, n)A sum: n added to the counter name. n is 0 or more (a require)
log.gauge(name, value)The latest value of name, such as a queue's length
log.histogram(name, value)How the values of name spread, such as each request's time in milliseconds: how many, their sum, the smallest, the largest, and how many fall in each bucket (OpenTelemetry's default bounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000)

Each also takes attributes, a [String: String] map that is empty unless given. Each name and set of attributes is a series of its own, whatever order the attributes are written in:

use std.log

fn main() {
    const ms = 12.5
    log.histogram("checkout.duration_ms", ms, attributes: [
        "http.route": "/orders/\{id}",
        "http.response.status_code": "200",
    ])
    log.count("orders_paid", 1, attributes: ["method": "card"])  // one series
    log.count("orders_paid", 1)                                  // and another
    print("recorded: sent only when [log.otlp] is set")
}

The program ran and printed:

kavach run metric-attributes.kav

recorded: sent only when [log.otlp] is set
metric-attributes.kav: Kavach code, then what running it prints

Give attributes with few values, such as a route's pattern, a method or a status, never ids: each new value makes a new series to keep and send. Like events, spans and metrics need no capability.

Limits and sizes

WhatLimit or sizeSource
A log filea new file after 10 MB (max_bytes); 5 older ones kept (keep)12-log.md
Events waiting for an OpenTelemetry collectorup to 10,000, sent at least once a second; at exit, up to 2 seconds to send the rest12-log.md

Every limit in Kavach, on one page: Limits and Sizes.