or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

embedded.mdindex.mdnoop.mdspan-context.mdspan-options.mdspan.mdtracer-provider-tracer.md
tile.json

embedded.mddocs/

API Implementation Patterns

The embedded package provides interfaces that help implementation authors safely handle the OpenTelemetry Trace API's non-standard evolution policy. This package is crucial for anyone implementing custom TracerProvider, Tracer, or Span types.

Package Import

import "go.opentelemetry.io/otel/trace/embedded"

API Evolution Policy

The OpenTelemetry Trace API does not conform to standard Go versioning:

  • Interfaces may have methods added in minor releases (no major version bump)
  • This is intentional to allow API evolution without breaking existing installations
  • Implementation authors must choose how to handle unimplemented methods

The Problem

Without embedded types, adding a method to an interface would silently break existing implementations:

// OpenTelemetry adds a new method in v1.20.0
type Tracer interface {
    Start(ctx context.Context, name string, opts ...SpanStartOption) (context.Context, Span)
    // NEW in v1.20.0
    NewMethod() string
}

// User's implementation from v1.19.0
type MyTracer struct {}

func (t *MyTracer) Start(...) {...}
// Missing NewMethod() - compiles but will panic at runtime!

The Solution: Three Implementation Patterns

The embedded package provides interfaces that force implementation authors to make an explicit choice about handling API evolution.

Pattern 1: Compilation Failure (Recommended)

Embed the corresponding embedded interface to get compilation errors when the API evolves:

import "go.opentelemetry.io/otel/trace/embedded"

type MyTracerProvider struct {
    embedded.TracerProvider // Embed this
    // your fields
}

func (tp *MyTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer {
    // your implementation
}

Benefits:

  • Compile-time detection of API changes
  • Forces implementation updates
  • Prevents runtime panics
  • Safe for users

When OpenTelemetry adds a method: Your code won't compile, signaling you need to update.

Pattern 2: Panic (Not Recommended)

Embed the API interface directly to panic on unimplemented methods:

import "go.opentelemetry.io/otel/trace"

type MyTracerProvider struct {
    trace.TracerProvider // Embed API interface directly
    // your fields
}

func (tp *MyTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer {
    // your implementation
}

Problems:

  • Runtime panics when users update OpenTelemetry
  • May affect transitive dependencies
  • Poor user experience

Not recommended - leads to runtime failures.

Pattern 3: Default to No-Op

Embed a no-op implementation to silently drop calls to unimplemented methods:

import "go.opentelemetry.io/otel/trace/noop"

type MyTracerProvider struct {
    noop.TracerProvider // Embed no-op implementation
    // your fields
}

func (tp *MyTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer {
    // your implementation
}

Use case:

  • When silent no-op is acceptable default behavior
  • Implementing partial functionality

Caution: Only embed noop types from go.opentelemetry.io/otel/trace/noop as they're guaranteed to implement all future API changes.

Embedded Interfaces

TracerProvider

type TracerProvider interface {
    // contains unexported methods
}

Embed this in your TracerProvider implementations:

type CustomTracerProvider struct {
    embedded.TracerProvider
    config *Config
}

func (ctp *CustomTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer {
    // Implementation
    return &CustomTracer{name: name}
}

Tracer

type Tracer interface {
    // contains unexported methods
}

Embed this in your Tracer implementations:

type CustomTracer struct {
    embedded.Tracer
    name string
    provider *CustomTracerProvider
}

func (ct *CustomTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
    // Implementation
    span := &CustomSpan{name: spanName}
    return trace.ContextWithSpan(ctx, span), span
}

Span

type Span interface {
    // contains unexported methods
}

Embed this in your Span implementations:

type CustomSpan struct {
    embedded.Span
    name string
    spanContext trace.SpanContext
}

func (cs *CustomSpan) End(options ...trace.SpanEndOption) {
    // Implementation
}

func (cs *CustomSpan) SetAttributes(kv ...attribute.KeyValue) {
    // Implementation
}

// ... implement other Span methods

Complete Implementation Example

package customtracer

import (
    "context"
    "sync"

    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
    "go.opentelemetry.io/otel/trace/embedded"
)

// CustomTracerProvider implements trace.TracerProvider
type CustomTracerProvider struct {
    embedded.TracerProvider // Compilation failure on API changes

    mu      sync.Mutex
    tracers map[string]*CustomTracer
}

func NewCustomTracerProvider() *CustomTracerProvider {
    return &CustomTracerProvider{
        tracers: make(map[string]*CustomTracer),
    }
}

func (ctp *CustomTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer {
    ctp.mu.Lock()
    defer ctp.mu.Unlock()

    if tracer, ok := ctp.tracers[name]; ok {
        return tracer
    }

    tracer := &CustomTracer{
        name:     name,
        provider: ctp,
    }
    ctp.tracers[name] = tracer
    return tracer
}

// CustomTracer implements trace.Tracer
type CustomTracer struct {
    embedded.Tracer // Compilation failure on API changes

    name     string
    provider *CustomTracerProvider
}

func (ct *CustomTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
    // Parse options
    config := trace.NewSpanStartConfig(opts...)

    // Create span
    span := &CustomSpan{
        name:        spanName,
        spanContext: trace.SpanContext{}, // Would normally generate IDs
        attributes:  config.Attributes(),
    }

    // Return context with span
    ctx = trace.ContextWithSpan(ctx, span)
    return ctx, span
}

// CustomSpan implements trace.Span
type CustomSpan struct {
    embedded.Span // Compilation failure on API changes

    name        string
    spanContext trace.SpanContext
    attributes  []attribute.KeyValue

    mu sync.Mutex
}

func (cs *CustomSpan) End(options ...trace.SpanEndOption) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    // End implementation
}

func (cs *CustomSpan) AddEvent(name string, options ...trace.EventOption) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    // AddEvent implementation
}

func (cs *CustomSpan) AddLink(link trace.Link) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    // AddLink implementation
}

func (cs *CustomSpan) IsRecording() bool {
    return true
}

func (cs *CustomSpan) RecordError(err error, options ...trace.EventOption) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    // RecordError implementation
}

func (cs *CustomSpan) SpanContext() trace.SpanContext {
    return cs.spanContext
}

func (cs *CustomSpan) SetStatus(code codes.Code, description string) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    // SetStatus implementation
}

