Skip to content

Pure-Python opentelemetry-proto (drop the protobuf dependency) [1/4 RFC] - #5503

Draft
ocelotl wants to merge 21 commits into
open-telemetry:mainfrom
ocelotl:pure-python-otlp-1-proto
Draft

ocelotl wants to merge 21 commits into
open-telemetry:mainfrom
ocelotl:pure-python-otlp-1-proto

Conversation

@ocelotl

@ocelotl ocelotl commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

What

Replace the google.protobuf-generated message classes in opentelemetry-proto with a hand-written pure-Python protobuf implementation, and drop the protobuf dependency (protobuf>=5.0, <8.0) entirely.

The public opentelemetry.proto.* import namespace is preserved: real code lives under a private opentelemetry._proto package, and the existing opentelemetry.proto.* modules become thin re-export shims (from opentelemetry._proto... import *). Downstream code importing opentelemetry.proto.trace.v1.trace_pb2 etc. keeps working unchanged.

Why

protobuf ships a compiled C extension (upb) and enforces a narrow, moving version range. That causes real friction:

  • Injection / zero-code instrumentation conflicts — when the SDK is injected into an arbitrary target process (K8s operator/injector), the bundled protobuf can collide with the version the target app pins, producing hard-to-diagnose ABI/version errors. A pure-Python encoder is safe to inject.
  • Dependency-resolution friction — the >=5.0,<8.0 range regularly clashes with other libraries (gRPC stacks, ML tooling, cloud SDKs).
  • Wheel/ABI portability — no compiled component means it runs anywhere CPython runs.

The OTLP exporters only ever serialize protobuf; they never parse arbitrary messages. The only decode needed is the export-service response, which is empty. So a small, auditable encode-only implementation is sufficient — this is not a general-purpose protobuf runtime.

Scope of this PR

This is the base of a 4-PR stacked series that makes the whole OTLP export path dependency-light. Merge order:

  1. opentelemetry-proto → pure-Python (this PR)
  2. opentelemetry-exporter-otlp-proto-common → pure-Python backend
  3. opentelemetry-exporter-otlp-proto-http → pure-Python + stdlib urllib (drops requests)
  4. opentelemetry-exporter-otlp-proto-grpc → pure-Python gRPC (drops grpcio)

