Sonny Labs Docs
SDKs

Observability (OpenTelemetry)

Send Sonny Labs SDK traces and metrics to your existing OpenTelemetry-compatible observability stack — Datadog, Honeycomb, Grafana Tempo, New Relic — with zero Sonny Labs-specific configuration.

The Sonny Labs SDKs emit OpenTelemetry traces and metrics out of the box. If your application is already configured for OpenTelemetry, traces and metrics for every Sonny Labs call flow into your existing collector and exporter automatically — no Sonny Labs-specific configuration required.

If your application is not configured for OpenTelemetry, the SDK emits nothing and runs at the same speed it always has. The default OpenTelemetry tracer / meter are no-ops.

What you get

For every API call, the SDK opens a span and (for scans) emits metric points:

┌─ your.request.handler                       (your span)

└─┬─ mcp.tool.sonny_scan                      (when using @sonnylabs/mcp)

  └── sonnylabs.api POST /v1/scans            (CLIENT span — outgoing HTTP)

Each span carries the verdict, scan id, surface, finding count, and max severity — enough to slice your trace view by "what blocked this request" without reading the full Sonny Labs payload.

Every outgoing request also carries the W3C traceparent header so your trace tooling can stitch the call to whatever follows.

Quick start — TypeScript

Install the OpenTelemetry SDK + OTLP exporters for both traces and metrics alongside @sonnylabs/sdk:

npm install @opentelemetry/sdk-node \
  @opentelemetry/sdk-metrics \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http

Initialise OpenTelemetry before importing the Sonny Labs SDK, typically as the first thing your process does. Wire both a traceExporter and a metricReaderNodeSDK does not start the metrics pipeline if you only pass traceExporter, so without the reader you'll see spans but no sonnylabs.scan.* metrics:

// telemetry.ts — import this from the very top of your entry point.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";

// Both exporters read OTEL_EXPORTER_OTLP_ENDPOINT (and the standard
// companion env vars) automatically — no need to pass `url`.
new NodeSDK({
  traceExporter: new OTLPTraceExporter(),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
  }),
  instrumentations: [getNodeAutoInstrumentations()],
}).start();

Then use @sonnylabs/sdk exactly as you did before — no changes:

import "./telemetry"; // FIRST.
import { SonnyLabsClient } from "@sonnylabs/sdk";

const sonny = new SonnyLabsClient({ apiKey: process.env.SONNYLABS_API_KEY! });
await sonny.createContentScan({
  surface: "user_message",
  content: { type: "text", text: prompt },
});
// → emits `sonnylabs.api POST /v1/scans` span to your collector

Quick start — Python

Install the SDK with the OpenTelemetry SDK + an OTLP exporter:

pip install sonnylabs-sdk \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp \
  opentelemetry-distro

Initialise OpenTelemetry via the standard opentelemetry-instrument wrapper:

opentelemetry-instrument \
  --traces_exporter otlp \
  --metrics_exporter otlp \
  --exporter_otlp_endpoint $OTEL_EXPORTER_OTLP_ENDPOINT \
  python app.py

Then use sonnylabs-sdk exactly as you did before — no changes:

from sonnylabs import SonnyLabsClient

client = SonnyLabsClient(api_key=os.environ["SONNYLABS_API_KEY"])
client.create_scan(
    surface="user_message",
    content={"type": "text", "text": prompt},
)
# → emits `sonnylabs.api POST /v1/scans` span to your collector

Span attribute reference

Every Sonny Labs SDK span carries the OpenTelemetry HTTP semantic attributes (http.request.method, url.template, etc.) plus these Sonny Labs-specific attributes:

AttributeTypeExampleEmitted on
sonnylabs.sdk.versionstring0.3.0every span
sonnylabs.request_idstringreq_01H…every response
sonnylabs.retry_countint2requests that retried
sonnylabs.error.codestringauth.api_key.expiredfailed requests
sonnylabs.degradedbooltruefail-open path
sonnylabs.scan.idstringscan_01HSCAN…successful scan
sonnylabs.scan.kindstringcontentsuccessful scan
sonnylabs.scan.surfacestringuser_messagesuccessful scan
sonnylabs.scan.decision.actionstringblockedsuccessful scan
sonnylabs.scan.decision.reasonstringrule_matchsuccessful scan
sonnylabs.scan.findings.countint3successful scan
sonnylabs.scan.findings.max_severitystringhighsuccessful scan

Path templates collapse concrete IDs (e.g. /v1/scans/scan_01HSCAN… becomes /v1/scans/{id}) on both url.template and the span name, so each unique path produces one bucket in your dashboards instead of one per scan.

Metric reference

MetricTypeUnitLabelsMeaning
sonnylabs.scan.durationhistogrammsaction, surfaceEnd-to-end latency of a POST /v1/scans call.
sonnylabs.scan.decisionscounteraction, reasonOne increment per scan outcome.
sonnylabs.scan.degradedcounterreason, codeScans that returned the fail-open synthetic.
sonnylabs.request.errorscountercode, statusFailed API calls (after retries).
sonnylabs.request.retriescounterstatusRetries actually performed.