func (cs *CustomSpan) SetName(name string) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    cs.name = name
}

func (cs *CustomSpan) SetAttributes(kv ...attribute.KeyValue) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    cs.attributes = append(cs.attributes, kv...)
}

func (cs *CustomSpan) TracerProvider() trace.TracerProvider {
    // Return the provider (would need to store reference)
    return nil
}

Migration When API Changes

When OpenTelemetry adds a new method to an interface:

Step 1: Compilation Fails

$ go build
./tracer.go:15:6: cannot use &CustomTracerProvider{...} (type *CustomTracerProvider) as type trace.TracerProvider in return argument:
    *CustomTracerProvider does not implement trace.TracerProvider (missing NewMethod method)

Step 2: Check OpenTelemetry Release Notes

## v1.20.0

### Breaking Changes for Implementers

- Added `NewMethod()` to `TracerProvider` interface
- Implementations must provide this method or update embedded type

Step 3: Implement New Method

func (ctp *CustomTracerProvider) NewMethod() string {
    // Implement new functionality
    return "implemented"
}

Step 4: Tests Pass

$ go build
Build successful

Why This Pattern Exists

Problem: Semantic Versioning Conflict

  • OpenTelemetry wants API stability (no breaking changes)
  • But also wants to evolve the API without major version bumps
  • Go's interface satisfaction is structural (implicit)
  • Adding interface methods is traditionally a breaking change

Solution: Explicit Opt-In

  • Implementers must embed special types
  • This creates an explicit contract
  • API evolution becomes intentional, not accidental
  • Implementers choose their update strategy

Best Practices

  1. Always embed embedded types in production implementations

    type MyTracer struct {
        embedded.Tracer // Required
        // ...
    }
  2. Monitor OpenTelemetry releases for API changes

    • Subscribe to release notifications
    • Read changelogs before updating
    • Test in non-production first
  3. Document your choice in implementation comments

    // MyTracerProvider implements trace.TracerProvider.
    // It embeds embedded.TracerProvider to ensure compilation failures
    // when the OpenTelemetry API is extended, preventing runtime panics.
    type MyTracerProvider struct {
        embedded.TracerProvider
        // ...
    }
  4. Only use noop embedding from official package

    import "go.opentelemetry.io/otel/trace/noop"
    
    type PartialTracer struct {
        noop.Tracer // Safe - maintained by OpenTelemetry
        // ...
    }
  5. Implement all interface methods - don't rely on embedded defaults

    // Bad: relies on embedded methods
    type MySpan struct {
        embedded.Span
    }
    
    // Good: implements all methods explicitly
    type MySpan struct {
        embedded.Span
    }
    func (s *MySpan) End(...) { /* implementation */ }
    func (s *MySpan) AddEvent(...) { /* implementation */ }
    // ... all other methods

Common Mistakes

❌ Not embedding anything

// WRONG: Will panic when API evolves
type MyTracer struct {
    // no embedded type
}

❌ Embedding API interface

// WRONG: Will panic on unimplemented methods
type MyTracer struct {
    trace.Tracer // Embeds API interface directly
}

❌ Creating custom embedded types

// WRONG: Custom embedded types don't track API evolution
type myEmbeddedTracer interface {
    // unexported methods
}

type MyTracer struct {
    myEmbeddedTracer
}

✅ Correct pattern

// CORRECT: Uses official embedded type
import "go.opentelemetry.io/otel/trace/embedded"

type MyTracer struct {
    embedded.Tracer
}

SDK vs Application Code

SDK Implementations (libraries providing TracerProvider):

  • Must implement interfaces
  • Must embed embedded types
  • Subject to API evolution

Application Code (using TracerProvider):

  • Consumes interfaces only
  • Not affected by interface changes
  • No need to embed anything
// SDK code - must embed
type SDKTracerProvider struct {
    embedded.TracerProvider // Required
}

// Application code - just uses interfaces
func main() {
    tp := sdk.NewTracerProvider() // No embedding needed
    tracer := tp.Tracer("app")
    // ...
}

Further Reading

  • OpenTelemetry API Evolution Design
  • Go Interfaces and Compatibility
  • Package documentation: go doc go.opentelemetry.io/otel/trace/embedded