Because this org's fork model can't host intermediate base branches upstream, each PR targets main and its diff is cumulative (this PR's changes appear in all four). Isolated per-package diffs are viewable via the fork compare links noted in PRs 2–4.

Known gaps / discussion points (why this is a draft)

  • profiles signal not ported. The pure-Python impl currently covers common, resource, trace, metrics, logs and the collector service messages, but not opentelemetry.proto.profiles. That namespace is removed here and would need to be added before this could replace the current package for profiles users.
  • Codegen vs hand-maintenance. The message classes should ideally be generated from the same .proto sources the project vendors, so schema changes stay mechanical. This PR carries a hand-written implementation; agreeing on a generator is part of the discussion.
  • Structure. The _proto (real) + proto (shim) split is one option; collapsing into proto directly is another. Happy to go either way.
  • Conformance. Output should be byte-for-byte identical to the protobuf-generated serialization; a differential test against the current encoders would be worth adding.
  • CI will not be green as-is (tests and docs need adaptation) — this is intentionally an RFC to gauge interest in the approach.

The reference implementation this is derived from has been running in a downstream distribution.


Stack (merge in order): #5503 (proto) → #5504 (common) → #5505 (http) → #5506 (grpc)

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-10-17 02:49 UTC

Move out of draft to request review.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@ocelotl
ocelotl force-pushed the pure-python-otlp-1-proto branch from 9989274 to 1da6e50 Compare August 20, 2026 13:21
@tammy-baylis-swi
tammy-baylis-swi marked this pull request as draft August 20, 2026 16:07
@ocelotl
ocelotl force-pushed the pure-python-otlp-1-proto branch 2 times, most recently from a6885cc to 6cd3ff2 Compare September 4, 2026 19:04
@ocelotl

ocelotl commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

WIP

@herin049

herin049 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Hey @ocelotl, thanks for this -- I also like the idea of having a dependency-less protobuf/gRPC exporter. I have a few questions about the overall approach:

  1. Is there a reason this can't be a separate package? Consumers that are fragile to additional dependencies can opt-in to utilizing this new package instead of the protobuf one, instead of forcing it on all consumers.
  2. Can you include some more detailed information on the performance overhead this introduces compared to the native upb backend? I recall that you mention that the overhead is very significant when directly comparing serialization time, but is less significant in the context of the total export duration. While the latter point is a valid argument, I would still like to see how much of an increase in memory/CPU this would introduce in the context of the entire OTel Python SDK. For example if the relative memory/CPU increase across the SDK as a whole with these changes is <5%, it might be acceptable, but if it were to double the memory/CPU overhead of the SDK as a whole, I would personally see that as unacceptable.
  3. I know I have raised this before, but have we considered using the Rust prost crate to create a native, dependency-less wheel to avoid the protobuf dependency. This would avoid the manual generation of message types, would eliminate the need to implement protobuf from scratch and would eliminate the performance downsides.
  4. If we were to proceed with some variation of this approach, we would need to implement a protoc plugin to automatically generate all message types (i.e. similar to what is done for JSON) -- manually modifying the message types every time the protocol is updated isn't acceptable in my eyes.

@xrmx

xrmx commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Another option would be to try protobuf-py. I've played a bit in converting the contrib opamp client to protobuf-py (python + rust) open-telemetry/opentelemetry-python-contrib#5006

@aabmass

aabmass commented Sep 10, 2026

Copy link
Copy Markdown
Member

protobuf ships a compiled C extension (upb) and enforces a narrow, moving version range. That causes real friction:

https://protobuf.dev/support/cross-version-runtime-guarantee/ the range shouldn't be too narrow

@herin049

herin049 commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Another option would be to try protobuf-py. I've played a bit in converting the contrib opamp client to protobuf-py (python + rust) open-telemetry/opentelemetry-python-contrib#5006

I think the solution we're trying to implement here is to have an installable exporter that can never introduce dependency conflicts with user provided packages. Switching to protobuf-py would still leave possibilities of dependency conflicts (e.g. if user packages depend on different versions of protobuf-py) unless we vendored it directly into the exporters.

Having a Python native protobuf implementation is somewhat irrelevant here.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

This PR has been automatically marked as stale because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 days of this comment.
If you're still working on this, please add a comment or push new commits.

@github-actions github-actions Bot added the Stale label Oct 3, 2026
ocelotl and others added 9 commits October 20, 2026 16:49
Swap the google.protobuf-generated message classes for hand-written pure-Python
encoders under opentelemetry._proto, keeping the public opentelemetry.proto.*
import namespace via thin re-export shims. Removes the protobuf (and native upb)
dependency entirely; only the serialize path used by the OTLP exporters is
implemented (plus the empty export-service response decode).
Restore the parts of the protobuf message API that the unchanged
otlp-proto-common/http/grpc components rely on, so replacing the
generated classes is transparent to them:

- add the top-level SpanFlags enum and nested Span.SpanKind /
  Status.StatusCode enums, with values also lifted onto the parent
  message class as protobuf does
- add a Message base with value equality (by serialized bytes, matching
  proto3 default omission), repr, SerializePartialToString and FromString
- support the metrics encoder's mutable API: as_int/as_double oneof
  setters on NumberDataPoint and Exemplar (with sfixed64 range checking),
  and auto-vivified gauge/sum/histogram/exponential_histogram/summary
  fields on Metric
- accept a mapping for the Span.status message field
- add the gRPC service Servicer and add_*Servicer_to_server helpers
  alongside the existing client Stub

opentelemetry-proto (668 tests, including the byte-for-byte differential
against protobuf) and otlp-proto-common (20 tests) pass.
A protoc plugin that generates the pure-Python, encode-only protobuf-wire
message classes from the .proto files, so the hand-written classes in
opentelemetry._proto can be generated instead of maintained by hand.

Mirrors the architecture of opentelemetry-codegen-json: a plugin entry
point (protoc-gen-pyproto / --pyproto_out), a generic CodeWriter, a
wire-specific type table, and a descriptor-walking generator. Output goes
to opentelemetry/_proto/<signal>/v1/<name>_pb2.py and imports the existing
_pyprotobuf runtime.

Proven for common/v1 and resource/v1: generated classes serialize
byte-identically to both the hand-written classes and real protobuf across
21 constructed messages. Other signals, the mutable API (Metric
auto-vivification, as_int/as_double setters), packed fields, maps, and
gRPC service stubs remain to be covered.
The pure-Python proto classes only serialize. Tests that decode OTLP bytes
compared the public classes to themselves, so they proved nothing. Generate a
real protobuf reference set under opentelemetry.proto._test (gated by the new
opentelemetry-proto[test] extra) and point the equivalence tests at it, so they
now compare the pure-Python implementation against real protobuf. The conftest
guard asserts the two sides are distinct implementations. Add
scripts/proto_codegen_test.sh to regenerate the reference set.
The http span, log, and metrics test suites decode the exported OTLP bytes to
assert on them. The pure-Python classes only serialize, so decode via the
protobuf-backed opentelemetry.proto._test classes and compare against the same
representation. The metrics batch-splitting unit tests keep the pure-Python
classes, since _split_metrics_data produces them; give the pure-Python KeyValue
and ResourceMetrics the same dict coercion protobuf constructors do so those
tests build messages the same way.
Treat a None scalar as the proto3 default so serialization omits it, matching
protobuf: the numeric field encoders omitted only an exact zero, so a None
timestamp (from a span with no start time) raised in struct.pack and the grpc
client reported 'Exception serializing request!'. Decode the log request with
the protobuf-backed reference class before MessageToDict, which needs a real
DESCRIPTOR.
Extend the opentelemetry-codegen-pyproto plugin to emit every signal completely
and replace the hand-written message modules with its output. The generator now
emits IntEnum enum classes with a proper import, drops the builtins-qualified
annotations, lifts top-level enum values to module scope, exposes the
global___<Name> aliases, and generates the mutable oneof API: auto-vivifying
message-oneof properties, scalar-oneof setters that clear siblings and
range-check integer widths, with SerializeToString reading the backing fields.
The generated classes serialize byte-for-byte identically to real protobuf (the
equivalence suite) and are a drop-in for the exporters. Add
scripts/proto_codegen_pyproto.sh to regenerate them.

This also adds the message types and enums the hand-written set omitted
(TracesData, MetricsData, EntityRef, AggregationTemporality, DataPointFlags).
Add the SPDX header to the hand-written pure-Python proto modules and their
tests. Add the 5503 changelog fragment. Let codespell accept the 'Nd' in the
struct format string in the field encoders.
The pure-Python vs protobuf benchmarks used the pytest-benchmark fixture but
lived under tests/, so the normal test env collected them and errored without
that plugin. Move them to opentelemetry-proto/benchmarks with a
benchmark-requirements.txt and a dedicated tox env, matching the other
packages.
@ocelotl
ocelotl force-pushed the pure-python-otlp-1-proto branch from 6cd3ff2 to abbba5e Compare October 20, 2026 22:50
@github-actions github-actions Bot removed the Stale label Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants