Skip to content

Telemetry

OpenTelemetry-compatible tracing for PromptKit sessions, following the OpenTelemetry GenAI Semantic Conventions.

The runtime/telemetry package integrates PromptKit with the OpenTelemetry Go SDK. It provides:

  • A real-time event listener that converts EventBus events into OTel spans as they occur
  • Typed spans using gen_ai.operation.name as a discriminator, per the GenAI Semantic Conventions
  • TracerProvider helpers for standalone OTLP export
  • Propagation setup for W3C Trace Context, W3C Baggage, and AWS X-Ray headers

Because it uses the standard OTel SDK, spans are exported through any configured SpanExporter — OTLP/HTTP, OTLP/gRPC, Jaeger, Zipkin, or custom exporters.

import "github.com/AltairaLabs/PromptKit/runtime/telemetry"

Each session produces a single trace. Span names follow the GenAI SIG convention {gen_ai.system} {gen_ai.operation.name} where applicable.

promptkit invoke_agent (root, SpanKindServer)
├── openai chat (SpanKindClient)
│ ├── [event] gen_ai.user.message
│ └── [event] gen_ai.assistant.message
├── execute_tool (SpanKindInternal)
├── openai chat (SpanKindClient)
│ └── [event] gen_ai.assistant.message
├── promptkit.middleware.auth (SpanKindInternal)
├── promptkit.eval.banned_words (SpanKindInternal)
├── promptkit.eval.response-quality (SpanKindInternal, instant)
├── promptkit.workflow.transition (SpanKindInternal, instant)
├── promptkit.workflow.transition (SpanKindInternal, instant)
└── promptkit.workflow.completed (SpanKindInternal, instant)

Every span carries a gen_ai.operation.name attribute (where applicable) that identifies its semantic type. This follows the GenAI Agent Spans convention.

Span Namegen_ai.operation.nameSpan KindDescription
promptkit invoke_agentinvoke_agentServerRoot session span
{system} chatchatClientLLM provider call
execute_toolexecute_toolInternalTool execution
promptkit.pipelineInternalPipeline execution
promptkit.middleware.{name}InternalMiddleware execution
promptkit.eval.{name}InternalGuardrail or eval execution
promptkit.workflow.transitionInternalWorkflow state transition (instant)
promptkit.workflow.completedInternalWorkflow terminal state (instant)

The simplest way to enable tracing is via the WithTracerProvider SDK option:

import (
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/AltairaLabs/PromptKit/sdk"
)
tp := sdktrace.NewTracerProvider(/* your exporter */)
defer tp.Shutdown(ctx)
conv, _ := sdk.Open("./app.pack.json", "chat",
sdk.WithTracerProvider(tp),
)

When a TracerProvider is configured, the SDK automatically wires an OTelEventListener into the EventBus. All pipeline events are converted to spans in real time — no manual wiring needed.

OTelEventListener converts runtime events into OTel spans in real time. It is safe for concurrent use and tolerates out-of-order event delivery (the EventBus dispatches events asynchronously).

func NewOTelEventListener(tracer trace.Tracer) *OTelEventListener
func (l *OTelEventListener) StartSession(parentCtx context.Context, sessionID string)
func (l *OTelEventListener) EndSession(sessionID string)

StartSession creates a root promptkit invoke_agent span, optionally parented under the span in parentCtx. All subsequent spans for this session are children of this root. EndSession ends the root span.

func (l *OTelEventListener) OnEvent(evt *events.Event)

Handles a single runtime event and creates/completes OTel spans accordingly. Pass this method to EventBus.SubscribeAll:

tracer := telemetry.Tracer(tp)
listener := telemetry.NewOTelEventListener(tracer)
listener.StartSession(ctx, sessionID)
bus.SubscribeAll(listener.OnEvent)
// ... run conversation ...
listener.EndSession(sessionID)

The root span for each conversation session.

Span name: promptkit invoke_agent

Attributes follow the GenAI Agent Spans convention:

AttributeSourceSpec reference
gen_ai.operation.name"invoke_agent"gen_ai.operation.name
gen_ai.system"promptkit"gen_ai.system
gen_ai.conversation.idSession IDgen_ai.conversation.id
gen_ai.agent.namePack name (when available)gen_ai.agent.name
gen_ai.agent.idPack ID (when available)gen_ai.agent.id
EventSpan
provider.call.started / provider.call.completed / provider.call.failed{system} chat span (SpanKindClient)

Span name: {provider} chat (e.g., openai chat, anthropic chat)

Attributes follow the GenAI Client Spans convention:

AttributeSourceSpec reference
gen_ai.operation.name"chat"gen_ai.operation.name
gen_ai.systemProvider namegen_ai.system
gen_ai.request.modelModel namegen_ai.request.model
gen_ai.usage.input_tokensInput token countgen_ai.usage.input_tokens
gen_ai.usage.output_tokensOutput token countgen_ai.usage.output_tokens
gen_ai.response.finish_reasonFinish reasongen_ai.response.finish_reasons
promptkit.message.countNumber of messagesPromptKit-specific
promptkit.tool.countNumber of toolsPromptKit-specific
promptkit.provider.costEstimated cost (USD)PromptKit-specific
EventSpan
pipeline.started / pipeline.completed / pipeline.failedpromptkit.pipeline span (SpanKindInternal)

