Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
4c0ab68
Add opentelemetry-pyproto: pure-Python OTel protobuf message classes
ocelotl Jun 22, 2026
602d86e
Add opentelemetry-exporter-otlp-pyproto-common: SDK-to-pyproto encoding
ocelotl Jun 22, 2026
882978d
Add opentelemetry-exporter-otlp-pyproto-http: HTTP OTLP exporter (no …
ocelotl Jun 22, 2026
eb72795
Move wire encoding into opentelemetry-pyproto as private _pyproto sub…
ocelotl Jun 22, 2026
fc4988d
Add tests for _pyproto wire encoding primitives (223 tests, all passing)
ocelotl Jun 22, 2026
d325dc6
Add documentation and fix import style in _pyproto and its tests
ocelotl Jun 22, 2026
10cc06c
Convert all bare imports to from-import style across pyproto packages
ocelotl Jun 22, 2026
ecfecc3
Add CONVENTIONS.md to opentelemetry-pyproto
ocelotl Jun 22, 2026
903d2b0
Document _wire.py thoroughly
ocelotl Jun 22, 2026
e4aa800
Rename _pyproto → _pyprotobuf and _wire.py → _pyprotobuf/fields.py
ocelotl Jun 22, 2026
3066994
Remove underscore prefix from all names inside _pyprotobuf/fields.py
ocelotl Jun 22, 2026
6836abc
Move _pyprotobuf tests into subdirectory and add test_fields.py
ocelotl Jun 22, 2026
9765ad4
Rename *_pyproto2.py → *_pypb2.py throughout
ocelotl Jun 22, 2026
2e82cff
Add commit cadence rule to AGENTS.md
ocelotl Jun 22, 2026
0876a68
Revert commit cadence rule from upstream AGENTS.md
ocelotl Jun 22, 2026
13bba25
Rename CONVENTIONS.md → AGENTS.md and add commit cadence rule
ocelotl Jun 22, 2026
307bb9e
Add protobuf as dev test dependency to opentelemetry-pyproto
ocelotl Jun 22, 2026
c23f9ed
Add google.protobuf oracle tests to _pyprotobuf test suite
ocelotl Jun 22, 2026
8a73719
Add opentelemetry-proto as dev dependency and register pyproto packag…
ocelotl Jun 22, 2026
8d3214b
Add tests/collector/trace/v1 oracle tests comparing pyproto vs opente…
ocelotl Jun 22, 2026
bf9ceb3
Add tests/collector/logs/v1 oracle tests comparing pyproto vs opentel…
ocelotl Jun 22, 2026
3b0fada
Add tests/collector/metrics/v1 oracle tests comparing pyproto vs open…
ocelotl Jun 22, 2026
d317144
Add tests/common/v1 oracle tests comparing pyproto vs opentelemetry-p…
ocelotl Jun 22, 2026
9bf3d00
Add tests/resource/v1 oracle tests comparing pyproto vs opentelemetry…
ocelotl Jun 22, 2026
3a21c1c
Add tests/logs/v1 oracle tests comparing pyproto vs opentelemetry-proto
ocelotl Jun 22, 2026
7498859
Add tests/trace/v1 oracle tests comparing pyproto vs opentelemetry-proto
ocelotl Jun 22, 2026
d071fcc
Add tests/metrics/v1 oracle tests comparing pyproto vs opentelemetry-…
ocelotl Jun 22, 2026
dfa52ea
Fix Span.flags field ordering in trace_pypb2.py
ocelotl Jun 22, 2026
96f91cd
Fix field ordering in metrics_pypb2.py
ocelotl Jun 22, 2026
7caba7e
Add dev dependency group to opentelemetry-exporter-otlp-pyproto-common
ocelotl Jun 22, 2026
ae80e1b
Add tests for pyproto-common _internal helpers
ocelotl Jun 22, 2026
642f19b
Add tests for pyproto-common trace encoder
ocelotl Jun 22, 2026
5b3808f
Add tests for pyproto-common log encoder
ocelotl Jun 22, 2026
6eb7277
Add tests for pyproto-common metrics encoder
ocelotl Jun 22, 2026
8e78a80
Add dev dependency group to pyproto-http for testing
ocelotl Jun 22, 2026
3c00c17
Add tests for pyproto/http __init__ (Compression, headers)
ocelotl Jun 22, 2026
509d244
Add tests for pyproto/http/_common (_is_retryable, _load_session_from…
ocelotl Jun 22, 2026
1ee2160
Add tests for pyproto/http/trace_exporter (OTLPSpanExporter, _append_…
ocelotl Jun 22, 2026
ad33341
Add tests for pyproto/http/_log_exporter (OTLPLogExporter, _append_lo…
ocelotl Jun 22, 2026
1ce7900
Add tests for pyproto/http/metric_exporter (OTLPMetricExporter, _spli…
ocelotl Jun 22, 2026
e8e29ab
Add __init__.py files to test dirs to fix pytest collection with shar…
ocelotl Jun 22, 2026
e85e52f
Relax api/sdk version constraints to ~= 1.12 for pyproto exporters
ocelotl Jun 22, 2026
77ec736
Add exporter self-instrumentation and DuplicateFilter to pyproto-http…
ocelotl Jun 22, 2026
0596ec8
Add opentelemetry-exporter-otlp-pyproto-grpc package
ocelotl Jun 22, 2026
59d87ff
Add tests for opentelemetry-exporter-otlp-pyproto-grpc
ocelotl Jun 22, 2026
b3584b3
Update uv.lock for opentelemetry-exporter-otlp-pyproto-grpc
ocelotl Jun 22, 2026
28b6ecb
Add benchmark tests: pyproto vs google.protobuf encoding speed
ocelotl Jun 26, 2026
7768890
Expand benchmark suite: per-field, scaling, and varint tests
ocelotl Jun 26, 2026
59b34b9
Add concat strategy, all-defaults, and construction/serialization spl…
ocelotl Jun 26, 2026
77de396
Add repeated embedded message benchmark (section 8)
ocelotl Jun 26, 2026
527cc62
opentelemetry-sdk: implement instrumentation.python declarative confi…
ocelotl Jun 26, 2026
4f92a8f
opentelemetry-sdk: normalize slash in YAML keys during dict-to-datacl…
ocelotl Jun 26, 2026
4ab7172
opentelemetry-api: fix CompositePropagator.extract returning None whe…
ocelotl Jun 27, 2026
5f72ea5
opentelemetry-sdk: implement instrumentation.python declarative confi…
ocelotl Jun 26, 2026
8b37ec5
opentelemetry-sdk: normalize slash in YAML keys during dict-to-datacl…
ocelotl Jun 26, 2026
58488d2
opentelemetry-sdk: validate instrumentation opts via config_dataclass
ocelotl Jun 29, 2026
bc14db0
refactor: inline _coerce_opts into configure_instrumentation
ocelotl Jun 30, 2026
a2a88df
refactor: rename _instrumentation to instrumentation, use from-import…
ocelotl Jun 30, 2026
5d54326
changelog: add fragment for PR 5372
ocelotl Jun 30, 2026
08b992f
fix(ci): fix pylint and ruff failures in instrumentation files
ocelotl Jun 30, 2026
72d74b7
fix(ci): move pylint disable inside class body so it applies to methods
ocelotl Jun 30, 2026
d58e2f1
refactor: rename config_dataclass attribute to configuration
ocelotl Jun 30, 2026
76ab381
ci: retrigger
ocelotl Jun 30, 2026
b58e602
fix: address review comments on instrumentation declarative config
ocelotl Jul 3, 2026
1d6fa79
fix: use from inspect import isclass instead of import inspect
ocelotl Jul 3, 2026
9b9fcd6
Merge branch 'pyproto' into instrumentation_config_and_pyprotobuf_htt…
ocelotl Jul 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5372.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: Add support for activating instrumentors from a declarative configuration file via the `instrumentation/development.python` section. Instrumentors can declare a `configuration` attribute to have their options validated through the same type-coercion pipeline used for SDK component configuration.
12 changes: 8 additions & 4 deletions docs/examples/declarative-config/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ The source files of this example are available :scm_web:`here
<docs/examples/declarative-config/>`.

Install the SDK with the ``file-configuration`` extra (it pulls in ``pyyaml``
and ``jsonschema``), the auto-instrumentation entry point, and the OTLP/HTTP
exporter:
and ``jsonschema``), the auto-instrumentation entry point, the OTLP/HTTP
exporter, and the ``requests`` instrumentation used by this example:

.. code-block:: sh

pip install "opentelemetry-sdk[file-configuration]" \
opentelemetry-distro \
opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-http \
opentelemetry-instrumentation-requests

Start an OTLP-capable backend locally so there is somewhere to send data. Write
the following file:
Expand Down Expand Up @@ -74,7 +75,10 @@ auto-instrumentation apply it. No configuration code lives in ``app.py``:
export OTEL_CONFIG_FILE=$(pwd)/otel-config.yaml
opentelemetry-instrument python app.py

You should see the exported span in the Collector's debug output.
You should see the exported span in the Collector's debug output. The
``instrumentation/development.python`` section in ``otel-config.yaml``
activates the ``requests`` instrumentation so outgoing HTTP calls made by
``app.py`` are automatically traced without any code changes.

Environment variable substitution
----------------------------------
Expand Down
5 changes: 5 additions & 0 deletions docs/examples/declarative-config/otel-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ logger_provider:
exporter:
otlp_http:
endpoint: http://localhost:4318/v1/logs

instrumentation/development:
python:
requests:
enabled: true
25 changes: 25 additions & 0 deletions docs/sdk/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,31 @@ OTLP/HTTP. The source is available :scm_web:`here
- name: api-key
value: ${OTLP_API_KEY}

Instrumentation
---------------

The ``instrumentation/development.python`` section activates Python
instrumentors by their ``opentelemetry_instrumentor`` entry-point name. Set
``enabled: false`` to suppress an instrumentor without removing its entry, and
pass any other keys as keyword arguments to ``instrument()``:

.. code-block:: yaml

instrumentation/development:
python:
requests:
enabled: true
urllib3:
enabled: true
max_spans_per_request: 10

If the instrumentor class declares a ``configuration`` class attribute pointing
to a dataclass, the options are validated and type-coerced through the same
pipeline used for SDK component configuration before being forwarded to
``instrument()``. Instrumentors that are already active (for example because
``opentelemetry-instrument`` ran before the file was applied) are silently
skipped.

Environment variable substitution
----------------------------------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
OpenTelemetry OTLP Exporter (pure-Python protobuf, common)
===========================================================

Common encoding utilities for the pure-Python OTLP exporter stack.
39 changes: 39 additions & 0 deletions exporter/opentelemetry-exporter-otlp-pyproto-common/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "opentelemetry-exporter-otlp-pyproto-common"
dynamic = ["version"]
description = "OpenTelemetry OTLP encoding using pure-Python protobuf (no google.protobuf)"
readme = "README.rst"
license = "Apache-2.0"
requires-python = ">=3.10"
dependencies = [
"opentelemetry-api ~= 1.12",
"opentelemetry-sdk ~= 1.12",
"opentelemetry-pyproto == 1.43.0.dev",
]

[dependency-groups]
dev = [
"opentelemetry-proto == 1.43.0.dev",
"opentelemetry-exporter-otlp-proto-common == 1.43.0.dev",
"opentelemetry-test-utils == 1.43.0.dev",
"protobuf>=5.0",
"pytest>=7.0",
]

[tool.uv.sources]
opentelemetry-proto = { workspace = true }
opentelemetry-exporter-otlp-proto-common = { workspace = true }
opentelemetry-test-utils = { workspace = true }

[tool.hatch.version]
path = "src/opentelemetry/exporter/otlp/pyproto/common/version/__init__.py"

[tool.hatch.build.targets.sdist]
include = ["/src"]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from collections import Counter
from collections.abc import Iterator
from contextlib import AbstractContextManager, contextmanager
from dataclasses import dataclass
from time import perf_counter
from typing import TYPE_CHECKING, Protocol

from opentelemetry.metrics import MeterProvider, get_meter_provider
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
OTEL_COMPONENT_NAME,
OTEL_COMPONENT_TYPE,
OtelComponentTypeValues,
)
from opentelemetry.semconv._incubating.metrics.otel_metrics import (
create_otel_sdk_exporter_log_exported,
create_otel_sdk_exporter_log_inflight,
create_otel_sdk_exporter_metric_data_point_exported,
create_otel_sdk_exporter_metric_data_point_inflight,
create_otel_sdk_exporter_operation_duration,
create_otel_sdk_exporter_span_exported,
create_otel_sdk_exporter_span_inflight,
)
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
from opentelemetry.semconv.attributes.server_attributes import (
SERVER_ADDRESS,
SERVER_PORT,
)

if TYPE_CHECKING:
from typing import Literal
from urllib.parse import ParseResult as UrlParseResult

from opentelemetry.util.types import Attributes, AttributeValue

_component_counter = Counter()


@dataclass
class ExportResult:
error: Exception | None = None
error_attrs: Attributes = None


class ExporterMetricsT(Protocol):
def export_operation(
self, num_items: int
) -> AbstractContextManager[ExportResult]: ...


class NoOpExporterMetrics:
@contextmanager
def export_operation(self, num_items: int) -> Iterator[ExportResult]:
yield ExportResult()


class ExporterMetrics:
def __init__(
self,
component_type: OtelComponentTypeValues | None,
signal: Literal["traces", "metrics", "logs"],
endpoint: UrlParseResult,
meter_provider: MeterProvider | None,
) -> None:
if signal == "traces":
create_exported = create_otel_sdk_exporter_span_exported
create_inflight = create_otel_sdk_exporter_span_inflight
elif signal == "logs":
create_exported = create_otel_sdk_exporter_log_exported
create_inflight = create_otel_sdk_exporter_log_inflight
else:
create_exported = (
create_otel_sdk_exporter_metric_data_point_exported
)
create_inflight = (
create_otel_sdk_exporter_metric_data_point_inflight
)

port = endpoint.port
if port is None:
if endpoint.scheme == "https":
port = 443
elif endpoint.scheme == "http":
port = 80

component_type_value = (
component_type.value if component_type else "unknown_otlp_exporter"
)
count = _component_counter[component_type_value]
_component_counter[component_type_value] = count + 1
self._standard_attrs: dict[str, AttributeValue] = {
OTEL_COMPONENT_TYPE: component_type_value,
OTEL_COMPONENT_NAME: f"{component_type_value}/{count}",
}
if endpoint.hostname:
self._standard_attrs[SERVER_ADDRESS] = endpoint.hostname
if port is not None:
self._standard_attrs[SERVER_PORT] = port

meter_provider = meter_provider or get_meter_provider()
meter = meter_provider.get_meter("opentelemetry-sdk")
self._inflight = create_inflight(meter)
self._exported = create_exported(meter)
self._duration = create_otel_sdk_exporter_operation_duration(meter)

@contextmanager
def export_operation(self, num_items: int) -> Iterator[ExportResult]:
start_time = perf_counter()
self._inflight.add(num_items, self._standard_attrs)

result = ExportResult()
try:
yield result
finally:
error = result.error
error_attrs = result.error_attrs

end_time = perf_counter()
self._inflight.add(-num_items, self._standard_attrs)
exported_attrs = (
{**self._standard_attrs, ERROR_TYPE: type(error).__qualname__}
if error
else self._standard_attrs
)
self._exported.add(num_items, exported_attrs)
duration_attrs = (
{**exported_attrs, **error_attrs}
if error_attrs
else exported_attrs
)
self._duration.record(end_time - start_time, duration_attrs)


def create_exporter_metrics(
component_type: OtelComponentTypeValues | None,
signal: Literal["traces", "metrics", "logs"],
endpoint: UrlParseResult,
meter_provider: MeterProvider | None,
enabled: bool,
) -> ExporterMetricsT:
if not enabled:
return NoOpExporterMetrics()

return ExporterMetrics(
component_type,
signal,
endpoint,
meter_provider,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from logging import getLogger
from collections.abc import Callable, Mapping, Sequence
from typing import Any, TypeVar

from opentelemetry.pyproto.common.v1.common_pypb2 import (
AnyValue,
ArrayValue,
InstrumentationScope as PB2InstrumentationScope,
KeyValue,
KeyValueList,
)
from opentelemetry.pyproto.resource.v1.resource_pypb2 import Resource as PB2Resource
from opentelemetry.sdk.trace import Resource
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.util.types import _ExtendedAttributes

_logger = getLogger(__name__)

_TypingResourceT = TypeVar("_TypingResourceT")
_ResourceDataT = TypeVar("_ResourceDataT")


def _encode_instrumentation_scope(
instrumentation_scope: InstrumentationScope,
) -> PB2InstrumentationScope:
if instrumentation_scope is None:
return PB2InstrumentationScope()
return PB2InstrumentationScope(
name=instrumentation_scope.name,
version=instrumentation_scope.version,
attributes=_encode_attributes(instrumentation_scope.attributes),
)


def _encode_resource(resource: Resource) -> PB2Resource:
return PB2Resource(attributes=_encode_attributes(resource.attributes))


def _encode_value(value: Any) -> AnyValue:
if value is None:
return AnyValue()
if isinstance(value, bool):
return AnyValue(bool_value=value)
if isinstance(value, str):
return AnyValue(string_value=value)
if isinstance(value, int):
return AnyValue(int_value=value)
if isinstance(value, float):
return AnyValue(double_value=value)
if isinstance(value, bytes):
return AnyValue(bytes_value=value)
if isinstance(value, Sequence):
return AnyValue(
array_value=ArrayValue(values=[_encode_value(v) for v in value])
)
if isinstance(value, Mapping):
return AnyValue(
kvlist_value=KeyValueList(
values=[_encode_key_value(str(k), v) for k, v in value.items()]
)
)
raise Exception(f"Invalid type {type(value)} of value {value}")


def _encode_key_value(key: str, value: Any) -> KeyValue:
return KeyValue(key=key, value=_encode_value(value))


def _encode_span_id(span_id: int) -> bytes:
return span_id.to_bytes(length=8, byteorder="big", signed=False)


def _encode_trace_id(trace_id: int) -> bytes:
return trace_id.to_bytes(length=16, byteorder="big", signed=False)


def _encode_attributes(
attributes: _ExtendedAttributes | None,
) -> list[KeyValue]:
if not attributes:
return []
pb2_attributes = []
for key, value in attributes.items():
try:
pb2_attributes.append(_encode_key_value(key, value))
except Exception as error:
_logger.exception("Failed to encode key %s: %s", key, error)
return pb2_attributes


def _get_resource_data(
sdk_resource_scope_data: dict[Resource, _ResourceDataT],
resource_class: Callable[..., _TypingResourceT],
name: str,
) -> list[_TypingResourceT]:
resource_data = []
for sdk_resource, scope_data in sdk_resource_scope_data.items():
collector_resource = PB2Resource(
attributes=_encode_attributes(sdk_resource.attributes)
)
resource_data.append(
resource_class(
**{
"resource": collector_resource,
f"scope_{name}": scope_data.values(),
}
)
)
return resource_data
Loading
Loading