OpenTelemetry’s OTLP is designed to be a universal translator for your telemetry data, letting you send metrics, traces, and logs to virtually any observability backend without changing your application code.
Here’s what that looks like in practice. Imagine you’re running a Go application. You’ve instrumented it with the OpenTelemetry SDK, and now you want to send that data to Honeycomb. Your exporter configuration in the Go code might look something like this:
import (
"context"
"log"
"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.10.0"
)
func initTracer() func() {
ctx := context.Background()
// Configure the OTLP gRPC exporter
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("api.honeycomb.io:443"), // Honeycomb's OTLP endpoint
otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()), // Use TLS for production
otlptracegrpc.WithHeaders(map[string]string{
"x-honeycomb-team": "YOUR_HONEYCOMB_API_KEY", // Your Honeycomb API key
}),
)
if err != nil {
log.Fatalf("failed to create OTLP trace exporter: %v", err)
}
// Set the service name and other resource attributes
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String("my-go-app"),
semconv.ServiceVersionKey.String("1.0.0"),
),
)
if err != nil {
log.Fatalf("failed to create resource: %v", err)
}
// Configure the tracer provider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
return func() {
// Shutdown the tracer provider when the application exits
if err := tp.Shutdown(ctx); err != nil {
log.Fatalf("failed to shutdown tracer provider: %v", err)
}
}
}
This code sets up a trace exporter that speaks OTLP over gRPC, targets Honeycomb’s specific endpoint, and includes the necessary API key in the headers. The sdktrace.WithBatcher(exporter) part is crucial; it tells the SDK to collect traces and send them in batches using this OTLP exporter.
Now, what if you wanted to send the same telemetry data to Datadog instead? You’d only change the exporter configuration. You’d swap out the otlptracegrpc.New call:
// ... inside initTracer function ...
// Configure the OTLP gRPC exporter for Datadog
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("api.datadoghq.com:443"), // Datadog's OTLP endpoint
otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()), // Use TLS for production
otlptracegrpc.WithHeaders(map[string]string{
"DD-API-KEY": "YOUR_DATADOG_API_KEY", // Your Datadog API key
}),
)
if err != nil {
log.Fatalf("failed to create OTLP trace exporter: %v", err)
}
// ... rest of the initTracer function remains the same ...
Notice how the sdktrace.WithBatcher(exporter) and the service resource configuration are untouched. The application logic that generates the telemetry remains identical. The only change is how it’s exported.
The underlying problem OpenTelemetry’s OTLP solves is the fragmentation of the observability tooling landscape. Before OTLP, each vendor had its own proprietary agent, SDK integration, or export format. Integrating with a new backend meant significant engineering effort to rewrite instrumentation or deploy and manage new agents. OTLP, defined by the OpenTelemetry project, standardizes the protocol and data format. This means your application, instrumented once with OpenTelemetry, can emit data in OTLP format, and then you can configure any OTLP-compatible collector or directly into an OTLP-compatible backend.
Internally, OTLP defines two main communication protocols: gRPC and HTTP/protobuf. The gRPC protocol is generally preferred for its efficiency and performance, especially for high-volume trace data. The HTTP/protobuf protocol offers broader compatibility, especially in environments where gRPC might be harder to set up or traverse firewalls. Both protocols use Protobuf to serialize the telemetry data into a structured format that can be understood by any OTLP receiver. The sdktrace.WithBatcher in the Go example, or similar constructs in other SDKs, ensures that data isn’t sent one span at a time, but rather batched efficiently to reduce network overhead.
The magic of OTLP isn’t just about sending data out. It’s also about the collector. You can deploy the OpenTelemetry Collector as an intermediary. This collector can receive data in OTLP format (or other formats like Jaeger, Prometheus, etc.), perform transformations (like filtering, sampling, adding metadata), and then export it to multiple backends simultaneously in their native formats or OTLP. This decouples your application from the specific requirements of each backend.
When you configure an OTLP exporter, you’re essentially telling your application’s SDK: "Here’s the address and authentication for the OTLP endpoint, and here’s how you should format the data (gRPC or HTTP)." The SDK then serializes your application’s internal trace, metric, or log data into the OTLP Protobuf messages and sends them over the wire. The receiver (whether it’s a direct backend endpoint or an OTel Collector) then deserializes these messages.
The OTLP specification defines clear Protobuf schemas for TraceService, MetricsService, and LogsService. Each service has methods like Export that accept a batch of telemetry data. For traces, this includes ExportTraceServiceRequest containing ResourceSpans which are collections of spans grouped by resource. For metrics, it’s ExportMetricsServiceRequest with ResourceMetrics, and for logs, ExportLogsServiceRequest with ResourceLogs. The Resource itself carries common attributes for all telemetry signals within it, like service name, version, and environment.
The surprising thing about OTLP is how it abstracts away not just the destination, but also the intermediate steps that used to be required. Before OTLP, if you wanted to send traces to Jaeger and Prometheus, you’d likely instrument your app for Jaeger, then run a Prometheus exporter, and maybe a separate agent for each. With OTLP, you instrument once, send OTLP to an OpenTelemetry Collector, and that collector can then speak Jaeger’s native protocol, Prometheus’s native protocol, and dozens of others simultaneously. The collector becomes the single point of integration for your backend ecosystem.
The next step after mastering OTLP export is understanding OTLP receivers and processors within the OpenTelemetry Collector.