Context Propagation¶
The SDK uses Go's context.Context for automatic trace and span propagation. This enables:
- Automatic parent-child span relationships
- Easy access to current trace/span from anywhere
- Clean API without passing trace objects everywhere
Starting Traces with Context¶
// Start a trace and get updated context
ctx, trace, _ := opik.StartTrace(ctx, client, "my-trace")
// The trace is now in the context
currentTrace := opik.TraceFromContext(ctx)
Starting Spans with Context¶
// Start a span - automatically nested under current span/trace
ctx, span, _ := opik.StartSpan(ctx, "my-span")
// Start another span - automatically nested under the previous span
ctx, childSpan, _ := opik.StartSpan(ctx, "child-span")
Retrieving from Context¶
// Get current trace
trace := opik.TraceFromContext(ctx)
if trace != nil {
fmt.Printf("Current trace: %s\n", trace.ID())
}
// Get current span
span := opik.SpanFromContext(ctx)
if span != nil {
fmt.Printf("Current span: %s\n", span.ID())
}
Practical Example¶
Context propagation makes it easy to add tracing to existing code:
func HandleRequest(ctx context.Context, req *Request) (*Response, error) {
// Start trace at entry point
ctx, trace, _ := opik.StartTrace(ctx, client, "handle-request",
opik.WithTraceInput(req),
)
defer trace.End(ctx)
// Call nested functions - they can access trace via context
result, err := processRequest(ctx, req)
if err != nil {
return nil, err
}
trace.End(ctx, opik.WithTraceOutput(result))
return result, nil
}
func processRequest(ctx context.Context, req *Request) (*Response, error) {
// Create span - automatically nested under trace
ctx, span, _ := opik.StartSpan(ctx, "process-request")
defer span.End(ctx)
// Call LLM
return callLLM(ctx, req.Query)
}
func callLLM(ctx context.Context, query string) (*Response, error) {
// Create LLM span - automatically nested under process-request span
ctx, span, _ := opik.StartSpan(ctx, "llm-call",
opik.WithSpanType(opik.SpanTypeLLM),
opik.WithSpanModel("gpt-4"),
)
defer span.End(ctx)
// Make the actual LLM call
response, err := llmClient.Complete(ctx, query)
span.End(ctx, opik.WithSpanOutput(map[string]any{"response": response}))
return response, err
}
Distributed Tracing¶
For microservices, propagate trace context across HTTP boundaries:
Injecting Headers (Client Side)¶
// Inject trace headers into outgoing request
req, _ := http.NewRequestWithContext(ctx, "POST", url, body)
opik.InjectDistributedTraceHeaders(ctx, req)
Extracting Headers (Server Side)¶
func handler(w http.ResponseWriter, r *http.Request) {
// Extract trace headers
headers := opik.ExtractDistributedTraceHeaders(r)
// Continue the distributed trace
ctx, span, _ := client.ContinueTrace(r.Context(), headers, "handle-request")
defer span.End(ctx)
// Process request...
}
Propagating HTTP Client¶
Use the built-in propagating client:
// Create client that auto-injects trace headers
httpClient := opik.PropagatingHTTPClient()
// All requests will include trace headers
resp, _ := httpClient.Do(req.WithContext(ctx))
Header Format¶
The SDK uses these headers for distributed tracing:
| Header | Description |
|---|---|
X-Opik-Trace-ID |
The trace ID |
X-Opik-Parent-Span-ID |
The parent span ID |