Attributes: promptkit.run.id, promptkit.pipeline.cost, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens

EventBehaviour
message.createdAppended as a SpanEvent on the active provider span

Messages are not separate spans. They are attached as span events on the currently active {system} chat span, following the GenAI Events conventions. If no provider span is active, the event is attached to the root session span.

Event name: gen_ai.<role>.message (e.g., gen_ai.user.message, gen_ai.assistant.message)

Event attributes:

AttributeTypeDescription
gen_ai.message.contentstringText content of the message
gen_ai.tool_callsstring (JSON)Tool calls requested by assistant (present only when non-empty)
gen_ai.tool_resultstring (JSON)Tool result for tool-role messages (present only when non-nil)
EventSpan
tool.call.started / tool.call.completed / tool.call.failedexecute_tool span (SpanKindInternal)

Attributes follow the GenAI Agent Spans convention:

AttributeTypeSpec reference
gen_ai.operation.name"execute_tool"gen_ai.operation.name
gen_ai.tool.namestringgen_ai.tool.name
gen_ai.tool.call.idstringgen_ai.tool.call.id
gen_ai.tool.call.argumentsstring (JSON)gen_ai.tool.call.arguments (omitted when nil)
gen_ai.tool.typestringgen_ai.tool.type"function" for regular tools, "extension" for MCP tools

Tool execution duration is captured by the span’s start/end timestamps. Success or failure is captured by the span status code. MCP tools (prefixed mcp__) are automatically detected and tagged with gen_ai.tool.type = "extension" per the MCP conventions.

EventSpan
middleware.started / middleware.completed / middleware.failedpromptkit.middleware.{name} span (SpanKindInternal)

Attributes: promptkit.middleware.name, promptkit.middleware.index

EventSpan
validation.started / validation.passed / validation.failedpromptkit.eval.{name} span (SpanKindInternal)

Guardrail validations are traced as evaluation spans using the GenAI Evaluation Attributes. The promptkit.guardrail attribute distinguishes guardrails from other evals.

Attributes:

AttributeTypeDescriptionSpec reference
gen_ai.evaluation.namestringValidator name (e.g., banned_words)gen_ai.evaluation.name
gen_ai.evaluation.scorefloat641.0 if passed, 0.0 if failedgen_ai.evaluation.score
gen_ai.evaluation.explanationstringError message or joined violations (on failure only)gen_ai.evaluation.explanation
promptkit.eval.typestringValidator type (e.g., output)PromptKit-specific
promptkit.guardrailbooltrue — distinguishes guardrails from evalsPromptKit-specific
EventSpan
eval.completed / eval.failedpromptkit.eval.{evalID} instant span (SpanKindInternal)

Evals (assertions, LLM judges, content checks) are traced as instant evaluation spans. They share the same GenAI Evaluation Attributes as guardrails but with promptkit.guardrail = false.

Attributes:

AttributeTypeDescriptionSpec reference
gen_ai.evaluation.namestringEval IDgen_ai.evaluation.name
gen_ai.evaluation.scorefloat64Numeric score (omitted when nil)gen_ai.evaluation.score
gen_ai.evaluation.explanationstringHuman-readable explanation (omitted when empty)gen_ai.evaluation.explanation
promptkit.eval.typestringHandler type (e.g., llm_judge, contains)PromptKit-specific
promptkit.guardrailboolfalse — distinguishes evals from guardrailsPromptKit-specific

Passed evals have span status Ok. Failed evals have span status Error with the explanation or error message.

EventSpan
workflow.transitionedpromptkit.workflow.transition instant span (SpanKindInternal)
workflow.completedpromptkit.workflow.completed instant span (SpanKindInternal)

Workflow spans are instant — their start and end times are both set to the event timestamp.

Transition attributes:

AttributeTypeDescription
promptkit.workflow.from_statestringState before transition
promptkit.workflow.to_statestringState after transition
promptkit.workflow.eventstringTrigger event
promptkit.workflow.prompt_taskstringPrompt task of the new state

Completion attributes:

AttributeTypeDescription
promptkit.workflow.final_statestringTerminal state reached
promptkit.workflow.transition_countintTotal number of transitions

When a *.failed event is received, the corresponding span’s status is set to codes.Error with the error message.

The EventBus dispatches each Publish() in a separate goroutine, so completion events can arrive before their corresponding start events. The listener handles this transparently by buffering early completions and applying them when the start event arrives.

func Tracer(tp trace.TracerProvider) trace.Tracer

Returns a named tracer with instrumentation scope github.com/AltairaLabs/PromptKit (version 1.0.0). If tp is nil, the global noop provider is used.

func NewTracerProvider(ctx context.Context, endpoint, serviceName string) (*sdktrace.TracerProvider, error)

Creates a TracerProvider that exports spans via OTLP/HTTP to the given endpoint. The caller is responsible for calling Shutdown on the returned provider. Use this for standalone applications that don’t have their own OTel setup.

tp, err := telemetry.NewTracerProvider(ctx,
"http://localhost:4318/v1/traces",
"my-service",
)
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(ctx)
func SetupPropagation()

Configures the global OTel text-map propagator to handle:

Call this once at application startup if you need distributed trace propagation across HTTP boundaries:

telemetry.SetupPropagation()