Docs / runtime / observability

Observability Exporters

Runtime observability exporters for Runra — Axiom, OpenTelemetry, JSONL, Postgres, and custom exporters. Event schema, configuration, and filtering

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.

Built-in Exporters

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

Axiom

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
});

Configuration

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

Example Axiom Query (APL)

['runra-events']
| where type == "exec.completed"
| summarize avg_duration = avg(data.durationMs) by bin(_time, 1m)

OpenTelemetry

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,
      },
    },
  },
});

Event-to-Span Mapping

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.

JSONL

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
    },
  },
});

Output Format

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}}

Analyzing JSONL Files

# 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"}'

Postgres

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,
    },
  },
});

Table Schema (Auto-Created)

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);

Example Queries

-- 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;

Console

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" },
  },
});

Multiple Exporters

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
        },
      },
    ],
  },
});

Event Filtering

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.*"],
}

Event Schema

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>;
}

Custom Exporter

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());

Next Steps