Add telemetry
This guide wires OpenTelemetry into an llmkit client so every call — success
and rejection alike — emits one OTEL GenAI span. The shape is identical across
the four SDKs: attach a Telemetry config whose export callback receives the
finished OTLP/JSON bytes. llmkit builds the span; you decide where the bytes go.
Telemetry is off unless you attach it, and a config with no export is a
ValidationError — there is no silent enabled-but-no-sink state.
Option A — point at a collector (batteries)
The HTTPExport / httpExport / http_export convenience returns an export
callback that POSTs each span to <endpoint>/v1/traces. Use it for low volume:
the POST is synchronous and fail-open (a hung collector never fails your call,
but it can add latency up to the timeout).
import "github.com/aktagon/llmkit-go"
c := llmkit.New("openai", os.Getenv("OPENAI_API_KEY")).
AddTelemetry(llmkit.Telemetry{
Export: llmkit.HTTPExport("https://collector:4318", nil),
})
resp, err := c.Text.Prompt(context.Background(), "Hello")
import { openai, httpExport } from "@aktagon/llmkit-ts";
const client = openai(process.env.OPENAI_API_KEY).addTelemetry({
export: httpExport("https://collector:4318"),
});
const resp = await client.text.prompt("Hello");
from llmkit import Telemetry, http_export, add_telemetry
from llmkit.builders import openai
client = add_telemetry(
openai(os.environ["OPENAI_API_KEY"]),
Telemetry(export=http_export("https://collector:4318")),
)
resp = await client.text.prompt("Hello")
use std::collections::HashMap;
use llmkit::builders::openai;
use llmkit::{http_export, Telemetry};
let client = openai(&key).add_telemetry(Telemetry {
export: http_export("https://collector:4318", HashMap::new()),
capture_content: false,
});
let resp = client.text().prompt("Hello").await?;
Option B — bring your own transport
If you already run an OpenTelemetry SDK, skip the built-in POST and hand the bytes straight to your batch processor. llmkit does no network I/O and spawns no worker — the callback runs synchronously on each call, so keep it non-blocking (enqueue and return).
c.AddTelemetry(llmkit.Telemetry{
Export: func(b []byte) { batchProcessor.Enqueue(b) },
})
client.addTelemetry({ export: (b) => batchProcessor.enqueue(b) });
add_telemetry(client, Telemetry(export=lambda b: batch_processor.enqueue(b)))
use std::sync::Arc;
client.add_telemetry(Telemetry {
export: Arc::new(|b| batch_processor.enqueue(b)),
capture_content: false,
});
What lands in your collector
Each span carries the OTEL GenAI attributes for the call:
gen_ai.operation.name— the operation (e.g.chat,generate_content)gen_ai.system— the provider (e.g.openai)gen_ai.request.model— the modelgen_ai.usage.input_tokens/gen_ai.usage.output_tokens— token countserror.type— present only on a rejection, with the span status set to error
The span shape is byte-identical across all four SDKs, so a shop running llmkit in more than one language reports into one collector with one set of dashboards and queries.
Next steps
- For high volume, always use Option B — the built-in POST is deliberately naive and has no batching or backpressure of its own.
- Content capture (prompt and completion text) is a separate, default-off tier;
it is gated by the
captureContentflag on theTelemetryconfig.