Skip to content

Merge instrumentation declarative config with pyprotobuf HTTP exporter (no-requests)#9

Draft
ocelotl wants to merge 67 commits into
mainfrom
instrumentation_config_and_pyprotobuf_http_exporter
Draft

Merge instrumentation declarative config with pyprotobuf HTTP exporter (no-requests)#9
ocelotl wants to merge 67 commits into
mainfrom
instrumentation_config_and_pyprotobuf_http_exporter

Conversation

@ocelotl

@ocelotl ocelotl commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Combines issue_5361 (instrumentation.python declarative config support) with no-requests (pure-Python protobuf implementation + OTLP pyproto HTTP exporter using urllib instead of requests) into one branch.
  • issue_5361 and no-requests's ancestor (pyproto) independently implemented the same instrumentation-config feature (both closing Declarative configuration: support for the language-specific declarative instrumentation configuration open-telemetry/opentelemetry-python#5361); kept issue_5361's more complete, review-addressed version (_configuration/instrumentation.py) and dropped the earlier draft (_configuration/_instrumentation.py), fixing the resulting duplicate import in _sdk.py.
  • Resolved a cosmetic comment-only conflict in _configuration/_conversion.py.

Test plan

  • configure_sdk / configure_instrumentation import cleanly
  • opentelemetry-sdk/tests/_configuration/test_instrumentation.py — 14/14 pass
  • pyproto-http exporter import failures for Compression confirmed pre-existing on standalone no-requests branch (missing dependency on ad-hoc PYTHONPATH, not caused by this merge)

ocelotl and others added 30 commits June 26, 2026 17:03
Hand-written message classes with SerializeToString() backed by python-protobuf
wire primitives instead of google.protobuf. Covers common, resource, trace, logs,
metrics, and collector service request types — a full equivalent of opentelemetry-proto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors opentelemetry-exporter-otlp-proto-common but imports from
opentelemetry.pyproto.* instead of google.protobuf-backed stubs.
Covers trace, metrics (with OTLPMetricExporterMixin), and log encoding.
Metrics encoding is restructured to use constructive object building
instead of protobuf in-place mutation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…google.protobuf)

Mirrors opentelemetry-exporter-otlp-proto-http but uses pyproto-common encoding.
Includes OTLPSpanExporter, OTLPLogExporter, OTLPMetricExporter with full retry
logic, compression support, and metrics batch splitting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…package

Embeds the python-protobuf wire encoding primitives (varint, tag, scalars,
enum) as opentelemetry.pyproto._pyproto, a private subpackage that cannot
be discovered or depended upon by end-user code on PYTHONPATH.

Removes the python-protobuf external dependency from pyproject.toml and
updates all internal imports (_wire.py, common_pyproto2.py,
metrics_pyproto2.py) to use the new location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
One test file per module:
  - test__varint.py  — boundary values, multi-byte encoding, negative guard
  - test__tag.py     — formula verification, hand-computed literals for all wire types
  - test__scalars.py — hand-computed literals + struct oracle for fixed-width types,
                       ZigZag arithmetic oracle for sint32/sint64, UTF-8 byte-count
                       correctness for encode_string, verbatim pass-through for encode_bytes
  - test__enum.py    — non-negative and negative values, identity with encode_int

