Skip to content

OpenTelemetry GenAI semantic conventions

The OpenTelemetry GenAI semantic conventions are a vendor-neutral vocabulary for model telemetry: what a span for a model call is named, which attributes carry the model and provider, and which metrics report token usage and latency. Genkit can emit telemetry in that shape, so any backend that speaks OTLP receives data it already understands, with no Genkit-specific mapping on your side.

This is separate from the Developer UI pipeline. The Developer UI is fed by a built-in provider that posts spans straight to the Genkit telemetry server; see Local observability and metrics for that workflow. The two compose, so you can keep the Developer UI while also exporting OTLP.

The conventions are defined by the spec, so the shape below is the same wherever Genkit implements them.

Model spans. One client span per model call, named chat <model>, carrying:

AttributeNotes
gen_ai.operation.namechat
gen_ai.provider.nameDerived from the plugin, for example gcp.gemini, gcp.vertex_ai, openai, anthropic. Unknown plugins pass through lowercased.
gen_ai.request.modelThe model name, without the plugin prefix.
gen_ai.request.*Request config that was actually set: temperature, top_p, top_k, max_tokens, stop_sequences, frequency_penalty, presence_penalty, seed, choice.count.
gen_ai.output.typejson or text, when an output format was requested.
gen_ai.usage.input_tokens, gen_ai.usage.output_tokensToken counts from the response. Reasoning and cached tokens land in gen_ai.usage.reasoning.output_tokens and gen_ai.usage.cache_read.input_tokens when the model reports them.
gen_ai.response.finish_reasonsMapped to spec values. A turn that ends in tool calls reports tool_calls.
error.typeOn failure. The exception is recorded on the span and rethrown unchanged.

Metrics. The two spec-defined client metrics:

  • gen_ai.client.token.usage, a histogram of token counts split by gen_ai.token.type (input or output).
  • gen_ai.client.operation.duration, in seconds, recorded for successful and failed calls alike. Failures carry error.type.

Both are tagged only with operation name, model, and provider, so cardinality stays low.

Everything else in the trace. Tool calls can be emitted as execute_tool spans, and every other Genkit action type gets a generic span named after the action, so the trace tree stays connected rather than showing model calls as orphans. Generic spans carry genkit.action.type, kept outside the reserved gen_ai.* namespace so GenAI-aware backends do not try to render Genkit payloads as spec content. Telemetry labels become span attributes, and tracing.SetSpanMetadata writes genkit:metadata:<key>, as with the default instrumentation.

A few mapping rules are specific to Go:

  • error.type is the Genkit status name, such as INVALID_ARGUMENT or RESOURCE_EXHAUSTED, and INTERNAL when the error has no status. The error is recorded on the span and returned to the caller unchanged.
  • A call that fails after the model responded, for example when output schema validation fails, still records the response: token usage, content, token metrics, and a finish reason of error.
  • Input and output tokens are recorded whenever the response reports usage. Reasoning and cache-read tokens are recorded only when they are not zero.
  • gen_ai.request.* values are read from the request config whether it is Genkit’s ai.GenerationCommonConfig or a provider SDK’s own config type. A value set to 0 is recorded.
  • Inline media is never captured. A data: URI becomes a spec blob part with its modality, MIME type, and estimated size, and remote media becomes a uri part.

The instrumentation lives in the otelgenai plugin:

Terminal window
go get github.com/firebase/genkit/go/plugins/otelgenai
go get go.opentelemetry.io/contrib/exporters/autoexport

Your application owns the OpenTelemetry SDK. Register its providers, set the GenAI instrumentation with tracing.SetInstrumentation, then call genkit.Init. This setup reads the standard OTEL_* environment variables through the contrib autoexport package, and otherwise exports OTLP http/protobuf to localhost:4318:

