open-telemetry/gen-otlp-protobuf
Generated OTLP protobuf classes for OpenTelemetry PHP, enabling efficient trace, metric, and log export. Requires google/protobuf, with the optional PHP C extension recommended for production performance.
This package is a low-level dependency: it ships generated PHP classes for the OTLP protobuf schema (Opentelemetry\Proto\...) and nothing else. In practice you almost never composer require it directly — it arrives transitively with the exporter that actually uses it:
composer require open-telemetry/exporter-otlp
composer show open-telemetry/gen-otlp-protobuf # confirm version resolved
It requires google/protobuf (the native PHP runtime). For anything beyond a local experiment, install the C extension — same API, dramatically faster serialization, and it takes precedence automatically when loaded:
pecl install protobuf
# then enable extension=protobuf.so in your php.ini
php -m | grep protobuf
Where to look first. Browse the generated classes in the package's autoload root. The single most useful entry point is the top-level message the exporter sends:
Opentelemetry\Proto\Collector\Trace\V1\ExportTraceServiceRequestOpentelemetry\Proto\Trace\V1\{ResourceSpans, ScopeSpans, Span, Status}Opentelemetry\Proto\Common\V1\{AnyValue, KeyValue}Opentelemetry\Proto\Resource\V1\ResourceMetrics and logs mirror the same shape (ExportMetricsServiceRequest, ExportLogsServiceRequest).
First use case: decode what you actually sent. Configure the exporter normally (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf), then capture a request body — from a proxy, a dd() in a custom exporter, or a tcpdump on the collector — and replay it through the generated classes. This is the fastest way to verify attribute mapping, span IDs and timestamps:
use Opentelemetry\Proto\Collector\Trace\V1\ExportTraceServiceRequest;
$request = new ExportTraceServiceRequest();
$request->mergeFromString($binaryBody); // the raw application/x-protobuf body
foreach ($request->getResourceSpans() as $rs) {
foreach ($rs->getResource()->getAttributes() as $kv) {
echo $kv->getKey(), '=', $kv->getValue()->getStringValue(), PHP_EOL;
}
foreach ($rs->getScopeSpans() as $ss) {
foreach ($ss->getSpans() as $span) {
printf("%s %s %.2fms%s", bin2hex($span->getSpanId()), $span->getName(),
($span->getEndTimeUnixNano() - $span->getStartTimeUnixNano()) / 1e6, PHP_EOL);
}
}
}
That one script covers 90% of the debugging you will ever do with this package.
Pattern 1 — Let the exporter own the types (the default). Wire the SDK once in a service provider and never touch protobuf classes again. The open-telemetry/exporter-otlp package constructs ExportTraceServiceRequest for you and sets Content-Type: application/x-protobuf. Your entire composer.json surface should be the SDK + exporter + configuration; pinning gen-otlp-protobuf yourself is a smell unless you are writing an exporter.
Pattern 2 — Custom exporter for a non-standard transport. This is where the package earns its keep. When you must, say, sign requests, route through a corporate gateway, or post to a vendor's HTTP endpoint with bespoke headers, implement SpanExporterInterface and use the generated types directly:
use Opentelemetry\Proto\Collector\Trace\V1\ExportTraceServiceRequest;
final class GatewaySpanExporter implements SpanExporterInterface
{
public function export(iterable $batch): bool
{
$request = new ExportTraceServiceRequest();
$request->setResourceSpans(/* map SDK ReadableSpan → ResourceSpans */);
return Http::withToken(config('otel.token'))
->withHeaders(['Content-Type' => 'application/x-protobuf'])
->withBody($request->serializeToString(), 'application/x-protobuf')
->post(config('otel.endpoint').'/v1/traces')
->successful();
}
}
Pattern 3 — Manual span construction for fixtures and tools. For an artisan command that emits synthetic telemetry, or a fixture generator for collector-side load tests, build messages field by field:
$value = new AnyValue();
$value->setStringValue('GET');
$attr = new KeyValue();
$attr->setKey('http.request.method');
$attr->setValue($value);
$span = new Span();
$span->setTraceId(hex2bin($traceIdHex)); // 16 raw bytes, not hex
$span->setSpanId(hex2bin($spanIdHex)); // 8 raw bytes
$span->setName('GET /orders');
$span->setKind(Span\SpanKind::SPAN_KIND_SERVER);
$span->setStartTimeUnixNano(hrtime(true));
$span->setAttributes([$attr]);
Pattern 4 — Assert on telemetry in tests. Keep OTEL_SDK_DISABLED=true in phpunit.xml globally, and in the few tests that need assertions swap in an in-memory exporter or a fake transport that captures the serialized body, then mergeFromString() it and assert on the resulting Span objects. This tests your instrumentation, not the SDK.
Pattern 5 — Instrument jobs and events, not just HTTP. Wrap queue workers with the same span builder you use in HTTP middleware, but attach the trace context to the job payload (W3C traceparent) so the consumer can resume the trace. The protobuf layer never changes between these paths — only the Resource attributes and span names do.
Opentelemetry\Proto (lowercase "t"), not OpenTelemetry. IDE autocompletion lies about this constantly.open-telemetry/opentelemetry-php (proto/otel). Patches go upstream; regeneration is a release-time job.gen-otlp-protobuf paired with a newer exporter silently drops fields — e.g. the remote-parent flags bit added in 1.2.0 is absent in older generated code, so your traces lose parent context without any error. Always upgrade the whole stack together and regenerate the collector image at the same cadence.google/protobuf and ext-protobuf expose identical classes; the extension wins if loaded. Never run both in separate processes and compare output formats — they match, but --ignore-platform-reqs during composer install in CI often hides a missing extension until you benchmark.unix_nano needs 64-bit PHP. These fields are int64. On a 32-bit build timestamps overflow and the collector rejects or misorders spans. Confirm PHP_INT_SIZE === 8.Span\SpanKind::SPAN_KIND_SERVER, Status\StatusCode::STATUS_CODE_ERROR, etc. Passing 'SERVER' yields silent garbage.AnyValue is a oneof. getValue() returns the field name ("string_value"), not the value. Call getStringValue() / getIntValue() / getBoolValue() based on that name.trace_id is 16 bytes, span_id is 8. If you sourced them from a traceparent header, run hex2bin() and validate length first — a 32-char string in setTraceId() produces a span the collector drops.RepeatedField objects. Getters return live collections you can iterate; setters accept plain arrays. Do not cast with (array).serializeToString() with application/x-protobuf, or serializeToJsonString() with application/json, never mixed. The HTTP exporter handles this for you — hand-rolling is where people get it wrong.forceFlush() in terminating callbacks or on SIGTERM, or you will lose traces on every deploy.ext-grpc + grpc/grpc and HTTP/2 end-to-end — corporate proxies break it routinely. OTLP/HTTP on port 4318 is the pragmatic default in Laravel deployments.OTEL_LOG_LEVEL=debug, log the raw body and Content-Type, then decode with mergeFromString(). If the collector complains about message size, tune batching or the collector's max_recv_msg_size_mib (default 4 MiB) rather than shrinking spans.open-telemetry/exporter-otlp resolve it so the protobuf encoding always matches the exporter that produces it.How can I help you explore Laravel packages today?