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

noop.mddocs/

No-Op Implementations

The noop package provides production-ready implementations of the OpenTelemetry Trace API that produce no telemetry and minimize computational resources. These implementations are useful for testing, disabling telemetry, or as default behaviors for API implementers.

Package Import

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

Purpose and Use Cases

No-op implementations:

  • Disable telemetry: Turn off tracing without code changes
  • Testing: Provide non-recording implementations in tests
  • Default behavior: Safe fallback when implementing custom Tracer/TracerProvider
  • Performance: Minimal overhead when tracing is not needed

Note: The main go.opentelemetry.io/otel/trace package also provides a deprecated NewNoopTracerProvider() function. New code should use noop.NewTracerProvider() from this package instead.

TracerProvider

type TracerProvider struct {
    embedded.TracerProvider
}

// NewTracerProvider returns a TracerProvider that does not record any
// telemetry.
func NewTracerProvider() TracerProvider

// Tracer returns an OpenTelemetry Tracer that does not record any telemetry.
func (TracerProvider) Tracer(string, ...trace.TracerOption) trace.Tracer

Usage Example

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

func main() {
    // Disable tracing globally
    otel.SetTracerProvider(noop.NewTracerProvider())

    // All tracing operations will now be no-ops
    tracer := otel.Tracer("my-service")
    ctx, span := tracer.Start(ctx, "operation")
    defer span.End()
    // No telemetry is produced
}

Testing Example

package myservice

import (
    "testing"
    "context"
    "go.opentelemetry.io/otel/trace/noop"
)

func TestServiceOperation(t *testing.T) {
    // Use no-op tracer provider for tests
    tp := noop.NewTracerProvider()
    service := NewService(tp)

    // Test service logic without producing trace data
    err := service.DoWork(context.Background())
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
}

Tracer

type Tracer struct {
    embedded.Tracer
}

// Start creates a span. The created span will be set in a child context of ctx
// and returned with the span.
//
// If ctx contains a span context, the returned span will also contain that
// span context. If the span context in ctx is for a non-recording span,
// that span instance will be returned directly.
func (Tracer) Start(ctx context.Context, _ string, _ ...trace.SpanStartOption) (context.Context, trace.Span)

Behavior

The no-op Tracer:

  • Creates no-op Span instances
  • Preserves context propagation (SpanContext is maintained)
  • Reuses non-recording spans when possible
  • Accepts all options but ignores them
  • Has no side effects

Usage Example

import (
    "context"
    "go.opentelemetry.io/otel/trace/noop"
)

func processWithNoOp(ctx context.Context) {
    tp := noop.NewTracerProvider()
    tracer := tp.Tracer("no-telemetry")

    // Span is created but does nothing
    ctx, span := tracer.Start(ctx, "process")
    defer span.End()

    // All span operations are no-ops
    span.SetAttributes(/* no effect */)
    span.AddEvent("event") // no effect
    span.SetStatus(codes.Ok, "") // no effect

    // But context propagation still works
    childCtx, childSpan := tracer.Start(ctx, "child")
    defer childSpan.End()
}

Span

type Span struct {
    embedded.Span
    // contains unexported fields
}

Span Methods (All No-Op)

// End does nothing.
func (Span) End(...trace.SpanEndOption)

// AddEvent does nothing.
func (Span) AddEvent(string, ...trace.EventOption)

// AddLink does nothing.
func (Span) AddLink(trace.Link)

// IsRecording always returns false.
func (Span) IsRecording() bool

// RecordError does nothing.
func (Span) RecordError(error, ...trace.EventOption)

// SpanContext returns an empty span context.
func (s Span) SpanContext() trace.SpanContext

// SetStatus does nothing.
func (Span) SetStatus(codes.Code, string)

// SetName does nothing.
func (Span) SetName(string)

// SetAttributes does nothing.
func (Span) SetAttributes(...attribute.KeyValue)

// TracerProvider returns a No-Op TracerProvider.
func (Span) TracerProvider() trace.TracerProvider

Key Behaviors

  1. IsRecording() always returns false: Allows optimizations in user code

    if span.IsRecording() {
        // This block never executes with no-op span
        expensiveData := computeMetrics()
        span.SetAttributes(expensiveData)
    }
  2. SpanContext() returns empty context: Still valid for propagation

    sc := span.SpanContext()
    // sc.IsValid() returns false for no-op spans
  3. All mutating operations are no-ops: Safe to call, but have no effect

Complete Example: Conditional Tracing

package main

import (
    "context"
    "flag"

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

var enableTracing = flag.Bool("trace", false, "enable distributed tracing")

func main() {
    flag.Parse()

    // Configure tracer provider based on flag
    var tp trace.TracerProvider
    if *enableTracing {
        // Use real implementation (e.g., from SDK)
        tp = setupRealTracer()
    } else {
        // Use no-op implementation
        tp = noop.NewTracerProvider()
    }

    otel.SetTracerProvider(tp)

    // Application code remains the same
    runApplication()
}

func runApplication() {
    tracer := otel.Tracer("my-app")
    ctx := context.Background()

    ctx, span := tracer.Start(ctx, "main-operation")
    defer span.End()

    // All tracing calls work regardless of provider
    span.SetAttributes(/* ... */)
    processRequest(ctx, tracer)
}

func processRequest(ctx context.Context, tracer trace.Tracer) {
    ctx, span := tracer.Start(ctx, "process-request")
    defer span.End()

    // Optimize expensive operations based on recording state
    if span.IsRecording() {
        // Only compute when actually recording
        metrics := computeExpensiveMetrics()
        span.SetAttributes(metrics...)
    }
}

func setupRealTracer() trace.TracerProvider {
    // Return actual SDK implementation
    return nil // placeholder
}

Using as Default Implementation

When implementing custom TracerProvider, embed no-op types for safe defaults:

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

// CustomTracerProvider with no-op defaults
type CustomTracerProvider struct {
    noop.TracerProvider // Embed for default behavior
    // custom fields
}

// Override only the methods you implement
func (ctp *CustomTracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
    // Custom implementation
    return &CustomTracer{
        name: name,
    }
}

// CustomTracer with no-op defaults
type CustomTracer struct {
    noop.Tracer // Embed for default behavior
    name string
}

// Override only the methods you implement
func (ct *CustomTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
    // Custom implementation
    return ctx, &CustomSpan{name: spanName}
}

// CustomSpan with no-op defaults
type CustomSpan struct {
    noop.Span // Embed for default behavior
    name string
}

// Override specific methods as needed
func (cs *CustomSpan) End(opts ...trace.SpanEndOption) {
    // Custom end logic
}

Comparison: No-Op vs Non-Recording Span

FeatureNo-Op SpanNon-Recording Span
IsRecording()Always falseAlways false
Context propagationMaintains SpanContextMaintains SpanContext
Sampling decisionNot sampledSampled but not exported
PerformanceMinimal overheadSlightly higher overhead
Use caseDisable tracingRespect sampling decisions

Performance Characteristics

No-op implementations are designed for minimal overhead:

// No-op operations have nearly zero cost
span.SetAttributes(attr1, attr2, attr3) // ~1-2 ns
span.AddEvent("event")                   // ~1-2 ns
span.End()                               // ~1-2 ns

Compared to recording spans which involve:

  • Memory allocation
  • Attribute validation
  • Event storage
  • Export pipeline processing

Best Practices

  1. Use for testing: Avoid test dependencies on trace infrastructure

    func TestMyFunction(t *testing.T) {
        tp := noop.NewTracerProvider()
        service := NewService(tp)
        // Test without trace overhead
    }
  2. Conditional tracing: Enable/disable based on configuration

    if config.TracingEnabled {
        tp = setupSDK()
    } else {
        tp = noop.NewTracerProvider()
    }
  3. Embed for partial implementations: Provide safe defaults

    type MyTracer struct {
        noop.Tracer // Unimplemented methods are no-ops
    }
  4. Check IsRecording(): Optimize expensive operations

    if span.IsRecording() {
        // Only compute if span is actually recording
    }
  5. Global default: Set no-op as default until SDK is initialized

    func init() {
        otel.SetTracerProvider(noop.NewTracerProvider())
    }
    
    func main() {
        // Later, replace with real implementation
        otel.SetTracerProvider(realProvider)
    }

Migration Path

Starting with no-op and migrating to full tracing:

// Phase 1: Add tracing instrumentation with no-op
func init() {
    otel.SetTracerProvider(noop.NewTracerProvider())
}

// Phase 2: Enable tracing in dev/staging
func initTelemetry() {
    if os.Getenv("ENV") != "production" {
        otel.SetTracerProvider(realSDK())
    } else {
        otel.SetTracerProvider(noop.NewTracerProvider())
    }
}

// Phase 3: Enable in production with sampling
func initTelemetry() {
    tp := realSDK()
    // SDK handles sampling; no need for no-op
    otel.SetTracerProvider(tp)
}