package main
import (
"context"
"fmt"
"log"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/otel"
otellogglobal "go.opentelemetry.io/otel/log/global"
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/core/tracing"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"github.com/firebase/genkit/go/plugins/otelgenai"
)
func main() {
ctx := context.Background()
shutdown := setupOTel(ctx)
defer shutdown(ctx) // Flushes spans, metrics, and logs.
tracing.SetInstrumentation(otelgenai.New(&otelgenai.Options{
EmitToolSpans: true,
}))
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Explain OpenTelemetry in one sentence."))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text())
}
// setupOTel registers the global trace, metric, and log providers, and returns
// a function that flushes them.
func setupOTel(ctx context.Context) func(context.Context) {
spanExp, err := autoexport.NewSpanExporter(ctx)
if err != nil {
log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(spanExp))
otel.SetTracerProvider(tp)
reader, err := autoexport.NewMetricReader(ctx)
if err != nil {
log.Fatal(err)
}
mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
otel.SetMeterProvider(mp)
logExp, err := autoexport.NewLogExporter(ctx)
if err != nil {
log.Fatal(err)
}
lp := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewBatchProcessor(logExp)))
otellogglobal.SetLoggerProvider(lp)
return func(ctx context.Context) {
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
_ = lp.Shutdown(ctx)
}
}

Point it at a collector with the usual environment variables, no code change:

Terminal window
export OTEL_SERVICE_NAME=my-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

autoexport accepts the grpc and http/protobuf OTLP protocols. The log provider is needed only for the EventOnly and SpanAndEvent content modes.

Some properties to know before you deploy:

  • It replaces the default encoding. tracing.SetInstrumentation replaces Genkit’s default OpenTelemetry provider, which writes the genkit:* attributes. To export both encodings, list both providers:

    tracing.SetInstrumentation(&tracing.OTelInstrumentation{}, otelgenai.New(nil))
  • The Developer UI keeps working. Under genkit start, the Developer UI’s own provider runs first, whatever you set, so it keeps showing Genkit’s traces next to this export.

  • Without an SDK, it does nothing. When no OpenTelemetry SDK is registered, spans and instruments are no-ops, so the instrumentation can stay wired up in builds that do not export. The first span logs one warning that nothing is exported.

  • The setting is process-wide. Genkit actions can run without a Genkit instance, for example a model taken straight from a plugin, and those calls are instrumented too.

Prompts and responses can contain PII, so content capture is off by default. The mode mirrors the spec’s ContentCapturingMode:

ModeWhere content goes
noContent (default)Not captured.
spanOnlySpan attributes gen_ai.input.messages, gen_ai.output.messages, and gen_ai.system_instructions, as JSON strings.
eventOnlyA gen_ai.client.inference.operation.details log record.
spanAndEventBoth.

In Go, the modes are the otelgenai.ContentCapturingMode constants otelgenai.NoContent, otelgenai.SpanOnly, otelgenai.EventOnly, and otelgenai.SpanAndEvent:

tracing.SetInstrumentation(otelgenai.New(&otelgenai.Options{
ContentCapturingMode: otelgenai.SpanOnly, // May contain PII.
}))

Left unset, the value comes from the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT environment variable (otelgenai.CaptureContentEnvVar) using the spec’s tokens (NO_CONTENT, SPAN_ONLY, EVENT_ONLY, SPAN_AND_EVENT), case-insensitive. An explicit value overrides it, and an unrecognized token logs one warning and falls back to NO_CONTENT. In the log record, content is carried as structured map and slice values; on the span, as JSON strings.

otelgenai.New takes an *otelgenai.Options. Pass nil for the defaults. The options are read once, in New.

FieldDefaultWhat it does
ContentCapturingModeenv, else NoContentWhere spec-shaped gen_ai.* message content is recorded.
CaptureActionIOfalseRecords raw Genkit input and output as genkit.input / genkit.output on every span. A debugging aid, independent of ContentCapturingMode. May contain PII. Inline media in model requests and responses is replaced by a size marker such as data:image/png;base64,[1834 bytes omitted]; other values, such as flow inputs, are recorded as is.
EmitToolSpansfalseexecute_tool spans for tool actions.
DisableMetricsfalseTurns off the token usage and operation duration metrics.
ScopeNamegenkit-genaiInstrumentation scope for the tracer, meter, and logger.
TracerProvider, MeterProvider, LoggerProviderthe globalsProviders for spans, metrics, and content events. A global registered after New is still picked up.

The otel-genai sample is a runnable end-to-end setup: traces to Jaeger and metrics to a collector, with a script that downloads and runs both without Docker.