sonnylabs.scan.degraded is the alerting signal for "Sonny Labs is unreachable and we are letting traffic through unscanned." If your application uses fail-open, watch this counter.

Span events

Inside a scan span you may also see:

  • retry — the SDK retried the call after a 429 / 503; attributes include attempt, wait_ms, status.
  • fail_open_degraded — recorded on the parent span (your request handler's span) when a scan fell open and returned the synthetic allow. Attributes: reason, code, status.

Sending data to your backend

The OpenTelemetry exporters are entirely on your side — the Sonny Labs SDK never sees them. Any backend that accepts the OTLP protocol is supported.

Grafana Tempo / Mimir

Point OTEL_EXPORTER_OTLP_ENDPOINT at your OTel Collector (or directly at Tempo's OTLP HTTP receiver), and Sonny Labs spans land in Tempo alongside the rest of your trace data.

Honeycomb

Honeycomb accepts OTLP HTTP directly. Set:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io"
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_INGEST_KEY"

Datadog

Run the Datadog OpenTelemetry Collector in front of your application, point the SDK's exporter at the collector, and Sonny Labs spans flow through to Datadog APM.

New Relic

New Relic accepts OTLP HTTP directly. Set:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp.nr-data.net:4318"
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_LICENSE_KEY"

Using @sonnylabs/mcp

@sonnylabs/mcp runs as a stdio process spawned by your agentic host (Claude Desktop, Cursor, Claude Code). The host doesn't typically have an OpenTelemetry pipeline of its own, so the MCP process needs to ship traces itself.

Run a local OpenTelemetry Collector on the operator's machine and point the MCP process at it via environment variables in your host's MCP config:

{
  "mcpServers": {
    "sonnylabs": {
      "command": "npx",
      "args": ["-y", "@sonnylabs/mcp"],
      "env": {
        "SONNYLABS_API_KEY": "sk_live_…",
        "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318",
      },
    },
  },
}

You'll see two spans per scan: an outer mcp.tool.sonny_scan (SERVER kind) and an inner sonnylabs.api POST /v1/scans (CLIENT kind). The MCP layer adds the tool name as mcp.tool.name so you can group by tool in your trace view.

Privacy default: the MCP server does not record tool arguments as span attributes by default — your host may route arbitrary user content through tool calls, including PII or secrets. To opt in for local debugging only:

SONNY_MCP_OTEL_RECORD_TOOL_ARGS=1

Self-hosted

The instrumentation is entirely client-side. If you run Sonny Labs self-hosted in your own VPC and you already operate an OpenTelemetry Collector inside the cluster, no extra configuration is needed — set OTEL_EXPORTER_OTLP_ENDPOINT on the application that uses the SDK and traces flow to your collector like any other workload's.

Redaction guarantees

The SDK is designed so scanned content never leaks into your trace store, even as a side channel:

  • Span attributes carry only counts (sonnylabs.scan.findings.count) and labels (sonnylabs.scan.findings.max_severity) — never the finding contents themselves.
  • PII / sensitive items inside findings[].items[] are never read by the instrumentation.
  • API keys and bearer tokens are never recorded as headers or attributes.
  • The scanned text (content.text) is never recorded anywhere.

If your trace exporter samples or stores traces, you can rely on the spans containing only metadata about the scan, not the scanned content.

What is not yet supported

  • Backend trace continuity. Today the sonnylabs.api CLIENT span ends at the network boundary; the Sonny Labs server does not yet emit a continuing SERVER span. The SDK sends the W3C traceparent header on every request so future server-side ingestion will extend traces end-to-end without any client changes. Subscribe to release notes for when this lands.
  • Auto-instrumentation distro packages. No framework-specific wrappers yet (FastAPI middleware, Express middleware). The standard OpenTelemetry auto-instrumentations cover the surrounding HTTP layer; Sonny Labs spans nest naturally inside them.

Troubleshooting

Traces showing up empty / no sonnylabs.* attributes. You initialised the OpenTelemetry SDK after importing @sonnylabs/sdk. Move the OpenTelemetry setup to the top of your entry point.

Spans appearing but no traceparent header on outgoing requests. In Node, the default context manager doesn't propagate across await boundaries unless an async-hooks context manager is registered. @opentelemetry/sdk-node and the standard Python opentelemetry-distro both register the async-hooks / context-vars manager automatically; if you're hand-rolling a provider, register AsyncHooksContextManager (JS) or rely on the default OTel context-vars manager (Python).

MCP server traces missing. Confirm OTEL_EXPORTER_OTLP_ENDPOINT is set inside the env block of the host's MCP config, not in your shell environment — most hosts launch the MCP server in a clean environment.

For anything else, email support@sonnylabs.ai.

On this page