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-httpInitialise OpenTelemetry before importing the Sonny Labs SDK,
typically as the first thing your process does. Wire both a
traceExporter and a metricReader — NodeSDK 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 collectorQuick start — Python
Install the SDK with the OpenTelemetry SDK + an OTLP exporter:
pip install sonnylabs-sdk \
opentelemetry-sdk \
opentelemetry-exporter-otlp \
opentelemetry-distroInitialise OpenTelemetry via the standard
opentelemetry-instrument wrapper:
opentelemetry-instrument \
--traces_exporter otlp \
--metrics_exporter otlp \
--exporter_otlp_endpoint $OTEL_EXPORTER_OTLP_ENDPOINT \
python app.pyThen 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 collectorSpan 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:
| Attribute | Type | Example | Emitted on |
|---|---|---|---|
sonnylabs.sdk.version | string | 0.3.0 | every span |
sonnylabs.request_id | string | req_01H… | every response |
sonnylabs.retry_count | int | 2 | requests that retried |
sonnylabs.error.code | string | auth.api_key.expired | failed requests |
sonnylabs.degraded | bool | true | fail-open path |
sonnylabs.scan.id | string | scan_01HSCAN… | successful scan |
sonnylabs.scan.kind | string | content | successful scan |
sonnylabs.scan.surface | string | user_message | successful scan |
sonnylabs.scan.decision.action | string | blocked | successful scan |
sonnylabs.scan.decision.reason | string | rule_match | successful scan |
sonnylabs.scan.findings.count | int | 3 | successful scan |
sonnylabs.scan.findings.max_severity | string | high | successful 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
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
sonnylabs.scan.duration | histogram | ms | action, surface | End-to-end latency of a POST /v1/scans call. |
sonnylabs.scan.decisions | counter | action, reason | One increment per scan outcome. | |
sonnylabs.scan.degraded | counter | reason, code | Scans that returned the fail-open synthetic. | |
sonnylabs.request.errors | counter | code, status | Failed API calls (after retries). | |
sonnylabs.request.retries | counter | status | Retries 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 includeattempt,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=1Self-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.apiCLIENT span ends at the network boundary; the Sonny Labs server does not yet emit a continuing SERVER span. The SDK sends the W3Ctraceparentheader 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.
MCP server
Run @sonnylabs/mcp inside Claude Desktop, Cursor, or Claude Code. Lets your agent scan prompts, manage API keys, and scaffold SonnyLabs into your codebase — all without leaving the chat. Apache-2.0.
Webhooks
Verify Sonny Labs outbound webhook signatures (HMAC-SHA256 over {t}.{body}) using the helpers shipped in the Python and TypeScript SDKs.