Saikuro provides a structured logging system that lets adapters forward log records to the runtime instead of writing directly to stderr. The runtime collects records from all connected adapters and routes them to a configurable log sink.

Architecture

When an adapter calls client.log(...), it constructs a LogRecord and wraps it in a log-type envelope. The runtime intercepts log envelopes before they reach any provider and dispatches them to the configured sink.

flowchart LR A["Adapter<br/>client.log()"] -->|"log envelope"| B["Runtime<br/>router"] B -->|"LogRecord"| C["LogSink"] B -.->|never routed| D["Provider"]

LogRecord

Each log record carries a timestamp, severity level, origin name, message, and optional structured fields:

Field Type Description
ts string ISO-8601 timestamp
level string trace, debug, info, warn, error
name string Logger name / origin (e.g. "myapp.handler")
msg string Human-readable message
fields object Optional structured context (key-value pairs)

Log Levels

Ordered from least to most severe:

Level Usage
trace Very detailed diagnostic information
debug General debugging information
info Normal operational messages
warn Unexpected but handled situations
error Failure conditions that should be investigated

Adapter API

All six adapters provide a log() method on the client:

// TypeScript
await client.log("info", "myapp", "started", { version: "1.0" });
# Python
await client.log("info", "myapp", "started", {"version": "1.0"})
// Rust
client.log(LogLevel::Info, "myapp", "started", Some(&json!({"version": "1.0"}))).await?;
// C#
await client.LogAsync("info", "myapp", "started", new { version = "1.0" });
// C
saikuro_client_log(client, "info", "myapp", "started", "{\"version\": \"1.0\"}");
// C++
client.log("info", "myapp", "started", "{\"version\": \"1.0\"}");

Automatic Log Forwarding (TypeScript)

The TypeScript adapter provides helpers to forward all log calls from your logger to the Saikuro runtime:

import { createLoggingHandler, wrapLogger } from "@nisoku/saikuro/logging_handler";
import { SaikuroClient } from "@nisoku/saikuro";

const client = await SaikuroClient.connect("unix:///tmp/saikuro.sock");

// Create a forwarding handler
const handler = createLoggingHandler(client.transport);
handler("info", "myapp", "started", { version: "1.0" });

// Wrap an existing logger (console, pino, etc.)
const wrapped = wrapLogger(client.transport, console);
wrapped.info("hello"); // forwards to Saikuro AND prints to console

You can also forward all internal adapter logs to the runtime:

import { setLogSink, createTransportSink } from "@nisoku/saikuro";

const client = await SaikuroClient.connect("unix:///tmp/saikuro.sock");
setLogSink(createTransportSink(client));

Runtime Configuration

Log Level

Control the minimum log level via the SAIKURO_LOG environment variable or the --log-level flag:

SAIKURO_LOG=debug saikuro-runtime
saikuro-runtime --log-level debug

JSON Output

For log aggregation tools, emit logs as newline-delimited JSON:

saikuro-runtime --json-logs

Custom Log Sink

The runtime uses a tracing-backed log sink by default. Rust applications can provide a custom sink:

use saikuro_router::tracing_log_sink;
use saikuro_core::log::{LogRecord, LogSink};

let sink: LogSink = Box::new(|record: LogRecord| {
    // Forward to your logging system
});

Log Envelope

Log records are transmitted as standard Saikuro envelopes:

{
  "version": 1,
  "type": "log",
  "id": "log-2026-01-01T00:00:00.000Z",
  "target": "$log",
  "args": [{
    "ts": "2026-01-01T00:00:00.000Z",
    "level": "info",
    "name": "myapp",
    "msg": "started",
    "fields": { "version": "1.0" }
  }]
}

Log envelopes fire-and-forget. The runtime never sends a response.

Next Steps