Also fixes _varint.py: adds ValueError guard for negative inputs (without it,
the encoding loop would be infinite for any negative value in Python).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Source files (_varint, _tag, _scalars, _enum):
  - Add module docstrings explaining wire types, bit layout, and examples
  - Add function docstrings with worked examples, valid ranges, and spec refs
  - Add line-by-line inline comments explaining each step of the encoding logic
  - Change 'import struct' to 'from struct import pack' and update all call sites
  - Align _scalars implementation with documented behavior: explicit ZigZag
    masking steps in encode_sint32/sint64, encode_int using named variable,
    encode_uint32/uint64 not masking (relying on encode_varint's guard)

Test files:
  - Change 'import math' to 'from math import e, inf, nan, pi, tau'
  - Change 'from struct import pack, unpack' everywhere
  - Move all inline function-body imports to module level
  - Remove unused 'raises' import from test__scalars.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace every `import x` with `from x import y` in all three pyproto
packages (opentelemetry-pyproto, pyproto-common, pyproto-http).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the from-import rule: always use `from x import y`,
never bare `import x`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a full module docstring explaining:
- why the module exists (proto3 field layer above the _pyproto kernel)
- why it is named _wire (protobuf wire format)
- why it lives outside _pyproto (layering: kernel must not know proto3 semantics)
- the four wire-type constants and which proto3 types use each

Expand every function docstring to explain the proto3 default-omission rule,
the on-wire layout, which OTel schema fields use it, and the distinction
between _dbl (omit on 0.0) and _opt_dbl (omit only on None).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename _pyproto/ to _pyprotobuf/
- Remove underscores from all module filenames inside the package:
  _varint.py → varint.py, _tag.py → tag.py,
  _scalars.py → scalars.py, _enum.py → enum.py
- Move _wire.py into _pyprotobuf/ and rename it to fields.py
- Update __init__.py to use relative imports from the new filenames
- fields.py uses relative imports from siblings (scalars, tag, varint)
- Update all message files: _pyproto → _pyprotobuf, _wire → _pyprotobuf.fields
- Rename test files: test__X.py → test_X.py and update their imports
- Update fields.py docstring to reflect new name and location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Function names: msg, string, byt, u64, bool_field, fix32, fix64, dbl,
opt_dbl, sint32, packed_uint64, packed_fix64, packed_double.
Constants: WT_VARINT, WT_64BIT, WT_LEN, WT_32BIT.

_str renamed to string to avoid shadowing the str builtin.
All callers in message files updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests for varint, tag, scalars, and enum are now under
tests/_pyprotobuf/ to mirror the source package structure.
New test_fields.py covers all 13 helpers and 4 wire-type
constants in _pyprotobuf/fields.py (385 tests total pass).

Assisted-by: Claude Sonnet 4.6
The _pypb2 suffix mirrors _pb2 directly — py + pb2 — making the
parallel with the standard protobuf-generated stack immediately
legible. Updated all import sites in the exporter packages.

Assisted-by: Claude Sonnet 4.6
Require a separate commit after every discrete change, keeping each
commit self-contained and the repository in a working state.

Assisted-by: Claude Sonnet 4.6
The rule belongs in opentelemetry-pyproto/AGENTS.md instead,
not in the upstream project's guidance file.

Assisted-by: Claude Sonnet 4.6
Moves the import convention into the standard AGENTS.md location
and adds the rule to commit after every discrete change.

Assisted-by: Claude Sonnet 4.6
Used for oracle tests that verify our pure-Python encoding
produces byte-for-byte identical output to google.protobuf.

Assisted-by: Claude Sonnet 4.6
Each existing test file gains a section that builds a dynamic proto2
message class and asserts byte-for-byte equality between our encoder
and google.protobuf's SerializeToString() output:

  test_varint.py  — encode_varint against a uint64 field
  test_tag.py     — encode_tag prefix across all four wire types and
                    field numbers spanning 1-byte to 4-byte tag varints
  test_enum.py    — encode_enum against a Color enum field (0/1/2)
  test_scalars.py — all 14 scalar encoders, one proto2 field each

503 tests total pass.

Assisted-by: Claude Sonnet 4.6
…es in workspace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lemetry-proto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…emetry-proto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…telemetry-proto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…roto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-proto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…proto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flags is proto field 16 but was written after parent_span_id (field 4).
Proto3 serializes fields in ascending field number order, so flags must
come after status (field 15). The encoder always sets flags to at least
0x100, so every encoded span produced byte-mismatched output vs proto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four classes wrote their high-numbered repeated fields first, violating
proto3's ascending field number order for deterministic serialization:

- Exemplar: filtered_attributes (field 7) written before time (2),
  span_id (4), trace_id (5), as_int (6)
- NumberDataPoint: attributes (field 7) written before time fields (2,3),
  as_double (4), exemplars (5), as_int (6)
- HistogramDataPoint: attributes (field 9) written before all other fields
- SummaryDataPoint: attributes (field 7) written before time fields and
  quantile_values (6)

All now emit fields in ascending field number order, producing identical
bytes to google.protobuf serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds opentelemetry-proto, opentelemetry-exporter-otlp-proto-common,
opentelemetry-test-utils, protobuf, and pytest as dev dependencies so
the package can run tests that compare pyproto and proto encoder output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ocelotl and others added 30 commits June 26, 2026 17:03
…trace_path)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gs_path)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t_metrics_data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed filenames

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows the pyproto exporter packages to work with stable PyPI releases
(e.g. opentelemetry-api 1.42.x) so the demo docker-compose can install
alongside instrumentation packages that require older semantic-conventions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… exporters

