Logging

Geblang's log module emits structured JSON log entries to stdout, stderr, files, network syslog, or custom handlers. A logger is a runtime handle returned by a destination constructor (log.stdout, log.stderr, log.file, log.toStream, log.syslog, or log.custom).

import log;

let logger = log.stdout();
log.info(logger, "server started", {"port": 8080});

Built-In Logger Destinations

Function Returns Description
stdout() logger handle Write JSON log lines to stdout
stderr() logger handle Write JSON log lines to stderr
file(path) logger handle Append JSON log lines to a file
toStream(stream) logger handle Write JSON log lines to any streams.IOStream (memory buffer, TCP socket, pipe, ...)
syslog(opts) logger handle Send RFC 5424 syslog records over UDP/TCP or to the local daemon (see Syslog)
close(logger) void Close and unregister a logger

toStream leaves the underlying stream alone on log.close, so the same stream can back multiple loggers or stay open for non-log traffic. Pair it with streams.memory() for in-process capture in tests, or with a network stream to ship logs to a TCP/TLS log collector.

import streams;
let buf = streams.memory();
let capture = log.toStream(buf);
log.info(capture, "captured", {"k": "v"});
log.close(capture);
io.println(buf.toString());     # the JSON line(s) just written
let out = log.stdout();
let err = log.stderr();
let file = log.file("/var/log/app.log");
defer log.close(file);

log.info(out, "ready");
log.warn(err, "using fallback config", {"path": "config/local.yaml"});
log.error(file, "request failed", {"status": 500, "path": "/api/users"});

Built-in loggers emit one JSON object per line. The exact field order is not part of the API, but entries include at least:

Field Type Description
level string info, warn, error, or debug
message string Log message
fields dict Structured fields passed by the caller
time string Timestamp as an RFC 3339 nanosecond string

Levels

Function Description
info(logger, message) Informational event
info(logger, message, fields) Informational event with fields
warn(logger, message) Warning event
warn(logger, message, fields) Warning event with fields
error(logger, message) Error event
error(logger, message, fields) Error event with fields
debug(logger, message) Debug event
debug(logger, message, fields) Debug event with fields

fields must be a dictionary. Keep fields machine-readable: prefer ids, status codes, durations, paths, and booleans over preformatted message text.

log.info(logger, "user login", {
    "userId": "42",
    "ip": request["remoteAddr"],
    "remember": true
});

log.LogInterface

Custom handlers can implement the exported log.LogInterface. Its required method has this shape:

func handle(string level, string message, dict<string, any> fields): void;

Use it when a class should be accepted by logging-aware code or when you want compile-time checking of the handler shape.

import io;
import json;
import log;

class JsonSink implements log.LogInterface {
    func handle(string level, string message, dict<string, any> fields): void {
        io.println(json.stringify({
            "level": level,
            "message": message,
            "fields": fields
        }));
    }
}

let logger = log.custom(JsonSink());
log.info(logger, "custom logger ready", {"handler": "json"});

log.custom(handler) still checks structurally for a compatible handle method, but implementing log.LogInterface makes the contract explicit:

class MemoryLogger implements log.LogInterface {
    list<dict<string, any>> entries = [];

    func handle(string level, string message, dict<string, any> fields): void {
        this.entries = this.entries.push({
            "level": level,
            "message": message,
            "fields": fields
        });
    }
}

let sink = MemoryLogger();
let logger = log.custom(sink);

log.error(logger, "validation failed", {"field": "email"});
io.println(sink.entries[0]["message"]);

Custom handlers are useful for:

  • writing to external services;
  • forwarding logs to test assertions;
  • adapting logs into framework-specific event systems;
  • redacting or transforming fields before output.

Logger Lifecycle

Call log.close(logger) when a logger is no longer needed. File loggers close their underlying file handle. Stdout/stderr/custom loggers unregister the runtime handle.

let file = log.file("app.log");
defer log.close(file);

Do not write to a logger after closing it; the runtime will report an unknown logger handle.

Logging Patterns

Create a request logger:

func logRequest(any logger, dict<string, any> req, int status, int durationMs): void {
    log.info(logger, "http request", {
        "method": req["method"],
        "path": req["path"],
        "status": status,
        "durationMs": durationMs
    });
}

Capture logs in tests:

class Capture implements log.LogInterface {
    list<dict<string, any>> entries = [];

    func handle(string level, string message, dict<string, any> fields): void {
        this.entries = this.entries.push({
            "level": level,
            "message": message,
            "fields": fields
        });
    }
}

let capture = Capture();
let logger = log.custom(capture);
log.warn(logger, "rate limit", {"limit": 100});

io.println(capture.entries.length());

Syslog

log.syslog(opts) sends entries to a syslog server or the local syslog daemon, framed as RFC 5424. It is a logger destination like the others, so the same log.info / log.warn / log.error / log.debug calls apply.

import log;

let logger = log.syslog({
    "network":  "udp",
    "address":  "logs.example.com:514",
    "facility": "local0",
    "app":      "checkout"
});
log.info(logger, "order placed", {"orderId": "A123"});
log.close(logger);

Options

Key Default Description
network "udp" "udp", "tcp", or "local" (the platform's syslog daemon socket).
address required for udp/tcp host:port of the syslog server. Ignored for "local".
facility "user" Syslog facility name: kern, user, daemon, auth, syslog, cron, local0-local7, and others.
app executable name RFC 5424 APP-NAME.
hostname OS hostname RFC 5424 HOSTNAME.

Cross-platform behaviour

"udp" and "tcp" work on Linux, macOS/BSD, and Windows. "local" uses the local daemon socket (/dev/log on Linux, /var/run/syslog on macOS/BSD) and is Unix-only; on Windows it raises an error, because Windows has no syslog daemon - send to a collector with "udp" or "tcp" instead.

Message format

Each record is an RFC 5424 line:

<PRI>1 TIMESTAMP HOSTNAME APP-NAME PROCID - - MSG

PRI is facility * 8 + severity, where the level maps to a severity: debug -> 7, info -> 6, warn -> 4 (warning), error -> 3 (err). MSG is the same JSON object the other destinations emit (level, message, fields, time), so a syslog pipeline and a file pipeline carry identical structured data.

log.syslog connects when constructed, so an unreachable host or a malformed address fails immediately. Once connected, a transient send failure is dropped rather than raised, so logging never crashes a running program.

See also Observability for metrics, tracing, and profiling.