Skip to content

Observability Exporters

Runra Runtime emits structured events for every operation. Observability exporters send these events to your monitoring stack. Configure multiple exporters simultaneously for different use cases.

ExporterKeyBest For
AxiomaxiomReal-time structured logging and dashboards
OpenTelemetryotelStandard observability pipelines
JSONLjsonlLocal file-based logging
PostgrespostgresLong-term storage and SQL analytics
ConsoleconsoleDevelopment and debugging
CustomcustomBring your own destination

Send events to Axiom for real-time search, dashboards, and alerts:

import { Runra } from "@runra/runtime";
const runra = new Runra({
observability: {
provider: "axiom",
config: {
token: process.env.AXIOM_TOKEN,
dataset: "runra-events",
batchSize: 100,
flushIntervalMs: 5000,
},
},
// ... sandbox and agent config
});
OptionTypeDefaultDescription
tokenstringAXIOM_TOKEN envAxiom API or ingest token
datasetstring"runra-events"Target dataset name
urlstring"https://cloud.axiom.co"Axiom deployment URL
batchSizenumber100Max events per ingest request
flushIntervalMsnumber5000Max interval between flushes
onErrorfunctionError handler callback
['runra-events']
| where type == "exec.completed"
| summarize avg_duration = avg(data.durationMs) by bin(_time, 1m)

Export events as OpenTelemetry spans and logs to any OTLP-compatible backend (Jaeger, Grafana Tempo, Honeycomb, Datadog):

const runra = new Runra({
observability: {
provider: "otel",
config: {
endpoint: "https://otel-collector.internal:4318/v1/traces",
protocol: "http/protobuf", // or "grpc"
headers: {
"X-API-Key": process.env.OTEL_API_KEY,
},
serviceName: "runra-runtime",
serviceVersion: "1.2.0",
spanProcessor: "batch", // or "simple"
batchConfig: {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduleDelayMs: 5000,
},
},
},
});
Runtime EventOTel Span
sandbox.createdSpan: sandbox.create
exec.completedSpan: sandbox.execute
file.createdSpan: sandbox.file.write
file.readSpan: sandbox.file.read
agent.tool_callSpan: agent.tool_call
sandbox.pausedSpan: sandbox.pause
sandbox.terminatedSpan: sandbox.terminate

Each span includes attributes from the event’s data field and links to the parent runId.

Write events to newline-delimited JSON files. Great for local debugging, CI pipelines, and offline analysis:

const runra = new Runra({
observability: {
provider: "jsonl",
config: {
filePath: "./logs/runra-events.jsonl",
append: true, // Append to existing file
rotateSizeBytes: 100 * 1024 * 1024, // 100 MB file rotation
rotateFiles: 10, // Keep 10 rotated files
gzip: true, // Gzip rotated files
},
},
});

Each line is a JSON object:

{"id":"evt_abc123","timestamp":"2026-06-15T10:30:00.000Z","type":"sandbox.created","sandboxId":"sb_x7k2m9p4q1","runId":"run_a1b2c3","data":{"image":"node:22","cpu":2,"memoryMb":4096}}
{"id":"evt_def456","timestamp":"2026-06-15T10:30:05.000Z","type":"exec.completed","sandboxId":"sb_x7k2m9p4q1","runId":"run_a1b2c3","data":{"command":"node --version","exitCode":0,"durationMs":45}}
Terminal window
# Parse and filter with jq
cat logs/runra-events.jsonl | jq 'select(.type == "exec.completed")'
# Count events by type
cat logs/runra-events.jsonl | jq -r '.type' | sort | uniq -c
# Calculate average execution duration
cat logs/runra-events.jsonl \
| jq -r 'select(.type == "exec.completed") | .data.durationMs' \
| awk '{sum+=$1; count++} END {print sum/count "ms"}'

Store events in PostgreSQL for long-term analytics and SQL querying:

const runra = new Runra({
observability: {
provider: "postgres",
config: {
connectionString: process.env.DATABASE_URL,
table: "runra_events",
schema: "public",
batchSize: 50,
flushIntervalMs: 2000,
createTable: true, // Auto-create table
useJsonColumn: true, // Store data as JSONB
ssl: true,
},
},
});
CREATE TABLE IF NOT EXISTS runra_events (
id TEXT PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
type TEXT NOT NULL,
sandbox_id TEXT NOT NULL,
run_id TEXT NOT NULL,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_events_type ON runra_events (type);
CREATE INDEX idx_events_sandbox ON runra_events (sandbox_id);
CREATE INDEX idx_events_run ON runra_events (run_id);
CREATE INDEX idx_events_timestamp ON runra_events (timestamp DESC);
-- Average execution time by sandbox
SELECT
sandbox_id,
AVG((data->>'durationMs')::numeric) AS avg_duration_ms
FROM runra_events
WHERE type = 'exec.completed'
GROUP BY sandbox_id;
-- Command error rate
SELECT
data->>'command' AS command,
COUNT(*) AS executions,
SUM(CASE WHEN (data->>'exitCode')::int != 0 THEN 1 ELSE 0 END) AS failures
FROM runra_events
WHERE type = 'exec.completed'
GROUP BY data->>'command'
HAVING COUNT(*) >= 10
ORDER BY failures DESC;

Simple console output for development. Supports JSON and pretty-print modes:

// Pretty-print (default)
const runra = new Runra({
observability: {
provider: "console",
config: {
format: "pretty", // "pretty" or "json"
level: "all", // "all", "sandbox", "exec", "agent", "error"
includeTimestamp: true,
colorize: true,
},
},
});
// JSON output (parseable)
const runra = new Runra({
observability: {
provider: "console",
config: { format: "json" },
},
});

Use multiple exporters simultaneously for different needs:

const runra = new Runra({
observability: {
exporters: [
{
provider: "axiom",
config: { token: "...", dataset: "runra-events" },
filter: { types: ["*"] }, // Send everything
},
{
provider: "jsonl",
config: { filePath: "./logs/runra.jsonl" },
filter: { types: ["exec.*", "sandbox.*"] }, // Only sandbox and exec events
},
{
provider: "postgres",
config: { connectionString: "..." },
filter: { types: ["*"] },
},
{
provider: "console",
config: { format: "pretty", level: "error" },
filter: {
types: ["exec.completed"],
condition: (e) => e.data.exitCode !== 0, // Only failed commands
},
},
],
},
});

Each exporter can filter which events it receives:

interface ExportFilter {
/** Event type patterns to include (supports "*" wildcard) */
types?: string[];
/** Custom condition function */
condition?: (event: RuntimeEvent) => boolean;
/** Minimum level */
minLevel?: "debug" | "info" | "warn" | "error";
}

Examples:

// Only send errors to a specific destination
filter: {
types: ["exec.completed"],
condition: (e) => e.data.exitCode !== 0,
}
// Send everything except file operations
filter: {
types: ["*"],
condition: (e) => !e.type.startsWith("file."),
}
// Only sandbox lifecycle events
filter: {
types: ["sandbox.*"],
}
interface RuntimeEvent {
/** Unique event ID (UUID v4) */
id: string;
/** ISO 8601 timestamp */
timestamp: string;
/** Event type discriminator */
type:
| "sandbox.created"
| "sandbox.started"
| "sandbox.paused"
| "sandbox.resumed"
| "sandbox.terminated"
| "sandbox.error"
| "exec.started"
| "exec.completed"
| "exec.streamed"
| "exec.error"
| "file.created"
| "file.read"
| "file.deleted"
| "file.error"
| "port.exposed"
| "port.closed"
| "agent.thinking"
| "agent.tool_call"
| "agent.tool_result"
| "agent.error"
| "cost.runtime"
| "cost.llm";
/** Sandbox context */
sandboxId: string;
/** Agent run context */
runId: string;
/** Type-specific payload */
data: Record<string, unknown>;
}

Implement the ObservabilityExporter interface:

import type { ObservabilityExporter, RuntimeEvent } from "@runra/runtime";
class MyCustomExporter implements ObservabilityExporter {
readonly id = "my-exporter";
private buffer: RuntimeEvent[] = [];
private endpoint: string;
async initialize(config: Record<string, unknown>): Promise<void> {
this.endpoint = config.endpoint as string;
}
async export(events: RuntimeEvent[]): Promise<void> {
// Send events to your system
const response = await fetch(this.endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ events }),
});
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
}
async flush(): Promise<void> {
// Flush any buffered events
}
async dispose(): Promise<void> {
// Cleanup
}
}
// Register and use
Runra.registerObservabilityExporter("my-exporter", () => new MyCustomExporter());