- Copy _exporter_metrics.py (no protobuf deps) into pyproto-common
- All three exporters: add meter_provider param, create_exporter_metrics(),
  wrap export loop in self._metrics.export_operation() context manager,
  record result.error / result.error_attrs on failure paths
- Metric exporter: add set_meter_provider() method
- Log exporter: add DuplicateFilter to prevent log spam on repeated failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add FromString classmethod to ExportTrace/Logs/MetricsServiceResponse in pyproto
  (required by gRPC stubs as response_deserializer)
- Add handwritten gRPC stub modules (*_pypb2_grpc.py) for trace, logs, and metrics
  collector services in opentelemetry-pyproto
- Create new exporter/opentelemetry-exporter-otlp-pyproto-grpc package with:
  - OTLPExporterMixin (exporter.py): gRPC export core without google.rpc RetryInfo
  - OTLPSpanExporter, OTLPMetricExporter, OTLPLogExporter backed by pyproto stubs
  - Full retry/reconnect logic, credentials, compression, internal metrics support
  - Entry points for traces, metrics, and logs exporters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 tests covering OTLPExporterMixin (channel creation, env vars, retry
logic, shutdown, compression) and all three signal exporters (trace,
metric, log), including metric batch splitting via max_export_batch_size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benchmarks a generic Record proto message (string, uint64, double, bool,
bytes, fixed64, fixed32, embedded message, packed repeated uint64 and
double) serialised by _pyprotobuf pure-Python helpers vs google.protobuf
C extension.  Includes a correctness assertion that both produce identical
bytes.  Run with: uv run pytest tests/_pyprotobuf/test_benchmark.py -v

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three new benchmark categories to tests/_pyprotobuf/test_benchmark.py:

- Per-field-type (11 field types × 2): each _pyprotobuf helper vs the
  equivalent google.protobuf single-field encode.  Includes a correctness
  parametrize that asserts identical bytes for every field type.

- Scaling (string / bytes / packed at 4 B / 256 B / 16 KB and 10 / 100 /
  1000 elements): measures how encoding time grows with payload size for the
  three field types whose cost is proportional to data length.

- Varint bit-width (1 / 2 / 3 / 5 bytes): benchmarks encode_varint at
  representative values for each continuation-byte count — the hottest
  code path since every tag and every varint-typed value goes through it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…it benchmarks

Three new benchmark categories:

5. Concatenation strategy — test_encode_concat_pyproto vs test_encode_join_pyproto:
   `+` and b"".join() are statistically indistinguishable (~58 µs mean each).
   The intermediate-object overhead of `+` is negligible at this message size.

6. All-default fields — test_all_defaults_pyproto vs test_all_defaults_protobuf:
   Measures the minimum SerializeToString() cost when every helper returns b"".
   pyproto: ~3.9 µs (9 function calls returning b"").
   google.protobuf: ~2.7 µs (empty Record() construction + serialize).

7. Construction vs serialization split — test_encode_protobuf_construct,
   test_encode_protobuf_serialize, and per-field test_field_serialize_only_protobuf:
   Construction alone: ~19.5 µs.  Serialization alone: ~2.1 µs.
   google.protobuf's cost is almost entirely in construction (~90%).
   Fair encoding-only comparison: pyproto ~57 µs vs protobuf serialize ~2 µs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Encodes a Container with 1, 5, and 20 Item sub-messages (each with string,
uint64, and double fields).  Tests whether per-element msg() wrapping cost
compounds linearly.  Includes correctness parametrize asserting identical
bytes at every N.

Results show the gap grows with N: at N=1 pyproto is ~1.5× slower; at
N=20 it is ~2.3× slower.  Cost scales linearly on both sides, but pyproto's
per-element overhead (encode_varint + string.encode + b"".join) accumulates
faster than google.protobuf's C-extension per-element path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g support

Add configure_instrumentation() to activate instrumentors listed under
instrumentation/development.python in a declarative config file. Each
key is resolved via the opentelemetry_instrumentor entry point group and
instrument(**opts) is called with the supplied options. An enabled: false
value suppresses a library without raising; unknown entry points and
instrumentor failures are logged without stopping the others.

Wire configure_instrumentation() into configure_sdk() so the python
section is applied alongside providers on the declarative config path.

Fixes open-telemetry#5361

Assisted-by: Claude Sonnet 4.6
…ass conversion

Schema keys like "otlp_file/development" use "/" as a namespace separator
for development/experimental features. Python dataclass field names cannot
contain "/" so they use "_" (e.g. "otlp_file_development"). Without
normalization, the key falls through to additional_properties and triggers
an entry-point lookup for "otlp_file/development" (which doesn't exist),
rather than using the dedicated _create_otlp_file_development_span_exporter
factory function.
…n no propagators configured

When _propagators is empty the for-loop never runs and context remains
None. Returning None causes callers (e.g. flask instrumentation) to
call context.attach(None), which sets the ContextVar explicitly to None
and corrupts all subsequent get_current() calls.

Return Context() as a safe default instead.
…g support

Add configure_instrumentation() to activate instrumentors listed under
instrumentation/development.python in a declarative config file. Each
key is resolved via the opentelemetry_instrumentor entry point group and
instrument(**opts) is called with the supplied options. An enabled: false
value suppresses a library without raising; unknown entry points and
instrumentor failures are logged without stopping the others.

Wire configure_instrumentation() into configure_sdk() so the python
section is applied alongside providers on the declarative config path.

Fixes open-telemetry#5361

Assisted-by: Claude Sonnet 4.6
…ass conversion

Schema keys like "otlp_file/development" use "/" as a namespace separator
for development/experimental features. Python dataclass field names cannot
contain "/" so they use "_" (e.g. "otlp_file_development"). Without
normalization, the key falls through to additional_properties and triggers
an entry-point lookup for "otlp_file/development" (which doesn't exist),
rather than using the dedicated _create_otlp_file_development_span_exporter
factory function.
If an instrumentor class exposes a config_dataclass attribute, the raw
YAML options are run through _dict_to_dataclass before instrument() is
called. This reuses the same type-coercion pipeline used for SDK
component configuration so instrumentors declare a typed dataclass
schema and the SDK handles the rest.

Instrumentors without config_dataclass continue to receive options
unchanged, preserving backwards compatibility.
- Rename walrus variable `v` to `value` (pylint C0103)
- Add pylint disable=no-self-use at class level in test file (pylint R6301)
- Move `instrumentation` import after `_tracer_provider` in _sdk.py (ruff I001)
- Convert `import logging/dataclasses/unittest` to from-imports
- Apply ruff format to reformatted lines
- Add inspect.isclass + is_dataclass guard before using the
  configuration attribute, so non-dataclass values are silently ignored
- Skip instrument() when the instrumentor is already active to avoid
  duplicate-instrumentation warnings when opentelemetry-instrument and
  declarative config are both in play
- Add Instrumentation section to docs/sdk/configuration.rst with
  instrumentation/development.python YAML example
- Add instrumentation/development example to the declarative-config
  example otel-config.yaml and README
Replace requests.Session-based transport with stdlib urllib.request/
urllib.error across the trace, log and metric HTTP exporters, sharing
a small _post/_build_ssl_context helper in _common.py. The credential-
provider entry point's requests.Session type check is commented out
(rather than removed) since there is no longer a requests.Session type
to validate against; the session= constructor param and its wiring are
dropped since there is no longer a Session object for it to populate.
…_http_exporter

# Conflicts:
#	opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_conversion.py
#	opentelemetry-sdk/tests/_configuration/test_instrumentation.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant