OpenTelemetry SDK & Language Architecture

A core architectural principle of OpenTelemetry is the strict separation between the API and the SDK.

graph TD
    AppCode[Application Code / Third-Party Libraries] -->|Calls| OTelAPI[OpenTelemetry API<br/>Interfaces, No-Op Defaults, Context]
    OTelAPI -.->|Implemented By| OTelSDK[OpenTelemetry SDK<br/>Batching, Sampling, Resource Detectors, Exporters]
    OTelSDK -->|OTLP gRPC/HTTP| Collector[OpenTelemetry Collector]

    style OTelAPI fill:#3b82f6,stroke:#1d4ed8,color:#fff
    style OTelSDK fill:#10b981,stroke:#047857,color:#fff

1. The API vs SDK Split

  • The API (Dependency for Libraries): Contains only interfaces and no-op implementations. Library authors (like HTTP framework or database driver maintainers) depend only on the OpenTelemetry API. If an application does not initialize an SDK, calls to the API are cost-free no-ops.
  • The SDK (Dependency for Applications): Contains the actual operational implementation: memory buffering, span batching, head/tail sampling, resource attribution, and network exporters. Only the top-level application binary imports and configures the SDK.

2. Auto-Instrumentation vs Manual Instrumentation

MechanismHow It WorksLanguages SupportedPros & Cons
Bytecode / AgentInstruments class loading at runtimeJava, .NETZero code changes; slight startup overhead
Monkey PatchingWraps library imports dynamicallyPython, Node.jsFast setup; may conflict with custom monkey patches
eBPF Auto-InstrumentationKernel probes capture network/syscallsGo, C++, Rust, Kernel100% non-invasive; limited L7 application-specific context
Manual InstrumentationExplicit calls in code (tracer.Start())All languagesFull control, domain context; requires code changes

3. Production SDK Initialization Pattern (Go)

package main
 
import (
	"context"
	"time"
 
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
 
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint("otel-collector:4317"),
	)
	if err != nil {
		return nil, err
	}
 
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("order-service"),
			semconv.DeploymentEnvironmentKey.String("production"),
		),
		resource.WithHost(),
		resource.WithProcess(),
	)
	if err != nil {
		return nil, err
	}
 
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter, sdktrace.WithBatchTimeout(5*time.Second)),
		sdktrace.WithResource(res),
		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), // 10% sampling
	)
 
	otel.SetTracerProvider(tp)
	return tp, nil
}