diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml index 85226173d58..0b94086be06 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml @@ -34,7 +34,7 @@ Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exp Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] -path = "src/opentelemetry/exporter/otlp/proto/common/version/__init__.py" +path = "src/opentelemetry/exporter/otlp/_proto/common/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-common/tests/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_exporter_metrics.py new file mode 100644 index 00000000000..6b3d1d76fa3 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_exporter_metrics.py @@ -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, + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/__init__.py new file mode 100644 index 00000000000..e91e6115425 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/__init__.py @@ -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._proto.common.v1.common_pb2 import ( + AnyValue, + ArrayValue, + InstrumentationScope as PB2InstrumentationScope, + KeyValue, + KeyValueList, +) +from opentelemetry._proto.resource.v1.resource_pb2 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 diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/_log_encoder/__init__.py new file mode 100644 index 00000000000..1a1777092b6 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/_log_encoder/__init__.py @@ -0,0 +1,87 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +from collections import defaultdict +from collections.abc import Sequence + +from opentelemetry.exporter.otlp._proto.common._internal import ( + _encode_attributes, + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, +) +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry._proto.logs.v1.logs_pb2 import ( + LogRecord, + ResourceLogs, + ScopeLogs, +) +from opentelemetry.sdk._logs import ReadableLogRecord + + +def encode_logs(batch: Sequence[ReadableLogRecord]) -> ExportLogsServiceRequest: + return ExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) + + +def _encode_log(readable_log_record: ReadableLogRecord) -> LogRecord: + log = readable_log_record.log_record + span_id = ( + b"" + if log.span_id == 0 + else _encode_span_id(log.span_id) + ) + trace_id = ( + b"" + if log.trace_id == 0 + else _encode_trace_id(log.trace_id) + ) + return LogRecord( + time_unix_nano=log.timestamp, + observed_time_unix_nano=log.observed_timestamp, + span_id=span_id, + trace_id=trace_id, + flags=int(log.trace_flags), + body=_encode_value(log.body), + severity_text=log.severity_text or "", + attributes=_encode_attributes(log.attributes), + dropped_attributes_count=readable_log_record.dropped_attributes, + severity_number=getattr(log.severity_number, "value", 0) or 0, + event_name=log.event_name or "", + ) + + +def _encode_resource_logs( + batch: Sequence[ReadableLogRecord], +) -> list[ResourceLogs]: + sdk_resource_logs: dict = defaultdict(lambda: defaultdict(list)) + + for readable_log in batch: + sdk_resource = readable_log.resource + sdk_instrumentation = readable_log.instrumentation_scope or None + pb2_log = _encode_log(readable_log) + sdk_resource_logs[sdk_resource][sdk_instrumentation].append(pb2_log) + + pb2_resource_logs = [] + for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): + scope_logs = [] + for sdk_instrumentation, pb2_logs in sdk_instrumentations.items(): + scope_logs.append( + ScopeLogs( + scope=_encode_instrumentation_scope(sdk_instrumentation), + log_records=pb2_logs, + schema_url=sdk_instrumentation.schema_url + if sdk_instrumentation + else "", + ) + ) + pb2_resource_logs.append( + ResourceLogs( + resource=_encode_resource(sdk_resource), + scope_logs=scope_logs, + schema_url=sdk_resource.schema_url, + ) + ) + return pb2_resource_logs diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/metrics_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/metrics_encoder/__init__.py new file mode 100644 index 00000000000..e299e7dde0f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/metrics_encoder/__init__.py @@ -0,0 +1,334 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +from logging import getLogger +from os import environ + +from opentelemetry.exporter.otlp._proto.common._internal import ( + _encode_attributes, + _encode_instrumentation_scope, + _encode_span_id, + _encode_trace_id, +) +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry._proto.metrics.v1.metrics_pb2 import ( + Exemplar, + ExponentialHistogram, + ExponentialHistogramDataPoint, + Gauge, + Histogram, + HistogramDataPoint, + Metric, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, + Sum, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource as PB2Resource +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, +) +from opentelemetry.sdk.metrics import ( + Counter, + Exemplar as SDKExemplar, + Histogram as SDKHistogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Gauge as SDKGauge, + MetricExporter, + MetricsData, + Sum as SDKSum, +) +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.sdk.metrics.export import ( + Histogram as HistogramType, +) +from opentelemetry.sdk.metrics.view import ( + Aggregation, + ExplicitBucketHistogramAggregation, + ExponentialBucketHistogramAggregation, +) + +_logger = getLogger(__name__) + + +class OTLPMetricExporterMixin: + def _common_configuration( + self, + preferred_temporality: dict[type, AggregationTemporality] | None = None, + preferred_aggregation: dict[type, Aggregation] | None = None, + ) -> None: + MetricExporter.__init__( + self, + preferred_temporality=self._get_temporality(preferred_temporality), + preferred_aggregation=self._get_aggregation(preferred_aggregation), + ) + + def _get_temporality( + self, preferred_temporality: dict[type, AggregationTemporality] + ) -> dict[type, AggregationTemporality]: + otel_exporter_otlp_metrics_temporality_preference = ( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "CUMULATIVE", + ) + .upper() + .strip() + ) + + if otel_exporter_otlp_metrics_temporality_preference == "DELTA": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + SDKHistogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.DELTA, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": + instrument_class_temporality = { + Counter: AggregationTemporality.DELTA, + UpDownCounter: AggregationTemporality.CUMULATIVE, + SDKHistogram: AggregationTemporality.DELTA, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + else: + if otel_exporter_otlp_metrics_temporality_preference != "CUMULATIVE": + _logger.warning( + "Unrecognized OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" + " value found: %s, using CUMULATIVE", + otel_exporter_otlp_metrics_temporality_preference, + ) + instrument_class_temporality = { + Counter: AggregationTemporality.CUMULATIVE, + UpDownCounter: AggregationTemporality.CUMULATIVE, + SDKHistogram: AggregationTemporality.CUMULATIVE, + ObservableCounter: AggregationTemporality.CUMULATIVE, + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, + ObservableGauge: AggregationTemporality.CUMULATIVE, + } + + instrument_class_temporality.update(preferred_temporality or {}) + return instrument_class_temporality + + def _get_aggregation( + self, preferred_aggregation: dict[type, Aggregation] + ) -> dict[type, Aggregation]: + otel_exporter_otlp_metrics_default_histogram_aggregation = environ.get( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "explicit_bucket_histogram", + ) + + if otel_exporter_otlp_metrics_default_histogram_aggregation == ( + "base2_exponential_bucket_histogram" + ): + instrument_class_aggregation: dict[type, Aggregation] = { + SDKHistogram: ExponentialBucketHistogramAggregation(), + } + else: + if otel_exporter_otlp_metrics_default_histogram_aggregation != ( + "explicit_bucket_histogram" + ): + _logger.warning( + "Invalid value for %s: %s, using explicit bucket histogram aggregation", + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + otel_exporter_otlp_metrics_default_histogram_aggregation, + ) + instrument_class_aggregation = { + SDKHistogram: ExplicitBucketHistogramAggregation(), + } + + instrument_class_aggregation.update(preferred_aggregation or {}) + return instrument_class_aggregation + + +class EncodingException(Exception): + def __init__(self, original_exception, metric): + super().__init__() + self.original_exception = original_exception + self.metric = metric + + def __str__(self): + return f"{self.metric}\n{self.original_exception}" + + +def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: + resource_metrics_list = [] + + for resource_metrics in data.resource_metrics: + scope_metrics_list = [] + sdk_resource = resource_metrics.resource + + for scope_metrics in resource_metrics.scope_metrics: + instrumentation_scope = scope_metrics.scope + pb2_scope_metrics = ScopeMetrics( + scope=_encode_instrumentation_scope(instrumentation_scope), + schema_url=instrumentation_scope.schema_url, + ) + + for metric in scope_metrics.metrics: + try: + pb2_metric = _encode_metric(metric) + except Exception as ex: + raise EncodingException(ex, metric) from None + if pb2_metric is not None: + pb2_scope_metrics.metrics.append(pb2_metric) + + scope_metrics_list.append(pb2_scope_metrics) + + resource_metrics_list.append( + ResourceMetrics( + resource=PB2Resource( + attributes=_encode_attributes(sdk_resource.attributes) + ), + scope_metrics=scope_metrics_list, + schema_url=sdk_resource.schema_url, + ) + ) + + return ExportMetricsServiceRequest(resource_metrics=resource_metrics_list) + + +def _encode_metric(metric) -> Metric | None: + kwargs: dict = dict( + name=metric.name, + description=metric.description, + unit=metric.unit, + ) + + if isinstance(metric.data, SDKGauge): + data_points = [] + for dp in metric.data.data_points: + pt_kwargs: dict = dict( + attributes=_encode_attributes(dp.attributes), + time_unix_nano=dp.time_unix_nano, + exemplars=_encode_exemplars(dp.exemplars), + ) + if isinstance(dp.value, int): + pt_kwargs["as_int"] = dp.value + else: + pt_kwargs["as_double"] = dp.value + data_points.append(NumberDataPoint(**pt_kwargs)) + kwargs["gauge"] = Gauge(data_points=data_points) + + elif isinstance(metric.data, HistogramType): + data_points = [] + for dp in metric.data.data_points: + data_points.append( + HistogramDataPoint( + attributes=_encode_attributes(dp.attributes), + time_unix_nano=dp.time_unix_nano, + start_time_unix_nano=dp.start_time_unix_nano, + exemplars=_encode_exemplars(dp.exemplars), + count=dp.count, + sum=dp.sum, + bucket_counts=list(dp.bucket_counts) if dp.bucket_counts else [], + explicit_bounds=list(dp.explicit_bounds) if dp.explicit_bounds else [], + max=dp.max, + min=dp.min, + ) + ) + kwargs["histogram"] = Histogram( + data_points=data_points, + aggregation_temporality=metric.data.aggregation_temporality, + ) + + elif isinstance(metric.data, SDKSum): + data_points = [] + for dp in metric.data.data_points: + pt_kwargs = dict( + attributes=_encode_attributes(dp.attributes), + start_time_unix_nano=dp.start_time_unix_nano, + time_unix_nano=dp.time_unix_nano, + exemplars=_encode_exemplars(dp.exemplars), + ) + if isinstance(dp.value, int): + pt_kwargs["as_int"] = dp.value + else: + pt_kwargs["as_double"] = dp.value + data_points.append(NumberDataPoint(**pt_kwargs)) + kwargs["sum"] = Sum( + data_points=data_points, + aggregation_temporality=metric.data.aggregation_temporality, + is_monotonic=metric.data.is_monotonic, + ) + + elif isinstance(metric.data, ExponentialHistogramType): + data_points = [] + for dp in metric.data.data_points: + positive = negative = None + if dp.positive.bucket_counts: + positive = ExponentialHistogramDataPoint.Buckets( + offset=dp.positive.offset, + bucket_counts=list(dp.positive.bucket_counts), + ) + if dp.negative.bucket_counts: + negative = ExponentialHistogramDataPoint.Buckets( + offset=dp.negative.offset, + bucket_counts=list(dp.negative.bucket_counts), + ) + data_points.append( + ExponentialHistogramDataPoint( + attributes=_encode_attributes(dp.attributes), + time_unix_nano=dp.time_unix_nano, + start_time_unix_nano=dp.start_time_unix_nano, + exemplars=_encode_exemplars(dp.exemplars), + count=dp.count, + sum=dp.sum, + scale=dp.scale, + zero_count=dp.zero_count, + positive=positive, + negative=negative, + flags=dp.flags, + max=dp.max, + min=dp.min, + ) + ) + kwargs["exponential_histogram"] = ExponentialHistogram( + data_points=data_points, + aggregation_temporality=metric.data.aggregation_temporality, + ) + + else: + _logger.warning("unsupported data type %s", metric.data.__class__.__name__) + return None + + return Metric(**kwargs) + + +def _encode_exemplars(sdk_exemplars: list[SDKExemplar]) -> list[Exemplar]: + result = [] + for sdk_exemplar in sdk_exemplars: + ex_kwargs: dict = dict( + time_unix_nano=sdk_exemplar.time_unix_nano, + filtered_attributes=_encode_attributes(sdk_exemplar.filtered_attributes), + ) + if ( + sdk_exemplar.span_id is not None + and sdk_exemplar.trace_id is not None + ): + ex_kwargs["span_id"] = _encode_span_id(sdk_exemplar.span_id) + ex_kwargs["trace_id"] = _encode_trace_id(sdk_exemplar.trace_id) + + if isinstance(sdk_exemplar.value, float): + ex_kwargs["as_double"] = sdk_exemplar.value + elif isinstance(sdk_exemplar.value, int): + ex_kwargs["as_int"] = sdk_exemplar.value + else: + raise ValueError("Exemplar value must be an int or float") + + result.append(Exemplar(**ex_kwargs)) + return result diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/trace_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/trace_encoder/__init__.py new file mode 100644 index 00000000000..f7fb19f6abb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/_internal/trace_encoder/__init__.py @@ -0,0 +1,160 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from logging import getLogger +from collections import defaultdict +from collections.abc import Sequence + +from opentelemetry.exporter.otlp._proto.common._internal import ( + _encode_attributes, + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, +) +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, +) +from opentelemetry._proto.trace.v1.trace_pb2 import ( + ResourceSpans, + ScopeSpans, + Span, + Status, +) +from opentelemetry.sdk.trace import Event, ReadableSpan +from opentelemetry.trace import Link, SpanKind +from opentelemetry.trace.span import SpanContext, TraceState + +# Map SDK SpanKind (0-4) to proto SpanKind (1-5) +_SPAN_KIND_MAP = { + SpanKind.INTERNAL: 1, + SpanKind.SERVER: 2, + SpanKind.CLIENT: 3, + SpanKind.PRODUCER: 4, + SpanKind.CONSUMER: 5, +} + +# proto SpanFlags values +_SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK = 0x00000100 +_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK = 0x00000200 + +_logger = getLogger(__name__) + + +def encode_spans(sdk_spans: Sequence[ReadableSpan]) -> ExportTraceServiceRequest: + return ExportTraceServiceRequest( + resource_spans=_encode_resource_spans(sdk_spans) + ) + + +def _encode_resource_spans( + sdk_spans: Sequence[ReadableSpan], +) -> list[ResourceSpans]: + sdk_resource_spans: dict = defaultdict(lambda: defaultdict(list)) + + for sdk_span in sdk_spans: + sdk_resource = sdk_span.resource + sdk_instrumentation = sdk_span.instrumentation_scope or None + pb2_span = _encode_span(sdk_span) + sdk_resource_spans[sdk_resource][sdk_instrumentation].append(pb2_span) + + pb2_resource_spans = [] + for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): + scope_spans = [] + for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): + scope_spans.append( + ScopeSpans( + scope=_encode_instrumentation_scope(sdk_instrumentation), + spans=pb2_spans, + schema_url=sdk_instrumentation.schema_url + if sdk_instrumentation + else "", + ) + ) + pb2_resource_spans.append( + ResourceSpans( + resource=_encode_resource(sdk_resource), + scope_spans=scope_spans, + schema_url=sdk_resource.schema_url, + ) + ) + return pb2_resource_spans + + +def _span_flags(parent_span_context: SpanContext | None) -> int: + flags = _SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK + if parent_span_context and parent_span_context.is_remote: + flags |= _SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK + return flags + + +def _encode_span(sdk_span: ReadableSpan) -> Span: + span_context = sdk_span.get_span_context() + return Span( + trace_id=_encode_trace_id(span_context.trace_id), + span_id=_encode_span_id(span_context.span_id), + trace_state=_encode_trace_state(span_context.trace_state), + parent_span_id=_encode_parent_id(sdk_span.parent), + name=sdk_span.name, + kind=_SPAN_KIND_MAP[sdk_span.kind], + start_time_unix_nano=sdk_span.start_time, + end_time_unix_nano=sdk_span.end_time, + attributes=_encode_attributes(sdk_span.attributes), + events=_encode_events(sdk_span.events), + links=_encode_links(sdk_span.links), + status=_encode_status(sdk_span.status), + dropped_attributes_count=sdk_span.dropped_attributes, + dropped_events_count=sdk_span.dropped_events, + dropped_links_count=sdk_span.dropped_links, + flags=_span_flags(sdk_span.parent), + ) + + +def _encode_events(events: Sequence[Event]) -> list[Span.Event] | None: + if not events: + return None + return [ + Span.Event( + name=event.name, + time_unix_nano=event.timestamp, + attributes=_encode_attributes(event.attributes), + dropped_attributes_count=event.dropped_attributes, + ) + for event in events + ] + + +def _encode_links(links: Sequence[Link]) -> list[Span.Link] | None: + if not links: + return None + return [ + Span.Link( + trace_id=_encode_trace_id(link.context.trace_id), + span_id=_encode_span_id(link.context.span_id), + attributes=_encode_attributes(link.attributes), + dropped_attributes_count=link.dropped_attributes, + flags=_span_flags(link.context), + ) + for link in links + ] + + +def _encode_status(status) -> Status | None: + if status is None: + return None + return Status( + code=status.status_code.value, + message=status.description or "", + ) + + +def _encode_trace_state(trace_state: TraceState) -> str: + if trace_state is None: + return "" + return ",".join(f"{key}={value}" for key, value in trace_state.items()) + + +def _encode_parent_id(context: SpanContext | None) -> bytes: + if context: + return _encode_span_id(context.span_id) + return b"" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/version/__init__.py new file mode 100644 index 00000000000..cdc135bf8ae --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/_proto/common/version/__init__.py @@ -0,0 +1 @@ +__version__ = "1.45.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py index 6b3d1d76fa3..9cb19bc4099 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py @@ -1,153 +1,13 @@ # 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, +from opentelemetry.exporter.otlp._proto.common._exporter_metrics import ( + ExportResult, + ExporterMetricsT, + ExporterMetrics, + NoOpExporterMetrics, + create_exporter_metrics, ) -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, - ) +__all__ = ["ExportResult", "ExporterMetricsT", "ExporterMetrics", "NoOpExporterMetrics", "create_exporter_metrics"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 7528b5994b8..ef585a5a6ac 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -2,128 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - -import logging -from collections.abc import Callable, Mapping, Sequence -from typing import ( - Any, - TypeVar, -) - -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - ArrayValue as PB2ArrayValue, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - KeyValueList as PB2KeyValueList, -) -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, +from opentelemetry.exporter.otlp._proto.common._internal import ( + _encode_attributes, + _encode_instrumentation_scope, + _encode_key_value, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, + _get_resource_data, ) -from opentelemetry.sdk.trace import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util.types import _ExtendedAttributes - -_logger = logging.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) -> PB2AnyValue: - if value is None: - return PB2AnyValue() - if isinstance(value, bool): - return PB2AnyValue(bool_value=value) - if isinstance(value, str): - return PB2AnyValue(string_value=value) - if isinstance(value, int): - return PB2AnyValue(int_value=value) - if isinstance(value, float): - return PB2AnyValue(double_value=value) - if isinstance(value, bytes): - return PB2AnyValue(bytes_value=value) - if isinstance(value, Sequence): - return PB2AnyValue( - array_value=PB2ArrayValue(values=[_encode_value(v) for v in value]) - ) - if isinstance(value, Mapping): - return PB2AnyValue( - kvlist_value=PB2KeyValueList( - 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) -> PB2KeyValue: - return PB2KeyValue(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[PB2KeyValue]: - if not attributes: - return [] - pb2_attributes = [] - for key, value in attributes.items(): - # pylint: disable=broad-exception-caught - 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 +__all__ = ["_encode_attributes", "_encode_instrumentation_scope", "_encode_key_value", "_encode_resource", "_encode_span_id", "_encode_trace_id", "_encode_value", "_get_resource_data"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py index 86b4380bd95..d40dc8fe163 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py @@ -1,95 +1,9 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from collections import defaultdict -from collections.abc import Sequence -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_attributes, - _encode_instrumentation_scope, - _encode_resource, - _encode_span_id, - _encode_trace_id, - _encode_value, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ( - ResourceLogs, - ScopeLogs, -) -from opentelemetry.sdk._logs import ReadableLogRecord - - -def encode_logs( - batch: Sequence[ReadableLogRecord], -) -> ExportLogsServiceRequest: - return ExportLogsServiceRequest(resource_logs=_encode_resource_logs(batch)) - - -def _encode_log(readable_log_record: ReadableLogRecord) -> PB2LogRecord: - span_id = ( - None - if readable_log_record.log_record.span_id == 0 - else _encode_span_id(readable_log_record.log_record.span_id) - ) - trace_id = ( - None - if readable_log_record.log_record.trace_id == 0 - else _encode_trace_id(readable_log_record.log_record.trace_id) - ) - return PB2LogRecord( - time_unix_nano=readable_log_record.log_record.timestamp, - observed_time_unix_nano=readable_log_record.log_record.observed_timestamp, - span_id=span_id, - trace_id=trace_id, - flags=int(readable_log_record.log_record.trace_flags), - body=_encode_value(readable_log_record.log_record.body), - severity_text=readable_log_record.log_record.severity_text, - attributes=_encode_attributes( - readable_log_record.log_record.attributes - ), - dropped_attributes_count=readable_log_record.dropped_attributes, - severity_number=getattr( - readable_log_record.log_record.severity_number, "value", None - ), - event_name=readable_log_record.log_record.event_name, - ) +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import ( + encode_logs, +) -def _encode_resource_logs( - batch: Sequence[ReadableLogRecord], -) -> list[ResourceLogs]: - sdk_resource_logs = defaultdict(lambda: defaultdict(list)) - - for readable_log in batch: - sdk_resource = readable_log.resource - sdk_instrumentation = readable_log.instrumentation_scope or None - pb2_log = _encode_log(readable_log) - - sdk_resource_logs[sdk_resource][sdk_instrumentation].append(pb2_log) - - pb2_resource_logs = [] - - for sdk_resource, sdk_instrumentations in sdk_resource_logs.items(): - scope_logs = [] - for sdk_instrumentation, pb2_logs in sdk_instrumentations.items(): - scope_logs.append( - ScopeLogs( - scope=(_encode_instrumentation_scope(sdk_instrumentation)), - log_records=pb2_logs, - schema_url=sdk_instrumentation.schema_url - if sdk_instrumentation - else None, - ) - ) - pb2_resource_logs.append( - ResourceLogs( - resource=_encode_resource(sdk_resource), - scope_logs=scope_logs, - schema_url=sdk_resource.schema_url, - ) - ) - - return pb2_resource_logs +__all__ = ["encode_logs"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py index fad7aaf3dd7..a8ad580f4bc 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py @@ -1,376 +1,11 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations -import logging -from os import environ -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_attributes, - _encode_instrumentation_scope, - _encode_span_id, - _encode_trace_id, +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import ( + EncodingException, + OTLPMetricExporterMixin, + encode_metrics, ) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, -) -from opentelemetry.sdk.metrics import ( - Counter, - Exemplar, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - Gauge, - MetricExporter, - MetricsData, - Sum, -) -from opentelemetry.sdk.metrics.export import ( - ExponentialHistogram as ExponentialHistogramType, -) -from opentelemetry.sdk.metrics.export import ( - Histogram as HistogramType, -) -from opentelemetry.sdk.metrics.view import ( - Aggregation, - ExplicitBucketHistogramAggregation, - ExponentialBucketHistogramAggregation, -) - -_logger = logging.getLogger(__name__) - - -class OTLPMetricExporterMixin: - def _common_configuration( - self, - preferred_temporality: dict[type, AggregationTemporality] - | None = None, - preferred_aggregation: dict[type, Aggregation] | None = None, - ) -> None: - MetricExporter.__init__( - self, - preferred_temporality=self._get_temporality(preferred_temporality), - preferred_aggregation=self._get_aggregation(preferred_aggregation), - ) - - def _get_temporality( - self, preferred_temporality: dict[type, AggregationTemporality] - ) -> dict[type, AggregationTemporality]: - otel_exporter_otlp_metrics_temporality_preference = ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - "CUMULATIVE", - ) - .upper() - .strip() - ) - - if otel_exporter_otlp_metrics_temporality_preference == "DELTA": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.DELTA, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - elif otel_exporter_otlp_metrics_temporality_preference == "LOWMEMORY": - instrument_class_temporality = { - Counter: AggregationTemporality.DELTA, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.DELTA, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - else: - if otel_exporter_otlp_metrics_temporality_preference != ( - "CUMULATIVE" - ): - _logger.warning( - "Unrecognized OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" - " value found: " - "%s, " - "using CUMULATIVE", - otel_exporter_otlp_metrics_temporality_preference, - ) - instrument_class_temporality = { - Counter: AggregationTemporality.CUMULATIVE, - UpDownCounter: AggregationTemporality.CUMULATIVE, - Histogram: AggregationTemporality.CUMULATIVE, - ObservableCounter: AggregationTemporality.CUMULATIVE, - ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, - ObservableGauge: AggregationTemporality.CUMULATIVE, - } - - instrument_class_temporality.update(preferred_temporality or {}) - - return instrument_class_temporality - - def _get_aggregation( - self, - preferred_aggregation: dict[type, Aggregation], - ) -> dict[type, Aggregation]: - otel_exporter_otlp_metrics_default_histogram_aggregation = environ.get( - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, - "explicit_bucket_histogram", - ) - - if otel_exporter_otlp_metrics_default_histogram_aggregation == ( - "base2_exponential_bucket_histogram" - ): - instrument_class_aggregation = { - Histogram: ExponentialBucketHistogramAggregation(), - } - - else: - if otel_exporter_otlp_metrics_default_histogram_aggregation != ( - "explicit_bucket_histogram" - ): - _logger.warning( - ( - "Invalid value for %s: %s, using explicit bucket " - "histogram aggregation" - ), - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, - otel_exporter_otlp_metrics_default_histogram_aggregation, - ) - - instrument_class_aggregation = { - Histogram: ExplicitBucketHistogramAggregation(), - } - - instrument_class_aggregation.update(preferred_aggregation or {}) - - return instrument_class_aggregation - - -class EncodingException(Exception): - """ - Raised by encode_metrics() when an exception is caught during encoding. Contains the problematic metric so - the misbehaving metric name and details can be logged during exception handling. - """ - - def __init__(self, original_exception, metric): - super().__init__() - self.original_exception = original_exception - self.metric = metric - - def __str__(self): - return f"{self.metric}\n{self.original_exception}" - - -def encode_metrics(data: MetricsData) -> ExportMetricsServiceRequest: - resource_metrics_dict = {} - - for resource_metrics in data.resource_metrics: - _encode_resource_metrics(resource_metrics, resource_metrics_dict) - - resource_data = [] - for ( - sdk_resource, - scope_data, - ) in resource_metrics_dict.items(): - resource_data.append( - pb2.ResourceMetrics( - resource=PB2Resource( - attributes=_encode_attributes(sdk_resource.attributes) - ), - scope_metrics=scope_data.values(), - schema_url=sdk_resource.schema_url, - ) - ) - return ExportMetricsServiceRequest(resource_metrics=resource_data) - - -def _encode_resource_metrics(resource_metrics, resource_metrics_dict): - resource = resource_metrics.resource - # It is safe to assume that each entry in data.resource_metrics is - # associated with an unique resource. - scope_metrics_dict = {} - resource_metrics_dict[resource] = scope_metrics_dict - for scope_metrics in resource_metrics.scope_metrics: - instrumentation_scope = scope_metrics.scope - - # The SDK groups metrics in instrumentation scopes already so - # there is no need to check for existing instrumentation scopes - # here. - pb2_scope_metrics = pb2.ScopeMetrics( - scope=_encode_instrumentation_scope(instrumentation_scope), - schema_url=instrumentation_scope.schema_url, - ) - - scope_metrics_dict[instrumentation_scope] = pb2_scope_metrics - - for metric in scope_metrics.metrics: - pb2_metric = pb2.Metric( - name=metric.name, - description=metric.description, - unit=metric.unit, - ) - - try: - _encode_metric(metric, pb2_metric) - except Exception as ex: - # `from None` so we don't get "During handling of the above exception, another exception occurred:" - raise EncodingException(ex, metric) from None - - pb2_scope_metrics.metrics.append(pb2_metric) - - -def _encode_metric(metric, pb2_metric): - if isinstance(metric.data, Gauge): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=_encode_attributes(data_point.attributes), - time_unix_nano=data_point.time_unix_nano, - exemplars=_encode_exemplars(data_point.exemplars), - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - pb2_metric.gauge.data_points.append(pt) - - elif isinstance(metric.data, HistogramType): - for data_point in metric.data.data_points: - pt = pb2.HistogramDataPoint( - attributes=_encode_attributes(data_point.attributes), - time_unix_nano=data_point.time_unix_nano, - start_time_unix_nano=data_point.start_time_unix_nano, - exemplars=_encode_exemplars(data_point.exemplars), - count=data_point.count, - sum=data_point.sum, - bucket_counts=data_point.bucket_counts, - explicit_bounds=data_point.explicit_bounds, - max=data_point.max, - min=data_point.min, - ) - pb2_metric.histogram.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.histogram.data_points.append(pt) - - elif isinstance(metric.data, Sum): - for data_point in metric.data.data_points: - pt = pb2.NumberDataPoint( - attributes=_encode_attributes(data_point.attributes), - start_time_unix_nano=data_point.start_time_unix_nano, - time_unix_nano=data_point.time_unix_nano, - exemplars=_encode_exemplars(data_point.exemplars), - ) - if isinstance(data_point.value, int): - pt.as_int = data_point.value - else: - pt.as_double = data_point.value - # note that because sum is a message type, the - # fields must be set individually rather than - # instantiating a pb2.Sum and setting it once - pb2_metric.sum.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.sum.is_monotonic = metric.data.is_monotonic - pb2_metric.sum.data_points.append(pt) - - elif isinstance(metric.data, ExponentialHistogramType): - for data_point in metric.data.data_points: - if data_point.positive.bucket_counts: - positive = pb2.ExponentialHistogramDataPoint.Buckets( - offset=data_point.positive.offset, - bucket_counts=data_point.positive.bucket_counts, - ) - else: - positive = None - - if data_point.negative.bucket_counts: - negative = pb2.ExponentialHistogramDataPoint.Buckets( - offset=data_point.negative.offset, - bucket_counts=data_point.negative.bucket_counts, - ) - else: - negative = None - - pt = pb2.ExponentialHistogramDataPoint( - attributes=_encode_attributes(data_point.attributes), - time_unix_nano=data_point.time_unix_nano, - start_time_unix_nano=data_point.start_time_unix_nano, - exemplars=_encode_exemplars(data_point.exemplars), - count=data_point.count, - sum=data_point.sum, - scale=data_point.scale, - zero_count=data_point.zero_count, - positive=positive, - negative=negative, - flags=data_point.flags, - max=data_point.max, - min=data_point.min, - ) - pb2_metric.exponential_histogram.aggregation_temporality = ( - metric.data.aggregation_temporality - ) - pb2_metric.exponential_histogram.data_points.append(pt) - - else: - _logger.warning( - "unsupported data type %s", - metric.data.__class__.__name__, - ) - - -def _encode_exemplars(sdk_exemplars: list[Exemplar]) -> list[pb2.Exemplar]: - """ - Converts a list of SDK Exemplars into a list of protobuf Exemplars. - - Args: - sdk_exemplars (list): The list of exemplars from the OpenTelemetry SDK. - - Returns: - list: A list of protobuf exemplars. - """ - pb_exemplars = [] - for sdk_exemplar in sdk_exemplars: - if ( - sdk_exemplar.span_id is not None - and sdk_exemplar.trace_id is not None - ): - pb_exemplar = pb2.Exemplar( - time_unix_nano=sdk_exemplar.time_unix_nano, - span_id=_encode_span_id(sdk_exemplar.span_id), - trace_id=_encode_trace_id(sdk_exemplar.trace_id), - filtered_attributes=_encode_attributes( - sdk_exemplar.filtered_attributes - ), - ) - else: - pb_exemplar = pb2.Exemplar( - time_unix_nano=sdk_exemplar.time_unix_nano, - filtered_attributes=_encode_attributes( - sdk_exemplar.filtered_attributes - ), - ) - - # Assign the value based on its type in the SDK exemplar - if isinstance(sdk_exemplar.value, float): - pb_exemplar.as_double = sdk_exemplar.value - elif isinstance(sdk_exemplar.value, int): - pb_exemplar.as_int = sdk_exemplar.value - else: - raise ValueError("Exemplar value must be an int or float") - pb_exemplars.append(pb_exemplar) - return pb_exemplars +__all__ = ["EncodingException", "OTLPMetricExporterMixin", "encode_metrics"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py index 96276a383e8..bae2ac3609d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/trace_encoder/__init__.py @@ -1,181 +1,9 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -import logging -from collections import defaultdict -from collections.abc import Sequence -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_attributes, - _encode_instrumentation_scope, - _encode_resource, - _encode_span_id, - _encode_trace_id, +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import ( + encode_spans, ) -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest as PB2ExportTraceServiceRequest, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( - ResourceSpans as PB2ResourceSpans, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans -from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan -from opentelemetry.proto.trace.v1.trace_pb2 import SpanFlags as PB2SpanFlags -from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status -from opentelemetry.sdk.trace import Event, ReadableSpan -from opentelemetry.trace import Link, SpanKind -from opentelemetry.trace.span import SpanContext, Status, TraceState - -# pylint: disable=E1101 -_SPAN_KIND_MAP = { - SpanKind.INTERNAL: PB2SPan.SpanKind.SPAN_KIND_INTERNAL, - SpanKind.SERVER: PB2SPan.SpanKind.SPAN_KIND_SERVER, - SpanKind.CLIENT: PB2SPan.SpanKind.SPAN_KIND_CLIENT, - SpanKind.PRODUCER: PB2SPan.SpanKind.SPAN_KIND_PRODUCER, - SpanKind.CONSUMER: PB2SPan.SpanKind.SPAN_KIND_CONSUMER, -} - -_logger = logging.getLogger(__name__) - - -def encode_spans( - sdk_spans: Sequence[ReadableSpan], -) -> PB2ExportTraceServiceRequest: - return PB2ExportTraceServiceRequest( - resource_spans=_encode_resource_spans(sdk_spans) - ) - - -def _encode_resource_spans( - sdk_spans: Sequence[ReadableSpan], -) -> list[PB2ResourceSpans]: - # We need to inspect the spans and group + structure them as: - # - # Resource - # Instrumentation Library - # Spans - # - # First loop organizes the SDK spans in this structure. Protobuf messages - # are not hashable so we stick with SDK data in this phase. - # - # Second loop encodes the data into Protobuf format. - # - sdk_resource_spans = defaultdict(lambda: defaultdict(list)) - - for sdk_span in sdk_spans: - sdk_resource = sdk_span.resource - sdk_instrumentation = sdk_span.instrumentation_scope or None - pb2_span = _encode_span(sdk_span) - - sdk_resource_spans[sdk_resource][sdk_instrumentation].append(pb2_span) - - pb2_resource_spans = [] - - for sdk_resource, sdk_instrumentations in sdk_resource_spans.items(): - scope_spans = [] - for sdk_instrumentation, pb2_spans in sdk_instrumentations.items(): - scope_spans.append( - PB2ScopeSpans( - scope=(_encode_instrumentation_scope(sdk_instrumentation)), - spans=pb2_spans, - schema_url=sdk_instrumentation.schema_url - if sdk_instrumentation - else None, - ) - ) - pb2_resource_spans.append( - PB2ResourceSpans( - resource=_encode_resource(sdk_resource), - scope_spans=scope_spans, - schema_url=sdk_resource.schema_url, - ) - ) - - return pb2_resource_spans - - -def _span_flags(parent_span_context: SpanContext | None) -> int: - flags = PB2SpanFlags.SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK - if parent_span_context and parent_span_context.is_remote: - flags |= PB2SpanFlags.SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK - return flags - - -def _encode_span(sdk_span: ReadableSpan) -> PB2SPan: - span_context = sdk_span.get_span_context() - return PB2SPan( - trace_id=_encode_trace_id(span_context.trace_id), - span_id=_encode_span_id(span_context.span_id), - trace_state=_encode_trace_state(span_context.trace_state), - parent_span_id=_encode_parent_id(sdk_span.parent), - name=sdk_span.name, - kind=_SPAN_KIND_MAP[sdk_span.kind], - start_time_unix_nano=sdk_span.start_time, - end_time_unix_nano=sdk_span.end_time, - attributes=_encode_attributes(sdk_span.attributes), - events=_encode_events(sdk_span.events), - links=_encode_links(sdk_span.links), - status=_encode_status(sdk_span.status), - dropped_attributes_count=sdk_span.dropped_attributes, - dropped_events_count=sdk_span.dropped_events, - dropped_links_count=sdk_span.dropped_links, - flags=_span_flags(sdk_span.parent), - ) - - -def _encode_events( - events: Sequence[Event], -) -> list[PB2SPan.Event] | None: - pb2_events = None - if events: - pb2_events = [] - for event in events: - encoded_event = PB2SPan.Event( - name=event.name, - time_unix_nano=event.timestamp, - attributes=_encode_attributes(event.attributes), - dropped_attributes_count=event.dropped_attributes, - ) - pb2_events.append(encoded_event) - return pb2_events - - -def _encode_links(links: Sequence[Link]) -> Sequence[PB2SPan.Link]: - pb2_links = None - if links: - pb2_links = [] - for link in links: - encoded_link = PB2SPan.Link( - trace_id=_encode_trace_id(link.context.trace_id), - span_id=_encode_span_id(link.context.span_id), - attributes=_encode_attributes(link.attributes), - dropped_attributes_count=link.dropped_attributes, - flags=_span_flags(link.context), - ) - pb2_links.append(encoded_link) - return pb2_links - - -def _encode_status(status: Status) -> PB2Status | None: - pb2_status = None - if status is not None: - pb2_status = PB2Status( - code=status.status_code.value, - message=status.description, - ) - return pb2_status - - -def _encode_trace_state(trace_state: TraceState) -> str | None: - pb2_trace_state = None - if trace_state is not None: - pb2_trace_state = ",".join( - [f"{key}={value}" for key, value in (trace_state.items())] - ) - return pb2_trace_state - -def _encode_parent_id(context: SpanContext | None) -> bytes | None: - if context: - return _encode_span_id(context.span_id) - return None +__all__ = ["encode_spans"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py index e55fa059673..d40dc8fe163 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import ( +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import ( encode_logs, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py index be1c83a27b1..31059c0bbd5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/metrics_encoder.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import ( encode_metrics, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py index 590ea7dfe13..bae2ac3609d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/trace_encoder.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import ( encode_spans, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version/__init__.py index 524a0260e55..e8b8f0d9807 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/version/__init__.py @@ -1,4 +1,9 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.45.0.dev" + +from opentelemetry.exporter.otlp._proto.common.version import ( + __version__, +) + +__all__ = ["__version__"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/conftest.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/conftest.py new file mode 100644 index 00000000000..e9ea917f62f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/conftest.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# Equivalence tests import the real (protobuf) encoder via the public +# ``opentelemetry.exporter.otlp.proto.common`` path and the pure-Python encoder +# via the private ``opentelemetry.exporter.otlp._proto.common`` path. Both this +# package and the real ``opentelemetry-exporter-otlp-proto-common`` provide the +# public path, so guard that it resolves to the real protobuf distribution and +# not this package's own re-export shim -- otherwise the equivalence assertions +# would silently compare the pure-Python implementation against itself. +from opentelemetry.exporter.otlp.proto.common import ( + trace_encoder as _public_trace_encoder, +) + +assert "pyproto" not in (_public_trace_encoder.__file__ or ""), ( + "opentelemetry.exporter.otlp.proto.common resolved to the pyproto shim " + f"({_public_trace_encoder.__file__}); the equivalence tests need the real " + "protobuf package to own that path." +) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_internal.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_internal.py new file mode 100644 index 00000000000..39f33806644 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_internal.py @@ -0,0 +1,159 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.proto.common._internal import ( + _encode_attributes as proto_encode_attributes, + _encode_instrumentation_scope as proto_encode_instrumentation_scope, + _encode_resource as proto_encode_resource, + _encode_span_id as proto_encode_span_id, + _encode_trace_id as proto_encode_trace_id, + _encode_value as proto_encode_value, +) +from opentelemetry.exporter.otlp._proto.common._internal import ( + _encode_attributes, + _encode_instrumentation_scope, + _encode_resource, + _encode_span_id, + _encode_trace_id, + _encode_value, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + + +# ── _encode_span_id / _encode_trace_id ─────────────────────────────────────── + +def test_encode_span_id_matches_proto() -> None: + span_id = 0x1234567890ABCDEF + assert _encode_span_id(span_id) == proto_encode_span_id(span_id) + + +def test_encode_trace_id_matches_proto() -> None: + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + assert _encode_trace_id(trace_id) == proto_encode_trace_id(trace_id) + + +def test_encode_span_id_zero() -> None: + assert _encode_span_id(0) == proto_encode_span_id(0) + + +# ── _encode_value ───────────────────────────────────────────────────────────── + +def test_encode_value_none_matches_proto() -> None: + assert _encode_value(None).SerializeToString() == proto_encode_value(None).SerializeToString() + + +def test_encode_value_string_matches_proto() -> None: + assert _encode_value("hello").SerializeToString() == proto_encode_value("hello").SerializeToString() + + +def test_encode_value_bool_true_matches_proto() -> None: + assert _encode_value(True).SerializeToString() == proto_encode_value(True).SerializeToString() + + +def test_encode_value_bool_false_matches_proto() -> None: + assert _encode_value(False).SerializeToString() == proto_encode_value(False).SerializeToString() + + +def test_encode_value_int_matches_proto() -> None: + assert _encode_value(42).SerializeToString() == proto_encode_value(42).SerializeToString() + + +def test_encode_value_negative_int_matches_proto() -> None: + assert _encode_value(-7).SerializeToString() == proto_encode_value(-7).SerializeToString() + + +def test_encode_value_float_matches_proto() -> None: + assert _encode_value(3.14).SerializeToString() == proto_encode_value(3.14).SerializeToString() + + +def test_encode_value_bytes_matches_proto() -> None: + assert _encode_value(b"\x01\x02\x03").SerializeToString() == proto_encode_value(b"\x01\x02\x03").SerializeToString() + + +def test_encode_value_list_matches_proto() -> None: + assert _encode_value([1, "a", True]).SerializeToString() == proto_encode_value([1, "a", True]).SerializeToString() + + +def test_encode_value_dict_matches_proto() -> None: + assert _encode_value({"k": "v", "n": 1}).SerializeToString() == proto_encode_value({"k": "v", "n": 1}).SerializeToString() + + +def test_encode_value_nested_matches_proto() -> None: + value = {"error": None, "tags": ["a", "b"], "count": 5} + assert _encode_value(value).SerializeToString() == proto_encode_value(value).SerializeToString() + + +# ── _encode_attributes ──────────────────────────────────────────────────────── + +def test_encode_attributes_empty_matches_proto() -> None: + our = _encode_attributes({}) + proto = proto_encode_attributes({}) + our_bytes = b"".join(kv.SerializeToString() for kv in our) + proto_bytes = b"".join(kv.SerializeToString() for kv in proto) + assert our_bytes == proto_bytes + + +def test_encode_attributes_with_values_matches_proto() -> None: + attrs = {"service.name": "my-service", "count": 5, "enabled": True, "ratio": 0.5} + our = _encode_attributes(attrs) + proto = proto_encode_attributes(attrs) + our_bytes = b"".join(kv.SerializeToString() for kv in our) + proto_bytes = b"".join(kv.SerializeToString() for kv in proto) + assert our_bytes == proto_bytes + + +def test_encode_attributes_none_matches_proto() -> None: + our = _encode_attributes(None) + proto = proto_encode_attributes(None) + assert our == proto == [] + + +# ── _encode_instrumentation_scope ───────────────────────────────────────────── + +def test_encode_instrumentation_scope_empty_matches_proto() -> None: + scope = InstrumentationScope(name="") + our = _encode_instrumentation_scope(scope) + proto = proto_encode_instrumentation_scope(scope) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_encode_instrumentation_scope_name_version_matches_proto() -> None: + scope = InstrumentationScope(name="mylib", version="1.2.3") + our = _encode_instrumentation_scope(scope) + proto = proto_encode_instrumentation_scope(scope) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_encode_instrumentation_scope_with_attributes_matches_proto() -> None: + scope = InstrumentationScope( + name="mylib", + version="2.0", + schema_url="https://example.com", + attributes={"env": "prod", "version": 2}, + ) + our = _encode_instrumentation_scope(scope) + proto = proto_encode_instrumentation_scope(scope) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_encode_instrumentation_scope_none_matches_proto() -> None: + our = _encode_instrumentation_scope(None) + proto = proto_encode_instrumentation_scope(None) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── _encode_resource ────────────────────────────────────────────────────────── + +def test_encode_resource_empty_matches_proto() -> None: + resource = Resource({}) + our = _encode_resource(resource) + proto = proto_encode_resource(resource) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_encode_resource_with_attributes_matches_proto() -> None: + resource = Resource({"service.name": "my-service", "host.name": "localhost", "pid": 1234}) + our = _encode_resource(resource) + proto = proto_encode_resource(resource) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_log_encoder.py new file mode 100644 index 00000000000..de7ef321c1a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_log_encoder.py @@ -0,0 +1,162 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._logs import LogRecord, SeverityNumber +from opentelemetry.exporter.otlp.proto.common._log_encoder import ( + encode_logs as proto_encode_logs, +) +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import ( + encode_logs as pyproto_encode_logs, +) +from opentelemetry.sdk._logs import LogRecordLimits, ReadWriteLogRecord +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + set_span_in_context, +) + +_CONTEXT = set_span_in_context( + NonRecordingSpan( + SpanContext( + 89564621134313219400156819398935297684, + 1312458408527513268, + False, + TraceFlags(0x01), + ) + ) +) + + +def _make_basic_log() -> ReadWriteLogRecord: + return ReadWriteLogRecord( + LogRecord( + timestamp=1644650195189786880, + observed_timestamp=1644650195189786881, + context=_CONTEXT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="Do not go gentle into that good night.", + attributes={"a": 1, "b": "c"}, + ), + resource=SDKResource( + {"first_resource": "value"}, + "resource_schema_url", + ), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + + +def test_encode_logs_basic_matches_proto() -> None: + logs = [_make_basic_log()] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() + + +def test_encode_logs_no_instrumentation_scope_matches_proto() -> None: + log = ReadWriteLogRecord( + LogRecord( + timestamp=1644650427658989056, + observed_timestamp=1644650427658989057, + context=_CONTEXT, + severity_text="DEBUG", + severity_number=SeverityNumber.DEBUG, + body={"error": None, "array_with_nones": [1, None, 2]}, + attributes={"a": 1, "b": "c"}, + ), + resource=SDKResource({"second_resource": "CASE"}), + instrumentation_scope=None, + ) + logs = [log] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() + + +def test_encode_logs_empty_resource_with_scope_attributes_matches_proto() -> None: + log = ReadWriteLogRecord( + LogRecord( + timestamp=1644650584292683033, + observed_timestamp=1644650584292683033, + context=_CONTEXT, + severity_text="FATAL", + severity_number=SeverityNumber.FATAL, + body="This instrumentation scope has a schema url and attributes", + attributes={ + "extended": { + "sequence": [{"inner": "mapping", "none": None}] + } + }, + ), + resource=SDKResource({}), + instrumentation_scope=InstrumentationScope( + "scope_with_attributes", + "scope_with_attributes_version", + "instrumentation_schema_url", + {"one": 1, "two": "2"}, + ), + ) + logs = [log] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() + + +def test_encode_logs_dropped_attributes_matches_proto() -> None: + log = ReadWriteLogRecord( + LogRecord( + timestamp=1644650195189786880, + context=_CONTEXT, + severity_text="WARN", + severity_number=SeverityNumber.WARN, + body="message with dropped attributes", + attributes={"a": 1, "b": "c", "user_id": "B121092"}, + ), + resource=SDKResource({"first_resource": "value"}), + limits=LogRecordLimits(max_attributes=1), + instrumentation_scope=InstrumentationScope( + "first_name", "first_version" + ), + ) + logs = [log] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() + + +def test_encode_logs_multiple_resources_matches_proto() -> None: + log1 = _make_basic_log() + log2 = ReadWriteLogRecord( + LogRecord( + timestamp=1644650249738562048, + observed_timestamp=1644650249738562049, + context=_CONTEXT, + severity_text="ERROR", + severity_number=SeverityNumber.ERROR, + body="second log", + ), + resource=SDKResource({"second_resource": "value2"}), + instrumentation_scope=InstrumentationScope( + "second_name", "second_version" + ), + ) + logs = [log1, log2] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() + + +def test_encode_logs_empty_list_matches_proto() -> None: + assert pyproto_encode_logs([]).SerializeToString() == proto_encode_logs([]).SerializeToString() + + +def test_encode_logs_no_trace_context_matches_proto() -> None: + no_context = set_span_in_context(NonRecordingSpan(SpanContext(0, 0, False))) + log = ReadWriteLogRecord( + LogRecord( + timestamp=1644650195189786880, + context=no_context, + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body="no trace context", + ), + resource=SDKResource({}), + instrumentation_scope=None, + ) + logs = [log] + assert pyproto_encode_logs(logs).SerializeToString() == proto_encode_logs(logs).SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_metrics_encoder.py new file mode 100644 index 00000000000..535f9c6d8a4 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_metrics_encoder.py @@ -0,0 +1,332 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics as proto_encode_metrics, +) +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import ( + encode_metrics as pyproto_encode_metrics, +) +from opentelemetry.sdk.metrics import Exemplar +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Buckets, + ExponentialHistogramDataPoint, + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.metrics.export import ( + ExponentialHistogram as ExponentialHistogramType, +) +from opentelemetry.sdk.metrics.export import Histogram as HistogramType +from opentelemetry.sdk.metrics.export import Gauge as SDKGauge +from opentelemetry.sdk.metrics.export import Sum as SDKSum +from opentelemetry.sdk.metrics.export import NumberDataPoint +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) + + +_SPAN_ID = int("6e0c63257de34c92", 16) +_TRACE_ID = int("d4cda95b652f4a1592b449d5929fda1b", 16) + + +def _wrap(metric: Metric) -> MetricsData: + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource({"service.name": "test"}), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope("test_scope", "1.0"), + metrics=[metric], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + + +# ── Gauge ───────────────────────────────────────────────────────────────────── + +def test_encode_gauge_int_matches_proto() -> None: + data = _wrap(Metric( + name="my.gauge", + description="A gauge", + unit="1", + data=SDKGauge(data_points=[ + NumberDataPoint( + attributes={"host": "localhost"}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=42, + exemplars=[], + ) + ]), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +def test_encode_gauge_double_matches_proto() -> None: + data = _wrap(Metric( + name="my.gauge.double", + description="", + unit="s", + data=SDKGauge(data_points=[ + NumberDataPoint( + attributes={"env": "prod"}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=3.14, + exemplars=[], + ) + ]), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +def test_encode_gauge_with_exemplar_matches_proto() -> None: + data = _wrap(Metric( + name="gauge.exemplar", + description="", + unit="1", + data=SDKGauge(data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=100.0, + exemplars=[ + Exemplar( + {"filtered": "yes"}, + 50.0, + 1641946016139533400, + _SPAN_ID, + _TRACE_ID, + ) + ], + ) + ]), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +# ── Sum ─────────────────────────────────────────────────────────────────────── + +def test_encode_sum_int_matches_proto() -> None: + data = _wrap(Metric( + name="my.counter", + description="A counter", + unit="1", + data=SDKSum( + data_points=[ + NumberDataPoint( + attributes={"a": 1, "b": False}, + start_time_unix_nano=1641946016139533000, + time_unix_nano=1641946016139533244, + value=10, + exemplars=[], + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +def test_encode_sum_double_delta_matches_proto() -> None: + data = _wrap(Metric( + name="my.updown", + description="", + unit="By", + data=SDKSum( + data_points=[ + NumberDataPoint( + attributes={"direction": "in"}, + start_time_unix_nano=1641946016139533000, + time_unix_nano=1641946016139533244, + value=2.5, + exemplars=[], + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + is_monotonic=False, + ), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +# ── Histogram ───────────────────────────────────────────────────────────────── + +def test_encode_histogram_matches_proto() -> None: + data = _wrap(Metric( + name="my.histogram", + description="A histogram", + unit="s", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={"a": 1, "b": True}, + start_time_unix_nano=1641946016139533244, + time_unix_nano=1641946016139533244, + exemplars=[ + Exemplar( + {"filtered": "banana"}, + 298.0, + 1641946016139533400, + _SPAN_ID, + _TRACE_ID, + ), + Exemplar( + {"filtered": "banana"}, + 298.0, + 1641946016139533400, + None, + None, + ), + ], + count=5, + sum=67, + bucket_counts=[1, 4], + explicit_bounds=[10.0, 20.0], + min=8, + max=18, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +def test_encode_histogram_no_exemplars_matches_proto() -> None: + data = _wrap(Metric( + name="simple.histogram", + description="", + unit="ms", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={"region": "us-east"}, + start_time_unix_nano=1641946016000000000, + time_unix_nano=1641946016139533244, + exemplars=[], + count=10, + sum=500.0, + bucket_counts=[2, 3, 5], + explicit_bounds=[100.0, 200.0], + min=10.0, + max=200.0, + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +# ── ExponentialHistogram ────────────────────────────────────────────────────── + +def test_encode_exponential_histogram_matches_proto() -> None: + data = _wrap(Metric( + name="exp.histogram", + description="", + unit="1", + data=ExponentialHistogramType( + data_points=[ + ExponentialHistogramDataPoint( + attributes={"host": "node1"}, + start_time_unix_nano=1641946016000000000, + time_unix_nano=1641946016139533244, + exemplars=[], + count=8, + sum=100.0, + scale=2, + zero_count=1, + positive=Buckets(offset=1, bucket_counts=[3, 4]), + negative=Buckets(offset=-1, bucket_counts=[1]), + flags=0, + min=1.0, + max=50.0, + ) + ], + aggregation_temporality=AggregationTemporality.DELTA, + ), + )) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +# ── Multiple metrics and resources ──────────────────────────────────────────── + +def test_encode_multiple_metrics_matches_proto() -> None: + gauge_metric = Metric( + name="gauge", + description="", + unit="1", + data=SDKGauge(data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=1, + exemplars=[], + ) + ]), + ) + counter_metric = Metric( + name="counter", + description="", + unit="1", + data=SDKSum( + data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=1641946016000000000, + time_unix_nano=1641946016139533244, + value=5, + exemplars=[], + ) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource({"service.name": "svc-a"}), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope("lib_a", "1.0"), + metrics=[gauge_metric, counter_metric], + schema_url="", + ) + ], + schema_url="", + ), + ResourceMetrics( + resource=Resource({"service.name": "svc-b"}), + scope_metrics=[ + ScopeMetrics( + scope=SDKInstrumentationScope("lib_b", "2.0"), + metrics=[gauge_metric], + schema_url="", + ) + ], + schema_url="resource_schema_url", + ), + ] + ) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() + + +def test_encode_metrics_empty_matches_proto() -> None: + data = MetricsData(resource_metrics=[]) + assert pyproto_encode_metrics(data).SerializeToString() == proto_encode_metrics(data).SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_trace_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_trace_encoder.py new file mode 100644 index 00000000000..80ce493c253 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/equivalence/test_trace_encoder.py @@ -0,0 +1,197 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( + encode_spans as proto_encode_spans, +) +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import ( + encode_spans as pyproto_encode_spans, +) +from opentelemetry.sdk.trace import Event as SDKEvent +from opentelemetry.sdk.trace import Resource as SDKResource +from opentelemetry.sdk.trace import SpanContext as SDKSpanContext +from opentelemetry.sdk.trace import _Span as SDKSpan +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.trace import Link as SDKLink +from opentelemetry.trace import SpanKind as SDKSpanKind +from opentelemetry.trace import TraceFlags as SDKTraceFlags +from opentelemetry.trace.status import Status as SDKStatus +from opentelemetry.trace.status import StatusCode as SDKStatusCode + + +def _make_exhaustive_spans() -> list[SDKSpan]: + trace_id = 0x3E0C63257DE34C926F9EFCD03927272E + + base_time = 683647322 * 10**9 + start_times = ( + base_time, + base_time + 150 * 10**6, + base_time + 300 * 10**6, + base_time + 400 * 10**6, + base_time + 500 * 10**6, + base_time + 600 * 10**6, + ) + end_times = ( + start_times[0] + (50 * 10**6), + start_times[1] + (100 * 10**6), + start_times[2] + (200 * 10**6), + start_times[3] + (300 * 10**6), + start_times[4] + (400 * 10**6), + start_times[5] + (500 * 10**6), + ) + + parent_span_context = SDKSpanContext( + trace_id, 0x1111111111111111, is_remote=True + ) + other_context = SDKSpanContext( + trace_id, 0x2222222222222222, is_remote=False + ) + + span1 = SDKSpan( + name="test-span-1", + context=SDKSpanContext( + trace_id, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=parent_span_context, + events=( + SDKEvent( + name="event0", + timestamp=base_time + 50 * 10**6, + attributes={ + "annotation_bool": True, + "annotation_string": "annotation_test", + "key_float": 0.3, + }, + ), + ), + links=( + SDKLink(context=other_context, attributes={"key_bool": True}), + ), + resource=SDKResource({}, "resource_schema_url"), + ) + span1.start(start_time=start_times[0]) + span1.set_attribute("key_bool", False) + span1.set_attribute("key_string", "hello_world") + span1.set_attribute("key_float", 111.22) + span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) + span1.end(end_time=end_times[0]) + + span2 = SDKSpan( + name="test-span-2", + context=parent_span_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span2.start(start_time=start_times[1]) + span2.end(end_time=end_times[1]) + + span3 = SDKSpan( + name="test-span-3", + context=other_context, + parent=None, + resource=SDKResource(attributes={"key_resource": "some_resource"}), + ) + span3.start(start_time=start_times[2]) + span3.set_attribute("key_string", "hello_world") + span3.end(end_time=end_times[2]) + + span4 = SDKSpan( + name="test-span-4", + context=other_context, + parent=None, + resource=SDKResource({}, "resource_schema_url"), + instrumentation_scope=SDKInstrumentationScope( + name="name", version="version" + ), + ) + span4.start(start_time=start_times[3]) + span4.end(end_time=end_times[3]) + + span5 = SDKSpan( + name="test-span-5", + context=other_context, + parent=None, + resource=SDKResource( + attributes={"key_resource": "another_resource"}, + schema_url="resource_schema_url", + ), + instrumentation_scope=SDKInstrumentationScope( + name="scope_1_name", + version="scope_1_version", + schema_url="scope_1_schema_url", + ), + ) + span5.start(start_time=start_times[4]) + span5.end(end_time=end_times[4]) + + span6 = SDKSpan( + name="test-span-6", + context=other_context, + parent=None, + resource=SDKResource( + attributes={"key_resource": "another_resource"}, + schema_url="resource_schema_url", + ), + instrumentation_scope=SDKInstrumentationScope( + name="scope_2_name", + version="scope_2_version", + schema_url="scope_2_schema_url", + attributes={"one": "1", "two": 2}, + ), + ) + span6.start(start_time=start_times[5]) + span6.end(end_time=end_times[5]) + + return [span1, span2, span3, span4, span5, span6] + + +def test_encode_spans_single_minimal() -> None: + span = SDKSpan( + name="hello", + context=SDKSpanContext( + 0x3E0C63257DE34C926F9EFCD03927272E, + 0x34BF92DEEFC58C92, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=None, + resource=SDKResource({}), + ) + span.start(start_time=1_000_000_000) + span.end(end_time=2_000_000_000) + spans = [span] + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() + + +def test_encode_spans_with_attributes_and_status() -> None: + spans = _make_exhaustive_spans()[:1] # span1 has attributes, events, links, status + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() + + +def test_encode_spans_multiple_resources() -> None: + spans = _make_exhaustive_spans() + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() + + +def test_encode_spans_with_instrumentation_scope() -> None: + spans = _make_exhaustive_spans()[3:4] # span4 has instrumentation scope + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() + + +def test_encode_spans_with_scope_attributes() -> None: + spans = _make_exhaustive_spans()[5:6] # span6 has scope with attributes + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() + + +def test_encode_spans_empty_list() -> None: + assert pyproto_encode_spans([]).SerializeToString() == proto_encode_spans([]).SerializeToString() + + +def test_encode_spans_exhaustive_matches_proto() -> None: + spans = _make_exhaustive_spans() + assert pyproto_encode_spans(spans).SerializeToString() == proto_encode_spans(spans).SerializeToString() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/performance/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-common/tests/performance/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/performance/test_benchmark_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/performance/test_benchmark_encoder.py new file mode 100644 index 00000000000..2cc7dfb1407 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/performance/test_benchmark_encoder.py @@ -0,0 +1,317 @@ +# tests/performance/test_benchmark_encoder.py +# +# Benchmark: the pure-Python OTLP encoders (opentelemetry.exporter.otlp._proto +# .common) vs the real google.protobuf-backed encoders (opentelemetry.exporter +# .otlp.proto.common) on the exporter's actual production hot path — real SDK +# spans / metrics / logs in, serialized OTLP export-request bytes out. +# +# This is the end-to-end counterpart to opentelemetry-pyproto's +# test_benchmark_otlp.py: instead of hand-built proto message trees, it drives +# encode_spans / encode_metrics / encode_logs with genuine SDK objects, so the +# numbers reflect what an exporter pays per export (SDK data -> proto messages +# -> wire bytes). +# +# Both encoder paths share an identical call signature and return objects with +# SerializeToString(); a byte-equality guard asserts they encode identically so +# the comparison is fair. +# +# The real encoders are only present when the upstream +# ``opentelemetry-exporter-otlp-proto-common`` owns the public +# ``opentelemetry.exporter.otlp.proto.common`` path (installed after this +# package so it wins the namespace). Otherwise that path is this package's +# re-export shim and the whole module skips, like the equivalence conftest. +# +# Run (install order matters — real protobuf encoder must win the public path): +# uv pip install . && uv pip install \ +# opentelemetry-sdk protobuf opentelemetry-proto \ +# opentelemetry-exporter-otlp-proto-common pytest-benchmark +# uv run pytest tests/performance/test_benchmark_encoder.py \ +# --benchmark-group-by=param --benchmark-sort=fullname + +from pytest import mark, skip + +# Public path: real upstream encoder when installed, else this package's shim. +from opentelemetry.exporter.otlp.proto.common import ( + trace_encoder as _public_trace_encoder, +) + +if "pyproto" in (_public_trace_encoder.__file__ or ""): + skip( + "opentelemetry.exporter.otlp.proto.common resolves to the pyproto shim " + f"({_public_trace_encoder.__file__}); install the real " + "opentelemetry-exporter-otlp-proto-common (after this package) to " + "benchmark the pure-Python encoders against the real protobuf ones.", + allow_module_level=True, + ) + +from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( + encode_spans as proto_encode_spans, +) +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( + encode_metrics as proto_encode_metrics, +) +from opentelemetry.exporter.otlp.proto.common._log_encoder import ( + encode_logs as proto_encode_logs, +) +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import ( + encode_spans as pyproto_encode_spans, +) +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import ( + encode_metrics as pyproto_encode_metrics, +) +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import ( + encode_logs as pyproto_encode_logs, +) + +from opentelemetry._logs import LogRecord, SeverityNumber +from opentelemetry.sdk._logs import ReadWriteLogRecord +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + Histogram as HistogramType, + HistogramDataPoint, + Metric, + MetricsData, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.resources import Resource as SDKResource +from opentelemetry.sdk.trace import Event as SDKEvent +from opentelemetry.sdk.trace import SpanContext as SDKSpanContext +from opentelemetry.sdk.trace import _Span as SDKSpan +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope as SDKInstrumentationScope, +) +from opentelemetry.trace import Link as SDKLink +from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + SpanKind as SDKSpanKind, + TraceFlags as SDKTraceFlags, + set_span_in_context, +) +from opentelemetry.trace.status import Status as SDKStatus +from opentelemetry.trace.status import StatusCode as SDKStatusCode + + +# ── Shared SDK fixtures ───────────────────────────────────────────────────── +# +# One resource + scope shared across all items so a batch collapses into a +# single ResourceSpans/ScopeSpans, as a real per-service export does. Values +# are index-derived (no randomness) for reproducibility. + +_BASE_NS = 1_700_000_000_000_000_000 +_TRACE_ID = 0x3E0C63257DE34C926F9EFCD03927272E +_PARENT_SPAN_ID = 0x1111111111111111 + +_RESOURCE = SDKResource( + { + "service.name": "checkout-service", + "service.version": "1.24.0", + "service.instance.id": "pod-7f9c-abc123", + "process.pid": 42317, + "host.name": "ip-10-0-12-34", + "telemetry.sdk.language": "python", + "telemetry.sdk.version": "1.30.0", + }, + "https://opentelemetry.io/schemas/1.30.0", +) +_TRACE_SCOPE = SDKInstrumentationScope( + "opentelemetry.instrumentation.flask", "0.48b0" +) +_METRIC_SCOPE = SDKInstrumentationScope("opentelemetry.sdk.metrics", "1.30.0") +_LOG_SCOPE = SDKInstrumentationScope("opentelemetry.sdk._logs", "1.30.0") + + +def _make_span(i: int) -> SDKSpan: + start = _BASE_NS + i * 1_000_000 + span = SDKSpan( + name="GET /api/orders/{id}", + context=SDKSpanContext( + _TRACE_ID + i, + 0x34BF92DEEFC58C92 ^ i, + is_remote=False, + trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), + ), + parent=SDKSpanContext(_TRACE_ID + i, _PARENT_SPAN_ID, is_remote=True), + kind=SDKSpanKind.SERVER, + events=( + SDKEvent( + name="cache.miss", + timestamp=start + 1_000_000, + attributes={"cache.key": f"order:{i}"}, + ), + SDKEvent( + name="db.query", + timestamp=start + 2_500_000, + attributes={"db.rows": 17, "db.statement": "SELECT 1"}, + ), + ), + links=( + SDKLink( + context=SDKSpanContext( + _TRACE_ID + i + 100, 0x2222222222222222, is_remote=False + ), + attributes={"link.kind": "follows_from"}, + ), + ), + resource=_RESOURCE, + instrumentation_scope=_TRACE_SCOPE, + ) + span.start(start_time=start) + span.set_attribute("http.request.method", "GET") + span.set_attribute("url.path", f"/api/orders/{i}") + span.set_attribute("http.response.status_code", 200) + span.set_attribute("server.address", "orders.internal") + span.set_attribute("network.protocol.version", "1.1") + span.set_attribute("db.system", "postgresql") + span.set_status(SDKStatus(SDKStatusCode.OK)) + span.end(end_time=start + 4_200_000) + return span + + +def _make_spans(n: int) -> list: + return [_make_span(i) for i in range(n)] + + +def _make_metric(i: int, n_dp: int) -> Metric: + start = _BASE_NS + return Metric( + name=f"http.server.request.duration.{i}", + description="Duration of HTTP server requests.", + unit="s", + data=HistogramType( + data_points=[ + HistogramDataPoint( + attributes={ + "http.request.method": "GET", + "http.response.status_code": 200 + d, + }, + start_time_unix_nano=start, + time_unix_nano=start + 60_000_000_000, + count=1_234, + sum=456.789, + bucket_counts=[0, 12, 145, 402, 388, 210, 61, 14, 2, 0], + explicit_bounds=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5], + min=0.002, + max=3.1, + exemplars=[], + ) + for d in range(n_dp) + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + ), + ) + + +def _make_metrics_data(n_metric: int, n_dp: int) -> MetricsData: + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=_RESOURCE, + scope_metrics=[ + ScopeMetrics( + scope=_METRIC_SCOPE, + metrics=[_make_metric(i, n_dp) for i in range(n_metric)], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + + +_LOG_CONTEXT = set_span_in_context( + NonRecordingSpan( + SpanContext(_TRACE_ID, 0x34BF92DEEFC58C92, False, SDKTraceFlags(0x01)) + ) +) + + +def _make_log(i: int) -> ReadWriteLogRecord: + ts = _BASE_NS + i * 1_000_000 + return ReadWriteLogRecord( + LogRecord( + timestamp=ts, + observed_timestamp=ts + 1_000, + context=_LOG_CONTEXT, + severity_text="INFO", + severity_number=SeverityNumber.INFO, + body=f"request {i} completed in 4.2ms with status 200", + attributes={ + "log.source": "access", + "http.route": "/api/orders/{id}", + "http.response.status_code": 200, + "thread.id": 140_234_567, + }, + ), + resource=_RESOURCE, + instrumentation_scope=_LOG_SCOPE, + ) + + +def _make_logs(n: int) -> list: + return [_make_log(i) for i in range(n)] + + +# Encoders + payload factories, keyed by signal. Each factory takes the scale +# tuple and returns the encoder input. +_SIGNALS = { + "trace": ( + pyproto_encode_spans, + proto_encode_spans, + lambda dims: _make_spans(*dims), + [("single", (1,)), ("batch_100", (100,)), ("batch_500", (500,))], + ), + "metrics": ( + pyproto_encode_metrics, + proto_encode_metrics, + lambda dims: _make_metrics_data(*dims), + [("single", (1, 1)), ("metrics_50", (50, 1)), ("dp_200", (20, 10))], + ), + "logs": ( + pyproto_encode_logs, + proto_encode_logs, + lambda dims: _make_logs(*dims), + [("single", (1,)), ("batch_100", (100,)), ("batch_500", (500,))], + ), +} + +_ALL_CASES = [ + (signal, label, dims) + for signal, (_py, _pb, _factory, scales) in _SIGNALS.items() + for label, dims in scales +] +_ALL_IDS = [f"{signal}-{label}" for signal, label, _ in _ALL_CASES] + + +# ── Fairness guard: both encoders must produce identical bytes ────────────── + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +def test_encoder_outputs_identical(signal, label, dims) -> None: + py_encode, pb_encode, factory, _ = _SIGNALS[signal] + payload = factory(dims) + py_bytes = py_encode(payload).SerializeToString() + pb_bytes = pb_encode(payload).SerializeToString() + assert py_bytes == pb_bytes, ( + f"{signal}/{label}: pyproto and protobuf encoders disagree " + f"({len(py_bytes)} vs {len(pb_bytes)} bytes)" + ) + + +# ── Encode + serialize: the per-export cost an exporter actually pays ─────── + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +def test_encode_pyproto(benchmark, signal, label, dims) -> None: + py_encode, _pb, factory, _ = _SIGNALS[signal] + payload = factory(dims) + result = benchmark(lambda: py_encode(payload).SerializeToString()) + assert len(result) > 0 + + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +def test_encode_protobuf(benchmark, signal, label, dims) -> None: + _py, pb_encode, factory, _ = _SIGNALS[signal] + payload = factory(dims) + result = benchmark(lambda: pb_encode(payload).SerializeToString()) + assert len(result) > 0 diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py deleted file mode 100644 index 4c2dd7bb427..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_attribute_encoder.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import unittest -from logging import ERROR - -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_attributes, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - ArrayValue as PB2ArrayValue, -) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue - - -class CallingStrRaisesException: - def __str__(self): - raise ValueError("Cannot encode") - - -class TestOTLPAttributeEncoder(unittest.TestCase): - def test_encode_attributes_all_kinds(self): - result = _encode_attributes( - { - "a": 1, # int - "b": 3.14, # float - "c": False, # bool - "hello": "world", # str - "greet": ["hola", "bonjour"], # Sequence[str] - "data": [1, 2], # Sequence[int] - "data_granular": [1.4, 2.4], # Sequence[float] - "binary_data": b"x00\x01\x02", # bytes - } - ) - self.assertEqual( - result, - [ - PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)), - PB2KeyValue(key="b", value=PB2AnyValue(double_value=3.14)), - PB2KeyValue(key="c", value=PB2AnyValue(bool_value=False)), - PB2KeyValue( - key="hello", value=PB2AnyValue(string_value="world") - ), - PB2KeyValue( - key="greet", - value=PB2AnyValue( - array_value=PB2ArrayValue( - values=[ - PB2AnyValue(string_value="hola"), - PB2AnyValue(string_value="bonjour"), - ] - ) - ), - ), - PB2KeyValue( - key="data", - value=PB2AnyValue( - array_value=PB2ArrayValue( - values=[ - PB2AnyValue(int_value=1), - PB2AnyValue(int_value=2), - ] - ) - ), - ), - PB2KeyValue( - key="data_granular", - value=PB2AnyValue( - array_value=PB2ArrayValue( - values=[ - PB2AnyValue(double_value=1.4), - PB2AnyValue(double_value=2.4), - ] - ) - ), - ), - PB2KeyValue( - key="binary_data", - value=PB2AnyValue(bytes_value=b"x00\x01\x02"), - ), - ], - ) - - def test_encode_attributes_error_logs_key(self): - with self.assertLogs(level=ERROR) as error: - result = _encode_attributes( - {"a": 1, "bad_key": CallingStrRaisesException(), "b": 2} - ) - - self.assertEqual(len(error.records), 1) - self.assertEqual(error.records[0].msg, "Failed to encode key %s: %s") - self.assertEqual(error.records[0].args[0], "bad_key") - self.assertIsInstance(error.records[0].args[1], Exception) - self.assertEqual( - result, - [ - PB2KeyValue(key="a", value=PB2AnyValue(int_value=1)), - PB2KeyValue(key="b", value=PB2AnyValue(int_value=2)), - ], - ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py deleted file mode 100644 index e6c1a601c07..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import unittest -from unittest.mock import Mock, patch -from urllib.parse import urlparse - -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, - NoOpExporterMetrics, - create_exporter_metrics, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - - -class TestExporterMetrics(unittest.TestCase): - def test_factory_returns_noop_when_disabled(self): - meter_provider = Mock() - - with patch( - "opentelemetry.exporter.otlp.proto.common." - "_exporter_metrics.get_meter_provider" - ) as get_meter_provider: - metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse("http://localhost:4318/v1/traces"), - meter_provider, - False, - ) - - self.assertIsInstance(metrics, NoOpExporterMetrics) - meter_provider.get_meter.assert_not_called() - get_meter_provider.assert_not_called() - - def test_factory_returns_exporter_metrics_when_enabled(self): - meter_provider = Mock() - meter_provider.get_meter.return_value = Mock() - - metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse("http://localhost:4318/v1/traces"), - meter_provider, - True, - ) - - self.assertIsInstance(metrics, ExporterMetrics) - meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") - - def test_noop_export_operation_yields_result(self): - metrics = NoOpExporterMetrics() - - with metrics.export_operation(1) as result: - result.error = RuntimeError("error") - - self.assertIsInstance(result.error, RuntimeError) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py deleted file mode 100644 index 6a4e2faa2b6..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py +++ /dev/null @@ -1,305 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import unittest - -from opentelemetry._logs import LogRecord, SeverityNumber -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_attributes, - _encode_span_id, - _encode_trace_id, - _encode_value, -) -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ( - ResourceLogs as PB2ResourceLogs, -) -from opentelemetry.proto.logs.v1.logs_pb2 import ScopeLogs as PB2ScopeLogs -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) -from opentelemetry.sdk._logs import LogRecordLimits, ReadWriteLogRecord -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - set_span_in_context, -) - -_CONTEXT_LOG = set_span_in_context( - NonRecordingSpan( - SpanContext( - 89564621134313219400156819398935297684, - 1312458408527513268, - False, - TraceFlags(0x01), - ) - ) -) - - -class TestOTLPLogEncoder(unittest.TestCase): - def test_encode_basic_log_record(self): - basic_log_record = ReadWriteLogRecord( - LogRecord( - timestamp=1644650195189786880, - observed_timestamp=1644650195189786881, - context=_CONTEXT_LOG, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Do not go gentle into that good night. Rage, rage against the dying of the light", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource( - {"first_resource": "value"}, - "resource_schema_url", - ), - instrumentation_scope=InstrumentationScope( - "first_name", "first_version" - ), - ) - pb2_service_request = ExportLogsServiceRequest( - resource_logs=[ - PB2ResourceLogs( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="first_resource", - value=PB2AnyValue(string_value="value"), - ) - ] - ), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="first_name", version="first_version" - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650195189786880, - observed_time_unix_nano=1644650195189786881, - trace_id=_encode_trace_id( - 89564621134313219400156819398935297684 - ), - span_id=_encode_span_id( - 1312458408527513268 - ), - flags=int(TraceFlags(0x01)), - severity_text="WARN", - severity_number=SeverityNumber.WARN.value, - body=_encode_value( - "Do not go gentle into that good night. Rage, rage against the dying of the light" - ), - attributes=_encode_attributes( - {"a": 1, "b": "c"} - ), - ) - ], - ), - ], - schema_url="resource_schema_url", - ) - ] - ) - self.assertEqual(encode_logs([basic_log_record]), pb2_service_request) - - def test_encode_log_record_with_no_instrumentation_scope_and_dict_body( - self, - ): - log_record_with_no_instrumentation_scope_and_dict_body = ( - ReadWriteLogRecord( - LogRecord( - timestamp=1644650427658989056, - observed_timestamp=1644650427658989057, - context=_CONTEXT_LOG, - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG, - body={"error": None, "array_with_nones": [1, None, 2]}, - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=None, - ) - ) - pb2_resource_logs = PB2ResourceLogs( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="second_resource", - value=PB2AnyValue(string_value="CASE"), - ) - ] - ), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope(), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650427658989056, - observed_time_unix_nano=1644650427658989057, - trace_id=_encode_trace_id( - 89564621134313219400156819398935297684 - ), - span_id=_encode_span_id(1312458408527513268), - flags=int(TraceFlags(0x01)), - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG.value, - body=_encode_value( - { - "error": None, - "array_with_nones": [1, None, 2], - } - ), - attributes=_encode_attributes({"a": 1, "b": "c"}), - ) - ], - ) - ], - ) - self.assertEqual( - encode_logs( - [log_record_with_no_instrumentation_scope_and_dict_body] - ), - ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), - ) - - def test_encode_log_record_with_empty_resource_and_dict_attribute_value( - self, - ): - log_record_with_empty_resource_and_dict_attribute_value = ReadWriteLogRecord( - LogRecord( - timestamp=1644650584292683033, - observed_timestamp=1644650584292683033, - context=_CONTEXT_LOG, - severity_text="FATAL", - severity_number=SeverityNumber.FATAL, - body="This instrumentation scope has a schema url and attributes", - attributes={ - "extended": { - "sequence": [{"inner": "mapping", "none": None}] - } - }, - ), - resource=SDKResource({}), - instrumentation_scope=InstrumentationScope( - "scope_with_attributes", - "scope_with_attributes_version", - "instrumentation_schema_url", - {"one": 1, "two": "2"}, - ), - ) - pb2_resource_logs = PB2ResourceLogs( - resource=PB2Resource(attributes=[]), - scope_logs=[ - PB2ScopeLogs( - scope=PB2InstrumentationScope( - name="scope_with_attributes", - version="scope_with_attributes_version", - attributes=_encode_attributes({"one": 1, "two": "2"}), - ), - log_records=[ - PB2LogRecord( - time_unix_nano=1644650584292683033, - observed_time_unix_nano=1644650584292683033, - trace_id=_encode_trace_id( - 89564621134313219400156819398935297684 - ), - span_id=_encode_span_id(1312458408527513268), - flags=int(TraceFlags(0x01)), - severity_text="FATAL", - severity_number=SeverityNumber.FATAL.value, - body=_encode_value( - "This instrumentation scope has a schema url and attributes" - ), - attributes=_encode_attributes( - { - "extended": { - "sequence": [ - {"inner": "mapping", "none": None} - ] - } - } - ), - ) - ], - schema_url="instrumentation_schema_url", - ) - ], - ) - self.assertEqual( - encode_logs( - [log_record_with_empty_resource_and_dict_attribute_value] - ), - ExportLogsServiceRequest(resource_logs=[pb2_resource_logs]), - ) - - def test_dropped_attributes_count(self): - sdk_logs = self._get_test_logs_dropped_attributes() - encoded_logs = encode_logs(sdk_logs) - self.assertTrue(hasattr(sdk_logs[0], "dropped_attributes")) - self.assertEqual( - # pylint:disable=no-member - encoded_logs.resource_logs[0] - .scope_logs[0] - .log_records[0] - .dropped_attributes_count, - 2, - ) - - @staticmethod - def _get_test_logs_dropped_attributes() -> list[ReadWriteLogRecord]: - ctx_log1 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 89564621134313219400156819398935297684, - 1312458408527513268, - False, - TraceFlags(0x01), - ) - ) - ) - log1 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650195189786880, - context=ctx_log1, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Do not go gentle into that good night. Rage, rage against the dying of the light", - attributes={"a": 1, "b": "c", "user_id": "B121092"}, - ), - resource=SDKResource({"first_resource": "value"}), - limits=LogRecordLimits(max_attributes=1), - instrumentation_scope=InstrumentationScope( - "first_name", "first_version" - ), - ) - ctx_log2 = set_span_in_context( - NonRecordingSpan(SpanContext(0, 0, False)) - ) - log2 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650249738562048, - context=ctx_log2, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Cooper, this is no time for caution!", - attributes={}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=InstrumentationScope( - "second_name", "second_version" - ), - ) - - return [log1, log2] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py deleted file mode 100644 index d3b3dc8c5cd..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py +++ /dev/null @@ -1,1090 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=protected-access,too-many-lines -import unittest - -from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( - EncodingException, -) -from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( - encode_metrics, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - InstrumentationScope, - KeyValue, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) -from opentelemetry.sdk.metrics import Exemplar -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - Buckets, - ExponentialHistogramDataPoint, - HistogramDataPoint, - Metric, - MetricsData, - ResourceMetrics, - ScopeMetrics, -) -from opentelemetry.sdk.metrics.export import ( - ExponentialHistogram as ExponentialHistogramType, -) -from opentelemetry.sdk.metrics.export import Histogram as HistogramType -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import ( - InstrumentationScope as SDKInstrumentationScope, -) -from opentelemetry.test.metrictestutil import _generate_gauge, _generate_sum - - -class TestOTLPMetricsEncoder(unittest.TestCase): - span_id = int("6e0c63257de34c92", 16) - trace_id = int("d4cda95b652f4a1592b449d5929fda1b", 16) - - histogram = Metric( - name="histogram", - description="foo", - unit="s", - data=HistogramType( - data_points=[ - HistogramDataPoint( - attributes={"a": 1, "b": True}, - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - exemplars=[ - Exemplar( - {"filtered": "banana"}, - 298.0, - 1641946016139533400, - span_id, - trace_id, - ), - Exemplar( - {"filtered": "banana"}, - 298.0, - 1641946016139533400, - None, - None, - ), - ], - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - min=8, - max=18, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - - def test_encode_sum_int(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_int", 33)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="sum_int", - unit="s", - description="foo", - sum=pb2.Sum( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_int=33, - ) - ], - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_sum_double(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_double", 2.98)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="sum_double", - unit="s", - description="foo", - sum=pb2.Sum( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_double=2.98, - ) - ], - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_gauge_int(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[_generate_gauge("gauge_int", 9000)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="gauge_int", - unit="s", - description="foo", - gauge=pb2.Gauge( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - time_unix_nano=1641946016139533244, - start_time_unix_nano=0, - as_int=9000, - ) - ], - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_gauge_double(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[_generate_gauge("gauge_double", 52.028)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="gauge_double", - unit="s", - description="foo", - gauge=pb2.Gauge( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - time_unix_nano=1641946016139533244, - as_double=52.028, - ) - ], - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_histogram(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[self.histogram], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[ - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - span_id=b"n\x0cc%}\xe3L\x92", - trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - ], - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_multiple_scope_histogram(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[self.histogram, self.histogram], - schema_url="instrumentation_scope_schema_url", - ), - ScopeMetrics( - scope=SDKInstrumentationScope( - name="second_name", - version="second_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[self.histogram], - schema_url="instrumentation_scope_schema_url", - ), - ScopeMetrics( - scope=SDKInstrumentationScope( - name="third_name", - version="third_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[self.histogram], - schema_url="instrumentation_scope_schema_url", - ), - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[ - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - span_id=b"n\x0cc%}\xe3L\x92", - trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - ], - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ), - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[ - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - span_id=b"n\x0cc%}\xe3L\x92", - trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - ], - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ), - ], - ), - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="second_name", version="second_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[ - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - span_id=b"n\x0cc%}\xe3L\x92", - trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - ], - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ), - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="third_name", version="third_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="histogram", - unit="s", - description="foo", - histogram=pb2.Histogram( - data_points=[ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946016139533244, - time_unix_nano=1641946016139533244, - count=5, - sum=67, - bucket_counts=[1, 4], - explicit_bounds=[10.0, 20.0], - exemplars=[ - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - span_id=b"n\x0cc%}\xe3L\x92", - trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b", - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - pb2.Exemplar( - time_unix_nano=1641946016139533400, - as_double=298, - filtered_attributes=[ - KeyValue( - key="filtered", - value=AnyValue( - string_value="banana" - ), - ) - ], - ), - ], - max=18.0, - min=8.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ), - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encode_exponential_histogram(self): - exponential_histogram = Metric( - name="exponential_histogram", - description="description", - unit="unit", - data=ExponentialHistogramType( - data_points=[ - ExponentialHistogramDataPoint( - attributes={"a": 1, "b": True}, - start_time_unix_nano=0, - time_unix_nano=1, - count=2, - sum=3, - scale=4, - zero_count=5, - positive=Buckets(offset=6, bucket_counts=[7, 8]), - negative=Buckets(offset=9, bucket_counts=[10, 11]), - flags=12, - min=13.0, - max=14.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[exponential_histogram], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", version="first_version" - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="exponential_histogram", - unit="unit", - description="description", - exponential_histogram=pb2.ExponentialHistogram( - data_points=[ - pb2.ExponentialHistogramDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=0, - time_unix_nano=1, - count=2, - sum=3, - scale=4, - zero_count=5, - positive=pb2.ExponentialHistogramDataPoint.Buckets( - offset=6, - bucket_counts=[7, 8], - ), - negative=pb2.ExponentialHistogramDataPoint.Buckets( - offset=9, - bucket_counts=[10, 11], - ), - flags=12, - exemplars=[], - min=13.0, - max=14.0, - ) - ], - aggregation_temporality=AggregationTemporality.DELTA, - ), - ) - ], - ) - ], - ) - ] - ) - # pylint: disable=protected-access - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) - - def test_encoding_exception_reraise(self): - # this number is too big to fit in a signed 64-bit proto field and causes a ValueError - big_number = 2**63 - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_double", big_number)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - with self.assertRaises(EncodingException) as context: - encode_metrics(metrics_data) - - # assert that the EncodingException wraps the metric and original exception - assert isinstance(context.exception.metric, Metric) - assert isinstance(context.exception.original_exception, ValueError) - - def test_encode_scope_with_attributes(self): - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes=None, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="instrumentation_scope_schema_url", - attributes={"one": 1, "two": "2"}, - ), - metrics=[_generate_sum("sum_int", 88)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - expected = ExportMetricsServiceRequest( - resource_metrics=[ - pb2.ResourceMetrics( - schema_url="resource_schema_url", - resource=OTLPResource(), - scope_metrics=[ - pb2.ScopeMetrics( - scope=InstrumentationScope( - name="first_name", - version="first_version", - attributes=[ - KeyValue( - key="one", value=AnyValue(int_value=1) - ), - KeyValue( - key="two", - value=AnyValue(string_value="2"), - ), - ], - ), - schema_url="instrumentation_scope_schema_url", - metrics=[ - pb2.Metric( - name="sum_int", - unit="s", - description="foo", - sum=pb2.Sum( - data_points=[ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=True - ), - ), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_int=88, - ) - ], - aggregation_temporality=AggregationTemporality.CUMULATIVE, - is_monotonic=True, - ), - ) - ], - ) - ], - ) - ] - ) - actual = encode_metrics(metrics_data) - self.assertEqual(expected, actual) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py deleted file mode 100644 index 0c685948b75..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_trace_encoder.py +++ /dev/null @@ -1,491 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=protected-access - -import unittest - -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_span_id, - _encode_trace_id, -) -from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import ( - _SPAN_KIND_MAP, - _encode_status, -) -from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest as PB2ExportTraceServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue as PB2AnyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.common.v1.common_pb2 import KeyValue as PB2KeyValue -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( - ResourceSpans as PB2ResourceSpans, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ScopeSpans as PB2ScopeSpans -from opentelemetry.proto.trace.v1.trace_pb2 import Span as PB2SPan -from opentelemetry.proto.trace.v1.trace_pb2 import Status as PB2Status -from opentelemetry.sdk.trace import Event as SDKEvent -from opentelemetry.sdk.trace import Resource as SDKResource -from opentelemetry.sdk.trace import SpanContext as SDKSpanContext -from opentelemetry.sdk.trace import _Span as SDKSpan -from opentelemetry.sdk.util.instrumentation import ( - InstrumentationScope as SDKInstrumentationScope, -) -from opentelemetry.trace import Link as SDKLink -from opentelemetry.trace import SpanKind as SDKSpanKind -from opentelemetry.trace import TraceFlags as SDKTraceFlags -from opentelemetry.trace.status import Status as SDKStatus -from opentelemetry.trace.status import StatusCode as SDKStatusCode - - -class TestOTLPTraceEncoder(unittest.TestCase): - def test_encode_spans(self): - otel_spans, expected_encoding = self.get_exhaustive_test_spans() - self.assertEqual(encode_spans(otel_spans), expected_encoding) - - @staticmethod - def get_exhaustive_otel_span_list() -> list[SDKSpan]: - trace_id = 0x3E0C63257DE34C926F9EFCD03927272E - - base_time = 683647322 * 10**9 # in ns - start_times = ( - base_time, - base_time + 150 * 10**6, - base_time + 300 * 10**6, - base_time + 400 * 10**6, - base_time + 500 * 10**6, - base_time + 600 * 10**6, - ) - end_times = ( - start_times[0] + (50 * 10**6), - start_times[1] + (100 * 10**6), - start_times[2] + (200 * 10**6), - start_times[3] + (300 * 10**6), - start_times[4] + (400 * 10**6), - start_times[5] + (500 * 10**6), - ) - - parent_span_context = SDKSpanContext( - trace_id, 0x1111111111111111, is_remote=True - ) - - other_context = SDKSpanContext( - trace_id, 0x2222222222222222, is_remote=False - ) - - span1 = SDKSpan( - name="test-span-1", - context=SDKSpanContext( - trace_id, - 0x34BF92DEEFC58C92, - is_remote=False, - trace_flags=SDKTraceFlags(SDKTraceFlags.SAMPLED), - ), - parent=parent_span_context, - events=( - SDKEvent( - name="event0", - timestamp=base_time + 50 * 10**6, - attributes={ - "annotation_bool": True, - "annotation_string": "annotation_test", - "key_float": 0.3, - }, - ), - ), - links=( - SDKLink(context=other_context, attributes={"key_bool": True}), - ), - resource=SDKResource({}, "resource_schema_url"), - ) - span1.start(start_time=start_times[0]) - span1.set_attribute("key_bool", False) - span1.set_attribute("key_string", "hello_world") - span1.set_attribute("key_float", 111.22) - span1.set_status(SDKStatus(SDKStatusCode.ERROR, "Example description")) - span1.end(end_time=end_times[0]) - - span2 = SDKSpan( - name="test-span-2", - context=parent_span_context, - parent=None, - resource=SDKResource(attributes={"key_resource": "some_resource"}), - ) - span2.start(start_time=start_times[1]) - span2.end(end_time=end_times[1]) - - span3 = SDKSpan( - name="test-span-3", - context=other_context, - parent=None, - resource=SDKResource(attributes={"key_resource": "some_resource"}), - ) - span3.start(start_time=start_times[2]) - span3.set_attribute("key_string", "hello_world") - span3.end(end_time=end_times[2]) - - span4 = SDKSpan( - name="test-span-4", - context=other_context, - parent=None, - resource=SDKResource({}, "resource_schema_url"), - instrumentation_scope=SDKInstrumentationScope( - name="name", version="version" - ), - ) - span4.start(start_time=start_times[3]) - span4.end(end_time=end_times[3]) - - span5 = SDKSpan( - name="test-span-5", - context=other_context, - parent=None, - resource=SDKResource( - attributes={"key_resource": "another_resource"}, - schema_url="resource_schema_url", - ), - instrumentation_scope=SDKInstrumentationScope( - name="scope_1_name", - version="scope_1_version", - schema_url="scope_1_schema_url", - ), - ) - span5.start(start_time=start_times[4]) - span5.end(end_time=end_times[4]) - - span6 = SDKSpan( - name="test-span-6", - context=other_context, - parent=None, - resource=SDKResource( - attributes={"key_resource": "another_resource"}, - schema_url="resource_schema_url", - ), - instrumentation_scope=SDKInstrumentationScope( - name="scope_2_name", - version="scope_2_version", - schema_url="scope_2_schema_url", - attributes={"one": "1", "two": 2}, - ), - ) - span6.start(start_time=start_times[5]) - span6.end(end_time=end_times[5]) - - return [span1, span2, span3, span4, span5, span6] - - def get_exhaustive_test_spans( - self, - ) -> tuple[list[SDKSpan], PB2ExportTraceServiceRequest]: - otel_spans = self.get_exhaustive_otel_span_list() - trace_id = _encode_trace_id(otel_spans[0].context.trace_id) - span_kind = _SPAN_KIND_MAP[SDKSpanKind.INTERNAL] - - pb2_service_request = PB2ExportTraceServiceRequest( - resource_spans=[ - PB2ResourceSpans( - schema_url="resource_schema_url", - resource=PB2Resource(), - scope_spans=[ - PB2ScopeSpans( - scope=PB2InstrumentationScope(), - spans=[ - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[0].context.span_id - ), - trace_state=None, - parent_span_id=_encode_span_id( - otel_spans[0].parent.span_id - ), - name=otel_spans[0].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 0 - ].start_time, - end_time_unix_nano=otel_spans[0].end_time, - attributes=[ - PB2KeyValue( - key="key_bool", - value=PB2AnyValue( - bool_value=False - ), - ), - PB2KeyValue( - key="key_string", - value=PB2AnyValue( - string_value="hello_world" - ), - ), - PB2KeyValue( - key="key_float", - value=PB2AnyValue( - double_value=111.22 - ), - ), - ], - events=[ - PB2SPan.Event( - name="event0", - time_unix_nano=otel_spans[0] - .events[0] - .timestamp, - attributes=[ - PB2KeyValue( - key="annotation_bool", - value=PB2AnyValue( - bool_value=True - ), - ), - PB2KeyValue( - key="annotation_string", - value=PB2AnyValue( - string_value="annotation_test" - ), - ), - PB2KeyValue( - key="key_float", - value=PB2AnyValue( - double_value=0.3 - ), - ), - ], - ) - ], - links=[ - PB2SPan.Link( - trace_id=_encode_trace_id( - otel_spans[0] - .links[0] - .context.trace_id - ), - span_id=_encode_span_id( - otel_spans[0] - .links[0] - .context.span_id - ), - attributes=[ - PB2KeyValue( - key="key_bool", - value=PB2AnyValue( - bool_value=True - ), - ), - ], - flags=0x100, - ) - ], - status=PB2Status( - code=SDKStatusCode.ERROR.value, - message="Example description", - ), - flags=0x300, - ) - ], - ), - PB2ScopeSpans( - scope=PB2InstrumentationScope( - name="name", - version="version", - ), - spans=[ - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[3].context.span_id - ), - trace_state=None, - parent_span_id=None, - name=otel_spans[3].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 3 - ].start_time, - end_time_unix_nano=otel_spans[3].end_time, - attributes=None, - events=None, - links=None, - status={}, - flags=0x100, - ) - ], - ), - ], - ), - PB2ResourceSpans( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="key_resource", - value=PB2AnyValue( - string_value="some_resource" - ), - ) - ] - ), - scope_spans=[ - PB2ScopeSpans( - scope=PB2InstrumentationScope(), - spans=[ - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[1].context.span_id - ), - trace_state=None, - parent_span_id=None, - name=otel_spans[1].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 1 - ].start_time, - end_time_unix_nano=otel_spans[1].end_time, - attributes=None, - events=None, - links=None, - status={}, - flags=0x100, - ), - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[2].context.span_id - ), - trace_state=None, - parent_span_id=None, - name=otel_spans[2].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 2 - ].start_time, - end_time_unix_nano=otel_spans[2].end_time, - attributes=[ - PB2KeyValue( - key="key_string", - value=PB2AnyValue( - string_value="hello_world" - ), - ), - ], - events=None, - links=None, - status={}, - flags=0x100, - ), - ], - ) - ], - ), - PB2ResourceSpans( - resource=PB2Resource( - attributes=[ - PB2KeyValue( - key="key_resource", - value=PB2AnyValue( - string_value="another_resource" - ), - ), - ], - ), - schema_url="resource_schema_url", - scope_spans=[ - PB2ScopeSpans( - scope=PB2InstrumentationScope( - name="scope_1_name", version="scope_1_version" - ), - schema_url="scope_1_schema_url", - spans=[ - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[4].context.span_id - ), - trace_state=None, - parent_span_id=None, - name=otel_spans[4].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 4 - ].start_time, - end_time_unix_nano=otel_spans[4].end_time, - attributes=None, - events=None, - links=None, - status={}, - flags=0x100, - ), - ], - ), - PB2ScopeSpans( - scope=PB2InstrumentationScope( - name="scope_2_name", - version="scope_2_version", - attributes=[ - PB2KeyValue( - key="one", - value=PB2AnyValue(string_value="1"), - ), - PB2KeyValue( - key="two", - value=PB2AnyValue(int_value=2), - ), - ], - ), - schema_url="scope_2_schema_url", - spans=[ - PB2SPan( - trace_id=trace_id, - span_id=_encode_span_id( - otel_spans[5].context.span_id - ), - trace_state=None, - parent_span_id=None, - name=otel_spans[5].name, - kind=span_kind, - start_time_unix_nano=otel_spans[ - 5 - ].start_time, - end_time_unix_nano=otel_spans[5].end_time, - attributes=None, - events=None, - links=None, - status={}, - flags=0x100, - ), - ], - ), - ], - ), - ] - ) - - return otel_spans, pb2_service_request - - def test_encode_status_code_translations(self): - self.assertEqual( - _encode_status(SDKStatus(status_code=SDKStatusCode.UNSET)), - PB2Status( - code=SDKStatusCode.UNSET.value, - ), - ) - - self.assertEqual( - _encode_status(SDKStatus(status_code=SDKStatusCode.OK)), - PB2Status( - code=SDKStatusCode.OK.value, - ), - ) - - self.assertEqual( - _encode_status(SDKStatus(status_code=SDKStatusCode.ERROR)), - PB2Status( - code=SDKStatusCode.ERROR.value, - ), - ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 8fa96cdcf80..c6152e4ba77 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -26,10 +26,6 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "googleapis-common-protos ~= 1.57", - "grpcio >= 1.63.2, < 2.0.0; python_version < '3.13'", - "grpcio >= 1.66.2, < 2.0.0; python_version == '3.13'", - "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.45.0.dev", "opentelemetry-sdk ~= 1.45.0.dev", @@ -50,13 +46,9 @@ otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.trace_exporter:OTLPSpa Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc" Repository = "https://github.com/open-telemetry/opentelemetry-python" -[project.optional-dependencies] -gcp-auth = [ - "opentelemetry-exporter-credential-provider-gcp >= 0.59b0", -] [tool.hatch.version] -path = "src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py" +path = "src/opentelemetry/exporter/otlp/_proto/grpc/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/__init__.py new file mode 100644 index 00000000000..a24bc04a0e2 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/__init__.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from .version import __version__ + +_USER_AGENT_HEADER_VALUE = "OTel-OTLP-Exporter-Python/" + __version__ +_OTLP_GRPC_CHANNEL_OPTIONS = [ + ("grpc.primary_user_agent", _USER_AGENT_HEADER_VALUE) +] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_log_exporter/__init__.py new file mode 100644 index 00000000000..0ddc2bccd9a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_log_exporter/__init__.py @@ -0,0 +1,122 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Iterable, Sequence +from collections.abc import Sequence as TypingSequence +from os import environ +from typing import Literal + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import ( + ChannelCredentials, + Compression, + StatusCode, +) + +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import encode_logs +from opentelemetry.exporter.otlp._proto.grpc.exporter import ( + OTLPExporterMixin, + _get_credentials, + environ_to_compression, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest +from opentelemetry._proto.collector.logs.v1.logs_service_pb2_grpc import LogsServiceStub +from opentelemetry.sdk._logs import ReadableLogRecord +from opentelemetry.sdk._logs.export import LogRecordExporter, LogRecordExportResult +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_INSECURE, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import OtelComponentTypeValues + + +class OTLPLogExporter( + LogRecordExporter, + OTLPExporterMixin[ + Sequence[ReadableLogRecord], + ExportLogsServiceRequest, + LogRecordExportResult, + LogsServiceStub, + ], +): + def __init__( + self, + endpoint: str | None = None, + insecure: bool | None = None, + credentials: ChannelCredentials | None = None, + headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, + timeout: float | None = None, + compression: Compression | None = None, + channel_options: tuple[tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) + if insecure is None and insecure_logs is not None: + insecure = insecure_logs.lower() == "true" + + if not insecure and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None: + credentials = _get_credentials( + credentials, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) + environ_timeout = float(environ_timeout) if environ_timeout is not None else None + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) + if compression is None + else compression + ) + + OTLPExporterMixin.__init__( + self, + endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT), + insecure=insecure, + credentials=credentials, + headers=headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS), + timeout=timeout or environ_timeout, + compression=compression, + stub=LogsServiceStub, + result=LogRecordExportResult, + channel_options=channel_options, + retryable_error_codes=retryable_error_codes, + component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, + signal="logs", + meter_provider=meter_provider, + ) + + def _translate_data(self, data: Sequence[ReadableLogRecord]) -> ExportLogsServiceRequest: + return encode_logs(data) + + def _count_data(self, data: Sequence[ReadableLogRecord]) -> int: + return len(data) + + def export( + self, + batch: Sequence[ReadableLogRecord], + ) -> Literal[LogRecordExportResult.SUCCESS, LogRecordExportResult.FAILURE]: + return OTLPExporterMixin._export(self, batch) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True + + @property + def _exporting(self) -> str: + return "logs" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/__init__.py new file mode 100644 index 00000000000..e57cf4aba95 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/__init__.py @@ -0,0 +1,2 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/_huffman_table.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/_huffman_table.py new file mode 100644 index 00000000000..1f021afbdb8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/_huffman_table.py @@ -0,0 +1,267 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""The HPACK Huffman code table from RFC 7541 appendix B. + +One ``(code, bit_length)`` pair per symbol 0-255, plus the 30-bit EOS +symbol at index 256. Data transcribed from the RFC.""" + +HUFFMAN_CODES = ( + (0x1ff8, 13), + (0x7fffd8, 23), + (0xfffffe2, 28), + (0xfffffe3, 28), + (0xfffffe4, 28), + (0xfffffe5, 28), + (0xfffffe6, 28), + (0xfffffe7, 28), + (0xfffffe8, 28), + (0xffffea, 24), + (0x3ffffffc, 30), + (0xfffffe9, 28), + (0xfffffea, 28), + (0x3ffffffd, 30), + (0xfffffeb, 28), + (0xfffffec, 28), + (0xfffffed, 28), + (0xfffffee, 28), + (0xfffffef, 28), + (0xffffff0, 28), + (0xffffff1, 28), + (0xffffff2, 28), + (0x3ffffffe, 30), + (0xffffff3, 28), + (0xffffff4, 28), + (0xffffff5, 28), + (0xffffff6, 28), + (0xffffff7, 28), + (0xffffff8, 28), + (0xffffff9, 28), + (0xffffffa, 28), + (0xffffffb, 28), + (0x14, 6), + (0x3f8, 10), + (0x3f9, 10), + (0xffa, 12), + (0x1ff9, 13), + (0x15, 6), + (0xf8, 8), + (0x7fa, 11), + (0x3fa, 10), + (0x3fb, 10), + (0xf9, 8), + (0x7fb, 11), + (0xfa, 8), + (0x16, 6), + (0x17, 6), + (0x18, 6), + (0x0, 5), + (0x1, 5), + (0x2, 5), + (0x19, 6), + (0x1a, 6), + (0x1b, 6), + (0x1c, 6), + (0x1d, 6), + (0x1e, 6), + (0x1f, 6), + (0x5c, 7), + (0xfb, 8), + (0x7ffc, 15), + (0x20, 6), + (0xffb, 12), + (0x3fc, 10), + (0x1ffa, 13), + (0x21, 6), + (0x5d, 7), + (0x5e, 7), + (0x5f, 7), + (0x60, 7), + (0x61, 7), + (0x62, 7), + (0x63, 7), + (0x64, 7), + (0x65, 7), + (0x66, 7), + (0x67, 7), + (0x68, 7), + (0x69, 7), + (0x6a, 7), + (0x6b, 7), + (0x6c, 7), + (0x6d, 7), + (0x6e, 7), + (0x6f, 7), + (0x70, 7), + (0x71, 7), + (0x72, 7), + (0xfc, 8), + (0x73, 7), + (0xfd, 8), + (0x1ffb, 13), + (0x7fff0, 19), + (0x1ffc, 13), + (0x3ffc, 14), + (0x22, 6), + (0x7ffd, 15), + (0x3, 5), + (0x23, 6), + (0x4, 5), + (0x24, 6), + (0x5, 5), + (0x25, 6), + (0x26, 6), + (0x27, 6), + (0x6, 5), + (0x74, 7), + (0x75, 7), + (0x28, 6), + (0x29, 6), + (0x2a, 6), + (0x7, 5), + (0x2b, 6), + (0x76, 7), + (0x2c, 6), + (0x8, 5), + (0x9, 5), + (0x2d, 6), + (0x77, 7), + (0x78, 7), + (0x79, 7), + (0x7a, 7), + (0x7b, 7), + (0x7ffe, 15), + (0x7fc, 11), + (0x3ffd, 14), + (0x1ffd, 13), + (0xffffffc, 28), + (0xfffe6, 20), + (0x3fffd2, 22), + (0xfffe7, 20), + (0xfffe8, 20), + (0x3fffd3, 22), + (0x3fffd4, 22), + (0x3fffd5, 22), + (0x7fffd9, 23), + (0x3fffd6, 22), + (0x7fffda, 23), + (0x7fffdb, 23), + (0x7fffdc, 23), + (0x7fffdd, 23), + (0x7fffde, 23), + (0xffffeb, 24), + (0x7fffdf, 23), + (0xffffec, 24), + (0xffffed, 24), + (0x3fffd7, 22), + (0x7fffe0, 23), + (0xffffee, 24), + (0x7fffe1, 23), + (0x7fffe2, 23), + (0x7fffe3, 23), + (0x7fffe4, 23), + (0x1fffdc, 21), + (0x3fffd8, 22), + (0x7fffe5, 23), + (0x3fffd9, 22), + (0x7fffe6, 23), + (0x7fffe7, 23), + (0xffffef, 24), + (0x3fffda, 22), + (0x1fffdd, 21), + (0xfffe9, 20), + (0x3fffdb, 22), + (0x3fffdc, 22), + (0x7fffe8, 23), + (0x7fffe9, 23), + (0x1fffde, 21), + (0x7fffea, 23), + (0x3fffdd, 22), + (0x3fffde, 22), + (0xfffff0, 24), + (0x1fffdf, 21), + (0x3fffdf, 22), + (0x7fffeb, 23), + (0x7fffec, 23), + (0x1fffe0, 21), + (0x1fffe1, 21), + (0x3fffe0, 22), + (0x1fffe2, 21), + (0x7fffed, 23), + (0x3fffe1, 22), + (0x7fffee, 23), + (0x7fffef, 23), + (0xfffea, 20), + (0x3fffe2, 22), + (0x3fffe3, 22), + (0x3fffe4, 22), + (0x7ffff0, 23), + (0x3fffe5, 22), + (0x3fffe6, 22), + (0x7ffff1, 23), + (0x3ffffe0, 26), + (0x3ffffe1, 26), + (0xfffeb, 20), + (0x7fff1, 19), + (0x3fffe7, 22), + (0x7ffff2, 23), + (0x3fffe8, 22), + (0x1ffffec, 25), + (0x3ffffe2, 26), + (0x3ffffe3, 26), + (0x3ffffe4, 26), + (0x7ffffde, 27), + (0x7ffffdf, 27), + (0x3ffffe5, 26), + (0xfffff1, 24), + (0x1ffffed, 25), + (0x7fff2, 19), + (0x1fffe3, 21), + (0x3ffffe6, 26), + (0x7ffffe0, 27), + (0x7ffffe1, 27), + (0x3ffffe7, 26), + (0x7ffffe2, 27), + (0xfffff2, 24), + (0x1fffe4, 21), + (0x1fffe5, 21), + (0x3ffffe8, 26), + (0x3ffffe9, 26), + (0xffffffd, 28), + (0x7ffffe3, 27), + (0x7ffffe4, 27), + (0x7ffffe5, 27), + (0xfffec, 20), + (0xfffff3, 24), + (0xfffed, 20), + (0x1fffe6, 21), + (0x3fffe9, 22), + (0x1fffe7, 21), + (0x1fffe8, 21), + (0x7ffff3, 23), + (0x3fffea, 22), + (0x3fffeb, 22), + (0x1ffffee, 25), + (0x1ffffef, 25), + (0xfffff4, 24), + (0xfffff5, 24), + (0x3ffffea, 26), + (0x7ffff4, 23), + (0x3ffffeb, 26), + (0x7ffffe6, 27), + (0x3ffffec, 26), + (0x3ffffed, 26), + (0x7ffffe7, 27), + (0x7ffffe8, 27), + (0x7ffffe9, 27), + (0x7ffffea, 27), + (0x7ffffeb, 27), + (0xffffffe, 28), + (0x7ffffec, 27), + (0x7ffffed, 27), + (0x7ffffee, 27), + (0x7ffffef, 27), + (0x7fffff0, 27), + (0x3ffffee, 26), + (0x3fffffff, 30), +) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/api.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/api.py new file mode 100644 index 00000000000..b92fc993209 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/api.py @@ -0,0 +1,149 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""grpc-python-compatible surface backed by the pure-Python _pygrpc transport. + +The OTLP gRPC exporter and the generated service stubs are written against a +small slice of the ``grpc`` module: ``Compression``, ``StatusCode``, +``RpcError``, ``ChannelCredentials``, ``ssl_channel_credentials``, and the +``insecure_channel`` / ``secure_channel`` factories whose channels expose +``unary_unary``. This module provides exactly that surface over ``_pygrpc`` so +the exporter runs without the ``grpcio`` C extension. Only unary calls are +supported (all OTLP export RPCs are unary). +""" + +import os +import ssl +import tempfile + +from .client import Channel as _PyGrpcChannel +from .client import Compression, RpcError, StatusCode + +__all__ = [ + "ChannelCredentials", + "Compression", + "RpcError", + "StatusCode", + "insecure_channel", + "secure_channel", + "ssl_channel_credentials", +] + + +class ChannelCredentials: + """TLS credentials for a secure channel, wrapping a configured SSLContext.""" + + def __init__(self, ssl_context): + self.ssl_context = ssl_context + + +def _write_temp(data): + if isinstance(data, str): + data = data.encode() + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "wb") as handle: + handle.write(data) + return path + + +def ssl_channel_credentials( + root_certificates=None, private_key=None, certificate_chain=None +): + """Build TLS credentials. ``root_certificates``, ``private_key``, and + ``certificate_chain`` are PEM bytes (as the exporter reads them from the + OTEL_EXPORTER_OTLP_* certificate files). ALPN ``h2`` is applied by the + transport when it wraps the socket, so it is not set here.""" + context = ssl.create_default_context() + if root_certificates: + cadata = ( + root_certificates.decode("ascii") + if isinstance(root_certificates, (bytes, bytearray)) + else root_certificates + ) + context.load_verify_locations(cadata=cadata) + if certificate_chain or private_key: + # ssl.load_cert_chain reads files only; stage the in-memory PEM in + # short-lived temp files, removed as soon as it is loaded. + cert_file = key_file = None + try: + if certificate_chain: + cert_file = _write_temp(certificate_chain) + if private_key: + key_file = _write_temp(private_key) + context.load_cert_chain(certfile=cert_file, keyfile=key_file) + finally: + for path in (cert_file, key_file): + if path: + try: + os.unlink(path) + except OSError: + pass + return ChannelCredentials(context) + + +def _strip_scheme(target): + for scheme in ("https://", "http://"): + if target.startswith(scheme): + return target[len(scheme) :] + return target + + +class _UnaryUnaryMultiCallable: + """The callable a service stub binds to one method.""" + + def __init__(self, channel, method, request_serializer, response_deserializer): + self._channel = channel + self._method = method + self._serialize = request_serializer + self._deserialize = response_deserializer + + def __call__(self, request, metadata=(), timeout=None): + response_bytes = self._channel._call( + self._method, self._serialize(request), metadata, timeout + ) + return self._deserialize(response_bytes) + + +class _Channel: + """A grpc-style channel over one _pygrpc connection.""" + + def __init__( + self, target, use_tls, ssl_context=None, compression=Compression.NoCompression + ): + self._pygrpc = _PyGrpcChannel( + _strip_scheme(target), use_tls=use_tls, ssl_context=ssl_context + ) + self._compression = compression or Compression.NoCompression + + def _call(self, method, request_bytes, metadata, timeout): + return self._pygrpc.unary_call( + method, + request_bytes, + metadata=tuple(metadata or ()), + timeout=timeout, + compression=self._compression, + ) + + def unary_unary(self, method, request_serializer, response_deserializer): + return _UnaryUnaryMultiCallable( + self, method, request_serializer, response_deserializer + ) + + def close(self): + self._pygrpc.close() + + +# grpc-python signature parity: (target, options=None, compression=None) and +# (target, credentials, options=None, compression=None). ``options`` are +# grpcio channel tuning hints with no _pygrpc equivalent and are ignored. +def insecure_channel(target, options=None, compression=None): + return _Channel(target, use_tls=False, compression=compression) + + +def secure_channel(target, credentials, options=None, compression=None): + return _Channel( + target, + use_tls=True, + ssl_context=credentials.ssl_context, + compression=compression, + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/client.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/client.py new file mode 100644 index 00000000000..80b3653ae68 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/client.py @@ -0,0 +1,281 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Unary gRPC calls over the pure-Python HTTP/2 connection.""" + +import enum +import socket +import struct +import zlib + +from .connection import ( + ConnectionTerminated, + Deadline, + H2Connection, + TransportError, +) + + +class StatusCode(enum.IntEnum): + """gRPC status codes (subset semantics identical to grpc.StatusCode).""" + + OK = 0 + CANCELLED = 1 + UNKNOWN = 2 + INVALID_ARGUMENT = 3 + DEADLINE_EXCEEDED = 4 + NOT_FOUND = 5 + ALREADY_EXISTS = 6 + PERMISSION_DENIED = 7 + RESOURCE_EXHAUSTED = 8 + FAILED_PRECONDITION = 9 + ABORTED = 10 + OUT_OF_RANGE = 11 + UNIMPLEMENTED = 12 + INTERNAL = 13 + UNAVAILABLE = 14 + DATA_LOSS = 15 + UNAUTHENTICATED = 16 + + +class RpcError(Exception): + def __init__(self, status_code, details=""): + super().__init__("{}: {}".format(status_code.name, details)) + self._status_code = status_code + self._details = details + + def code(self): + return self._status_code + + def details(self): + return self._details + + +def _grpc_timeout_header(timeout_seconds): + # grpc-timeout is a value plus a unit, with at most 8 digits (gRPC + # over-HTTP2 spec). Milliseconds keep sub-second precision for normal + # export timeouts; fall back to coarser units when the millisecond value + # would overflow 8 digits (~27.7 hours). + for scale, unit in ((1000, b"m"), (1, b"S"), (1.0 / 60, b"M"), (1.0 / 3600, b"H")): + value = max(1, int(timeout_seconds * scale)) + if value < 100000000: + return b"%d%s" % (value, unit) + return b"99999999H" + + +# Cap on a single decompressed response message, bounding the amplification a +# gzip bomb from a malicious endpoint can inflict on an in-process client. +# gRPC's own default receive limit is 4 MiB; OTLP export responses are far +# smaller. +MAX_DECOMPRESSED_MESSAGE = 4 << 20 + + +def _gunzip(data, max_size): + """Decompress a gzip (RFC 1952) gRPC message, rejecting output over + ``max_size`` bytes so a small compressed payload cannot expand without + bound.""" + decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS) + try: + out = decompressor.decompress(data, max_size) + if decompressor.unconsumed_tail: + raise RpcError( + StatusCode.RESOURCE_EXHAUSTED, + "decompressed message exceeds {}-byte limit".format(max_size), + ) + return out + decompressor.flush() + except zlib.error as error: + raise RpcError( + StatusCode.INTERNAL, "corrupt gzip response: {}".format(error) + ) from error + + +def _frame_message(message_bytes, compress): + if compress: + # gzip wrapper, not raw deflate: gRPC's "gzip" encoding is RFC 1952. + compressor = zlib.compressobj(9, zlib.DEFLATED, 16 + zlib.MAX_WBITS) + message_bytes = compressor.compress(message_bytes) + compressor.flush() + return struct.pack(">BL", 1 if compress else 0, len(message_bytes)) + message_bytes + + +def _unframe_messages(body, encoding): + messages = [] + offset = 0 + while offset < len(body): + if offset + 5 > len(body): + raise RpcError(StatusCode.INTERNAL, "truncated gRPC message prefix") + compressed_flag, length = struct.unpack_from(">BL", body, offset) + offset += 5 + if offset + length > len(body): + raise RpcError(StatusCode.INTERNAL, "truncated gRPC message body") + message = bytes(body[offset : offset + length]) + offset += length + if compressed_flag: + if encoding != b"gzip": + raise RpcError( + StatusCode.INTERNAL, + "compressed message with unsupported grpc-encoding {!r}".format( + encoding + ), + ) + message = _gunzip(message, MAX_DECOMPRESSED_MESSAGE) + messages.append(message) + return messages + + +class Compression(enum.Enum): + NoCompression = 0 + Gzip = 2 + + +# gRPC's HTTP-status-to-grpc-status mapping for responses that never carry a +# grpc-status trailer (gRPC over HTTP/2 spec, "Responses" section). +_HTTP_TO_GRPC_STATUS = { + b"400": StatusCode.INTERNAL, + b"401": StatusCode.UNAUTHENTICATED, + b"403": StatusCode.PERMISSION_DENIED, + b"404": StatusCode.UNIMPLEMENTED, + b"429": StatusCode.UNAVAILABLE, + b"502": StatusCode.UNAVAILABLE, + b"503": StatusCode.UNAVAILABLE, + b"504": StatusCode.UNAVAILABLE, +} + + +class Channel: + """A lazily connected channel to one gRPC endpoint, unary calls only. + + ``target`` is ``host:port``. One transparent reconnect per call absorbs + idle timeouts and graceful GOAWAYs. + """ + + def __init__(self, target, use_tls=True, ssl_context=None): + host, sep, port = target.rpartition(":") + if not sep or not host or not port.isdigit(): + raise ValueError( + "target must be host:port with a numeric port, got {!r}. " + "Bracket IPv6 literals, e.g. [::1]:4317.".format(target) + ) + # The authority header keeps the target verbatim (brackets included); + # the socket and TLS SNI need the IPv6 literal without its brackets. + if host.startswith("[") and host.endswith("]"): + host = host[1:-1] + self._host = host + self._port = int(port) + self._authority = target + self._use_tls = use_tls + self._ssl_context = ssl_context + self._connection = None + + def close(self): + if self._connection is not None: + self._connection.close() + self._connection = None + + def _connect(self, deadline): + if self._connection is None: + self._connection = H2Connection( + self._host, + self._port, + self._use_tls, + ssl_context=self._ssl_context, + connect_timeout=deadline.remaining(), + ) + return self._connection + + def unary_call( + self, + path, + request_bytes, + metadata=(), + timeout=10.0, + compression=Compression.NoCompression, + ): + """Invoke ``path`` (``/package.Service/Method``); returns the response + message bytes, or raises RpcError.""" + # One deadline spans both attempts (connect, TLS, preface, and I/O), so + # a transparent reconnect cannot double the caller's timeout budget. + deadline = Deadline(timeout) + for attempt in (1, 2): + try: + return self._unary_call_once( + path, request_bytes, metadata, deadline, compression + ) + except socket.timeout as error: + # A timeout is terminal: the deadline is shared across attempts, + # so a retry has no budget left. + self.close() + raise RpcError(StatusCode.DEADLINE_EXCEEDED, str(error)) from error + except ConnectionTerminated as error: + self.close() + # Retry only a stream the server did not process (GOAWAY + # last_stream_id below ours); otherwise a retry could duplicate + # an export the server already accepted. + if attempt == 1 and not error.stream_processed: + continue + raise RpcError(StatusCode.UNAVAILABLE, str(error)) from error + except (TransportError, OSError) as error: + self.close() + if attempt == 2: + raise RpcError(StatusCode.UNAVAILABLE, str(error)) from error + + def _unary_call_once(self, path, request_bytes, metadata, deadline, compression): + connection = self._connect(deadline) + compress = compression == Compression.Gzip + + headers = [ + (b":method", b"POST"), + (b":scheme", b"https" if self._use_tls else b"http"), + (b":path", path.encode() if isinstance(path, str) else path), + (b":authority", self._authority.encode()), + (b"te", b"trailers"), + (b"content-type", b"application/grpc"), + (b"user-agent", b"otlp-pyproto-python/0.1"), + ] + # Advertise the remaining budget so the server can abandon work once we + # would give up; remaining() raises socket.timeout if already expired. + remaining = deadline.remaining() + if remaining is not None: + headers.append((b"grpc-timeout", _grpc_timeout_header(remaining))) + if compress: + headers.append((b"grpc-encoding", b"gzip")) + headers.append((b"grpc-accept-encoding", b"identity, gzip")) + for name, value in metadata: + name = name.encode() if isinstance(name, str) else name + value = value.encode() if isinstance(value, str) else value + headers.append((name.lower(), value)) + + body = _frame_message(request_bytes, compress) + header_sets, response_body = connection.request(headers, body, deadline) + + response_headers = dict(header_sets[0]) if header_sets else {} + trailers = dict(header_sets[-1]) if header_sets else {} + + http_status = response_headers.get(b":status") + if http_status is not None and http_status != b"200": + raise RpcError( + _HTTP_TO_GRPC_STATUS.get(http_status, StatusCode.UNKNOWN), + "HTTP status {}".format(http_status.decode()), + ) + + grpc_status = trailers.get(b"grpc-status") + if grpc_status is None: + raise RpcError(StatusCode.INTERNAL, "missing grpc-status trailer") + try: + status_code = StatusCode(int(grpc_status)) + except ValueError: + # Non-numeric, or a numeric code outside the known set: gRPC maps + # both to UNKNOWN rather than crashing the caller. + status_code = StatusCode.UNKNOWN + if status_code != StatusCode.OK: + message = trailers.get(b"grpc-message", b"") + raise RpcError(status_code, message.decode("utf-8", "replace")) + + messages = _unframe_messages( + response_body, response_headers.get(b"grpc-encoding", b"identity") + ) + if len(messages) != 1: + raise RpcError( + StatusCode.INTERNAL, + "expected exactly 1 response message, got {}".format(len(messages)), + ) + return messages[0] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/connection.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/connection.py new file mode 100644 index 00000000000..fdd4b3faf3e --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/connection.py @@ -0,0 +1,375 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""A minimal HTTP/2 client connection for unary gRPC calls. + +Speaks exactly the subset of RFC 7540 a unary gRPC client needs: connection +preface and SETTINGS exchange, one request stream at a time, flow-control +accounting on both directions, PING replies, and GOAWAY handling. +No server push (disabled via SETTINGS), no priorities, no concurrent streams. +""" + +import socket +import ssl +import struct +import time + +from . import frames +from .hpack import Decoder as HpackDecoder +from .hpack import HpackError +from .hpack import encode as hpack_encode +from .hpack import encode_dynamic_table_size_update + +# Caps on server-controlled sizes, to bound the memory a malicious or +# misconfigured endpoint can make an in-process client allocate. gRPC unary +# OTLP responses are tiny (an empty or small partial-success message), so these +# limits are generous headroom, not tuning knobs. +MAX_RECV_FRAME_SIZE = 1 << 20 # reject any single frame larger than 1 MiB +MAX_RESPONSE_BODY = 4 << 20 # total DATA payload accepted per response +MAX_HEADER_BLOCK = 1 << 20 # total HEADERS + CONTINUATION payload per block + + +class TransportError(Exception): + """Connection-level failure: the connection is no longer usable.""" + + +class ConnectionTerminated(TransportError): + """The peer sent GOAWAY. + + ``last_stream_id`` is the highest stream the peer promises it processed; + a stream with a higher id was not processed and is safe to retry. + ``stream_processed`` is set by request() once it knows the active stream id. + """ + + def __init__(self, last_stream_id, error_code, debug_data): + super().__init__( + "GOAWAY last_stream_id={} error_code={} debug={!r}".format( + last_stream_id, error_code, debug_data + ) + ) + self.last_stream_id = last_stream_id + self.error_code = error_code + self.debug_data = debug_data + self.stream_processed = True # conservative until request() decides + + +class StreamReset(TransportError): + """The peer sent RST_STREAM for the active stream.""" + + def __init__(self, error_code): + super().__init__("RST_STREAM error_code={}".format(error_code)) + self.error_code = error_code + + +class Deadline: + def __init__(self, timeout): + self._expires = None if timeout is None else time.monotonic() + timeout + + def remaining(self): + if self._expires is None: + return None + remaining = self._expires - time.monotonic() + if remaining <= 0: + raise socket.timeout("deadline exceeded") + return remaining + + +class H2Connection: + """One HTTP/2 connection; streams are used strictly sequentially.""" + + def __init__( + self, host, port, use_tls, ssl_context=None, connect_timeout=10, sock=None + ): + # ``sock`` injects a ready transport (a connected, ALPN-negotiated + # socket) instead of dialing one. Production callers never pass it; it + # is the seam unit tests use to drive the protocol over an in-memory + # socket. When provided, dialing and TLS setup are skipped. + self._host = host + self._port = port + self._next_stream_id = 1 + self._recv_buffer = b"" + self._peer_max_frame_size = frames.DEFAULT_MAX_FRAME_SIZE + self._peer_initial_window = frames.DEFAULT_INITIAL_WINDOW_SIZE + self._send_window_connection = frames.DEFAULT_INITIAL_WINDOW_SIZE + self._hpack_decoder = HpackDecoder() + + if sock is None: + sock = socket.create_connection((host, port), timeout=connect_timeout) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if use_tls: + context = ssl_context or ssl.create_default_context() + context.set_alpn_protocols(["h2"]) + sock = context.wrap_socket(sock, server_hostname=host) + negotiated = sock.selected_alpn_protocol() + if negotiated != "h2": + sock.close() + raise TransportError( + "server did not negotiate HTTP/2 via ALPN (got {!r})".format( + negotiated + ) + ) + # Without TLS this is h2c with prior knowledge, which gRPC servers + # (including grpc-go) accept on their plaintext listeners. + self._sock = sock + + self._sock.sendall( + frames.CONNECTION_PREFACE + + frames.encode_frame( + frames.settings_frame({frames.SETTINGS_ENABLE_PUSH: 0}) + ) + ) + + def close(self): + try: + self._sock.close() + except OSError: + pass + + # --- low-level I/O ------------------------------------------------------ + + def _recv_exactly(self, count, deadline): + while len(self._recv_buffer) < count: + self._sock.settimeout(deadline.remaining()) + chunk = self._sock.recv(65536) + if not chunk: + raise TransportError("connection closed by peer") + self._recv_buffer += chunk + data, self._recv_buffer = ( + self._recv_buffer[:count], + self._recv_buffer[count:], + ) + return data + + def _read_frame(self, deadline): + header = self._recv_exactly(frames.FRAME_HEADER_LEN, deadline) + length, frame = frames.decode_frame_header(header) + if length > MAX_RECV_FRAME_SIZE: + raise TransportError( + "frame length {} exceeds {}-byte limit".format( + length, MAX_RECV_FRAME_SIZE + ) + ) + if length: + frame.payload = self._recv_exactly(length, deadline) + return frame + + def _send_frame(self, frame, deadline): + self._sock.settimeout(deadline.remaining()) + self._sock.sendall(frames.encode_frame(frame)) + + # --- connection-level frame dispatch ------------------------------------- + + def _handle_connection_frame(self, frame, stream_id, stream_state, deadline): + """Process a frame; returns True if it belonged to the active stream.""" + if frame.stream_id == 0: + if frame.type == frames.SETTINGS and not frame.flags & frames.FLAG_ACK: + settings = frames.parse_settings(frame) + if frames.SETTINGS_MAX_FRAME_SIZE in settings: + self._peer_max_frame_size = settings[ + frames.SETTINGS_MAX_FRAME_SIZE + ] + if frames.SETTINGS_INITIAL_WINDOW_SIZE in settings: + delta = ( + settings[frames.SETTINGS_INITIAL_WINDOW_SIZE] + - self._peer_initial_window + ) + self._peer_initial_window += delta + stream_state["send_window"] += delta + self._send_frame(frames.settings_frame(ack=True), deadline) + elif frame.type == frames.PING and not frame.flags & frames.FLAG_ACK: + self._send_frame( + frames.Frame( + frames.PING, frames.FLAG_ACK, 0, frame.payload + ), + deadline, + ) + elif frame.type == frames.WINDOW_UPDATE: + (increment,) = struct.unpack(">L", frame.payload) + self._send_window_connection += increment & 0x7FFFFFFF + elif frame.type == frames.GOAWAY: + last_stream_id, error_code, debug_data = frames.parse_goaway(frame) + raise ConnectionTerminated(last_stream_id, error_code, debug_data) + return False + if frame.stream_id != stream_id: + # Sequential usage: frames for other streams are stale leftovers. + return False + if frame.type == frames.WINDOW_UPDATE: + (increment,) = struct.unpack(">L", frame.payload) + stream_state["send_window"] += increment & 0x7FFFFFFF + return False + if frame.type == frames.RST_STREAM: + (error_code,) = struct.unpack(">L", frame.payload) + raise StreamReset(error_code) + return True + + # --- the one operation this connection exists for ------------------------ + + def _send_headers(self, stream_id, header_block, deadline): + """Send a header block, splitting it across HEADERS + CONTINUATION + frames when it exceeds the peer's maximum frame size.""" + max_size = self._peer_max_frame_size + if len(header_block) <= max_size: + self._send_frame( + frames.Frame( + frames.HEADERS, frames.FLAG_END_HEADERS, stream_id, header_block + ), + deadline, + ) + return + self._send_frame( + frames.Frame(frames.HEADERS, 0, stream_id, header_block[:max_size]), + deadline, + ) + offset = max_size + while offset < len(header_block): + chunk = header_block[offset : offset + max_size] + offset += len(chunk) + end_headers = offset >= len(header_block) + self._send_frame( + frames.Frame( + frames.CONTINUATION, + frames.FLAG_END_HEADERS if end_headers else 0, + stream_id, + chunk, + ), + deadline, + ) + + def _process_response_frame(self, frame, stream_id, stream_state, resp, deadline): + """Fold one active-stream response frame into ``resp`` (a dict with + ``header_sets``, ``body_chunks``, and ``header_fragments``). Returns + True once the response stream has ended. + + Connection-level frames (SETTINGS, PING, WINDOW_UPDATE, GOAWAY) and + stale other-stream frames are delegated to _handle_connection_frame and + never complete the response. + """ + if resp["header_fragments"] is not None: + if frame.type != frames.CONTINUATION or frame.stream_id != stream_id: + raise TransportError("expected CONTINUATION frame") + resp["header_fragments"].append(frame.payload) + resp["header_bytes"] += len(frame.payload) + if resp["header_bytes"] > MAX_HEADER_BLOCK: + raise TransportError( + "header block exceeds {}-byte limit".format(MAX_HEADER_BLOCK) + ) + if frame.flags & frames.FLAG_END_HEADERS: + resp["header_sets"].append( + self._hpack_decoder.decode(b"".join(resp["header_fragments"])) + ) + resp["header_fragments"] = None + return resp["pending_end_stream"] + return False + if not self._handle_connection_frame( + frame, stream_id, stream_state, deadline + ): + return False + if frame.type == frames.HEADERS: + fragment = frames.strip_padding(frame) + resp["pending_end_stream"] = bool(frame.flags & frames.FLAG_END_STREAM) + if frame.flags & frames.FLAG_END_HEADERS: + resp["header_sets"].append(self._hpack_decoder.decode(fragment)) + return resp["pending_end_stream"] + resp["header_fragments"] = [fragment] + resp["header_bytes"] = len(fragment) + elif frame.type == frames.DATA: + data = frames.strip_padding(frame) + resp["body_chunks"].append(data) + resp["body_bytes"] += len(data) + if resp["body_bytes"] > MAX_RESPONSE_BODY: + raise TransportError( + "response body exceeds {}-byte limit".format(MAX_RESPONSE_BODY) + ) + if frame.payload: + # Replenish our receive windows for what we consumed. + self._send_frame( + frames.window_update_frame(0, len(frame.payload)), deadline + ) + if not frame.flags & frames.FLAG_END_STREAM: + self._send_frame( + frames.window_update_frame(stream_id, len(frame.payload)), + deadline, + ) + if frame.flags & frames.FLAG_END_STREAM: + # Stream ended without trailers (not valid gRPC, but the caller + # decides what to do with what it got). + return True + return False + + def request(self, headers, body, deadline): + """Send one request and return ``(header_sets, body_bytes)``. + + ``header_sets`` is the list of decoded header blocks received on the + stream, in order: initial response headers, then trailers — or a + single trailers-only block. + + Sending the body and reading the response share one loop: a server that + responds early (a trailers-only rejection while we are still uploading, + blocked on flow control) is honored immediately rather than discarded. + """ + stream_id = self._next_stream_id + self._next_stream_id += 2 + stream_state = {"send_window": self._peer_initial_window} + resp = { + "header_sets": [], + "body_chunks": [], + "body_bytes": 0, + "header_fragments": None, # non-None while accumulating a block + "header_bytes": 0, + "pending_end_stream": False, + } + + header_block = hpack_encode(headers) + if stream_id == 1: + # First header block on the connection: declare that our encoder + # will not use the HPACK dynamic table (max size 0). + header_block = encode_dynamic_table_size_update(0) + header_block + self._send_headers(stream_id, header_block, deadline) + + offset = 0 + body_sent = False + while True: + if not body_sent: + available = min( + self._peer_max_frame_size, + stream_state["send_window"], + self._send_window_connection, + ) + if available > 0 or offset >= len(body): + chunk = body[offset : offset + available] if available > 0 else b"" + offset += len(chunk) + body_sent = offset >= len(body) + self._send_frame( + frames.Frame( + frames.DATA, + frames.FLAG_END_STREAM if body_sent else 0, + stream_id, + chunk, + ), + deadline, + ) + stream_state["send_window"] -= len(chunk) + self._send_window_connection -= len(chunk) + continue + # Blocked on flow control: fall through and read a frame. It may + # be a WINDOW_UPDATE that unblocks us, or an early response. + + frame = self._read_frame(deadline) + try: + complete = self._process_response_frame( + frame, stream_id, stream_state, resp, deadline + ) + except ConnectionTerminated as error: + # A stream id above the peer's last_stream_id was not processed, + # so the request is safe to retry; at or below, it may have been. + error.stream_processed = stream_id <= error.last_stream_id + raise + except (HpackError, ValueError, struct.error) as error: + # Malformed frame from the peer: the connection can no longer be + # trusted (HPACK state may be desynced). Surface it as a + # transport failure so the caller closes and does not reuse it. + raise TransportError( + "malformed frame from peer: {}".format(error) + ) from error + if complete: + return resp["header_sets"], b"".join(resp["body_chunks"]) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/frames.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/frames.py new file mode 100644 index 00000000000..e4a47bd95e8 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/frames.py @@ -0,0 +1,136 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""HTTP/2 (RFC 7540) frame codec — the subset a unary gRPC client needs.""" + +import struct + +FRAME_HEADER_LEN = 9 + +# Frame types. +DATA = 0x0 +HEADERS = 0x1 +PRIORITY = 0x2 +RST_STREAM = 0x3 +SETTINGS = 0x4 +PUSH_PROMISE = 0x5 +PING = 0x6 +GOAWAY = 0x7 +WINDOW_UPDATE = 0x8 +CONTINUATION = 0x9 + +# Flags. +FLAG_END_STREAM = 0x1 # DATA, HEADERS +FLAG_ACK = 0x1 # SETTINGS, PING +FLAG_END_HEADERS = 0x4 # HEADERS, CONTINUATION +FLAG_PADDED = 0x8 # DATA, HEADERS +FLAG_PRIORITY = 0x20 # HEADERS + +# SETTINGS identifiers. +SETTINGS_HEADER_TABLE_SIZE = 0x1 +SETTINGS_ENABLE_PUSH = 0x2 +SETTINGS_MAX_CONCURRENT_STREAMS = 0x3 +SETTINGS_INITIAL_WINDOW_SIZE = 0x4 +SETTINGS_MAX_FRAME_SIZE = 0x5 +SETTINGS_MAX_HEADER_LIST_SIZE = 0x6 + +CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + +DEFAULT_INITIAL_WINDOW_SIZE = 65535 +DEFAULT_MAX_FRAME_SIZE = 16384 + + +class Frame: + __slots__ = ("type", "flags", "stream_id", "payload") + + def __init__(self, frame_type, flags, stream_id, payload=b""): + self.type = frame_type + self.flags = flags + self.stream_id = stream_id + self.payload = payload + + def __repr__(self): + return "Frame(type=0x{:x}, flags=0x{:x}, stream={}, len={})".format( + self.type, self.flags, self.stream_id, len(self.payload) + ) + + +def encode_frame(frame): + length = len(frame.payload) + if length > 0xFFFFFF: + raise ValueError( + "frame payload {} exceeds the 24-bit length field".format(length) + ) + header = struct.pack( + ">BHBBL", + (length >> 16) & 0xFF, + length & 0xFFFF, + frame.type, + frame.flags, + frame.stream_id & 0x7FFFFFFF, + ) + return header + frame.payload + + +def decode_frame_header(header): + """Decode the 9-octet frame header; returns ``(length, Frame)`` with an + empty payload the caller fills once it has read ``length`` more octets.""" + high, low, frame_type, flags, stream_id = struct.unpack(">BHBBL", header) + length = (high << 16) | low + return length, Frame(frame_type, flags, stream_id & 0x7FFFFFFF) + + +def strip_padding(frame): + """Return the payload of a DATA or HEADERS frame with padding removed.""" + payload = frame.payload + if frame.flags & FLAG_PADDED: + if not payload: + raise ValueError("padded frame with empty payload") + pad_length = payload[0] + payload = payload[1:] + if pad_length > len(payload): + raise ValueError("padding longer than payload") + payload = payload[: len(payload) - pad_length] + if frame.type == HEADERS and frame.flags & FLAG_PRIORITY: + if len(payload) < 5: + raise ValueError("HEADERS priority section truncated") + payload = payload[5:] + return payload + + +def settings_frame(settings=None, ack=False): + if ack: + return Frame(SETTINGS, FLAG_ACK, 0) + payload = b"".join( + struct.pack(">HL", key, value) for key, value in (settings or {}).items() + ) + return Frame(SETTINGS, 0, 0, payload) + + +def parse_settings(frame): + if len(frame.payload) % 6: + raise ValueError("SETTINGS payload not a multiple of 6") + return { + struct.unpack_from(">HL", frame.payload, offset)[0]: struct.unpack_from( + ">HL", frame.payload, offset + )[1] + for offset in range(0, len(frame.payload), 6) + } + + +def window_update_frame(stream_id, increment): + return Frame(WINDOW_UPDATE, 0, stream_id, struct.pack(">L", increment)) + + +def rst_stream_frame(stream_id, error_code): + return Frame(RST_STREAM, 0, stream_id, struct.pack(">L", error_code)) + + +def goaway_frame(last_stream_id, error_code): + return Frame(GOAWAY, 0, 0, struct.pack(">LL", last_stream_id, error_code)) + + +def parse_goaway(frame): + last_stream_id, error_code = struct.unpack_from(">LL", frame.payload, 0) + debug_data = frame.payload[8:] + return last_stream_id & 0x7FFFFFFF, error_code, debug_data diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/hpack.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/hpack.py new file mode 100644 index 00000000000..1f8ffebc2b0 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/_pygrpc/hpack.py @@ -0,0 +1,299 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Minimal HPACK (RFC 7541) codec for the pure-Python gRPC transport. + +The encoder emits only static-table references and literal header fields +without indexing (never-indexed for sensitive headers), and never applies +Huffman coding. That subset is spec-compliant and keeps the encoder trivial; +compression efficiency is irrelevant for the handful of headers a unary gRPC +call sends. + +The decoder is complete: indexed fields, all literal forms, dynamic table +maintenance with eviction, dynamic table size updates, and Huffman-coded +string literals, because the peer chooses its own encodings. +""" + +from ._huffman_table import HUFFMAN_CODES + + +class HpackError(Exception): + """Raised on malformed HPACK input (RFC 7541 section 5.3 decoding error).""" + + +# RFC 7541 appendix A. Index 1 is STATIC_TABLE[0]. +STATIC_TABLE = ( + (b":authority", b""), + (b":method", b"GET"), + (b":method", b"POST"), + (b":path", b"/"), + (b":path", b"/index.html"), + (b":scheme", b"http"), + (b":scheme", b"https"), + (b":status", b"200"), + (b":status", b"204"), + (b":status", b"206"), + (b":status", b"304"), + (b":status", b"400"), + (b":status", b"404"), + (b":status", b"500"), + (b"accept-charset", b""), + (b"accept-encoding", b"gzip, deflate"), + (b"accept-language", b""), + (b"accept-ranges", b""), + (b"accept", b""), + (b"access-control-allow-origin", b""), + (b"age", b""), + (b"allow", b""), + (b"authorization", b""), + (b"cache-control", b""), + (b"content-disposition", b""), + (b"content-encoding", b""), + (b"content-language", b""), + (b"content-length", b""), + (b"content-location", b""), + (b"content-range", b""), + (b"content-type", b""), + (b"cookie", b""), + (b"date", b""), + (b"etag", b""), + (b"expect", b""), + (b"expires", b""), + (b"from", b""), + (b"host", b""), + (b"if-match", b""), + (b"if-modified-since", b""), + (b"if-none-match", b""), + (b"if-range", b""), + (b"if-unmodified-since", b""), + (b"last-modified", b""), + (b"link", b""), + (b"location", b""), + (b"max-forwards", b""), + (b"proxy-authenticate", b""), + (b"proxy-authorization", b""), + (b"range", b""), + (b"referer", b""), + (b"refresh", b""), + (b"retry-after", b""), + (b"server", b""), + (b"set-cookie", b""), + (b"strict-transport-security", b""), + (b"transfer-encoding", b""), + (b"user-agent", b""), + (b"vary", b""), + (b"via", b""), + (b"www-authenticate", b""), +) + +_STATIC_PAIR_INDEX = {pair: i + 1 for i, pair in enumerate(STATIC_TABLE)} +_STATIC_NAME_INDEX = {} +for _i, (_name, _value) in enumerate(STATIC_TABLE): + _STATIC_NAME_INDEX.setdefault(_name, _i + 1) + +# Headers whose values must never be compressed or indexed by intermediaries. +_NEVER_INDEXED_NAMES = frozenset((b"authorization", b"proxy-authorization")) + + +def _encode_integer(value, prefix_bits, first_byte_flags): + """RFC 7541 section 5.1 integer representation.""" + max_prefix = (1 << prefix_bits) - 1 + if value < max_prefix: + return bytes((first_byte_flags | value,)) + out = bytearray((first_byte_flags | max_prefix,)) + value -= max_prefix + while value >= 0x80: + out.append(0x80 | (value & 0x7F)) + value >>= 7 + out.append(value) + return bytes(out) + + +def _encode_string(data): + # H bit 0: raw octets, no Huffman. + return _encode_integer(len(data), 7, 0x00) + data + + +def encode_dynamic_table_size_update(size): + """Encode a dynamic table size update instruction (RFC 7541 section 6.3). + + This encoder never indexes into the dynamic table, so it declares a + maximum size of 0 once at the start of a connection's first header block. + Because 0 is unconditionally at or below any ``SETTINGS_HEADER_TABLE_SIZE`` + the peer advertises, no later settings change can require a further update, + and strict decoders that expect an explicit size signal are satisfied. + """ + return _encode_integer(size, 5, 0x20) + + +def encode(headers): + """Encode ``[(name, value), ...]`` byte pairs into an HPACK header block. + + Names must already be lowercase; pseudo-headers must come first, as + required by RFC 7540 section 8.1.2.1 — both are the caller's contract. + """ + out = bytearray() + for name, value in headers: + pair_index = _STATIC_PAIR_INDEX.get((name, value)) + if pair_index is not None: + out += _encode_integer(pair_index, 7, 0x80) + continue + name_index = _STATIC_NAME_INDEX.get(name, 0) + if name in _NEVER_INDEXED_NAMES: + out += _encode_integer(name_index, 4, 0x10) + else: + out += _encode_integer(name_index, 4, 0x00) + if name_index == 0: + out += _encode_string(name) + out += _encode_string(value) + return bytes(out) + + +class _HuffmanDecoder: + """Bit-walking decoder over the RFC 7541 appendix B code table.""" + + def __init__(self, codes): + # Binary prefix tree: nodes are two-slot lists; leaves are symbols. + self._root = [None, None] + for symbol, (code, length) in enumerate(codes): + node = self._root + for bit_position in range(length - 1, -1, -1): + bit = (code >> bit_position) & 1 + if bit_position == 0: + node[bit] = symbol + else: + if node[bit] is None: + node[bit] = [None, None] + node = node[bit] + + def decode(self, data): + out = bytearray() + node = self._root + bits_since_symbol = 0 + all_ones_since_symbol = True + for byte in data: + for bit_position in range(7, -1, -1): + bit = (byte >> bit_position) & 1 + nxt = node[bit] + if nxt is None: + raise HpackError("invalid Huffman code") + if isinstance(nxt, int): + if nxt == 256: + # EOS must never appear in the payload proper. + raise HpackError("Huffman EOS symbol in payload") + out.append(nxt) + node = self._root + bits_since_symbol = 0 + all_ones_since_symbol = True + else: + node = nxt + bits_since_symbol += 1 + all_ones_since_symbol = all_ones_since_symbol and bit == 1 + if node is not self._root: + # Trailing bits must be a strict prefix of EOS: at most 7 bits, + # all ones (RFC 7541 section 5.2). + if bits_since_symbol > 7 or not all_ones_since_symbol: + raise HpackError("invalid Huffman padding") + return bytes(out) + + +_HUFFMAN = _HuffmanDecoder(HUFFMAN_CODES) + + +class Decoder: + """Stateful HPACK decoder: one instance per HTTP/2 connection direction.""" + + def __init__(self, max_dynamic_table_size=4096): + self._entries = [] # newest first; index 62 is self._entries[0] + self._size = 0 + self._max_size = max_dynamic_table_size + # Protocol ceiling from SETTINGS_HEADER_TABLE_SIZE; size updates in + # the header block must not exceed it. + self._settings_max_size = max_dynamic_table_size + + def _evict(self): + while self._size > self._max_size and self._entries: + name, value = self._entries.pop() + self._size -= len(name) + len(value) + 32 + + def _add(self, name, value): + self._entries.insert(0, (name, value)) + self._size += len(name) + len(value) + 32 + self._evict() + + def _lookup(self, index): + if index == 0: + raise HpackError("header field index 0") + if index <= len(STATIC_TABLE): + return STATIC_TABLE[index - 1] + dynamic_index = index - len(STATIC_TABLE) - 1 + if dynamic_index >= len(self._entries): + raise HpackError("header field index beyond table: {}".format(index)) + return self._entries[dynamic_index] + + def decode(self, block): + """Decode one complete header block into ``[(name, value), ...]``.""" + headers = [] + pos = 0 + length = len(block) + + def read_integer(prefix_bits, first_byte): + nonlocal pos + max_prefix = (1 << prefix_bits) - 1 + value = first_byte & max_prefix + if value < max_prefix: + return value + shift = 0 + while True: + if pos >= length: + raise HpackError("truncated integer") + byte = block[pos] + pos += 1 + value += (byte & 0x7F) << shift + shift += 7 + if shift > 62: + raise HpackError("integer overflow") + if not byte & 0x80: + return value + + def read_string(): + nonlocal pos + if pos >= length: + raise HpackError("truncated string") + first = block[pos] + pos += 1 + huffman = bool(first & 0x80) + str_length = read_integer(7, first) + if pos + str_length > length: + raise HpackError("truncated string payload") + data = bytes(block[pos:pos + str_length]) + pos += str_length + return _HUFFMAN.decode(data) if huffman else data + + while pos < length: + first = block[pos] + pos += 1 + if first & 0x80: + # Indexed header field. + headers.append(self._lookup(read_integer(7, first))) + elif first & 0x40: + # Literal with incremental indexing. + name_index = read_integer(6, first) + name = self._lookup(name_index)[0] if name_index else read_string() + value = read_string() + self._add(name, value) + headers.append((name, value)) + elif first & 0x20: + # Dynamic table size update. + new_size = read_integer(5, first) + if new_size > self._settings_max_size: + raise HpackError("dynamic table size update beyond SETTINGS limit") + self._max_size = new_size + self._evict() + else: + # Literal without indexing (0x00) or never indexed (0x10). + name_index = read_integer(4, first) + name = self._lookup(name_index)[0] if name_index else read_string() + value = read_string() + headers.append((name, value)) + return headers diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/exporter.py new file mode 100644 index 00000000000..855a80acec2 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/exporter.py @@ -0,0 +1,439 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +import random +import threading +from abc import ABC, abstractmethod +from collections.abc import Iterable, Sequence +from collections.abc import Sequence as TypingSequence +from logging import getLogger +from os import environ +from time import time +from typing import Generic, Literal, TypeVar +from urllib.parse import urlparse + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import ( + ChannelCredentials, + Compression, + RpcError, + StatusCode, + insecure_channel, + secure_channel, + ssl_channel_credentials, +) + +from opentelemetry.exporter.otlp._proto.common._exporter_metrics import ( + create_exporter_metrics, +) +from opentelemetry.exporter.otlp._proto.grpc import ( + _OTLP_GRPC_CHANNEL_OPTIONS, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry._proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceStub, +) +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2_grpc import ( + MetricsServiceStub, +) +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, +) +from opentelemetry._proto.collector.trace.v1.trace_service_pb2_grpc import ( + TraceServiceStub, +) +from opentelemetry.sdk._logs import ReadableLogRecord +from opentelemetry.sdk._logs.export import LogRecordExportResult +from opentelemetry.sdk._shared_internal import DuplicateFilter +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_INSECURE, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.metrics.export import MetricExportResult, MetricsData +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExportResult +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( + RPC_RESPONSE_STATUS_CODE, +) +from opentelemetry.util._importlib_metadata import entry_points +from opentelemetry.util.re import parse_env_headers + +_RETRYABLE_ERROR_CODES = frozenset([ + StatusCode.CANCELLED, + StatusCode.DEADLINE_EXCEEDED, + StatusCode.RESOURCE_EXHAUSTED, + StatusCode.ABORTED, + StatusCode.OUT_OF_RANGE, + StatusCode.UNAVAILABLE, + StatusCode.DATA_LOSS, +]) +_MAX_RETRYS = 6 +logger = getLogger(__name__) +logger.addFilter(DuplicateFilter()) + +SDKDataT = TypeVar( + "SDKDataT", + TypingSequence[ReadableLogRecord], + MetricsData, + TypingSequence[ReadableSpan], +) +ExportServiceRequestT = TypeVar( + "ExportServiceRequestT", + ExportTraceServiceRequest, + ExportMetricsServiceRequest, + ExportLogsServiceRequest, +) +ExportResultT = TypeVar( + "ExportResultT", + LogRecordExportResult, + MetricExportResult, + SpanExportResult, +) +ExportStubT = TypeVar( + "ExportStubT", TraceServiceStub, MetricsServiceStub, LogsServiceStub +) + +_ENVIRON_TO_COMPRESSION = { + None: None, + "gzip": Compression.Gzip, +} + + +class InvalidCompressionValueException(Exception): + def __init__(self, environ_key: str, environ_value: str): + super().__init__( + f'Invalid value "{environ_value}" for compression envvar {environ_key}' + ) + + +def environ_to_compression(environ_key: str) -> Compression | None: + environ_value = ( + environ[environ_key].lower().strip() + if environ_key in environ + else None + ) + if environ_value not in _ENVIRON_TO_COMPRESSION and environ_value is not None: + raise InvalidCompressionValueException(environ_key, environ_value) + return _ENVIRON_TO_COMPRESSION[environ_value] + + +def _read_file(file_path: str) -> bytes | None: + try: + with open(file_path, "rb") as file: + return file.read() + except FileNotFoundError as e: + logger.exception( + "Failed to read file: %s. Please check if the file exists and is accessible.", + e.filename, + ) + return None + + +def _load_credentials( + certificate_file: str | None, + client_key_file: str | None, + client_certificate_file: str | None, +) -> ChannelCredentials: + root_certificates = _read_file(certificate_file) if certificate_file else None + private_key = _read_file(client_key_file) if client_key_file else None + certificate_chain = _read_file(client_certificate_file) if client_certificate_file else None + return ssl_channel_credentials( + root_certificates=root_certificates, + private_key=private_key, + certificate_chain=certificate_chain, + ) + + +def _get_credentials( + creds: ChannelCredentials | None, + credential_entry_point_env_key: str, + certificate_file_env_key: str, + client_key_file_env_key: str, + client_certificate_file_env_key: str, +) -> ChannelCredentials: + if creds is not None: + return creds + _credential_env = environ.get(credential_entry_point_env_key) + if _credential_env: + try: + maybe_channel_creds = next( + iter( + entry_points( + group="opentelemetry_otlp_credential_provider", + name=_credential_env, + ) + ) + ).load()() + except StopIteration: + raise RuntimeError( + f"Requested component '{_credential_env}' not found in " + f"entry point 'opentelemetry_otlp_credential_provider'" + ) + if isinstance(maybe_channel_creds, ChannelCredentials): + return maybe_channel_creds + else: + raise RuntimeError( + f"Requested component '{_credential_env}' is of type {type(maybe_channel_creds)}" + f" must be of type `grpc.ChannelCredentials`." + ) + + certificate_file = environ.get(certificate_file_env_key) + if certificate_file: + client_key_file = environ.get(client_key_file_env_key) + client_certificate_file = environ.get(client_certificate_file_env_key) + credentials = _load_credentials(certificate_file, client_key_file, client_certificate_file) + if credentials is not None: + return credentials + return ssl_channel_credentials() + + +class OTLPExporterMixin(ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT, ExportStubT]): + def __init__( + self, + stub: ExportStubT, + result: ExportResultT, + endpoint: str | None = None, + insecure: bool | None = None, + credentials: ChannelCredentials | None = None, + headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, + timeout: float | None = None, + compression: Compression | None = None, + channel_options: tuple[tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, + *, + component_type: OtelComponentTypeValues | None = None, + signal: Literal["traces", "metrics", "logs"] = "traces", + meter_provider: MeterProvider | None = None, + ): + super().__init__() + self._result = result + self._stub = stub + self._endpoint = endpoint or environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317") + + parsed_url = urlparse(self._endpoint) + + if parsed_url.scheme == "https": + insecure = False + insecure_exporter = environ.get(OTEL_EXPORTER_OTLP_INSECURE) + if insecure is None: + if insecure_exporter is not None: + insecure = insecure_exporter.lower() == "true" + else: + insecure = parsed_url.scheme == "http" + + if parsed_url.netloc: + self._endpoint = parsed_url.netloc + + self._insecure = insecure + self._credentials = credentials + self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) + if isinstance(self._headers, str): + temp_headers = parse_env_headers(self._headers, liberal=True) + self._headers = tuple(temp_headers.items()) + elif isinstance(self._headers, dict): + self._headers = tuple(self._headers.items()) + if self._headers is None: + self._headers = tuple() + + if channel_options: + overridden_options = {opt_name for (opt_name, _) in channel_options} + default_options = tuple( + (opt_name, opt_value) + for opt_name, opt_value in _OTLP_GRPC_CHANNEL_OPTIONS + if opt_name not in overridden_options + ) + self._channel_options = default_options + channel_options + else: + self._channel_options = tuple(_OTLP_GRPC_CHANNEL_OPTIONS) + + self._timeout = timeout or float(environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10)) + self._collector_kwargs = None + + self._compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION) + if compression is None + else compression + ) or Compression.NoCompression + + self._retryable_error_codes = retryable_error_codes or os.environ.get( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES + ) + if isinstance(self._retryable_error_codes, str): + self._retryable_error_codes = frozenset( + StatusCode[code.strip().upper()] + for code in self._retryable_error_codes.split(",") + if code.strip() + ) + elif self._retryable_error_codes is not None: + self._retryable_error_codes = frozenset(self._retryable_error_codes) + else: + self._retryable_error_codes = _RETRYABLE_ERROR_CODES + + self._channel = None + self._client = None + self._shutdown_in_progress = threading.Event() + self._shutdown = False + + if not self._insecure: + self._credentials = _get_credentials( + self._credentials, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + ) + + self._component_type = component_type + self._signal: Literal["traces", "metrics", "logs"] = signal + self._parsed_url = parsed_url + self._metrics = create_exporter_metrics( + self._component_type, + signal, + parsed_url, + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) + + self._initialize_channel_and_stub() + + def _initialize_channel_and_stub(self): + if self._insecure: + self._channel = insecure_channel( + self._endpoint, + compression=self._compression, + options=self._channel_options, + ) + else: + assert self._credentials is not None + self._channel = secure_channel( + self._endpoint, + self._credentials, + compression=self._compression, + options=self._channel_options, + ) + self._client = self._stub(self._channel) + + @abstractmethod + def _translate_data(self, data: SDKDataT) -> ExportServiceRequestT: + pass + + @abstractmethod + def _count_data(self, data: SDKDataT) -> int: + pass + + def _export(self, data: SDKDataT) -> ExportResultT: + if self._shutdown: + logger.warning("Exporter already shutdown, ignoring batch") + return self._result.FAILURE + + with self._metrics.export_operation(self._count_data(data)) as result: + deadline_sec = time() + self._timeout + for retry_num in range(_MAX_RETRYS): + backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) + try: + if self._client is None: + return self._result.FAILURE + self._client.Export( + request=self._translate_data(data), + metadata=self._headers, + timeout=deadline_sec - time(), + ) + return self._result.SUCCESS + except RpcError as error: + if ( + error.code() == StatusCode.UNAVAILABLE + and retry_num == 0 + ): + logger.debug( + "Reinitializing gRPC channel for %s exporter due to UNAVAILABLE error", + self._exporting, + ) + try: + if self._channel: + self._channel.close() + except Exception as e: + logger.debug( + "Error closing channel for %s exporter to %s: %s", + self._exporting, + self._endpoint, + str(e), + ) + self._initialize_channel_and_stub() + + if ( + error.code() not in self._retryable_error_codes + or retry_num + 1 == _MAX_RETRYS + or backoff_seconds > (deadline_sec - time()) + or self._shutdown + ): + logger.error( + "Failed to export %s to %s, error code: %s, error details: %s", + self._exporting, + self._endpoint, + error.code(), + error.details(), + exc_info=error.code() == StatusCode.UNKNOWN, + ) + result.error = error + result.error_attrs = {RPC_RESPONSE_STATUS_CODE: error.code().name} + return self._result.FAILURE + logger.warning( + "Transient error %s encountered while exporting %s to %s, retrying in %.2fs. Error details: %s", + error.code(), + self._exporting, + self._endpoint, + backoff_seconds, + error.details(), + ) + shutdown = self._shutdown_in_progress.wait(backoff_seconds) + if shutdown: + logger.warning("Shutdown in progress, aborting retry.") + break + return self._result.FAILURE + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + if self._shutdown: + logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._shutdown_in_progress.set() + if self._channel: + self._channel.close() + + @property + @abstractmethod + def _exporting(self) -> str: + pass + + def _set_meter_provider(self, meter_provider: MeterProvider) -> None: + self._metrics = create_exporter_metrics( + self._component_type, + self._signal, + self._parsed_url, + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/metric_exporter/__init__.py new file mode 100644 index 00000000000..4d0ba5377a5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/metric_exporter/__init__.py @@ -0,0 +1,202 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from collections.abc import Iterable +from collections.abc import Sequence as TypingSequence +from dataclasses import replace +from os import environ + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import ( + ChannelCredentials, + Compression, + StatusCode, +) + +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import OTLPMetricExporterMixin, encode_metrics +from opentelemetry.exporter.otlp._proto.grpc.exporter import ( + OTLPExporterMixin, + _get_credentials, + environ_to_compression, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2_grpc import MetricsServiceStub +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_INSECURE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, +) +from opentelemetry.sdk.metrics._internal.aggregation import Aggregation +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + DataPointT, + Gauge, + Metric, + MetricExporter, + MetricExportResult, + MetricsData, + ResourceMetrics, + ScopeMetrics, + Sum, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import OtelComponentTypeValues + + +class OTLPMetricExporter( + MetricExporter, + OTLPExporterMixin[ + MetricsData, + ExportMetricsServiceRequest, + MetricExportResult, + MetricsServiceStub, + ], + OTLPMetricExporterMixin, +): + def __init__( + self, + endpoint: str | None = None, + insecure: bool | None = None, + credentials: ChannelCredentials | None = None, + headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, + timeout: float | None = None, + compression: Compression | None = None, + preferred_temporality: dict[type, AggregationTemporality] | None = None, + preferred_aggregation: dict[type, Aggregation] | None = None, + max_export_batch_size: int | None = None, + channel_options: tuple[tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + insecure_metrics = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) + if insecure is None and insecure_metrics is not None: + insecure = insecure_metrics.lower() == "true" + + if not insecure and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None: + credentials = _get_credentials( + credentials, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT) + environ_timeout = float(environ_timeout) if environ_timeout is not None else None + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_METRICS_COMPRESSION) + if compression is None + else compression + ) + + self._common_configuration(preferred_temporality, preferred_aggregation) + + OTLPExporterMixin.__init__( + self, + stub=MetricsServiceStub, + result=MetricExportResult, + endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT), + insecure=insecure, + credentials=credentials, + headers=headers or environ.get(OTEL_EXPORTER_OTLP_METRICS_HEADERS), + timeout=timeout or environ_timeout, + compression=compression, + channel_options=channel_options, + retryable_error_codes=retryable_error_codes, + component_type=OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, + signal="metrics", + meter_provider=meter_provider, + ) + + self._max_export_batch_size: int | None = max_export_batch_size + + def _translate_data(self, data: MetricsData) -> ExportMetricsServiceRequest: + return encode_metrics(data) + + def _count_data(self, data: MetricsData) -> int: + num_items = 0 + for resource_metrics in data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + num_items += len(metric.data.data_points) + return num_items + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float = 10_000, + **kwargs, + ) -> MetricExportResult: + if self._max_export_batch_size is None: + return self._export(data=metrics_data) + + export_result = MetricExportResult.SUCCESS + for split_metrics_data in self._split_metrics_data(metrics_data): + split_export_result = self._export(data=split_metrics_data) + if split_export_result is MetricExportResult.FAILURE: + export_result = MetricExportResult.FAILURE + return export_result + + def _split_metrics_data(self, metrics_data: MetricsData) -> Iterable[MetricsData]: + assert self._max_export_batch_size is not None + batch_size: int = 0 + split_resource_metrics: list[ResourceMetrics] = [] + + for resource_metrics in metrics_data.resource_metrics: + split_scope_metrics: list[ScopeMetrics] = [] + split_resource_metrics.append(replace(resource_metrics, scope_metrics=split_scope_metrics)) + + for scope_metrics in resource_metrics.scope_metrics: + split_metrics: list[Metric] = [] + split_scope_metrics.append(replace(scope_metrics, metrics=split_metrics)) + + for metric in scope_metrics.metrics: + split_data_points: list[DataPointT] = [] + split_metrics.append(replace(metric, data=replace(metric.data, data_points=split_data_points))) + + for data_point in metric.data.data_points: + split_data_points.append(data_point) + batch_size += 1 + + if batch_size >= self._max_export_batch_size: + yield MetricsData(resource_metrics=split_resource_metrics) + batch_size = 0 + split_data_points = [] + split_metrics = [replace(metric, data=replace(metric.data, data_points=split_data_points))] + split_scope_metrics = [replace(scope_metrics, metrics=split_metrics)] + split_resource_metrics = [replace(resource_metrics, scope_metrics=split_scope_metrics)] + + if not split_data_points: + split_metrics.pop() + + if not split_metrics: + split_scope_metrics.pop() + + if not split_scope_metrics: + split_resource_metrics.pop() + + if batch_size > 0: + yield MetricsData(resource_metrics=split_resource_metrics) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) + + def set_meter_provider(self, meter_provider: MeterProvider): + return self._set_meter_provider(meter_provider) + + @property + def _exporting(self) -> str: + return "metrics" + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/trace_exporter/__init__.py new file mode 100644 index 00000000000..2787107e95a --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/trace_exporter/__init__.py @@ -0,0 +1,118 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Iterable, Sequence +from collections.abc import Sequence as TypingSequence +from os import environ + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import ( + ChannelCredentials, + Compression, + StatusCode, +) + +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import encode_spans +from opentelemetry.exporter.otlp._proto.grpc.exporter import ( + OTLPExporterMixin, + _get_credentials, + environ_to_compression, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest +from opentelemetry._proto.collector.trace.v1.trace_service_pb2_grpc import TraceServiceStub +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, +) +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.semconv._incubating.attributes.otel_attributes import OtelComponentTypeValues + + +class OTLPSpanExporter( + SpanExporter, + OTLPExporterMixin[ + Sequence[ReadableSpan], + ExportTraceServiceRequest, + SpanExportResult, + TraceServiceStub, + ], +): + def __init__( + self, + endpoint: str | None = None, + insecure: bool | None = None, + credentials: ChannelCredentials | None = None, + headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, + timeout: float | None = None, + compression: Compression | None = None, + channel_options: tuple[tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) + if insecure is None and insecure_spans is not None: + insecure = insecure_spans.lower() == "true" + + if not insecure and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None: + credentials = _get_credentials( + credentials, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) + environ_timeout = float(environ_timeout) if environ_timeout is not None else None + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) + if compression is None + else compression + ) + + OTLPExporterMixin.__init__( + self, + stub=TraceServiceStub, + result=SpanExportResult, + endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), + insecure=insecure, + credentials=credentials, + headers=headers or environ.get(OTEL_EXPORTER_OTLP_TRACES_HEADERS), + timeout=timeout or environ_timeout, + compression=compression, + channel_options=channel_options, + retryable_error_codes=retryable_error_codes, + component_type=OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, + signal="traces", + meter_provider=meter_provider, + ) + + def _translate_data(self, data: Sequence[ReadableSpan]) -> ExportTraceServiceRequest: + return encode_spans(data) + + def _count_data(self, data: Sequence[ReadableSpan]) -> int: + return len(data) + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + return self._export(spans) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) + + def force_flush(self, timeout_millis: int = 30000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True + + @property + def _exporting(self): + return "traces" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/version/__init__.py new file mode 100644 index 00000000000..524a0260e55 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/_proto/grpc/version/__init__.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "1.45.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py index 590d4b88fd3..6b2fe1f56f1 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/__init__.py @@ -1,68 +1,10 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from opentelemetry.exporter.otlp._proto.grpc import * # noqa: F401,F403 +import opentelemetry.exporter.otlp._proto.grpc as _src -""" -This library allows to export tracing data to an OTLP collector. - -Usage ------ - -The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the -`OTLP`_ collector. - -You can configure the exporter with the following environment variables: - -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` -- :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` -- :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_HEADERS` -- :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` -- :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` -- :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` - -.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter - from opentelemetry.sdk.resources import Resource - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - - # Resource can be required for some backends, e.g. Jaeger - # If resource wouldn't be set - traces wouldn't appears in Jaeger - resource = Resource.create({ - "service.name": "service" - }) - - trace.set_tracer_provider(TracerProvider(resource=resource)) - tracer = trace.get_tracer(__name__) - - otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True) - - span_processor = BatchSpanProcessor(otlp_exporter) - - trace.get_tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - -API ---- -""" - -from .version import __version__ - -_USER_AGENT_HEADER_VALUE = "OTel-OTLP-Exporter-Python/" + __version__ -_OTLP_GRPC_CHANNEL_OPTIONS = [ - # this will appear in the http User-Agent header - ("grpc.primary_user_agent", _USER_AGENT_HEADER_VALUE) -] +for _n in dir(_src): + if not _n.startswith('__'): + globals().setdefault(_n, getattr(_src, _n)) +del _src, _n diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 7f86fe8fa61..859b9940455 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -1,136 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from collections.abc import Iterable, Sequence -from collections.abc import Sequence as TypingSequence -from os import environ -from typing import Literal +import sys as _sys +import opentelemetry.exporter.otlp._proto.grpc._log_exporter as _mod -from grpc import ChannelCredentials, Compression, StatusCode -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( - OTLPExporterMixin, - _get_credentials, - environ_to_compression, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( - LogsServiceStub, -) -from opentelemetry.sdk._logs import ReadableLogRecord -from opentelemetry.sdk._logs.export import ( - LogRecordExporter, - LogRecordExportResult, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_INSECURE, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - - -class OTLPLogExporter( - LogRecordExporter, - OTLPExporterMixin[ - Sequence[ReadableLogRecord], - ExportLogsServiceRequest, - LogRecordExportResult, - LogsServiceStub, - ], -): - def __init__( - self, - endpoint: str | None = None, - insecure: bool | None = None, - credentials: ChannelCredentials | None = None, - headers: TypingSequence[tuple[str, str]] - | dict[str, str] - | str - | None = None, - timeout: float | None = None, - compression: Compression | None = None, - channel_options: tuple[tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, - *, - meter_provider: MeterProvider | None = None, - ): - insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) - if insecure is None and insecure_logs is not None: - insecure = insecure_logs.lower() == "true" - - if ( - not insecure - and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None - ): - credentials = _get_credentials( - credentials, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - ) - - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) - environ_timeout = ( - float(environ_timeout) if environ_timeout is not None else None - ) - - compression = ( - environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) - if compression is None - else compression - ) - - OTLPExporterMixin.__init__( - self, - endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT), - insecure=insecure, - credentials=credentials, - headers=headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS), - timeout=timeout or environ_timeout, - compression=compression, - stub=LogsServiceStub, - result=LogRecordExportResult, - channel_options=channel_options, - retryable_error_codes=retryable_error_codes, - component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, - signal="logs", - meter_provider=meter_provider, - ) - - def _translate_data( - self, data: Sequence[ReadableLogRecord] - ) -> ExportLogsServiceRequest: - return encode_logs(data) - - def _count_data(self, data: Sequence[ReadableLogRecord]): - return len(data) - - def export( # type: ignore [reportIncompatibleMethodOverride] - self, - batch: Sequence[ReadableLogRecord], - ) -> Literal[LogRecordExportResult.SUCCESS, LogRecordExportResult.FAILURE]: - return OTLPExporterMixin._export(self, batch) - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) - - def force_flush(self, timeout_millis: int = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True - - @property - def _exporting(self) -> str: - return "logs" +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index f7fa0b8697d..097fbb64dd8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -1,575 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -"""OTLP Exporter +import sys as _sys +import opentelemetry.exporter.otlp._proto.grpc.exporter as _mod -This module provides a mixin class for OTLP exporters that send telemetry data -to an OTLP-compatible receiver via gRPC. It includes a configurable reconnection -logic to handle transient collector outages. - -""" - -import os -import random -import threading -from abc import ABC, abstractmethod -from collections.abc import ( - Callable, - Iterable, - Sequence, # noqa: F401 -) -from collections.abc import Sequence as TypingSequence -from logging import getLogger -from os import environ -from time import time -from typing import ( # noqa: F401 - Any, - Generic, - Literal, - NewType, - Optional, - TypeVar, -) -from urllib.parse import urlparse - -from google.rpc.error_details_pb2 import RetryInfo -from typing_extensions import deprecated - -from grpc import ( - ChannelCredentials, - Compression, - RpcError, - StatusCode, - insecure_channel, - secure_channel, - ssl_channel_credentials, -) -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - create_exporter_metrics, -) -from opentelemetry.exporter.otlp.proto.common._internal import ( - _get_resource_data, -) -from opentelemetry.exporter.otlp.proto.grpc import ( - _OTLP_GRPC_CHANNEL_OPTIONS, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( - LogsServiceStub, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( - MetricsServiceStub, -) -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest, -) -from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( - TraceServiceStub, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - AnyValue, - ArrayValue, - KeyValue, -) -from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 -from opentelemetry.sdk._logs import ReadableLogRecord -from opentelemetry.sdk._logs.export import LogRecordExportResult -from opentelemetry.sdk._shared_internal import DuplicateFilter -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_INSECURE, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics.export import MetricExportResult, MetricsData -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.semconv._incubating.attributes.rpc_attributes import ( - RPC_RESPONSE_STATUS_CODE, -) -from opentelemetry.util._importlib_metadata import entry_points -from opentelemetry.util.re import parse_env_headers - -_RETRYABLE_ERROR_CODES = frozenset( - [ - StatusCode.CANCELLED, - StatusCode.DEADLINE_EXCEEDED, - StatusCode.RESOURCE_EXHAUSTED, - StatusCode.ABORTED, - StatusCode.OUT_OF_RANGE, - StatusCode.UNAVAILABLE, - StatusCode.DATA_LOSS, - ] -) -_MAX_RETRYS = 6 -logger = getLogger(__name__) -# This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. -logger.addFilter(DuplicateFilter()) -SDKDataT = TypeVar( - "SDKDataT", - TypingSequence[ReadableLogRecord], - MetricsData, - TypingSequence[ReadableSpan], -) -ResourceDataT = TypeVar("ResourceDataT") -TypingResourceT = TypeVar("TypingResourceT") -ExportServiceRequestT = TypeVar( - "ExportServiceRequestT", - ExportTraceServiceRequest, - ExportMetricsServiceRequest, - ExportLogsServiceRequest, -) -ExportResultT = TypeVar( - "ExportResultT", - LogRecordExportResult, - MetricExportResult, - SpanExportResult, -) -ExportStubT = TypeVar( - "ExportStubT", TraceServiceStub, MetricsServiceStub, LogsServiceStub -) - -_ENVIRON_TO_COMPRESSION = { - None: None, - "gzip": Compression.Gzip, -} - - -class InvalidCompressionValueException(Exception): - def __init__(self, environ_key: str, environ_value: str): - super().__init__( - f'Invalid value "{environ_value}" for compression envvar {environ_key}' - ) - - -def environ_to_compression(environ_key: str) -> Compression | None: - environ_value = ( - environ[environ_key].lower().strip() - if environ_key in environ - else None - ) - if ( - environ_value not in _ENVIRON_TO_COMPRESSION - and environ_value is not None - ): - raise InvalidCompressionValueException(environ_key, environ_value) - return _ENVIRON_TO_COMPRESSION[environ_value] - - -@deprecated( - "Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead. Deprecated since version 1.18.0.", -) -def get_resource_data( - sdk_resource_scope_data: dict[SDKResource, ResourceDataT], - resource_class: Callable[..., TypingResourceT], - name: str, -) -> list[TypingResourceT]: - return _get_resource_data(sdk_resource_scope_data, resource_class, name) - - -def _read_file(file_path: str) -> bytes | None: - try: - with open(file_path, "rb") as file: - return file.read() - except FileNotFoundError as e: - logger.exception( - "Failed to read file: %s. Please check if the file exists and is accessible.", - e.filename, - ) - return None - - -def _load_credentials( - certificate_file: str | None, - client_key_file: str | None, - client_certificate_file: str | None, -) -> ChannelCredentials: - root_certificates = ( - _read_file(certificate_file) if certificate_file else None - ) - private_key = _read_file(client_key_file) if client_key_file else None - certificate_chain = ( - _read_file(client_certificate_file) - if client_certificate_file - else None - ) - - return ssl_channel_credentials( - root_certificates=root_certificates, - private_key=private_key, - certificate_chain=certificate_chain, - ) - - -def _get_credentials( - creds: ChannelCredentials | None, - credential_entry_point_env_key: str, - certificate_file_env_key: str, - client_key_file_env_key: str, - client_certificate_file_env_key: str, -) -> ChannelCredentials: - if creds is not None: - return creds - _credential_env = environ.get(credential_entry_point_env_key) - if _credential_env: - try: - maybe_channel_creds = next( - iter( - entry_points( - group="opentelemetry_otlp_credential_provider", - name=_credential_env, - ) - ) - ).load()() - except StopIteration: - raise RuntimeError( - f"Requested component '{_credential_env}' not found in " - f"entry point 'opentelemetry_otlp_credential_provider'" - ) - if isinstance(maybe_channel_creds, ChannelCredentials): - return maybe_channel_creds - else: - raise RuntimeError( - f"Requested component '{_credential_env}' is of type {type(maybe_channel_creds)}" - f" must be of type `grpc.ChannelCredentials`." - ) - - certificate_file = environ.get(certificate_file_env_key) - if certificate_file: - client_key_file = environ.get(client_key_file_env_key) - client_certificate_file = environ.get(client_certificate_file_env_key) - credentials = _load_credentials( - certificate_file, client_key_file, client_certificate_file - ) - if credentials is not None: - return credentials - return ssl_channel_credentials() - - -# pylint: disable=no-member -class OTLPExporterMixin( - ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT, ExportStubT] -): - """OTLP gRPC exporter mixin. - - This class provides the base functionality for OTLP exporters that send - telemetry data (spans or metrics) to an OTLP-compatible receiver via gRPC. - It includes a configurable reconnection mechanism to handle transient - receiver outages. - - Args: - endpoint: OTLP-compatible receiver endpoint - insecure: Connection type - credentials: ChannelCredentials object for server authentication - headers: Headers to send when exporting - timeout: Backend request timeout in seconds - compression: gRPC compression method to use - channel_options: gRPC channel options - """ - - def __init__( - self, - stub: ExportStubT, - result: ExportResultT, - endpoint: str | None = None, - insecure: bool | None = None, - credentials: ChannelCredentials | None = None, - headers: TypingSequence[tuple[str, str]] - | dict[str, str] - | str - | None = None, - timeout: float | None = None, - compression: Compression | None = None, - channel_options: tuple[tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, - *, - component_type: OtelComponentTypeValues | None = None, - signal: Literal["traces", "metrics", "logs"] = "traces", - meter_provider: MeterProvider | None = None, - ): - super().__init__() - self._result = result - self._stub = stub - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317" - ) - - parsed_url = urlparse(self._endpoint) - - if parsed_url.scheme == "https": - insecure = False - insecure_exporter = environ.get(OTEL_EXPORTER_OTLP_INSECURE) - if insecure is None: - if insecure_exporter is not None: - insecure = insecure_exporter.lower() == "true" - else: - insecure = parsed_url.scheme == "http" - - if parsed_url.netloc: - self._endpoint = parsed_url.netloc - - self._insecure = insecure - self._credentials = credentials - self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) - if isinstance(self._headers, str): - temp_headers = parse_env_headers(self._headers, liberal=True) - self._headers = tuple(temp_headers.items()) - elif isinstance(self._headers, dict): - self._headers = tuple(self._headers.items()) - if self._headers is None: - self._headers = tuple() - - if channel_options: - # merge the default channel options with the one passed as parameter - overridden_options = { - opt_name for (opt_name, _) in channel_options - } - default_options = tuple( - (opt_name, opt_value) - for opt_name, opt_value in _OTLP_GRPC_CHANNEL_OPTIONS - if opt_name not in overridden_options - ) - self._channel_options = default_options + channel_options - else: - self._channel_options = tuple(_OTLP_GRPC_CHANNEL_OPTIONS) - - self._timeout = timeout or float( - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, 10) - ) - self._collector_kwargs = None - - self._compression = ( - environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION) - if compression is None - else compression - ) or Compression.NoCompression - - self._retryable_error_codes = retryable_error_codes or os.environ.get( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES - ) - if isinstance(self._retryable_error_codes, str): - self._retryable_error_codes = frozenset( - StatusCode[code.strip().upper()] - for code in self._retryable_error_codes.split(",") - if code.strip() - ) - elif self._retryable_error_codes is not None: - self._retryable_error_codes = frozenset( - self._retryable_error_codes - ) - else: - self._retryable_error_codes = _RETRYABLE_ERROR_CODES - - self._channel = None - self._client = None - - self._shutdown_in_progress = threading.Event() - self._shutdown = False - - if not self._insecure: - self._credentials = _get_credentials( - self._credentials, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - ) - - self._component_type = component_type - self._signal: Literal["traces", "metrics", "logs"] = signal - self._parsed_url = parsed_url - self._metrics = create_exporter_metrics( - self._component_type, - signal, - parsed_url, - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) - - self._initialize_channel_and_stub() - - def _initialize_channel_and_stub(self): - """ - Create a new gRPC channel and stub. - - This method is used during initialization and by the reconnection - mechanism to reinitialize the channel on transient errors. - """ - if self._insecure: - self._channel = insecure_channel( - self._endpoint, - compression=self._compression, - options=self._channel_options, - ) - else: - assert self._credentials is not None - self._channel = secure_channel( - self._endpoint, - self._credentials, - compression=self._compression, - options=self._channel_options, - ) - self._client = self._stub(self._channel) # type: ignore [reportCallIssue] - - @abstractmethod - def _translate_data( - self, - data: SDKDataT, - ) -> ExportServiceRequestT: - pass - - @abstractmethod - def _count_data( - self, - data: SDKDataT, - ) -> int: - pass - - def _export( - self, - data: SDKDataT, - ) -> ExportResultT: - if self._shutdown: - logger.warning("Exporter already shutdown, ignoring batch") - return self._result.FAILURE # type: ignore [reportReturnType] - - with self._metrics.export_operation(self._count_data(data)) as result: - # FIXME remove this check if the export type for traces - # gets updated to a class that represents the proto - # TracesData and use the code below instead. - deadline_sec = time() + self._timeout - for retry_num in range(_MAX_RETRYS): - try: - if self._client is None: - return self._result.FAILURE - self._client.Export( - request=self._translate_data(data), - metadata=self._headers, - timeout=deadline_sec - time(), - ) - return self._result.SUCCESS # type: ignore [reportReturnType] - except RpcError as error: - retry_info_bin = dict(error.trailing_metadata()).get( # type: ignore [reportAttributeAccessIssue] - "google.rpc.retryinfo-bin" # type: ignore [reportArgumentType] - ) - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) - if retry_info_bin is not None: - retry_info = RetryInfo() - retry_info.ParseFromString(retry_info_bin) - backoff_seconds = ( - retry_info.retry_delay.seconds - + retry_info.retry_delay.nanos / 1.0e9 - ) - - # For UNAVAILABLE errors, reinitialize the channel to force reconnection - if ( - error.code() == StatusCode.UNAVAILABLE - and retry_num == 0 - ): # type: ignore - logger.debug( - "Reinitializing gRPC channel for %s exporter due to UNAVAILABLE error", - self._exporting, - ) - try: - if self._channel: - self._channel.close() - except Exception as e: - logger.debug( - "Error closing channel for %s exporter to %s: %s", - self._exporting, - self._endpoint, - str(e), - ) - # Enable channel reconnection for subsequent calls - self._initialize_channel_and_stub() - - if ( - error.code() not in self._retryable_error_codes # type: ignore [reportAttributeAccessIssue] - or retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - logger.error( - "Failed to export %s to %s, error code: %s, error details: %s", - self._exporting, - self._endpoint, - error.code(), # type: ignore [reportAttributeAccessIssue] - error.details(), - exc_info=error.code() == StatusCode.UNKNOWN, # type: ignore [reportAttributeAccessIssue] - ) - result.error = error - result.error_attrs = { - RPC_RESPONSE_STATUS_CODE: error.code().name - } - return self._result.FAILURE # type: ignore [reportReturnType] - logger.warning( - "Transient error %s encountered while exporting %s to %s, retrying in %.2fs. Error details: %s", - error.code(), # type: ignore [reportAttributeAccessIssue] - self._exporting, - self._endpoint, - backoff_seconds, - error.details(), - ) - shutdown = self._shutdown_in_progress.wait(backoff_seconds) - if shutdown: - logger.warning("Shutdown in progress, aborting retry.") - break - # Not possible to reach here but the linter is complaining. - return self._result.FAILURE # type: ignore [reportReturnType] - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - """ - Shut down the exporter. - - Args: - timeout_millis: Timeout in milliseconds for shutting down the exporter. - """ - if self._shutdown: - logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - if self._channel: - self._channel.close() - - @property - @abstractmethod - def _exporting(self) -> str: - """ - Returns a string that describes the overall exporter, to be used in - warning messages. - """ - pass - - def _set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = create_exporter_metrics( - self._component_type, - self._signal, - self._parsed_url, - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index de97551f9fe..348ccfced3a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -1,293 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations +import sys as _sys +import opentelemetry.exporter.otlp._proto.grpc.metric_exporter as _mod -from collections.abc import Iterable -from collections.abc import Sequence as TypingSequence -from dataclasses import replace -from logging import getLogger -from os import environ - -from grpc import ChannelCredentials, Compression, StatusCode -from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( - OTLPMetricExporterMixin, -) -from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( - encode_metrics, -) -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 - OTLPExporterMixin, - _get_credentials, - environ_to_compression, - get_resource_data, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2_grpc import ( - MetricsServiceStub, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - InstrumentationScope, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_INSECURE, - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, -) -from opentelemetry.sdk.metrics._internal.aggregation import Aggregation -from opentelemetry.sdk.metrics.export import ( # noqa: F401 - AggregationTemporality, - DataPointT, - Gauge, - Metric, - MetricExporter, - MetricExportResult, - MetricsData, - ResourceMetrics, - ScopeMetrics, - Sum, -) -from opentelemetry.sdk.metrics.export import ( # noqa: F401 - ExponentialHistogram as ExponentialHistogramType, -) -from opentelemetry.sdk.metrics.export import ( # noqa: F401 - Histogram as HistogramType, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - -_logger = getLogger(__name__) - - -class OTLPMetricExporter( - MetricExporter, - OTLPExporterMixin[ - MetricsData, - ExportMetricsServiceRequest, - MetricExportResult, - MetricsServiceStub, - ], - OTLPMetricExporterMixin, -): - """OTLP metric exporter - - Args: - endpoint: Target URL to which the exporter is going to send metrics - max_export_batch_size: Maximum number of data points to export in a single request. This is to deal with - gRPC's 4MB message size limit. If not set there is no limit to the number of data points in a request. - If it is set and the number of data points exceeds the max, the request will be split. - """ - - def __init__( - self, - endpoint: str | None = None, - insecure: bool | None = None, - credentials: ChannelCredentials | None = None, - headers: TypingSequence[tuple[str, str]] - | dict[str, str] - | str - | None = None, - timeout: float | None = None, - compression: Compression | None = None, - preferred_temporality: dict[type, AggregationTemporality] - | None = None, - preferred_aggregation: dict[type, Aggregation] | None = None, - max_export_batch_size: int | None = None, - channel_options: tuple[tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, - *, - meter_provider: MeterProvider | None = None, - ): - insecure_metrics = environ.get(OTEL_EXPORTER_OTLP_METRICS_INSECURE) - if insecure is None and insecure_metrics is not None: - insecure = insecure_metrics.lower() == "true" - - if ( - not insecure - and environ.get(OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE) is not None - ): - credentials = _get_credentials( - credentials, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_METRICS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - ) - - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT) - environ_timeout = ( - float(environ_timeout) if environ_timeout is not None else None - ) - - compression = ( - environ_to_compression(OTEL_EXPORTER_OTLP_METRICS_COMPRESSION) - if compression is None - else compression - ) - - self._common_configuration( - preferred_temporality, preferred_aggregation - ) - - OTLPExporterMixin.__init__( - self, - stub=MetricsServiceStub, - result=MetricExportResult, - endpoint=endpoint - or environ.get(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT), - insecure=insecure, - credentials=credentials, - headers=headers or environ.get(OTEL_EXPORTER_OTLP_METRICS_HEADERS), - timeout=timeout or environ_timeout, - compression=compression, - channel_options=channel_options, - retryable_error_codes=retryable_error_codes, - component_type=OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, - signal="metrics", - meter_provider=meter_provider, - ) - - self._max_export_batch_size: int | None = max_export_batch_size - - def _translate_data( # type: ignore [reportIncompatibleMethodOverride] - self, data: MetricsData - ) -> ExportMetricsServiceRequest: - return encode_metrics(data) - - def _count_data(self, data: MetricsData): - num_items = 0 - - for resource_metrics in data.resource_metrics: - for scope_metrics in resource_metrics.scope_metrics: - for metric in scope_metrics.metrics: - num_items += len(metric.data.data_points) - - return num_items - - def export( - self, - metrics_data: MetricsData, - timeout_millis: float = 10_000, - **kwargs, - ) -> MetricExportResult: - # TODO(#2663): OTLPExporterMixin should pass timeout to gRPC - if self._max_export_batch_size is None: - return self._export(data=metrics_data) - - export_result = MetricExportResult.SUCCESS - - for split_metrics_data in self._split_metrics_data(metrics_data): - split_export_result = self._export(data=split_metrics_data) - - if split_export_result is MetricExportResult.FAILURE: - export_result = MetricExportResult.FAILURE - return export_result - - def _split_metrics_data( - self, - metrics_data: MetricsData, - ) -> Iterable[MetricsData]: - assert self._max_export_batch_size is not None - batch_size: int = 0 - split_resource_metrics: list[ResourceMetrics] = [] - - for resource_metrics in metrics_data.resource_metrics: - split_scope_metrics: list[ScopeMetrics] = [] - split_resource_metrics.append( - replace( - resource_metrics, - scope_metrics=split_scope_metrics, - ) - ) - for scope_metrics in resource_metrics.scope_metrics: - split_metrics: list[Metric] = [] - split_scope_metrics.append( - replace( - scope_metrics, - metrics=split_metrics, - ) - ) - for metric in scope_metrics.metrics: - split_data_points: list[DataPointT] = [] - split_metrics.append( - replace( - metric, - data=replace( - metric.data, - data_points=split_data_points, - ), - ) - ) - - for data_point in metric.data.data_points: - split_data_points.append(data_point) - batch_size += 1 - - if batch_size >= self._max_export_batch_size: - yield MetricsData( - resource_metrics=split_resource_metrics - ) - # Reset all the variables - batch_size = 0 - split_data_points = [] - split_metrics = [ - replace( - metric, - data=replace( - metric.data, - data_points=split_data_points, - ), - ) - ] - split_scope_metrics = [ - replace( - scope_metrics, - metrics=split_metrics, - ) - ] - split_resource_metrics = [ - replace( - resource_metrics, - scope_metrics=split_scope_metrics, - ) - ] - - if not split_data_points: - # If data_points is empty remove the whole metric - split_metrics.pop() - - if not split_metrics: - # If metrics is empty remove the whole scope_metrics - split_scope_metrics.pop() - - if not split_scope_metrics: - # If scope_metrics is empty remove the whole resource_metrics - split_resource_metrics.pop() - - if batch_size > 0: - yield MetricsData(resource_metrics=split_resource_metrics) - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) - - def set_meter_provider(self, meter_provider: MeterProvider): - return self._set_meter_provider(meter_provider) - - @property - def _exporting(self) -> str: - return "metrics" - - def force_flush(self, timeout_millis: float = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py index 7d396c17a80..41cfa6a678a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py @@ -1,162 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -"""OTLP Span Exporter""" +import sys as _sys +import opentelemetry.exporter.otlp._proto.grpc.trace_exporter as _mod -import logging -from collections.abc import Iterable, Sequence -from collections.abc import Sequence as TypingSequence -from os import environ - -from grpc import ChannelCredentials, Compression, StatusCode -from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( - encode_spans, -) -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 - OTLPExporterMixin, - _get_credentials, - environ_to_compression, - get_resource_data, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest, -) -from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( - TraceServiceStub, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - InstrumentationScope, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - ResourceSpans, - ScopeSpans, - Status, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - Span as CollectorSpan, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_INSECURE, - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, -) -from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - -logger = logging.getLogger(__name__) - - -# pylint: disable=no-member -class OTLPSpanExporter( - SpanExporter, - OTLPExporterMixin[ - Sequence[ReadableSpan], - ExportTraceServiceRequest, - SpanExportResult, - TraceServiceStub, - ], -): - # pylint: disable=unsubscriptable-object - """OTLP span exporter - - Args: - endpoint: OpenTelemetry Collector receiver endpoint - insecure: Connection type - credentials: Credentials object for server authentication - headers: Headers to send when exporting - timeout: Backend request timeout in seconds - compression: gRPC compression method to use - """ - - def __init__( - self, - endpoint: str | None = None, - insecure: bool | None = None, - credentials: ChannelCredentials | None = None, - headers: TypingSequence[tuple[str, str]] - | dict[str, str] - | str - | None = None, - timeout: float | None = None, - compression: Compression | None = None, - channel_options: tuple[tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, - *, - meter_provider: MeterProvider | None = None, - ): - insecure_spans = environ.get(OTEL_EXPORTER_OTLP_TRACES_INSECURE) - if insecure is None and insecure_spans is not None: - insecure = insecure_spans.lower() == "true" - - if ( - not insecure - and environ.get(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE) is not None - ): - credentials = _get_credentials( - credentials, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_TRACES_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - ) - - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_TRACES_TIMEOUT) - environ_timeout = ( - float(environ_timeout) if environ_timeout is not None else None - ) - - compression = ( - environ_to_compression(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) - if compression is None - else compression - ) - - OTLPExporterMixin.__init__( - self, - stub=TraceServiceStub, - result=SpanExportResult, - endpoint=endpoint - or environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), - insecure=insecure, - credentials=credentials, - headers=headers or environ.get(OTEL_EXPORTER_OTLP_TRACES_HEADERS), - timeout=timeout or environ_timeout, - compression=compression, - channel_options=channel_options, - retryable_error_codes=retryable_error_codes, - component_type=OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, - signal="traces", - meter_provider=meter_provider, - ) - - def _translate_data( - self, data: Sequence[ReadableSpan] - ) -> ExportTraceServiceRequest: - return encode_spans(data) - - def _count_data(self, data: Sequence[ReadableSpan]): - return len(data) - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - return self._export(spans) - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True - - @property - def _exporting(self): - return "traces" +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py index 524a0260e55..220d6d72702 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/version/__init__.py @@ -1,4 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.45.0.dev" +import sys as _sys +import opentelemetry.exporter.otlp._proto.grpc.version as _mod + +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/conftest.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/conftest.py new file mode 100644 index 00000000000..0879e7a2f97 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/conftest.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.proto.grpc import trace_exporter as _pub + +assert "pyproto" not in (_pub.__file__ or ""), ( + "opentelemetry.exporter.otlp.proto.grpc resolved to the pyproto shim " + f"({_pub.__file__}); equivalence tests need the real proto-grpc package." +) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/test_trace_equivalence.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/test_trace_equivalence.py new file mode 100644 index 00000000000..5af0dc1fa21 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/equivalence/test_trace_equivalence.py @@ -0,0 +1,52 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import patch + +from opentelemetry.exporter.otlp._proto.grpc.trace_exporter import ( + OTLPSpanExporter as PyprotoSpanExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter as ProtoSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter + + +def _sample_spans(): + captured = [] + + class _Capture(SpanExporter): + def export(self, spans): + captured.extend(spans) + return 0 + + def shutdown(self): + pass + + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(_Capture())) + tracer = provider.get_tracer("equivalence") + with tracer.start_as_current_span("op") as span: + span.set_attribute("str", "v") + span.set_attribute("int", 7) + span.set_attribute("bool", True) + return list(captured) + + +def test_grpc_serialized_request_matches_proto(): + spans = _sample_spans() + + with patch( + "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" + ): + proto_exporter = ProtoSpanExporter(insecure=True) + proto_bytes = proto_exporter._translate_data(spans).SerializeToString() + + with patch( + "opentelemetry.exporter.otlp._proto.grpc.exporter.insecure_channel" + ): + pyproto_exporter = PyprotoSpanExporter(insecure=True) + pyproto_bytes = pyproto_exporter._translate_data(spans).SerializeToString() + + assert pyproto_bytes == proto_bytes diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py deleted file mode 100644 index 5f501cd96eb..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ /dev/null @@ -1,543 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=too-many-lines - -import time -from os.path import dirname -from unittest import TestCase -from unittest.mock import Mock, patch - -from google.protobuf.json_format import MessageToDict -from grpc import ChannelCredentials, Compression - -from opentelemetry._logs import LogRecord, SeverityNumber -from opentelemetry.exporter.otlp.proto.common._internal import _encode_value -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( - OTLPLogExporter, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord as PB2LogRecord -from opentelemetry.proto.logs.v1.logs_pb2 import ResourceLogs, ScopeLogs -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) -from opentelemetry.sdk._logs import ReadWriteLogRecord -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, -) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - set_span_in_context, -) - -THIS_DIR = dirname(__file__) - - -class TestOTLPLogExporter(TestCase): - def setUp(self): - self.exporter = OTLPLogExporter() - ctx_log_data_1 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 2604504634922341076776623263868986797, - 5213367945872657620, - False, - TraceFlags(0x01), - ) - ) - ) - self.log_data_1 = ReadWriteLogRecord( - LogRecord( - timestamp=int(time.time() * 1e9), - context=ctx_log_data_1, - severity_text="WARNING", - severity_number=SeverityNumber.WARN, - body="Zhengzhou, We have a heaviest rains in 1000 years", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"key": "value"}), - instrumentation_scope=InstrumentationScope( - "first_name", "first_version" - ), - ) - ctx_log_data_2 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 2604504634922341076776623263868986799, - 5213367945872657623, - False, - TraceFlags(0x01), - ) - ) - ) - self.log_data_2 = ReadWriteLogRecord( - LogRecord( - timestamp=int(time.time() * 1e9), - context=ctx_log_data_2, - severity_text="INFO", - severity_number=SeverityNumber.INFO2, - body="Sydney, Opera House is closed", - attributes={"custom_attr": [1, 2, 3]}, - ), - resource=SDKResource({"key": "value"}), - instrumentation_scope=InstrumentationScope( - "second_name", "second_version" - ), - ) - ctx_log_data_3 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 2604504634922341076776623263868986800, - 5213367945872657628, - False, - TraceFlags(0x01), - ) - ) - ) - self.log_data_3 = ReadWriteLogRecord( - LogRecord( - timestamp=int(time.time() * 1e9), - context=ctx_log_data_3, - severity_text="ERROR", - severity_number=SeverityNumber.WARN, - body="Mumbai, Boil water before drinking", - ), - resource=SDKResource({"service": "myapp"}), - instrumentation_scope=InstrumentationScope( - "third_name", "third_version" - ), - ) - ctx_log_data_4 = set_span_in_context( - NonRecordingSpan( - SpanContext(0, 5213367945872657629, False, TraceFlags(0x01)) - ) - ) - self.log_data_4 = ReadWriteLogRecord( - LogRecord( - timestamp=int(time.time() * 1e9), - context=ctx_log_data_4, - severity_text="ERROR", - severity_number=SeverityNumber.WARN, - body="Invalid trace id check", - ), - resource=SDKResource({"service": "myapp"}), - instrumentation_scope=InstrumentationScope( - "fourth_name", "fourth_version" - ), - ) - ctx_log_data_5 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 2604504634922341076776623263868986801, - 0, - False, - TraceFlags(0x01), - ) - ) - ) - self.log_data_5 = ReadWriteLogRecord( - LogRecord( - timestamp=int(time.time() * 1e9), - context=ctx_log_data_5, - severity_text="ERROR", - severity_number=SeverityNumber.WARN, - body="Invalid span id check", - ), - resource=SDKResource({"service": "myapp"}), - instrumentation_scope=InstrumentationScope( - "fifth_name", "fifth_version" - ), - ) - - def test_exporting(self): - # pylint: disable=protected-access - self.assertEqual(self.exporter._exporting, "logs") - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables(self, mock_exporter_mixin): - OTLPLogExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "logs:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNone(kwargs["credentials"]) - - # Create a new test method specifically for client certificates - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR - + "/../fixtures/test.cert", - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: THIS_DIR - + "/../fixtures/test-client-cert.pem", - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: THIS_DIR - + "/../fixtures/test-client-key.pem", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables_with_client_certificates(self, mock_exporter_mixin): - OTLPLogExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "logs:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR - + "/../fixtures/test.cert", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - @patch("logging.Logger.error") - def test_env_variables_with_only_certificate( - self, mock_logger_error, mock_exporter_mixin - ): - OTLPLogExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "logs:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - mock_logger_error.assert_not_called() - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317", - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR - + "/../fixtures/test.cert", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2", - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - @patch("logging.Logger.error") - def test_kwargs_have_precedence_over_env_variables( - self, mock_logger_error, mock_exporter_mixin - ): - credentials_mock = Mock() - OTLPLogExporter( - endpoint="logs:4318", - headers=(("an", "header"),), - timeout=20, - credentials=credentials_mock, - compression=Compression.NoCompression, - channel_options=(("some", "options"),), - ) - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "logs:4318") - self.assertEqual(kwargs["headers"], (("an", "header"),)) - self.assertEqual(kwargs["timeout"], 20) - self.assertEqual(kwargs["compression"], Compression.NoCompression) - self.assertEqual(kwargs["credentials"], credentials_mock) - self.assertEqual(kwargs["channel_options"], (("some", "options"),)) - - mock_logger_error.assert_not_called() - - def export_log_and_deserialize(self, log_data): - # pylint: disable=protected-access - translated_data = self.exporter._translate_data([log_data]) - request_dict = MessageToDict(translated_data) - log_records = ( - request_dict.get("resourceLogs")[0] - .get("scopeLogs")[0] - .get("logRecords") - ) - return log_records - - def test_exported_log_without_trace_id(self): - log_records = self.export_log_and_deserialize(self.log_data_4) - if log_records: - log_record = log_records[0] - self.assertIn("spanId", log_record) - self.assertNotIn( - "traceId", - log_record, - "traceId should not be present in the log record", - ) - else: - self.fail("No log records found") - - def test_exported_log_without_span_id(self): - log_records = self.export_log_and_deserialize(self.log_data_5) - if log_records: - log_record = log_records[0] - self.assertIn("traceId", log_record) - self.assertNotIn( - "spanId", - log_record, - "spanId should not be present in the log record", - ) - else: - self.fail("No log records found") - - def test_translate_log_data(self): - expected = ExportLogsServiceRequest( - resource_logs=[ - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="key", value=AnyValue(string_value="value") - ), - ] - ), - scope_logs=[ - ScopeLogs( - scope=PB2InstrumentationScope( - name="first_name", version="first_version" - ), - log_records=[ - PB2LogRecord( - # pylint: disable=no-member - time_unix_nano=self.log_data_1.log_record.timestamp, - observed_time_unix_nano=self.log_data_1.log_record.observed_timestamp, - severity_number=self.log_data_1.log_record.severity_number.value, - severity_text="WARNING", - span_id=int.to_bytes( - 5213367945872657620, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986797, - 16, - "big", - ), - body=_encode_value( - "Zhengzhou, We have a heaviest rains in 1000 years" - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(string_value="c"), - ), - ], - flags=int( - self.log_data_1.log_record.trace_flags - ), - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual( - expected, self.exporter._translate_data([self.log_data_1]) - ) - - def test_count_log_data(self): - # pylint: disable=protected-access - self.assertEqual(1, self.exporter._count_data([self.log_data_1])) - - def test_translate_multiple_logs(self): - expected = ExportLogsServiceRequest( - resource_logs=[ - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="key", value=AnyValue(string_value="value") - ), - ] - ), - scope_logs=[ - ScopeLogs( - scope=PB2InstrumentationScope( - name="first_name", version="first_version" - ), - log_records=[ - PB2LogRecord( - # pylint: disable=no-member - time_unix_nano=self.log_data_1.log_record.timestamp, - observed_time_unix_nano=self.log_data_1.log_record.observed_timestamp, - severity_number=self.log_data_1.log_record.severity_number.value, - severity_text="WARNING", - span_id=int.to_bytes( - 5213367945872657620, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986797, - 16, - "big", - ), - body=_encode_value( - "Zhengzhou, We have a heaviest rains in 1000 years" - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(string_value="c"), - ), - ], - flags=int( - self.log_data_1.log_record.trace_flags - ), - ) - ], - ), - ScopeLogs( - scope=PB2InstrumentationScope( - name="second_name", version="second_version" - ), - log_records=[ - PB2LogRecord( - # pylint: disable=no-member - time_unix_nano=self.log_data_2.log_record.timestamp, - observed_time_unix_nano=self.log_data_2.log_record.observed_timestamp, - severity_number=self.log_data_2.log_record.severity_number.value, - severity_text="INFO", - span_id=int.to_bytes( - 5213367945872657623, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986799, - 16, - "big", - ), - body=_encode_value( - "Sydney, Opera House is closed" - ), - attributes=[ - KeyValue( - key="custom_attr", - value=_encode_value([1, 2, 3]), - ), - ], - flags=int( - self.log_data_2.log_record.trace_flags - ), - ) - ], - ), - ], - ), - ResourceLogs( - resource=OTLPResource( - attributes=[ - KeyValue( - key="service", - value=AnyValue(string_value="myapp"), - ), - ] - ), - scope_logs=[ - ScopeLogs( - scope=PB2InstrumentationScope( - name="third_name", version="third_version" - ), - log_records=[ - PB2LogRecord( - # pylint: disable=no-member - time_unix_nano=self.log_data_3.log_record.timestamp, - observed_time_unix_nano=self.log_data_3.log_record.observed_timestamp, - severity_number=self.log_data_3.log_record.severity_number.value, - severity_text="ERROR", - span_id=int.to_bytes( - 5213367945872657628, 8, "big" - ), - trace_id=int.to_bytes( - 2604504634922341076776623263868986800, - 16, - "big", - ), - body=_encode_value( - "Mumbai, Boil water before drinking" - ), - attributes=[], - flags=int( - self.log_data_3.log_record.trace_flags - ), - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual( - expected, - self.exporter._translate_data( - [self.log_data_1, self.log_data_2, self.log_data_3] - ), - ) - - def test_count_multiple_logs(self): - self.assertEqual( - 3, - # pylint: disable=protected-access - self.exporter._count_data( - [self.log_data_1, self.log_data_2, self.log_data_3] - ), - ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/test_benchmark_pygrpc.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/test_benchmark_pygrpc.py new file mode 100644 index 00000000000..1fe59b22ffa --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/performance/test_benchmark_pygrpc.py @@ -0,0 +1,128 @@ +# tests/performance/test_benchmark_pygrpc.py +# +# Benchmark: the pure-Python gRPC transport (_pygrpc) send-side hot path. +# +# The distribution replaces grpcio's C-core HTTP/2 stack with a hand-rolled +# pure-Python one. Unlike protobuf encoding (amortized over a whole export +# batch), this cost is paid *per RPC*: every export HPACK-encodes the request +# headers and builds HTTP/2 HEADERS + DATA frames. HPACK (with Huffman) and +# frame construction are tight byte-loops — exactly where pure-Python is +# slowest — so this file measures that per-RPC overhead in absolute terms. +# +# grpcio does not expose HPACK/framing as callable units (it is buried in the C +# core), so there is no drop-in C baseline. Where a fair pure-Python reference +# exists — the MIT-licensed ``hpack`` package used by h2 — we benchmark against +# it to show our codec is competitive; that comparison is skipped when the +# package is absent. The absolute numbers stand on their own regardless. +# +# Run: +# uv pip install . && uv pip install pytest-benchmark hpack +# uv run pytest tests/performance/test_benchmark_pygrpc.py \ +# --benchmark-group-by=group,param --benchmark-sort=fullname + +from pytest import mark, skip + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import frames +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.hpack import ( + Decoder, + encode as hpack_encode, +) +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.client import _frame_message + +try: + import hpack as ref_hpack +except ImportError: # pragma: no cover + ref_hpack = None + + +# Headers a unary OTLP/gRPC export sends (mirrors _pygrpc.client.unary_call). +_GRPC_REQUEST_HEADERS = [ + (b":method", b"POST"), + (b":scheme", b"https"), + (b":path", b"/opentelemetry.proto.collector.trace.v1.TraceService/Export"), + (b":authority", b"ingress.example.com:4317"), + (b"te", b"trailers"), + (b"content-type", b"application/grpc"), + (b"grpc-timeout", b"10S"), + (b"grpc-encoding", b"identity"), + (b"grpc-accept-encoding", b"identity, gzip"), + (b"user-agent", b"otel-otlp-pyproto-exporter"), + (b"authorization", b"Bearer sekrit-token"), +] + +# OTLP export message-body sizes: a single small span, a typical batch, and a +# large batch. Content is irrelevant to framing cost, only length matters. +_MESSAGE_SIZES = [("small_200B", 200), ("batch_16KB", 16_384), ("large_256KB", 262_144)] +_STREAM_ID = 1 + + +def _grpc_data_frame(message_bytes: bytes) -> bytes: + """gRPC length-prefix a message and wrap it in an end-of-stream DATA frame.""" + framed = _frame_message(message_bytes, compress=False) + return frames.encode_frame( + frames.Frame(frames.DATA, frames.FLAG_END_STREAM, _STREAM_ID, framed) + ) + + +def _full_request_send(message_bytes: bytes) -> bytes: + """The complete pure-Python send-side CPU cost of one unary export RPC: + HPACK-encode headers, build the HEADERS frame, gRPC-frame the message, and + build the DATA frame.""" + header_block = hpack_encode(_GRPC_REQUEST_HEADERS) + headers_frame = frames.encode_frame( + frames.Frame( + frames.HEADERS, frames.FLAG_END_HEADERS, _STREAM_ID, header_block + ) + ) + return headers_frame + _grpc_data_frame(message_bytes) + + +# ── Fairness / validity guards ────────────────────────────────────────────── + +def test_hpack_roundtrips() -> None: + block = hpack_encode(_GRPC_REQUEST_HEADERS) + assert Decoder().decode(block) == _GRPC_REQUEST_HEADERS + + +@mark.skipif(ref_hpack is None, reason="reference hpack package not installed") +def test_hpack_output_decodes_under_reference() -> None: + # Our block need not be byte-identical to the reference encoder's (indexing + # strategies differ), but it must be valid HPACK the reference can read. + decoded = ref_hpack.Decoder().decode(hpack_encode(_GRPC_REQUEST_HEADERS), raw=True) + assert decoded == _GRPC_REQUEST_HEADERS + + +# ── HPACK header encoding: ours vs the reference pure-Python codec ────────── + +@mark.benchmark(group="hpack_encode_headers") +def test_hpack_headers_pygrpc(benchmark) -> None: + result = benchmark(lambda: hpack_encode(_GRPC_REQUEST_HEADERS)) + assert len(result) > 0 + + +@mark.skipif(ref_hpack is None, reason="reference hpack package not installed") +@mark.benchmark(group="hpack_encode_headers") +def test_hpack_headers_reference(benchmark) -> None: + # Fresh encoder per call to match our stateless module-level encode(). + result = benchmark(lambda: ref_hpack.Encoder().encode(_GRPC_REQUEST_HEADERS)) + assert len(result) > 0 + + +# ── HTTP/2 DATA-frame construction, by message size ───────────────────────── + +@mark.parametrize("label,size", _MESSAGE_SIZES, ids=[s[0] for s in _MESSAGE_SIZES]) +@mark.benchmark(group="data_frame") +def test_data_frame_pygrpc(benchmark, label, size) -> None: + message = b"\x7f" * size + result = benchmark(lambda: _grpc_data_frame(message)) + assert len(result) > size + + +# ── Full per-RPC send-side (headers + framing), by message size ───────────── + +@mark.parametrize("label,size", _MESSAGE_SIZES, ids=[s[0] for s in _MESSAGE_SIZES]) +@mark.benchmark(group="full_request_send") +def test_full_request_send_pygrpc(benchmark, label, size) -> None: + message = b"\x7f" * size + result = benchmark(lambda: _full_request_send(message)) + assert len(result) > size diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py deleted file mode 100644 index 50996264a09..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ /dev/null @@ -1,797 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import threading -import time -import unittest -from collections.abc import Sequence -from concurrent.futures import ( # pylint: disable=no-name-in-module - ThreadPoolExecutor, -) -from logging import WARNING, getLogger -from platform import system -from typing import Any -from unittest import TestCase -from unittest.mock import Mock, patch - -import grpc -from google.protobuf.duration_pb2 import ( # pylint: disable=no-name-in-module - Duration, -) -from google.rpc.error_details_pb2 import ( # pylint: disable=no-name-in-module - RetryInfo, -) -from grpc import ChannelCredentials, Compression, StatusCode, server - -from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( - encode_spans, -) -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( # noqa: F401 - _RETRYABLE_ERROR_CODES, - InvalidCompressionValueException, - OTLPExporterMixin, - environ_to_compression, -) -from opentelemetry.exporter.otlp.proto.grpc.version import __version__ -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest, - ExportTraceServiceResponse, -) -from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import ( - TraceServiceServicer, - TraceServiceStub, - add_TraceServiceServicer_to_server, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import InMemoryMetricReader -from opentelemetry.sdk.trace import ReadableSpan, _Span -from opentelemetry.sdk.trace.export import ( - SpanExporter, - SpanExportResult, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.test.mock_test_classes import IterEntryPoint - -logger = getLogger(__name__) - - -# The below tests use this test SpanExporter and Spans, but are testing the -# underlying behavior in the mixin. A MetricExporter or LogRecordExporter could -# just as easily be used. -class OTLPSpanExporterForTesting( - SpanExporter, - OTLPExporterMixin[ - ReadableSpan, - ExportTraceServiceRequest, - SpanExportResult, - TraceServiceStub, - ], -): - def __init__(self, **kwargs): - super().__init__( - TraceServiceStub, - SpanExportResult, - component_type=OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, - signal="traces", - meter_provider=kwargs.pop("meter_provider", None), - **kwargs, - ) - - def _translate_data( - self, data: Sequence[ReadableSpan] - ) -> ExportTraceServiceRequest: - return encode_spans(data) - - def _count_data(self, data: Sequence[ReadableSpan]) -> int: - return len(data) - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - return self._export(spans) - - @property - def _exporting(self): - return "traces" - - def shutdown(self, timeout_millis: float = 30_000, **kwargs): - return OTLPExporterMixin.shutdown(self, timeout_millis, **kwargs) - - -class TraceServiceServicerWithExportParams(TraceServiceServicer): - def __init__( - self, - export_result: StatusCode, - optional_retry_nanos: int | None = None, - optional_export_sleep: float | None = None, - optional_error_details: str | None = None, - ): - self.export_result = export_result - self.optional_export_sleep = optional_export_sleep - self.optional_retry_nanos = optional_retry_nanos - self.num_requests = 0 - self.optional_error_details = optional_error_details - - # pylint: disable=invalid-name,unused-argument - def Export(self, request, context): - self.num_requests += 1 - if self.optional_export_sleep: - time.sleep(self.optional_export_sleep) - if self.export_result != StatusCode.OK and self.optional_retry_nanos: - context.set_trailing_metadata( - ( - ( - "google.rpc.retryinfo-bin", - RetryInfo( - retry_delay=Duration( - nanos=self.optional_retry_nanos - ) - ).SerializeToString(), - ), - ) - ) - context.set_code(self.export_result) - if self.optional_error_details: - context.set_details(self.optional_error_details) - - return ExportTraceServiceResponse() - - -class ThreadWithReturnValue(threading.Thread): - def __init__( - self, - target=None, - args=(), - ): - super().__init__(target=target, args=args) - self._return = None - - def run(self): - try: - if self._target is not None: # type: ignore - self._return = self._target(*self._args, **self._kwargs) # type: ignore - finally: - # Avoid a refcycle if the thread is running a function with - # an argument that has a member that points to the thread. - del self._target, self._args, self._kwargs # type: ignore - - def join(self, timeout: float | None = None) -> Any: - super().join(timeout=timeout) - return self._return - - -# pylint: disable-next=too-many-public-methods -class TestOTLPExporterMixin(TestCase): - def setUp(self): - self.server = server(ThreadPoolExecutor(max_workers=10)) - - self.server.add_insecure_port("127.0.0.1:4317") - - self.server.start() - - self.metric_reader = InMemoryMetricReader() - self.meter_provider = MeterProvider( - metric_readers=[self.metric_reader] - ) - self.exporter = OTLPSpanExporterForTesting( - insecure=True, meter_provider=self.meter_provider - ) - self.span = _Span( - "a", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), - ) - - def tearDown(self): - self.server.stop(None) - - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - def test_otlp_exporter_endpoint(self, mock_secure, mock_insecure): - expected_endpoint = "localhost:4317" - endpoints = [ - ( - "http://localhost:4317", - None, - mock_insecure, - ), - ( - "localhost:4317", - None, - mock_secure, - ), - ( - "http://localhost:4317", - True, - mock_insecure, - ), - ( - "localhost:4317", - True, - mock_insecure, - ), - ( - "http://localhost:4317", - False, - mock_secure, - ), - ( - "localhost:4317", - False, - mock_secure, - ), - ( - "https://localhost:4317", - False, - mock_secure, - ), - ( - "https://localhost:4317", - None, - mock_secure, - ), - ( - "https://localhost:4317", - True, - mock_secure, - ), - ] - for endpoint, insecure, mock_method in endpoints: - OTLPSpanExporterForTesting(endpoint=endpoint, insecure=insecure) - self.assertEqual( - 1, - mock_method.call_count, - f"expected {mock_method} to be called for {endpoint} {insecure}", - ) - self.assertEqual( - expected_endpoint, - mock_method.call_args[0][0], - f"expected {expected_endpoint} got {mock_method.call_args[0][0]} {endpoint}", - ) - mock_method.reset_mock() - - def test_environ_to_compression(self): - with patch.dict( - "os.environ", - { - "test_gzip": "gzip", - "test_gzip_caseinsensitive_with_whitespace": " GzIp ", - "test_invalid": "some invalid compression", - }, - ): - self.assertEqual( - environ_to_compression("test_gzip"), Compression.Gzip - ) - self.assertEqual( - environ_to_compression( - "test_gzip_caseinsensitive_with_whitespace" - ), - Compression.Gzip, - ) - self.assertIsNone( - environ_to_compression("missing_key"), - ) - with self.assertRaises(InvalidCompressionValueException): - environ_to_compression("test_invalid") - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch.dict("os.environ", {}) - def test_otlp_exporter_otlp_compression_unspecified( - self, mock_insecure_channel - ): - """No env or kwarg should be NoCompression""" - OTLPSpanExporterForTesting(insecure=True) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.NoCompression, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ), - ) - - @patch.dict( - "os.environ", - { - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider" - }, - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.entry_points") - def test_that_credential_gets_passed_to_exporter(self, mock_entry_points): - credential = ChannelCredentials(None) - - def f(): - return credential - - mock_entry_points.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - exporter = OTLPSpanExporterForTesting(insecure=False) - # pylint: disable=protected-access - assert exporter._credentials is credential - - @patch.dict( - "os.environ", - { - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider" - }, - ) - def test_that_missing_entry_point_raises_exception(self): - with self.assertRaises(RuntimeError): - OTLPSpanExporterForTesting(insecure=False) - - @patch.dict( - "os.environ", - { - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER: "credential_provider" - }, - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.entry_points") - def test_that_entry_point_returning_bad_type_raises_exception( - self, mock_entry_points - ): - def f(): - return 1 - - mock_entry_points.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - with self.assertRaises(RuntimeError): - OTLPSpanExporterForTesting(insecure=False) - - # pylint: disable=no-self-use, disable=unused-argument - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - @patch.dict("os.environ", {}) - def test_no_credentials_ssl_channel_called( - self, secure_channel, mock_ssl_channel - ): - OTLPSpanExporterForTesting(insecure=False) - self.assertTrue(mock_ssl_channel.called) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) - def test_otlp_exporter_otlp_compression_envvar( - self, mock_insecure_channel - ): - """Just OTEL_EXPORTER_OTLP_COMPRESSION should work""" - OTLPSpanExporterForTesting(insecure=True) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.Gzip, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ), - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "} - ) - def test_shutdown(self): - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams(StatusCode.OK), - self.server, - ) - exporter = OTLPSpanExporterForTesting( - insecure=True, meter_provider=self.meter_provider - ) - self.assertEqual( - exporter.export([self.span]), SpanExportResult.SUCCESS - ) - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - - exporter.shutdown() - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export([self.span]), SpanExportResult.FAILURE - ) - self.assertEqual( - warning.records[0].message, - "Exporter already shutdown, ignoring batch", - ) - - @unittest.skipIf( - system() == "Windows", - "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", - ) - def test_shutdown_interrupts_export_retry_backoff(self): - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams( - StatusCode.UNAVAILABLE, - ), - self.server, - ) - - export_thread = ThreadWithReturnValue( - target=self.exporter.export, args=([self.span],) - ) - with self.assertLogs(level=WARNING) as warning: - begin_wait = time.time() - export_thread.start() - # Wait a bit for export to fail and the backoff sleep to start - time.sleep(0.05) - # The code should now be in a 1 second backoff. - # pylint: disable=protected-access - self.assertFalse(self.exporter._shutdown_in_progress.is_set()) - self.exporter.shutdown() - self.assertTrue(self.exporter._shutdown_in_progress.is_set()) - export_result = export_thread.join() - end_wait = time.time() - self.assertEqual(export_result, SpanExportResult.FAILURE) - # Shutdown should have interrupted the sleep. - self.assertTrue(end_wait - begin_wait < 0.2) - self.assertEqual( - warning.records[1].message, - "Shutdown in progress, aborting retry.", - ) - - def test_export_over_closed_grpc_channel(self): - # pylint: disable=protected-access - - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams(StatusCode.OK), - self.server, - ) - self.exporter.export([self.span]) - self.exporter.shutdown() - data = self.exporter._translate_data([self.span]) - with self.assertRaises(ValueError) as err: - self.exporter._client.Export(request=data) - self.assertEqual( - str(err.exception), "Cannot invoke RPC on closed channel!" - ) - - @unittest.skipIf( - system() == "Windows", - "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", - ) - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - def test_retry_info_is_respected(self): - mock_trace_service = TraceServiceServicerWithExportParams( - StatusCode.UNAVAILABLE, - optional_retry_nanos=200000000, # .2 seconds - ) - add_TraceServiceServicer_to_server( - mock_trace_service, - self.server, - ) - exporter = OTLPSpanExporterForTesting( - insecure=True, timeout=10, meter_provider=self.meter_provider - ) - before = time.time() - self.assertEqual( - exporter.export([self.span]), - SpanExportResult.FAILURE, - ) - after = time.time() - self.assertEqual(mock_trace_service.num_requests, 6) - # 1 second plus wiggle room so the test passes consistently. - self.assertAlmostEqual(after - before, 1, 1) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[0].data.data_points[0].attributes["error.type"], - "_InactiveRpcError", - ) - self.assertEqual( - metrics[0] - .data.data_points[0] - .attributes["rpc.response.status_code"], - "UNAVAILABLE", - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertEqual( - metrics[1].data.data_points[0].attributes["error.type"], - "_InactiveRpcError", - ) - self.assertNotIn( - "rpc.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "rpc.response.status_code", - metrics[2].data.data_points[0].attributes, - ) - - @unittest.skipIf( - system() == "Windows", - "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", - ) - def test_retry_not_made_if_would_exceed_timeout(self): - mock_trace_service = TraceServiceServicerWithExportParams( - StatusCode.UNAVAILABLE - ) - add_TraceServiceServicer_to_server( - mock_trace_service, - self.server, - ) - exporter = OTLPSpanExporterForTesting(insecure=True, timeout=4) - before = time.time() - self.assertEqual( - exporter.export([self.span]), - SpanExportResult.FAILURE, - ) - after = time.time() - # Our retry starts with a 1 second backoff then doubles. - # First call at time 0, second at time 1, third at time 3, fourth would exceed timeout. - self.assertEqual(mock_trace_service.num_requests, 3) - # There's a +/-20% jitter on each backoff. - self.assertTrue(2.35 < after - before < 3.65) - - @unittest.skipIf( - system() == "Windows", - "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", - ) - def test_timeout_set_correctly(self): - mock_trace_service = TraceServiceServicerWithExportParams( - StatusCode.UNAVAILABLE, optional_export_sleep=0.25 - ) - add_TraceServiceServicer_to_server( - mock_trace_service, - self.server, - ) - exporter = OTLPSpanExporterForTesting(insecure=True, timeout=1.4) - # Should timeout after 1.4 seconds. First attempt takes .25 seconds - # Then a 1 second sleep, then deadline exceeded after .15 seconds, - # mid way through second call. - with self.assertLogs(level=WARNING) as warning: - before = time.time() - # Eliminate the jitter. - with patch("random.uniform", return_value=1): - self.assertEqual( - exporter.export([self.span]), - SpanExportResult.FAILURE, - ) - after = time.time() - self.assertEqual( - "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED, error details: Deadline Exceeded", - warning.records[-1].message, - ) - self.assertEqual(mock_trace_service.num_requests, 2) - self.assertAlmostEqual(after - before, 1.4, 1) - - def test_channel_options_set_correctly(self): - """Test that gRPC channel options are set correctly for keepalive and reconnection""" - # This test verifies that the channel is created with the right options - # We patch grpc.insecure_channel to ensure it is called without errors - with patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" - ) as mock_channel: - OTLPSpanExporterForTesting(insecure=True) - self.assertTrue(mock_channel.called) - - def test_otlp_headers_from_env(self): - # pylint: disable=protected-access - # This ensures that there is no other header than standard user-agent. - self.assertEqual( - self.exporter._headers, - (), - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - def test_permanent_failure(self): - exporter = OTLPSpanExporterForTesting( - insecure=True, meter_provider=self.meter_provider - ) - with self.assertLogs(level=WARNING) as warning: - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams( - StatusCode.ALREADY_EXISTS, - optional_error_details="This already exists.", - ), - self.server, - ) - self.assertEqual( - exporter.export([self.span]), SpanExportResult.FAILURE - ) - self.assertEqual( - warning.records[-1].message, - "Failed to export traces to localhost:4317, error code: StatusCode.ALREADY_EXISTS, error details: This already exists.", - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[0].data.data_points[0].attributes["error.type"], - "_InactiveRpcError", - ) - self.assertEqual( - metrics[0] - .data.data_points[0] - .attributes["rpc.response.status_code"], - "ALREADY_EXISTS", - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertEqual( - metrics[1].data.data_points[0].attributes["error.type"], - "_InactiveRpcError", - ) - self.assertNotIn( - "rpc.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "rpc.response.status_code", - metrics[2].data.data_points[0].attributes, - ) - - def test_unavailable_reconnects(self): - """Test that the exporter reconnects on UNAVAILABLE error""" - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams(StatusCode.UNAVAILABLE), - self.server, - ) - - # Spy on grpc.insecure_channel to verify it's called for reconnection - with patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel", - side_effect=grpc.insecure_channel, - ) as mock_insecure_channel: - # Mock sleep to avoid waiting - with patch("time.sleep"): - # We expect FAILURE because the server keeps returning UNAVAILABLE - # but we want to verify reconnection attempts happened - self.exporter.export([self.span]) - - # Verify that we attempted to reinitialize the channel (called insecure_channel) - # Since the initial channel was created in setUp (unpatched), this call - # must be from the reconnection logic. - self.assertTrue(mock_insecure_channel.called) - - def test_retryable_error_codes_initialization(self): - # pylint: disable=protected-access - self.assertEqual( - self.exporter._retryable_error_codes, _RETRYABLE_ERROR_CODES - ) - custom_codes = [StatusCode.INTERNAL, StatusCode.UNKNOWN] - exporter = OTLPSpanExporterForTesting( - insecure=True, retryable_error_codes=custom_codes - ) - self.assertEqual( - exporter._retryable_error_codes, frozenset(custom_codes) - ) - - @patch.dict( - "os.environ", - { - "OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES": ",INTERNAL, unknown,,,dEAdline_Exceeded " - }, - ) - def test_retryable_error_codes_initialization_from_env(self): - expected_codes = frozenset( - { - StatusCode.INTERNAL, - StatusCode.UNKNOWN, - StatusCode.DEADLINE_EXCEEDED, - } - ) - exporter = OTLPSpanExporterForTesting() - # pylint: disable=protected-access - self.assertEqual(exporter._retryable_error_codes, expected_codes) - - @unittest.skipIf( - system() == "Windows", - "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", - ) - def test_retryable_error_codes_custom(self): - # Test that a custom error code is retried if specified - custom_codes = [StatusCode.INTERNAL] - mock_trace_service = TraceServiceServicerWithExportParams( - StatusCode.INTERNAL, - optional_retry_nanos=200000000, # .2 seconds - ) - add_TraceServiceServicer_to_server( - mock_trace_service, - self.server, - ) - exporter = OTLPSpanExporterForTesting( - insecure=True, retryable_error_codes=custom_codes, timeout=10 - ) - - self.assertEqual( - exporter.export([self.span]), - SpanExportResult.FAILURE, - ) - - self.assertEqual(mock_trace_service.num_requests, 6) - - # Test that a default retryable code is NOT retried if not in custom_codes - mock_trace_service.num_requests = 0 - mock_trace_service.export_result = StatusCode.UNAVAILABLE - self.assertEqual( - exporter.export([self.span]), - SpanExportResult.FAILURE, - ) - self.assertEqual(mock_trace_service.num_requests, 1) - - def assert_standard_metric_attrs(self, attributes): - self.assertEqual( - attributes["otel.component.type"], "otlp_grpc_span_exporter" - ) - self.assertTrue( - attributes["otel.component.name"].startswith( - "otlp_grpc_span_exporter/" - ) - ) - self.assertEqual(attributes["server.address"], "localhost") - self.assertEqual(attributes["server.port"], 4317) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py deleted file mode 100644 index 496fe3054dc..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ /dev/null @@ -1,855 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=too-many-lines -from logging import WARNING -from os import environ -from os.path import dirname -from unittest import TestCase -from unittest.mock import patch - -from grpc import ChannelCredentials, Compression - -from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( - OTLPMetricExporter, -) -from opentelemetry.exporter.otlp.proto.grpc.version import __version__ -from opentelemetry.proto.common.v1.common_pb2 import InstrumentationScope -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_INSECURE, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, -) -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - Gauge, - Metric, - MetricsData, - NumberDataPoint, - ResourceMetrics, - ScopeMetrics, -) -from opentelemetry.sdk.metrics.view import ( - ExplicitBucketHistogramAggregation, - ExponentialBucketHistogramAggregation, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import ( - InstrumentationScope as SDKInstrumentationScope, -) -from opentelemetry.test.metrictestutil import _generate_sum - -THIS_DIR = dirname(__file__) - - -class TestOTLPMetricExporter(TestCase): - # pylint: disable=too-many-public-methods - - def setUp(self): - self.exporter = OTLPMetricExporter() - - self.metrics = { - "sum_int": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_int", 33)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - } - - def test_exporting(self): - # pylint: disable=protected-access - self.assertEqual(self.exporter._exporting, "metrics") - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, - ) - def test_preferred_temporality(self): - # pylint: disable=protected-access - exporter = OTLPMetricExporter( - preferred_temporality={Counter: AggregationTemporality.CUMULATIVE} - ) - self.assertEqual( - exporter._preferred_temporality[Counter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - exporter._preferred_temporality[UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - exporter._preferred_temporality[Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - exporter._preferred_temporality[ObservableCounter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - exporter._preferred_temporality[ObservableUpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - exporter._preferred_temporality[ObservableGauge], - AggregationTemporality.CUMULATIVE, - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables(self, mock_exporter_mixin): - OTLPMetricExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNone(kwargs["credentials"]) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR - + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: THIS_DIR - + "/fixtures/test-client-cert.pem", - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: THIS_DIR - + "/fixtures/test-client-key.pem", - OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables_with_client_certificates(self, mock_exporter_mixin): - OTLPMetricExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: THIS_DIR - + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - @patch("logging.Logger.error") - def test_env_variables_with_only_certificate( - self, mock_logger_error, mock_exporter_mixin - ): - OTLPMetricExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - mock_logger_error.assert_not_called() - - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - # pylint: disable=unused-argument - def test_no_credentials_error(self, mock_ssl_channel, mock_secure): - OTLPMetricExporter(insecure=False) - self.assertTrue(mock_ssl_channel.called) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_METRICS_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - # pylint: disable=unused-argument - def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): - exporter = OTLPMetricExporter() - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, - ( - ("key1", "value1"), - ("key2", "VALUE=2"), - ), - ) - exporter = OTLPMetricExporter( - headers=(("key3", "value3"), ("key4", "value4")) - ) - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, - ( - ("key3", "value3"), - ("key4", "value4"), - ), - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_METRICS_INSECURE: "True"}, - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - # pylint: disable=unused-argument - def test_otlp_insecure_from_env(self, mock_insecure): - OTLPMetricExporter() - # pylint: disable=protected-access - self.assertTrue(mock_insecure.called) - self.assertEqual( - 1, - mock_insecure.call_count, - f"expected {mock_insecure} to be called", - ) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) - def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): - """Specifying kwarg should take precedence over env""" - OTLPMetricExporter( - insecure=True, compression=Compression.NoCompression - ) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.NoCompression, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ), - ) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - def test_otlp_exporter_otlp_channel_options_kwarg( - self, mock_insecure_channel - ): - OTLPMetricExporter( - insecure=True, channel_options=(("some", "options"),) - ) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.NoCompression, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ("some", "options"), - ), - ) - - def test_split_metrics_data_many_data_points(self): - # GIVEN - metrics_data = MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ) - # WHEN - split_metrics_data: list[MetricsData] = list( - # pylint: disable=protected-access - OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( - metrics_data=metrics_data, - ) - ) - # THEN - self.assertEqual( - [ - MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - ], - ), - ], - ), - ], - ), - ] - ), - MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_split_metrics_data_nb_data_points_equal_batch_size(self): - # GIVEN - metrics_data = MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ) - # WHEN - split_metrics_data: list[MetricsData] = list( - # pylint: disable=protected-access - OTLPMetricExporter(max_export_batch_size=3)._split_metrics_data( - metrics_data=metrics_data, - ) - ) - # THEN - self.assertEqual( - [ - MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_split_metrics_data_many_resources_scopes_metrics(self): - # GIVEN - metrics_data = MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - ], - ), - _gauge( - index=2, - data_points=[ - _number_data_point(12), - ], - ), - ], - ), - _scope_metrics( - index=2, - metrics=[ - _gauge( - index=3, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - _resource_metrics( - index=2, - scope_metrics=[ - _scope_metrics( - index=3, - metrics=[ - _gauge( - index=4, - data_points=[ - _number_data_point(14), - ], - ), - ], - ), - ], - ), - ] - ) - # WHEN - split_metrics_data: list[MetricsData] = list( - # pylint: disable=protected-access - OTLPMetricExporter(max_export_batch_size=2)._split_metrics_data( - metrics_data=metrics_data, - ) - ) - # THEN - self.assertEqual( - [ - MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - ], - ), - _gauge( - index=2, - data_points=[ - _number_data_point(12), - ], - ), - ], - ), - ], - ), - ] - ), - MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=2, - metrics=[ - _gauge( - index=3, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - _resource_metrics( - index=2, - scope_metrics=[ - _scope_metrics( - index=3, - metrics=[ - _gauge( - index=4, - data_points=[ - _number_data_point(14), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_count_metrics_data(self): - # GIVEN - metrics_data = MetricsData( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - ], - ), - _gauge( - index=2, - data_points=[ - _number_data_point(12), - ], - ), - ], - ), - _scope_metrics( - index=2, - metrics=[ - _gauge( - index=3, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - _resource_metrics( - index=2, - scope_metrics=[ - _scope_metrics( - index=3, - metrics=[ - _gauge( - index=4, - data_points=[ - _number_data_point(14), - ], - ), - ], - ), - ], - ), - ] - ) - # WHEN - # pylint: disable=protected-access - count = OTLPMetricExporter(max_export_batch_size=2)._count_data( - metrics_data, - ) - # THEN - self.assertEqual(count, 4) - - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - def test_insecure_https_endpoint(self, mock_secure_channel): - OTLPMetricExporter(endpoint="https://ab.c:123", insecure=True) - mock_secure_channel.assert_called() - - def test_aggregation_temporality(self): - # pylint: disable=protected-access - - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual( - temporality, AggregationTemporality.CUMULATIVE - ) - - with patch.dict( - environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"} - ): - with self.assertLogs(level=WARNING): - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual( - temporality, AggregationTemporality.CUMULATIVE - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Counter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableCounter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ - ObservableUpDownCounter - ], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableGauge], - AggregationTemporality.CUMULATIVE, - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Counter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ - ObservableUpDownCounter - ], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableGauge], - AggregationTemporality.CUMULATIVE, - ) - - def test_exponential_explicit_bucket_histogram(self): - self.assertIsInstance( - # pylint: disable=protected-access - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - - with patch.dict( - environ, - { - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram" - }, - ): - self.assertIsInstance( - # pylint: disable=protected-access - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExponentialBucketHistogramAggregation, - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "abc"}, - ): - with self.assertLogs(level=WARNING) as log: - self.assertIsInstance( - # pylint: disable=protected-access - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - self.assertIn( - ( - "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" - "HISTOGRAM_AGGREGATION: abc, using explicit bucket " - "histogram aggregation" - ), - log.output[0], - ) - - with patch.dict( - environ, - { - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram" - }, - ): - self.assertIsInstance( - # pylint: disable=protected-access - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - - def test_preferred_aggregation_override(self): - histogram_aggregation = ExplicitBucketHistogramAggregation( - boundaries=[0.05, 0.1, 0.5, 1, 5, 10], - ) - - exporter = OTLPMetricExporter( - preferred_aggregation={ - Histogram: histogram_aggregation, - }, - ) - - self.assertEqual( - # pylint: disable=protected-access - exporter._preferred_aggregation[Histogram], - histogram_aggregation, - ) - - -def _resource_metrics( - index: int, scope_metrics: list[ScopeMetrics] -) -> ResourceMetrics: - return ResourceMetrics( - resource=Resource( - attributes={"a": index}, - schema_url=f"resource_url_{index}", - ), - schema_url=f"resource_url_{index}", - scope_metrics=scope_metrics, - ) - - -def _scope_metrics(index: int, metrics: list[Metric]) -> ScopeMetrics: - return ScopeMetrics( - scope=InstrumentationScope(name=f"scope_{index}"), - schema_url=f"scope_url_{index}", - metrics=metrics, - ) - - -def _gauge(index: int, data_points: list[NumberDataPoint]) -> Metric: - return Metric( - name=f"gauge_{index}", - description="description", - unit="unit", - data=Gauge(data_points=data_points), - ) - - -def _number_data_point(value: int) -> NumberDataPoint: - return NumberDataPoint( - attributes={"a": 1, "b": True}, - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - value=value, - ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py deleted file mode 100644 index 64cd091a6c1..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ /dev/null @@ -1,799 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=too-many-lines - -import os -from unittest import TestCase -from unittest.mock import Mock, PropertyMock, patch - -from grpc import ChannelCredentials, Compression - -from opentelemetry.attributes import BoundedAttributes -from opentelemetry.exporter.otlp.proto.common._internal import ( - _encode_key_value, -) -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( - OTLPSpanExporter, -) -from opentelemetry.exporter.otlp.proto.grpc.version import __version__ -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( - ExportTraceServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - AnyValue, - ArrayValue, - KeyValue, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as OTLPResource, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( - ResourceSpans, - ScopeSpans, - Status, -) -from opentelemetry.proto.trace.v1.trace_pb2 import Span as OTLPSpan -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_INSECURE, - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, -) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.trace import Status as SDKStatus -from opentelemetry.sdk.trace import StatusCode as SDKStatusCode -from opentelemetry.sdk.trace import TracerProvider, _Span -from opentelemetry.sdk.trace.export import ( - SimpleSpanProcessor, -) -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.test.spantestutil import ( - get_span_with_dropped_attributes_events_links, -) - -THIS_DIR = os.path.dirname(__file__) - - -class TestOTLPSpanExporter(TestCase): - # pylint: disable=too-many-public-methods - - def setUp(self): - tracer_provider = TracerProvider() - self.exporter = OTLPSpanExporter(insecure=True) - tracer_provider.add_span_processor(SimpleSpanProcessor(self.exporter)) - self.tracer = tracer_provider.get_tracer(__name__) - - event_mock = Mock( - **{ - "timestamp": 1591240820506462784, - "attributes": BoundedAttributes( - attributes={"a": 1, "b": False} - ), - } - ) - - type(event_mock).name = PropertyMock(return_value="a") - type(event_mock).dropped_attributes = PropertyMock(return_value=0) - self.span = _Span( - "a", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), - resource=SDKResource({"a": 1, "b": False}), - parent=Mock(**{"span_id": 12345}), - attributes=BoundedAttributes(attributes={"a": 1, "b": True}), - events=[event_mock], - links=[ - Mock( - **{ - "context.trace_id": 1, - "context.span_id": 2, - "attributes": BoundedAttributes( - attributes={"a": 1, "b": False} - ), - "dropped_attributes": 0, - "kind": OTLPSpan.SpanKind.SPAN_KIND_INTERNAL, # pylint: disable=no-member - } - ) - ], - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), - ) - - self.span2 = _Span( - "b", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), - resource=SDKResource({"a": 2, "b": False}), - parent=Mock(**{"span_id": 12345}), - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), - ) - - self.span3 = _Span( - "c", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), - resource=SDKResource({"a": 1, "b": False}), - parent=Mock(**{"span_id": 12345}), - instrumentation_scope=InstrumentationScope( - name="name2", version="version2" - ), - ) - - self.span.start() - self.span.end() - self.span2.start() - self.span2.end() - self.span3.start() - self.span3.end() - - def test_exporting(self): - # pylint: disable=protected-access - self.assertEqual(self.exporter._exporting, "traces") - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables(self, mock_exporter_mixin): - OTLPSpanExporter() - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNone(kwargs["credentials"]) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR - + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: THIS_DIR - + "/fixtures/test-client-cert.pem", - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: THIS_DIR - + "/fixtures/test-client-key.pem", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - def test_env_variables_with_client_certificates(self, mock_exporter_mixin): - OTLPSpanExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "collector:4317", - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: THIS_DIR - + "/fixtures/test.cert", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = value=2", - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "10", - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", - }, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__" - ) - @patch("logging.Logger.error") - def test_env_variables_with_only_certificate( - self, mock_logger_error, mock_exporter_mixin - ): - OTLPSpanExporter() - - self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1) - _, kwargs = mock_exporter_mixin.call_args_list[0] - self.assertEqual(kwargs["endpoint"], "collector:4317") - self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = value=2") - self.assertEqual(kwargs["timeout"], 10) - self.assertEqual(kwargs["compression"], Compression.Gzip) - self.assertIsNotNone(kwargs["credentials"]) - self.assertIsInstance(kwargs["credentials"], ChannelCredentials) - - mock_logger_error.assert_not_called() - - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - # pylint: disable=unused-argument - def test_no_credentials_error(self, mock_ssl_channel, mock_secure): - OTLPSpanExporter(insecure=False) - self.assertTrue(mock_ssl_channel.called) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_TRACES_HEADERS: " key1=value1,KEY2 = VALUE=2 "}, - ) - @patch( - "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel") - # pylint: disable=unused-argument - def test_otlp_headers_from_env(self, mock_ssl_channel, mock_secure): - exporter = OTLPSpanExporter() - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, - ( - ("key1", "value1"), - ("key2", "VALUE=2"), - ), - ) - exporter = OTLPSpanExporter( - headers=(("key3", "value3"), ("key4", "value4")) - ) - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, - ( - ("key3", "value3"), - ("key4", "value4"), - ), - ) - exporter = OTLPSpanExporter( - headers={"key5": "value5", "key6": "value6"} - ) - # pylint: disable=protected-access - self.assertEqual( - exporter._headers, - ( - ("key5", "value5"), - ("key6", "value6"), - ), - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_TRACES_INSECURE: "True"}, - ) - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - # pylint: disable=unused-argument - def test_otlp_insecure_from_env(self, mock_insecure): - OTLPSpanExporter() - # pylint: disable=protected-access - self.assertTrue(mock_insecure.called) - self.assertEqual( - 1, - mock_insecure.call_count, - f"expected {mock_insecure} to be called", - ) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch.dict("os.environ", {OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}) - def test_otlp_exporter_otlp_compression_kwarg(self, mock_insecure_channel): - """Specifying kwarg should take precedence over env""" - OTLPSpanExporter(insecure=True, compression=Compression.NoCompression) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.NoCompression, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ), - ) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, - ) - def test_otlp_exporter_otlp_compression_precendence( - self, mock_insecure_channel - ): - """OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as higher priority than - OTEL_EXPORTER_OTLP_COMPRESSION - """ - OTLPSpanExporter(insecure=True) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.Gzip, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ), - ) - - # pylint: disable=no-self-use - @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel") - def test_otlp_exporter_otlp_channel_options_kwarg( - self, mock_insecure_channel - ): - OTLPSpanExporter(insecure=True, channel_options=(("some", "options"),)) - mock_insecure_channel.assert_called_once_with( - "localhost:4317", - compression=Compression.NoCompression, - options=( - ( - "grpc.primary_user_agent", - "OTel-OTLP-Exporter-Python/" + __version__, - ), - ("some", "options"), - ), - ) - - def test_translate_spans(self): - expected = ExportTraceServiceRequest( - resource_spans=[ - ResourceSpans( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_spans=[ - ScopeSpans( - scope=PB2InstrumentationScope( - name="name", version="version" - ), - spans=[ - OTLPSpan( - # pylint: disable=no-member - name="a", - start_time_unix_nano=self.span.start_time, - end_time_unix_nano=self.span.end_time, - trace_state="a=b,c=d", - span_id=int.to_bytes( - 10217189687419569865, 8, "big" - ), - trace_id=int.to_bytes( - 67545097771067222548457157018666467027, - 16, - "big", - ), - parent_span_id=( - b"\000\000\000\000\000\00009" - ), - kind=( - OTLPSpan.SpanKind.SPAN_KIND_INTERNAL - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(bool_value=True), - ), - ], - events=[ - OTLPSpan.Event( - name="a", - time_unix_nano=1591240820506462784, - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=False - ), - ), - ], - ) - ], - status=Status(code=0, message=""), - links=[ - OTLPSpan.Link( - trace_id=int.to_bytes( - 1, 16, "big" - ), - span_id=int.to_bytes(2, 8, "big"), - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=False - ), - ), - ], - flags=0x300, - ) - ], - flags=0x300, - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual(expected, self.exporter._translate_data([self.span])) - - def test_count_spans(self): - # pylint: disable=protected-access - self.assertEqual(1, self.exporter._count_data([self.span])) - - def test_translate_spans_multi(self): - expected = ExportTraceServiceRequest( - resource_spans=[ - ResourceSpans( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=1)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_spans=[ - ScopeSpans( - scope=PB2InstrumentationScope( - name="name", version="version" - ), - spans=[ - OTLPSpan( - # pylint: disable=no-member - name="a", - start_time_unix_nano=self.span.start_time, - end_time_unix_nano=self.span.end_time, - trace_state="a=b,c=d", - span_id=int.to_bytes( - 10217189687419569865, 8, "big" - ), - trace_id=int.to_bytes( - 67545097771067222548457157018666467027, - 16, - "big", - ), - parent_span_id=( - b"\000\000\000\000\000\00009" - ), - kind=( - OTLPSpan.SpanKind.SPAN_KIND_INTERNAL - ), - attributes=[ - KeyValue( - key="a", - value=AnyValue(int_value=1), - ), - KeyValue( - key="b", - value=AnyValue(bool_value=True), - ), - ], - events=[ - OTLPSpan.Event( - name="a", - time_unix_nano=1591240820506462784, - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=False - ), - ), - ], - ) - ], - status=Status(code=0, message=""), - links=[ - OTLPSpan.Link( - trace_id=int.to_bytes( - 1, 16, "big" - ), - span_id=int.to_bytes(2, 8, "big"), - attributes=[ - KeyValue( - key="a", - value=AnyValue( - int_value=1 - ), - ), - KeyValue( - key="b", - value=AnyValue( - bool_value=False - ), - ), - ], - flags=0x300, - ) - ], - flags=0x300, - ) - ], - ), - ScopeSpans( - scope=PB2InstrumentationScope( - name="name2", version="version2" - ), - spans=[ - OTLPSpan( - # pylint: disable=no-member - name="c", - start_time_unix_nano=self.span3.start_time, - end_time_unix_nano=self.span3.end_time, - trace_state="a=b,c=d", - span_id=int.to_bytes( - 10217189687419569865, 8, "big" - ), - trace_id=int.to_bytes( - 67545097771067222548457157018666467027, - 16, - "big", - ), - parent_span_id=( - b"\000\000\000\000\000\00009" - ), - kind=( - OTLPSpan.SpanKind.SPAN_KIND_INTERNAL - ), - status=Status(code=0, message=""), - flags=0x300, - ) - ], - ), - ], - ), - ResourceSpans( - resource=OTLPResource( - attributes=[ - KeyValue(key="a", value=AnyValue(int_value=2)), - KeyValue( - key="b", value=AnyValue(bool_value=False) - ), - ] - ), - scope_spans=[ - ScopeSpans( - scope=PB2InstrumentationScope( - name="name", version="version" - ), - spans=[ - OTLPSpan( - # pylint: disable=no-member - name="b", - start_time_unix_nano=self.span2.start_time, - end_time_unix_nano=self.span2.end_time, - trace_state="a=b,c=d", - span_id=int.to_bytes( - 10217189687419569865, 8, "big" - ), - trace_id=int.to_bytes( - 67545097771067222548457157018666467027, - 16, - "big", - ), - parent_span_id=( - b"\000\000\000\000\000\00009" - ), - kind=( - OTLPSpan.SpanKind.SPAN_KIND_INTERNAL - ), - status=Status(code=0, message=""), - flags=0x300, - ) - ], - ) - ], - ), - ] - ) - - # pylint: disable=protected-access - self.assertEqual( - expected, - self.exporter._translate_data([self.span, self.span2, self.span3]), - ) - - def test_count_spans_multi(self): - self.assertEqual( - # pylint: disable=protected-access - 3, - self.exporter._count_data([self.span, self.span2, self.span3]), - ) - - def _check_translated_status( - self, - translated: ExportTraceServiceRequest, - code_expected: Status, - ): - status = translated.resource_spans[0].scope_spans[0].spans[0].status - - self.assertEqual( - status.code, - code_expected, - ) - - def test_span_status_translate(self): - # pylint: disable=protected-access,no-member - unset = SDKStatus(status_code=SDKStatusCode.UNSET) - ok = SDKStatus(status_code=SDKStatusCode.OK) - error = SDKStatus(status_code=SDKStatusCode.ERROR) - unset_translated = self.exporter._translate_data( - [_create_span_with_status(unset)] - ) - ok_translated = self.exporter._translate_data( - [_create_span_with_status(ok)] - ) - error_translated = self.exporter._translate_data( - [_create_span_with_status(error)] - ) - self._check_translated_status( - unset_translated, - Status.STATUS_CODE_UNSET, - ) - self._check_translated_status( - ok_translated, - Status.STATUS_CODE_OK, - ) - self._check_translated_status( - error_translated, - Status.STATUS_CODE_ERROR, - ) - - # pylint:disable=no-member - def test_translate_key_values(self): - bool_value = _encode_key_value("bool_type", False) - self.assertTrue(isinstance(bool_value, KeyValue)) - self.assertEqual(bool_value.key, "bool_type") - self.assertTrue(isinstance(bool_value.value, AnyValue)) - self.assertFalse(bool_value.value.bool_value) - - str_value = _encode_key_value("str_type", "str") - self.assertTrue(isinstance(str_value, KeyValue)) - self.assertEqual(str_value.key, "str_type") - self.assertTrue(isinstance(str_value.value, AnyValue)) - self.assertEqual(str_value.value.string_value, "str") - - int_value = _encode_key_value("int_type", 2) - self.assertTrue(isinstance(int_value, KeyValue)) - self.assertEqual(int_value.key, "int_type") - self.assertTrue(isinstance(int_value.value, AnyValue)) - self.assertEqual(int_value.value.int_value, 2) - - double_value = _encode_key_value("double_type", 3.2) - self.assertTrue(isinstance(double_value, KeyValue)) - self.assertEqual(double_value.key, "double_type") - self.assertTrue(isinstance(double_value.value, AnyValue)) - self.assertEqual(double_value.value.double_value, 3.2) - - seq_value = _encode_key_value("seq_type", ["asd", "123"]) - self.assertTrue(isinstance(seq_value, KeyValue)) - self.assertEqual(seq_value.key, "seq_type") - self.assertTrue(isinstance(seq_value.value, AnyValue)) - self.assertTrue(isinstance(seq_value.value.array_value, ArrayValue)) - - arr_value = seq_value.value.array_value - self.assertTrue(isinstance(arr_value.values[0], AnyValue)) - self.assertEqual(arr_value.values[0].string_value, "asd") - self.assertTrue(isinstance(arr_value.values[1], AnyValue)) - self.assertEqual(arr_value.values[1].string_value, "123") - - def test_dropped_values(self): - span = get_span_with_dropped_attributes_events_links() - # pylint:disable=protected-access - translated = self.exporter._translate_data([span]) - self.assertEqual( - 1, - translated.resource_spans[0] - .scope_spans[0] - .spans[0] - .dropped_links_count, - ) - self.assertEqual( - 2, - translated.resource_spans[0] - .scope_spans[0] - .spans[0] - .dropped_attributes_count, - ) - self.assertEqual( - 3, - translated.resource_spans[0] - .scope_spans[0] - .spans[0] - .dropped_events_count, - ) - self.assertEqual( - 2, - translated.resource_spans[0] - .scope_spans[0] - .spans[0] - .links[0] - .dropped_attributes_count, - ) - self.assertEqual( - 2, - translated.resource_spans[0] - .scope_spans[0] - .spans[0] - .events[0] - .dropped_attributes_count, - ) - - -def _create_span_with_status(status: SDKStatus): - span = _Span( - "a", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), - parent=Mock(**{"span_id": 12345}), - instrumentation_scope=InstrumentationScope( - name="name", version="version" - ), - ) - span.set_status(status) - return span diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/py.typed b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/py.typed rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/py.typed b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/py.typed rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-cert.pem b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-cert.pem rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-key.pem b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test-client-key.pem rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test.cert b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-grpc/tests/fixtures/test.cert rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/py.typed b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/py.typed rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/__init__.py diff --git a/opentelemetry-proto/src/opentelemetry/proto/py.typed b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py similarity index 100% rename from opentelemetry-proto/src/opentelemetry/proto/py.typed rename to exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/test___init__.py new file mode 100644 index 00000000000..5844a2401f1 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/test___init__.py @@ -0,0 +1,136 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import patch + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import api as grpc +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import Compression, StatusCode + +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.sdk._logs.export import LogRecordExportResult + +_INSECURE_CH = "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" + + +class _FakeRpcError(grpc.RpcError): + def __init__(self, code, details=""): + self._code = code + self._details = details + + def code(self): + return self._code + + def details(self): + return self._details + + +def _make_exporter(**kwargs): + with patch(_INSECURE_CH): + exporter = OTLPLogExporter(insecure=True, **kwargs) + return exporter + + +class TestOTLPLogExporterConstructor(unittest.TestCase): + + def test_defaults(self): + with patch(_INSECURE_CH) as mock_ch: + exporter = OTLPLogExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "localhost:4317") + self.assertEqual(exporter._timeout, 10.0) + self.assertFalse(exporter._shutdown) + exporter.shutdown() + + def test_logs_endpoint_env_var(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs-host:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPLogExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "logs-host:4317") + + def test_logs_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4317", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPLogExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "logs:4317") + + def test_logs_timeout_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": "12"}): + exporter = _make_exporter() + self.assertEqual(exporter._timeout, 12.0) + exporter.shutdown() + + def test_logs_insecure_env_var_true(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_LOGS_INSECURE": "true"}): + with patch(_INSECURE_CH) as mock_insecure: + OTLPLogExporter() + mock_insecure.assert_called_once() + + def test_logs_headers_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_LOGS_HEADERS": "x-log=yes"}): + exporter = _make_exporter() + self.assertIn(("x-log", "yes"), exporter._headers) + exporter.shutdown() + + def test_logs_compression_env_var_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_LOGS_COMPRESSION": "gzip"}): + with patch(_INSECURE_CH) as mock_ch: + OTLPLogExporter(insecure=True) + _, kwargs = mock_ch.call_args + self.assertEqual(kwargs.get("compression"), Compression.Gzip) + + def test_arg_endpoint_overrides_env(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://env:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPLogExporter(insecure=True, endpoint="http://arg:4317") + args, _ = mock_ch.call_args + self.assertEqual(args[0], "arg:4317") + + def test_exporting_property(self): + exporter = _make_exporter() + self.assertEqual(exporter._exporting, "logs") + exporter.shutdown() + + +class TestOTLPLogExporterExport(unittest.TestCase): + + def test_export_success(self): + exporter = _make_exporter() + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.SUCCESS) + exporter.shutdown() + + def test_export_failure_non_retryable(self): + exporter = _make_exporter() + exporter._client.Export.side_effect = _FakeRpcError(StatusCode.PERMISSION_DENIED) + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + def test_export_after_shutdown(self): + exporter = _make_exporter() + exporter.shutdown() + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + + def test_force_flush_returns_true(self): + exporter = _make_exporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + def test_shutdown_sets_flag(self): + exporter = _make_exporter() + self.assertFalse(exporter._shutdown) + exporter.shutdown() + self.assertTrue(exporter._shutdown) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/test___init__.py new file mode 100644 index 00000000000..a7bd27acfbb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/test___init__.py @@ -0,0 +1,224 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import Mock, patch + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import api as grpc +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import Compression, StatusCode + +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + MetricExportResult, + MetricsData, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.metrics.export import Gauge as SDKGauge +from opentelemetry.sdk.metrics.export import Metric +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +_INSECURE_CH = "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" + + +class _FakeRpcError(grpc.RpcError): + def __init__(self, code, details=""): + self._code = code + self._details = details + + def code(self): + return self._code + + def details(self): + return self._details + + +def _make_exporter(**kwargs): + with patch(_INSECURE_CH): + exporter = OTLPMetricExporter(insecure=True, **kwargs) + return exporter + + +def _make_metrics_data(n_data_points: int = 1) -> MetricsData: + data_points = [ + NumberDataPoint( + attributes={"i": i}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=float(i), + exemplars=[], + ) + for i in range(n_data_points) + ] + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource({"service.name": "test"}), + scope_metrics=[ + ScopeMetrics( + scope=InstrumentationScope("test", "1.0"), + metrics=[ + Metric( + name="my.gauge", + description="", + unit="1", + data=SDKGauge(data_points=data_points), + ) + ], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + + +class TestOTLPMetricExporterConstructor(unittest.TestCase): + + def test_defaults(self): + with patch(_INSECURE_CH) as mock_ch: + exporter = OTLPMetricExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "localhost:4317") + self.assertEqual(exporter._timeout, 10.0) + self.assertIsNone(exporter._max_export_batch_size) + exporter.shutdown() + + def test_metrics_endpoint_env_var(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics-host:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPMetricExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "metrics-host:4317") + + def test_metrics_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4317", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPMetricExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "metrics:4317") + + def test_metrics_timeout_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "15"}): + exporter = _make_exporter() + self.assertEqual(exporter._timeout, 15.0) + exporter.shutdown() + + def test_metrics_insecure_env_var_true(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_METRICS_INSECURE": "true"}): + with patch(_INSECURE_CH) as mock_insecure: + OTLPMetricExporter() + mock_insecure.assert_called_once() + + def test_metrics_headers_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_METRICS_HEADERS": "x-metric=1"}): + exporter = _make_exporter() + self.assertIn(("x-metric", "1"), exporter._headers) + exporter.shutdown() + + def test_max_export_batch_size_stored(self): + exporter = _make_exporter(max_export_batch_size=100) + self.assertEqual(exporter._max_export_batch_size, 100) + exporter.shutdown() + + def test_exporting_property(self): + exporter = _make_exporter() + self.assertEqual(exporter._exporting, "metrics") + exporter.shutdown() + + +class TestOTLPMetricExporterExport(unittest.TestCase): + + def test_export_success(self): + exporter = _make_exporter() + result = exporter.export(_make_metrics_data()) + self.assertEqual(result, MetricExportResult.SUCCESS) + exporter.shutdown() + + def test_export_empty_metrics(self): + exporter = _make_exporter() + result = exporter.export(MetricsData(resource_metrics=[])) + self.assertEqual(result, MetricExportResult.SUCCESS) + exporter.shutdown() + + def test_export_failure_non_retryable(self): + exporter = _make_exporter() + exporter._client.Export.side_effect = _FakeRpcError(StatusCode.PERMISSION_DENIED) + result = exporter.export(_make_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() + + def test_export_after_shutdown(self): + exporter = _make_exporter() + exporter.shutdown() + result = exporter.export(_make_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + + def test_force_flush_returns_true(self): + exporter = _make_exporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + def test_set_meter_provider(self): + from opentelemetry.sdk.metrics import MeterProvider + exporter = _make_exporter() + mp = MeterProvider() + exporter.set_meter_provider(mp) # should not raise + exporter.shutdown() + + +class TestOTLPMetricExporterSplitBatch(unittest.TestCase): + """Tests for batch splitting via max_export_batch_size.""" + + def test_no_split_when_no_max_batch_size(self): + exporter = _make_exporter() + metrics_data = _make_metrics_data(n_data_points=5) + result = exporter.export(metrics_data) + self.assertEqual(result, MetricExportResult.SUCCESS) + # Single Export call (no splitting) + self.assertEqual(exporter._client.Export.call_count, 1) + exporter.shutdown() + + def test_split_into_multiple_batches(self): + # 5 data points, batch size 2 → 3 Export calls + exporter = _make_exporter(max_export_batch_size=2) + metrics_data = _make_metrics_data(n_data_points=5) + result = exporter.export(metrics_data) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertEqual(exporter._client.Export.call_count, 3) + exporter.shutdown() + + def test_split_exact_batch_size(self): + # 4 data points, batch size 4 → 1 Export call + exporter = _make_exporter(max_export_batch_size=4) + metrics_data = _make_metrics_data(n_data_points=4) + result = exporter.export(metrics_data) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertEqual(exporter._client.Export.call_count, 1) + exporter.shutdown() + + def test_split_partial_failure_returns_failure(self): + exporter = _make_exporter(max_export_batch_size=2) + metrics_data = _make_metrics_data(n_data_points=4) + call_count = [0] + + def fail_on_second(*args, **kwargs): + call_count[0] += 1 + if call_count[0] == 2: + raise _FakeRpcError(StatusCode.PERMISSION_DENIED) + + exporter._client.Export.side_effect = fail_on_second + result = exporter.export(metrics_data) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/test_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/test_exporter.py new file mode 100644 index 00000000000..bc06e95c511 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/test_exporter.py @@ -0,0 +1,285 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from collections.abc import Sequence +from unittest.mock import Mock, patch, call + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import api as grpc +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import Compression, StatusCode + +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + InvalidCompressionValueException, + OTLPExporterMixin, + _RETRYABLE_ERROR_CODES, + environ_to_compression, +) +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest +from opentelemetry._proto.collector.trace.v1.trace_service_pb2_grpc import TraceServiceStub +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + +_INSECURE_CH = "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" +_SECURE_CH = "opentelemetry.exporter.otlp.proto.grpc.exporter.secure_channel" +_SSL_CREDS = "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" +_UNIFORM = "opentelemetry.exporter.otlp.proto.grpc.exporter.random.uniform" + + +class _FakeRpcError(grpc.RpcError): + def __init__(self, code, details=""): + self._code = code + self._details = details + + def code(self): + return self._code + + def details(self): + return self._details + + +def _make_exporter(**kwargs): + """Create OTLPSpanExporter with a mock insecure channel, returning (exporter, mock_channel).""" + with patch(_INSECURE_CH) as mock_ch: + mock_channel = Mock() + mock_ch.return_value = mock_channel + exporter = OTLPSpanExporter(insecure=True, **kwargs) + return exporter, mock_channel + + +class TestOTLPExporterMixinChannel(unittest.TestCase): + + def test_insecure_true_uses_insecure_channel(self): + with patch(_INSECURE_CH) as mock_insecure: + exporter = OTLPSpanExporter(insecure=True) + mock_insecure.assert_called_once() + exporter.shutdown() + + def test_http_scheme_implies_insecure(self): + with patch(_INSECURE_CH) as mock_insecure: + exporter = OTLPSpanExporter(endpoint="http://collector:4317") + mock_insecure.assert_called_once() + exporter.shutdown() + + def test_insecure_false_uses_secure_channel(self): + with patch(_SSL_CREDS, return_value=Mock(spec=grpc.ChannelCredentials)): + with patch(_SECURE_CH) as mock_secure: + exporter = OTLPSpanExporter(insecure=False) + mock_secure.assert_called_once() + exporter.shutdown() + + def test_https_scheme_implies_secure(self): + with patch(_SSL_CREDS, return_value=Mock(spec=grpc.ChannelCredentials)): + with patch(_SECURE_CH) as mock_secure: + exporter = OTLPSpanExporter(endpoint="https://collector:4317") + mock_secure.assert_called_once() + exporter.shutdown() + + def test_default_endpoint_is_localhost_4317(self): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter(insecure=True) + args, _ = mock_insecure.call_args + self.assertEqual(args[0], "localhost:4317") + + def test_endpoint_netloc_extracted(self): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter(insecure=True, endpoint="http://myhost:4317") + args, _ = mock_insecure.call_args + self.assertEqual(args[0], "myhost:4317") + + def test_generic_endpoint_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://env-host:4317"}): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter(insecure=True) + args, _ = mock_insecure.call_args + self.assertEqual(args[0], "env-host:4317") + + def test_insecure_env_var_true(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_INSECURE": "true"}): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter() + mock_insecure.assert_called_once() + + def test_compression_gzip_passed_to_channel(self): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter(insecure=True, compression=Compression.Gzip) + _, kwargs = mock_insecure.call_args + self.assertEqual(kwargs.get("compression"), Compression.Gzip) + + +class TestOTLPExporterMixinHeaders(unittest.TestCase): + + def test_headers_dict_converted_to_tuple(self): + exporter, _ = _make_exporter(headers={"x-my-header": "val"}) + self.assertIn(("x-my-header", "val"), exporter._headers) + exporter.shutdown() + + def test_headers_str_parsed(self): + exporter, _ = _make_exporter(headers="x-token=abc,x-env=xyz") + self.assertIn(("x-token", "abc"), exporter._headers) + self.assertIn(("x-env", "xyz"), exporter._headers) + exporter.shutdown() + + def test_headers_env_var_parsed(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_HEADERS": "x-hdr=foo"}): + exporter, _ = _make_exporter() + self.assertIn(("x-hdr", "foo"), exporter._headers) + exporter.shutdown() + + def test_no_headers_gives_empty_tuple(self): + exporter, _ = _make_exporter() + self.assertEqual(exporter._headers, tuple()) + exporter.shutdown() + + +class TestOTLPExporterMixinTimeout(unittest.TestCase): + + def test_default_timeout(self): + exporter, _ = _make_exporter() + self.assertEqual(exporter._timeout, 10.0) + exporter.shutdown() + + def test_timeout_arg(self): + exporter, _ = _make_exporter(timeout=42) + self.assertEqual(exporter._timeout, 42.0) + exporter.shutdown() + + def test_timeout_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TIMEOUT": "25"}): + exporter, _ = _make_exporter() + self.assertEqual(exporter._timeout, 25.0) + exporter.shutdown() + + +class TestOTLPExporterMixinRetryableCodes(unittest.TestCase): + + def test_default_retryable_codes(self): + exporter, _ = _make_exporter() + self.assertEqual(exporter._retryable_error_codes, _RETRYABLE_ERROR_CODES) + exporter.shutdown() + + def test_custom_codes_from_arg(self): + exporter, _ = _make_exporter(retryable_error_codes=[StatusCode.NOT_FOUND]) + self.assertIn(StatusCode.NOT_FOUND, exporter._retryable_error_codes) + self.assertNotIn(StatusCode.UNAVAILABLE, exporter._retryable_error_codes) + exporter.shutdown() + + def test_custom_codes_from_env_var(self): + env = {"OTEL_PYTHON_EXPORTER_OTLP_GRPC_RETRYABLE_ERROR_CODES": "NOT_FOUND,UNKNOWN"} + with patch.dict("os.environ", env): + exporter, _ = _make_exporter() + self.assertIn(StatusCode.NOT_FOUND, exporter._retryable_error_codes) + self.assertIn(StatusCode.UNKNOWN, exporter._retryable_error_codes) + exporter.shutdown() + + +class TestOTLPExporterMixinExport(unittest.TestCase): + + def _exporter_with_mock_client(self, **kwargs): + exporter, mock_channel = _make_exporter(**kwargs) + # TraceServiceStub.__init__ sets self.Export = channel.unary_unary(...) + # mock_channel.unary_unary(...) returns a Mock, so _client.Export is that Mock + return exporter, exporter._client.Export + + def test_export_success(self): + exporter, mock_export = self._exporter_with_mock_client() + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_export.assert_called_once() + exporter.shutdown() + + def test_export_after_shutdown_returns_failure(self): + exporter, _ = self._exporter_with_mock_client() + exporter.shutdown() + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + + def test_export_non_retryable_error_fails_immediately(self): + exporter, mock_export = self._exporter_with_mock_client() + mock_export.side_effect = _FakeRpcError(StatusCode.NOT_FOUND, "not found") + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + # Should only attempt once (non-retryable) + self.assertEqual(mock_export.call_count, 1) + exporter.shutdown() + + def test_export_retryable_then_deadline_exceeded(self): + # Large backoff immediately exceeds short timeout → exits after first attempt + exporter, mock_export = self._exporter_with_mock_client(timeout=0.01) + mock_export.side_effect = _FakeRpcError(StatusCode.UNAVAILABLE) + with patch(_UNIFORM, return_value=100.0): + with patch(_INSECURE_CH): # reinit channel on UNAVAILABLE + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_export_max_retries_exhausted(self): + # Use RESOURCE_EXHAUSTED — retryable but does not trigger channel reinit + exporter, mock_export = self._exporter_with_mock_client(timeout=999) + mock_export.side_effect = _FakeRpcError(StatusCode.RESOURCE_EXHAUSTED) + with patch(_UNIFORM, return_value=0.00001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=False): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_shutdown_interrupts_retry(self): + # Use RESOURCE_EXHAUSTED — retryable but does not trigger channel reinit + exporter, mock_export = self._exporter_with_mock_client(timeout=999) + mock_export.side_effect = _FakeRpcError(StatusCode.RESOURCE_EXHAUSTED) + with patch(_UNIFORM, return_value=0.00001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=True): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_unavailable_triggers_channel_reinit(self): + exporter, mock_export = self._exporter_with_mock_client(timeout=0.001) + mock_export.side_effect = _FakeRpcError(StatusCode.UNAVAILABLE) + with patch(_UNIFORM, return_value=100.0): + with patch(_INSECURE_CH) as mock_new_channel: + exporter.export([]) + # channel was reinitialized once on first UNAVAILABLE + mock_new_channel.assert_called_once() + exporter.shutdown() + + +class TestOTLPExporterMixinShutdown(unittest.TestCase): + + def test_shutdown_sets_flag_and_closes_channel(self): + exporter, mock_channel = _make_exporter() + self.assertFalse(exporter._shutdown) + exporter.shutdown() + self.assertTrue(exporter._shutdown) + mock_channel.close.assert_called_once() + + def test_shutdown_twice_logs_warning(self): + exporter, _ = _make_exporter() + exporter.shutdown() + with self.assertLogs(level="WARNING") as cm: + exporter.shutdown() + self.assertTrue(any("already shutdown" in msg for msg in cm.output)) + + def test_force_flush_returns_true(self): + exporter, _ = _make_exporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + +class TestEnvironToCompression(unittest.TestCase): + + def test_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): + result = environ_to_compression("OTEL_EXPORTER_OTLP_COMPRESSION") + self.assertEqual(result, Compression.Gzip) + + def test_missing_env_returns_none(self): + result = environ_to_compression("OTEL_EXPORTER_OTLP_COMPRESSION") + self.assertIsNone(result) + + def test_invalid_value_raises(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "zstd"}): + with self.assertRaises(InvalidCompressionValueException): + environ_to_compression("OTEL_EXPORTER_OTLP_COMPRESSION") diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/test___init__.py new file mode 100644 index 00000000000..7c7e1cf5a42 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/opentelemetry/exporter/otlp/proto/grpc/trace_exporter/test___init__.py @@ -0,0 +1,140 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import Mock, patch + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import api as grpc +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.api import Compression, StatusCode + +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace.export import SpanExportResult + +_INSECURE_CH = "opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel" +_SSL_CREDS = "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials" + + +class _FakeRpcError(grpc.RpcError): + def __init__(self, code, details=""): + self._code = code + self._details = details + + def code(self): + return self._code + + def details(self): + return self._details + + +def _make_exporter(**kwargs): + with patch(_INSECURE_CH): + exporter = OTLPSpanExporter(insecure=True, **kwargs) + return exporter + + +class TestOTLPSpanExporterConstructor(unittest.TestCase): + + def test_defaults(self): + with patch(_INSECURE_CH) as mock_ch: + exporter = OTLPSpanExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "localhost:4317") + self.assertEqual(exporter._timeout, 10.0) + self.assertFalse(exporter._shutdown) + exporter.shutdown() + + def test_traces_endpoint_env_var(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces-host:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPSpanExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "traces-host:4317") + + def test_traces_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4317", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPSpanExporter(insecure=True) + args, _ = mock_ch.call_args + self.assertEqual(args[0], "traces:4317") + + def test_traces_timeout_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "7"}): + exporter = _make_exporter() + self.assertEqual(exporter._timeout, 7.0) + exporter.shutdown() + + def test_traces_timeout_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TIMEOUT": "20", + "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "3", + }): + exporter = _make_exporter() + self.assertEqual(exporter._timeout, 3.0) + exporter.shutdown() + + def test_traces_insecure_env_var_true(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TRACES_INSECURE": "true"}): + with patch(_INSECURE_CH) as mock_insecure: + OTLPSpanExporter() + mock_insecure.assert_called_once() + + def test_traces_compression_env_var_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip"}): + with patch(_INSECURE_CH) as mock_ch: + OTLPSpanExporter(insecure=True) + _, kwargs = mock_ch.call_args + self.assertEqual(kwargs.get("compression"), Compression.Gzip) + + def test_traces_headers_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "x-trace=yes"}): + exporter = _make_exporter() + self.assertIn(("x-trace", "yes"), exporter._headers) + exporter.shutdown() + + def test_arg_endpoint_overrides_env(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env:4317", + }): + with patch(_INSECURE_CH) as mock_ch: + OTLPSpanExporter(insecure=True, endpoint="http://arg:4317") + args, _ = mock_ch.call_args + self.assertEqual(args[0], "arg:4317") + + +class TestOTLPSpanExporterExport(unittest.TestCase): + + def test_export_success(self): + exporter = _make_exporter() + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.SUCCESS) + exporter.shutdown() + + def test_export_failure_non_retryable(self): + exporter = _make_exporter() + exporter._client.Export.side_effect = _FakeRpcError(StatusCode.PERMISSION_DENIED) + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_export_after_shutdown(self): + exporter = _make_exporter() + exporter.shutdown() + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + + def test_force_flush_returns_true(self): + exporter = _make_exporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + def test_exporting_property(self): + exporter = _make_exporter() + self.assertEqual(exporter._exporting, "traces") + exporter.shutdown() diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_client.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_client.py new file mode 100644 index 00000000000..438c911d94d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_client.py @@ -0,0 +1,226 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for the gRPC message framing and gzip handling in client.py.""" + +import socket +import zlib + +import pytest + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.client import ( + MAX_DECOMPRESSED_MESSAGE, + Channel, + RpcError, + StatusCode, + _frame_message, + _gunzip, + _unframe_messages, +) +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.connection import ( + ConnectionTerminated, + TransportError, +) + + +def _gzip(data): + compressor = zlib.compressobj(9, zlib.DEFLATED, 16 + zlib.MAX_WBITS) + return compressor.compress(data) + compressor.flush() + + +def test_frame_unframe_roundtrip_uncompressed(): + body = _frame_message(b"hello", compress=False) + assert _unframe_messages(body, b"identity") == [b"hello"] + + +def test_frame_unframe_roundtrip_gzip(): + body = _frame_message(b"hello world" * 50, compress=True) + assert _unframe_messages(body, b"gzip") == [b"hello world" * 50] + + +def test_gunzip_roundtrip(): + payload = b"the quick brown fox" * 100 + assert _gunzip(_gzip(payload), 1 << 20) == payload + + +def test_gunzip_rejects_bomb(): + # ~8 MiB of zeros compresses to a few KiB; decompressing under a 4 MiB cap + # must raise rather than allocate the full expansion. + bomb = _gzip(b"\x00" * (8 << 20)) + assert len(bomb) < (1 << 20) + with pytest.raises(RpcError) as excinfo: + _gunzip(bomb, MAX_DECOMPRESSED_MESSAGE) + assert excinfo.value.code() == StatusCode.RESOURCE_EXHAUSTED + + +def test_unframe_rejects_compressed_flag_under_identity_encoding(): + # compressed_flag=1 but the stream advertised identity encoding. + framed = _frame_message(b"payload", compress=True) + with pytest.raises(RpcError) as excinfo: + _unframe_messages(framed, b"identity") + assert excinfo.value.code() == StatusCode.INTERNAL + + +def test_unframe_truncated_prefix(): + with pytest.raises(RpcError): + _unframe_messages(b"\x00\x00\x00", b"identity") + + +def _channel_with_fake_once(monkeypatch, once): + channel = Channel("host:1", use_tls=False) + monkeypatch.setattr(channel, "_unary_call_once", once) + monkeypatch.setattr(channel, "close", lambda: None) + return channel + + +def test_retry_shares_one_deadline_across_attempts(monkeypatch): + seen_deadlines = [] + + def once(path, request_bytes, metadata, deadline, compression): + seen_deadlines.append(deadline) + if len(seen_deadlines) == 1: + raise TransportError("first attempt fails") + return b"ok" + + channel = _channel_with_fake_once(monkeypatch, once) + assert channel.unary_call("/S/M", b"req") == b"ok" + # Both attempts ran, and both received the same Deadline object — the + # timeout budget is not restarted on reconnect. + assert len(seen_deadlines) == 2 + assert seen_deadlines[0] is seen_deadlines[1] + + +def test_socket_timeout_maps_to_deadline_exceeded_without_retry(monkeypatch): + attempts = [] + + def once(path, request_bytes, metadata, deadline, compression): + attempts.append(1) + raise socket.timeout("deadline exceeded") + + channel = _channel_with_fake_once(monkeypatch, once) + with pytest.raises(RpcError) as excinfo: + channel.unary_call("/S/M", b"req") + assert excinfo.value.code() == StatusCode.DEADLINE_EXCEEDED + assert len(attempts) == 1 # a timeout is terminal, not retried + + +def test_two_transport_failures_map_to_unavailable(monkeypatch): + attempts = [] + + def once(path, request_bytes, metadata, deadline, compression): + attempts.append(1) + raise TransportError("boom") + + channel = _channel_with_fake_once(monkeypatch, once) + with pytest.raises(RpcError) as excinfo: + channel.unary_call("/S/M", b"req") + assert excinfo.value.code() == StatusCode.UNAVAILABLE + assert len(attempts) == 2 + + +def test_goaway_unprocessed_stream_is_retried(monkeypatch): + attempts = [] + + def once(path, request_bytes, metadata, deadline, compression): + attempts.append(1) + if len(attempts) == 1: + error = ConnectionTerminated(0, 0, b"") # last_stream_id below ours + error.stream_processed = False + raise error + return b"ok" + + channel = _channel_with_fake_once(monkeypatch, once) + assert channel.unary_call("/S/M", b"req") == b"ok" + assert len(attempts) == 2 + + +def test_goaway_processed_stream_is_not_retried(monkeypatch): + attempts = [] + + def once(path, request_bytes, metadata, deadline, compression): + attempts.append(1) + error = ConnectionTerminated(99, 0, b"") # our stream was processed + error.stream_processed = True + raise error + + channel = _channel_with_fake_once(monkeypatch, once) + with pytest.raises(RpcError) as excinfo: + channel.unary_call("/S/M", b"req") + assert excinfo.value.code() == StatusCode.UNAVAILABLE + assert len(attempts) == 1 # no retry: the export may already be applied + + +class _FakeConnection: + def __init__(self, header_sets, body=b""): + self._header_sets = header_sets + self._body = body + + def request(self, headers, body, deadline): + return self._header_sets, self._body + + def close(self): + pass + + +def _channel_with_response(monkeypatch, header_sets, body=b""): + channel = Channel("host:1", use_tls=False) + fake = _FakeConnection(header_sets, body) + monkeypatch.setattr(channel, "_connect", lambda deadline: fake) + return channel + + +def test_ok_response_returns_message(monkeypatch): + channel = _channel_with_response( + monkeypatch, + [ + [(b":status", b"200"), (b"content-type", b"application/grpc")], + [(b"grpc-status", b"0")], + ], + body=_frame_message(b"response", compress=False), + ) + assert channel.unary_call("/S/M", b"req") == b"response" + + +@pytest.mark.parametrize( + "http_status,expected", + [ + (b"401", StatusCode.UNAUTHENTICATED), + (b"403", StatusCode.PERMISSION_DENIED), + (b"404", StatusCode.UNIMPLEMENTED), + (b"503", StatusCode.UNAVAILABLE), + (b"418", StatusCode.UNKNOWN), # unmapped -> UNKNOWN, not blanket UNAVAILABLE + ], +) +def test_non_200_http_status_mapping(monkeypatch, http_status, expected): + channel = _channel_with_response(monkeypatch, [[(b":status", http_status)]]) + with pytest.raises(RpcError) as excinfo: + channel.unary_call("/S/M", b"req") + assert excinfo.value.code() == expected + + +def test_unknown_numeric_grpc_status_maps_to_unknown(monkeypatch): + # A numeric status outside the known set must map to UNKNOWN, not crash. + channel = _channel_with_response( + monkeypatch, + [ + [(b":status", b"200"), (b"content-type", b"application/grpc")], + [(b"grpc-status", b"999")], + ], + ) + with pytest.raises(RpcError) as excinfo: + channel.unary_call("/S/M", b"req") + assert excinfo.value.code() == StatusCode.UNKNOWN + + +def test_ipv6_target_strips_brackets_but_keeps_authority(): + channel = Channel("[::1]:4317", use_tls=False) + assert channel._host == "::1" + assert channel._port == 4317 + assert channel._authority == "[::1]:4317" + + +def test_target_requires_numeric_port(): + with pytest.raises(ValueError): + Channel("host:notaport", use_tls=False) + with pytest.raises(ValueError): + Channel("hostwithoutport", use_tls=False) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_connection.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_connection.py new file mode 100644 index 00000000000..fcb853214a5 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_connection.py @@ -0,0 +1,342 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for H2Connection.request() driven by an in-memory socket. + +The fake socket feeds a scripted sequence of server frames and captures what +the client sends, so the full request state machine — flow control, early +responses, trailers, CONTINUATION, RST_STREAM, GOAWAY — is exercised without a +network. Server-side header blocks are built with this package's own HPACK +encoder, which the client's decoder reads back. +""" + +import struct + +import pytest + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import connection as conn_mod +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import frames +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.connection import ( + ConnectionTerminated, + Deadline, + H2Connection, + StreamReset, + TransportError, +) +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.hpack import encode as hpack_encode + + +class FakeSocket: + """A scripted, in-memory stand-in for a connected socket.""" + + def __init__(self, inbound=b""): + self._inbound = bytearray(inbound) + self.sent = bytearray() + self.closed = False + + def settimeout(self, _timeout): + pass + + def setsockopt(self, *_args): + pass + + def recv(self, size): + if not self._inbound: + return b"" # EOF -> TransportError in the client + chunk = bytes(self._inbound[:size]) + del self._inbound[:size] + return chunk + + def sendall(self, data): + self.sent += data + + def feed(self, data): + self._inbound += data + + def close(self): + self.closed = True + + +def headers_frame(stream_id, header_list, end_stream, end_headers=True): + flags = 0 + if end_headers: + flags |= frames.FLAG_END_HEADERS + if end_stream: + flags |= frames.FLAG_END_STREAM + return frames.encode_frame( + frames.Frame(frames.HEADERS, flags, stream_id, hpack_encode(header_list)) + ) + + +def continuation_frame(stream_id, header_block, end_headers): + flags = frames.FLAG_END_HEADERS if end_headers else 0 + return frames.encode_frame( + frames.Frame(frames.CONTINUATION, flags, stream_id, header_block) + ) + + +def data_frame(stream_id, payload, end_stream): + flags = frames.FLAG_END_STREAM if end_stream else 0 + return frames.encode_frame(frames.Frame(frames.DATA, flags, stream_id, payload)) + + +def settings_frame(): + return frames.encode_frame(frames.settings_frame({})) + + +def make_connection(inbound): + sock = FakeSocket(inbound) + conn = H2Connection("example.com", 443, use_tls=False, sock=sock) + return conn, sock + + +OK_RESPONSE_HEADERS = [(b":status", b"200"), (b"content-type", b"application/grpc")] +OK_TRAILERS = [(b"grpc-status", b"0")] + + +def sent_frames(sock, skip_preface=True): + """Decode the frames the client sent, skipping the connection preface.""" + data = bytes(sock.sent) + if skip_preface: + assert data.startswith(frames.CONNECTION_PREFACE) + data = data[len(frames.CONNECTION_PREFACE) :] + out = [] + offset = 0 + while offset + frames.FRAME_HEADER_LEN <= len(data): + length, frame = frames.decode_frame_header( + data[offset : offset + frames.FRAME_HEADER_LEN] + ) + offset += frames.FRAME_HEADER_LEN + frame.payload = data[offset : offset + length] + offset += length + out.append(frame) + return out + + +def test_first_header_block_declares_zero_dynamic_table(): + inbound = settings_frame() + headers_frame( + 1, [(b":status", b"200"), (b"grpc-status", b"0")], end_stream=True + ) + conn, sock = make_connection(inbound) + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + first_headers = next(f for f in sent_frames(sock) if f.type == frames.HEADERS) + # Leading byte 0x20 is the dynamic-table-size-update-to-0 instruction. + assert first_headers.payload[0] == 0x20 + + +def test_large_header_block_split_into_continuation(): + inbound = settings_frame() + headers_frame( + 1, [(b":status", b"200"), (b"grpc-status", b"0")], end_stream=True + ) + conn, sock = make_connection(inbound) + conn._peer_max_frame_size = 20 # force the header block across frames + conn.request( + [(b":method", b"POST"), (b"x-big", b"v" * 100)], b"req", Deadline(5) + ) + sent = sent_frames(sock) + header_frames = [f for f in sent if f.type == frames.HEADERS] + continuations = [f for f in sent if f.type == frames.CONTINUATION] + assert len(header_frames) == 1 + assert len(continuations) >= 1 + # The HEADERS frame must not carry END_HEADERS; the last CONTINUATION must. + assert not header_frames[0].flags & frames.FLAG_END_HEADERS + assert continuations[-1].flags & frames.FLAG_END_HEADERS + assert not continuations[0].flags & frames.FLAG_END_HEADERS or len( + continuations + ) == 1 + + +def test_happy_path_headers_data_trailers(): + inbound = ( + settings_frame() + + headers_frame(1, OK_RESPONSE_HEADERS, end_stream=False) + + data_frame(1, b"\x00\x00\x00\x00\x03abc", end_stream=False) + + headers_frame(1, OK_TRAILERS, end_stream=True) + ) + conn, _sock = make_connection(inbound) + header_sets, body = conn.request( + [(b":method", b"POST"), (b":path", b"/S/M")], b"request-body", Deadline(5) + ) + assert dict(header_sets[0])[b":status"] == b"200" + assert dict(header_sets[-1])[b"grpc-status"] == b"0" + assert body == b"\x00\x00\x00\x00\x03abc" + + +def test_trailers_only_response(): + inbound = settings_frame() + headers_frame( + 1, [(b":status", b"200"), (b"grpc-status", b"12")], end_stream=True + ) + conn, _sock = make_connection(inbound) + header_sets, body = conn.request( + [(b":method", b"POST")], b"req", Deadline(5) + ) + assert len(header_sets) == 1 + assert dict(header_sets[0])[b"grpc-status"] == b"12" + assert body == b"" + + +def test_early_trailers_only_response_while_blocked_on_flow_control(): + # Regression test for the flow-control-blocked-send discard bug: the server + # rejects a large upload (bigger than the 65535-byte connection send + # window) with a trailers-only response before draining the body. The + # client must surface that response, not spin until the deadline. + body = b"x" * 70000 + inbound = settings_frame() + headers_frame( + 1, [(b":status", b"200"), (b"grpc-status", b"8")], end_stream=True + ) + conn, sock = make_connection(inbound) + header_sets, response_body = conn.request( + [(b":method", b"POST")], body, Deadline(5) + ) + assert dict(header_sets[-1])[b"grpc-status"] == b"8" + assert response_body == b"" + # The client stopped uploading once the response arrived: it sent the + # HEADERS plus at most the first window of DATA, never the whole body. + data_bytes_sent = sum( + len(f.payload) for f in sent_frames(sock) if f.type == frames.DATA + ) + assert data_bytes_sent <= 65535 + + +def test_flow_control_resumes_after_window_update(): + # Body exceeds the connection send window; the server opens it with a + # WINDOW_UPDATE, then completes normally. The whole body must be sent. + body = b"y" * 70000 + inbound = ( + settings_frame() + + frames.encode_frame(frames.window_update_frame(0, 1 << 20)) + + frames.encode_frame(frames.window_update_frame(1, 1 << 20)) + + headers_frame(1, OK_RESPONSE_HEADERS, end_stream=False) + + headers_frame(1, OK_TRAILERS, end_stream=True) + ) + conn, sock = make_connection(inbound) + header_sets, _body = conn.request([(b":method", b"POST")], body, Deadline(5)) + assert dict(header_sets[-1])[b"grpc-status"] == b"0" + data_bytes_sent = sum( + len(f.payload) for f in sent_frames(sock) if f.type == frames.DATA + ) + assert data_bytes_sent == 70000 + + +def test_continuation_reassembly_for_trailers(): + # The trailer header block is split across a HEADERS frame (END_STREAM set, + # END_HEADERS unset) and a following CONTINUATION frame that ends it. + block = hpack_encode(OK_TRAILERS) + split = len(block) // 2 + inbound = ( + settings_frame() + + headers_frame(1, OK_RESPONSE_HEADERS, end_stream=False) + + frames.encode_frame( + frames.Frame(frames.HEADERS, frames.FLAG_END_STREAM, 1, block[:split]) + ) + + continuation_frame(1, block[split:], end_headers=True) + ) + conn, _sock = make_connection(inbound) + header_sets, _body = conn.request([(b":method", b"POST")], b"req", Deadline(5)) + assert dict(header_sets[-1])[b"grpc-status"] == b"0" + + +def test_rst_stream_raises_stream_reset(): + inbound = settings_frame() + frames.encode_frame( + frames.rst_stream_frame(1, 2) + ) + conn, _sock = make_connection(inbound) + with pytest.raises(StreamReset): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_goaway_raises_connection_terminated(): + inbound = settings_frame() + frames.encode_frame(frames.goaway_frame(0, 1)) + conn, _sock = make_connection(inbound) + with pytest.raises(ConnectionTerminated): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_ping_is_acked(): + inbound = ( + settings_frame() + + frames.encode_frame(frames.Frame(frames.PING, 0, 0, b"12345678")) + + headers_frame(1, [(b":status", b"200"), (b"grpc-status", b"0")], end_stream=True) + ) + conn, sock = make_connection(inbound) + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + ping_acks = [ + f + for f in sent_frames(sock) + if f.type == frames.PING and f.flags & frames.FLAG_ACK + ] + assert len(ping_acks) == 1 and ping_acks[0].payload == b"12345678" + + +def test_peer_closed_connection_raises_transport_error(): + conn, _sock = make_connection(settings_frame()) # no response, then EOF + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_malformed_hpack_in_response_raises_transport_error(): + # A response HEADERS block that decodes to header index 0 is an HPACK + # error; it must surface as TransportError, not a raw HpackError. + bad_block = b"\x80" # indexed header field, index 0 -> illegal + inbound = settings_frame() + frames.encode_frame( + frames.Frame( + frames.HEADERS, + frames.FLAG_END_HEADERS | frames.FLAG_END_STREAM, + 1, + bad_block, + ) + ) + conn, _sock = make_connection(inbound) + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_truncated_control_frame_raises_transport_error(): + # A WINDOW_UPDATE with a 1-byte payload cannot be unpacked; the raw + # struct.error must be converted to TransportError. + inbound = settings_frame() + frames.encode_frame( + frames.Frame(frames.WINDOW_UPDATE, 0, 0, b"\x01") + ) + conn, _sock = make_connection(inbound) + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_oversized_frame_rejected_before_payload_read(): + # A frame header declaring a length over the cap must be rejected on the + # 9-byte header alone, before any payload is allocated. + big = conn_mod.MAX_RECV_FRAME_SIZE + 1 + oversized_header = struct.pack( + ">BHBBL", (big >> 16) & 0xFF, big & 0xFFFF, frames.DATA, 0, 1 + ) + conn, _sock = make_connection(settings_frame() + oversized_header) + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_response_body_cap(monkeypatch): + monkeypatch.setattr(conn_mod, "MAX_RESPONSE_BODY", 10) + inbound = ( + settings_frame() + + headers_frame(1, OK_RESPONSE_HEADERS, end_stream=False) + + data_frame(1, b"x" * 20, end_stream=False) + ) + conn, _sock = make_connection(inbound) + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) + + +def test_header_block_cap(monkeypatch): + monkeypatch.setattr(conn_mod, "MAX_HEADER_BLOCK", 4) + block = hpack_encode(OK_TRAILERS) + inbound = ( + settings_frame() + + headers_frame(1, OK_RESPONSE_HEADERS, end_stream=False) + + frames.encode_frame( + frames.Frame(frames.HEADERS, frames.FLAG_END_STREAM, 1, block[:1]) + ) + + continuation_frame(1, block[1:], end_headers=True) + ) + conn, _sock = make_connection(inbound) + with pytest.raises(TransportError): + conn.request([(b":method", b"POST")], b"req", Deadline(5)) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_frames.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_frames.py new file mode 100644 index 00000000000..dd341def6cb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_frames.py @@ -0,0 +1,86 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc import frames + + +def roundtrip(frame): + encoded = frames.encode_frame(frame) + length, decoded = frames.decode_frame_header(encoded[: frames.FRAME_HEADER_LEN]) + decoded.payload = encoded[frames.FRAME_HEADER_LEN :] + assert length == len(decoded.payload) + return decoded + + +def test_data_frame_roundtrip(): + frame = roundtrip( + frames.Frame(frames.DATA, frames.FLAG_END_STREAM, 3, b"payload") + ) + assert (frame.type, frame.flags, frame.stream_id, frame.payload) == ( + frames.DATA, + frames.FLAG_END_STREAM, + 3, + b"payload", + ) + + +def test_settings_roundtrip(): + frame = roundtrip( + frames.settings_frame( + { + frames.SETTINGS_MAX_FRAME_SIZE: 1 << 20, + frames.SETTINGS_INITIAL_WINDOW_SIZE: 12345, + } + ) + ) + assert frames.parse_settings(frame) == { + frames.SETTINGS_MAX_FRAME_SIZE: 1 << 20, + frames.SETTINGS_INITIAL_WINDOW_SIZE: 12345, + } + + +def test_settings_ack_has_empty_payload(): + frame = frames.settings_frame(ack=True) + assert frame.flags == frames.FLAG_ACK and frame.payload == b"" + + +def test_goaway_roundtrip(): + frame = roundtrip(frames.goaway_frame(7, 2)) + last_stream_id, error_code, debug = frames.parse_goaway(frame) + assert (last_stream_id, error_code, debug) == (7, 2, b"") + + +def test_padded_data_frame_stripping(): + # PADDED flag: first octet is the pad length, padding trails the data. + payload = bytes((3,)) + b"data" + b"\x00" * 3 + frame = frames.Frame(frames.DATA, frames.FLAG_PADDED, 1, payload) + assert frames.strip_padding(frame) == b"data" + + +def test_padding_longer_than_payload_is_an_error(): + frame = frames.Frame(frames.DATA, frames.FLAG_PADDED, 1, bytes((200,)) + b"x") + with pytest.raises(ValueError): + frames.strip_padding(frame) + + +def test_headers_priority_section_stripping(): + payload = b"\x00\x00\x00\x01\x10headerblock" + frame = frames.Frame(frames.HEADERS, frames.FLAG_PRIORITY, 1, payload) + assert frames.strip_padding(frame) == b"headerblock" + + +def test_stream_id_high_bit_is_masked(): + frame = roundtrip(frames.Frame(frames.DATA, 0, 0xFFFFFFFF, b"")) + assert frame.stream_id == 0x7FFFFFFF + + +def test_encode_frame_rejects_payload_over_24_bits(): + class _HugePayload: + # Reports an over-limit length without allocating 16 MiB. + def __len__(self): + return 0x1000000 + + with pytest.raises(ValueError): + frames.encode_frame(frames.Frame(frames.DATA, 0, 1, _HugePayload())) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_hpack.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_hpack.py new file mode 100644 index 00000000000..30885e92132 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/unit/test_hpack.py @@ -0,0 +1,228 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""HPACK codec tests. + +Cross-checked against the reference MIT-licensed ``hpack`` package when it is +installed (it is a dev-only dependency of this suite); the RFC 7541 appendix C +stories run unconditionally. +""" + +import pytest + +from opentelemetry.exporter.otlp._proto.grpc._pygrpc.hpack import ( + Decoder, + HpackError, + encode, + encode_dynamic_table_size_update, +) + +try: + import hpack as ref_hpack +except ImportError: # pragma: no cover + ref_hpack = None + +requires_ref = pytest.mark.skipif( + ref_hpack is None, reason="reference hpack package not installed" +) + + +GRPC_REQUEST_HEADERS = [ + (b":method", b"POST"), + (b":scheme", b"https"), + (b":path", b"/opentelemetry.proto.collector.trace.v1.TraceService/Export"), + (b":authority", b"ingress.example.com:4317"), + (b"te", b"trailers"), + (b"content-type", b"application/grpc"), + (b"grpc-timeout", b"10S"), + (b"user-agent", b"otel-otlp-pyproto-exporter"), + (b"authorization", b"Bearer sekrit token"), +] + + +# --- RFC 7541 appendix C stories (hand-transcribed request examples) ------- + + +def test_rfc7541_c_2_1_literal_with_indexing(): + block = bytes.fromhex( + "400a637573746f6d2d6b65790d637573746f6d2d686561646572" + ) + decoder = Decoder() + assert decoder.decode(block) == [(b"custom-key", b"custom-header")] + # The entry must have been added to the dynamic table (index 62). + assert decoder.decode(bytes((0x80 | 62,))) == [ + (b"custom-key", b"custom-header") + ] + + +def test_rfc7541_c_2_2_literal_without_indexing(): + block = bytes.fromhex("040c2f73616d706c652f70617468") + assert Decoder().decode(block) == [(b":path", b"/sample/path")] + + +def test_rfc7541_c_2_3_literal_never_indexed(): + block = bytes.fromhex("100870617373776f726406736563726574") + assert Decoder().decode(block) == [(b"password", b"secret")] + + +def test_rfc7541_c_2_4_indexed_field(): + assert Decoder().decode(bytes((0x82,))) == [(b":method", b"GET")] + + +def test_rfc7541_c_3_request_examples_without_huffman(): + decoder = Decoder() + first = bytes.fromhex("828684410f7777772e6578616d706c652e636f6d") + assert decoder.decode(first) == [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"www.example.com"), + ] + second = bytes.fromhex("828684be58086e6f2d6361636865") + assert decoder.decode(second) == [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"www.example.com"), + (b"cache-control", b"no-cache"), + ] + third = bytes.fromhex( + "828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565" + ) + assert decoder.decode(third) == [ + (b":method", b"GET"), + (b":scheme", b"https"), + (b":path", b"/index.html"), + (b":authority", b"www.example.com"), + (b"custom-key", b"custom-value"), + ] + + +def test_rfc7541_c_4_request_examples_with_huffman(): + decoder = Decoder() + first = bytes.fromhex("828684418cf1e3c2e5f23a6ba0ab90f4ff") + assert decoder.decode(first) == [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"www.example.com"), + ] + second = bytes.fromhex("828684be5886a8eb10649cbf") + assert decoder.decode(second) == [ + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"www.example.com"), + (b"cache-control", b"no-cache"), + ] + third = bytes.fromhex( + "828785bf408825a849e95ba97d7f8925a849e95bb8e8b4bf" + ) + assert decoder.decode(third) == [ + (b":method", b"GET"), + (b":scheme", b"https"), + (b":path", b"/index.html"), + (b":authority", b"www.example.com"), + (b"custom-key", b"custom-value"), + ] + + +# --- Error handling --------------------------------------------------------- + + +def test_dynamic_table_size_update_encoding(): + # 001 prefix (0x20) with a 5-bit integer; size 0 is a single 0x20 byte. + assert encode_dynamic_table_size_update(0) == b"\x20" + assert encode_dynamic_table_size_update(4096) == bytes.fromhex("3fe11f") + + +def test_decoder_accepts_leading_size_update_then_headers(): + block = encode_dynamic_table_size_update(0) + encode( + [(b":method", b"POST"), (b"content-type", b"application/grpc")] + ) + assert Decoder().decode(block) == [ + (b":method", b"POST"), + (b"content-type", b"application/grpc"), + ] + + +@requires_ref +def test_reference_decoder_accepts_our_leading_size_update(): + block = encode_dynamic_table_size_update(0) + encode(GRPC_REQUEST_HEADERS) + decoded = ref_hpack.Decoder().decode(block, raw=True) + assert [(bytes(n), bytes(v)) for n, v in decoded] == GRPC_REQUEST_HEADERS + + +def test_index_zero_is_an_error(): + with pytest.raises(HpackError): + Decoder().decode(bytes((0x80,))) + + +def test_index_beyond_tables_is_an_error(): + with pytest.raises(HpackError): + Decoder().decode(bytes((0x80 | 0x7F, 0x7F))) + + +def test_truncated_string_is_an_error(): + with pytest.raises(HpackError): + Decoder().decode(bytes((0x00, 0x05, 0x61))) + + +def test_invalid_huffman_padding_is_an_error(): + # Literal without indexing, new name, Huffman-coded, padding of zeros. + with pytest.raises(HpackError): + Decoder().decode(bytes((0x00, 0x81, 0x00, 0x01, 0x61))) + + +# --- Round trips against the reference implementation ----------------------- + + +@requires_ref +def test_our_encoder_against_reference_decoder(): + block = encode(GRPC_REQUEST_HEADERS) + decoded = ref_hpack.Decoder().decode(block, raw=True) + assert [(bytes(n), bytes(v)) for n, v in decoded] == GRPC_REQUEST_HEADERS + + +@requires_ref +@pytest.mark.parametrize("huffman", [False, True]) +def test_reference_encoder_against_our_decoder(huffman): + encoder = ref_hpack.Encoder() + encoder.huffman = huffman + decoder = Decoder() + # Multiple blocks over one connection: exercises the dynamic table. + for round_number in range(3): + headers = [ + (b":status", b"200"), + (b"content-type", b"application/grpc"), + (b"grpc-status", b"0"), + (b"grpc-message", b""), + (b"x-round", str(round_number).encode()), + (b"x-large", (b"v" * 300)), + ] + block = encoder.encode(headers, huffman=huffman) + assert decoder.decode(block) == headers + + +@requires_ref +def test_dynamic_table_eviction_against_reference(): + encoder = ref_hpack.Encoder() + encoder.header_table_size = 128 # emits a size-update instruction + decoder = Decoder() + for round_number in range(20): + headers = [ + ( + "x-key-{}".format(round_number).encode(), + ("value-{}".format(round_number) * 5).encode(), + ), + ] + block = encoder.encode(headers, huffman=True) + assert decoder.decode(block) == headers + + +@requires_ref +def test_huffman_all_byte_values_round_trip(): + payload = bytes(range(256)) + encoder = ref_hpack.Encoder() + block = encoder.encode([(b"x-bin", payload)], huffman=True) + assert Decoder().decode(block) == [(b"x-bin", payload)] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 00830f48179..c0391007421 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -26,12 +26,10 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "googleapis-common-protos ~= 1.52", "opentelemetry-api ~= 1.15", "opentelemetry-proto == 1.45.0.dev", "opentelemetry-sdk ~= 1.45.0.dev", "opentelemetry-exporter-otlp-proto-common == 1.45.0.dev", - "requests ~= 2.7", "typing-extensions >= 4.5.0", ] @@ -48,13 +46,9 @@ otlp_proto_http = "opentelemetry.exporter.otlp.proto.http._log_exporter:OTLPLogE Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http" Repository = "https://github.com/open-telemetry/opentelemetry-python" -[project.optional-dependencies] -gcp-auth = [ - "opentelemetry-exporter-credential-provider-gcp >= 0.59b0", -] [tool.hatch.version] -path = "src/opentelemetry/exporter/otlp/proto/http/version/__init__.py" +path = "src/opentelemetry/exporter/otlp/_proto/http/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/__init__.py new file mode 100644 index 00000000000..faf9c9e9b69 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/__init__.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum + +from .version import __version__ + +_OTLP_HTTP_HEADERS = { + "Content-Type": "application/x-protobuf", + "User-Agent": "OTel-OTLP-Exporter-Python/" + __version__, +} + + +class Compression(Enum): + NoCompression = "none" + Deflate = "deflate" + Gzip = "gzip" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_common/__init__.py new file mode 100644 index 00000000000..b5ada05ff9d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_common/__init__.py @@ -0,0 +1,151 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import ssl +from os import environ +from typing import Callable, Literal +from urllib.error import HTTPError, URLError +from urllib.request import ( + HTTPSHandler, + OpenerDirector, + Request, + build_opener, +) +from warnings import warn + +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, +) +from opentelemetry.util._importlib_metadata import entry_points + +# A client sends one already-serialized, already-compressed payload and reports +# back the HTTP status code and reason phrase. Transport-level failures are +# raised as URLError so the exporters' retry loops treat them as retryable. +_Client = Callable[[str, bytes, "dict[str, str]", float], "tuple[int, str]"] + +_CREDENTIAL_PROVIDER_ENTRY_POINT = "opentelemetry_otlp_credential_provider" + +_DEPRECATED_SESSION_MESSAGE = ( + "Passing a requests.Session (or any object exposing a .post() method) to the " + "OTLP HTTP exporter - through the `session` argument, or via a " + "'opentelemetry_otlp_credential_provider' entry point named by " + "OTEL_PYTHON_EXPORTER_OTLP_HTTP[_TRACES|_METRICS|_LOGS]_CREDENTIAL_PROVIDER - " + "is deprecated and will be removed in a future release. Return a " + "urllib.request.OpenerDirector instead." +) + + +def _is_retryable(status_code: int) -> bool: + if status_code == 408: + return True + if 500 <= status_code <= 599: + return True + return False + + +def _build_ssl_context( + certificate_file: str | bool, + client_cert: str | tuple[str, str | None] | None, +) -> ssl.SSLContext: + context = ssl.create_default_context( + cafile=certificate_file if isinstance(certificate_file, str) else None + ) + if certificate_file is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + if client_cert: + certfile, keyfile = ( + client_cert if isinstance(client_cert, tuple) else (client_cert, None) + ) + context.load_cert_chain(certfile, keyfile) + return context + + +def _opener_client(opener: OpenerDirector) -> _Client: + def post(url, data, headers, timeout_sec): + request = Request(url, data=data, headers=headers, method="POST") + try: + with opener.open(request, timeout=timeout_sec) as response: + return response.status, response.reason + except HTTPError as error: + return error.code, error.reason + + return post + + +def _session_client(session: object) -> _Client: + def post(url, data, headers, timeout_sec): + try: + response = session.post( + url, data=data, headers=headers, timeout=timeout_sec + ) + except OSError as error: + # requests' transport exceptions subclass OSError; surface them as a + # URLError so the exporters' retry loops handle them uniformly. + raise URLError(error) from error + return response.status_code, response.reason + + return post + + +def _load_provider_from_envvar( + cred_envvar: Literal[ + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", + ], +) -> object | None: + name = environ.get( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER + ) or environ.get(cred_envvar) + if not name: + return None + try: + provider = next( + iter(entry_points(group=_CREDENTIAL_PROVIDER_ENTRY_POINT, name=name)) + ) + except StopIteration: + raise RuntimeError( + f"Requested component '{name}' not found in entry point " + f"'{_CREDENTIAL_PROVIDER_ENTRY_POINT}'" + ) + return provider.load()() + + +def _resolve_client( + session: object | None, + cred_envvar: Literal[ + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", + ], + ssl_context: ssl.SSLContext, +) -> _Client: + """Resolve the HTTP client the exporter sends through. + + Precedence mirrors the original ``requests``-based exporter: an explicit + ``session`` argument wins, otherwise the object produced by the credential + provider named in the environment, otherwise a default stdlib opener built + from ``ssl_context``. + + A ``urllib.request.OpenerDirector`` is the supported injectable. A + ``requests.Session`` (detected structurally, without importing ``requests``) + is still accepted for backwards compatibility but is deprecated. + """ + injectable = ( + session + if session is not None + else _load_provider_from_envvar(cred_envvar) + ) + if injectable is None: + return _opener_client(build_opener(HTTPSHandler(context=ssl_context))) + if isinstance(injectable, OpenerDirector): + return _opener_client(injectable) + if callable(getattr(injectable, "post", None)): + warn(_DEPRECATED_SESSION_MESSAGE, DeprecationWarning, stacklevel=2) + return _session_client(injectable) + raise RuntimeError( + "OTLP HTTP credential provider must return a " + "urllib.request.OpenerDirector (or, deprecated, a requests.Session); " + f"got {type(injectable)}" + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_log_exporter/__init__.py new file mode 100644 index 00000000000..ec763a8690b --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/_log_exporter/__init__.py @@ -0,0 +1,266 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +from collections.abc import Sequence +from gzip import GzipFile +from io import BytesIO +from logging import getLogger +from os import environ +from random import uniform +from threading import Event +from time import time +from urllib.error import URLError +from urllib.parse import urlparse +from zlib import compress + +from opentelemetry.exporter.otlp._proto.common._exporter_metrics import ( + create_exporter_metrics, +) +from opentelemetry.exporter.otlp._proto.common._internal._log_encoder import ( + encode_logs, +) +from opentelemetry.exporter.otlp._proto.http import ( + _OTLP_HTTP_HEADERS, + Compression, +) +from opentelemetry.exporter.otlp._proto.http._common import ( + _build_ssl_context, + _is_retryable, + _resolve_client, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry.sdk._logs import ReadableLogRecord +from opentelemetry.sdk._logs.export import ( + LogRecordExporter, + LogRecordExportResult, +) +from opentelemetry.sdk._shared_internal import DuplicateFilter +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.util.re import parse_env_headers + +_logger = getLogger(__name__) +_logger.addFilter(DuplicateFilter()) + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +DEFAULT_TIMEOUT = 10 +_MAX_RETRYS = 6 + + +class OTLPLogExporter(LogRecordExporter): + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: dict[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + session: object | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + self._shutdown_is_occuring = Event() + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + _append_logs_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers(headers_string, liberal=True) + self._timeout = timeout or float( + environ.get( + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} + if self._compression is not Compression.NoCompression: + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) + self._client = _resolve_client( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, + self._ssl_context, + ) + self._shutdown = False + + self._metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, + "logs", + urlparse(self._endpoint), + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) + + def _export(self, serialized_data: bytes, timeout_sec: float | None = None): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = compress(serialized_data) + if timeout_sec is None: + timeout_sec = self._timeout + try: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + except URLError: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + + def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogRecordExportResult.FAILURE + + with self._metrics.export_operation(len(batch)) as result: + serialized_data = encode_logs(batch).SerializeToString() + deadline_sec = time() + self._timeout + for retry_num in range(_MAX_RETRYS): + backoff_seconds = 2**retry_num * uniform(0.8, 1.2) + export_error: Exception | None = None + try: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: + return LogRecordExportResult.SUCCESS + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason + export_error = error + retryable = True + status_code = None + + if not retryable: + _logger.error( + "Failed to export logs batch code: %s, reason: %s", + status_code, + reason, + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return LogRecordExportResult.FAILURE + + if ( + retry_num + 1 == _MAX_RETRYS + or backoff_seconds > (deadline_sec - time()) + or self._shutdown + ): + _logger.error( + "Failed to export logs batch due to timeout, max retries or shutdown." + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return LogRecordExportResult.FAILURE + + _logger.warning( + "Transient error %s encountered while exporting logs batch, retrying in %.2fs.", + reason, + backoff_seconds, + ) + if self._shutdown_is_occuring.wait(backoff_seconds): + _logger.warning("Shutdown in progress, aborting retry.") + break + return LogRecordExportResult.FAILURE + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._shutdown_is_occuring.set() + + +def _compression_from_env() -> Compression: + return Compression( + environ.get( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + + +def _append_logs_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_LOGS_EXPORT_PATH + return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/metric_exporter/__init__.py new file mode 100644 index 00000000000..910845cbf82 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/metric_exporter/__init__.py @@ -0,0 +1,498 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import os +from collections.abc import Iterable +from gzip import GzipFile +from io import BytesIO +from logging import getLogger +from os import environ +from random import uniform +from threading import Event +from time import time +from urllib.error import URLError +from urllib.parse import urlparse +from zlib import compress + +from opentelemetry.exporter.otlp._proto.common._internal import ( + _get_resource_data, +) +from opentelemetry.exporter.otlp._proto.common._exporter_metrics import ( + create_exporter_metrics, +) +from opentelemetry.exporter.otlp._proto.common._internal.metrics_encoder import ( + OTLPMetricExporterMixin, + encode_metrics, +) +from opentelemetry.exporter.otlp._proto.http import ( + _OTLP_HTTP_HEADERS, + Compression, +) +from opentelemetry.exporter.otlp._proto.http._common import ( + _build_ssl_context, + _is_retryable, + _resolve_client, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) +from opentelemetry._proto.metrics.v1.metrics_pb2 import ( + ExponentialHistogram, + Gauge, + Histogram, + Metric, + ResourceMetrics, + ScopeMetrics, + Sum, + Summary, +) +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.metrics._internal.aggregation import Aggregation +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + MetricExporter, + MetricExportResult, + MetricsData, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.util.re import parse_env_headers + +_logger = getLogger(__name__) + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" +DEFAULT_TIMEOUT = 10 +_MAX_RETRYS = 6 + + +class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: dict[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + session: object | None = None, + preferred_temporality: dict[type, AggregationTemporality] | None = None, + preferred_aggregation: dict[type, Aggregation] | None = None, + max_export_batch_size: int | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + self._shutdown_in_progress = Event() + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + _append_metrics_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers(headers_string, liberal=True) + self._timeout = timeout or float( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} + if self._compression is not Compression.NoCompression: + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) + self._client = _resolve_client( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, + self._ssl_context, + ) + self._common_configuration(preferred_temporality, preferred_aggregation) + self._max_export_batch_size = max_export_batch_size + self._shutdown = False + + self._metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, + "metrics", + urlparse(self._endpoint), + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) + + def _export(self, serialized_data: bytes, timeout_sec: float | None = None): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = compress(serialized_data) + if timeout_sec is None: + timeout_sec = self._timeout + try: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + except URLError: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + + def _export_with_retries( + self, + export_request: ExportMetricsServiceRequest, + deadline_sec: float, + num_items: int, + ) -> MetricExportResult: + with self._metrics.export_operation(num_items) as result: + serialized_data = export_request.SerializeToString() + for retry_num in range(_MAX_RETRYS): + backoff_seconds = 2**retry_num * uniform(0.8, 1.2) + export_error: Exception | None = None + try: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: + return MetricExportResult.SUCCESS + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason + export_error = error + retryable = True + status_code = None + + if not retryable: + _logger.error( + "Failed to export metrics batch code: %s, reason: %s", + status_code, + reason, + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return MetricExportResult.FAILURE + + if ( + retry_num + 1 == _MAX_RETRYS + or backoff_seconds > (deadline_sec - time()) + or self._shutdown + ): + _logger.error( + "Failed to export metrics batch due to timeout, max retries or shutdown." + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return MetricExportResult.FAILURE + + _logger.warning( + "Transient error %s encountered while exporting metrics batch, retrying in %.2fs.", + reason, + backoff_seconds, + ) + if self._shutdown_in_progress.wait(backoff_seconds): + _logger.warning("Shutdown in progress, aborting retry.") + break + return MetricExportResult.FAILURE + + def export( + self, + metrics_data: MetricsData, + timeout_millis: float | None = 10000, + **kwargs, + ) -> MetricExportResult: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return MetricExportResult.FAILURE + + num_items = 0 + for resource_metrics in metrics_data.resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + num_items += len(metric.data.data_points) + + export_request = encode_metrics(metrics_data) + deadline_sec = time() + self._timeout + + if self._max_export_batch_size is None: + return self._export_with_retries(export_request, deadline_sec, num_items) + + for split_request in _split_metrics_data(export_request, self._max_export_batch_size): + if self._export_with_retries(split_request, deadline_sec, num_items) != MetricExportResult.SUCCESS: + return MetricExportResult.FAILURE + + return MetricExportResult.SUCCESS + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._shutdown_in_progress.set() + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + def set_meter_provider(self, meter_provider: MeterProvider) -> None: + self._metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, + "metrics", + urlparse(self._endpoint), + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) + + @property + def _exporting(self) -> str: + return "metrics" + + +def _split_metrics_data( + metrics_data: ExportMetricsServiceRequest, + max_export_batch_size: int, +) -> Iterable[ExportMetricsServiceRequest]: + if not max_export_batch_size: + yield metrics_data + return + + batch_size = 0 + split_resource_metrics: list[dict] = [] + + for resource_metrics in metrics_data.resource_metrics: + split_scope_metrics: list[dict] = [] + split_resource_metrics.append({ + "resource": resource_metrics.resource, + "schema_url": resource_metrics.schema_url, + "scope_metrics": split_scope_metrics, + }) + + for scope_metrics in resource_metrics.scope_metrics: + split_metrics: list[dict] = [] + split_scope_metrics.append({ + "scope": scope_metrics.scope, + "schema_url": scope_metrics.schema_url, + "metrics": split_metrics, + }) + + for metric in scope_metrics.metrics: + split_data_points: list = [] + field_name = metric.WhichOneof("data") + if not field_name: + _logger.warning("Tried to split an unsupported metric type. Skipping.") + continue + + data_container = getattr(metric, field_name) + metric_dict: dict = { + "name": metric.name, + "description": metric.description, + "unit": metric.unit, + field_name: {"data_points": split_data_points}, + } + if hasattr(data_container, "aggregation_temporality"): + metric_dict[field_name]["aggregation_temporality"] = data_container.aggregation_temporality + if hasattr(data_container, "is_monotonic"): + metric_dict[field_name]["is_monotonic"] = data_container.is_monotonic + split_metrics.append(metric_dict) + + for data_point in data_container.data_points: + split_data_points.append(data_point) + batch_size += 1 + + if batch_size >= max_export_batch_size: + yield ExportMetricsServiceRequest( + resource_metrics=_build_resource_metrics(split_resource_metrics) + ) + batch_size = 0 + split_data_points = [] + + field_name = metric.WhichOneof("data") + if field_name is None: + _logger.warning("Tried to split an unsupported metric type. Skipping.") + continue + data_container = getattr(metric, field_name) + metric_dict = { + "name": metric.name, + "description": metric.description, + "unit": metric.unit, + field_name: {"data_points": split_data_points}, + } + if hasattr(data_container, "aggregation_temporality"): + metric_dict[field_name]["aggregation_temporality"] = data_container.aggregation_temporality + if hasattr(data_container, "is_monotonic"): + metric_dict[field_name]["is_monotonic"] = data_container.is_monotonic + + split_metrics = [metric_dict] + split_scope_metrics = [{ + "scope": scope_metrics.scope, + "schema_url": scope_metrics.schema_url, + "metrics": split_metrics, + }] + split_resource_metrics = [{ + "resource": resource_metrics.resource, + "schema_url": resource_metrics.schema_url, + "scope_metrics": split_scope_metrics, + }] + + if not split_data_points: + split_metrics.pop() + + if not split_metrics: + split_scope_metrics.pop() + + if not split_scope_metrics: + split_resource_metrics.pop() + + if batch_size > 0: + yield ExportMetricsServiceRequest( + resource_metrics=_build_resource_metrics(split_resource_metrics) + ) + + +def _build_resource_metrics(split_resource_metrics: list[dict]) -> list[ResourceMetrics]: + result = [] + for rm in split_resource_metrics: + scope_metrics_list = [] + for sm in rm.get("scope_metrics", []): + metrics_list = [] + for metric in sm.get("metrics", []): + new_metric = _build_metric(metric) + if new_metric is not None: + metrics_list.append(new_metric) + scope_metrics_list.append(ScopeMetrics( + scope=sm.get("scope"), + metrics=metrics_list, + schema_url=sm.get("schema_url") or "", + )) + result.append(ResourceMetrics( + resource=rm.get("resource"), + scope_metrics=scope_metrics_list, + schema_url=rm.get("schema_url") or "", + )) + return result + + +def _build_metric(metric: dict) -> Metric | None: + kwargs: dict = dict( + name=metric.get("name"), + description=metric.get("description"), + unit=metric.get("unit"), + ) + if "sum" in metric: + d = metric["sum"] + kwargs["sum"] = Sum( + data_points=list(d.get("data_points", [])), + aggregation_temporality=d.get("aggregation_temporality", 0), + is_monotonic=d.get("is_monotonic", False), + ) + elif "histogram" in metric: + d = metric["histogram"] + kwargs["histogram"] = Histogram( + data_points=list(d.get("data_points", [])), + aggregation_temporality=d.get("aggregation_temporality", 0), + ) + elif "exponential_histogram" in metric: + d = metric["exponential_histogram"] + kwargs["exponential_histogram"] = ExponentialHistogram( + data_points=list(d.get("data_points", [])), + aggregation_temporality=d.get("aggregation_temporality", 0), + ) + elif "gauge" in metric: + d = metric["gauge"] + kwargs["gauge"] = Gauge(data_points=list(d.get("data_points", []))) + elif "summary" in metric: + d = metric["summary"] + kwargs["summary"] = Summary(data_points=list(d.get("data_points", []))) + else: + _logger.warning("Tried to build an unsupported metric type. Skipping.") + return None + return Metric(**kwargs) + + +def _compression_from_env() -> Compression: + return Compression( + environ.get( + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + + +def _append_metrics_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_METRICS_EXPORT_PATH + return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}" + + +def get_resource_data(sdk_resource_scope_data, resource_class, name): + return _get_resource_data(sdk_resource_scope_data, resource_class, name) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/trace_exporter/__init__.py new file mode 100644 index 00000000000..97de8e63cdb --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/trace_exporter/__init__.py @@ -0,0 +1,261 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +from collections.abc import Sequence +from gzip import GzipFile +from io import BytesIO +from logging import getLogger +from os import environ +from random import uniform +from threading import Event +from time import time +from urllib.error import URLError +from urllib.parse import urlparse +from zlib import compress + +from opentelemetry.exporter.otlp._proto.common._exporter_metrics import ( + create_exporter_metrics, +) +from opentelemetry.exporter.otlp._proto.common._internal.trace_encoder import ( + encode_spans, +) +from opentelemetry.exporter.otlp._proto.http import ( + _OTLP_HTTP_HEADERS, + Compression, +) +from opentelemetry.exporter.otlp._proto.http._common import ( + _build_ssl_context, + _is_retryable, + _resolve_client, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_CLIENT_KEY, + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, + OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.util.re import parse_env_headers + +_logger = getLogger(__name__) + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_TRACES_EXPORT_PATH = "v1/traces" +DEFAULT_TIMEOUT = 10 +_MAX_RETRYS = 6 + + +class OTLPSpanExporter(SpanExporter): + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: dict[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + session: object | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + self._shutdown_in_progress = Event() + self._endpoint = endpoint or environ.get( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + _append_trace_path( + environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) + ), + ) + self._certificate_file = certificate_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), + ) + self._client_key_file = client_key_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), + ) + self._client_certificate_file = client_certificate_file or environ.get( + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, + environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), + ) + self._client_cert = ( + (self._client_certificate_file, self._client_key_file) + if self._client_certificate_file and self._client_key_file + else self._client_certificate_file + ) + headers_string = environ.get( + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), + ) + self._headers = headers or parse_env_headers(headers_string, liberal=True) + self._timeout = timeout or float( + environ.get( + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), + ) + ) + self._compression = compression or _compression_from_env() + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} + if self._compression is not Compression.NoCompression: + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) + self._client = _resolve_client( + session, + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, + self._ssl_context, + ) + self._shutdown = False + + self._metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + urlparse(self._endpoint), + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") + .strip() + .lower() + == "true", + ) + + def _export(self, serialized_data: bytes, timeout_sec: float | None = None): + data = serialized_data + if self._compression == Compression.Gzip: + gzip_data = BytesIO() + with GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: + gzip_stream.write(serialized_data) + data = gzip_data.getvalue() + elif self._compression == Compression.Deflate: + data = compress(serialized_data) + if timeout_sec is None: + timeout_sec = self._timeout + try: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + except URLError: + return self._client( + self._endpoint, + data, + self._request_headers, + timeout_sec, + ) + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return SpanExportResult.FAILURE + + with self._metrics.export_operation(len(spans)) as result: + serialized_data = encode_spans(spans).SerializeToString() + deadline_sec = time() + self._timeout + for retry_num in range(_MAX_RETRYS): + backoff_seconds = 2**retry_num * uniform(0.8, 1.2) + export_error: Exception | None = None + try: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: + return SpanExportResult.SUCCESS + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason + export_error = error + retryable = True + status_code = None + + if not retryable: + _logger.error( + "Failed to export span batch code: %s, reason: %s", + status_code, + reason, + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return SpanExportResult.FAILURE + + if ( + retry_num + 1 == _MAX_RETRYS + or backoff_seconds > (deadline_sec - time()) + or self._shutdown + ): + _logger.error( + "Failed to export span batch due to timeout, max retries or shutdown." + ) + error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: status_code} + if status_code is not None + else None + ) + result.error = export_error + result.error_attrs = error_attrs + return SpanExportResult.FAILURE + + _logger.warning( + "Transient error %s encountered while exporting span batch, retrying in %.2fs.", + reason, + backoff_seconds, + ) + if self._shutdown_in_progress.wait(backoff_seconds): + _logger.warning("Shutdown in progress, aborting retry.") + break + return SpanExportResult.FAILURE + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._shutdown_in_progress.set() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + return True + + +def _compression_from_env() -> Compression: + return Compression( + environ.get( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), + ) + .lower() + .strip() + ) + + +def _append_trace_path(endpoint: str) -> str: + if endpoint.endswith("/"): + return endpoint + DEFAULT_TRACES_EXPORT_PATH + return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/version/__init__.py new file mode 100644 index 00000000000..cdc135bf8ae --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/_proto/http/version/__init__.py @@ -0,0 +1 @@ +__version__ = "1.45.0.dev" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py index 69f51ec0557..88071ac788c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/__init__.py @@ -1,75 +1,10 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from opentelemetry.exporter.otlp._proto.http import * # noqa: F401,F403 +import opentelemetry.exporter.otlp._proto.http as _src -""" -This library allows to export tracing data to an OTLP collector. - -Usage ------ - -The **OTLP Span Exporter** allows to export `OpenTelemetry`_ traces to the -`OTLP`_ collector. - -You can configure the exporter with the following environment variables: - -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` -- :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` -- :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` -- :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` -- :envvar:`OTEL_EXPORTER_OTLP_HEADERS` -- :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` -- :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` -- :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` - -.. _OTLP: https://github.com/open-telemetry/opentelemetry-collector/ -.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ - -.. code:: python - - from opentelemetry import trace - from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter - from opentelemetry.sdk.resources import Resource - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import BatchSpanProcessor - - # Resource can be required for some backends, e.g. Jaeger - # If resource wouldn't be set - traces wouldn't appears in Jaeger - resource = Resource.create({ - "service.name": "service" - }) - - trace.set_tracer_provider(TracerProvider(resource=resource)) - tracer = trace.get_tracer(__name__) - - otlp_exporter = OTLPSpanExporter() - - span_processor = BatchSpanProcessor(otlp_exporter) - - trace.get_tracer_provider().add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - print("Hello world!") - -API ---- -""" - -import enum - -from .version import __version__ - -_OTLP_HTTP_HEADERS = { - "Content-Type": "application/x-protobuf", - "User-Agent": "OTel-OTLP-Exporter-Python/" + __version__, -} - - -class Compression(enum.Enum): - NoCompression = "none" - Deflate = "deflate" - Gzip = "gzip" +for _n in dir(_src): + if not _n.startswith('__'): + globals().setdefault(_n, getattr(_src, _n)) +del _src, _n diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 46db16dd86a..cb5453729aa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -1,79 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from os import environ -from typing import Literal +import sys as _sys +import opentelemetry.exporter.otlp._proto.http._common as _mod -import requests - -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, -) -from opentelemetry.util._importlib_metadata import entry_points - -# 64 MiB, in bytes. -_DEFAULT_MAX_REQUEST_SIZE = 64 * 1024 * 1024 - - -class RequestPayloadTooLargeError(Exception): - """A serialized OTLP request exceeded the configured ``max_request_size``. - - The class name is emitted as the ``error.type`` attribute on the exporter's - failed-export metric, so renaming it changes observable telemetry. - """ - - -def _is_retryable(resp: requests.Response) -> bool: - if resp.status_code == 408: - return True - if resp.status_code >= 500 and resp.status_code <= 599: - return True - return False - - -def _is_request_too_large( - serialized_data: bytes, max_request_size: int -) -> bool: - """Return True if the serialized request exceeds a positive size limit. - - The size is measured on the uncompressed serialized request, matching the - OTLP specification's "before compression" request-size limit. A - ``max_request_size`` of ``0`` (or any non-positive value) disables the - check. - """ - return max_request_size > 0 and len(serialized_data) > max_request_size - - -def _load_session_from_envvar( - cred_envvar: Literal[ - "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", - "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", - "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", - ], -) -> requests.Session | None: - _credential_env = environ.get( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER - ) or environ.get(cred_envvar) - if _credential_env: - try: - maybe_session = next( - iter( - entry_points( - group="opentelemetry_otlp_credential_provider", - name=_credential_env, - ) - ) - ).load()() - except StopIteration: - raise RuntimeError( - f"Requested component '{_credential_env}' not found in " - f"entry point 'opentelemetry_otlp_credential_provider'" - ) - if isinstance(maybe_session, requests.Session): - return maybe_session - else: - raise RuntimeError( - f"Requested component '{_credential_env}' is of type {type(maybe_session)}" - f" must be of type `requests.Session`." - ) - return None +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index c25ebec6756..9a97b20c420 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -1,337 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -import gzip -import logging -import os -import random -import threading -import zlib -from collections.abc import Sequence -from io import BytesIO -from os import environ -from time import time -from urllib.parse import urlparse +import sys as _sys +import opentelemetry.exporter.otlp._proto.http._log_exporter as _mod -import requests -from requests.exceptions import ConnectionError - -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - create_exporter_metrics, -) -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs -from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, - Compression, -) -from opentelemetry.exporter.otlp.proto.http._common import ( - _DEFAULT_MAX_REQUEST_SIZE, - RequestPayloadTooLargeError, - _is_request_too_large, - _is_retryable, - _load_session_from_envvar, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.sdk._logs import ReadableLogRecord -from opentelemetry.sdk._logs.export import ( - LogRecordExporter, - LogRecordExportResult, -) -from opentelemetry.sdk._shared_internal import DuplicateFilter -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) -from opentelemetry.util.re import parse_env_headers - -_logger = logging.getLogger(__name__) -# This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. -_logger.addFilter(DuplicateFilter()) - - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_LOGS_EXPORT_PATH = "v1/logs" -DEFAULT_TIMEOUT = 10 # in seconds -_MAX_RETRYS = 6 - - -class OTLPLogExporter(LogRecordExporter): - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: dict[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - session: requests.Session | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - ): - """OTLP HTTP log exporter. - - Args: - endpoint: Target URL to which the exporter is going to send logs. - certificate_file: Path to the CA certificate file for TLS. - client_key_file: Path to the client key file for mTLS. - client_certificate_file: Path to the client certificate file for mTLS. - headers: Headers to send with each export request. - timeout: Timeout in seconds for each export request. - compression: Compression to use; one of none, gzip, deflate. - session: Requests session to use at export. - max_request_size: Maximum size in bytes of a serialized request, - measured before compression. A request exceeding this size is - dropped before being sent. Defaults to 64 MiB; a value of 0 (or - any non-positive value) disables the limit. Batch processors - group log records by count rather than serialized size, so a - batch whose serialized request exceeds this limit is dropped as - a whole and recorded as a failed export; reduce the processor's - ``max_export_batch_size`` (or raise/disable this limit) if - batches may approach it. - meter_provider: MeterProvider used for the exporter's own metrics. - """ - self._shutdown_is_occuring = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - _append_logs_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - # Keeping these as instance variables because they are used in tests - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - self._max_request_size = ( - _DEFAULT_MAX_REQUEST_SIZE - if max_request_size is None - else max_request_size - ) - self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER - ) - or requests.Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) - self._shutdown = False - - self._metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, - "logs", - urlparse(self._endpoint), - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) - - def _export( - self, serialized_data: bytes, timeout_sec: float | None = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - - def export( - self, batch: Sequence[ReadableLogRecord] - ) -> LogRecordExportResult: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring batch") - return LogRecordExportResult.FAILURE - - with self._metrics.export_operation(len(batch)) as result: - serialized_data = encode_logs(batch).SerializeToString() - if _is_request_too_large(serialized_data, self._max_request_size): - _logger.warning( - "Dropping logs batch: serialized size %d bytes exceeds " - "max_request_size %d bytes.", - len(serialized_data), - self._max_request_size, - ) - result.error = RequestPayloadTooLargeError( - f"Serialized logs request size {len(serialized_data)} " - f"bytes exceeds max_request_size " - f"{self._max_request_size} bytes." - ) - return LogRecordExportResult.FAILURE - deadline_sec = time() + self._timeout - for retry_num in range(_MAX_RETRYS): - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) - export_error: Exception | None = None - try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: - return LogRecordExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code - - if not retryable: - _logger.error( - "Failed to export logs batch code: %s, reason: %s", - status_code, - reason, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return LogRecordExportResult.FAILURE - - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export logs batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return LogRecordExportResult.FAILURE - _logger.warning( - "Transient error %s encountered while exporting logs batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_is_occuring.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return LogRecordExportResult.FAILURE - - def force_flush(self, timeout_millis: int = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True - - def shutdown(self): - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_is_occuring.set() - self._session.close() - - -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - -def _append_logs_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_LOGS_EXPORT_PATH - return endpoint + f"/{DEFAULT_LOGS_EXPORT_PATH}" +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 7020beb7f32..8a3857974cb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -1,780 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations -import gzip -import logging -import os -import random -import threading -import zlib -from collections.abc import Callable, Iterable -from io import BytesIO -from os import environ -from time import time -from typing import ( # noqa: F401 - Any, - Optional, -) -from urllib.parse import urlparse +import sys as _sys +import opentelemetry.exporter.otlp._proto.http.metric_exporter as _mod -import requests -from requests.exceptions import ConnectionError -from typing_extensions import deprecated - -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - create_exporter_metrics, -) -from opentelemetry.exporter.otlp.proto.common._internal import ( - _get_resource_data, -) -from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( - OTLPMetricExporterMixin, -) -from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( - encode_metrics, -) -from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, - Compression, -) -from opentelemetry.exporter.otlp.proto.http._common import ( - _DEFAULT_MAX_REQUEST_SIZE, - RequestPayloadTooLargeError, - _is_request_too_large, - _is_retryable, - _load_session_from_envvar, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 - ExportMetricsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - AnyValue, - ArrayValue, - InstrumentationScope, - KeyValue, - KeyValueList, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 -from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics._internal.aggregation import Aggregation -from opentelemetry.sdk.metrics.export import ( # noqa: F401 - AggregationTemporality, - Gauge, - MetricExporter, - MetricExportResult, - MetricsData, - Sum, -) -from opentelemetry.sdk.metrics.export import ( # noqa: F401 - Histogram as HistogramType, -) -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) -from opentelemetry.util.re import parse_env_headers - -_logger = logging.getLogger(__name__) - - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_METRICS_EXPORT_PATH = "v1/metrics" -DEFAULT_TIMEOUT = 10 # in seconds -_MAX_RETRYS = 6 - - -class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin): - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: dict[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - session: requests.Session | None = None, - preferred_temporality: dict[type, AggregationTemporality] - | None = None, - preferred_aggregation: dict[type, Aggregation] | None = None, - max_export_batch_size: int | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - ): - """OTLP HTTP metrics exporter - - Args: - endpoint: Target URL to which the exporter is going to send metrics - certificate_file: Path to the certificate file to use for any TLS - client_key_file: Path to the client key file to use for any TLS - client_certificate_file: Path to the client certificate file to use for any TLS - headers: Headers to be sent with HTTP requests at export - timeout: Timeout in seconds for export - compression: Compression to use; one of none, gzip, deflate - session: Requests session to use at export - preferred_temporality: Map of preferred temporality for each metric type. - See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what - preferred temporality is. - preferred_aggregation: Map of preferred aggregation for each metric type. - See `opentelemetry.sdk.metrics.export.MetricReader` for more details on what - preferred aggregation is. - max_export_batch_size: Maximum number of data points to export in a single request. - If not set there is no limit to the number of data points in a request. - If it is set and the number of data points exceeds the max, the request will be split. - max_request_size: Maximum size in bytes of a serialized request, measured before - compression. A request exceeding this size is dropped before being sent. Defaults - to 64 MiB; a value of 0 (or any non-positive value) disables the limit. Requests - are sized after any ``max_export_batch_size`` splitting; a batch whose serialized - request still exceeds this limit is dropped as a whole and recorded as a failed - export. Reduce ``max_export_batch_size`` (or raise/disable this limit) if batches - may approach it. - meter_provider: MeterProvider used for the exporter's own metrics. - """ - self._shutdown_in_progress = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - _append_metrics_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER - ) - or requests.Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) - - self._common_configuration( - preferred_temporality, preferred_aggregation - ) - self._max_export_batch_size: int | None = max_export_batch_size - self._max_request_size = ( - _DEFAULT_MAX_REQUEST_SIZE - if max_request_size is None - else max_request_size - ) - self._shutdown = False - - self._metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, - "metrics", - urlparse(self._endpoint), - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) - - def _export( - self, serialized_data: bytes, timeout_sec: float | None = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - - def _export_with_retries( - self, - export_request: ExportMetricsServiceRequest, - deadline_sec: float, - num_items: int, - ) -> MetricExportResult: - """Export serialized data with retry logic until success, non-transient error, or exponential backoff maxed out. - - Args: - export_request: ExportMetricsServiceRequest object containing metrics data to export - deadline_sec: timestamp deadline for the export - - Returns: - MetricExportResult: SUCCESS if export succeeded, FAILURE otherwise - """ - with self._metrics.export_operation(num_items) as result: - serialized_data = export_request.SerializeToString() - if _is_request_too_large(serialized_data, self._max_request_size): - _logger.warning( - "Dropping metrics batch: serialized size %d bytes exceeds " - "max_request_size %d bytes.", - len(serialized_data), - self._max_request_size, - ) - result.error = RequestPayloadTooLargeError( - f"Serialized metrics request size {len(serialized_data)} " - f"bytes exceeds max_request_size " - f"{self._max_request_size} bytes." - ) - return MetricExportResult.FAILURE - deadline_sec = time() + self._timeout - for retry_num in range(_MAX_RETRYS): - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) - export_error: Exception | None = None - try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: - return MetricExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code - - if not retryable: - _logger.error( - "Failed to export metrics batch code: %s, reason: %s", - status_code, - reason, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return MetricExportResult.FAILURE - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export metrics batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return MetricExportResult.FAILURE - - _logger.warning( - "Transient error %s encountered while exporting metrics batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_in_progress.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return MetricExportResult.FAILURE - - def export( - self, - metrics_data: MetricsData, - timeout_millis: float | None = 10000, - **kwargs, - ) -> MetricExportResult: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring batch") - return MetricExportResult.FAILURE - - export_request = encode_metrics(metrics_data) - deadline_sec = time() + self._timeout - - # If no batch size configured, export as single batch with retries as configured - if self._max_export_batch_size is None: - return self._export_with_retries( - export_request, - deadline_sec, - _count_data_points(export_request), - ) - - # Else, export in batches of configured size - batched_export_requests = _split_metrics_data( - export_request, self._max_export_batch_size - ) - - for split_metrics_data in batched_export_requests: - export_result = self._export_with_retries( - split_metrics_data, - deadline_sec, - _count_data_points(split_metrics_data), - ) - if export_result != MetricExportResult.SUCCESS: - return MetricExportResult.FAILURE - - # Only returns SUCCESS if all batches succeeded - return MetricExportResult.SUCCESS - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - self._session.close() - - @property - def _exporting(self) -> str: - return "metrics" - - def force_flush(self, timeout_millis: float = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True - - def set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, - "metrics", - urlparse(self._endpoint), - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) - - -def _count_data_points(export_request: ExportMetricsServiceRequest) -> int: - """Count the number of data points in an encoded metrics export request.""" - count = 0 - for resource_metrics in export_request.resource_metrics: - for scope_metrics in resource_metrics.scope_metrics: - for metric in scope_metrics.metrics: - field_name = metric.WhichOneof("data") - if field_name: - count += len(getattr(metric, field_name).data_points) - return count - - -def _split_metrics_data( - metrics_data: ExportMetricsServiceRequest, - max_export_batch_size: int | None = None, -) -> Iterable[ExportMetricsServiceRequest]: - """Splits metrics data into several ExportMetricsServiceRequest (copies protobuf originals), - based on configured data point max export batch size. - - Args: - metrics_data: metrics object based on HTTP protocol buffer definition - - Returns: - Iterable[ExportMetricsServiceRequest]: An iterable of ExportMetricsServiceRequest objects containing - ExportMetricsServiceRequest.ResourceMetrics, ExportMetricsServiceRequest.ScopeMetrics, ExportMetricsServiceRequest.Metrics, and data points - """ - if not max_export_batch_size: - return metrics_data - - batch_size: int = 0 - # Stores split metrics data as editable references - # used to write batched pb2 objects for export when finalized - split_resource_metrics = [] - - for resource_metrics in metrics_data.resource_metrics: - split_scope_metrics = [] - split_resource_metrics.append( - { - "resource": resource_metrics.resource, - "schema_url": resource_metrics.schema_url, - "scope_metrics": split_scope_metrics, - } - ) - - for scope_metrics in resource_metrics.scope_metrics: - split_metrics = [] - split_scope_metrics.append( - { - "scope": scope_metrics.scope, - "schema_url": scope_metrics.schema_url, - "metrics": split_metrics, - } - ) - - for metric in scope_metrics.metrics: - split_data_points = [] - field_name = metric.WhichOneof("data") - if not field_name: - _logger.warning( - "Tried to split and export an unsupported metric type. Skipping." - ) - continue - - # Get data container using field name - # and build metric dictionary dynamically for conciseness - data_container = getattr(metric, field_name) - metric_dict = { - "name": metric.name, - "description": metric.description, - "unit": metric.unit, - field_name: { - "data_points": split_data_points, - }, - } - if hasattr(data_container, "aggregation_temporality"): - metric_dict[field_name]["aggregation_temporality"] = ( - data_container.aggregation_temporality - ) - if hasattr(data_container, "is_monotonic"): - metric_dict[field_name]["is_monotonic"] = ( - data_container.is_monotonic - ) - split_metrics.append(metric_dict) - - current_data_points = data_container.data_points - for data_point in current_data_points: - split_data_points.append(data_point) - batch_size += 1 - - if batch_size >= max_export_batch_size: - yield ExportMetricsServiceRequest( - resource_metrics=_get_split_resource_metrics_pb2( - split_resource_metrics - ) - ) - - # Reset all the reference variables with current metrics_data position - # minus yielded data_points. Need to clear data_points and keep metric - # to avoid duplicate data_point export - batch_size = 0 - split_data_points = [] - - # Rebuild metric dict generically using same approach as initial creation - field_name = metric.WhichOneof("data") - if field_name is None: - _logger.warning( - "Tried to split and export an unsupported metric type. Skipping." - ) - continue - data_container = getattr(metric, field_name) - metric_dict = { - "name": metric.name, - "description": metric.description, - "unit": metric.unit, - field_name: { - "data_points": split_data_points, - }, - } - if hasattr(data_container, "aggregation_temporality"): - metric_dict[field_name][ - "aggregation_temporality" - ] = data_container.aggregation_temporality - if hasattr(data_container, "is_monotonic"): - metric_dict[field_name]["is_monotonic"] = ( - data_container.is_monotonic - ) - - split_metrics = [metric_dict] - split_scope_metrics = [ - { - "scope": scope_metrics.scope, - "schema_url": scope_metrics.schema_url, - "metrics": split_metrics, - } - ] - split_resource_metrics = [ - { - "resource": resource_metrics.resource, - "schema_url": resource_metrics.schema_url, - "scope_metrics": split_scope_metrics, - } - ] - - if not split_data_points: - # If data_points is empty remove the whole metric - split_metrics.pop() - - if not split_metrics: - # If metrics is empty remove the whole scope_metrics - split_scope_metrics.pop() - - if not split_scope_metrics: - # If scope_metrics is empty remove the whole resource_metrics - split_resource_metrics.pop() - - if batch_size > 0: - yield ExportMetricsServiceRequest( - resource_metrics=_get_split_resource_metrics_pb2( - split_resource_metrics - ) - ) - - -def _get_split_resource_metrics_pb2( - split_resource_metrics: list[dict], -) -> list[pb2.ResourceMetrics]: - """Helper that returns a list of pb2.ResourceMetrics objects based on split_resource_metrics. - Example input: - - ```python - [ - { - "resource": , - "schema_url": "http://foo-bar", - "scope_metrics": [ - "scope": , - "schema_url": "http://foo-baz", - "metrics": [ - { - "name": "apples", - "description": "number of apples purchased", - "sum": { - "aggregation_temporality": 1, - "is_monotonic": "false", - "data_points": [ - { - start_time_unix_nano: 1000 - time_unix_nano: 1001 - exemplars { - time_unix_nano: 1002 - span_id: "foo-span" - trace_id: "foo-trace" - as_int: 5 - } - as_int: 5 - } - ] - } - }, - ], - ], - }, - ] - ``` - - Args: - split_resource_metrics: A list of dict representations of ResourceMetrics, - ScopeMetrics, Metrics, and data points. - - Returns: - List[pb2.ResourceMetrics]: A list of pb2.ResourceMetrics objects containing - pb2.ScopeMetrics, pb2.Metrics, and data points - """ - split_resource_metrics_pb = [] - for resource_metrics in split_resource_metrics: - new_resource_metrics = pb2.ResourceMetrics( - resource=resource_metrics.get("resource"), - scope_metrics=[], - schema_url=resource_metrics.get("schema_url") or "", - ) - for scope_metrics in resource_metrics.get("scope_metrics", []): - new_scope_metrics = pb2.ScopeMetrics( - scope=scope_metrics.get("scope"), - metrics=[], - schema_url=scope_metrics.get("schema_url") or "", - ) - - for metric in scope_metrics.get("metrics", []): - new_metric = None - data_points = [] - - if "sum" in metric: - new_metric = pb2.Metric( - name=metric.get("name"), - description=metric.get("description"), - unit=metric.get("unit"), - sum=pb2.Sum( - data_points=[], - aggregation_temporality=metric.get("sum").get( - "aggregation_temporality" - ), - is_monotonic=metric.get("sum").get("is_monotonic"), - ), - ) - data_points = metric.get("sum").get("data_points") - elif "histogram" in metric: - new_metric = pb2.Metric( - name=metric.get("name"), - description=metric.get("description"), - unit=metric.get("unit"), - histogram=pb2.Histogram( - data_points=[], - aggregation_temporality=metric.get( - "histogram" - ).get("aggregation_temporality"), - ), - ) - data_points = metric.get("histogram").get("data_points") - elif "exponential_histogram" in metric: - new_metric = pb2.Metric( - name=metric.get("name"), - description=metric.get("description"), - unit=metric.get("unit"), - exponential_histogram=pb2.ExponentialHistogram( - data_points=[], - aggregation_temporality=metric.get( - "exponential_histogram" - ).get("aggregation_temporality"), - ), - ) - data_points = metric.get("exponential_histogram").get( - "data_points" - ) - elif "gauge" in metric: - new_metric = pb2.Metric( - name=metric.get("name"), - description=metric.get("description"), - unit=metric.get("unit"), - gauge=pb2.Gauge( - data_points=[], - ), - ) - data_points = metric.get("gauge").get("data_points") - elif "summary" in metric: - new_metric = pb2.Metric( - name=metric.get("name"), - description=metric.get("description"), - unit=metric.get("unit"), - summary=pb2.Summary( - data_points=[], - ), - ) - data_points = metric.get("summary").get("data_points") - else: - _logger.warning( - "Tried to split and export an unsupported metric type. Skipping." - ) - continue - - # Append data points generically using the field name from the metric dict - for field_name in [ - "sum", - "histogram", - "exponential_histogram", - "gauge", - "summary", - ]: - if field_name in metric: - metric_data_container = getattr(new_metric, field_name) - for data_point in data_points: - metric_data_container.data_points.append( - data_point - ) - break - - new_scope_metrics.metrics.append(new_metric) - new_resource_metrics.scope_metrics.append(new_scope_metrics) - split_resource_metrics_pb.append(new_resource_metrics) - return split_resource_metrics_pb - - -@deprecated( - "Use one of the encoders from opentelemetry-exporter-otlp-proto-common instead. Deprecated since version 1.18.0.", -) -def get_resource_data( - sdk_resource_scope_data: dict[SDKResource, Any], # ResourceDataT? - resource_class: Callable[..., PB2Resource], - name: str, -) -> list[PB2Resource]: - return _get_resource_data(sdk_resource_scope_data, resource_class, name) - - -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - -def _append_metrics_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_METRICS_EXPORT_PATH - return endpoint + f"/{DEFAULT_METRICS_EXPORT_PATH}" +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 56d0a92a9e6..d49c83ed91e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -1,330 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -import gzip -import logging -import os -import random -import threading -import zlib -from collections.abc import Sequence -from io import BytesIO -from os import environ -from time import time -from urllib.parse import urlparse +import sys as _sys +import opentelemetry.exporter.otlp._proto.http.trace_exporter as _mod -import requests -from requests.exceptions import ConnectionError - -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - create_exporter_metrics, -) -from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( - encode_spans, -) -from opentelemetry.exporter.otlp.proto.http import ( - _OTLP_HTTP_HEADERS, - Compression, -) -from opentelemetry.exporter.otlp.proto.http._common import ( - _DEFAULT_MAX_REQUEST_SIZE, - RequestPayloadTooLargeError, - _is_request_too_large, - _is_retryable, - _load_session_from_envvar, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) -from opentelemetry.util.re import parse_env_headers - -_logger = logging.getLogger(__name__) - - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_TRACES_EXPORT_PATH = "v1/traces" -DEFAULT_TIMEOUT = 10 # in seconds -_MAX_RETRYS = 6 - - -class OTLPSpanExporter(SpanExporter): - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: dict[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - session: requests.Session | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - ): - """OTLP HTTP span exporter. - - Args: - endpoint: Target URL to which the exporter is going to send spans. - certificate_file: Path to the CA certificate file for TLS. - client_key_file: Path to the client key file for mTLS. - client_certificate_file: Path to the client certificate file for mTLS. - headers: Headers to send with each export request. - timeout: Timeout in seconds for each export request. - compression: Compression to use; one of none, gzip, deflate. - session: Requests session to use at export. - max_request_size: Maximum size in bytes of a serialized request, - measured before compression. A request exceeding this size is - dropped before being sent. Defaults to 64 MiB; a value of 0 (or - any non-positive value) disables the limit. Batch processors - group spans by count rather than serialized size, so a batch - whose serialized request exceeds this limit is dropped as a - whole and recorded as a failed export; reduce the processor's - ``max_export_batch_size`` (or raise/disable this limit) if - batches may approach it. - meter_provider: MeterProvider used for the exporter's own metrics. - """ - self._shutdown_in_progress = threading.Event() - self._endpoint = endpoint or environ.get( - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - _append_trace_path( - environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT) - ), - ) - self._certificate_file = certificate_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True), - ) - self._client_key_file = client_key_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_KEY, None), - ) - self._client_certificate_file = client_certificate_file or environ.get( - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - environ.get(OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, None), - ) - self._client_cert = ( - (self._client_certificate_file, self._client_key_file) - if self._client_certificate_file and self._client_key_file - else self._client_certificate_file - ) - headers_string = environ.get( - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""), - ) - self._headers = headers or parse_env_headers( - headers_string, liberal=True - ) - self._timeout = timeout or float( - environ.get( - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, - environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT), - ) - ) - self._max_request_size = ( - _DEFAULT_MAX_REQUEST_SIZE - if max_request_size is None - else max_request_size - ) - self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER - ) - or requests.Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - # let users override our defaults - self._session.headers.update(self._headers) - if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) - self._shutdown = False - - self._metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse(self._endpoint), - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "") - .strip() - .lower() - == "true", - ) - - def _export( - self, serialized_data: bytes, timeout_sec: float | None = None - ): - data = serialized_data - if self._compression == Compression.Gzip: - gzip_data = BytesIO() - with gzip.GzipFile(fileobj=gzip_data, mode="w") as gzip_stream: - gzip_stream.write(serialized_data) - data = gzip_data.getvalue() - elif self._compression == Compression.Deflate: - data = zlib.compress(serialized_data) - - if timeout_sec is None: - timeout_sec = self._timeout - - # By default, keep-alive is enabled in Session's request - # headers. Backends may choose to close the connection - # while a post happens which causes an unhandled - # exception. This try/except will retry the post on such exceptions - try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, - ) - return resp - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring batch") - return SpanExportResult.FAILURE - - with self._metrics.export_operation(len(spans)) as result: - serialized_data = encode_spans(spans).SerializePartialToString() - if _is_request_too_large(serialized_data, self._max_request_size): - _logger.warning( - "Dropping span batch: serialized size %d bytes exceeds " - "max_request_size %d bytes.", - len(serialized_data), - self._max_request_size, - ) - result.error = RequestPayloadTooLargeError( - f"Serialized span request size {len(serialized_data)} " - f"bytes exceeds max_request_size " - f"{self._max_request_size} bytes." - ) - return SpanExportResult.FAILURE - deadline_sec = time() + self._timeout - for retry_num in range(_MAX_RETRYS): - # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. - backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) - export_error: Exception | None = None - try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: - return SpanExportResult.SUCCESS - except requests.exceptions.RequestException as error: - reason = error - export_error = error - retryable = isinstance(error, ConnectionError) - status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code - - if not retryable: - _logger.error( - "Failed to export span batch code: %s, reason: %s", - status_code, - reason, - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return SpanExportResult.FAILURE - - if ( - retry_num + 1 == _MAX_RETRYS - or backoff_seconds > (deadline_sec - time()) - or self._shutdown - ): - _logger.error( - "Failed to export span batch due to timeout, " - "max retries or shutdown." - ) - error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: status_code} - if status_code is not None - else None - ) - result.error = export_error - result.error_attrs = error_attrs - return SpanExportResult.FAILURE - _logger.warning( - "Transient error %s encountered while exporting span batch, retrying in %.2fs.", - reason, - backoff_seconds, - ) - shutdown = self._shutdown_in_progress.wait(backoff_seconds) - if shutdown: - _logger.warning("Shutdown in progress, aborting retry.") - break - return SpanExportResult.FAILURE - - def shutdown(self): - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._shutdown_in_progress.set() - self._session.close() - - def force_flush(self, timeout_millis: int = 30000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True - - -def _compression_from_env() -> Compression: - compression = ( - environ.get( - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"), - ) - .lower() - .strip() - ) - return Compression(compression) - - -def _append_trace_path(endpoint: str) -> str: - if endpoint.endswith("/"): - return endpoint + DEFAULT_TRACES_EXPORT_PATH - return endpoint + f"/{DEFAULT_TRACES_EXPORT_PATH}" +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py deleted file mode 100644 index 252dab88742..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/encoder/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import logging # noqa: F401 -from collections import abc # noqa: F401 -from collections.abc import Sequence # noqa: F401 - -from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa: F401 - ExportTraceServiceRequest as PB2ExportTraceServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - AnyValue as PB2AnyValue, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - ArrayValue as PB2ArrayValue, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - InstrumentationScope as PB2InstrumentationScope, -) -from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 - KeyValue as PB2KeyValue, -) -from opentelemetry.proto.resource.v1.resource_pb2 import ( # noqa: F401 - Resource as PB2Resource, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - ResourceSpans as PB2ResourceSpans, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - ScopeSpans as PB2ScopeSpans, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - Span as PB2SPan, -) -from opentelemetry.proto.trace.v1.trace_pb2 import ( # noqa: F401 - Status as PB2Status, -) -from opentelemetry.sdk.trace import ( - Event, # noqa: F401 - Resource, # noqa: F401 -) -from opentelemetry.sdk.trace import Span as SDKSpan # noqa: F401 -from opentelemetry.sdk.util.instrumentation import ( # noqa: F401 - InstrumentationScope, -) -from opentelemetry.trace import ( - Link, # noqa: F401 - SpanKind, # noqa: F401 -) -from opentelemetry.trace.span import ( # noqa: F401 - SpanContext, - Status, - TraceState, -) -from opentelemetry.util.types import Attributes # noqa: F401 diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version/__init__.py index 524a0260e55..f6819bcba34 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/version/__init__.py @@ -1,4 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.45.0.dev" +import sys as _sys +import opentelemetry.exporter.otlp._proto.http.version as _mod + +_sys.modules[__name__] = _mod diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py deleted file mode 100644 index 9ead610069e..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ /dev/null @@ -1,1637 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=too-many-lines -import threading -import time -from logging import WARNING -from os import environ -from unittest import TestCase -from unittest.mock import ANY, MagicMock, Mock, patch - -import requests -from requests import Session -from requests.exceptions import ConnectionError -from requests.models import Response - -from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( - encode_metrics, -) -from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_METRICS_EXPORT_PATH, - DEFAULT_TIMEOUT, - OTLPMetricExporter, - _get_split_resource_metrics_pb2, - _split_metrics_data, -) -from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( - ExportMetricsServiceRequest, -) -from opentelemetry.proto.common.v1.common_pb2 import ( - InstrumentationScope, - KeyValue, -) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as Pb2Resource, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, - OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics import ( - Counter, - Histogram, - MeterProvider, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, -) -from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - InMemoryMetricReader, - MetricExportResult, - MetricsData, - ResourceMetrics, - ScopeMetrics, -) -from opentelemetry.sdk.metrics.view import ( - ExplicitBucketHistogramAggregation, - ExponentialBucketHistogramAggregation, -) -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import ( - InstrumentationScope as SDKInstrumentationScope, -) -from opentelemetry.test.metrictestutil import _generate_sum -from opentelemetry.test.mock_test_classes import IterEntryPoint - -OS_ENV_ENDPOINT = "os.env.base" -OS_ENV_CERTIFICATE = "os/env/base.crt" -OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" -OS_ENV_CLIENT_KEY = "os/env/client-key.pem" -OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" -OS_ENV_TIMEOUT = "30" - - -# pylint: disable=protected-access,too-many-public-methods -class TestOTLPMetricExporter(TestCase): - # pylint: disable=too-many-public-methods - def setUp(self): - self.metric_reader = InMemoryMetricReader() - self.meter_provider = MeterProvider( - metric_readers=[self.metric_reader] - ) - self.metrics = { - "sum_int": MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=[_generate_sum("sum_int", 33)], - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ), - } - - def test_max_request_size_default(self): - self.assertEqual( - OTLPMetricExporter()._max_request_size, 64 * 1024 * 1024 - ) - - @patch.object(Session, "post") - def test_oversized_payload_dropped_before_send(self, mock_post): - exporter = OTLPMetricExporter(max_request_size=1) - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - mock_post.assert_not_called() - - @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) - def test_max_request_size_zero_disables(self, _mock_export): - exporter = OTLPMetricExporter(max_request_size=0) - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.SUCCESS, - ) - - @patch.object(Session, "post") - def test_negative_max_request_size_disables_limit(self, mock_post): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - exporter = OTLPMetricExporter(max_request_size=-1) - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.SUCCESS, - ) - mock_post.assert_called() - - @patch.object(Session, "post") - def test_oversized_payload_dropped_with_batch_splitting_enabled( - self, mock_post - ): - # With batch-splitting enabled, the byte check still applies to each - # post-split request, so a too-small limit drops every split before - # sending (an oversized split aborts the batch, like any other - # non-retryable per-split failure). - exporter = OTLPMetricExporter( - max_request_size=1, max_export_batch_size=1 - ) - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - mock_post.assert_not_called() - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_oversized_payload_records_failure_metric(self, mock_post): - exporter = OTLPMetricExporter( - max_request_size=1, meter_provider=self.meter_provider - ) - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - mock_post.assert_not_called() - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - exported = next( - metric - for metric in scope_metrics.metrics - if metric.name == "otel.sdk.exporter.metric_data_point.exported" - ) - self.assertEqual( - exported.data.data_points[0].attributes["error.type"], - "RequestPayloadTooLargeError", - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_split_export_records_per_split_data_point_count(self, mock_post): - # When a batch is split, each split must record its own data-point - # count in the self-observability metric, not the whole-batch count. - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - metrics_data = MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1}, schema_url="resource_schema_url" - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="name", version="version" - ), - metrics=[ - _generate_sum("s1", 1), - _generate_sum("s2", 2), - _generate_sum("s3", 3), - ], - schema_url="scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - exporter = OTLPMetricExporter( - max_export_batch_size=1, meter_provider=self.meter_provider - ) - self.assertEqual( - exporter.export(metrics_data), MetricExportResult.SUCCESS - ) - self.assertEqual(mock_post.call_count, 3) - internal = self.metric_reader.get_metrics_data() - scope_metrics = internal.resource_metrics[0].scope_metrics[0] - exported = next( - metric - for metric in scope_metrics.metrics - if metric.name == "otel.sdk.exporter.metric_data_point.exported" - ) - total = sum(dp.value for dp in exported.data.data_points) - self.assertEqual(total, 3) - - def test_constructor_default(self): - exporter = OTLPMetricExporter() - - self.assertEqual( - exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH - ) - self.assertEqual(exporter._certificate_file, True) - self.assertEqual(exporter._client_certificate_file, None) - self.assertEqual(exporter._client_key_file, None) - self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) - self.assertIs(exporter._compression, DEFAULT_COMPRESSION) - self.assertEqual(exporter._headers, {}) - self.assertIsInstance(exporter._session, Session) - self.assertIn("User-Agent", exporter._session.headers) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "OTel-OTLP-Exporter-Python/" + __version__, - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: "metrics/certificate.env", - OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: "metrics/client-cert.pem", - OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: "metrics/client-key.pem", - OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: Compression.Deflate.value, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", - OTEL_EXPORTER_OTLP_METRICS_HEADERS: "metricsEnv1=val1,metricsEnv2=val2,metricEnv3===val3==,User-agent=metrics-user-agent", - OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "40", - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER: "credential_provider", - }, - ) - @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") - def test_exporter_metrics_env_take_priority(self, mock_entry_points): - credential = Session() - - def f(): - return credential - - mock_entry_points.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - exporter = OTLPMetricExporter() - - self.assertEqual(exporter._endpoint, "https://metrics.endpoint.env") - self.assertEqual(exporter._certificate_file, "metrics/certificate.env") - self.assertEqual( - exporter._client_certificate_file, "metrics/client-cert.pem" - ) - self.assertEqual(exporter._client_key_file, "metrics/client-key.pem") - self.assertEqual(exporter._timeout, 40) - self.assertIs(exporter._compression, Compression.Deflate) - self.assertEqual( - exporter._headers, - { - "metricsenv1": "val1", - "metricsenv2": "val2", - "metricenv3": "==val3==", - "user-agent": "metrics-user-agent", - }, - ) - self.assertIsInstance(exporter._session, Session) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "metrics-user-agent", - ) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "https://metrics.endpoint.env", - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - }, - ) - def test_exporter_constructor_take_priority(self): - exporter = OTLPMetricExporter( - endpoint="example.com/1234", - certificate_file="path/to/service.crt", - client_key_file="path/to/client-key.pem", - client_certificate_file="path/to/client-cert.pem", - headers={"testHeader1": "value1", "testHeader2": "value2"}, - timeout=20, - compression=Compression.NoCompression, - session=Session(), - ) - - self.assertEqual(exporter._endpoint, "example.com/1234") - self.assertEqual(exporter._certificate_file, "path/to/service.crt") - self.assertEqual( - exporter._client_certificate_file, "path/to/client-cert.pem" - ) - self.assertEqual(exporter._client_key_file, "path/to/client-key.pem") - self.assertEqual(exporter._timeout, 20) - self.assertIs(exporter._compression, Compression.NoCompression) - self.assertEqual( - exporter._headers, - {"testHeader1": "value1", "testHeader2": "value2"}, - ) - self.assertIsInstance(exporter._session, Session) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - }, - ) - def test_exporter_env(self): - exporter = OTLPMetricExporter() - - self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) - self.assertEqual( - exporter._client_certificate_file, OS_ENV_CLIENT_CERTIFICATE - ) - self.assertEqual(exporter._client_key_file, OS_ENV_CLIENT_KEY) - self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) - self.assertIs(exporter._compression, Compression.Gzip) - self.assertEqual( - exporter._headers, - { - "envheader1": "val1", - "envheader2": "val2", - "user-agent": "Overridden", - }, - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT}, - ) - def test_exporter_env_endpoint_without_slash(self): - exporter = OTLPMetricExporter() - - self.assertEqual( - exporter._endpoint, - OS_ENV_ENDPOINT + f"/{DEFAULT_METRICS_EXPORT_PATH}", - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"}, - ) - def test_exporter_env_endpoint_with_slash(self): - exporter = OTLPMetricExporter() - - self.assertEqual( - exporter._endpoint, - OS_ENV_ENDPOINT + f"/{DEFAULT_METRICS_EXPORT_PATH}", - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_HEADERS: "envHeader1=val1,envHeader2=val2,missingValue" - }, - ) - def test_headers_parse_from_env(self): - with self.assertLogs(level="WARNING") as cm: - _ = OTLPMetricExporter() - - self.assertEqual( - cm.records[0].message, - ( - "Header format invalid! Header values in environment " - "variables must be URL encoded per the OpenTelemetry " - "Protocol Exporter specification or a comma separated " - "list of name=value occurrences: missingValue" - ), - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "} - ) - @patch.object(Session, "post") - def test_success(self, mock_post): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - - exporter = OTLPMetricExporter() - exporter.set_meter_provider(self.meter_provider) - - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.SUCCESS, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.metric_data_point.exported" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[1].name, "otel.sdk.exporter.metric_data_point.inflight" - ) - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertEqual( - metrics[2].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_failure(self, mock_post): - resp = Response() - resp.status_code = 401 - mock_post.return_value = resp - - exporter = OTLPMetricExporter() - exporter.set_meter_provider(self.meter_provider) - - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.metric_data_point.exported" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[0].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[1].name, "otel.sdk.exporter.metric_data_point.inflight" - ) - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[2].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertEqual( - metrics[2] - .data.data_points[0] - .attributes["http.response.status_code"], - 401, - ) - - @patch.object(Session, "post") - def test_serialization(self, mock_post): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - - exporter = OTLPMetricExporter() - - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.SUCCESS, - ) - - serialized_data = encode_metrics(self.metrics["sum_int"]) - mock_post.assert_called_once_with( - url=exporter._endpoint, - data=serialized_data.SerializeToString(), - verify=exporter._certificate_file, - timeout=ANY, # Timeout is a float based on real time, can't put an exact value here. - cert=exporter._client_cert, - ) - - def test_split_metrics_data_many_data_points(self): - metrics_data = ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ) - split_metrics_data: list[ExportMetricsServiceRequest] = list( - # pylint: disable=protected-access - _split_metrics_data( - metrics_data=metrics_data, - max_export_batch_size=2, - ) - ) - - self.assertEqual( - [ - ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - ], - ), - ], - ), - ], - ), - ] - ), - ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_split_metrics_data_nb_data_points_equal_batch_size(self): - metrics_data = ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ) - - split_metrics_data: list[ExportMetricsServiceRequest] = list( - # pylint: disable=protected-access - _split_metrics_data( - metrics_data=metrics_data, - max_export_batch_size=3, - ) - ) - - self.assertEqual( - [ - ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - _number_data_point(12), - _number_data_point(13), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_split_metrics_data_many_resources_scopes_metrics(self): - # GIVEN - metrics_data = ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - ], - ), - _gauge( - index=2, - data_points=[ - _number_data_point(12), - ], - ), - ], - ), - _scope_metrics( - index=2, - metrics=[ - _gauge( - index=3, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - _resource_metrics( - index=2, - scope_metrics=[ - _scope_metrics( - index=3, - metrics=[ - _gauge( - index=4, - data_points=[ - _number_data_point(14), - ], - ), - ], - ), - ], - ), - ] - ) - - split_metrics_data: list[ExportMetricsServiceRequest] = list( - # pylint: disable=protected-access - _split_metrics_data( - metrics_data=metrics_data, - max_export_batch_size=2, - ) - ) - - self.assertEqual( - [ - ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=1, - metrics=[ - _gauge( - index=1, - data_points=[ - _number_data_point(11), - ], - ), - _gauge( - index=2, - data_points=[ - _number_data_point(12), - ], - ), - ], - ), - ], - ), - ] - ), - ExportMetricsServiceRequest( - resource_metrics=[ - _resource_metrics( - index=1, - scope_metrics=[ - _scope_metrics( - index=2, - metrics=[ - _gauge( - index=3, - data_points=[ - _number_data_point(13), - ], - ), - ], - ), - ], - ), - _resource_metrics( - index=2, - scope_metrics=[ - _scope_metrics( - index=3, - metrics=[ - _gauge( - index=4, - data_points=[ - _number_data_point(14), - ], - ), - ], - ), - ], - ), - ] - ), - ], - split_metrics_data, - ) - - def test_get_split_resource_metrics_pb2_one_of_each(self): - split_resource_metrics = [ - { - "resource": Pb2Resource( - attributes=[ - KeyValue(key="foo", value={"string_value": "bar"}) - ], - ), - "schema_url": "http://foo-bar", - "scope_metrics": [ - { - "scope": InstrumentationScope( - name="foo-scope", version="1.0.0" - ), - "schema_url": "http://foo-baz", - "metrics": [ - { - "name": "foo-metric", - "description": "foo-description", - "unit": "foo-unit", - "sum": { - "aggregation_temporality": 1, - "is_monotonic": True, - "data_points": [ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="dp_key", - value={ - "string_value": "dp_value" - }, - ) - ], - start_time_unix_nano=12345, - time_unix_nano=12350, - as_double=42.42, - ) - ], - }, - } - ], - } - ], - } - ] - - result = _get_split_resource_metrics_pb2(split_resource_metrics) - self.assertEqual(len(result), 1) - self.assertIsInstance(result[0], pb2.ResourceMetrics) - self.assertEqual(result[0].schema_url, "http://foo-bar") - self.assertEqual(len(result[0].scope_metrics), 1) - self.assertEqual(result[0].scope_metrics[0].scope.name, "foo-scope") - self.assertEqual(len(result[0].scope_metrics[0].metrics), 1) - self.assertEqual( - result[0].scope_metrics[0].metrics[0].name, "foo-metric" - ) - self.assertEqual( - result[0].scope_metrics[0].metrics[0].sum.is_monotonic, True - ) - - def test_get_split_resource_metrics_pb2_multiples(self): - split_resource_metrics = [ - { - "resource": Pb2Resource( - attributes=[ - KeyValue(key="foo1", value={"string_value": "bar2"}) - ], - ), - "schema_url": "http://foo-bar-1", - "scope_metrics": [ - { - "scope": InstrumentationScope( - name="foo-scope-1", version="1.0.0" - ), - "schema_url": "http://foo-baz-1", - "metrics": [ - { - "name": "foo-metric-1", - "description": "foo-description-1", - "unit": "foo-unit-1", - "gauge": { - "data_points": [ - pb2.NumberDataPoint( - attributes=[ - KeyValue( - key="dp_key", - value={ - "string_value": "dp_value" - }, - ) - ], - start_time_unix_nano=12345, - time_unix_nano=12350, - as_double=42.42, - ) - ], - }, - } - ], - } - ], - }, - { - "resource": Pb2Resource( - attributes=[ - KeyValue(key="foo2", value={"string_value": "bar2"}) - ], - ), - "schema_url": "http://foo-bar-2", - "scope_metrics": [ - { - "scope": InstrumentationScope( - name="foo-scope-2", version="2.0.0" - ), - "schema_url": "http://foo-baz-2", - "metrics": [ - { - "name": "foo-metric-2", - "description": "foo-description-2", - "unit": "foo-unit-2", - "histogram": { - "aggregation_temporality": 2, - "data_points": [ - pb2.HistogramDataPoint( - attributes=[ - KeyValue( - key="dp_key", - value={ - "string_value": "dp_value" - }, - ) - ], - start_time_unix_nano=12345, - time_unix_nano=12350, - ) - ], - }, - } - ], - } - ], - }, - ] - - result = _get_split_resource_metrics_pb2(split_resource_metrics) - self.assertEqual(len(result), 2) - self.assertEqual(result[0].schema_url, "http://foo-bar-1") - self.assertEqual(result[1].schema_url, "http://foo-bar-2") - self.assertEqual(len(result[0].scope_metrics), 1) - self.assertEqual(len(result[1].scope_metrics), 1) - self.assertEqual(result[0].scope_metrics[0].scope.name, "foo-scope-1") - self.assertEqual(result[1].scope_metrics[0].scope.name, "foo-scope-2") - self.assertEqual( - result[0].scope_metrics[0].metrics[0].name, "foo-metric-1" - ) - self.assertEqual( - result[1].scope_metrics[0].metrics[0].name, "foo-metric-2" - ) - - def test_get_split_resource_metrics_pb2_unsupported_metric_type(self): - split_resource_metrics = [ - { - "resource": Pb2Resource( - attributes=[ - KeyValue(key="foo", value={"string_value": "bar"}) - ], - ), - "schema_url": "http://foo-bar", - "scope_metrics": [ - { - "scope": InstrumentationScope( - name="foo", version="1.0.0" - ), - "schema_url": "http://foo-baz", - "metrics": [ - { - "name": "unsupported-metric", - "description": "foo-bar", - "unit": "foo-bar", - "unsupported_metric_type": {}, - } - ], - } - ], - } - ] - - with self.assertLogs(level="WARNING") as log: - result = _get_split_resource_metrics_pb2(split_resource_metrics) - self.assertEqual(len(result), 1) - self.assertIn( - "Tried to split and export an unsupported metric type", - log.output[0], - ) - - @staticmethod - def _create_metrics_data_multiple_data_points( - num_data_points: int, - ) -> MetricsData: - """Helper to create MetricsData with specified number of data points for testing batch splitting.""" - metrics = [] - for idx in range(num_data_points): - metrics.append(_generate_sum(f"sum_int_{idx}", 33)) - - return MetricsData( - resource_metrics=[ - ResourceMetrics( - resource=Resource( - attributes={"a": 1, "b": False}, - schema_url="resource_schema_url", - ), - scope_metrics=[ - ScopeMetrics( - scope=SDKInstrumentationScope( - name="first_name", - version="first_version", - schema_url="insrumentation_scope_schema_url", - ), - metrics=metrics, - schema_url="instrumentation_scope_schema_url", - ) - ], - schema_url="resource_schema_url", - ) - ] - ) - - @patch.object(Session, "post") - def test_export_max_export_batch_size_single_batch_integration( - self, mock_post - ): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - - # 2 data points, batch size of 3: fits in one batch - metrics_data = ( - TestOTLPMetricExporter._create_metrics_data_multiple_data_points(2) - ) - exporter = OTLPMetricExporter(max_export_batch_size=3) - result = exporter.export(metrics_data) - - self.assertEqual(result, MetricExportResult.SUCCESS) - self.assertEqual(mock_post.call_count, 1) - mock_post.assert_called_once() - - call_args = mock_post.call_args - self.assertEqual(call_args.kwargs["url"], exporter._endpoint) - self.assertIsInstance(call_args.kwargs["data"], bytes) - self.assertEqual( - call_args.kwargs["verify"], exporter._certificate_file - ) - batch_data = call_args.kwargs["data"] - request = ExportMetricsServiceRequest() - request.ParseFromString(batch_data) - self.assertEqual(len(request.resource_metrics), 1) - metrics = request.resource_metrics[0].scope_metrics[0].metrics - self.assertEqual(len(metrics), 2) - metric_names = {metric.name for metric in metrics} - self.assertEqual(metric_names, {"sum_int_0", "sum_int_1"}) - - @patch.object(Session, "post") - def test_export_max_export_batch_size_multiple_batches_integration( - self, mock_post - ): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - - # 3 data points, batch size of 2: requires 2 batches - metrics_data = ( - TestOTLPMetricExporter._create_metrics_data_multiple_data_points(3) - ) - exporter = OTLPMetricExporter(max_export_batch_size=2) - result = exporter.export(metrics_data) - - self.assertEqual(result, MetricExportResult.SUCCESS) - self.assertEqual(mock_post.call_count, 2) - - for call_args in mock_post.call_args_list: - self.assertEqual(call_args.kwargs["url"], exporter._endpoint) - self.assertIsInstance(call_args.kwargs["data"], bytes) - self.assertEqual( - call_args.kwargs["verify"], exporter._certificate_file - ) - self.assertEqual(len(mock_post.call_args_list), 2) - - # First batch should contain sum_int_0 and sum_int_1 - first_batch_data = mock_post.call_args_list[0].kwargs["data"] - first_request = ExportMetricsServiceRequest() - first_request.ParseFromString(first_batch_data) - self.assertEqual(len(first_request.resource_metrics), 1) - first_metrics = ( - first_request.resource_metrics[0].scope_metrics[0].metrics - ) - self.assertEqual(len(first_metrics), 2) - first_metric_names = {metric.name for metric in first_metrics} - self.assertEqual(first_metric_names, {"sum_int_0", "sum_int_1"}) - - # Second batch should contain sum_int_2 - second_batch_data = mock_post.call_args_list[1].kwargs["data"] - second_request = ExportMetricsServiceRequest() - second_request.ParseFromString(second_batch_data) - self.assertEqual(len(second_request.resource_metrics), 1) - second_metrics = ( - second_request.resource_metrics[0].scope_metrics[0].metrics - ) - self.assertEqual(len(second_metrics), 1) - self.assertEqual(second_metrics[0].name, "sum_int_2") - - @patch.object(Session, "post") - def test_export_max_export_batch_size_retry_scenarios_integration( - self, mock_post - ): - # Setup HTTP responses: first request succeeds, second fails non-retryable - success_resp = Response() - success_resp.status_code = 200 - failure_resp = Response() - failure_resp.status_code = 400 - failure_resp.reason = "Bad Request" - mock_post.side_effect = [success_resp, failure_resp] - - # 3 data points, batch size of 2: requires 2 batches - metrics_data = ( - TestOTLPMetricExporter._create_metrics_data_multiple_data_points(3) - ) - exporter = OTLPMetricExporter(max_export_batch_size=2) - - # Export should fail when second batch fails - result = exporter.export(metrics_data) - self.assertEqual(result, MetricExportResult.FAILURE) - self.assertEqual(mock_post.call_count, 2) - - # Verify the content of successful first batch - first_batch_data = mock_post.call_args_list[0].kwargs["data"] - first_request = ExportMetricsServiceRequest() - first_request.ParseFromString(first_batch_data) - self.assertEqual(len(first_request.resource_metrics), 1) - first_metrics = ( - first_request.resource_metrics[0].scope_metrics[0].metrics - ) - self.assertEqual(len(first_metrics), 2) - first_metric_names = {metric.name for metric in first_metrics} - self.assertEqual(first_metric_names, {"sum_int_0", "sum_int_1"}) - - @patch.object(Session, "post") - def test_export_max_export_batch_size_retryable_failure_integration( - self, mock_post - ): - success_resp = Response() - success_resp.status_code = 200 - retryable_failure_resp = Response() - retryable_failure_resp.status_code = 503 - retryable_failure_resp.reason = "Service Unavailable" - mock_post.side_effect = [ - success_resp, - retryable_failure_resp, - success_resp, - ] - - # 3 data points, batch size of 2: requires 2 batches - metrics_data = ( - TestOTLPMetricExporter._create_metrics_data_multiple_data_points(3) - ) - exporter = OTLPMetricExporter(max_export_batch_size=2, timeout=2.0) - - # Export should eventually succeed after retry - result = exporter.export(metrics_data) - self.assertEqual(result, MetricExportResult.SUCCESS) - self.assertEqual( - mock_post.call_count, 3 - ) # First batch + retry of second batch - - first_batch_data = mock_post.call_args_list[0].kwargs["data"] - first_request = ExportMetricsServiceRequest() - first_request.ParseFromString(first_batch_data) - self.assertEqual(len(first_request.resource_metrics), 1) - first_metrics = ( - first_request.resource_metrics[0].scope_metrics[0].metrics - ) - self.assertEqual(len(first_metrics), 2) - first_metric_names = {metric.name for metric in first_metrics} - self.assertEqual(first_metric_names, {"sum_int_0", "sum_int_1"}) - # Second batch (retry) should contain sum_int_2 - second_batch_data = mock_post.call_args_list[2].kwargs["data"] - second_request = ExportMetricsServiceRequest() - second_request.ParseFromString(second_batch_data) - self.assertEqual(len(second_request.resource_metrics), 1) - second_metrics = ( - second_request.resource_metrics[0].scope_metrics[0].metrics - ) - self.assertEqual(len(second_metrics), 1) - self.assertEqual(second_metrics[0].name, "sum_int_2") - - def test_aggregation_temporality(self): - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual(temporality, AggregationTemporality.CUMULATIVE) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "CUMULATIVE"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual( - temporality, AggregationTemporality.CUMULATIVE - ) - - with patch.dict( - environ, {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "ABC"} - ): - with self.assertLogs(level=WARNING): - otlp_metric_exporter = OTLPMetricExporter() - - for ( - temporality - ) in otlp_metric_exporter._preferred_temporality.values(): - self.assertEqual( - temporality, AggregationTemporality.CUMULATIVE - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "DELTA"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Counter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableCounter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ - ObservableUpDownCounter - ], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableGauge], - AggregationTemporality.CUMULATIVE, - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: "LOWMEMORY"}, - ): - otlp_metric_exporter = OTLPMetricExporter() - - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Counter], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[UpDownCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[Histogram], - AggregationTemporality.DELTA, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableCounter], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ - ObservableUpDownCounter - ], - AggregationTemporality.CUMULATIVE, - ) - self.assertEqual( - otlp_metric_exporter._preferred_temporality[ObservableGauge], - AggregationTemporality.CUMULATIVE, - ) - - def test_exponential_explicit_bucket_histogram(self): - self.assertIsInstance( - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - - with patch.dict( - environ, - { - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "base2_exponential_bucket_histogram" - }, - ): - self.assertIsInstance( - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExponentialBucketHistogramAggregation, - ) - - with patch.dict( - environ, - {OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "abc"}, - ): - with self.assertLogs(level=WARNING) as log: - self.assertIsInstance( - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - self.assertIn( - ( - "Invalid value for OTEL_EXPORTER_OTLP_METRICS_DEFAULT_" - "HISTOGRAM_AGGREGATION: abc, using explicit bucket " - "histogram aggregation" - ), - log.output[0], - ) - - with patch.dict( - environ, - { - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: "explicit_bucket_histogram" - }, - ): - self.assertIsInstance( - OTLPMetricExporter()._preferred_aggregation[Histogram], - ExplicitBucketHistogramAggregation, - ) - - @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) - def test_2xx_status_code(self, mock_otlp_metric_exporter): - """ - Test that any HTTP 2XX code returns a successful result - """ - - self.assertEqual( - OTLPMetricExporter().export(MagicMock()), - MetricExportResult.SUCCESS, - ) - - @patch.dict("os.environ", {}, clear=True) - @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) - def test_exporter_metrics_disabled_after_set_meter_provider( - self, _mock_export - ): - exporter = OTLPMetricExporter() - exporter.set_meter_provider(self.meter_provider) - - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.SUCCESS, - ) - - self.assertIsNone(self.metric_reader.get_metrics_data()) - - def test_preferred_aggregation_override(self): - histogram_aggregation = ExplicitBucketHistogramAggregation( - boundaries=[0.05, 0.1, 0.5, 1, 5, 10], - ) - - exporter = OTLPMetricExporter( - preferred_aggregation={ - Histogram: histogram_aggregation, - }, - ) - - self.assertEqual( - exporter._preferred_aggregation[Histogram], histogram_aggregation - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_retry_timeout(self, mock_post): - exporter = OTLPMetricExporter( - timeout=1.5, meter_provider=self.meter_provider - ) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - with self.assertLogs(level=WARNING) as warning: - before = time.time() - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - after = time.time() - - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - self.assertEqual(mock_post.call_count, 2) - # There's a +/-20% jitter on each backoff. - self.assertTrue(0.75 < after - before < 1.25) - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting metrics batch, retrying in", - warning.records[0].message, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.metric_data_point.exported" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[0].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[1].name, "otel.sdk.exporter.metric_data_point.inflight" - ) - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[2].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertEqual( - metrics[2] - .data.data_points[0] - .attributes["http.response.status_code"], - 503, - ) - - @patch.object(Session, "post") - def test_export_no_collector_available_retryable(self, mock_post): - exporter = OTLPMetricExporter(timeout=1.5) - msg = "Server not available." - mock_post.side_effect = ConnectionError(msg) - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - # Check for greater 2 because the request is on each retry - # done twice at the moment. - self.assertGreater(mock_post.call_count, 2) - self.assertIn( - f"Transient error {msg} encountered while exporting metrics batch, retrying in", - warning.records[0].message, - ) - - @patch.object(Session, "post") - def test_export_no_collector_available(self, mock_post): - exporter = OTLPMetricExporter(timeout=1.5) - - mock_post.side_effect = requests.exceptions.RequestException() - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export(self.metrics["sum_int"]), - MetricExportResult.FAILURE, - ) - self.assertEqual(mock_post.call_count, 1) - self.assertIn( - "Failed to export metrics batch code", - warning.records[0].message, - ) - - @patch.object(Session, "post") - def test_timeout_set_correctly(self, mock_post): - resp = Response() - resp.status_code = 200 - - def export_side_effect(*args, **kwargs): - # Timeout should be set to something slightly less than 400 milliseconds depending on how much time has passed. - self.assertAlmostEqual(0.4, kwargs["timeout"], 2) - return resp - - mock_post.side_effect = export_side_effect - exporter = OTLPMetricExporter(timeout=0.4) - exporter.export(self.metrics["sum_int"]) - - @patch.object(Session, "post") - def test_shutdown_interrupts_retry_backoff(self, mock_post): - exporter = OTLPMetricExporter(timeout=1.5) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - thread = threading.Thread( - target=exporter.export, args=(self.metrics["sum_int"],) - ) - with self.assertLogs(level=WARNING) as warning: - before = time.time() - thread.start() - # Wait for the first attempt to fail, then enter a 1 second backoff. - time.sleep(0.05) - # Should cause export to wake up and return. - exporter.shutdown() - thread.join() - after = time.time() - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting metrics batch, retrying in", - warning.records[0].message, - ) - self.assertIn( - "Shutdown in progress, aborting retry.", - warning.records[1].message, - ) - - assert after - before < 0.2 - - def assert_standard_metric_attrs(self, attributes): - self.assertEqual( - attributes["otel.component.type"], "otlp_http_metric_exporter" - ) - self.assertTrue( - attributes["otel.component.name"].startswith( - "otlp_http_metric_exporter/" - ) - ) - self.assertEqual(attributes["server.address"], "localhost") - self.assertEqual(attributes["server.port"], 4318) - - -def _resource_metrics( - index: int, scope_metrics: list[pb2.ScopeMetrics] -) -> pb2.ResourceMetrics: - return pb2.ResourceMetrics( - resource={ - "attributes": [KeyValue(key="a", value={"int_value": index})], - }, - schema_url=f"resource_url_{index}", - scope_metrics=scope_metrics, - ) - - -def _scope_metrics(index: int, metrics: list[pb2.Metric]) -> pb2.ScopeMetrics: - return pb2.ScopeMetrics( - scope=InstrumentationScope(name=f"scope_{index}"), - schema_url=f"scope_url_{index}", - metrics=metrics, - ) - - -def _gauge(index: int, data_points: list[pb2.NumberDataPoint]) -> pb2.Metric: - return pb2.Metric( - name=f"gauge_{index}", - description="description", - unit="unit", - gauge=pb2.Gauge(data_points=data_points), - ) - - -def _number_data_point(value: int) -> pb2.NumberDataPoint: - return pb2.NumberDataPoint( - attributes=[ - KeyValue(key="a", value={"int_value": 1}), - KeyValue(key="b", value={"bool_value": True}), - ], - start_time_unix_nano=1641946015139533244, - time_unix_nano=1641946016139533244, - as_int=value, - ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py deleted file mode 100644 index 3663b0eb9bc..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ /dev/null @@ -1,727 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -# pylint: disable=protected-access - -import threading -import time -import unittest -from logging import WARNING -from unittest.mock import MagicMock, Mock, patch - -import requests -from google.protobuf.json_format import MessageToDict -from requests import Session -from requests.exceptions import ConnectionError -from requests.models import Response - -from opentelemetry._logs import LogRecord, SeverityNumber -from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_LOGS_EXPORT_PATH, - DEFAULT_TIMEOUT, - OTLPLogExporter, -) -from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.sdk._logs import ReadWriteLogRecord -from opentelemetry.sdk._logs.export import LogRecordExportResult -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import InMemoryMetricReader -from opentelemetry.sdk.resources import Resource as SDKResource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.test.mock_test_classes import IterEntryPoint -from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - TraceFlags, - set_span_in_context, -) - -ENV_ENDPOINT = "http://localhost.env:8080/" -ENV_CERTIFICATE = "/etc/base.crt" -ENV_CLIENT_CERTIFICATE = "/etc/client-cert.pem" -ENV_CLIENT_KEY = "/etc/client-key.pem" -ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" -ENV_TIMEOUT = "30" - - -# pylint: disable=too-many-public-methods -class TestOTLPHTTPLogExporter(unittest.TestCase): - def setUp(self): - self.metric_reader = InMemoryMetricReader() - self.meter_provider = MeterProvider( - metric_readers=[self.metric_reader] - ) - - def test_constructor_default(self): - exporter = OTLPLogExporter() - - self.assertEqual( - exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH - ) - self.assertEqual(exporter._certificate_file, True) - self.assertEqual(exporter._client_certificate_file, None) - self.assertEqual(exporter._client_key_file, None) - self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) - self.assertIs(exporter._compression, DEFAULT_COMPRESSION) - self.assertEqual(exporter._headers, {}) - self.assertIsInstance(exporter._session, requests.Session) - self.assertIn("User-Agent", exporter._session.headers) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "OTel-OTLP-Exporter-Python/" + __version__, - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: "logs/certificate.env", - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE: "logs/client-cert.pem", - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY: "logs/client-key.pem", - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: Compression.Deflate.value, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.endpoint.env", - OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==,User-agent=LogsUserAgent", - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "40", - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "credential_provider", - }, - ) - @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") - def test_exporter_logs_env_take_priority(self, mock_entry_points): - credential = Session() - - def f(): - return credential - - mock_entry_points.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - exporter = OTLPLogExporter() - - self.assertEqual(exporter._endpoint, "https://logs.endpoint.env") - self.assertEqual(exporter._certificate_file, "logs/certificate.env") - self.assertEqual( - exporter._client_certificate_file, "logs/client-cert.pem" - ) - self.assertEqual(exporter._client_key_file, "logs/client-key.pem") - self.assertEqual(exporter._timeout, 40) - self.assertIs(exporter._compression, Compression.Deflate) - self.assertEqual( - exporter._headers, - { - "logsenv1": "val1", - "logsenv2": "val2", - "logsenv3": "==val3==", - "user-agent": "LogsUserAgent", - }, - ) - self.assertIs(exporter._session, credential) - self.assertIsInstance(exporter._session, requests.Session) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "LogsUserAgent", - ) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - - @patch.dict( - "os.environ", - { - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "provider_without_entry_point", - }, - ) - @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") - def test_exception_raised_when_entrypoint_returns_wrong_type( - self, mock_entry_points - ): - def f(): - return 1 - - mock_entry_points.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - with self.assertRaises(RuntimeError): - OTLPLogExporter() - - @patch.dict( - "os.environ", - { - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER: "provider_without_entry_point", - }, - ) - def test_exception_raised_when_entrypoint_does_not_exist(self): - with self.assertRaises(RuntimeError): - OTLPLogExporter() - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, - }, - ) - def test_exporter_constructor_take_priority(self): - sess = MagicMock() - exporter = OTLPLogExporter( - endpoint="endpoint.local:69/logs", - certificate_file="/hello.crt", - client_key_file="/client-key.pem", - client_certificate_file="/client-cert.pem", - headers={"testHeader1": "value1", "testHeader2": "value2"}, - timeout=70, - compression=Compression.NoCompression, - session=sess(), - ) - - self.assertEqual(exporter._endpoint, "endpoint.local:69/logs") - self.assertEqual(exporter._certificate_file, "/hello.crt") - self.assertEqual(exporter._client_certificate_file, "/client-cert.pem") - self.assertEqual(exporter._client_key_file, "/client-key.pem") - self.assertEqual(exporter._timeout, 70) - self.assertIs(exporter._compression, Compression.NoCompression) - self.assertEqual( - exporter._headers, - {"testHeader1": "value1", "testHeader2": "value2"}, - ) - self.assertTrue(sess.called) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT, - }, - ) - def test_exporter_env(self): - exporter = OTLPLogExporter() - - self.assertEqual( - exporter._endpoint, ENV_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH - ) - self.assertEqual(exporter._certificate_file, ENV_CERTIFICATE) - self.assertEqual( - exporter._client_certificate_file, ENV_CLIENT_CERTIFICATE - ) - self.assertEqual(exporter._client_key_file, ENV_CLIENT_KEY) - self.assertEqual(exporter._timeout, int(ENV_TIMEOUT)) - self.assertIs(exporter._compression, Compression.Gzip) - self.assertEqual( - exporter._headers, - { - "envheader1": "val1", - "envheader2": "val2", - "user-agent": "Overridden", - }, - ) - self.assertIsInstance(exporter._session, requests.Session) - - @staticmethod - def export_log_and_deserialize(log): - with patch("requests.Session.post") as mock_post: - exporter = OTLPLogExporter() - exporter.export([log]) - request_body = mock_post.call_args[1]["data"] - request = ExportLogsServiceRequest() - request.ParseFromString(request_body) - request_dict = MessageToDict(request) - log_records = ( - request_dict.get("resourceLogs")[0] - .get("scopeLogs")[0] - .get("logRecords") - ) - return log_records - - def test_exported_log_without_trace_id(self): - ctx = set_span_in_context( - NonRecordingSpan( - SpanContext( - 0, - 1312458408527513292, - False, - TraceFlags(0x01), - ) - ) - ) - log = ReadWriteLogRecord( - LogRecord( - timestamp=1644650195189786182, - context=ctx, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Invalid trace id check", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"first_resource": "value"}), - instrumentation_scope=InstrumentationScope("name", "version"), - ) - log_records = TestOTLPHTTPLogExporter.export_log_and_deserialize(log) - if log_records: - log_record = log_records[0] - self.assertIn("spanId", log_record) - self.assertNotIn( - "traceId", - log_record, - "trace_id should not be present in the log record", - ) - else: - self.fail("No log records found") - - def test_exported_log_without_span_id(self): - ctx = set_span_in_context( - NonRecordingSpan( - SpanContext( - 89564621134313219400156819398935297696, - 0, - False, - TraceFlags(0x01), - ) - ) - ) - - log = ReadWriteLogRecord( - LogRecord( - timestamp=1644650195189786360, - context=ctx, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Invalid span id check", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"first_resource": "value"}), - instrumentation_scope=InstrumentationScope("name", "version"), - ) - log_records = TestOTLPHTTPLogExporter.export_log_and_deserialize(log) - if log_records: - log_record = log_records[0] - self.assertIn("traceId", log_record) - self.assertNotIn( - "spanId", - log_record, - "spanId should not be present in the log record", - ) - else: - self.fail("No log records found") - - @staticmethod - def _get_sdk_log_data() -> list[ReadWriteLogRecord]: - ctx_log1 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 89564621134313219400156819398935297684, - 1312458408527513268, - False, - TraceFlags(0x01), - ) - ) - ) - log1 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650195189786880, - context=ctx_log1, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Do not go gentle into that good night. Rage, rage against the dying of the light", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"first_resource": "value"}), - instrumentation_scope=InstrumentationScope( - "first_name", "first_version" - ), - ) - - ctx_log2 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 0, - 0, - False, - ) - ) - ) - log2 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650249738562048, - context=ctx_log2, - severity_text="WARN", - severity_number=SeverityNumber.WARN, - body="Cooper, this is no time for caution!", - attributes={}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=InstrumentationScope( - "second_name", "second_version" - ), - ) - ctx_log3 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 271615924622795969659406376515024083555, - 4242561578944770265, - False, - TraceFlags(0x01), - ) - ) - ) - log3 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650427658989056, - context=ctx_log3, - severity_text="DEBUG", - severity_number=SeverityNumber.DEBUG, - body="To our galaxy", - attributes={"a": 1, "b": "c"}, - ), - resource=SDKResource({"second_resource": "CASE"}), - instrumentation_scope=None, - ) - ctx_log4 = set_span_in_context( - NonRecordingSpan( - SpanContext( - 212592107417388365804938480559624925555, - 6077757853989569223, - False, - TraceFlags(0x01), - ) - ) - ) - log4 = ReadWriteLogRecord( - LogRecord( - timestamp=1644650584292683008, - context=ctx_log4, - severity_text="INFO", - severity_number=SeverityNumber.INFO, - body="Love is the one thing that transcends time and space", - attributes={"filename": "model.py", "func_name": "run_method"}, - ), - resource=SDKResource({"first_resource": "value"}), - instrumentation_scope=InstrumentationScope( - "another_name", "another_version" - ), - ) - - return [log1, log2, log3, log4] - - @patch.object(OTLPLogExporter, "_export", return_value=Mock(ok=True)) - def test_2xx_status_code(self, mock_otlp_metric_exporter): - """ - Test that any HTTP 2XX code returns a successful result - """ - - self.assertEqual( - OTLPLogExporter().export(MagicMock()), - LogRecordExportResult.SUCCESS, - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "} - ) - @patch.object(Session, "post") - def test_retry_timeout(self, mock_post): - exporter = OTLPLogExporter( - timeout=1.5, meter_provider=self.meter_provider - ) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - with self.assertLogs(level=WARNING) as warning: - before = time.time() - # Set timeout to 1.5 seconds - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.FAILURE, - ) - after = time.time() - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - self.assertEqual(mock_post.call_count, 2) - # There's a +/-20% jitter on each backoff. - self.assertTrue(0.75 < after - before < 1.25) - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting logs batch, retrying in", - warning.records[0].message, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual(metrics[0].name, "otel.sdk.exporter.log.exported") - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[0].data.data_points[0].attributes, - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.log.inflight") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[2].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertEqual( - metrics[2] - .data.data_points[0] - .attributes["http.response.status_code"], - 503, - ) - - @patch.object(Session, "post") - def test_export_no_collector_available_retryable(self, mock_post): - exporter = OTLPLogExporter(timeout=1.5) - msg = "Server not available." - mock_post.side_effect = ConnectionError(msg) - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.FAILURE, - ) - # Check for greater 2 because the request is on each retry - # done twice at the moment. - self.assertGreater(mock_post.call_count, 2) - self.assertIn( - f"Transient error {msg} encountered while exporting logs batch, retrying in", - warning.records[0].message, - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_export_no_collector_available(self, mock_post): - exporter = OTLPLogExporter( - timeout=1.5, meter_provider=self.meter_provider - ) - - mock_post.side_effect = requests.exceptions.RequestException() - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.FAILURE, - ) - self.assertEqual(mock_post.call_count, 1) - self.assertIn( - "Failed to export logs batch code", - warning.records[0].message, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual(metrics[0].name, "otel.sdk.exporter.log.exported") - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[0].data.data_points[0].attributes["error.type"], - "RequestException", - ) - self.assertNotIn( - "http.response.status_code", - metrics[0].data.data_points[0].attributes, - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.log.inflight") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual( - metrics[2].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertEqual( - metrics[2].data.data_points[0].attributes["error.type"], - "RequestException", - ) - self.assertNotIn( - "http.response.status_code", - metrics[2].data.data_points[0].attributes, - ) - - @patch.object(Session, "post") - def test_timeout_set_correctly(self, mock_post): - resp = Response() - resp.status_code = 200 - - def export_side_effect(*args, **kwargs): - # Timeout should be set to something slightly less than 400 milliseconds depending on how much time has passed. - self.assertAlmostEqual(0.4, kwargs["timeout"], 2) - return resp - - mock_post.side_effect = export_side_effect - exporter = OTLPLogExporter(timeout=0.4) - exporter.export(self._get_sdk_log_data()) - - @patch.object(Session, "post") - def test_shutdown_interrupts_retry_backoff(self, mock_post): - exporter = OTLPLogExporter(timeout=1.5) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - thread = threading.Thread( - target=exporter.export, args=(self._get_sdk_log_data(),) - ) - with self.assertLogs(level=WARNING) as warning: - before = time.time() - thread.start() - # Wait for the first attempt to fail, then enter a 1 second backoff. - time.sleep(0.05) - # Should cause export to wake up and return. - exporter.shutdown() - thread.join() - after = time.time() - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting logs batch, retrying in", - warning.records[0].message, - ) - self.assertIn( - "Shutdown in progress, aborting retry.", - warning.records[1].message, - ) - - assert after - before < 0.2 - - def test_max_request_size_default(self): - self.assertEqual(OTLPLogExporter()._max_request_size, 64 * 1024 * 1024) - - @patch.object(Session, "post") - def test_oversized_payload_dropped_before_send(self, mock_post): - exporter = OTLPLogExporter(max_request_size=1) - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.FAILURE, - ) - mock_post.assert_not_called() - - @patch.object(OTLPLogExporter, "_export", return_value=Mock(ok=True)) - def test_max_request_size_zero_disables(self, _mock_export): - exporter = OTLPLogExporter(max_request_size=0) - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.SUCCESS, - ) - - @patch.object(Session, "post") - def test_negative_max_request_size_disables_limit(self, mock_post): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - exporter = OTLPLogExporter(max_request_size=-1) - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.SUCCESS, - ) - mock_post.assert_called() - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_oversized_payload_records_failure_metric(self, mock_post): - exporter = OTLPLogExporter( - max_request_size=1, meter_provider=self.meter_provider - ) - self.assertEqual( - exporter.export(self._get_sdk_log_data()), - LogRecordExportResult.FAILURE, - ) - mock_post.assert_not_called() - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - exported = next( - metric - for metric in scope_metrics.metrics - if metric.name == "otel.sdk.exporter.log.exported" - ) - self.assertEqual( - exported.data.data_points[0].attributes["error.type"], - "RequestPayloadTooLargeError", - ) - - def assert_standard_metric_attrs(self, attributes): - self.assertEqual( - attributes["otel.component.type"], "otlp_http_log_exporter" - ) - self.assertTrue( - attributes["otel.component.name"].startswith( - "otlp_http_log_exporter/" - ) - ) - self.assertEqual(attributes["server.address"], "localhost") - self.assertEqual(attributes["server.port"], 4318) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py deleted file mode 100644 index fa8b5fc1f44..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ /dev/null @@ -1,629 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import gzip -import threading -import time -import unittest -from http.server import BaseHTTPRequestHandler, HTTPServer -from logging import WARNING -from unittest.mock import MagicMock, Mock, patch - -import requests -from requests import Session -from requests.exceptions import ConnectionError -from requests.models import Response - -from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( - encode_spans, -) -from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( - DEFAULT_COMPRESSION, - DEFAULT_ENDPOINT, - DEFAULT_TIMEOUT, - DEFAULT_TRACES_EXPORT_PATH, - OTLPSpanExporter, -) -from opentelemetry.exporter.otlp.proto.http.version import __version__ -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION, - OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY, - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, - OTEL_EXPORTER_OTLP_TRACES_HEADERS, - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import InMemoryMetricReader -from opentelemetry.sdk.trace import _Span -from opentelemetry.sdk.trace.export import SpanExportResult -from opentelemetry.test.mock_test_classes import IterEntryPoint - -OS_ENV_ENDPOINT = "os.env.base" -OS_ENV_CERTIFICATE = "os/env/base.crt" -OS_ENV_CLIENT_CERTIFICATE = "os/env/client-cert.pem" -OS_ENV_CLIENT_KEY = "os/env/client-key.pem" -OS_ENV_HEADERS = "envHeader1=val1,envHeader2=val2,User-agent=Overridden" -OS_ENV_TIMEOUT = "30" -BASIC_SPAN = _Span( - "abc", - context=Mock( - **{ - "trace_state": {"a": "b", "c": "d"}, - "span_id": 10217189687419569865, - "trace_id": 67545097771067222548457157018666467027, - } - ), -) - - -class _RecordingOTLPHandler(BaseHTTPRequestHandler): - # do_POST is the required override name from BaseHTTPRequestHandler. - def do_POST(self): # pylint: disable=invalid-name - content_length = int(self.headers.get("Content-Length", 0)) - self.server.received_bodies.append(self.rfile.read(content_length)) - self.send_response(200) - self.end_headers() - - def log_message(self, *args): # silence the test server's stderr logging - pass - - -def _start_recording_server(): - server = HTTPServer(("127.0.0.1", 0), _RecordingOTLPHandler) - server.received_bodies = [] - thread = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - return server, thread - - -# pylint: disable=protected-access,too-many-public-methods -class TestOTLPSpanExporter(unittest.TestCase): - def setUp(self): - self.metric_reader = InMemoryMetricReader() - self.meter_provider = MeterProvider( - metric_readers=[self.metric_reader] - ) - - def test_constructor_default(self): - exporter = OTLPSpanExporter() - - self.assertEqual( - exporter._endpoint, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH - ) - self.assertEqual(exporter._certificate_file, True) - self.assertEqual(exporter._client_certificate_file, None) - self.assertEqual(exporter._client_key_file, None) - self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) - self.assertIs(exporter._compression, DEFAULT_COMPRESSION) - self.assertEqual(exporter._headers, {}) - self.assertIsInstance(exporter._session, requests.Session) - self.assertIn("User-Agent", exporter._session.headers) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "OTel-OTLP-Exporter-Python/" + __version__, - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "traces/certificate.env", - OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "traces/client-cert.pem", - OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "traces/client-key.pem", - OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: Compression.Deflate.value, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", - OTEL_EXPORTER_OTLP_TRACES_HEADERS: "tracesEnv1=val1,tracesEnv2=val2,traceEnv3===val3==,User-agent=TraceUserAgent", - OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "40", - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER: "credential_provider", - }, - ) - @patch("opentelemetry.exporter.otlp.proto.http._common.entry_points") - def test_exporter_traces_env_take_priority(self, mock_entry_point): - credential = Session() - - def f(): - return credential - - mock_entry_point.configure_mock( - return_value=[IterEntryPoint("custom_credential", f)] - ) - exporter = OTLPSpanExporter() - - self.assertEqual(exporter._endpoint, "https://traces.endpoint.env") - self.assertEqual(exporter._certificate_file, "traces/certificate.env") - self.assertEqual( - exporter._client_certificate_file, "traces/client-cert.pem" - ) - self.assertEqual(exporter._client_key_file, "traces/client-key.pem") - self.assertEqual(exporter._timeout, 40) - self.assertIs(exporter._compression, Compression.Deflate) - self.assertEqual( - exporter._headers, - { - "tracesenv1": "val1", - "tracesenv2": "val2", - "traceenv3": "==val3==", - "user-agent": "TraceUserAgent", - }, - ) - self.assertIs(exporter._session, credential) - self.assertIsInstance(exporter._session, requests.Session) - self.assertEqual( - exporter._session.headers.get("Content-Type"), - "application/x-protobuf", - ) - self.assertEqual( - exporter._session.headers.get("User-Agent"), - "TraceUserAgent", - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT, - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.endpoint.env", - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - }, - ) - def test_exporter_constructor_take_priority(self): - exporter = OTLPSpanExporter( - endpoint="example.com/1234", - certificate_file="path/to/service.crt", - client_key_file="path/to/client-key.pem", - client_certificate_file="path/to/client-cert.pem", - headers={"testHeader1": "value1", "testHeader2": "value2"}, - timeout=20, - compression=Compression.NoCompression, - session=requests.Session(), - ) - - self.assertEqual(exporter._endpoint, "example.com/1234") - self.assertEqual(exporter._certificate_file, "path/to/service.crt") - self.assertEqual( - exporter._client_certificate_file, "path/to/client-cert.pem" - ) - self.assertEqual(exporter._client_key_file, "path/to/client-key.pem") - self.assertEqual(exporter._timeout, 20) - self.assertIs(exporter._compression, Compression.NoCompression) - self.assertEqual( - exporter._headers, - {"testHeader1": "value1", "testHeader2": "value2"}, - ) - self.assertIsInstance(exporter._session, requests.Session) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_CERTIFICATE: OS_ENV_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: OS_ENV_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_CLIENT_KEY: OS_ENV_CLIENT_KEY, - OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value, - OTEL_EXPORTER_OTLP_HEADERS: OS_ENV_HEADERS, - OTEL_EXPORTER_OTLP_TIMEOUT: OS_ENV_TIMEOUT, - }, - ) - def test_exporter_env(self): - exporter = OTLPSpanExporter() - - self.assertEqual(exporter._certificate_file, OS_ENV_CERTIFICATE) - self.assertEqual( - exporter._client_certificate_file, OS_ENV_CLIENT_CERTIFICATE - ) - self.assertEqual(exporter._client_key_file, OS_ENV_CLIENT_KEY) - self.assertEqual(exporter._timeout, int(OS_ENV_TIMEOUT)) - self.assertIs(exporter._compression, Compression.Gzip) - self.assertEqual( - exporter._headers, - { - "envheader1": "val1", - "envheader2": "val2", - "user-agent": "Overridden", - }, - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT}, - ) - def test_exporter_env_endpoint_without_slash(self): - exporter = OTLPSpanExporter() - - self.assertEqual( - exporter._endpoint, - OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}", - ) - - @patch.dict( - "os.environ", - {OTEL_EXPORTER_OTLP_ENDPOINT: OS_ENV_ENDPOINT + "/"}, - ) - def test_exporter_env_endpoint_with_slash(self): - exporter = OTLPSpanExporter() - - self.assertEqual( - exporter._endpoint, - OS_ENV_ENDPOINT + f"/{DEFAULT_TRACES_EXPORT_PATH}", - ) - - @patch.dict( - "os.environ", - { - OTEL_EXPORTER_OTLP_HEADERS: "envHeader1=val1,envHeader2=val2,missingValue" - }, - ) - def test_headers_parse_from_env(self): - with self.assertLogs(level="WARNING") as cm: - _ = OTLPSpanExporter() - - self.assertEqual( - cm.records[0].message, - ( - "Header format invalid! Header values in environment " - "variables must be URL encoded per the OpenTelemetry " - "Protocol Exporter specification or a comma separated " - "list of name=value occurrences: missingValue" - ), - ) - - @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) - def test_2xx_status_code(self, mock_otlp_metric_exporter): - """ - Test that any HTTP 2XX code returns a successful result - """ - - self.assertEqual( - OTLPSpanExporter().export(MagicMock()), SpanExportResult.SUCCESS - ) - - @patch.dict("os.environ", {}, clear=True) - @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) - def test_exporter_metrics_disabled_by_default(self, _mock_export): - exporter = OTLPSpanExporter(meter_provider=self.meter_provider) - - self.assertEqual( - exporter.export([BASIC_SPAN]), SpanExportResult.SUCCESS - ) - - self.assertIsNone(self.metric_reader.get_metrics_data()) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "} - ) - @patch.object(Session, "post") - def test_retry_timeout(self, mock_post): - exporter = OTLPSpanExporter( - timeout=1.5, meter_provider=self.meter_provider - ) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - with self.assertLogs(level=WARNING) as warning: - before = time.time() - # Set timeout to 1.5 seconds - self.assertEqual( - exporter.export([BASIC_SPAN]), - SpanExportResult.FAILURE, - ) - after = time.time() - # First call at time 0, second at time 1, then an early return before the second backoff sleep b/c it would exceed timeout. - self.assertEqual(mock_post.call_count, 2) - # There's a +/-20% jitter on each backoff. - self.assertTrue(0.75 < after - before < 1.25) - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting span batch, retrying in", - warning.records[0].message, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[0] - .data.data_points[0] - .attributes["http.response.status_code"], - 503, - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[1].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[2].data.data_points[0].attributes, - ) - - @patch.object(Session, "post") - def test_export_no_collector_available_retryable(self, mock_post): - exporter = OTLPSpanExporter(timeout=1.5) - msg = "Server not available." - mock_post.side_effect = ConnectionError(msg) - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export([BASIC_SPAN]), - SpanExportResult.FAILURE, - ) - # Check for greater 2 because the request is on each retry - # done twice at the moment. - self.assertGreater(mock_post.call_count, 2) - self.assertIn( - f"Transient error {msg} encountered while exporting span batch, retrying in", - warning.records[0].message, - ) - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_export_no_collector_available(self, mock_post): - exporter = OTLPSpanExporter( - timeout=1.5, meter_provider=self.meter_provider - ) - - mock_post.side_effect = requests.exceptions.RequestException() - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - exporter.export([BASIC_SPAN]), - SpanExportResult.FAILURE, - ) - self.assertEqual(mock_post.call_count, 1) - self.assertIn( - "Failed to export span batch code", - warning.records[0].message, - ) - - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - self.assertEqual(scope_metrics.scope.name, "opentelemetry-sdk") - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) - self.assertEqual(len(metrics), 3) - self.assertEqual( - metrics[0].name, "otel.sdk.exporter.operation.duration" - ) - self.assert_standard_metric_attrs( - metrics[0].data.data_points[0].attributes - ) - self.assertEqual( - metrics[0].data.data_points[0].attributes["error.type"], - "RequestException", - ) - self.assertNotIn( - "http.response.status_code", - metrics[0].data.data_points[0].attributes, - ) - self.assertEqual(metrics[1].name, "otel.sdk.exporter.span.exported") - self.assert_standard_metric_attrs( - metrics[1].data.data_points[0].attributes - ) - self.assertEqual( - metrics[1].data.data_points[0].attributes["error.type"], - "RequestException", - ) - self.assertNotIn( - "http.response.status_code", - metrics[1].data.data_points[0].attributes, - ) - self.assertEqual(metrics[2].name, "otel.sdk.exporter.span.inflight") - self.assert_standard_metric_attrs( - metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "error.type", metrics[2].data.data_points[0].attributes - ) - self.assertNotIn( - "http.response.status_code", - metrics[2].data.data_points[0].attributes, - ) - - @patch.object(Session, "post") - def test_timeout_set_correctly(self, mock_post): - resp = Response() - resp.status_code = 200 - - def export_side_effect(*args, **kwargs): - # Timeout should be set to something slightly less than 400 milliseconds depending on how much time has passed. - self.assertAlmostEqual(0.4, kwargs["timeout"], 2) - return resp - - mock_post.side_effect = export_side_effect - exporter = OTLPSpanExporter(timeout=0.4) - exporter.export([BASIC_SPAN]) - - @patch.object(Session, "post") - def test_shutdown_interrupts_retry_backoff(self, mock_post): - exporter = OTLPSpanExporter(timeout=1.5) - - resp = Response() - resp.status_code = 503 - resp.reason = "UNAVAILABLE" - mock_post.return_value = resp - thread = threading.Thread(target=exporter.export, args=([BASIC_SPAN],)) - with self.assertLogs(level=WARNING) as warning: - before = time.time() - thread.start() - # Wait for the first attempt to fail, then enter a 1 second backoff. - time.sleep(0.05) - # Should cause export to wake up and return. - exporter.shutdown() - thread.join() - after = time.time() - self.assertIn( - "Transient error UNAVAILABLE encountered while exporting span batch, retrying in", - warning.records[0].message, - ) - self.assertIn( - "Shutdown in progress, aborting retry.", - warning.records[1].message, - ) - - assert after - before < 0.2 - - def test_max_request_size_default(self): - exporter = OTLPSpanExporter() - self.assertEqual(exporter._max_request_size, 64 * 1024 * 1024) - - @patch.object(Session, "post") - def test_oversized_payload_dropped_before_send(self, mock_post): - exporter = OTLPSpanExporter(max_request_size=1) - self.assertEqual( - exporter.export([BASIC_SPAN]), SpanExportResult.FAILURE - ) - mock_post.assert_not_called() - - @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) - def test_max_request_size_zero_disables(self, _mock_export): - exporter = OTLPSpanExporter(max_request_size=0) - self.assertEqual( - exporter.export([BASIC_SPAN]), SpanExportResult.SUCCESS - ) - - @patch.object(Session, "post") - def test_negative_max_request_size_disables_limit(self, mock_post): - resp = Response() - resp.status_code = 200 - mock_post.return_value = resp - exporter = OTLPSpanExporter(max_request_size=-1) - self.assertEqual( - exporter.export([BASIC_SPAN]), SpanExportResult.SUCCESS - ) - mock_post.assert_called() - - @patch.object(Session, "post") - def test_oversized_payload_measured_before_compression(self, mock_post): - # The limit applies to the uncompressed serialized request. Build a - # highly compressible batch whose gzip size is below a limit that the - # uncompressed size still exceeds, then assert it is still dropped -- - # which can only hold if size is measured before compression. - spans = [BASIC_SPAN] * 200 - uncompressed = encode_spans(spans).SerializePartialToString() - compressed = gzip.compress(uncompressed) - limit = (len(compressed) + len(uncompressed)) // 2 - # Guard the discriminating condition: compressed < limit < uncompressed. - self.assertLess(len(compressed), limit) - self.assertLess(limit, len(uncompressed)) - exporter = OTLPSpanExporter( - max_request_size=limit, compression=Compression.Gzip - ) - self.assertEqual(exporter.export(spans), SpanExportResult.FAILURE) - mock_post.assert_not_called() - - @patch.dict( - "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} - ) - @patch.object(Session, "post") - def test_oversized_payload_records_failure_metric(self, mock_post): - exporter = OTLPSpanExporter( - max_request_size=1, meter_provider=self.meter_provider - ) - self.assertEqual( - exporter.export([BASIC_SPAN]), SpanExportResult.FAILURE - ) - mock_post.assert_not_called() - metrics_data = self.metric_reader.get_metrics_data() - scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - exported = next( - metric - for metric in scope_metrics.metrics - if metric.name == "otel.sdk.exporter.span.exported" - ) - self.assertEqual( - exported.data.data_points[0].attributes["error.type"], - "RequestPayloadTooLargeError", - ) - - def test_end_to_end_export_sends_request_over_http(self): - server, thread = _start_recording_server() - port = server.server_address[1] - try: - exporter = OTLPSpanExporter( - endpoint=f"http://127.0.0.1:{port}/v1/traces" - ) - result = exporter.export([BASIC_SPAN]) - finally: - server.shutdown() - thread.join() - server.server_close() - self.assertEqual(result, SpanExportResult.SUCCESS) - self.assertEqual(len(server.received_bodies), 1) - self.assertGreater(len(server.received_bodies[0]), 0) - - def test_end_to_end_oversized_request_never_reaches_server(self): - server, thread = _start_recording_server() - port = server.server_address[1] - try: - exporter = OTLPSpanExporter( - endpoint=f"http://127.0.0.1:{port}/v1/traces", - max_request_size=1, - ) - result = exporter.export([BASIC_SPAN]) - finally: - server.shutdown() - thread.join() - server.server_close() - self.assertEqual(result, SpanExportResult.FAILURE) - self.assertEqual(server.received_bodies, []) - - def assert_standard_metric_attrs(self, attributes): - self.assertEqual( - attributes["otel.component.type"], "otlp_http_span_exporter" - ) - self.assertTrue( - attributes["otel.component.name"].startswith( - "otlp_http_span_exporter/" - ) - ) - self.assertEqual(attributes["server.address"], "localhost") - self.assertEqual(attributes["server.port"], 4318) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_common/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_common/test___init__.py new file mode 100644 index 00000000000..3af32ecb852 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_common/test___init__.py @@ -0,0 +1,127 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import ssl +import warnings +from unittest.mock import Mock, patch +from urllib.error import URLError +from urllib.request import HTTPSHandler, build_opener + +import pytest + +from opentelemetry.exporter.otlp.proto.http._common import ( + _is_retryable, + _load_provider_from_envvar, + _resolve_client, +) + + +# ── _is_retryable ───────────────────────────────────────────────────────────── + +def test_is_retryable_408(): + assert _is_retryable(408) is True + + +def test_is_retryable_500(): + assert _is_retryable(500) is True + + +def test_is_retryable_503(): + assert _is_retryable(503) is True + + +def test_is_retryable_599(): + assert _is_retryable(599) is True + + +def test_is_retryable_200(): + assert _is_retryable(200) is False + + +def test_is_retryable_404(): + assert _is_retryable(404) is False + + +def test_is_retryable_400(): + assert _is_retryable(400) is False + + +# ── _load_provider_from_envvar ──────────────────────────────────────────────── + +_CRED_ENVVAR = "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER" +_GENERIC_ENVVAR = "OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER" + + +def test_load_provider_returns_none_when_no_env_var(): + with patch.dict("os.environ", {}, clear=True): + assert _load_provider_from_envvar(_CRED_ENVVAR) is None + + +def test_load_provider_raises_on_unknown_provider(): + with patch.dict("os.environ", {_GENERIC_ENVVAR: "nonexistent_provider"}): + with pytest.raises(RuntimeError, match="not found in entry point"): + _load_provider_from_envvar(_CRED_ENVVAR) + + +def test_load_provider_returns_value_from_provider(): + sentinel = Mock() + mock_ep = Mock() + mock_ep.load.return_value = lambda: sentinel + with patch.dict("os.environ", {_GENERIC_ENVVAR: "my_provider"}): + with patch( + "opentelemetry.exporter.otlp.proto.http._common.entry_points", + return_value=iter([mock_ep]), + ): + assert _load_provider_from_envvar(_CRED_ENVVAR) is sentinel + + +# ── _resolve_client ─────────────────────────────────────────────────────────── + +_CTX = ssl.create_default_context() + + +def test_resolve_client_default_is_opener_without_warning(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + with patch.dict("os.environ", {}, clear=True): + client = _resolve_client(None, _CRED_ENVVAR, _CTX) + assert callable(client) + + +def test_resolve_client_accepts_opener_director_without_warning(): + opener = build_opener(HTTPSHandler(context=_CTX)) + with warnings.catch_warnings(): + warnings.simplefilter("error") + client = _resolve_client(opener, _CRED_ENVVAR, _CTX) + assert callable(client) + + +def test_resolve_client_session_is_deprecated_but_routed(): + class _Resp: + status_code = 200 + reason = "OK" + + session = Mock() + session.post.return_value = _Resp() + with pytest.warns(DeprecationWarning): + client = _resolve_client(session, _CRED_ENVVAR, _CTX) + status, reason = client("http://endpoint", b"data", {"k": "v"}, 3.0) + assert (status, reason) == (200, "OK") + session.post.assert_called_once_with( + "http://endpoint", data=b"data", headers={"k": "v"}, timeout=3.0 + ) + + +def test_resolve_client_session_transport_error_becomes_urlerror(): + session = Mock() + session.post.side_effect = OSError("boom") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + client = _resolve_client(session, _CRED_ENVVAR, _CTX) + with pytest.raises(URLError): + client("http://endpoint", b"data", {}, 1.0) + + +def test_resolve_client_rejects_invalid_injectable(): + with pytest.raises(RuntimeError, match="OpenerDirector"): + _resolve_client(object(), _CRED_ENVVAR, _CTX) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_log_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_log_exporter/test___init__.py new file mode 100644 index 00000000000..2f6a44bc86f --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/_log_exporter/test___init__.py @@ -0,0 +1,219 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import patch +from urllib.error import URLError + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + DEFAULT_ENDPOINT, + DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPLogExporter, + _append_logs_path, +) +from opentelemetry.sdk._logs.export import LogRecordExportResult + +_UNIFORM = "opentelemetry.exporter.otlp.proto.http._log_exporter.uniform" + + +def _ok(): + return (200, "OK") + + +def _503(): + return (503, "Service Unavailable") + + +def _404(): + return (404, "Not Found") + + +class TestOTLPLogExporter(unittest.TestCase): + + # ── constructor ─────────────────────────────────────────────────────────── + + def test_constructor_defaults(self): + exporter = OTLPLogExporter() + self.assertEqual( + exporter._endpoint, + DEFAULT_ENDPOINT + DEFAULT_LOGS_EXPORT_PATH, + ) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertEqual(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._request_headers["Content-Type"], "application/x-protobuf" + ) + self.assertFalse(exporter._shutdown) + exporter.shutdown() + + def test_generic_endpoint_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318"}): + exporter = OTLPLogExporter() + self.assertEqual(exporter._endpoint, "http://collector:4318/v1/logs") + exporter.shutdown() + + def test_logs_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4318", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://logs:4318/v1/logs", + }): + exporter = OTLPLogExporter() + self.assertEqual(exporter._endpoint, "http://logs:4318/v1/logs") + exporter.shutdown() + + def test_constructor_arg_overrides_env(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://env:4318/v1/logs", + }): + exporter = OTLPLogExporter(endpoint="http://arg:4318/v1/logs") + self.assertEqual(exporter._endpoint, "http://arg:4318/v1/logs") + exporter.shutdown() + + def test_timeout_generic_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TIMEOUT": "20"}): + exporter = OTLPLogExporter() + self.assertEqual(exporter._timeout, 20.0) + exporter.shutdown() + + def test_timeout_logs_env_var_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TIMEOUT": "20", + "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": "5", + }): + exporter = OTLPLogExporter() + self.assertEqual(exporter._timeout, 5.0) + exporter.shutdown() + + def test_timeout_arg_overrides_env(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_LOGS_TIMEOUT": "99"}): + exporter = OTLPLogExporter(timeout=3) + self.assertEqual(exporter._timeout, 3) + exporter.shutdown() + + def test_compression_env_var_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): + exporter = OTLPLogExporter() + self.assertEqual(exporter._compression, Compression.Gzip) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") + exporter.shutdown() + + def test_compression_env_var_deflate(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "deflate"}): + exporter = OTLPLogExporter() + self.assertEqual(exporter._compression, Compression.Deflate) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "deflate") + exporter.shutdown() + + def test_compression_logs_env_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", + "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION": "deflate", + }): + exporter = OTLPLogExporter() + self.assertEqual(exporter._compression, Compression.Deflate) + exporter.shutdown() + + def test_compression_arg_gzip(self): + exporter = OTLPLogExporter(compression=Compression.Gzip) + self.assertEqual(exporter._compression, Compression.Gzip) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") + exporter.shutdown() + + def test_no_compression_header_for_none_compression(self): + exporter = OTLPLogExporter(compression=Compression.NoCompression) + self.assertNotIn("Content-Encoding", exporter._request_headers) + exporter.shutdown() + + # ── export ──────────────────────────────────────────────────────────────── + + def test_export_success(self): + exporter = OTLPLogExporter() + with patch.object(exporter, "_export", return_value=_ok()): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.SUCCESS) + exporter.shutdown() + + def test_export_after_shutdown_returns_failure(self): + exporter = OTLPLogExporter() + exporter.shutdown() + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + + def test_export_non_retryable_failure(self): + exporter = OTLPLogExporter() + with patch.object(exporter, "_export", return_value=_404()): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + def test_export_retry_then_deadline_exceeded(self): + exporter = OTLPLogExporter(timeout=0.01) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=100.0): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + def test_export_max_retries_exhausted(self): + exporter = OTLPLogExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_is_occuring, "wait", return_value=False): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + def test_shutdown_interrupts_retry(self): + exporter = OTLPLogExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_is_occuring, "wait", return_value=True): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + def test_connection_error_is_retryable(self): + exporter = OTLPLogExporter(timeout=0.01) + with patch.object(exporter, "_export", side_effect=URLError("refused")): + with patch(_UNIFORM, return_value=100.0): + result = exporter.export([]) + self.assertEqual(result, LogRecordExportResult.FAILURE) + exporter.shutdown() + + # ── shutdown ────────────────────────────────────────────────────────────── + + def test_shutdown_sets_flag(self): + exporter = OTLPLogExporter() + self.assertFalse(exporter._shutdown) + exporter.shutdown() + self.assertTrue(exporter._shutdown) + + def test_shutdown_twice_logs_warning(self): + exporter = OTLPLogExporter() + exporter.shutdown() + with self.assertLogs(level="WARNING") as cm: + exporter.shutdown() + self.assertTrue(any("already shutdown" in msg for msg in cm.output)) + + def test_force_flush_returns_true(self): + exporter = OTLPLogExporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + +class TestAppendLogsPath(unittest.TestCase): + def test_with_trailing_slash(self): + self.assertEqual( + _append_logs_path("http://localhost:4318/"), + "http://localhost:4318/v1/logs", + ) + + def test_without_trailing_slash(self): + self.assertEqual( + _append_logs_path("http://localhost:4318"), + "http://localhost:4318/v1/logs", + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/metric_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/metric_exporter/test___init__.py new file mode 100644 index 00000000000..a3996a98263 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/metric_exporter/test___init__.py @@ -0,0 +1,367 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import patch + +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( + encode_metrics, +) +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + DEFAULT_ENDPOINT, + DEFAULT_METRICS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPMetricExporter, + _append_metrics_path, + _split_metrics_data, +) +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + MetricExportResult, + MetricsData, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, +) +from opentelemetry.sdk.metrics.export import Gauge as SDKGauge +from opentelemetry.sdk.metrics.export import Metric, Sum as SDKSum +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +_UNIFORM = "opentelemetry.exporter.otlp.proto.http.metric_exporter.uniform" + + +def _ok(): + return (200, "OK") + + +def _503(): + return (503, "Service Unavailable") + + +def _404(): + return (404, "Not Found") + + +def _make_metrics_data(n_gauge_points: int = 1) -> MetricsData: + data_points = [ + NumberDataPoint( + attributes={"i": i}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=float(i), + exemplars=[], + ) + for i in range(n_gauge_points) + ] + return MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource({"service.name": "test"}), + scope_metrics=[ + ScopeMetrics( + scope=InstrumentationScope("test", "1.0"), + metrics=[ + Metric( + name="my.gauge", + description="", + unit="1", + data=SDKGauge(data_points=data_points), + ) + ], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + + +def _empty_metrics_data() -> MetricsData: + return MetricsData(resource_metrics=[]) + + +class TestOTLPMetricExporter(unittest.TestCase): + + # ── constructor ─────────────────────────────────────────────────────────── + + def test_constructor_defaults(self): + exporter = OTLPMetricExporter() + self.assertEqual( + exporter._endpoint, + DEFAULT_ENDPOINT + DEFAULT_METRICS_EXPORT_PATH, + ) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertEqual(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._request_headers["Content-Type"], "application/x-protobuf" + ) + self.assertIsNone(exporter._max_export_batch_size) + self.assertFalse(exporter._shutdown) + exporter.shutdown() + + def test_generic_endpoint_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318"}): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._endpoint, "http://collector:4318/v1/metrics") + exporter.shutdown() + + def test_metrics_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4318", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://metrics:4318/v1/metrics", + }): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._endpoint, "http://metrics:4318/v1/metrics") + exporter.shutdown() + + def test_constructor_arg_overrides_env(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://env:4318/v1/metrics", + }): + exporter = OTLPMetricExporter(endpoint="http://arg:4318/v1/metrics") + self.assertEqual(exporter._endpoint, "http://arg:4318/v1/metrics") + exporter.shutdown() + + def test_timeout_generic_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TIMEOUT": "20"}): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._timeout, 20.0) + exporter.shutdown() + + def test_timeout_metrics_env_var_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TIMEOUT": "20", + "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "5", + }): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._timeout, 5.0) + exporter.shutdown() + + def test_compression_env_var_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._compression, Compression.Gzip) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") + exporter.shutdown() + + def test_compression_metrics_env_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "deflate", + }): + exporter = OTLPMetricExporter() + self.assertEqual(exporter._compression, Compression.Deflate) + exporter.shutdown() + + def test_max_export_batch_size_arg(self): + exporter = OTLPMetricExporter(max_export_batch_size=100) + self.assertEqual(exporter._max_export_batch_size, 100) + exporter.shutdown() + + # ── export ──────────────────────────────────────────────────────────────── + + def test_export_success_empty_data(self): + exporter = OTLPMetricExporter() + with patch.object(exporter, "_export", return_value=_ok()): + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.SUCCESS) + exporter.shutdown() + + def test_export_success_with_data(self): + exporter = OTLPMetricExporter() + with patch.object(exporter, "_export", return_value=_ok()): + result = exporter.export(_make_metrics_data(2)) + self.assertEqual(result, MetricExportResult.SUCCESS) + exporter.shutdown() + + def test_export_after_shutdown_returns_failure(self): + exporter = OTLPMetricExporter() + exporter.shutdown() + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + + def test_export_non_retryable_failure(self): + exporter = OTLPMetricExporter() + with patch.object(exporter, "_export", return_value=_404()): + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() + + def test_export_retry_then_deadline_exceeded(self): + exporter = OTLPMetricExporter(timeout=0.01) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=100.0): + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() + + def test_export_max_retries_exhausted(self): + exporter = OTLPMetricExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=False): + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() + + def test_shutdown_interrupts_retry(self): + exporter = OTLPMetricExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=True): + result = exporter.export(_empty_metrics_data()) + self.assertEqual(result, MetricExportResult.FAILURE) + exporter.shutdown() + + def test_export_with_batch_size_sends_multiple_requests(self): + # 3 data points with batch_size=1 → 3 separate HTTP requests + exporter = OTLPMetricExporter(max_export_batch_size=1) + call_count = 0 + + def export_side_effect(*_args, **_kwargs): + nonlocal call_count + call_count += 1 + return _ok() + + with patch.object(exporter, "_export", side_effect=export_side_effect): + result = exporter.export(_make_metrics_data(3)) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertEqual(call_count, 3) + exporter.shutdown() + + # ── shutdown ────────────────────────────────────────────────────────────── + + def test_shutdown_sets_flag(self): + exporter = OTLPMetricExporter() + self.assertFalse(exporter._shutdown) + exporter.shutdown() + self.assertTrue(exporter._shutdown) + + def test_shutdown_twice_logs_warning(self): + exporter = OTLPMetricExporter() + exporter.shutdown() + with self.assertLogs(level="WARNING") as cm: + exporter.shutdown() + self.assertTrue(any("already shutdown" in msg for msg in cm.output)) + + def test_force_flush_returns_true(self): + exporter = OTLPMetricExporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + +class TestAppendMetricsPath(unittest.TestCase): + def test_with_trailing_slash(self): + self.assertEqual( + _append_metrics_path("http://localhost:4318/"), + "http://localhost:4318/v1/metrics", + ) + + def test_without_trailing_slash(self): + self.assertEqual( + _append_metrics_path("http://localhost:4318"), + "http://localhost:4318/v1/metrics", + ) + + +class TestSplitMetricsData(unittest.TestCase): + + def _make_request(self, n_points: int): + return encode_metrics(_make_metrics_data(n_points)) + + def test_split_no_batch_size_yields_unchanged(self): + # max_export_batch_size=0 (falsy) → yields data as-is + req = self._make_request(3) + batches = list(_split_metrics_data(req, 0)) + self.assertEqual(len(batches), 1) + self.assertIs(batches[0], req) + + def test_split_batch_larger_than_data_yields_one(self): + req = self._make_request(3) + batches = list(_split_metrics_data(req, 10)) + self.assertEqual(len(batches), 1) + total_points = sum( + len(sm.metrics[0].gauge.data_points) + for batch in batches + for rm in batch.resource_metrics + for sm in rm.scope_metrics + ) + self.assertEqual(total_points, 3) + + def test_split_batch_size_1_yields_one_per_point(self): + req = self._make_request(3) + batches = list(_split_metrics_data(req, 1)) + self.assertEqual(len(batches), 3) + for batch in batches: + total = sum( + len(sm.metrics[0].gauge.data_points) + for rm in batch.resource_metrics + for sm in rm.scope_metrics + ) + self.assertEqual(total, 1) + + def test_split_batch_size_2_yields_two_batches_for_four_points(self): + req = self._make_request(4) + batches = list(_split_metrics_data(req, 2)) + self.assertEqual(len(batches), 2) + + def test_split_empty_request_yields_nothing(self): + req = encode_metrics(_empty_metrics_data()) + batches = list(_split_metrics_data(req, 1)) + self.assertEqual(len(batches), 0) + + def test_split_preserves_sum_metric(self): + data = MetricsData( + resource_metrics=[ + ResourceMetrics( + resource=Resource({"service.name": "test"}), + scope_metrics=[ + ScopeMetrics( + scope=InstrumentationScope("test", "1.0"), + metrics=[ + Metric( + name="my.counter", + description="", + unit="1", + data=SDKSum( + data_points=[ + NumberDataPoint( + attributes={}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=1, + exemplars=[], + ), + NumberDataPoint( + attributes={"host": "b"}, + start_time_unix_nano=0, + time_unix_nano=1641946016139533244, + value=2, + exemplars=[], + ), + ], + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=True, + ), + ) + ], + schema_url="", + ) + ], + schema_url="", + ) + ] + ) + req = encode_metrics(data) + batches = list(_split_metrics_data(req, 1)) + self.assertEqual(len(batches), 2) + for batch in batches: + sm = batch.resource_metrics[0].scope_metrics[0] + metric = sm.metrics[0] + self.assertEqual(metric.name, "my.counter") + self.assertEqual(len(metric.sum.data_points), 1) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/test___init__.py new file mode 100644 index 00000000000..5b35467be55 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/test___init__.py @@ -0,0 +1,25 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.proto.http import Compression, _OTLP_HTTP_HEADERS +from opentelemetry.exporter.otlp.proto.http.version import __version__ + + +def test_compression_no_compression_value(): + assert Compression.NoCompression.value == "none" + + +def test_compression_deflate_value(): + assert Compression.Deflate.value == "deflate" + + +def test_compression_gzip_value(): + assert Compression.Gzip.value == "gzip" + + +def test_otlp_http_headers_content_type(): + assert _OTLP_HTTP_HEADERS["Content-Type"] == "application/x-protobuf" + + +def test_otlp_http_headers_user_agent(): + assert _OTLP_HTTP_HEADERS["User-Agent"] == f"OTel-OTLP-Exporter-Python/{__version__}" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/trace_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/trace_exporter/test___init__.py new file mode 100644 index 00000000000..0d83331f2cd --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/unit/opentelemetry/exporter/otlp/proto/http/trace_exporter/test___init__.py @@ -0,0 +1,221 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=protected-access + +import unittest +from unittest.mock import patch +from urllib.error import URLError + +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + DEFAULT_ENDPOINT, + DEFAULT_TIMEOUT, + DEFAULT_TRACES_EXPORT_PATH, + OTLPSpanExporter, + _append_trace_path, +) +from opentelemetry.sdk.trace.export import SpanExportResult + +_UNIFORM = "opentelemetry.exporter.otlp.proto.http.trace_exporter.uniform" + + +def _ok(): + return (200, "OK") + + +def _503(): + return (503, "Service Unavailable") + + +def _404(): + return (404, "Not Found") + + +class TestOTLPSpanExporter(unittest.TestCase): + + # ── constructor ─────────────────────────────────────────────────────────── + + def test_constructor_defaults(self): + exporter = OTLPSpanExporter() + self.assertEqual( + exporter._endpoint, + DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH, + ) + self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) + self.assertEqual(exporter._compression, Compression.NoCompression) + self.assertEqual( + exporter._request_headers["Content-Type"], "application/x-protobuf" + ) + self.assertFalse(exporter._shutdown) + exporter.shutdown() + + def test_generic_endpoint_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318"}): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._endpoint, "http://collector:4318/v1/traces") + exporter.shutdown() + + def test_traces_endpoint_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://generic:4318", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://traces:4318/v1/traces", + }): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._endpoint, "http://traces:4318/v1/traces") + exporter.shutdown() + + def test_constructor_arg_overrides_env(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env:4318/v1/traces", + }): + exporter = OTLPSpanExporter(endpoint="http://arg:4318/v1/traces") + self.assertEqual(exporter._endpoint, "http://arg:4318/v1/traces") + exporter.shutdown() + + def test_timeout_generic_env_var(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TIMEOUT": "20"}): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._timeout, 20.0) + exporter.shutdown() + + def test_timeout_traces_env_var_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_TIMEOUT": "20", + "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "5", + }): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._timeout, 5.0) + exporter.shutdown() + + def test_timeout_arg_overrides_env(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "99"}): + exporter = OTLPSpanExporter(timeout=3) + self.assertEqual(exporter._timeout, 3) + exporter.shutdown() + + def test_compression_env_var_gzip(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._compression, Compression.Gzip) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") + exporter.shutdown() + + def test_compression_env_var_deflate(self): + with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "deflate"}): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._compression, Compression.Deflate) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "deflate") + exporter.shutdown() + + def test_compression_traces_env_overrides_generic(self): + with patch.dict("os.environ", { + "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", + "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "deflate", + }): + exporter = OTLPSpanExporter() + self.assertEqual(exporter._compression, Compression.Deflate) + exporter.shutdown() + + def test_compression_arg_gzip(self): + exporter = OTLPSpanExporter(compression=Compression.Gzip) + self.assertEqual(exporter._compression, Compression.Gzip) + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") + exporter.shutdown() + + def test_no_compression_header_for_none_compression(self): + exporter = OTLPSpanExporter(compression=Compression.NoCompression) + self.assertNotIn("Content-Encoding", exporter._request_headers) + exporter.shutdown() + + # ── export ──────────────────────────────────────────────────────────────── + + def test_export_success(self): + exporter = OTLPSpanExporter() + with patch.object(exporter, "_export", return_value=_ok()): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.SUCCESS) + exporter.shutdown() + + def test_export_after_shutdown_returns_failure(self): + exporter = OTLPSpanExporter() + exporter.shutdown() + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + + def test_export_non_retryable_failure(self): + exporter = OTLPSpanExporter() + with patch.object(exporter, "_export", return_value=_404()): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_export_retry_then_deadline_exceeded(self): + # backoff(100) > remaining timeout(0.01) → exits on first retry + exporter = OTLPSpanExporter(timeout=0.01) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=100.0): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_export_max_retries_exhausted(self): + exporter = OTLPSpanExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=False): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_shutdown_interrupts_retry(self): + exporter = OTLPSpanExporter(timeout=100) + with patch.object(exporter, "_export", return_value=_503()): + with patch(_UNIFORM, return_value=0.0001): + with patch.object(exporter._shutdown_in_progress, "wait", return_value=True): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + def test_connection_error_is_retryable(self): + # URLError from _export goes into the retry path; deadline kills it fast. + exporter = OTLPSpanExporter(timeout=0.01) + with patch.object(exporter, "_export", side_effect=URLError("refused")): + with patch(_UNIFORM, return_value=100.0): + result = exporter.export([]) + self.assertEqual(result, SpanExportResult.FAILURE) + exporter.shutdown() + + # ── shutdown ────────────────────────────────────────────────────────────── + + def test_shutdown_sets_flag(self): + exporter = OTLPSpanExporter() + self.assertFalse(exporter._shutdown) + exporter.shutdown() + self.assertTrue(exporter._shutdown) + + def test_shutdown_twice_logs_warning(self): + exporter = OTLPSpanExporter() + exporter.shutdown() + with self.assertLogs(level="WARNING") as cm: + exporter.shutdown() + self.assertTrue(any("already shutdown" in msg for msg in cm.output)) + + def test_force_flush_returns_true(self): + exporter = OTLPSpanExporter() + self.assertTrue(exporter.force_flush()) + exporter.shutdown() + + +class TestAppendTracePath(unittest.TestCase): + def test_with_trailing_slash(self): + self.assertEqual( + _append_trace_path("http://localhost:4318/"), + "http://localhost:4318/v1/traces", + ) + + def test_without_trailing_slash(self): + self.assertEqual( + _append_trace_path("http://localhost:4318"), + "http://localhost:4318/v1/traces", + ) diff --git a/opentelemetry-proto/pyproject.toml b/opentelemetry-proto/pyproject.toml index 645550627bb..a6482395b96 100644 --- a/opentelemetry-proto/pyproject.toml +++ b/opentelemetry-proto/pyproject.toml @@ -25,7 +25,6 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "protobuf>=5.0, < 8.0", ] [project.urls] @@ -33,7 +32,7 @@ Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/ope Repository = "https://github.com/open-telemetry/opentelemetry-python" [tool.hatch.version] -path = "src/opentelemetry/proto/version/__init__.py" +path = "src/opentelemetry/_proto/version/__init__.py" [tool.hatch.build.targets.sdist] include = [ diff --git a/opentelemetry-proto/src/opentelemetry/_proto/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/__init__.py new file mode 100644 index 00000000000..9f186998550 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/__init__.py @@ -0,0 +1,39 @@ +from .enum import encode_enum +from .scalars import ( + encode_bool, + encode_bytes, + encode_double, + encode_fixed32, + encode_fixed64, + encode_float, + encode_int, + encode_sfixed32, + encode_sfixed64, + encode_sint32, + encode_sint64, + encode_string, + encode_uint32, + encode_uint64, +) +from .tag import encode_tag +from .varint import encode_varint + +__all__ = [ + "encode_bool", + "encode_bytes", + "encode_double", + "encode_enum", + "encode_fixed32", + "encode_fixed64", + "encode_float", + "encode_int", + "encode_sfixed32", + "encode_sfixed64", + "encode_sint32", + "encode_sint64", + "encode_string", + "encode_tag", + "encode_uint32", + "encode_uint64", + "encode_varint", +] diff --git a/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/enum.py b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/enum.py new file mode 100644 index 00000000000..229b44787b5 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/enum.py @@ -0,0 +1,53 @@ +"""Encoder for protobuf enum field values. + +Protobuf enum fields use wire type 0 (varint) and share the exact same wire +encoding as int32. This module exists as its own file — separate from +_scalars.py — because an enum is not a scalar type. Enum fields carry named +constants, not raw numeric values. The distinction matters at the .proto +language level even though the wire encoding is identical to int32. + +Reference: + https://protobuf.dev/programming-guides/encoding/ +""" + +from .scalars import encode_int + + +def encode_enum(value: int) -> bytes: + """Encode a protobuf enum value. + + Protobuf enum fields use wire type 0 (varint) and share the exact same + wire encoding as int32. The encoding guide states: + + Enum values are always 32-bit integers and the encoding follows the + same rules as int32. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + + Enum values in practice + ----------------------- + In proto3, enum values defined in a .proto file are non-negative named + constants (0, 1, 2, …). The first value of any proto3 enum must be 0. + + In proto2, negative enum values are legal. Negative enum values can also + appear in proto3 messages received from a proto2 sender: the decoder + preserves them as their raw integer value. In both cases the int32 wire + encoding applies, meaning negative enum values produce a 10-byte varint + (via 64-bit sign extension, exactly as encode_int does for negative ints). + + This function is a thin wrapper around encode_int. Its purpose is + readability at call sites: code that encodes an enum field can call + encode_enum to make the intent explicit, rather than encode_int, which + suggests an arbitrary integer. + """ + # Delegate entirely to encode_int, which implements the int32 wire rule: + # + # - Non-negative values → encode_varint directly (compact, 1–5 bytes). + # - Negative values → mask to 64-bit unsigned two's complement, then + # encode_varint (always 10 bytes). + # + # No additional logic is needed: the spec defines enum encoding as + # identical to int32, so encode_int is the correct and complete + # implementation of both. + return encode_int(value) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/fields.py b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/fields.py new file mode 100644 index 00000000000..4b278a50670 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/fields.py @@ -0,0 +1,542 @@ +"""Proto3 field-level encoding helpers for SerializeToString() implementations. + +Why this module exists +---------------------- +The other modules in this package (varint, tag, scalars, enum) provide the +raw encoding kernel: functions that take a Python value and return its bytes +representation in the protobuf wire format. Those functions know nothing about +proto3 messages. They do not know about field numbers, wire-type tags, or the +rule that says a field whose value equals the proto3 default must be omitted +from the serialised output. + +This module sits one level above that kernel. It provides one helper per +proto3 scalar category (uint64, sint32, double, string, bytes, bool, fixed32, +fixed64) plus helpers for embedded messages and packed repeated fields. Each +helper combines three operations that every SerializeToString() method must +perform for every field it writes: + + 1. Apply the proto3 default-omission rule — if the value equals the type's + default (0, 0.0, False, b"", ""), return b"" immediately. + 2. Encode the tag — (field_number << 3) | wire_type, then varint-encoded. + 3. Encode the value — using the appropriate primitive from this package. + +Without these helpers, every SerializeToString() method would repeat that +three-step pattern inline for every field, cluttering the message classes with +low-level wire-format details and making the default-omission logic easy to get +wrong or forget. + +Why it is named "fields" +------------------------ +Each function in this module encodes one proto3 field: it takes a field number +and a value, and returns the complete on-wire bytes for that field — tag plus +encoded value, or b"" when the value is the proto3 default. The word "field" +is the right abstraction because a proto3 field is exactly this: a field number, +a wire type, a default-omission rule, and a value encoding. The other modules +in this package encode raw values; this module encodes fields. + +Layering within _pyprotobuf +---------------------------- + varint.py — encode a varint integer to bytes + tag.py — encode a (field_number, wire_type) tag using varint + scalars.py — encode all proto3 scalar types to bytes + enum.py — encode a proto3 enum value (thin wrapper over scalars) + fields.py — encode a complete proto3 field (tag + default check + value) + +The message classes (common_pb2.py, metrics_pb2.py, etc.) use this +module directly. They import the encode_* primitives from the package root +(__init__.py) and the field helpers from here. + +Wire-type constants +------------------- +The protobuf specification defines six wire types. The wire type is stored in +the three lowest bits of every tag integer. Only four wire types appear in +proto3 messages (wire types 3 and 4, the deprecated group delimiters, do not +appear in new proto3 code): + + WT_VARINT = 0 — one or more bytes with a continuation bit in the MSB of + each byte. Used for: int32, int64, uint32, uint64, sint32, + sint64, bool, enum. + WT_64BIT = 1 — exactly 8 bytes, little-endian. Used for: double, + fixed64, sfixed64. + WT_LEN = 2 — a varint length prefix followed by that many bytes. Used + for: string, bytes, embedded messages, packed repeated + fields. + WT_32BIT = 5 — exactly 4 bytes, little-endian. Used for: float, fixed32, + sfixed32. + +Reference: https://protobuf.dev/programming-guides/encoding/ +""" + +from __future__ import annotations + +from struct import pack + +from .scalars import encode_fixed32, encode_fixed64, encode_sint32 +from .tag import encode_tag +from .varint import encode_varint + +WT_VARINT = 0 # int32, int64, uint32, uint64, bool, enum +WT_64BIT = 1 # double, fixed64, sfixed64 +WT_LEN = 2 # string, bytes, embedded messages, packed arrays +WT_32BIT = 5 # float, fixed32, sfixed32 + + +def msg(field: int, content: bytes) -> bytes: + """Encode an embedded message or group as a length-delimited field. + + In the protobuf wire format, an embedded message is a length-delimited + field (wire type 2). The layout on the wire is: + + tag (varint) | byte_length (varint) | content (bytes) + + The tag encodes the field number and wire type 2. The byte_length is the + number of bytes in the already-serialised sub-message. The content is the + verbatim output of the sub-message's own SerializeToString() call. + + Unlike the scalar helpers, msg does not apply an omission rule for empty + content. An embedded message with no fields set serialises to b"" (zero + bytes), and the caller decides whether to write it at all. In practice, + message fields are either written unconditionally when present or guarded + by an explicit ``if self.field is not None`` check in the caller; the empty + check is the caller's responsibility, not this helper's. + + This helper is also used for oneof sub-messages, repeated message fields + (called once per element), and any other length-delimited payload. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + content: + The serialised bytes of the sub-message (the return value of + sub_message.SerializeToString()). + + Returns + ------- + bytes + tag + varint(len(content)) + content + """ + return encode_tag(field, WT_LEN) + encode_varint(len(content)) + content + + +def string(field: int, value: str) -> bytes: + """Encode a proto3 string field, omitting it when the value is empty. + + Proto3 defines the default value for string fields as the empty string + "". A field equal to its default must not be written to the wire, so this + helper returns b"" when value is "". + + The encoding on the wire is: + + tag (varint) | byte_length (varint) | utf-8 bytes + + The proto3 specification requires that string fields contain valid UTF-8. + Python's str.encode("utf-8") enforces this at the encoding step. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + The string to encode. An empty string causes this helper to return + b"" (field omitted). + + Returns + ------- + bytes + b"" if value is empty, otherwise tag + varint(len(utf8)) + utf8. + """ + if not value: + return b"" + utf8 = value.encode("utf-8") + return encode_tag(field, WT_LEN) + encode_varint(len(utf8)) + utf8 + + +def byt(field: int, value: bytes) -> bytes: + """Encode a proto3 bytes field, omitting it when the value is empty. + + Proto3 defines the default value for bytes fields as the empty byte + string b"". A field equal to its default must not be written, so this + helper returns b"" when value is b"". + + The encoding on the wire is identical to a string field: + + tag (varint) | byte_length (varint) | raw bytes + + Unlike string, no UTF-8 encoding step is needed because the caller already + holds raw bytes. This helper is used for span_id, trace_id, and any other + proto3 bytes field. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + The bytes to encode. An empty bytes object causes this helper to + return b"" (field omitted). + + Returns + ------- + bytes + b"" if value is empty, otherwise tag + varint(len(value)) + value. + """ + if not value: + return b"" + return encode_tag(field, WT_LEN) + encode_varint(len(value)) + value + + +def u64(field: int, value: int) -> bytes: + """Encode a proto3 uint64 (or any varint-encoded integer) field. + + This helper covers all proto3 field types whose wire type is WT_VARINT + and whose value is a non-negative integer: uint32, uint64, int32 (when + non-negative), int64 (when non-negative), bool (encoded as 0 or 1), and + enum (encoded as its integer value). + + Proto3 defines the default value for these types as 0. A zero value must + not be written to the wire. + + The encoding on the wire is: + + tag (varint) | value (varint) + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + A non-negative integer. Zero causes this helper to return b"" + (field omitted). + + Returns + ------- + bytes + b"" if value is 0, otherwise tag + varint(value). + """ + if value == 0: + return b"" + return encode_tag(field, WT_VARINT) + encode_varint(value) + + +def bool_field(field: int, value: bool) -> bytes: + """Encode a proto3 bool field, omitting it when False. + + Proto3 defines the default value for bool as False (encoded as 0). A + False value must not be written to the wire. + + A True value is encoded as varint 1. The wire layout is: + + tag (varint) | 0x01 + + This helper is separate from u64 because booleans carry different + semantic meaning even though they share wire type 0 (varint). Keeping + them distinct makes SerializeToString() implementations easier to read. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + The boolean to encode. False causes this helper to return b"" + (field omitted). True is encoded as varint 1. + + Returns + ------- + bytes + b"" if value is False, otherwise tag + 0x01. + """ + if not value: + return b"" + return encode_tag(field, WT_VARINT) + encode_varint(1) + + +def fix32(field: int, value: int) -> bytes: + """Encode a proto3 fixed32 field (4-byte little-endian uint32). + + fixed32 uses wire type WT_32BIT. The decoder reads exactly 4 bytes after + the tag. This makes fixed32 more efficient than uint32 for values that are + frequently large (close to 2^32), because it avoids varint overhead. + + Proto3 default is 0. A zero value must not be written to the wire. + + The encoding on the wire is: + + tag (varint) | 4-byte little-endian uint32 + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + An unsigned 32-bit integer. Zero causes this helper to return b"" + (field omitted). + + Returns + ------- + bytes + b"" if value is 0, otherwise tag + 4 bytes little-endian. + """ + if value == 0: + return b"" + return encode_tag(field, WT_32BIT) + encode_fixed32(value) + + +def fix64(field: int, value: int) -> bytes: + """Encode a proto3 fixed64 field (8-byte little-endian uint64). + + fixed64 uses wire type WT_64BIT. The decoder reads exactly 8 bytes after + the tag. In the OTel proto schemas, fixed64 is used for nanosecond + timestamps (start_time_unix_nano, time_unix_nano), counts, and bucket + counts that are guaranteed to be non-negative. + + Proto3 default is 0. A zero value must not be written to the wire. + + The encoding on the wire is: + + tag (varint) | 8-byte little-endian uint64 + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + An unsigned 64-bit integer. Zero causes this helper to return b"" + (field omitted). + + Returns + ------- + bytes + b"" if value is 0, otherwise tag + 8 bytes little-endian. + """ + if value == 0: + return b"" + return encode_tag(field, WT_64BIT) + encode_fixed64(value) + + +def dbl(field: int, value: float) -> bytes: + """Encode a proto3 double field (8-byte IEEE 754), omitting when zero. + + double uses wire type WT_64BIT. The decoder reads exactly 8 bytes and + interprets them as an IEEE 754 double-precision floating-point number in + little-endian byte order. + + Proto3 default is 0.0. A zero value must not be written to the wire. + Note that -0.0 compares equal to 0.0 in Python, so dbl(field, -0.0) + returns b"" — if that distinction matters, use opt_dbl instead. + + This helper is used for scalar double fields that have a meaningful zero + default and should be omitted when zero: for example, the `sum` field of + SummaryDataPoint and the `zero_threshold` field of + ExponentialHistogramDataPoint. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + An IEEE 754 double. 0.0 causes this helper to return b"" + (field omitted). Infinity and NaN are encoded as-is. + + Returns + ------- + bytes + b"" if value == 0.0, otherwise tag + 8-byte little-endian double. + """ + if value == 0.0: + return b"" + return encode_tag(field, WT_64BIT) + pack(" bytes: + """Encode an optional proto3 double field, written even when 0.0. + + Some double fields in the OTel proto schemas are declared optional + (proto3 optional syntax), which means the presence of the field is + significant regardless of its value. A value of 0.0 is a meaningful + measurement and must be written; only None (field not set) causes the + field to be omitted. + + This is distinct from dbl, which omits 0.0 as the proto3 default. + opt_dbl is used for fields such as `sum`, `min`, and `max` on histogram + data points, where None means "not present" and 0.0 means "present and + measured as zero". + + The encoding on the wire is identical to dbl when the value is present: + + tag (varint) | 8-byte little-endian double + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + An IEEE 754 double, or None. None causes this helper to return b"" + (field omitted). Any float, including 0.0, -0.0, inf, and nan, is + encoded and written to the wire. + + Returns + ------- + bytes + b"" if value is None, otherwise tag + 8-byte little-endian double. + """ + if value is None: + return b"" + return encode_tag(field, WT_64BIT) + pack(" bytes: + """Encode a proto3 sint32 field (ZigZag varint), omitting when zero. + + sint32 uses wire type WT_VARINT but applies ZigZag encoding before the + varint step. ZigZag maps signed integers to unsigned integers so that + small negative values produce short varints rather than the ten-byte + varints that two's-complement representation would require: + + 0 → 0 + -1 → 1 + 1 → 2 + -2 → 3 + n → 2*n (for n >= 0) + n → -2*n - 1 (for n < 0) + + This is the correct encoding for the proto3 `sint32` type. It is NOT used + for `int32` fields (use u64 for non-negative int32, or encode_int from + the package for signed int32). + + In the OTel proto schemas, sint32 appears on the `scale` field of + ExponentialHistogramDataPoint and the `offset` field of its Buckets + sub-message. These fields represent signed exponents and can be negative. + + Proto3 default is 0. A zero value must not be written to the wire. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + value: + A signed 32-bit integer. Zero causes this helper to return b"" + (field omitted). + + Returns + ------- + bytes + b"" if value is 0, otherwise tag + ZigZag-encoded varint. + """ + if value == 0: + return b"" + return encode_tag(field, WT_VARINT) + encode_sint32(value) + + +def packed_uint64(field: int, values: list[int]) -> bytes: + """Encode a packed repeated uint64 field. + + In proto3, repeated scalar fields are packed by default. "Packed" means + all elements are encoded contiguously without a tag before each one. + Instead, a single tag and a single length prefix wrap the entire payload: + + tag (varint) | payload_length (varint) | element0 (varint) | element1 (varint) | ... + + Each element is encoded as an independent varint, exactly as encode_varint + would encode it if the field were scalar. The decoder knows how many + elements are present by tracking how many bytes it has consumed relative + to payload_length. + + An empty list produces b"" (field omitted). This matches proto3 behaviour: + a repeated field with zero elements is indistinguishable from the field + being absent. + + This helper is used for fields such as bucket_counts in + ExponentialHistogramDataPoint.Buckets. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + values: + A list of non-negative integers. An empty list causes this helper + to return b"" (field omitted). + + Returns + ------- + bytes + b"" if values is empty, otherwise tag + varint(len(payload)) + payload, + where payload is the concatenation of varint-encoded elements. + """ + if not values: + return b"" + payload = b"".join(encode_varint(v) for v in values) + return encode_tag(field, WT_LEN) + encode_varint(len(payload)) + payload + + +def packed_fix64(field: int, values: list[int]) -> bytes: + """Encode a packed repeated fixed64 field. + + Like packed_uint64 but each element is encoded as an 8-byte + little-endian uint64 rather than a varint. The wire layout is: + + tag (varint) | payload_length (varint) | element0 (8 bytes) | element1 (8 bytes) | ... + + fixed64 encoding is efficient for values that are close to 2^64 or are + accessed in bulk (the payload is a flat array of 8-byte little-endian + words, amenable to memcpy). + + An empty list produces b"" (field omitted). + + This helper is used for the bucket_counts field of HistogramDataPoint, + where counts are non-negative 64-bit integers. + + Parameters + ---------- + field: + The field number as declared in the .proto schema. + values: + A list of non-negative 64-bit integers. An empty list causes this + helper to return b"" (field omitted). + + Returns + ------- + bytes + b"" if values is empty, otherwise tag + varint(len(payload)) + payload, + where payload is the concatenation of 8-byte little-endian encodings. + """ + if not values: + return b"" + payload = b"".join(encode_fixed64(v) for v in values) + return encode_tag(field, WT_LEN) + encode_varint(len(payload)) + payload + + +def packed_double(field: int, values: list[float]) -> bytes: + """Encode a packed repeated double field. + + Like packed_uint64 but each element is an IEEE 754 double (8 bytes, + little-endian). The wire layout is: + + tag (varint) | payload_length (varint) | element0 (8 bytes) | element1 (8 bytes) | ... + + struct.pack with format " bytes: + """Encode an unsigned 32-bit integer as a protobuf uint32 field value. + + Protobuf uint32 fields use wire type 0 (varint). The value is encoded + as a plain unsigned varint with no transformation applied. + + Valid range: [0, 2^32 - 1]. + + Unlike int32, uint32 has no signed interpretation, so negative inputs + are not defined. Values in [0, 127] encode to a single byte; larger + values require more bytes as their magnitude grows. + + Example — encoding 0: + + encode_varint(0) == b'\\x00' + + Example — encoding 300: + + 300 = 0b1_0010_1100 + Split into 7-bit groups (little-endian): 0b010_1100, 0b000_0010 + Add continuation bit to first group: 0b1_010_1100 == 0xAC + Second group (no continuation): 0b000_0010 == 0x02 + Result: b'\\xac\\x02' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # uint32 encoding is identical to plain varint encoding — no sign extension, + # no ZigZag, no transformation. encode_varint raises ValueError for negative + # inputs, which correctly rejects values outside the uint32 domain. + return encode_varint(value) + + +def encode_uint64(value: int) -> bytes: + """Encode an unsigned 64-bit integer as a protobuf uint64 field value. + + Protobuf uint64 fields use wire type 0 (varint). The value is encoded + as a plain unsigned varint with no transformation applied. + + Valid range: [0, 2^64 - 1]. + + This is the 64-bit counterpart of encode_uint32. The encoding is + identical — both delegate to encode_varint — but the domain is larger. + Values up to 2^64 - 1 require at most 10 varint bytes. + + Example — encoding 2^32 (first value that exceeds the uint32 domain): + + 2^32 = 4294967296 = 0x1_0000_0000 + Varint encoding requires 5 bytes: + b'\\x80\\x80\\x80\\x80\\x10' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # Identical to encode_uint32 at the implementation level. The separate + # function exists so call sites can express the .proto field type precisely. + return encode_varint(value) + + +def encode_bool(value: bool) -> bytes: + """Encode a Python bool as a protobuf bool field value. + + Protobuf bool fields use wire type 0 (varint). The encoding guide defines + exactly two valid encodings: + + False -> varint 0 -> 0x00 (one byte) + True -> varint 1 -> 0x01 (one byte) + + The spec states that values other than 0 or 1 are not valid on the wire + for a bool field. This function enforces that constraint by mapping any + truthy value to 1 and any falsy value to 0, regardless of the input's + actual numeric value. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # The conditional expression evaluates value in a boolean context. + # + # Using (1 if value else 0) instead of int(value) is intentional. + # int(True) == 1 and int(False) == 0, which would work correctly for + # actual bool inputs. However, int(5) == 5, and varint 5 is not a valid + # protobuf bool encoding. The conditional collapses any truthy integer, + # non-empty string, non-empty list, etc. to exactly 1, and any falsy + # value to exactly 0, maintaining spec compliance regardless of what + # the caller passes. + # + # Both 0 and 1 fit in a single varint byte (no continuation bit needed), + # so encode_varint always returns exactly one byte here: + # + # encode_bool(False) -> b'\x00' + # encode_bool(True) -> b'\x01' + return encode_varint(1 if value else 0) + + +def encode_int(value: int) -> bytes: + """Encode a signed integer as a protobuf varint for int32 and int64 fields. + + Protobuf int32 and int64 fields both use wire type 0 (varint). The two + types share the same wire encoding — int32 values are sign-extended to + 64 bits before encoding, so they produce identical byte sequences to the + equivalent int64 value. + + Non-negative values pass straight through to encode_varint unchanged. + + Negative values are handled by a rule from the encoding guide: + + If you use int32 or int64 as the type for a negative number, the + resulting varint is always ten bytes long — it is, effectively, + treated like a very large unsigned integer. + + The rule comes from how the spec defines the encoding: a negative int32 or + int64 is sign-extended to a full 64-bit two's complement value before being + treated as an unsigned integer for varint encoding. A 64-bit value with + all its high bits set always requires 10 varint bytes regardless of the + original magnitude. + + Concrete example — encoding -1: + + -1 in two's complement 64-bit: + + 0xFFFFFFFFFFFFFFFF (all 64 bits set to 1) + + That unsigned 64-bit integer requires all 10 varint bytes: + + 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01 + + Each of the first nine bytes has its continuation bit set (0xFF = + 1111_1111: 7 payload bits all 1, plus the continuation bit). The tenth + byte 0x01 carries the final payload bits with no continuation bit. + + This fixed 10-byte cost applies to -1 the same as to the most negative + 64-bit integer -2^63. The magnitude of the negative value does not affect + the byte count. This is why the encoding guide recommends sint32/sint64 + (see encode_sint32 and encode_sint64) for fields that often hold negative + values. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + if value >= 0: + # Non-negative values need no transformation. encode_varint encodes + # them directly as unsigned varints, which matches the int32/int64 + # wire encoding exactly. + return encode_varint(value) + + # For negative values, the protobuf spec requires 64-bit sign extension + # followed by unsigned varint encoding. + # + # Python integers have arbitrary precision — there is no native 64-bit + # boundary. The bitwise AND with 0xFFFFFFFFFFFFFFFF (a mask of 64 ones, + # equal to 2^64 - 1) extracts exactly the lower 64 bits of value. + # + # For any negative integer, Python's two's complement representation means + # the lower 64 bits equal the 64-bit unsigned two's complement value: + # + # -1 & 0xFFFFFFFFFFFFFFFF == 0xFFFFFFFFFFFFFFFF (2^64 - 1) + # -2 & 0xFFFFFFFFFFFFFFFF == 0xFFFFFFFFFFFFFFFE (2^64 - 2) + # -128 & 0xFFFFFFFFFFFFFFFF == 0xFFFFFFFFFFFFFF80 + # + # Every result is in the range [2^63, 2^64 - 1], so encode_varint will + # always produce exactly 10 bytes for any negative input. + unsigned = value & 0xFFFFFFFFFFFFFFFF + return encode_varint(unsigned) + + +def encode_sint32(value: int) -> bytes: + """Encode a signed 32-bit integer using ZigZag encoding for sint32 fields. + + Protobuf sint32 fields use wire type 0 (varint) but apply a ZigZag + transformation before varint encoding. ZigZag maps signed integers to + non-negative integers by interleaving the negative and positive sequences: + + 0 -> 0 + -1 -> 1 + 1 -> 2 + -2 -> 3 + 2 -> 4 + -3 -> 5 + 3 -> 6 + ... + n >= 0 -> 2 * n + n < 0 -> -2 * n - 1 + + The key property is that the ZigZag output is small whenever the input's + absolute magnitude is small, regardless of sign. This makes sint32 much + more efficient than int32 for fields that frequently hold negative values: + + Encoding -1 with encode_int: 10 bytes (64-bit two's complement) + Encoding -1 with encode_sint32: 1 byte (ZigZag maps -1 to 1) + + Encoding -64 with encode_int: 10 bytes + Encoding -64 with encode_sint32: 2 bytes (ZigZag maps -64 to 127) + + The official formula for the 32-bit ZigZag transformation is: + + zigzag32(n) = (n << 1) ^ (n >> 31) + + The right shift (n >> 31) is arithmetic in Python, propagating the sign + bit: it produces 0 for non-negative n and -1 (all bits set) for negative n. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # Step 1: apply the ZigZag transformation. + # + # (value << 1) shifts all bits one position left. This doubles the absolute + # value and frees bit 0 to carry the original sign. For positive n the + # result is 2*n (an even number). For negative n, the shift operates on the + # two's complement representation. + # + # (value >> 31) is an arithmetic right shift by 31 positions. Python + # propagates the sign bit into all vacated positions, so: + # + # non-negative n -> (n >> 31) == 0 (binary: all zeros) + # negative n -> (n >> 31) == -1 (binary: all ones, i.e. 0xFF...FF) + # + # XOR with 0 is a no-op, so positive inputs pass through as 2*n. + # XOR with -1 flips every bit, which is equivalent to bitwise NOT. For + # negative inputs that produces the ZigZag output -2*n - 1. + # + # Worked examples: + # + # n = -1: + # (-1 << 1) == -2 binary: ...1111_1110 + # (-1 >> 31) == -1 binary: ...1111_1111 + # (-2) ^ (-1) == 1 binary: ...0000_0001 ✓ ZigZag(-1) = 1 + # + # n = -2: + # (-2 << 1) == -4 binary: ...1111_1100 + # (-2 >> 31) == -1 binary: ...1111_1111 + # (-4) ^ (-1) == 3 binary: ...0000_0011 ✓ ZigZag(-2) = 3 + # + # n = 1: + # (1 << 1) == 2 binary: ...0000_0010 + # (1 >> 31) == 0 binary: ...0000_0000 + # 2 ^ 0 == 2 binary: ...0000_0010 ✓ ZigZag(1) = 2 + zigzag_value = (value << 1) ^ (value >> 31) + + # Step 2: mask to 32 bits. + # + # Python integers have arbitrary precision, so the XOR result above can + # have more than 32 significant bits for inputs outside the sint32 range + # [-2^31, 2^31 - 1]. The mask clips the result to the 32-bit unsigned + # domain [0, 2^32 - 1], which is the correct output range for sint32 + # ZigZag encoding. + # + # For valid sint32 inputs the ZigZag result already fits in 32 bits and + # the mask has no effect. + # + # 0xFFFFFFFF == 2^32 - 1 == 32 bits of all ones. + zigzag_value &= 0xFFFFFFFF + + # Step 3: varint-encode the non-negative ZigZag result. + # + # The ZigZag value is now in [0, 2^32 - 1]. encode_varint encodes it in + # 1–5 bytes depending on its magnitude, compared to the flat 10 bytes that + # encode_int would use for any negative input. + return encode_varint(zigzag_value) + + +def encode_sint64(value: int) -> bytes: + """Encode a signed 64-bit integer using ZigZag encoding for sint64 fields. + + This is the 64-bit counterpart of encode_sint32. The ZigZag interleaving + and the three-step structure (transform, mask, varint-encode) are identical. + The only differences from encode_sint32 are: + + - The arithmetic right shift uses 63 instead of 31, propagating the + sign bit across all 63 remaining bit positions of a 64-bit value. + - The mask uses 0xFFFFFFFFFFFFFFFF (64 ones) instead of 0xFFFFFFFF + (32 ones), constraining the output to [0, 2^64 - 1]. + - The varint output is at most 10 bytes instead of 5. + + The ZigZag mapping is the same interleaving as sint32, extended to 64 bits: + + 0 -> 0 + -1 -> 1 + 1 -> 2 + -2 -> 3 + 2 -> 4 + ... + n >= 0 -> 2 * n + n < 0 -> -2 * n - 1 + + The official formula for the 64-bit ZigZag transformation is: + + zigzag64(n) = (n << 1) ^ (n >> 63) + + Worked examples: + + n = -1: + (-1 << 1) == -2 binary: ...1111_1110 + (-1 >> 63) == -1 binary: ...1111_1111 + (-2) ^ (-1) == 1 binary: ...0000_0001 ✓ ZigZag(-1) = 1 + Encoded: b'\\x01' (1 byte, vs 10 bytes from encode_int) + + n = -(2**63): (most negative sint64 value) + zigzag64(-(2**63)) == 2**64 - 1 (largest 64-bit unsigned integer) + Encoded: 10 bytes (the worst case for sint64) + + n = 2**63 - 1: (most positive sint64 value) + zigzag64(2**63 - 1) == 2**64 - 2 + Encoded: 10 bytes (the worst case alongside the above) + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # Step 1: apply the 64-bit ZigZag transformation. + # + # (value >> 63) produces 0 for non-negative values and -1 for negative + # values, for the same reason as (value >> 31) in encode_sint32: Python + # performs arithmetic right shifts that propagate the sign bit. + # + # The XOR with 0 leaves positive inputs unchanged (result = 2*n). + # The XOR with -1 flips every bit for negative inputs (result = -2*n - 1). + zigzag_value = (value << 1) ^ (value >> 63) + + # Step 2: mask to 64 bits. + # + # Same rationale as in encode_sint32: Python integers are unbounded, so + # the mask constrains the ZigZag result to the valid 64-bit unsigned range + # [0, 2^64 - 1]. For inputs in the sint64 range [-2^63, 2^63 - 1] the + # result already fits and the mask is a no-op. + # + # 0xFFFFFFFFFFFFFFFF == 2^64 - 1 == 64 bits of all ones. + zigzag_value &= 0xFFFFFFFFFFFFFFFF + + # Step 3: varint-encode the non-negative ZigZag result. + # + # The ZigZag value is in [0, 2^64 - 1], which encode_varint encodes in + # 1–10 bytes. The maximum 10-byte output only occurs at the extremes of + # the sint64 range (see worked examples in the docstring above). + return encode_varint(zigzag_value) + + +# ── Wire type 5 — 32-bit fixed-width ───────────────────────────────────────── + + +def encode_float(value: float) -> bytes: + """Encode a Python float as a protobuf float field value. + + Protobuf float fields use wire type 5 (32-bit fixed-width). The value is + stored as a 4-byte IEEE 754 single-precision floating-point number in + little-endian byte order. + + Unlike varint-encoded numbers, wire type 5 always occupies exactly 4 bytes. + This makes float fields predictable in size but more expensive than varint + for small integer-valued floats. + + Precision note + -------------- + Python's float type is a 64-bit IEEE 754 double-precision number. Converting + to single precision loses approximately 7 significant decimal digits of + precision (single has ~7, double has ~15-16). pack handles the conversion + silently; values outside the float32 range become inf or -inf. + + Byte layout — IEEE 754 single precision (32 bits) + -------------------------------------------------- + The 32 bits are arranged as: + + bit 31: sign (0 = positive, 1 = negative) + bits 30–23: exponent, biased by 127 + bits 22–0: mantissa (the fractional part, with an implicit leading 1) + + The four bytes are written in little-endian order: the least significant + byte is first. + + Example — encoding 1.0: + + IEEE 754 single for 1.0: + sign = 0 + exponent = 127 (biased) = 0b0111_1111 + mantissa = 0 (implicit leading 1, no fractional part) + + Combined 32-bit value: 0x3F800000 + + Little-endian bytes: 0x00, 0x00, 0x80, 0x3F + + Result: b'\\x00\\x00\\x80\\x3f' + + Example — encoding -1.0: + + Same as 1.0 but with the sign bit set. + + Combined 32-bit value: 0xBF800000 + + Little-endian bytes: 0x00, 0x00, 0x80, 0xBF + + Result: b'\\x00\\x00\\x80\\xbf' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode an unsigned integer as a protobuf fixed32 field value. + + Protobuf fixed32 fields use wire type 5 (32-bit fixed-width). The value + is stored as a 4-byte unsigned integer in little-endian byte order. + + The valid range is [0, 2^32 - 1]. + + Unlike uint32, which uses a varint and grows with the value's magnitude, + fixed32 always occupies exactly 4 bytes. This makes fixed32 more efficient + than uint32 for values that are consistently large (roughly above 2^28, + where a varint would already require 5 bytes), and less efficient for small + values. + + Example — encoding 1: + + 1 as a 4-byte little-endian unsigned integer: + + byte 0 (LSB): 0x01 + byte 1: 0x00 + byte 2: 0x00 + byte 3 (MSB): 0x00 + + Result: b'\\x01\\x00\\x00\\x00' + + Example — encoding 2^32 - 1 (maximum value): + + 0xFFFFFFFF as little-endian bytes: 0xFF, 0xFF, 0xFF, 0xFF + + Result: b'\\xff\\xff\\xff\\xff' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode a signed integer as a protobuf sfixed32 field value. + + Protobuf sfixed32 fields use wire type 5 (32-bit fixed-width). The value + is stored as a 4-byte signed integer in little-endian two's complement byte + order. + + The valid range is [-2^31, 2^31 - 1]. + + Unlike int32, which uses a varint and always costs 10 bytes for negative + values, sfixed32 always occupies exactly 4 bytes regardless of sign. This + makes sfixed32 more efficient than int32 for negative values and for large + positive values near 2^31. + + Example — encoding -1: + + -1 in 32-bit two's complement: 0xFFFFFFFF + + Little-endian bytes: 0xFF, 0xFF, 0xFF, 0xFF + + Result: b'\\xff\\xff\\xff\\xff' + + Example — encoding -2^31 (minimum value): + + -2147483648 in 32-bit two's complement: 0x80000000 + + Little-endian bytes: 0x00, 0x00, 0x00, 0x80 + + Result: b'\\x00\\x00\\x00\\x80' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode a Python float as a protobuf double field value. + + Protobuf double fields use wire type 1 (64-bit fixed-width). The value is + stored as an 8-byte IEEE 754 double-precision floating-point number in + little-endian byte order. + + Unlike wire type 5 (float), wire type 1 uses 8 bytes and matches Python's + native float precision exactly — no precision is lost in the conversion. + + Byte layout — IEEE 754 double precision (64 bits) + -------------------------------------------------- + The 64 bits are arranged as: + + bit 63: sign (0 = positive, 1 = negative) + bits 62–52: exponent, biased by 1023 + bits 51–0: mantissa (the fractional part, with an implicit leading 1) + + The eight bytes are written in little-endian order. + + Example — encoding 1.0: + + IEEE 754 double for 1.0: + sign = 0 + exponent = 1023 (biased) = 0b011_1111_1111 + mantissa = 0 + + Combined 64-bit value: 0x3FF0000000000000 + + Little-endian bytes: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F + + Result: b'\\x00\\x00\\x00\\x00\\x00\\x00\\xf0\\x3f' + + Special values (inf, -inf, nan) are represented in their standard IEEE 754 + double-precision forms and are encoded without error. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode an unsigned integer as a protobuf fixed64 field value. + + Protobuf fixed64 fields use wire type 1 (64-bit fixed-width). The value + is stored as an 8-byte unsigned integer in little-endian byte order. + + The valid range is [0, 2^64 - 1]. + + fixed64 always occupies exactly 8 bytes. It is more efficient than uint64 + (varint) for values consistently above 2^56, where a varint would already + require 9–10 bytes. For smaller values, uint64's variable length is more + compact. + + Example — encoding 1: + + 1 as an 8-byte little-endian unsigned integer: + + bytes: 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + + Result: b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00' + + Example — encoding 2^64 - 1 (maximum value): + + 0xFFFFFFFFFFFFFFFF as little-endian bytes: eight 0xFF bytes. + + Result: b'\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode a signed integer as a protobuf sfixed64 field value. + + Protobuf sfixed64 fields use wire type 1 (64-bit fixed-width). The value + is stored as an 8-byte signed integer in little-endian two's complement + byte order. + + The valid range is [-2^63, 2^63 - 1]. + + sfixed64 always occupies exactly 8 bytes regardless of sign. Unlike int64 + (varint), which always costs 10 bytes for negative values, sfixed64 saves + 2 bytes for negative inputs and for large positive values near 2^63. + + Example — encoding -1: + + -1 in 64-bit two's complement: 0xFFFFFFFFFFFFFFFF + + Little-endian bytes: eight 0xFF bytes. + + Result: b'\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff' + + Example — encoding -2^63 (minimum value): + + -9223372036854775808 in 64-bit two's complement: 0x8000000000000000 + + Little-endian bytes: 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 + + Result: b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # pack with format ' bytes: + """Encode a Python str as a protobuf string field value. + + Protobuf string fields use wire type 2 (length-delimited). The encoding + is a two-part sequence: + + 1. A varint giving the number of bytes in the UTF-8 representation of + the string. This is the byte count, not the character count — they + differ for any character outside ASCII. + 2. The UTF-8 encoded bytes of the string. + + The protobuf spec requires that string fields contain valid UTF-8. Python + str objects are always valid Unicode, so encoding to UTF-8 always succeeds + (Python's str cannot represent unpaired surrogates in normal use). + + The length prefix allows the decoder to know exactly how many bytes to read + for the string value without scanning for a terminator. + + Example — encoding "hi": + + UTF-8 bytes: b'hi' (2 bytes, both ASCII) + Length varint: encode_varint(2) == b'\\x02' + Result: b'\\x02hi' + + Example — encoding "é" (U+00E9, LATIN SMALL LETTER E WITH ACUTE): + + UTF-8 bytes: b'\\xc3\\xa9' (2 bytes; this character needs 2 UTF-8 bytes) + Length varint: encode_varint(2) == b'\\x02' + Result: b'\\x02\\xc3\\xa9' + + Example — encoding "" (empty string): + + UTF-8 bytes: b'' (0 bytes) + Length varint: encode_varint(0) == b'\\x00' + Result: b'\\x00' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # Encode the string to UTF-8 bytes. + # + # UTF-8 is the only encoding the protobuf spec allows for string fields. + # The result is a bytes object whose length may be greater than len(value) + # if the string contains non-ASCII characters (each such character encodes + # to 2–4 UTF-8 bytes). + utf8_bytes = value.encode("utf-8") + + # The length prefix is the number of UTF-8 bytes, encoded as a varint. + # + # The decoder reads this varint first to know how many bytes to consume + # for the field value. Without the length prefix, the decoder would have + # no way to find where the string ends in the byte stream. + length_prefix = encode_varint(len(utf8_bytes)) + + # Concatenate the length prefix and the UTF-8 bytes. + # + # The + operator on bytes objects produces a new bytes object containing + # the bytes of length_prefix followed immediately by the bytes of + # utf8_bytes. This is the complete wire encoding for the field value. + return length_prefix + utf8_bytes + + +def encode_bytes(value: bytes) -> bytes: + """Encode a Python bytes object as a protobuf bytes field value. + + Protobuf bytes fields use wire type 2 (length-delimited). The encoding + is a two-part sequence: + + 1. A varint giving the number of raw bytes in the payload. + 2. The raw bytes themselves, copied verbatim. + + Unlike string fields, bytes fields impose no encoding constraint on their + content — any byte sequence is valid, including sequences that are not + valid UTF-8. + + The length prefix allows the decoder to know exactly how many bytes to read + for the field value without scanning for a terminator. + + Example — encoding b'\\x00\\x01\\x02': + + Payload length: 3 bytes + Length varint: encode_varint(3) == b'\\x03' + Result: b'\\x03\\x00\\x01\\x02' + + Example — encoding b'' (empty bytes): + + Payload length: 0 bytes + Length varint: encode_varint(0) == b'\\x00' + Result: b'\\x00' + + Example — encoding b'\\xff\\xff': + + Payload length: 2 bytes + Length varint: encode_varint(2) == b'\\x02' + Result: b'\\x02\\xff\\xff' + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # The length prefix is the number of bytes in value, encoded as a varint. + # + # For large payloads (e.g. 128 bytes or more), encode_varint returns a + # multi-byte varint for the length prefix. For payloads up to 127 bytes + # the length fits in a single varint byte. + length_prefix = encode_varint(len(value)) + + # Concatenate the length prefix and the raw payload bytes. + # + # value is copied verbatim — no transformation is applied to the content. + return length_prefix + value diff --git a/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/tag.py b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/tag.py new file mode 100644 index 00000000000..70b081f6ffe --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/tag.py @@ -0,0 +1,151 @@ +"""Tag encoding for the protobuf wire format. + +Every protobuf message on the wire is a flat sequence of records. Each record +has exactly two parts: + + 1. A tag — a varint that encodes the field number and wire type. + 2. A value — the encoded field value, whose byte layout depends on the wire + type stored in the preceding tag. + +The tag is the reader's only guide to what follows. Without it, the decoder +would not know where one field ends and the next begins. + +Wire types +---------- +The protobuf spec defines six wire types. The wire type tells the decoder how +many bytes to consume for the value — not what the logical type is, just how +to delimit it on the wire: + + 0 Varint — one or more bytes, continuation bit in MSB of each byte. + 1 64-bit — exactly 8 bytes (used for fixed64, sfixed64, double). + 2 Length-delimited — a varint length prefix followed by that many bytes + (used for string, bytes, embedded messages, packed + repeated fields). + 3 Start group — deprecated; marks the start of a group (proto2 only). + 4 End group — deprecated; marks the end of a group (proto2 only). + 5 32-bit — exactly 4 bytes (used for fixed32, sfixed32, float). + +Wire types 3 and 4 are not used in proto3 and should not appear in new code. + +Tag bit layout +-------------- +The tag integer packs the field number and wire type into a single value: + + bits [2:0] — wire type (3 bits, enough for values 0–7) + bits [N:3] — field number (all remaining higher bits) + +The formula is: + + tag = (field_number << 3) | wire_type + +Three bits are reserved for the wire type because there are only six defined +wire type values (0–5), which fit comfortably in 3 bits (range 0–7). + +Example — field 1, wire type 0 (varint): + + field_number = 1 -> binary 0000_0001 + field_number << 3 -> binary 0000_1000 (decimal 8) + wire_type = 0 -> binary 0000_0000 + tag = 8 | 0 -> binary 0000_1000 (decimal 8) + + Varint-encoded: 0x08 (fits in one byte, no continuation bit needed) + +Example — field 2, wire type 2 (length-delimited): + + field_number = 2 -> binary 0000_0010 + field_number << 3 -> binary 0001_0000 (decimal 16) + wire_type = 2 -> binary 0000_0010 + tag = 16|2 -> binary 0001_0010 (decimal 18) + + Varint-encoded: 0x12 (one byte) + +Example — field 16, wire type 0 (varint): + + field_number = 16 -> binary 0001_0000 + field_number << 3 -> binary 1000_0000 (decimal 128) + wire_type = 0 -> binary 0000_0000 + tag = 128 -> varint requires two bytes: 0x80 0x01 + + The first byte 0x80 has its continuation bit set, meaning another byte + follows. The second byte 0x01 carries the remaining bits with no + continuation bit. This is the standard varint encoding for 128. + +Reference: + https://protobuf.dev/programming-guides/encoding/ +""" + +from .varint import encode_varint + + +def encode_tag(field_number: int, wire_type: int) -> bytes: + """Encode a protobuf record tag. + + The tag is the varint-encoded integer produced by: + + tag = (field_number << 3) | wire_type + + It is written before every field value in a serialised protobuf message. + The decoder reads this tag first to learn the field number (which .proto + field this value belongs to) and the wire type (how many bytes to read for + the value). + + Parameters + ---------- + field_number: + The field number as declared in the .proto schema. Must be a positive + integer. Field numbers 1–15 produce a one-byte tag for wire types 0–5; + field numbers 16–2047 produce a two-byte tag. + wire_type: + One of the six protobuf wire type constants (0–5). See the module + docstring for the full list and their meanings. + + Reference: + https://protobuf.dev/programming-guides/encoding/ + """ + # field_number << 3 shifts the field number left by three bit positions. + # + # This makes room in the three lowest bits of the tag integer for the wire + # type. The three-bit reservation comes directly from the protobuf spec: + # wire types 0–5 fit in 3 bits, so the spec dedicates exactly 3 bits to + # the wire type in every tag. + # + # Example: + # + # field_number = 1 + # field_number in binary = 0000_0001 + # field_number << 3 = 0000_1000 (decimal 8) + # + # After the shift, bits [2:0] are always zero, leaving space for + # the wire type to be OR'd in below. + field_number_bits = field_number << 3 + + # The bitwise OR writes the wire type into the lowest three bits. + # + # Because field_number_bits always has its lowest three bits set to zero + # (from the shift above), and wire_type is always in the range 0–5 (which + # fits in three bits), the OR simply places the wire type value into those + # three vacated bit positions without disturbing the field number bits. + # + # Example (field 1, wire type 2): + # + # field_number_bits = 0b0000_1000 (8) + # wire_type = 0b0000_0010 (2) + # tag = 0b0000_1010 (10) + # + # Example (field 15, wire type 0): + # + # field_number_bits = 0b0111_1000 (120) + # wire_type = 0b0000_0000 (0) + # tag = 0b0111_1000 (120) + # + # Field numbers 1–15 combined with any wire type 0–5 always produce a tag + # integer of at most 127, which encodes as a single varint byte. This is + # why the protobuf style guide recommends reserving field numbers 1–15 for + # the most frequently used fields: their tags are one byte instead of two. + tag = field_number_bits | wire_type + + # The tag integer is itself encoded as a varint before being written to the + # wire. For field numbers 1–15 the tag integer is at most 127, fitting in + # one varint byte. For field number 16 and above the tag integer exceeds + # 127 and requires two or more varint bytes. + return encode_varint(tag) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/varint.py b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/varint.py new file mode 100644 index 00000000000..d1d478f82cc --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/_pyprotobuf/varint.py @@ -0,0 +1,135 @@ +"""Varint encoding for the protobuf wire format. + +Reference: + https://protobuf.dev/programming-guides/encoding/ + +The protobuf encoding guide explains the wire format mostly from the point of +view of inspecting or decoding bytes that already exist. For example, it shows +how a varint byte sequence can be read by removing each byte's continuation bit, +then combining the remaining 7-bit payload groups. + +This module implements the encoder side of the same algorithm. Instead of +starting with bytes and recovering the integer, we start with the integer and +produce the bytes. That means the operations appear in the opposite direction: + + documented explanation / decoding view: + bytes -> remove continuation bits -> collect 7-bit groups -> integer + + implementation / encoding view: + integer -> extract 7-bit groups -> add continuation bits -> bytes + +The bit layout is the same in both directions. +""" + + +def encode_varint(value: int) -> bytes: + """Encode a non-negative integer as a protobuf varint. + + A protobuf varint stores an integer as one or more bytes. + + Each byte has two parts: + + bit 7: continuation bit + bits 0-6: seven payload bits + + The continuation bit is the most significant bit of the byte: + + 0b1000_0000 == 0x80 + + The payload bits are the lower seven bits of the byte: + + 0b0111_1111 == 0x7F + + The official protobuf encoding guide describes this format here: + + https://protobuf.dev/programming-guides/encoding/ + + The guide explains that the payload is split into 7-bit groups and that the + groups are stored in little-endian order. In this context, little-endian + means the least significant 7-bit group is written first. + + This implementation directly performs that encoder-side operation: + + 1. Take the least significant 7 bits of the integer. + 2. Write those 7 bits into the next output byte. + 3. If more integer bits remain, set the byte's continuation bit. + 4. Shift the integer right by 7 bits so the next group becomes the new + least significant group. + 5. Repeat until the remaining integer fits in one final 7-bit payload. + + This is different from the document's presentation because the document is + mostly showing how to interpret already-encoded bytes. Here we are producing + those bytes. The two views are inverse operations of the same wire-format + rule. + """ + # Protobuf varints, as implemented by this function, encode unsigned integer + # payloads. Signed integer fields need additional field-specific handling + # before varint encoding. For example, sint32/sint64 use ZigZag encoding + # before the resulting non-negative integer is encoded as a varint. + if value < 0: + raise ValueError("varint values must be non-negative") + + # bytearray is used because we build the encoded byte sequence one byte at a + # time. A bytearray is mutable, so appending to it is clearer and cheaper + # than repeatedly concatenating immutable bytes objects. + output = bytearray() + + # 0x7F is binary 0111_1111. + # + # If value is greater than 0x7F, it cannot fit in one protobuf varint byte, + # because one varint byte has only seven payload bits. In that case, we must + # emit one byte containing the current least significant 7-bit group and then + # continue encoding the remaining higher bits. + while value > 0x7F: + # value & 0x7F keeps only the lower seven bits of value. + # + # This extracts exactly one protobuf varint payload group. + # + # Example: + # + # value = 0b1001_0110 # decimal 150 + # 0x7F = 0b0111_1111 + # value & 0x7F = 0b0001_0110 # decimal 22 + # + # This corresponds to the guide's 7-bit payload group, but from the + # encoder direction. The guide often starts from encoded bytes and strips + # the continuation bit. Here we start from the integer and extract the + # payload bits before adding the continuation bit. + payload_bits = value & 0x7F + + # 0x80 is binary 1000_0000. + # + # payload_bits | 0x80 sets the most significant bit of the output byte. + # That most significant bit is the protobuf varint continuation bit. + # + # Setting it to 1 means: + # + # "this is not the final varint byte; another byte follows" + # + # This byte needs the continuation bit because the while condition has + # already proven that value has more than seven bits left to encode. + output.append(payload_bits | 0x80) + + # Shift value right by seven bits. + # + # This discards the payload group we just emitted and moves the next + # higher 7-bit group into the lowest seven bits, ready for the next loop + # iteration. + # + # This is why protobuf varints are little-endian at the 7-bit-group + # level: we emit the least significant group first, then move toward more + # significant groups. + value >>= 7 + + # When the loop ends, value is between 0 and 0x7F inclusive, so it fits in a + # single 7-bit payload group. + # + # This is the final varint byte, so we do NOT set the continuation bit. + # A most significant bit of 0 means: + # + # "this is the final byte of the varint" + output.append(value) + + # Convert the mutable bytearray into immutable bytes, which is the natural + # representation for encoded wire-format data in Python. + return bytes(output) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2.py new file mode 100644 index 00000000000..99d98932004 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2.py @@ -0,0 +1,32 @@ +"""Pure-Python equivalents of collector/logs/v1/logs_service_pb2.py. + +Field numbers: + ExportLogsServiceRequest resource_logs=1 + ExportLogsServiceResponse (empty — no fields used in export path) +""" + +from __future__ import annotations + +from opentelemetry._proto.logs.v1.logs_pb2 import ResourceLogs +from opentelemetry._proto._pyprotobuf.fields import msg + + +class ExportLogsServiceRequest: + def __init__(self, resource_logs: list[ResourceLogs] | None = None): + self.resource_logs: list[ResourceLogs] = ( + list(resource_logs) if resource_logs else [] + ) + + def SerializeToString(self) -> bytes: + return b"".join( + msg(1, rl.SerializeToString()) for rl in self.resource_logs + ) + + +class ExportLogsServiceResponse: + @classmethod + def FromString(cls, data: bytes) -> 'ExportLogsServiceResponse': + return cls() + + def SerializeToString(self) -> bytes: + return b"" diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2_grpc.py new file mode 100644 index 00000000000..51e1224144f --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/logs/v1/logs_service_pb2_grpc.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, + ExportLogsServiceResponse, +) + + +class LogsServiceStub: + def __init__(self, channel): + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.logs.v1.LogsService/Export', + request_serializer=ExportLogsServiceRequest.SerializeToString, + response_deserializer=ExportLogsServiceResponse.FromString, + ) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2.py new file mode 100644 index 00000000000..f9cdc448420 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2.py @@ -0,0 +1,32 @@ +"""Pure-Python equivalents of collector/metrics/v1/metrics_service_pb2.py. + +Field numbers: + ExportMetricsServiceRequest resource_metrics=1 + ExportMetricsServiceResponse (empty — no fields used in export path) +""" + +from __future__ import annotations + +from opentelemetry._proto.metrics.v1.metrics_pb2 import ResourceMetrics +from opentelemetry._proto._pyprotobuf.fields import msg + + +class ExportMetricsServiceRequest: + def __init__(self, resource_metrics: list[ResourceMetrics] | None = None): + self.resource_metrics: list[ResourceMetrics] = ( + list(resource_metrics) if resource_metrics else [] + ) + + def SerializeToString(self) -> bytes: + return b"".join( + msg(1, rm.SerializeToString()) for rm in self.resource_metrics + ) + + +class ExportMetricsServiceResponse: + @classmethod + def FromString(cls, data: bytes) -> 'ExportMetricsServiceResponse': + return cls() + + def SerializeToString(self) -> bytes: + return b"" diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2_grpc.py new file mode 100644 index 00000000000..4c523b4286e --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/metrics/v1/metrics_service_pb2_grpc.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, + ExportMetricsServiceResponse, +) + + +class MetricsServiceStub: + def __init__(self, channel): + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', + request_serializer=ExportMetricsServiceRequest.SerializeToString, + response_deserializer=ExportMetricsServiceResponse.FromString, + ) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2.py new file mode 100644 index 00000000000..de8801b934e --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2.py @@ -0,0 +1,32 @@ +"""Pure-Python equivalents of collector/trace/v1/trace_service_pb2.py. + +Field numbers: + ExportTraceServiceRequest resource_spans=1 + ExportTraceServiceResponse (empty — no fields used in export path) +""" + +from __future__ import annotations + +from opentelemetry._proto.trace.v1.trace_pb2 import ResourceSpans +from opentelemetry._proto._pyprotobuf.fields import msg + + +class ExportTraceServiceRequest: + def __init__(self, resource_spans: list[ResourceSpans] | None = None): + self.resource_spans: list[ResourceSpans] = ( + list(resource_spans) if resource_spans else [] + ) + + def SerializeToString(self) -> bytes: + return b"".join( + msg(1, rs.SerializeToString()) for rs in self.resource_spans + ) + + +class ExportTraceServiceResponse: + @classmethod + def FromString(cls, data: bytes) -> 'ExportTraceServiceResponse': + return cls() + + def SerializeToString(self) -> bytes: + return b"" diff --git a/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2_grpc.py new file mode 100644 index 00000000000..106455b1bba --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/collector/trace/v1/trace_service_pb2_grpc.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, + ExportTraceServiceResponse, +) + + +class TraceServiceStub: + def __init__(self, channel): + self.Export = channel.unary_unary( + '/opentelemetry.proto.collector.trace.v1.TraceService/Export', + request_serializer=ExportTraceServiceRequest.SerializeToString, + response_deserializer=ExportTraceServiceResponse.FromString, + ) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/common/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/common/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/common/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/common/v1/common_pb2.py new file mode 100644 index 00000000000..bb07a82c9a0 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/common/v1/common_pb2.py @@ -0,0 +1,132 @@ +"""Pure-Python equivalents of opentelemetry/proto/common/v1/common_pb2.py. + +Field numbers: + AnyValue (oneof value) string_value=1 bool_value=2 int_value=3 + double_value=4 array_value=5 kvlist_value=6 + bytes_value=7 + ArrayValue values=1 + KeyValueList values=1 + KeyValue key=1 value=2 + InstrumentationScope name=1 version=2 attributes=3 + dropped_attributes_count=4 +""" + +from __future__ import annotations + +from struct import pack + +from opentelemetry._proto._pyprotobuf import encode_int, encode_tag, encode_varint + +from opentelemetry._proto._pyprotobuf.fields import msg, string, u64, WT_LEN, WT_VARINT, WT_64BIT + + +class AnyValue: + """Oneof value container — exactly one field is set.""" + + def __init__( + self, + string_value: str | None = None, + bool_value: bool | None = None, + int_value: int | None = None, + double_value: float | None = None, + array_value: "ArrayValue | None" = None, + kvlist_value: "KeyValueList | None" = None, + bytes_value: bytes | None = None, + ): + self._which: str | None = None + if string_value is not None: + self.string_value = string_value + self._which = "string_value" + elif bool_value is not None: + self.bool_value = bool_value + self._which = "bool_value" + elif int_value is not None: + self.int_value = int_value + self._which = "int_value" + elif double_value is not None: + self.double_value = double_value + self._which = "double_value" + elif array_value is not None: + self.array_value = array_value + self._which = "array_value" + elif kvlist_value is not None: + self.kvlist_value = kvlist_value + self._which = "kvlist_value" + elif bytes_value is not None: + self.bytes_value = bytes_value + self._which = "bytes_value" + + def WhichOneof(self, oneof_name: str) -> str | None: + if oneof_name == "value": + return self._which + return None + + def SerializeToString(self) -> bytes: + # oneof: always written even when the value equals the proto3 default. + if self._which == "string_value": + utf8 = self.string_value.encode("utf-8") + return encode_tag(1, WT_LEN) + encode_varint(len(utf8)) + utf8 + if self._which == "bool_value": + return encode_tag(2, WT_VARINT) + encode_varint(1 if self.bool_value else 0) + if self._which == "int_value": + return encode_tag(3, WT_VARINT) + encode_int(self.int_value) + if self._which == "double_value": + return encode_tag(4, WT_64BIT) + pack(" bytes: + return b"".join(msg(1, v.SerializeToString()) for v in self.values) + + +class KeyValueList: + def __init__(self, values: "list[KeyValue] | None" = None): + self.values: list[KeyValue] = list(values) if values else [] + + def SerializeToString(self) -> bytes: + return b"".join(msg(1, kv.SerializeToString()) for kv in self.values) + + +class KeyValue: + def __init__(self, key: str = "", value: AnyValue | None = None): + self.key = key + self.value = value + + def SerializeToString(self) -> bytes: + result = string(1, self.key) + if self.value is not None: + result += msg(2, self.value.SerializeToString()) + return result + + +class InstrumentationScope: + def __init__( + self, + name: str = "", + version: str = "", + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + ): + self.name = name + self.version = version + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + + def SerializeToString(self) -> bytes: + return ( + string(1, self.name) + + string(2, self.version) + + b"".join(msg(3, kv.SerializeToString()) for kv in self.attributes) + + u64(4, self.dropped_attributes_count) + ) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/logs/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/logs/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/logs/v1/logs_pb2.py new file mode 100644 index 00000000000..29c06607ed7 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/logs/v1/logs_pb2.py @@ -0,0 +1,114 @@ +"""Pure-Python equivalents of opentelemetry/proto/logs/v1/logs_pb2.py. + +Field numbers: + LogRecord time_unix_nano=1 severity_number=2 severity_text=3 + body=5 attributes=6 dropped_attrs_count=7 flags=8 + trace_id=9 span_id=10 observed_time_unix_nano=11 + event_name=12 + ScopeLogs scope=1 log_records=2 schema_url=3 + ResourceLogs resource=1 scope_logs=2 schema_url=3 +""" + +from __future__ import annotations + +from opentelemetry._proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource +from opentelemetry._proto._pyprotobuf.fields import ( + byt, + fix32, + fix64, + msg, + string, + u64, +) + + +class LogRecord: + def __init__( + self, + time_unix_nano: int = 0, + severity_number: int = 0, + severity_text: str = "", + body: AnyValue | None = None, + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + flags: int = 0, + trace_id: bytes = b"", + span_id: bytes = b"", + observed_time_unix_nano: int = 0, + event_name: str = "", + ): + self.time_unix_nano = time_unix_nano + self.severity_number = severity_number + self.severity_text = severity_text + self.body = body + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + self.flags = flags + self.trace_id = trace_id + self.span_id = span_id + self.observed_time_unix_nano = observed_time_unix_nano + self.event_name = event_name + + def SerializeToString(self) -> bytes: + result = ( + fix64(1, self.time_unix_nano) + + u64(2, self.severity_number) + + string(3, self.severity_text) + ) + if self.body is not None: + result += msg(5, self.body.SerializeToString()) + result += ( + b"".join(msg(6, kv.SerializeToString()) for kv in self.attributes) + + u64(7, self.dropped_attributes_count) + + fix32(8, self.flags) + + byt(9, self.trace_id) + + byt(10, self.span_id) + + fix64(11, self.observed_time_unix_nano) + + string(12, self.event_name) + ) + return result + + +class ScopeLogs: + def __init__( + self, + scope: InstrumentationScope | None = None, + log_records: list[LogRecord] | None = None, + schema_url: str = "", + ): + self.scope = scope + self.log_records: list[LogRecord] = list(log_records) if log_records else [] + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.scope is not None: + result += msg(1, self.scope.SerializeToString()) + result += b"".join(msg(2, lr.SerializeToString()) for lr in self.log_records) + result += string(3, self.schema_url) + return result + + +class ResourceLogs: + def __init__( + self, + resource: Resource | None = None, + scope_logs: list[ScopeLogs] | None = None, + schema_url: str = "", + ): + self.resource = resource + self.scope_logs: list[ScopeLogs] = list(scope_logs) if scope_logs else [] + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.resource is not None: + result += msg(1, self.resource.SerializeToString()) + result += b"".join(msg(2, sl.SerializeToString()) for sl in self.scope_logs) + result += string(3, self.schema_url) + return result diff --git a/opentelemetry-proto/src/opentelemetry/_proto/metrics/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/metrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/metrics/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/metrics/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/metrics/v1/metrics_pb2.py new file mode 100644 index 00000000000..f479ac44a6b --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/metrics/v1/metrics_pb2.py @@ -0,0 +1,500 @@ +"""Pure-Python equivalents of opentelemetry/proto/metrics/v1/metrics_pb2.py. + +Field numbers: + Exemplar filtered_attributes=7 time_unix_nano=2 + as_double=3(wire_64bit) as_int=6(sfixed64) + span_id=4 trace_id=5 + NumberDataPoint attributes=7 start_time_unix_nano=2 time_unix_nano=3 + as_double=4(wire_64bit) as_int=6(sfixed64) + exemplars=5 flags=8 + HistogramDataPoint attributes=9 start_time_unix_nano=2 time_unix_nano=3 + count=4(fixed64) sum=5(opt_dbl) + bucket_counts=6(packed_fix64) + explicit_bounds=7(packed_dbl) exemplars=8 + flags=10 min=11(opt_dbl) max=12(opt_dbl) + ExponentialHistogramDataPoint + attributes=1 start_time_unix_nano=2 time_unix_nano=3 + count=4(fixed64) sum=5(opt_dbl) scale=6(sint32) + zero_count=7(fixed64) positive=8 negative=9 + flags=10 exemplars=11 min=12(opt_dbl) + max=13(opt_dbl) zero_threshold=14(dbl) + ExponentialHistogramDataPoint.Buckets + offset=1(sint32) bucket_counts=2(packed_uint64) + SummaryDataPoint attributes=7 start_time_unix_nano=2 time_unix_nano=3 + count=4(fixed64) sum=5(dbl) quantile_values=6 flags=8 + SummaryDataPoint.ValueAtQuantile quantile=1(dbl) value=2(dbl) + Gauge data_points=1 + Sum data_points=1 aggregation_temporality=2 is_monotonic=3 + Histogram data_points=1 aggregation_temporality=2 + ExponentialHistogram data_points=1 aggregation_temporality=2 + Summary data_points=1 + Metric name=1 description=2 unit=3 + oneof data: gauge=5 sum=7 histogram=9 + exponential_histogram=10 summary=11 + ScopeMetrics scope=1 metrics=2 schema_url=3 + ResourceMetrics resource=1 scope_metrics=2 schema_url=3 +""" + +from __future__ import annotations + +from struct import pack + +from opentelemetry._proto.common.v1.common_pb2 import ( + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource +from opentelemetry._proto._pyprotobuf.fields import ( + byt, + bool_field, + dbl, + fix64, + msg, + opt_dbl, + packed_double, + packed_fix64, + packed_uint64, + sint32, + string, + u64, + WT_64BIT, +) +from opentelemetry._proto._pyprotobuf import encode_tag + + +def _sfixed64(field: int, value: int) -> bytes: + """sfixed64 field (little-endian signed 64-bit int).""" + if value == 0: + return b"" + return encode_tag(field, WT_64BIT) + pack(" bytes: + """Oneof as_double: always write, even if 0.0.""" + return encode_tag(field, WT_64BIT) + pack(" bytes: + """Oneof as_int: always write, even if 0.""" + return encode_tag(field, WT_64BIT) + pack(" str | None: + if oneof_name == "value": + return self._which + return None + + def SerializeToString(self) -> bytes: + result = fix64(2, self.time_unix_nano) + if self._which == "as_double": + result += _as_double(3, self._as_double) # type: ignore[arg-type] + result += byt(4, self.span_id) + byt(5, self.trace_id) + if self._which == "as_int": + result += _as_int(6, self._as_int) # type: ignore[arg-type] + result += b"".join( + msg(7, kv.SerializeToString()) for kv in self.filtered_attributes + ) + return result + + +class NumberDataPoint: + def __init__( + self, + attributes: list[KeyValue] | None = None, + start_time_unix_nano: int = 0, + time_unix_nano: int = 0, + as_double: float | None = None, + as_int: int | None = None, + exemplars: list[Exemplar] | None = None, + flags: int = 0, + ): + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.start_time_unix_nano = start_time_unix_nano + self.time_unix_nano = time_unix_nano + self._as_double = as_double + self._as_int = as_int + self.exemplars: list[Exemplar] = list(exemplars) if exemplars else [] + self.flags = flags + self._which = ( + "as_double" if as_double is not None else + "as_int" if as_int is not None else None + ) + + def WhichOneof(self, oneof_name: str) -> str | None: + if oneof_name == "value": + return self._which + return None + + def SerializeToString(self) -> bytes: + result = fix64(2, self.start_time_unix_nano) + fix64(3, self.time_unix_nano) + if self._which == "as_double": + result += _as_double(4, self._as_double) # type: ignore[arg-type] + result += b"".join(msg(5, ex.SerializeToString()) for ex in self.exemplars) + if self._which == "as_int": + result += _as_int(6, self._as_int) # type: ignore[arg-type] + result += b"".join(msg(7, kv.SerializeToString()) for kv in self.attributes) + result += u64(8, self.flags) + return result + + +class HistogramDataPoint: + def __init__( + self, + attributes: list[KeyValue] | None = None, + start_time_unix_nano: int = 0, + time_unix_nano: int = 0, + count: int = 0, + sum: float | None = None, + bucket_counts: list[int] | None = None, + explicit_bounds: list[float] | None = None, + exemplars: list[Exemplar] | None = None, + flags: int = 0, + min: float | None = None, + max: float | None = None, + ): + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.start_time_unix_nano = start_time_unix_nano + self.time_unix_nano = time_unix_nano + self.count = count + self.sum = sum + self.bucket_counts: list[int] = list(bucket_counts) if bucket_counts else [] + self.explicit_bounds: list[float] = ( + list(explicit_bounds) if explicit_bounds else [] + ) + self.exemplars: list[Exemplar] = list(exemplars) if exemplars else [] + self.flags = flags + self.min = min + self.max = max + + def SerializeToString(self) -> bytes: + return ( + fix64(2, self.start_time_unix_nano) + + fix64(3, self.time_unix_nano) + + fix64(4, self.count) + + opt_dbl(5, self.sum) + + packed_fix64(6, self.bucket_counts) + + packed_double(7, self.explicit_bounds) + + b"".join(msg(8, ex.SerializeToString()) for ex in self.exemplars) + + b"".join(msg(9, kv.SerializeToString()) for kv in self.attributes) + + u64(10, self.flags) + + opt_dbl(11, self.min) + + opt_dbl(12, self.max) + ) + + +class ExponentialHistogramDataPoint: + class Buckets: + def __init__( + self, + offset: int = 0, + bucket_counts: list[int] | None = None, + ): + self.offset = offset + self.bucket_counts: list[int] = ( + list(bucket_counts) if bucket_counts else [] + ) + + def SerializeToString(self) -> bytes: + return ( + sint32(1, self.offset) + + packed_uint64(2, self.bucket_counts) + ) + + def __init__( + self, + attributes: list[KeyValue] | None = None, + start_time_unix_nano: int = 0, + time_unix_nano: int = 0, + count: int = 0, + sum: float | None = None, + scale: int = 0, + zero_count: int = 0, + positive: "ExponentialHistogramDataPoint.Buckets | None" = None, + negative: "ExponentialHistogramDataPoint.Buckets | None" = None, + flags: int = 0, + exemplars: list[Exemplar] | None = None, + min: float | None = None, + max: float | None = None, + zero_threshold: float = 0.0, + ): + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.start_time_unix_nano = start_time_unix_nano + self.time_unix_nano = time_unix_nano + self.count = count + self.sum = sum + self.scale = scale + self.zero_count = zero_count + self.positive = positive + self.negative = negative + self.flags = flags + self.exemplars: list[Exemplar] = list(exemplars) if exemplars else [] + self.min = min + self.max = max + self.zero_threshold = zero_threshold + + def SerializeToString(self) -> bytes: + result = b"".join(msg(1, kv.SerializeToString()) for kv in self.attributes) + result += ( + fix64(2, self.start_time_unix_nano) + + fix64(3, self.time_unix_nano) + + fix64(4, self.count) + + opt_dbl(5, self.sum) + + sint32(6, self.scale) + + fix64(7, self.zero_count) + ) + if self.positive is not None: + result += msg(8, self.positive.SerializeToString()) + if self.negative is not None: + result += msg(9, self.negative.SerializeToString()) + result += ( + u64(10, self.flags) + + b"".join(msg(11, ex.SerializeToString()) for ex in self.exemplars) + + opt_dbl(12, self.min) + + opt_dbl(13, self.max) + + dbl(14, self.zero_threshold) + ) + return result + + +class SummaryDataPoint: + class ValueAtQuantile: + def __init__(self, quantile: float = 0.0, value: float = 0.0): + self.quantile = quantile + self.value = value + + def SerializeToString(self) -> bytes: + return dbl(1, self.quantile) + dbl(2, self.value) + + def __init__( + self, + attributes: list[KeyValue] | None = None, + start_time_unix_nano: int = 0, + time_unix_nano: int = 0, + count: int = 0, + sum: float = 0.0, + quantile_values: list["SummaryDataPoint.ValueAtQuantile"] | None = None, + flags: int = 0, + ): + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.start_time_unix_nano = start_time_unix_nano + self.time_unix_nano = time_unix_nano + self.count = count + self.sum = sum + self.quantile_values: list[SummaryDataPoint.ValueAtQuantile] = ( + list(quantile_values) if quantile_values else [] + ) + self.flags = flags + + def SerializeToString(self) -> bytes: + return ( + fix64(2, self.start_time_unix_nano) + + fix64(3, self.time_unix_nano) + + fix64(4, self.count) + + dbl(5, self.sum) + + b"".join(msg(6, qv.SerializeToString()) for qv in self.quantile_values) + + b"".join(msg(7, kv.SerializeToString()) for kv in self.attributes) + + u64(8, self.flags) + ) + + +class Gauge: + def __init__(self, data_points: list[NumberDataPoint] | None = None): + self.data_points: list[NumberDataPoint] = ( + list(data_points) if data_points else [] + ) + + def SerializeToString(self) -> bytes: + return b"".join(msg(1, dp.SerializeToString()) for dp in self.data_points) + + +class Sum: + def __init__( + self, + data_points: list[NumberDataPoint] | None = None, + aggregation_temporality: int = 0, + is_monotonic: bool = False, + ): + self.data_points: list[NumberDataPoint] = ( + list(data_points) if data_points else [] + ) + self.aggregation_temporality = aggregation_temporality + self.is_monotonic = is_monotonic + + def SerializeToString(self) -> bytes: + temp = self.aggregation_temporality + temp_int = temp.value if hasattr(temp, "value") else int(temp) + return ( + b"".join(msg(1, dp.SerializeToString()) for dp in self.data_points) + + u64(2, temp_int) + + bool_field(3, self.is_monotonic) + ) + + +class Histogram: + def __init__( + self, + data_points: list[HistogramDataPoint] | None = None, + aggregation_temporality: int = 0, + ): + self.data_points: list[HistogramDataPoint] = ( + list(data_points) if data_points else [] + ) + self.aggregation_temporality = aggregation_temporality + + def SerializeToString(self) -> bytes: + temp = self.aggregation_temporality + temp_int = temp.value if hasattr(temp, "value") else int(temp) + return ( + b"".join(msg(1, dp.SerializeToString()) for dp in self.data_points) + + u64(2, temp_int) + ) + + +class ExponentialHistogram: + def __init__( + self, + data_points: list[ExponentialHistogramDataPoint] | None = None, + aggregation_temporality: int = 0, + ): + self.data_points: list[ExponentialHistogramDataPoint] = ( + list(data_points) if data_points else [] + ) + self.aggregation_temporality = aggregation_temporality + + def SerializeToString(self) -> bytes: + temp = self.aggregation_temporality + temp_int = temp.value if hasattr(temp, "value") else int(temp) + return ( + b"".join(msg(1, dp.SerializeToString()) for dp in self.data_points) + + u64(2, temp_int) + ) + + +class Summary: + def __init__(self, data_points: list[SummaryDataPoint] | None = None): + self.data_points: list[SummaryDataPoint] = ( + list(data_points) if data_points else [] + ) + + def SerializeToString(self) -> bytes: + return b"".join(msg(1, dp.SerializeToString()) for dp in self.data_points) + + +class Metric: + def __init__( + self, + name: str = "", + description: str = "", + unit: str = "", + gauge: Gauge | None = None, + sum: Sum | None = None, + histogram: Histogram | None = None, + exponential_histogram: ExponentialHistogram | None = None, + summary: Summary | None = None, + ): + self.name = name + self.description = description + self.unit = unit + self.gauge = gauge + self.sum = sum + self.histogram = histogram + self.exponential_histogram = exponential_histogram + self.summary = summary + + # Resolve oneof data field name + if gauge is not None: + self._which_data = "gauge" + elif sum is not None: + self._which_data = "sum" + elif histogram is not None: + self._which_data = "histogram" + elif exponential_histogram is not None: + self._which_data = "exponential_histogram" + elif summary is not None: + self._which_data = "summary" + else: + self._which_data = None + + def WhichOneof(self, oneof_name: str) -> str | None: + if oneof_name == "data": + return self._which_data + return None + + def SerializeToString(self) -> bytes: + result = string(1, self.name) + string(2, self.description) + string(3, self.unit) + if self.gauge is not None: + result += msg(5, self.gauge.SerializeToString()) + elif self.sum is not None: + result += msg(7, self.sum.SerializeToString()) + elif self.histogram is not None: + result += msg(9, self.histogram.SerializeToString()) + elif self.exponential_histogram is not None: + result += msg(10, self.exponential_histogram.SerializeToString()) + elif self.summary is not None: + result += msg(11, self.summary.SerializeToString()) + return result + + +class ScopeMetrics: + def __init__( + self, + scope: InstrumentationScope | None = None, + metrics: list[Metric] | None = None, + schema_url: str = "", + ): + self.scope = scope + self.metrics: list[Metric] = list(metrics) if metrics else [] + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.scope is not None: + result += msg(1, self.scope.SerializeToString()) + result += b"".join(msg(2, m.SerializeToString()) for m in self.metrics) + result += string(3, self.schema_url) + return result + + +class ResourceMetrics: + def __init__( + self, + resource: Resource | None = None, + scope_metrics: list[ScopeMetrics] | None = None, + schema_url: str = "", + ): + self.resource = resource + self.scope_metrics: list[ScopeMetrics] = ( + list(scope_metrics) if scope_metrics else [] + ) + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.resource is not None: + result += msg(1, self.resource.SerializeToString()) + result += b"".join(msg(2, sm.SerializeToString()) for sm in self.scope_metrics) + result += string(3, self.schema_url) + return result diff --git a/opentelemetry-proto/src/opentelemetry/_proto/resource/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/resource/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/resource/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/resource/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/resource/v1/resource_pb2.py new file mode 100644 index 00000000000..2fb21980e13 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/resource/v1/resource_pb2.py @@ -0,0 +1,26 @@ +"""Pure-Python equivalents of opentelemetry/proto/resource/v1/resource_pb2.py. + +Field numbers: + Resource attributes=1 dropped_attributes_count=2 +""" + +from __future__ import annotations + +from opentelemetry._proto.common.v1.common_pb2 import KeyValue +from opentelemetry._proto._pyprotobuf.fields import msg, u64 + + +class Resource: + def __init__( + self, + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + ): + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + + def SerializeToString(self) -> bytes: + return ( + b"".join(msg(1, kv.SerializeToString()) for kv in self.attributes) + + u64(2, self.dropped_attributes_count) + ) diff --git a/opentelemetry-proto/src/opentelemetry/_proto/trace/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/trace/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/trace/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/trace/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/_proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/_proto/trace/v1/trace_pb2.py new file mode 100644 index 00000000000..2c63cbd9cf9 --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/trace/v1/trace_pb2.py @@ -0,0 +1,188 @@ +"""Pure-Python equivalents of opentelemetry/proto/trace/v1/trace_pb2.py. + +Field numbers: + Status message=2 code=3 + Span.Event time_unix_nano=1 name=2 attributes=3 dropped_attrs_count=4 + Span.Link trace_id=1 span_id=2 trace_state=3 attributes=4 + dropped_attrs_count=5 flags=6 + Span trace_id=1 span_id=2 trace_state=3 parent_span_id=4 + flags=16 name=5 kind=6 start_time_unix_nano=7 + end_time_unix_nano=8 attributes=9 dropped_attrs_count=10 + events=11 dropped_events_count=12 links=13 + dropped_links_count=14 status=15 + ScopeSpans scope=1 spans=2 schema_url=3 + ResourceSpans resource=1 scope_spans=2 schema_url=3 +""" + +from __future__ import annotations + +from opentelemetry._proto.common.v1.common_pb2 import ( + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource +from opentelemetry._proto._pyprotobuf.fields import ( + byt, + fix32, + fix64, + msg, + string, + u64, +) + + +class Status: + def __init__(self, message: str = "", code: int = 0): + self.message = message + self.code = code + + def SerializeToString(self) -> bytes: + return string(2, self.message) + u64(3, self.code) + + +class Span: + class Event: + def __init__( + self, + time_unix_nano: int = 0, + name: str = "", + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + ): + self.time_unix_nano = time_unix_nano + self.name = name + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + + def SerializeToString(self) -> bytes: + return ( + fix64(1, self.time_unix_nano) + + string(2, self.name) + + b"".join(msg(3, kv.SerializeToString()) for kv in self.attributes) + + u64(4, self.dropped_attributes_count) + ) + + class Link: + def __init__( + self, + trace_id: bytes = b"", + span_id: bytes = b"", + trace_state: str = "", + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + flags: int = 0, + ): + self.trace_id = trace_id + self.span_id = span_id + self.trace_state = trace_state + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + self.flags = flags + + def SerializeToString(self) -> bytes: + return ( + byt(1, self.trace_id) + + byt(2, self.span_id) + + string(3, self.trace_state) + + b"".join(msg(4, kv.SerializeToString()) for kv in self.attributes) + + u64(5, self.dropped_attributes_count) + + fix32(6, self.flags) + ) + + def __init__( + self, + trace_id: bytes = b"", + span_id: bytes = b"", + trace_state: str = "", + parent_span_id: bytes = b"", + flags: int = 0, + name: str = "", + kind: int = 0, + start_time_unix_nano: int = 0, + end_time_unix_nano: int = 0, + attributes: list[KeyValue] | None = None, + dropped_attributes_count: int = 0, + events: list["Span.Event"] | None = None, + dropped_events_count: int = 0, + links: list["Span.Link"] | None = None, + dropped_links_count: int = 0, + status: Status | None = None, + ): + self.trace_id = trace_id + self.span_id = span_id + self.trace_state = trace_state + self.parent_span_id = parent_span_id + self.flags = flags + self.name = name + self.kind = kind + self.start_time_unix_nano = start_time_unix_nano + self.end_time_unix_nano = end_time_unix_nano + self.attributes: list[KeyValue] = list(attributes) if attributes else [] + self.dropped_attributes_count = dropped_attributes_count + self.events: list[Span.Event] = list(events) if events else [] + self.dropped_events_count = dropped_events_count + self.links: list[Span.Link] = list(links) if links else [] + self.dropped_links_count = dropped_links_count + self.status = status + + def SerializeToString(self) -> bytes: + result = ( + byt(1, self.trace_id) + + byt(2, self.span_id) + + string(3, self.trace_state) + + byt(4, self.parent_span_id) + + string(5, self.name) + + u64(6, self.kind) + + fix64(7, self.start_time_unix_nano) + + fix64(8, self.end_time_unix_nano) + + b"".join(msg(9, kv.SerializeToString()) for kv in self.attributes) + + u64(10, self.dropped_attributes_count) + + b"".join(msg(11, ev.SerializeToString()) for ev in self.events) + + u64(12, self.dropped_events_count) + + b"".join(msg(13, lk.SerializeToString()) for lk in self.links) + + u64(14, self.dropped_links_count) + ) + if self.status is not None: + result += msg(15, self.status.SerializeToString()) + result += fix32(16, self.flags) + return result + + +class ScopeSpans: + def __init__( + self, + scope: InstrumentationScope | None = None, + spans: list[Span] | None = None, + schema_url: str = "", + ): + self.scope = scope + self.spans: list[Span] = list(spans) if spans else [] + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.scope is not None: + result += msg(1, self.scope.SerializeToString()) + result += b"".join(msg(2, sp.SerializeToString()) for sp in self.spans) + result += string(3, self.schema_url) + return result + + +class ResourceSpans: + def __init__( + self, + resource: Resource | None = None, + scope_spans: list[ScopeSpans] | None = None, + schema_url: str = "", + ): + self.resource = resource + self.scope_spans: list[ScopeSpans] = list(scope_spans) if scope_spans else [] + self.schema_url = schema_url + + def SerializeToString(self) -> bytes: + result = b"" + if self.resource is not None: + result += msg(1, self.resource.SerializeToString()) + result += b"".join(msg(2, ss.SerializeToString()) for ss in self.scope_spans) + result += string(3, self.schema_url) + return result diff --git a/opentelemetry-proto/src/opentelemetry/_proto/version/__init__.py b/opentelemetry-proto/src/opentelemetry/_proto/version/__init__.py new file mode 100644 index 00000000000..cdc135bf8ae --- /dev/null +++ b/opentelemetry-proto/src/opentelemetry/_proto/version/__init__.py @@ -0,0 +1 @@ +__version__ = "1.45.0.dev" diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py index 81f124f6303..5cab986ec39 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.py @@ -1,34 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/collector/logs/v1/logs_service.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.logs.v1 import logs_pb2 as opentelemetry_dot_proto_dot_logs_dot_v1_dot_logs__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"u\n\x19\x45xportLogsServiceResponse\x12X\n\x0fpartial_success\x18\x01 \x01(\x0b\x32?.opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess\"O\n\x18\x45xportLogsPartialSuccess\x12\x1c\n\x14rejected_log_records\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x98\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\xaa\x02%OpenTelemetry.Proto.Collector.Logs.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.logs.v1.logs_service_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n(io.opentelemetry.proto.collector.logs.v1B\020LogsServiceProtoP\001Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\252\002%OpenTelemetry.Proto.Collector.Logs.V1' - _globals['_EXPORTLOGSSERVICEREQUEST']._serialized_start=139 - _globals['_EXPORTLOGSSERVICEREQUEST']._serialized_end=231 - _globals['_EXPORTLOGSSERVICERESPONSE']._serialized_start=233 - _globals['_EXPORTLOGSSERVICERESPONSE']._serialized_end=350 - _globals['_EXPORTLOGSPARTIALSUCCESS']._serialized_start=352 - _globals['_EXPORTLOGSPARTIALSUCCESS']._serialized_end=431 - _globals['_LOGSSERVICE']._serialized_start=434 - _globals['_LOGSSERVICE']._serialized_end=591 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi deleted file mode 100644 index 99e2a0ac101..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2.pyi +++ /dev/null @@ -1,117 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2020, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.logs.v1.logs_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class ExportLogsServiceRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_LOGS_FIELD_NUMBER: builtins.int - @property - def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs]: - """An array of ResourceLogs. - For data coming from a single resource this array will typically contain one - element. Intermediary nodes (such as OpenTelemetry Collector) that receive - data from multiple origins typically batch the data before forwarding further and - in that case this array will contain multiple elements. - """ - def __init__( - self, - *, - resource_logs: collections.abc.Iterable[opentelemetry.proto.logs.v1.logs_pb2.ResourceLogs] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_logs", b"resource_logs"]) -> None: ... - -global___ExportLogsServiceRequest = ExportLogsServiceRequest - -@typing_extensions.final -class ExportLogsServiceResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int - @property - def partial_success(self) -> global___ExportLogsPartialSuccess: - """The details of a partially successful export request. - - If the request is only partially accepted - (i.e. when the server accepts only parts of the data and rejects the rest) - the server MUST initialize the `partial_success` field and MUST - set the `rejected_` with the number of items it rejected. - - Servers MAY also make use of the `partial_success` field to convey - warnings/suggestions to senders even when the request was fully accepted. - In such cases, the `rejected_` MUST have a value of `0` and - the `error_message` MUST be non-empty. - - A `partial_success` message with an empty value (rejected_ = 0 and - `error_message` = "") is equivalent to it not being set/present. Senders - SHOULD interpret it the same way as in the full success case. - """ - def __init__( - self, - *, - partial_success: global___ExportLogsPartialSuccess | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... - -global___ExportLogsServiceResponse = ExportLogsServiceResponse - -@typing_extensions.final -class ExportLogsPartialSuccess(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - REJECTED_LOG_RECORDS_FIELD_NUMBER: builtins.int - ERROR_MESSAGE_FIELD_NUMBER: builtins.int - rejected_log_records: builtins.int - """The number of rejected log records. - - A `rejected_` field holding a `0` value indicates that the - request was fully accepted. - """ - error_message: builtins.str - """A developer-facing human-readable message in English. It should be used - either to explain why the server rejected parts of the data during a partial - success or to convey warnings/suggestions during a full success. The message - should offer guidance on how users can address such issues. - - error_message is an optional field. An error_message with an empty value - is equivalent to it not being set. - """ - def __init__( - self, - *, - rejected_log_records: builtins.int = ..., - error_message: builtins.str = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_log_records", b"rejected_log_records"]) -> None: ... - -global___ExportLogsPartialSuccess = ExportLogsPartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py index bb64c98fa25..da80a79f0c0 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py @@ -1,110 +1,5 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -from opentelemetry.proto.collector.logs.v1 import logs_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2 -GRPC_GENERATED_VERSION = '1.63.2' -GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - warnings.warn( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in opentelemetry/proto/collector/logs/v1/logs_service_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning - ) - - -class LogsServiceStub(object): - """Service that can be used to push logs between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this - case logs are sent/received to/from multiple Applications). - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Export = channel.unary_unary( - '/opentelemetry.proto.collector.logs.v1.LogsService/Export', - request_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, - _registered_method=True) - - -class LogsServiceServicer(object): - """Service that can be used to push logs between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this - case logs are sent/received to/from multiple Applications). - """ - - def Export(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_LogsServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Export': grpc.unary_unary_rpc_method_handler( - servicer.Export, - request_deserializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.collector.logs.v1.LogsService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class LogsService(object): - """Service that can be used to push logs between one Application instrumented with - OpenTelemetry and an collector, or between an collector and a central collector (in this - case logs are sent/received to/from multiple Applications). - """ - - @staticmethod - def Export(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/opentelemetry.proto.collector.logs.v1.LogsService/Export', - opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceRequest.SerializeToString, - opentelemetry_dot_proto_dot_collector_dot_logs_dot_v1_dot_logs__service__pb2.ExportLogsServiceResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) +from opentelemetry._proto.collector.logs.v1.logs_service_pb2_grpc import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py index 6083655c882..d111e2ce3df 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.py @@ -1,34 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.metrics.v1 import metrics_pb2 as opentelemetry_dot_proto_dot_metrics_dot_v1_dot_metrics__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.metrics.v1.metrics_service_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n+io.opentelemetry.proto.collector.metrics.v1B\023MetricsServiceProtoP\001Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\252\002(OpenTelemetry.Proto.Collector.Metrics.V1' - _globals['_EXPORTMETRICSSERVICEREQUEST']._serialized_start=154 - _globals['_EXPORTMETRICSSERVICEREQUEST']._serialized_end=258 - _globals['_EXPORTMETRICSSERVICERESPONSE']._serialized_start=260 - _globals['_EXPORTMETRICSSERVICERESPONSE']._serialized_end=386 - _globals['_EXPORTMETRICSPARTIALSUCCESS']._serialized_start=388 - _globals['_EXPORTMETRICSPARTIALSUCCESS']._serialized_end=470 - _globals['_METRICSSERVICE']._serialized_start=473 - _globals['_METRICSSERVICE']._serialized_end=645 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi deleted file mode 100644 index fe3c44f3c37..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2.pyi +++ /dev/null @@ -1,117 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.metrics.v1.metrics_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class ExportMetricsServiceRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_METRICS_FIELD_NUMBER: builtins.int - @property - def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics]: - """An array of ResourceMetrics. - For data coming from a single resource this array will typically contain one - element. Intermediary nodes (such as OpenTelemetry Collector) that receive - data from multiple origins typically batch the data before forwarding further and - in that case this array will contain multiple elements. - """ - def __init__( - self, - *, - resource_metrics: collections.abc.Iterable[opentelemetry.proto.metrics.v1.metrics_pb2.ResourceMetrics] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_metrics", b"resource_metrics"]) -> None: ... - -global___ExportMetricsServiceRequest = ExportMetricsServiceRequest - -@typing_extensions.final -class ExportMetricsServiceResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int - @property - def partial_success(self) -> global___ExportMetricsPartialSuccess: - """The details of a partially successful export request. - - If the request is only partially accepted - (i.e. when the server accepts only parts of the data and rejects the rest) - the server MUST initialize the `partial_success` field and MUST - set the `rejected_` with the number of items it rejected. - - Servers MAY also make use of the `partial_success` field to convey - warnings/suggestions to senders even when the request was fully accepted. - In such cases, the `rejected_` MUST have a value of `0` and - the `error_message` MUST be non-empty. - - A `partial_success` message with an empty value (rejected_ = 0 and - `error_message` = "") is equivalent to it not being set/present. Senders - SHOULD interpret it the same way as in the full success case. - """ - def __init__( - self, - *, - partial_success: global___ExportMetricsPartialSuccess | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... - -global___ExportMetricsServiceResponse = ExportMetricsServiceResponse - -@typing_extensions.final -class ExportMetricsPartialSuccess(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - REJECTED_DATA_POINTS_FIELD_NUMBER: builtins.int - ERROR_MESSAGE_FIELD_NUMBER: builtins.int - rejected_data_points: builtins.int - """The number of rejected data points. - - A `rejected_` field holding a `0` value indicates that the - request was fully accepted. - """ - error_message: builtins.str - """A developer-facing human-readable message in English. It should be used - either to explain why the server rejected parts of the data during a partial - success or to convey warnings/suggestions during a full success. The message - should offer guidance on how users can address such issues. - - error_message is an optional field. An error_message with an empty value - is equivalent to it not being set. - """ - def __init__( - self, - *, - rejected_data_points: builtins.int = ..., - error_message: builtins.str = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_data_points", b"rejected_data_points"]) -> None: ... - -global___ExportMetricsPartialSuccess = ExportMetricsPartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py index f124bfe4adc..e7ee94e209e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py @@ -1,110 +1,5 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -from opentelemetry.proto.collector.metrics.v1 import metrics_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2 -GRPC_GENERATED_VERSION = '1.63.2' -GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - warnings.warn( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in opentelemetry/proto/collector/metrics/v1/metrics_service_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning - ) - - -class MetricsServiceStub(object): - """Service that can be used to push metrics between one Application - instrumented with OpenTelemetry and a collector, or between a collector and a - central collector. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Export = channel.unary_unary( - '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', - request_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, - _registered_method=True) - - -class MetricsServiceServicer(object): - """Service that can be used to push metrics between one Application - instrumented with OpenTelemetry and a collector, or between a collector and a - central collector. - """ - - def Export(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_MetricsServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Export': grpc.unary_unary_rpc_method_handler( - servicer.Export, - request_deserializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.collector.metrics.v1.MetricsService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class MetricsService(object): - """Service that can be used to push metrics between one Application - instrumented with OpenTelemetry and a collector, or between a collector and a - central collector. - """ - - @staticmethod - def Export(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', - opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceRequest.SerializeToString, - opentelemetry_dot_proto_dot_collector_dot_metrics_dot_v1_dot_metrics__service__pb2.ExportMetricsServiceResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2_grpc import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.py deleted file mode 100644 index 9e2f6198299..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/collector/profiles/v1development/profiles_service.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from opentelemetry.proto.profiles.v1development import profiles_pb2 as opentelemetry_dot_proto_dot_profiles_dot_v1development_dot_profiles__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\nKopentelemetry/proto/collector/profiles/v1development/profiles_service.proto\x12\x34opentelemetry.proto.collector.profiles.v1development\x1a\x39opentelemetry/proto/profiles/v1development/profiles.proto\"\xcb\x01\n\x1c\x45xportProfilesServiceRequest\x12W\n\x11resource_profiles\x18\x01 \x03(\x0b\x32<.opentelemetry.proto.profiles.v1development.ResourceProfiles\x12R\n\ndictionary\x18\x02 \x01(\x0b\x32>.opentelemetry.proto.profiles.v1development.ProfilesDictionary\"\x8c\x01\n\x1d\x45xportProfilesServiceResponse\x12k\n\x0fpartial_success\x18\x01 \x01(\x0b\x32R.opentelemetry.proto.collector.profiles.v1development.ExportProfilesPartialSuccess\"P\n\x1c\x45xportProfilesPartialSuccess\x12\x19\n\x11rejected_profiles\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xc7\x01\n\x0fProfilesService\x12\xb3\x01\n\x06\x45xport\x12R.opentelemetry.proto.collector.profiles.v1development.ExportProfilesServiceRequest\x1aS.opentelemetry.proto.collector.profiles.v1development.ExportProfilesServiceResponse\"\x00\x42\xc9\x01\n7io.opentelemetry.proto.collector.profiles.v1developmentB\x14ProfilesServiceProtoP\x01Z?go.opentelemetry.io/proto/otlp/collector/profiles/v1development\xaa\x02\x34OpenTelemetry.Proto.Collector.Profiles.V1Developmentb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.profiles.v1development.profiles_service_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n7io.opentelemetry.proto.collector.profiles.v1developmentB\024ProfilesServiceProtoP\001Z?go.opentelemetry.io/proto/otlp/collector/profiles/v1development\252\0024OpenTelemetry.Proto.Collector.Profiles.V1Development' - _globals['_EXPORTPROFILESSERVICEREQUEST']._serialized_start=193 - _globals['_EXPORTPROFILESSERVICEREQUEST']._serialized_end=396 - _globals['_EXPORTPROFILESSERVICERESPONSE']._serialized_start=399 - _globals['_EXPORTPROFILESSERVICERESPONSE']._serialized_end=539 - _globals['_EXPORTPROFILESPARTIALSUCCESS']._serialized_start=541 - _globals['_EXPORTPROFILESPARTIALSUCCESS']._serialized_end=621 - _globals['_PROFILESSERVICE']._serialized_start=624 - _globals['_PROFILESSERVICE']._serialized_end=823 -# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.pyi deleted file mode 100644 index e8b7a82095c..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2.pyi +++ /dev/null @@ -1,123 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2023, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.profiles.v1development.profiles_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class ExportProfilesServiceRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_PROFILES_FIELD_NUMBER: builtins.int - DICTIONARY_FIELD_NUMBER: builtins.int - @property - def resource_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.profiles.v1development.profiles_pb2.ResourceProfiles]: - """An array of ResourceProfiles. - For data coming from a single resource this array will typically contain one - element. Intermediary nodes (such as OpenTelemetry Collector) that receive - data from multiple origins typically batch the data before forwarding further and - in that case this array will contain multiple elements. - """ - @property - def dictionary(self) -> opentelemetry.proto.profiles.v1development.profiles_pb2.ProfilesDictionary: - """The reference table containing all data shared by profiles across the message being sent.""" - def __init__( - self, - *, - resource_profiles: collections.abc.Iterable[opentelemetry.proto.profiles.v1development.profiles_pb2.ResourceProfiles] | None = ..., - dictionary: opentelemetry.proto.profiles.v1development.profiles_pb2.ProfilesDictionary | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary", "resource_profiles", b"resource_profiles"]) -> None: ... - -global___ExportProfilesServiceRequest = ExportProfilesServiceRequest - -@typing_extensions.final -class ExportProfilesServiceResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int - @property - def partial_success(self) -> global___ExportProfilesPartialSuccess: - """The details of a partially successful export request. - - If the request is only partially accepted - (i.e. when the server accepts only parts of the data and rejects the rest) - the server MUST initialize the `partial_success` field and MUST - set the `rejected_` with the number of items it rejected. - - Servers MAY also make use of the `partial_success` field to convey - warnings/suggestions to senders even when the request was fully accepted. - In such cases, the `rejected_` MUST have a value of `0` and - the `error_message` MUST be non-empty. - - A `partial_success` message with an empty value (rejected_ = 0 and - `error_message` = "") is equivalent to it not being set/present. Senders - SHOULD interpret it the same way as in the full success case. - """ - def __init__( - self, - *, - partial_success: global___ExportProfilesPartialSuccess | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... - -global___ExportProfilesServiceResponse = ExportProfilesServiceResponse - -@typing_extensions.final -class ExportProfilesPartialSuccess(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - REJECTED_PROFILES_FIELD_NUMBER: builtins.int - ERROR_MESSAGE_FIELD_NUMBER: builtins.int - rejected_profiles: builtins.int - """The number of rejected profiles. - - A `rejected_` field holding a `0` value indicates that the - request was fully accepted. - """ - error_message: builtins.str - """A developer-facing human-readable message in English. It should be used - either to explain why the server rejected parts of the data during a partial - success or to convey warnings/suggestions during a full success. The message - should offer guidance on how users can address such issues. - - error_message is an optional field. An error_message with an empty value - is equivalent to it not being set. - """ - def __init__( - self, - *, - rejected_profiles: builtins.int = ..., - error_message: builtins.str = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_profiles", b"rejected_profiles"]) -> None: ... - -global___ExportProfilesPartialSuccess = ExportProfilesPartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2_grpc.py deleted file mode 100644 index 3742ae591e3..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2_grpc.py +++ /dev/null @@ -1,107 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from opentelemetry.proto.collector.profiles.v1development import profiles_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2 - -GRPC_GENERATED_VERSION = '1.63.2' -GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - warnings.warn( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in opentelemetry/proto/collector/profiles/v1development/profiles_service_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning - ) - - -class ProfilesServiceStub(object): - """Service that can be used to push profiles between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Export = channel.unary_unary( - '/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export', - request_serializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.FromString, - _registered_method=True) - - -class ProfilesServiceServicer(object): - """Service that can be used to push profiles between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector. - """ - - def Export(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_ProfilesServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Export': grpc.unary_unary_rpc_method_handler( - servicer.Export, - request_deserializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.collector.profiles.v1development.ProfilesService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class ProfilesService(object): - """Service that can be used to push profiles between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector. - """ - - @staticmethod - def Export(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export', - opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceRequest.SerializeToString, - opentelemetry_dot_proto_dot_collector_dot_profiles_dot_v1development_dot_profiles__service__pb2.ExportProfilesServiceResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py index c0ad62bfdbd..c86e73d64fb 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.py @@ -1,34 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/collector/trace/v1/trace_service.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.trace.v1 import trace_pb2 as opentelemetry_dot_proto_dot_trace_dot_v1_dot_trace__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"x\n\x1a\x45xportTraceServiceResponse\x12Z\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x41.opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess\"J\n\x19\x45xportTracePartialSuccess\x12\x16\n\x0erejected_spans\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x9c\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\xaa\x02&OpenTelemetry.Proto.Collector.Trace.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.collector.trace.v1.trace_service_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n)io.opentelemetry.proto.collector.trace.v1B\021TraceServiceProtoP\001Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\252\002&OpenTelemetry.Proto.Collector.Trace.V1' - _globals['_EXPORTTRACESERVICEREQUEST']._serialized_start=144 - _globals['_EXPORTTRACESERVICEREQUEST']._serialized_end=240 - _globals['_EXPORTTRACESERVICERESPONSE']._serialized_start=242 - _globals['_EXPORTTRACESERVICERESPONSE']._serialized_end=362 - _globals['_EXPORTTRACEPARTIALSUCCESS']._serialized_start=364 - _globals['_EXPORTTRACEPARTIALSUCCESS']._serialized_end=438 - _globals['_TRACESERVICE']._serialized_start=441 - _globals['_TRACESERVICE']._serialized_end=603 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi deleted file mode 100644 index ceb4db5213f..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2.pyi +++ /dev/null @@ -1,117 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.trace.v1.trace_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class ExportTraceServiceRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_SPANS_FIELD_NUMBER: builtins.int - @property - def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans]: - """An array of ResourceSpans. - For data coming from a single resource this array will typically contain one - element. Intermediary nodes (such as OpenTelemetry Collector) that receive - data from multiple origins typically batch the data before forwarding further and - in that case this array will contain multiple elements. - """ - def __init__( - self, - *, - resource_spans: collections.abc.Iterable[opentelemetry.proto.trace.v1.trace_pb2.ResourceSpans] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_spans", b"resource_spans"]) -> None: ... - -global___ExportTraceServiceRequest = ExportTraceServiceRequest - -@typing_extensions.final -class ExportTraceServiceResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - PARTIAL_SUCCESS_FIELD_NUMBER: builtins.int - @property - def partial_success(self) -> global___ExportTracePartialSuccess: - """The details of a partially successful export request. - - If the request is only partially accepted - (i.e. when the server accepts only parts of the data and rejects the rest) - the server MUST initialize the `partial_success` field and MUST - set the `rejected_` with the number of items it rejected. - - Servers MAY also make use of the `partial_success` field to convey - warnings/suggestions to senders even when the request was fully accepted. - In such cases, the `rejected_` MUST have a value of `0` and - the `error_message` MUST be non-empty. - - A `partial_success` message with an empty value (rejected_ = 0 and - `error_message` = "") is equivalent to it not being set/present. Senders - SHOULD interpret it the same way as in the full success case. - """ - def __init__( - self, - *, - partial_success: global___ExportTracePartialSuccess | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["partial_success", b"partial_success"]) -> None: ... - -global___ExportTraceServiceResponse = ExportTraceServiceResponse - -@typing_extensions.final -class ExportTracePartialSuccess(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - REJECTED_SPANS_FIELD_NUMBER: builtins.int - ERROR_MESSAGE_FIELD_NUMBER: builtins.int - rejected_spans: builtins.int - """The number of rejected spans. - - A `rejected_` field holding a `0` value indicates that the - request was fully accepted. - """ - error_message: builtins.str - """A developer-facing human-readable message in English. It should be used - either to explain why the server rejected parts of the data during a partial - success or to convey warnings/suggestions during a full success. The message - should offer guidance on how users can address such issues. - - error_message is an optional field. An error_message with an empty value - is equivalent to it not being set. - """ - def __init__( - self, - *, - rejected_spans: builtins.int = ..., - error_message: builtins.str = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "rejected_spans", b"rejected_spans"]) -> None: ... - -global___ExportTracePartialSuccess = ExportTracePartialSuccess diff --git a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py index f1cdf0355b4..bba0e2ebede 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py +++ b/opentelemetry-proto/src/opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py @@ -1,110 +1,5 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -from opentelemetry.proto.collector.trace.v1 import trace_service_pb2 as opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2 -GRPC_GENERATED_VERSION = '1.63.2' -GRPC_VERSION = grpc.__version__ -EXPECTED_ERROR_RELEASE = '1.65.0' -SCHEDULED_RELEASE_DATE = 'June 25, 2024' -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - warnings.warn( - f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in opentelemetry/proto/collector/trace/v1/trace_service_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' - + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', - RuntimeWarning - ) - - -class TraceServiceStub(object): - """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector (in this - case spans are sent/received to/from multiple Applications). - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Export = channel.unary_unary( - '/opentelemetry.proto.collector.trace.v1.TraceService/Export', - request_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, - response_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, - _registered_method=True) - - -class TraceServiceServicer(object): - """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector (in this - case spans are sent/received to/from multiple Applications). - """ - - def Export(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_TraceServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Export': grpc.unary_unary_rpc_method_handler( - servicer.Export, - request_deserializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.FromString, - response_serializer=opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'opentelemetry.proto.collector.trace.v1.TraceService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class TraceService(object): - """Service that can be used to push spans between one Application instrumented with - OpenTelemetry and a collector, or between a collector and a central collector (in this - case spans are sent/received to/from multiple Applications). - """ - - @staticmethod - def Export(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/opentelemetry.proto.collector.trace.v1.TraceService/Export', - opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceRequest.SerializeToString, - opentelemetry_dot_proto_dot_collector_dot_trace_dot_v1_dot_trace__service__pb2.ExportTraceServiceResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) +from opentelemetry._proto.collector.trace.v1.trace_service_pb2_grpc import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py index 1e816f201f8..04ce80a2aed 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.py @@ -1,37 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/common/v1/common.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xad\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x12\x1f\n\x15string_value_strindex\x18\x08 \x01(\x05H\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"e\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12\x14\n\x0ckey_strindex\x18\x03 \x01(\x05\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\"X\n\tEntityRef\x12\x12\n\nschema_url\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07id_keys\x18\x03 \x03(\t\x12\x18\n\x10\x64\x65scription_keys\x18\x04 \x03(\tB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.common.v1.common_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n io.opentelemetry.proto.common.v1B\013CommonProtoP\001Z(go.opentelemetry.io/proto/otlp/common/v1\252\002\035OpenTelemetry.Proto.Common.V1' - _globals['_ANYVALUE']._serialized_start=78 - _globals['_ANYVALUE']._serialized_end=379 - _globals['_ARRAYVALUE']._serialized_start=381 - _globals['_ARRAYVALUE']._serialized_end=450 - _globals['_KEYVALUELIST']._serialized_start=452 - _globals['_KEYVALUELIST']._serialized_end=523 - _globals['_KEYVALUE']._serialized_start=525 - _globals['_KEYVALUE']._serialized_end=626 - _globals['_INSTRUMENTATIONSCOPE']._serialized_start=629 - _globals['_INSTRUMENTATIONSCOPE']._serialized_end=777 - _globals['_ENTITYREF']._serialized_start=779 - _globals['_ENTITYREF']._serialized_end=867 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.common.v1.common_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi deleted file mode 100644 index 205bba7510f..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/common/v1/common_pb2.pyi +++ /dev/null @@ -1,280 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class AnyValue(google.protobuf.message.Message): - """Represents any type of attribute value. AnyValue may contain a - primitive value such as a string or integer or it may contain an arbitrary nested - object containing arrays, key-value lists and primitives. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - STRING_VALUE_FIELD_NUMBER: builtins.int - BOOL_VALUE_FIELD_NUMBER: builtins.int - INT_VALUE_FIELD_NUMBER: builtins.int - DOUBLE_VALUE_FIELD_NUMBER: builtins.int - ARRAY_VALUE_FIELD_NUMBER: builtins.int - KVLIST_VALUE_FIELD_NUMBER: builtins.int - BYTES_VALUE_FIELD_NUMBER: builtins.int - STRING_VALUE_STRINDEX_FIELD_NUMBER: builtins.int - string_value: builtins.str - bool_value: builtins.bool - int_value: builtins.int - double_value: builtins.float - @property - def array_value(self) -> global___ArrayValue: ... - @property - def kvlist_value(self) -> global___KeyValueList: ... - bytes_value: builtins.bytes - string_value_strindex: builtins.int - """Reference to the string value in ProfilesDictionary.string_table. - - Note: This is currently used exclusively in the Profiling signal. - Implementers of OTLP receivers for signals other than Profiling should - treat the presence of this value as a non-fatal issue. - Log an error or warning indicating an unexpected field intended for the - Profiling signal and process the data as if this value were absent or - empty, ignoring its semantic content for the non-Profiling signal. - - Status: [Development] - """ - def __init__( - self, - *, - string_value: builtins.str = ..., - bool_value: builtins.bool = ..., - int_value: builtins.int = ..., - double_value: builtins.float = ..., - array_value: global___ArrayValue | None = ..., - kvlist_value: global___KeyValueList | None = ..., - bytes_value: builtins.bytes = ..., - string_value_strindex: builtins.int = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "string_value_strindex", b"string_value_strindex", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "string_value_strindex", b"string_value_strindex", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["string_value", "bool_value", "int_value", "double_value", "array_value", "kvlist_value", "bytes_value", "string_value_strindex"] | None: ... - -global___AnyValue = AnyValue - -@typing_extensions.final -class ArrayValue(google.protobuf.message.Message): - """ArrayValue is a list of AnyValue messages. We need ArrayValue as a message - since oneof in AnyValue does not allow repeated fields. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - VALUES_FIELD_NUMBER: builtins.int - @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: - """Array of values. The array may be empty (contain 0 elements).""" - def __init__( - self, - *, - values: collections.abc.Iterable[global___AnyValue] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... - -global___ArrayValue = ArrayValue - -@typing_extensions.final -class KeyValueList(google.protobuf.message.Message): - """KeyValueList is a list of KeyValue messages. We need KeyValueList as a message - since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need - a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to - avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches - are semantically equivalent. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - VALUES_FIELD_NUMBER: builtins.int - @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: - """A collection of key/value pairs of key-value pairs. The list may be empty (may - contain 0 elements). - - The keys MUST be unique (it is not allowed to have more than one - value with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - def __init__( - self, - *, - values: collections.abc.Iterable[global___KeyValue] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... - -global___KeyValueList = KeyValueList - -@typing_extensions.final -class KeyValue(google.protobuf.message.Message): - """Represents a key-value pair that is used to store Span attributes, Link - attributes, etc. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - KEY_STRINDEX_FIELD_NUMBER: builtins.int - key: builtins.str - """The key name of the pair. - key_ref MUST NOT be set if key is used. - """ - @property - def value(self) -> global___AnyValue: - """The value of the pair.""" - key_strindex: builtins.int - """Reference to the string key in ProfilesDictionary.string_table. - key MUST NOT be set if key_strindex is used. - - Note: This is currently used exclusively in the Profiling signal. - Implementers of OTLP receivers for signals other than Profiling should - treat the presence of this key as a non-fatal issue. - Log an error or warning indicating an unexpected field intended for the - Profiling signal and process the data as if this value were absent or - empty, ignoring its semantic content for the non-Profiling signal. - - Status: [Development] - """ - def __init__( - self, - *, - key: builtins.str = ..., - value: global___AnyValue | None = ..., - key_strindex: builtins.int = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "key_strindex", b"key_strindex", "value", b"value"]) -> None: ... - -global___KeyValue = KeyValue - -@typing_extensions.final -class InstrumentationScope(google.protobuf.message.Message): - """InstrumentationScope is a message representing the instrumentation scope information - such as the fully qualified name and version. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - NAME_FIELD_NUMBER: builtins.int - VERSION_FIELD_NUMBER: builtins.int - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - name: builtins.str - """A name denoting the Instrumentation scope. - An empty instrumentation scope name means the name is unknown. - """ - version: builtins.str - """Defines the version of the instrumentation scope. - An empty instrumentation scope version means the version is unknown. - """ - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: - """Additional attributes that describe the scope. [Optional]. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - """The number of attributes that were discarded. Attributes - can be discarded because their keys are too long or because there are too many - attributes. If this value is 0, then no attributes were dropped. - """ - def __init__( - self, - *, - name: builtins.str = ..., - version: builtins.str = ..., - attributes: collections.abc.Iterable[global___KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "name", b"name", "version", b"version"]) -> None: ... - -global___InstrumentationScope = InstrumentationScope - -@typing_extensions.final -class EntityRef(google.protobuf.message.Message): - """A reference to an Entity. - Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. - - Status: [Development] - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SCHEMA_URL_FIELD_NUMBER: builtins.int - TYPE_FIELD_NUMBER: builtins.int - ID_KEYS_FIELD_NUMBER: builtins.int - DESCRIPTION_KEYS_FIELD_NUMBER: builtins.int - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the entity data - is recorded in. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - - This schema_url applies to the data in this message and to the Resource attributes - referenced by id_keys and description_keys. - TODO: discuss if we are happy with this somewhat complicated definition of what - the schema_url applies to. - - This field obsoletes the schema_url field in ResourceMetrics/ResourceSpans/ResourceLogs. - """ - type: builtins.str - """Defines the type of the entity. MUST not change during the lifetime of the entity. - For example: "service" or "host". This field is required and MUST not be empty - for valid entities. - """ - @property - def id_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: - """Attribute Keys that identify the entity. - MUST not change during the lifetime of the entity. The Id must contain at least one attribute. - These keys MUST exist in the containing {message}.attributes. - """ - @property - def description_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: - """Descriptive (non-identifying) attribute keys of the entity. - MAY change over the lifetime of the entity. MAY be empty. - These attribute keys are not part of entity's identity. - These keys MUST exist in the containing {message}.attributes. - """ - def __init__( - self, - *, - schema_url: builtins.str = ..., - type: builtins.str = ..., - id_keys: collections.abc.Iterable[builtins.str] | None = ..., - description_keys: collections.abc.Iterable[builtins.str] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["description_keys", b"description_keys", "id_keys", b"id_keys", "schema_url", b"schema_url", "type", b"type"]) -> None: ... - -global___EntityRef = EntityRef diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py index 3fe64e28961..5d2cc3ed509 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.py @@ -1,39 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/logs/v1/logs.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xa3\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x83\x03\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0c\x12\x12\n\nevent_name\x18\x0c \x01(\tJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*Y\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAGS_DO_NOT_USE\x10\x00\x12&\n!LOG_RECORD_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x42s\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1\xaa\x02\x1bOpenTelemetry.Proto.Logs.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.logs.v1.logs_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n\036io.opentelemetry.proto.logs.v1B\tLogsProtoP\001Z&go.opentelemetry.io/proto/otlp/logs/v1\252\002\033OpenTelemetry.Proto.Logs.V1' - _globals['_SEVERITYNUMBER']._serialized_start=961 - _globals['_SEVERITYNUMBER']._serialized_end=1668 - _globals['_LOGRECORDFLAGS']._serialized_start=1670 - _globals['_LOGRECORDFLAGS']._serialized_end=1759 - _globals['_LOGSDATA']._serialized_start=163 - _globals['_LOGSDATA']._serialized_end=239 - _globals['_RESOURCELOGS']._serialized_start=242 - _globals['_RESOURCELOGS']._serialized_end=405 - _globals['_SCOPELOGS']._serialized_start=408 - _globals['_SCOPELOGS']._serialized_end=568 - _globals['_LOGRECORD']._serialized_start=571 - _globals['_LOGRECORD']._serialized_end=958 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.logs.v1.logs_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi deleted file mode 100644 index 03edd535b7c..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/logs/v1/logs_pb2.pyi +++ /dev/null @@ -1,365 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2020, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import opentelemetry.proto.common.v1.common_pb2 -import opentelemetry.proto.resource.v1.resource_pb2 -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _SeverityNumber: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _SeverityNumberEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SeverityNumber.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - SEVERITY_NUMBER_UNSPECIFIED: _SeverityNumber.ValueType # 0 - SEVERITY_NUMBER_TRACE: _SeverityNumber.ValueType # 1 - SEVERITY_NUMBER_TRACE2: _SeverityNumber.ValueType # 2 - SEVERITY_NUMBER_TRACE3: _SeverityNumber.ValueType # 3 - SEVERITY_NUMBER_TRACE4: _SeverityNumber.ValueType # 4 - SEVERITY_NUMBER_DEBUG: _SeverityNumber.ValueType # 5 - SEVERITY_NUMBER_DEBUG2: _SeverityNumber.ValueType # 6 - SEVERITY_NUMBER_DEBUG3: _SeverityNumber.ValueType # 7 - SEVERITY_NUMBER_DEBUG4: _SeverityNumber.ValueType # 8 - SEVERITY_NUMBER_INFO: _SeverityNumber.ValueType # 9 - SEVERITY_NUMBER_INFO2: _SeverityNumber.ValueType # 10 - SEVERITY_NUMBER_INFO3: _SeverityNumber.ValueType # 11 - SEVERITY_NUMBER_INFO4: _SeverityNumber.ValueType # 12 - SEVERITY_NUMBER_WARN: _SeverityNumber.ValueType # 13 - SEVERITY_NUMBER_WARN2: _SeverityNumber.ValueType # 14 - SEVERITY_NUMBER_WARN3: _SeverityNumber.ValueType # 15 - SEVERITY_NUMBER_WARN4: _SeverityNumber.ValueType # 16 - SEVERITY_NUMBER_ERROR: _SeverityNumber.ValueType # 17 - SEVERITY_NUMBER_ERROR2: _SeverityNumber.ValueType # 18 - SEVERITY_NUMBER_ERROR3: _SeverityNumber.ValueType # 19 - SEVERITY_NUMBER_ERROR4: _SeverityNumber.ValueType # 20 - SEVERITY_NUMBER_FATAL: _SeverityNumber.ValueType # 21 - SEVERITY_NUMBER_FATAL2: _SeverityNumber.ValueType # 22 - SEVERITY_NUMBER_FATAL3: _SeverityNumber.ValueType # 23 - SEVERITY_NUMBER_FATAL4: _SeverityNumber.ValueType # 24 - -class SeverityNumber(_SeverityNumber, metaclass=_SeverityNumberEnumTypeWrapper): - """Possible values for LogRecord.SeverityNumber.""" - -SEVERITY_NUMBER_UNSPECIFIED: SeverityNumber.ValueType # 0 -SEVERITY_NUMBER_TRACE: SeverityNumber.ValueType # 1 -SEVERITY_NUMBER_TRACE2: SeverityNumber.ValueType # 2 -SEVERITY_NUMBER_TRACE3: SeverityNumber.ValueType # 3 -SEVERITY_NUMBER_TRACE4: SeverityNumber.ValueType # 4 -SEVERITY_NUMBER_DEBUG: SeverityNumber.ValueType # 5 -SEVERITY_NUMBER_DEBUG2: SeverityNumber.ValueType # 6 -SEVERITY_NUMBER_DEBUG3: SeverityNumber.ValueType # 7 -SEVERITY_NUMBER_DEBUG4: SeverityNumber.ValueType # 8 -SEVERITY_NUMBER_INFO: SeverityNumber.ValueType # 9 -SEVERITY_NUMBER_INFO2: SeverityNumber.ValueType # 10 -SEVERITY_NUMBER_INFO3: SeverityNumber.ValueType # 11 -SEVERITY_NUMBER_INFO4: SeverityNumber.ValueType # 12 -SEVERITY_NUMBER_WARN: SeverityNumber.ValueType # 13 -SEVERITY_NUMBER_WARN2: SeverityNumber.ValueType # 14 -SEVERITY_NUMBER_WARN3: SeverityNumber.ValueType # 15 -SEVERITY_NUMBER_WARN4: SeverityNumber.ValueType # 16 -SEVERITY_NUMBER_ERROR: SeverityNumber.ValueType # 17 -SEVERITY_NUMBER_ERROR2: SeverityNumber.ValueType # 18 -SEVERITY_NUMBER_ERROR3: SeverityNumber.ValueType # 19 -SEVERITY_NUMBER_ERROR4: SeverityNumber.ValueType # 20 -SEVERITY_NUMBER_FATAL: SeverityNumber.ValueType # 21 -SEVERITY_NUMBER_FATAL2: SeverityNumber.ValueType # 22 -SEVERITY_NUMBER_FATAL3: SeverityNumber.ValueType # 23 -SEVERITY_NUMBER_FATAL4: SeverityNumber.ValueType # 24 -global___SeverityNumber = SeverityNumber - -class _LogRecordFlags: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _LogRecordFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_LogRecordFlags.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - LOG_RECORD_FLAGS_DO_NOT_USE: _LogRecordFlags.ValueType # 0 - """The zero value for the enum. Should not be used for comparisons. - Instead use bitwise "and" with the appropriate mask as shown above. - """ - LOG_RECORD_FLAGS_TRACE_FLAGS_MASK: _LogRecordFlags.ValueType # 255 - """Bits 0-7 are used for trace flags.""" - -class LogRecordFlags(_LogRecordFlags, metaclass=_LogRecordFlagsEnumTypeWrapper): - """LogRecordFlags represents constants used to interpret the - LogRecord.flags field, which is protobuf 'fixed32' type and is to - be used as bit-fields. Each non-zero value defined in this enum is - a bit-mask. To extract the bit-field, for example, use an - expression like: - - (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) - """ - -LOG_RECORD_FLAGS_DO_NOT_USE: LogRecordFlags.ValueType # 0 -"""The zero value for the enum. Should not be used for comparisons. -Instead use bitwise "and" with the appropriate mask as shown above. -""" -LOG_RECORD_FLAGS_TRACE_FLAGS_MASK: LogRecordFlags.ValueType # 255 -"""Bits 0-7 are used for trace flags.""" -global___LogRecordFlags = LogRecordFlags - -@typing_extensions.final -class LogsData(google.protobuf.message.Message): - """LogsData represents the logs data that can be stored in a persistent storage, - OR can be embedded by other protocols that transfer OTLP logs data but do not - implement the OTLP protocol. - - The main difference between this message and collector protocol is that - in this message there will not be any "control" or "metadata" specific to - OTLP protocol. - - When new fields are added into this message, the OTLP request MUST be updated - as well. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_LOGS_FIELD_NUMBER: builtins.int - @property - def resource_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceLogs]: - """An array of ResourceLogs. - For data coming from a single resource this array will typically contain - one element. Intermediary nodes that receive data from multiple origins - typically batch the data before forwarding further and in that case this - array will contain multiple elements. - """ - def __init__( - self, - *, - resource_logs: collections.abc.Iterable[global___ResourceLogs] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_logs", b"resource_logs"]) -> None: ... - -global___LogsData = LogsData - -@typing_extensions.final -class ResourceLogs(google.protobuf.message.Message): - """A collection of ScopeLogs from a Resource.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_FIELD_NUMBER: builtins.int - SCOPE_LOGS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: - """The resource for the logs in this message. - If this field is not set then resource info is unknown. - """ - @property - def scope_logs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeLogs]: - """A list of ScopeLogs that originate from a resource.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the resource data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "scope_logs" field which have their own schema_url field. - """ - def __init__( - self, - *, - resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., - scope_logs: collections.abc.Iterable[global___ScopeLogs] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_logs", b"scope_logs"]) -> None: ... - -global___ResourceLogs = ResourceLogs - -@typing_extensions.final -class ScopeLogs(google.protobuf.message.Message): - """A collection of Logs produced by a Scope.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SCOPE_FIELD_NUMBER: builtins.int - LOG_RECORDS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: - """The instrumentation scope information for the logs in this message. - Semantically when InstrumentationScope isn't set, it is equivalent with - an empty instrumentation scope name (unknown). - """ - @property - def log_records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LogRecord]: - """A list of log records.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the log data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "scope" field and all logs in the - "log_records" field. - """ - def __init__( - self, - *, - scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., - log_records: collections.abc.Iterable[global___LogRecord] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["log_records", b"log_records", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... - -global___ScopeLogs = ScopeLogs - -@typing_extensions.final -class LogRecord(google.protobuf.message.Message): - """A log record according to OpenTelemetry Log Data Model: - https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - OBSERVED_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - SEVERITY_NUMBER_FIELD_NUMBER: builtins.int - SEVERITY_TEXT_FIELD_NUMBER: builtins.int - BODY_FIELD_NUMBER: builtins.int - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - TRACE_ID_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - EVENT_NAME_FIELD_NUMBER: builtins.int - time_unix_nano: builtins.int - """time_unix_nano is the time when the event occurred. - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - Value of 0 indicates unknown or missing timestamp. - """ - observed_time_unix_nano: builtins.int - """Time when the event was observed by the collection system. - For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) - this timestamp is typically set at the generation time and is equal to Timestamp. - For events originating externally and collected by OpenTelemetry (e.g. using - Collector) this is the time when OpenTelemetry's code observed the event measured - by the clock of the OpenTelemetry code. This field MUST be set once the event is - observed by OpenTelemetry. - - For converting OpenTelemetry log data to formats that support only one timestamp or - when receiving OpenTelemetry log data by recipients that support only one timestamp - internally the following logic is recommended: - - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - Value of 0 indicates unknown or missing timestamp. - """ - severity_number: global___SeverityNumber.ValueType - """Numerical value of the severity, normalized to values described in Log Data Model. - [Optional]. - """ - severity_text: builtins.str - """The severity text (also known as log level). The original string representation as - it is known at the source. [Optional]. - """ - @property - def body(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: - """A value containing the body of the log record. Can be for example a human-readable - string message (including multi-line) describing the event in a free form or it can - be a structured data composed of arrays and maps of other values. [Optional]. - """ - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """Additional attributes that describe the specific event occurrence. [Optional]. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - flags: builtins.int - """Flags, a bit field. 8 least significant bits are the trace flags as - defined in W3C Trace Context specification. 24 most significant bits are reserved - and must be set to 0. Readers must not assume that 24 most significant bits - will be zero and must correctly mask the bits when reading 8-bit trace flag (use - flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. - """ - trace_id: builtins.bytes - """A unique identifier for a trace. All logs from the same trace share - the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR - of length other than 16 bytes is considered invalid (empty string in OTLP/JSON - is zero-length and thus is also invalid). - - This field is optional. - - The receivers SHOULD assume that the log record is not associated with a - trace if any of the following is true: - - the field is not present, - - the field contains an invalid value. - """ - span_id: builtins.bytes - """A unique identifier for a span within a trace, assigned when the span - is created. The ID is an 8-byte array. An ID with all zeroes OR of length - other than 8 bytes is considered invalid (empty string in OTLP/JSON - is zero-length and thus is also invalid). - - This field is optional. If the sender specifies a valid span_id then it SHOULD also - specify a valid trace_id. - - The receivers SHOULD assume that the log record is not associated with a - span if any of the following is true: - - the field is not present, - - the field contains an invalid value. - """ - event_name: builtins.str - """A unique identifier of event category/type. - All events with the same event_name are expected to conform to the same - schema for both their attributes and their body. - - Recommended to be fully qualified and short (no longer than 256 characters). - - Presence of event_name on the log record identifies this record - as an event. - - [Optional]. - """ - def __init__( - self, - *, - time_unix_nano: builtins.int = ..., - observed_time_unix_nano: builtins.int = ..., - severity_number: global___SeverityNumber.ValueType = ..., - severity_text: builtins.str = ..., - body: opentelemetry.proto.common.v1.common_pb2.AnyValue | None = ..., - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - flags: builtins.int = ..., - trace_id: builtins.bytes = ..., - span_id: builtins.bytes = ..., - event_name: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["body", b"body"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "body", b"body", "dropped_attributes_count", b"dropped_attributes_count", "event_name", b"event_name", "flags", b"flags", "observed_time_unix_nano", b"observed_time_unix_nano", "severity_number", b"severity_number", "severity_text", b"severity_text", "span_id", b"span_id", "time_unix_nano", b"time_unix_nano", "trace_id", b"trace_id"]) -> None: ... - -global___LogRecord = LogRecord diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py index a337a58476b..198854f8d36 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.py @@ -1,63 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/metrics/v1/metrics.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xcd\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x12\x39\n\x08metadata\x18\x0c \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValueB\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.metrics.v1.metrics_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n!io.opentelemetry.proto.metrics.v1B\014MetricsProtoP\001Z)go.opentelemetry.io/proto/otlp/metrics/v1\252\002\036OpenTelemetry.Proto.Metrics.V1' - _globals['_AGGREGATIONTEMPORALITY']._serialized_start=3546 - _globals['_AGGREGATIONTEMPORALITY']._serialized_end=3686 - _globals['_DATAPOINTFLAGS']._serialized_start=3688 - _globals['_DATAPOINTFLAGS']._serialized_end=3782 - _globals['_METRICSDATA']._serialized_start=172 - _globals['_METRICSDATA']._serialized_end=260 - _globals['_RESOURCEMETRICS']._serialized_start=263 - _globals['_RESOURCEMETRICS']._serialized_end=438 - _globals['_SCOPEMETRICS']._serialized_start=441 - _globals['_SCOPEMETRICS']._serialized_end=600 - _globals['_METRIC']._serialized_start=603 - _globals['_METRIC']._serialized_end=1064 - _globals['_GAUGE']._serialized_start=1066 - _globals['_GAUGE']._serialized_end=1143 - _globals['_SUM']._serialized_start=1146 - _globals['_SUM']._serialized_end=1332 - _globals['_HISTOGRAM']._serialized_start=1335 - _globals['_HISTOGRAM']._serialized_end=1508 - _globals['_EXPONENTIALHISTOGRAM']._serialized_start=1511 - _globals['_EXPONENTIALHISTOGRAM']._serialized_end=1706 - _globals['_SUMMARY']._serialized_start=1708 - _globals['_SUMMARY']._serialized_end=1788 - _globals['_NUMBERDATAPOINT']._serialized_start=1791 - _globals['_NUMBERDATAPOINT']._serialized_end=2053 - _globals['_HISTOGRAMDATAPOINT']._serialized_start=2056 - _globals['_HISTOGRAMDATAPOINT']._serialized_end=2414 - _globals['_EXPONENTIALHISTOGRAMDATAPOINT']._serialized_start=2417 - _globals['_EXPONENTIALHISTOGRAMDATAPOINT']._serialized_end=3019 - _globals['_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS']._serialized_start=2947 - _globals['_EXPONENTIALHISTOGRAMDATAPOINT_BUCKETS']._serialized_end=2995 - _globals['_SUMMARYDATAPOINT']._serialized_start=3022 - _globals['_SUMMARYDATAPOINT']._serialized_end=3347 - _globals['_SUMMARYDATAPOINT_VALUEATQUANTILE']._serialized_start=3291 - _globals['_SUMMARYDATAPOINT_VALUEATQUANTILE']._serialized_end=3341 - _globals['_EXEMPLAR']._serialized_start=3350 - _globals['_EXEMPLAR']._serialized_end=3543 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.metrics.v1.metrics_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi deleted file mode 100644 index 0f374be93ee..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/metrics/v1/metrics_pb2.pyi +++ /dev/null @@ -1,1177 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import opentelemetry.proto.common.v1.common_pb2 -import opentelemetry.proto.resource.v1.resource_pb2 -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _AggregationTemporality: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _AggregationTemporalityEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AggregationTemporality.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - AGGREGATION_TEMPORALITY_UNSPECIFIED: _AggregationTemporality.ValueType # 0 - """UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" - AGGREGATION_TEMPORALITY_DELTA: _AggregationTemporality.ValueType # 1 - """DELTA is an AggregationTemporality for a metric aggregator which reports - changes since last report time. Successive metrics contain aggregation of - values from continuous and non-overlapping intervals. - - The values for a DELTA metric are based only on the time interval - associated with one measurement cycle. There is no dependency on - previous measurements like is the case for CUMULATIVE metrics. - - For example, consider a system measuring the number of requests that - it receives and reports the sum of these requests every second as a - DELTA metric: - - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0+1 to - t_0+2 with a value of 2. - """ - AGGREGATION_TEMPORALITY_CUMULATIVE: _AggregationTemporality.ValueType # 2 - """CUMULATIVE is an AggregationTemporality for a metric aggregator which - reports changes since a fixed start time. This means that current values - of a CUMULATIVE metric depend on all previous measurements since the - start time. Because of this, the sender is required to retain this state - in some form. If this state is lost or invalidated, the CUMULATIVE metric - values MUST be reset and a new fixed start time following the last - reported measurement time sent MUST be used. - - For example, consider a system measuring the number of requests that - it receives and reports the sum of these requests every second as a - CUMULATIVE metric: - - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+2 with a value of 5. - 9. The system experiences a fault and loses state. - 10. The system recovers and resumes receiving at time=t_1. - 11. A request is received, the system measures 1 request. - 12. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_1 to - t_0+1 with a value of 1. - - Note: Even though, when reporting changes since last report time, using - CUMULATIVE is valid, it is not recommended. This may cause problems for - systems that do not use start_time to determine when the aggregation - value was reset (e.g. Prometheus). - """ - -class AggregationTemporality(_AggregationTemporality, metaclass=_AggregationTemporalityEnumTypeWrapper): - """AggregationTemporality defines how a metric aggregator reports aggregated - values. It describes how those values relate to the time interval over - which they are aggregated. - """ - -AGGREGATION_TEMPORALITY_UNSPECIFIED: AggregationTemporality.ValueType # 0 -"""UNSPECIFIED is the default AggregationTemporality, it MUST not be used.""" -AGGREGATION_TEMPORALITY_DELTA: AggregationTemporality.ValueType # 1 -"""DELTA is an AggregationTemporality for a metric aggregator which reports -changes since last report time. Successive metrics contain aggregation of -values from continuous and non-overlapping intervals. - -The values for a DELTA metric are based only on the time interval -associated with one measurement cycle. There is no dependency on -previous measurements like is the case for CUMULATIVE metrics. - -For example, consider a system measuring the number of requests that -it receives and reports the sum of these requests every second as a -DELTA metric: - - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0+1 to - t_0+2 with a value of 2. -""" -AGGREGATION_TEMPORALITY_CUMULATIVE: AggregationTemporality.ValueType # 2 -"""CUMULATIVE is an AggregationTemporality for a metric aggregator which -reports changes since a fixed start time. This means that current values -of a CUMULATIVE metric depend on all previous measurements since the -start time. Because of this, the sender is required to retain this state -in some form. If this state is lost or invalidated, the CUMULATIVE metric -values MUST be reset and a new fixed start time following the last -reported measurement time sent MUST be used. - -For example, consider a system measuring the number of requests that -it receives and reports the sum of these requests every second as a -CUMULATIVE metric: - - 1. The system starts receiving at time=t_0. - 2. A request is received, the system measures 1 request. - 3. A request is received, the system measures 1 request. - 4. A request is received, the system measures 1 request. - 5. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+1 with a value of 3. - 6. A request is received, the system measures 1 request. - 7. A request is received, the system measures 1 request. - 8. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_0 to - t_0+2 with a value of 5. - 9. The system experiences a fault and loses state. - 10. The system recovers and resumes receiving at time=t_1. - 11. A request is received, the system measures 1 request. - 12. The 1 second collection cycle ends. A metric is exported for the - number of requests received over the interval of time t_1 to - t_0+1 with a value of 1. - -Note: Even though, when reporting changes since last report time, using -CUMULATIVE is valid, it is not recommended. This may cause problems for -systems that do not use start_time to determine when the aggregation -value was reset (e.g. Prometheus). -""" -global___AggregationTemporality = AggregationTemporality - -class _DataPointFlags: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _DataPointFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_DataPointFlags.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - DATA_POINT_FLAGS_DO_NOT_USE: _DataPointFlags.ValueType # 0 - """The zero value for the enum. Should not be used for comparisons. - Instead use bitwise "and" with the appropriate mask as shown above. - """ - DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK: _DataPointFlags.ValueType # 1 - """This DataPoint is valid but has no recorded value. This value - SHOULD be used to reflect explicitly missing data in a series, as - for an equivalent to the Prometheus "staleness marker". - """ - -class DataPointFlags(_DataPointFlags, metaclass=_DataPointFlagsEnumTypeWrapper): - """DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a - bit-field representing 32 distinct boolean flags. Each flag defined in this - enum is a bit-mask. To test the presence of a single flag in the flags of - a data point, for example, use an expression like: - - (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK - """ - -DATA_POINT_FLAGS_DO_NOT_USE: DataPointFlags.ValueType # 0 -"""The zero value for the enum. Should not be used for comparisons. -Instead use bitwise "and" with the appropriate mask as shown above. -""" -DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK: DataPointFlags.ValueType # 1 -"""This DataPoint is valid but has no recorded value. This value -SHOULD be used to reflect explicitly missing data in a series, as -for an equivalent to the Prometheus "staleness marker". -""" -global___DataPointFlags = DataPointFlags - -@typing_extensions.final -class MetricsData(google.protobuf.message.Message): - """MetricsData represents the metrics data that can be stored in a persistent - storage, OR can be embedded by other protocols that transfer OTLP metrics - data but do not implement the OTLP protocol. - - MetricsData - └─── ResourceMetrics - ├── Resource - ├── SchemaURL - └── ScopeMetrics - ├── Scope - ├── SchemaURL - └── Metric - ├── Name - ├── Description - ├── Unit - └── data - ├── Gauge - ├── Sum - ├── Histogram - ├── ExponentialHistogram - └── Summary - - The main difference between this message and collector protocol is that - in this message there will not be any "control" or "metadata" specific to - OTLP protocol. - - When new fields are added into this message, the OTLP request MUST be updated - as well. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_METRICS_FIELD_NUMBER: builtins.int - @property - def resource_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceMetrics]: - """An array of ResourceMetrics. - For data coming from a single resource this array will typically contain - one element. Intermediary nodes that receive data from multiple origins - typically batch the data before forwarding further and in that case this - array will contain multiple elements. - """ - def __init__( - self, - *, - resource_metrics: collections.abc.Iterable[global___ResourceMetrics] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_metrics", b"resource_metrics"]) -> None: ... - -global___MetricsData = MetricsData - -@typing_extensions.final -class ResourceMetrics(google.protobuf.message.Message): - """A collection of ScopeMetrics from a Resource.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_FIELD_NUMBER: builtins.int - SCOPE_METRICS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: - """The resource for the metrics in this message. - If this field is not set then no resource info is known. - """ - @property - def scope_metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeMetrics]: - """A list of metrics that originate from a resource.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the resource data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "scope_metrics" field which have their own schema_url field. - """ - def __init__( - self, - *, - resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., - scope_metrics: collections.abc.Iterable[global___ScopeMetrics] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_metrics", b"scope_metrics"]) -> None: ... - -global___ResourceMetrics = ResourceMetrics - -@typing_extensions.final -class ScopeMetrics(google.protobuf.message.Message): - """A collection of Metrics produced by an Scope.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SCOPE_FIELD_NUMBER: builtins.int - METRICS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: - """The instrumentation scope information for the metrics in this message. - Semantically when InstrumentationScope isn't set, it is equivalent with - an empty instrumentation scope name (unknown). - """ - @property - def metrics(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Metric]: - """A list of metrics that originate from an instrumentation library.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the metric data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "scope" field and all metrics in the - "metrics" field. - """ - def __init__( - self, - *, - scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., - metrics: collections.abc.Iterable[global___Metric] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["metrics", b"metrics", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... - -global___ScopeMetrics = ScopeMetrics - -@typing_extensions.final -class Metric(google.protobuf.message.Message): - """Defines a Metric which has one or more timeseries. The following is a - brief summary of the Metric data model. For more details, see: - - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md - - The data model and relation between entities is shown in the - diagram below. Here, "DataPoint" is the term used to refer to any - one of the specific data point value types, and "points" is the term used - to refer to any one of the lists of points contained in the Metric. - - - Metric is composed of a metadata and data. - - Metadata part contains a name, description, unit. - - Data is one of the possible types (Sum, Gauge, Histogram, Summary). - - DataPoint contains timestamps, attributes, and one of the possible value type - fields. - - Metric - +------------+ - |name | - |description | - |unit | +------------------------------------+ - |data |---> |Gauge, Sum, Histogram, Summary, ... | - +------------+ +------------------------------------+ - - Data [One of Gauge, Sum, Histogram, Summary, ...] - +-----------+ - |... | // Metadata about the Data. - |points |--+ - +-----------+ | - | +---------------------------+ - | |DataPoint 1 | - v |+------+------+ +------+ | - +-----+ ||label |label |...|label | | - | 1 |-->||value1|value2|...|valueN| | - +-----+ |+------+------+ +------+ | - | . | |+-----+ | - | . | ||value| | - | . | |+-----+ | - | . | +---------------------------+ - | . | . - | . | . - | . | . - | . | +---------------------------+ - | . | |DataPoint M | - +-----+ |+------+------+ +------+ | - | M |-->||label |label |...|label | | - +-----+ ||value1|value2|...|valueN| | - |+------+------+ +------+ | - |+-----+ | - ||value| | - |+-----+ | - +---------------------------+ - - Each distinct type of DataPoint represents the output of a specific - aggregation function, the result of applying the DataPoint's - associated function of to one or more measurements. - - All DataPoint types have three common fields: - - Attributes includes key-value pairs associated with the data point - - TimeUnixNano is required, set to the end time of the aggregation - - StartTimeUnixNano is optional, but strongly encouraged for DataPoints - having an AggregationTemporality field, as discussed below. - - Both TimeUnixNano and StartTimeUnixNano values are expressed as - UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - - # TimeUnixNano - - This field is required, having consistent interpretation across - DataPoint types. TimeUnixNano is the moment corresponding to when - the data point's aggregate value was captured. - - Data points with the 0 value for TimeUnixNano SHOULD be rejected - by consumers. - - # StartTimeUnixNano - - StartTimeUnixNano in general allows detecting when a sequence of - observations is unbroken. This field indicates to consumers the - start time for points with cumulative and delta - AggregationTemporality, and it should be included whenever possible - to support correct rate calculation. Although it may be omitted - when the start time is truly unknown, setting StartTimeUnixNano is - strongly encouraged. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - NAME_FIELD_NUMBER: builtins.int - DESCRIPTION_FIELD_NUMBER: builtins.int - UNIT_FIELD_NUMBER: builtins.int - GAUGE_FIELD_NUMBER: builtins.int - SUM_FIELD_NUMBER: builtins.int - HISTOGRAM_FIELD_NUMBER: builtins.int - EXPONENTIAL_HISTOGRAM_FIELD_NUMBER: builtins.int - SUMMARY_FIELD_NUMBER: builtins.int - METADATA_FIELD_NUMBER: builtins.int - name: builtins.str - """The name of the metric.""" - description: builtins.str - """A description of the metric, which can be used in documentation.""" - unit: builtins.str - """The unit in which the metric value is reported. Follows the format - described by https://unitsofmeasure.org/ucum.html. - """ - @property - def gauge(self) -> global___Gauge: ... - @property - def sum(self) -> global___Sum: ... - @property - def histogram(self) -> global___Histogram: ... - @property - def exponential_histogram(self) -> global___ExponentialHistogram: ... - @property - def summary(self) -> global___Summary: ... - @property - def metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """Additional metadata attributes that describe the metric. [Optional]. - Attributes are non-identifying. - Consumers SHOULD NOT need to be aware of these attributes. - These attributes MAY be used to encode information allowing - for lossless roundtrip translation to / from another data model. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - def __init__( - self, - *, - name: builtins.str = ..., - description: builtins.str = ..., - unit: builtins.str = ..., - gauge: global___Gauge | None = ..., - sum: global___Sum | None = ..., - histogram: global___Histogram | None = ..., - exponential_histogram: global___ExponentialHistogram | None = ..., - summary: global___Summary | None = ..., - metadata: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["data", b"data", "exponential_histogram", b"exponential_histogram", "gauge", b"gauge", "histogram", b"histogram", "sum", b"sum", "summary", b"summary"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["data", b"data", "description", b"description", "exponential_histogram", b"exponential_histogram", "gauge", b"gauge", "histogram", b"histogram", "metadata", b"metadata", "name", b"name", "sum", b"sum", "summary", b"summary", "unit", b"unit"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["data", b"data"]) -> typing_extensions.Literal["gauge", "sum", "histogram", "exponential_histogram", "summary"] | None: ... - -global___Metric = Metric - -@typing_extensions.final -class Gauge(google.protobuf.message.Message): - """Gauge represents the type of a scalar metric that always exports the - "current value" for every data point. It should be used for an "unknown" - aggregation. - - A Gauge does not support different aggregation temporalities. Given the - aggregation is unknown, points cannot be combined using the same - aggregation, regardless of aggregation temporalities. Therefore, - AggregationTemporality is not included. Consequently, this also means - "StartTimeUnixNano" is ignored for all data points. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - DATA_POINTS_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: - """The time series data points. - Note: Multiple time series may be included (same timestamp, different attributes). - """ - def __init__( - self, - *, - data_points: collections.abc.Iterable[global___NumberDataPoint] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["data_points", b"data_points"]) -> None: ... - -global___Gauge = Gauge - -@typing_extensions.final -class Sum(google.protobuf.message.Message): - """Sum represents the type of a scalar metric that is calculated as a sum of all - reported measurements over a time interval. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - DATA_POINTS_FIELD_NUMBER: builtins.int - AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - IS_MONOTONIC_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NumberDataPoint]: - """The time series data points. - Note: Multiple time series may be included (same timestamp, different attributes). - """ - aggregation_temporality: global___AggregationTemporality.ValueType - """aggregation_temporality describes if the aggregator reports delta changes - since last report time, or cumulative changes since a fixed start time. - """ - is_monotonic: builtins.bool - """Represents whether the sum is monotonic.""" - def __init__( - self, - *, - data_points: collections.abc.Iterable[global___NumberDataPoint] | None = ..., - aggregation_temporality: global___AggregationTemporality.ValueType = ..., - is_monotonic: builtins.bool = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points", "is_monotonic", b"is_monotonic"]) -> None: ... - -global___Sum = Sum - -@typing_extensions.final -class Histogram(google.protobuf.message.Message): - """Histogram represents the type of a metric that is calculated by aggregating - as a Histogram of all reported measurements over a time interval. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - DATA_POINTS_FIELD_NUMBER: builtins.int - AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___HistogramDataPoint]: - """The time series data points. - Note: Multiple time series may be included (same timestamp, different attributes). - """ - aggregation_temporality: global___AggregationTemporality.ValueType - """aggregation_temporality describes if the aggregator reports delta changes - since last report time, or cumulative changes since a fixed start time. - """ - def __init__( - self, - *, - data_points: collections.abc.Iterable[global___HistogramDataPoint] | None = ..., - aggregation_temporality: global___AggregationTemporality.ValueType = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points"]) -> None: ... - -global___Histogram = Histogram - -@typing_extensions.final -class ExponentialHistogram(google.protobuf.message.Message): - """ExponentialHistogram represents the type of a metric that is calculated by aggregating - as a ExponentialHistogram of all reported double measurements over a time interval. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - DATA_POINTS_FIELD_NUMBER: builtins.int - AGGREGATION_TEMPORALITY_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ExponentialHistogramDataPoint]: - """The time series data points. - Note: Multiple time series may be included (same timestamp, different attributes). - """ - aggregation_temporality: global___AggregationTemporality.ValueType - """aggregation_temporality describes if the aggregator reports delta changes - since last report time, or cumulative changes since a fixed start time. - """ - def __init__( - self, - *, - data_points: collections.abc.Iterable[global___ExponentialHistogramDataPoint] | None = ..., - aggregation_temporality: global___AggregationTemporality.ValueType = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregation_temporality", b"aggregation_temporality", "data_points", b"data_points"]) -> None: ... - -global___ExponentialHistogram = ExponentialHistogram - -@typing_extensions.final -class Summary(google.protobuf.message.Message): - """Summary metric data are used to convey quantile summaries, - a Prometheus (see: https://prometheus.io/docs/concepts/metric_types/#summary) - and OpenMetrics (see: https://github.com/prometheus/OpenMetrics/blob/4dbf6075567ab43296eed941037c12951faafb92/protos/prometheus.proto#L45) - data type. These data points cannot always be merged in a meaningful way. - While they can be useful in some applications, histogram data points are - recommended for new applications. - Summary metrics do not have an aggregation temporality field. This is - because the count and sum fields of a SummaryDataPoint are assumed to be - cumulative values. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - DATA_POINTS_FIELD_NUMBER: builtins.int - @property - def data_points(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint]: - """The time series data points. - Note: Multiple time series may be included (same timestamp, different attributes). - """ - def __init__( - self, - *, - data_points: collections.abc.Iterable[global___SummaryDataPoint] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["data_points", b"data_points"]) -> None: ... - -global___Summary = Summary - -@typing_extensions.final -class NumberDataPoint(google.protobuf.message.Message): - """NumberDataPoint is a single data point in a timeseries that describes the - time-varying scalar value of a metric. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - ATTRIBUTES_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - AS_DOUBLE_FIELD_NUMBER: builtins.int - AS_INT_FIELD_NUMBER: builtins.int - EXEMPLARS_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """The set of key/value pairs that uniquely identify the timeseries from - where this point belongs. The list may be empty (may contain 0 elements). - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - start_time_unix_nano: builtins.int - """StartTimeUnixNano is optional but strongly encouraged, see the - the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - time_unix_nano: builtins.int - """TimeUnixNano is required, see the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - as_double: builtins.float - as_int: builtins.int - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: - """(Optional) List of exemplars collected from - measurements that were used to form the data point - """ - flags: builtins.int - """Flags that apply to this specific data point. See DataPointFlags - for the available flags and their meaning. - """ - def __init__( - self, - *, - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - start_time_unix_nano: builtins.int = ..., - time_unix_nano: builtins.int = ..., - as_double: builtins.float = ..., - as_int: builtins.int = ..., - exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., - flags: builtins.int = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "attributes", b"attributes", "exemplars", b"exemplars", "flags", b"flags", "start_time_unix_nano", b"start_time_unix_nano", "time_unix_nano", b"time_unix_nano", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["as_double", "as_int"] | None: ... - -global___NumberDataPoint = NumberDataPoint - -@typing_extensions.final -class HistogramDataPoint(google.protobuf.message.Message): - """HistogramDataPoint is a single data point in a timeseries that describes the - time-varying values of a Histogram. A Histogram contains summary statistics - for a population of values, it may optionally contain the distribution of - those values across a set of buckets. - - If the histogram contains the distribution of values, then both - "explicit_bounds" and "bucket counts" fields must be defined. - If the histogram does not contain the distribution of values, then both - "explicit_bounds" and "bucket_counts" must be omitted and only "count" and - "sum" are known. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - ATTRIBUTES_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - COUNT_FIELD_NUMBER: builtins.int - SUM_FIELD_NUMBER: builtins.int - BUCKET_COUNTS_FIELD_NUMBER: builtins.int - EXPLICIT_BOUNDS_FIELD_NUMBER: builtins.int - EXEMPLARS_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - MIN_FIELD_NUMBER: builtins.int - MAX_FIELD_NUMBER: builtins.int - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """The set of key/value pairs that uniquely identify the timeseries from - where this point belongs. The list may be empty (may contain 0 elements). - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - start_time_unix_nano: builtins.int - """StartTimeUnixNano is optional but strongly encouraged, see the - the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - time_unix_nano: builtins.int - """TimeUnixNano is required, see the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - count: builtins.int - """count is the number of values in the population. Must be non-negative. This - value must be equal to the sum of the "count" fields in buckets if a - histogram is provided. - """ - sum: builtins.float - """sum of the values in the population. If count is zero then this field - must be zero. - - Note: Sum should only be filled out when measuring non-negative discrete - events, and is assumed to be monotonic over the values of these events. - Negative events *can* be recorded, but sum should not be filled out when - doing so. This is specifically to enforce compatibility w/ OpenMetrics, - see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram - """ - @property - def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """bucket_counts is an optional field contains the count values of histogram - for each bucket. - - The sum of the bucket_counts must equal the value in the count field. - - The number of elements in bucket_counts array must be by one greater than - the number of elements in explicit_bounds array. The exception to this rule - is when the length of bucket_counts is 0, then the length of explicit_bounds - must also be 0. - """ - @property - def explicit_bounds(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: - """explicit_bounds specifies buckets with explicitly defined bounds for values. - - The boundaries for bucket at index i are: - - (-infinity, explicit_bounds[i]] for i == 0 - (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) - (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) - - The values in the explicit_bounds array must be strictly increasing. - - Histogram buckets are inclusive of their upper boundary, except the last - bucket where the boundary is at infinity. This format is intentionally - compatible with the OpenMetrics histogram definition. - - If bucket_counts length is 0 then explicit_bounds length must also be 0, - otherwise the data point is invalid. - """ - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: - """(Optional) List of exemplars collected from - measurements that were used to form the data point - """ - flags: builtins.int - """Flags that apply to this specific data point. See DataPointFlags - for the available flags and their meaning. - """ - min: builtins.float - """min is the minimum value over (start_time, end_time].""" - max: builtins.float - """max is the maximum value over (start_time, end_time].""" - def __init__( - self, - *, - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - start_time_unix_nano: builtins.int = ..., - time_unix_nano: builtins.int = ..., - count: builtins.int = ..., - sum: builtins.float | None = ..., - bucket_counts: collections.abc.Iterable[builtins.int] | None = ..., - explicit_bounds: collections.abc.Iterable[builtins.float] | None = ..., - exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., - flags: builtins.int = ..., - min: builtins.float | None = ..., - max: builtins.float | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "max", b"max", "min", b"min", "sum", b"sum"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "attributes", b"attributes", "bucket_counts", b"bucket_counts", "count", b"count", "exemplars", b"exemplars", "explicit_bounds", b"explicit_bounds", "flags", b"flags", "max", b"max", "min", b"min", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano"]) -> None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_max", b"_max"]) -> typing_extensions.Literal["max"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_min", b"_min"]) -> typing_extensions.Literal["min"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum", b"_sum"]) -> typing_extensions.Literal["sum"] | None: ... - -global___HistogramDataPoint = HistogramDataPoint - -@typing_extensions.final -class ExponentialHistogramDataPoint(google.protobuf.message.Message): - """ExponentialHistogramDataPoint is a single data point in a timeseries that describes the - time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains - summary statistics for a population of values, it may optionally contain the - distribution of those values across a set of buckets. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - @typing_extensions.final - class Buckets(google.protobuf.message.Message): - """Buckets are a set of bucket counts, encoded in a contiguous array - of counts. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - OFFSET_FIELD_NUMBER: builtins.int - BUCKET_COUNTS_FIELD_NUMBER: builtins.int - offset: builtins.int - """The bucket index of the first entry in the bucket_counts array. - - Note: This uses a varint encoding as a simple form of compression. - """ - @property - def bucket_counts(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """An array of count values, where bucket_counts[i] carries - the count of the bucket at index (offset+i). bucket_counts[i] is the count - of values greater than base^(offset+i) and less than or equal to - base^(offset+i+1). - - Note: By contrast, the explicit HistogramDataPoint uses - fixed64. This field is expected to have many buckets, - especially zeros, so uint64 has been selected to ensure - varint encoding. - """ - def __init__( - self, - *, - offset: builtins.int = ..., - bucket_counts: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["bucket_counts", b"bucket_counts", "offset", b"offset"]) -> None: ... - - ATTRIBUTES_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - COUNT_FIELD_NUMBER: builtins.int - SUM_FIELD_NUMBER: builtins.int - SCALE_FIELD_NUMBER: builtins.int - ZERO_COUNT_FIELD_NUMBER: builtins.int - POSITIVE_FIELD_NUMBER: builtins.int - NEGATIVE_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - EXEMPLARS_FIELD_NUMBER: builtins.int - MIN_FIELD_NUMBER: builtins.int - MAX_FIELD_NUMBER: builtins.int - ZERO_THRESHOLD_FIELD_NUMBER: builtins.int - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """The set of key/value pairs that uniquely identify the timeseries from - where this point belongs. The list may be empty (may contain 0 elements). - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - start_time_unix_nano: builtins.int - """StartTimeUnixNano is optional but strongly encouraged, see the - the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - time_unix_nano: builtins.int - """TimeUnixNano is required, see the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - count: builtins.int - """The number of values in the population. Must be - non-negative. This value must be equal to the sum of the "bucket_counts" - values in the positive and negative Buckets plus the "zero_count" field. - """ - sum: builtins.float - """The sum of the values in the population. If count is zero then this field - must be zero. - - Note: Sum should only be filled out when measuring non-negative discrete - events, and is assumed to be monotonic over the values of these events. - Negative events *can* be recorded, but sum should not be filled out when - doing so. This is specifically to enforce compatibility w/ OpenMetrics, - see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#histogram - """ - scale: builtins.int - """scale describes the resolution of the histogram. Boundaries are - located at powers of the base, where: - - base = (2^(2^-scale)) - - The histogram bucket identified by `index`, a signed integer, - contains values that are greater than (base^index) and - less than or equal to (base^(index+1)). - - The positive and negative ranges of the histogram are expressed - separately. Negative values are mapped by their absolute value - into the negative range using the same scale as the positive range. - - scale is not restricted by the protocol, as the permissible - values depend on the range of the data. - """ - zero_count: builtins.int - """The count of values that are either exactly zero or - within the region considered zero by the instrumentation at the - tolerated degree of precision. This bucket stores values that - cannot be expressed using the standard exponential formula as - well as values that have been rounded to zero. - - Implementations MAY consider the zero bucket to have probability - mass equal to (zero_count / count). - """ - @property - def positive(self) -> global___ExponentialHistogramDataPoint.Buckets: - """positive carries the positive range of exponential bucket counts.""" - @property - def negative(self) -> global___ExponentialHistogramDataPoint.Buckets: - """negative carries the negative range of exponential bucket counts.""" - flags: builtins.int - """Flags that apply to this specific data point. See DataPointFlags - for the available flags and their meaning. - """ - @property - def exemplars(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Exemplar]: - """(Optional) List of exemplars collected from - measurements that were used to form the data point - """ - min: builtins.float - """The minimum value over (start_time, end_time].""" - max: builtins.float - """The maximum value over (start_time, end_time].""" - zero_threshold: builtins.float - """ZeroThreshold may be optionally set to convey the width of the zero - region. Where the zero region is defined as the closed interval - [-ZeroThreshold, ZeroThreshold]. - When ZeroThreshold is 0, zero count bucket stores values that cannot be - expressed using the standard exponential formula as well as values that - have been rounded to zero. - """ - def __init__( - self, - *, - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - start_time_unix_nano: builtins.int = ..., - time_unix_nano: builtins.int = ..., - count: builtins.int = ..., - sum: builtins.float | None = ..., - scale: builtins.int = ..., - zero_count: builtins.int = ..., - positive: global___ExponentialHistogramDataPoint.Buckets | None = ..., - negative: global___ExponentialHistogramDataPoint.Buckets | None = ..., - flags: builtins.int = ..., - exemplars: collections.abc.Iterable[global___Exemplar] | None = ..., - min: builtins.float | None = ..., - max: builtins.float | None = ..., - zero_threshold: builtins.float = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "max", b"max", "min", b"min", "negative", b"negative", "positive", b"positive", "sum", b"sum"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["_max", b"_max", "_min", b"_min", "_sum", b"_sum", "attributes", b"attributes", "count", b"count", "exemplars", b"exemplars", "flags", b"flags", "max", b"max", "min", b"min", "negative", b"negative", "positive", b"positive", "scale", b"scale", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano", "zero_count", b"zero_count", "zero_threshold", b"zero_threshold"]) -> None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_max", b"_max"]) -> typing_extensions.Literal["max"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_min", b"_min"]) -> typing_extensions.Literal["min"] | None: ... - @typing.overload - def WhichOneof(self, oneof_group: typing_extensions.Literal["_sum", b"_sum"]) -> typing_extensions.Literal["sum"] | None: ... - -global___ExponentialHistogramDataPoint = ExponentialHistogramDataPoint - -@typing_extensions.final -class SummaryDataPoint(google.protobuf.message.Message): - """SummaryDataPoint is a single data point in a timeseries that describes the - time-varying values of a Summary metric. The count and sum fields represent - cumulative values. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - @typing_extensions.final - class ValueAtQuantile(google.protobuf.message.Message): - """Represents the value at a given quantile of a distribution. - - To record Min and Max values following conventions are used: - - The 1.0 quantile is equivalent to the maximum value observed. - - The 0.0 quantile is equivalent to the minimum value observed. - - See the following issue for more context: - https://github.com/open-telemetry/opentelemetry-proto/issues/125 - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - QUANTILE_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - quantile: builtins.float - """The quantile of a distribution. Must be in the interval - [0.0, 1.0]. - """ - value: builtins.float - """The value at the given quantile of a distribution. - - Quantile values must NOT be negative. - """ - def __init__( - self, - *, - quantile: builtins.float = ..., - value: builtins.float = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["quantile", b"quantile", "value", b"value"]) -> None: ... - - ATTRIBUTES_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - COUNT_FIELD_NUMBER: builtins.int - SUM_FIELD_NUMBER: builtins.int - QUANTILE_VALUES_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """The set of key/value pairs that uniquely identify the timeseries from - where this point belongs. The list may be empty (may contain 0 elements). - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - start_time_unix_nano: builtins.int - """StartTimeUnixNano is optional but strongly encouraged, see the - the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - time_unix_nano: builtins.int - """TimeUnixNano is required, see the detailed comments above Metric. - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - count: builtins.int - """count is the number of values in the population. Must be non-negative.""" - sum: builtins.float - """sum of the values in the population. If count is zero then this field - must be zero. - - Note: Sum should only be filled out when measuring non-negative discrete - events, and is assumed to be monotonic over the values of these events. - Negative events *can* be recorded, but sum should not be filled out when - doing so. This is specifically to enforce compatibility w/ OpenMetrics, - see: https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#summary - """ - @property - def quantile_values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___SummaryDataPoint.ValueAtQuantile]: - """(Optional) list of values at different quantiles of the distribution calculated - from the current snapshot. The quantiles must be strictly increasing. - """ - flags: builtins.int - """Flags that apply to this specific data point. See DataPointFlags - for the available flags and their meaning. - """ - def __init__( - self, - *, - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - start_time_unix_nano: builtins.int = ..., - time_unix_nano: builtins.int = ..., - count: builtins.int = ..., - sum: builtins.float = ..., - quantile_values: collections.abc.Iterable[global___SummaryDataPoint.ValueAtQuantile] | None = ..., - flags: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "count", b"count", "flags", b"flags", "quantile_values", b"quantile_values", "start_time_unix_nano", b"start_time_unix_nano", "sum", b"sum", "time_unix_nano", b"time_unix_nano"]) -> None: ... - -global___SummaryDataPoint = SummaryDataPoint - -@typing_extensions.final -class Exemplar(google.protobuf.message.Message): - """A representation of an exemplar, which is a sample input measurement. - Exemplars also hold information about the environment when the measurement - was recorded, for example the span and trace ID of the active span when the - exemplar was recorded. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - FILTERED_ATTRIBUTES_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - AS_DOUBLE_FIELD_NUMBER: builtins.int - AS_INT_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - TRACE_ID_FIELD_NUMBER: builtins.int - @property - def filtered_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """The set of key/value pairs that were filtered out by the aggregator, but - recorded alongside the original measurement. Only key/value pairs that were - filtered out by the aggregator should be included - """ - time_unix_nano: builtins.int - """time_unix_nano is the exact time when this exemplar was recorded - - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - 1970. - """ - as_double: builtins.float - as_int: builtins.int - span_id: builtins.bytes - """(Optional) Span ID of the exemplar trace. - span_id may be missing if the measurement is not recorded inside a trace - or if the trace is not sampled. - """ - trace_id: builtins.bytes - """(Optional) Trace ID of the exemplar trace. - trace_id may be missing if the measurement is not recorded inside a trace - or if the trace is not sampled. - """ - def __init__( - self, - *, - filtered_attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - time_unix_nano: builtins.int = ..., - as_double: builtins.float = ..., - as_int: builtins.int = ..., - span_id: builtins.bytes = ..., - trace_id: builtins.bytes = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["as_double", b"as_double", "as_int", b"as_int", "filtered_attributes", b"filtered_attributes", "span_id", b"span_id", "time_unix_nano", b"time_unix_nano", "trace_id", b"trace_id", "value", b"value"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["as_double", "as_int"] | None: ... - -global___Exemplar = Exemplar diff --git a/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.py deleted file mode 100644 index f78c6abd713..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/profiles/v1development/profiles.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n9opentelemetry/proto/profiles/v1development/profiles.proto\x12*opentelemetry.proto.profiles.v1development\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"\xf6\x03\n\x12ProfilesDictionary\x12J\n\rmapping_table\x18\x01 \x03(\x0b\x32\x33.opentelemetry.proto.profiles.v1development.Mapping\x12L\n\x0elocation_table\x18\x02 \x03(\x0b\x32\x34.opentelemetry.proto.profiles.v1development.Location\x12L\n\x0e\x66unction_table\x18\x03 \x03(\x0b\x32\x34.opentelemetry.proto.profiles.v1development.Function\x12\x44\n\nlink_table\x18\x04 \x03(\x0b\x32\x30.opentelemetry.proto.profiles.v1development.Link\x12\x14\n\x0cstring_table\x18\x05 \x03(\t\x12T\n\x0f\x61ttribute_table\x18\x06 \x03(\x0b\x32;.opentelemetry.proto.profiles.v1development.KeyValueAndUnit\x12\x46\n\x0bstack_table\x18\x07 \x03(\x0b\x32\x31.opentelemetry.proto.profiles.v1development.Stack\"\xbb\x01\n\x0cProfilesData\x12W\n\x11resource_profiles\x18\x01 \x03(\x0b\x32<.opentelemetry.proto.profiles.v1development.ResourceProfiles\x12R\n\ndictionary\x18\x02 \x01(\x0b\x32>.opentelemetry.proto.profiles.v1development.ProfilesDictionary\"\xbe\x01\n\x10ResourceProfiles\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12Q\n\x0escope_profiles\x18\x02 \x03(\x0b\x32\x39.opentelemetry.proto.profiles.v1development.ScopeProfiles\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xae\x01\n\rScopeProfiles\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x45\n\x08profiles\x18\x02 \x03(\x0b\x32\x33.opentelemetry.proto.profiles.v1development.Profile\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xb1\x03\n\x07Profile\x12J\n\x0bsample_type\x18\x01 \x01(\x0b\x32\x35.opentelemetry.proto.profiles.v1development.ValueType\x12\x43\n\x07samples\x18\x02 \x03(\x0b\x32\x32.opentelemetry.proto.profiles.v1development.Sample\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x15\n\rduration_nano\x18\x04 \x01(\x04\x12J\n\x0bperiod_type\x18\x05 \x01(\x0b\x32\x35.opentelemetry.proto.profiles.v1development.ValueType\x12\x0e\n\x06period\x18\x06 \x01(\x03\x12\x12\n\nprofile_id\x18\x07 \x01(\x0c\x12 \n\x18\x64ropped_attributes_count\x18\x08 \x01(\r\x12\x1f\n\x17original_payload_format\x18\t \x01(\t\x12\x18\n\x10original_payload\x18\n \x01(\x0c\x12\x19\n\x11\x61ttribute_indices\x18\x0b \x03(\x05\")\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\"9\n\tValueType\x12\x15\n\rtype_strindex\x18\x01 \x01(\x05\x12\x15\n\runit_strindex\x18\x02 \x01(\x05\"z\n\x06Sample\x12\x13\n\x0bstack_index\x18\x01 \x01(\x05\x12\x19\n\x11\x61ttribute_indices\x18\x02 \x03(\x05\x12\x12\n\nlink_index\x18\x03 \x01(\x05\x12\x0e\n\x06values\x18\x04 \x03(\x03\x12\x1c\n\x14timestamps_unix_nano\x18\x05 \x03(\x06\"\x80\x01\n\x07Mapping\x12\x14\n\x0cmemory_start\x18\x01 \x01(\x04\x12\x14\n\x0cmemory_limit\x18\x02 \x01(\x04\x12\x13\n\x0b\x66ile_offset\x18\x03 \x01(\x04\x12\x19\n\x11\x66ilename_strindex\x18\x04 \x01(\x05\x12\x19\n\x11\x61ttribute_indices\x18\x05 \x03(\x05\"!\n\x05Stack\x12\x18\n\x10location_indices\x18\x01 \x03(\x05\"\x8e\x01\n\x08Location\x12\x15\n\rmapping_index\x18\x01 \x01(\x05\x12\x0f\n\x07\x61\x64\x64ress\x18\x02 \x01(\x04\x12?\n\x05lines\x18\x03 \x03(\x0b\x32\x30.opentelemetry.proto.profiles.v1development.Line\x12\x19\n\x11\x61ttribute_indices\x18\x04 \x03(\x05\"<\n\x04Line\x12\x16\n\x0e\x66unction_index\x18\x01 \x01(\x05\x12\x0c\n\x04line\x18\x02 \x01(\x03\x12\x0e\n\x06\x63olumn\x18\x03 \x01(\x03\"n\n\x08\x46unction\x12\x15\n\rname_strindex\x18\x01 \x01(\x05\x12\x1c\n\x14system_name_strindex\x18\x02 \x01(\x05\x12\x19\n\x11\x66ilename_strindex\x18\x03 \x01(\x05\x12\x12\n\nstart_line\x18\x04 \x01(\x03\"v\n\x0fKeyValueAndUnit\x12\x14\n\x0ckey_strindex\x18\x01 \x01(\x05\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12\x15\n\runit_strindex\x18\x03 \x01(\x05\x42\xa4\x01\n-io.opentelemetry.proto.profiles.v1developmentB\rProfilesProtoP\x01Z5go.opentelemetry.io/proto/otlp/profiles/v1development\xaa\x02*OpenTelemetry.Proto.Profiles.V1Developmentb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.profiles.v1development.profiles_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n-io.opentelemetry.proto.profiles.v1developmentB\rProfilesProtoP\001Z5go.opentelemetry.io/proto/otlp/profiles/v1development\252\002*OpenTelemetry.Proto.Profiles.V1Development' - _globals['_PROFILESDICTIONARY']._serialized_start=198 - _globals['_PROFILESDICTIONARY']._serialized_end=700 - _globals['_PROFILESDATA']._serialized_start=703 - _globals['_PROFILESDATA']._serialized_end=890 - _globals['_RESOURCEPROFILES']._serialized_start=893 - _globals['_RESOURCEPROFILES']._serialized_end=1083 - _globals['_SCOPEPROFILES']._serialized_start=1086 - _globals['_SCOPEPROFILES']._serialized_end=1260 - _globals['_PROFILE']._serialized_start=1263 - _globals['_PROFILE']._serialized_end=1696 - _globals['_LINK']._serialized_start=1698 - _globals['_LINK']._serialized_end=1739 - _globals['_VALUETYPE']._serialized_start=1741 - _globals['_VALUETYPE']._serialized_end=1798 - _globals['_SAMPLE']._serialized_start=1800 - _globals['_SAMPLE']._serialized_end=1922 - _globals['_MAPPING']._serialized_start=1925 - _globals['_MAPPING']._serialized_end=2053 - _globals['_STACK']._serialized_start=2055 - _globals['_STACK']._serialized_end=2088 - _globals['_LOCATION']._serialized_start=2091 - _globals['_LOCATION']._serialized_end=2233 - _globals['_LINE']._serialized_start=2235 - _globals['_LINE']._serialized_end=2295 - _globals['_FUNCTION']._serialized_start=2297 - _globals['_FUNCTION']._serialized_end=2407 - _globals['_KEYVALUEANDUNIT']._serialized_start=2409 - _globals['_KEYVALUEANDUNIT']._serialized_end=2527 -# @@protoc_insertion_point(module_scope) diff --git a/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.pyi deleted file mode 100644 index 01938eded00..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/profiles/v1development/profiles_pb2.pyi +++ /dev/null @@ -1,800 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2023, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -This file includes work covered by the following copyright and permission notices: - -Copyright 2016 Google Inc. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.common.v1.common_pb2 -import opentelemetry.proto.resource.v1.resource_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class ProfilesDictionary(google.protobuf.message.Message): - """ Relationships Diagram - - ┌──────────────────┐ LEGEND - │ ProfilesData │ ─────┐ - └──────────────────┘ │ ─────▶ embedded - │ │ - │ 1-n │ ─────▷ referenced by index - ▼ ▼ - ┌──────────────────┐ ┌────────────────────┐ - │ ResourceProfiles │ │ ProfilesDictionary │ - └──────────────────┘ └────────────────────┘ - │ - │ 1-n - ▼ - ┌──────────────────┐ - │ ScopeProfiles │ - └──────────────────┘ - │ - │ 1-n - ▼ - ┌──────────────────┐ - │ Profile │ - └──────────────────┘ - │ n-1 - │ 1-n ┌───────────────────────────────────────┐ - ▼ │ ▽ - ┌──────────────────┐ 1-n ┌─────────────────┐ ┌──────────┐ - │ Sample │ ──────▷ │ KeyValueAndUnit │ │ Link │ - └──────────────────┘ └─────────────────┘ └──────────┘ - │ △ △ - │ n-1 │ │ 1-n - ▽ │ │ - ┌──────────────────┐ │ │ - │ Stack │ │ │ - └──────────────────┘ │ │ - │ 1-n │ │ - │ 1-n ┌────────────────┘ │ - ▽ │ │ - ┌──────────────────┐ n-1 ┌─────────────┐ - │ Location │ ──────▷ │ Mapping │ - └──────────────────┘ └─────────────┘ - │ - │ 1-n - ▼ - ┌──────────────────┐ - │ Line │ - └──────────────────┘ - │ - │ 1-1 - ▽ - ┌──────────────────┐ - │ Function │ - └──────────────────┘ - - ProfilesDictionary represents the profiles data shared across the - entire message being sent. The following applies to all fields in this - message: - - - A dictionary is an array of dictionary items. Users of the dictionary - compactly reference the items using the index within the array. - - - A dictionary MUST have a zero value encoded as the first element. This - allows for _index fields pointing into the dictionary to use a 0 pointer - value to indicate 'null' / 'not set'. Unless otherwise defined, a 'zero - value' message value is one with all default field values, so as to - minimize wire encoded size. - - - There SHOULD NOT be dupes in a dictionary. The identity of dictionary - items is based on their value, recursively as needed. If a particular - implementation does emit duplicated items, it MUST NOT attempt to give them - meaning based on the index or order. A profile processor may remove - duplicate items and this MUST NOT have any observable effects for - consumers. - - - There SHOULD NOT be orphaned (unreferenced) items in a dictionary. A - profile processor may remove ("garbage-collect") orphaned items and this - MUST NOT have any observable effects for consumers. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - MAPPING_TABLE_FIELD_NUMBER: builtins.int - LOCATION_TABLE_FIELD_NUMBER: builtins.int - FUNCTION_TABLE_FIELD_NUMBER: builtins.int - LINK_TABLE_FIELD_NUMBER: builtins.int - STRING_TABLE_FIELD_NUMBER: builtins.int - ATTRIBUTE_TABLE_FIELD_NUMBER: builtins.int - STACK_TABLE_FIELD_NUMBER: builtins.int - @property - def mapping_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Mapping]: - """Mappings from address ranges to the image/binary/library mapped - into that address range referenced by locations via Location.mapping_index. - - mapping_table[0] must always be zero value (Mapping{}) and present. - """ - @property - def location_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Location]: - """Locations referenced by samples via Stack.location_indices. - - location_table[0] must always be zero value (Location{}) and present. - """ - @property - def function_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Function]: - """Functions referenced by locations via Line.function_index. - - function_table[0] must always be zero value (Function{}) and present. - """ - @property - def link_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Link]: - """Links referenced by samples via Sample.link_index. - - link_table[0] must always be zero value (Link{}) and present. - """ - @property - def string_table(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: - """A common table for strings referenced by various messages. - - string_table[0] must always be "" and present. - """ - @property - def attribute_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValueAndUnit]: - """A common table for attributes referenced by the Profile, Sample, Mapping - and Location messages below through attribute_indices field. Each entry is - a key/value pair with an optional unit. Since this is a dictionary table, - multiple entries with the same key may be present, unlike direct attribute - tables like Resource.attributes. The referencing attribute_indices fields, - though, do maintain the key uniqueness requirement. - - It's recommended to use attributes for variables with bounded cardinality, - such as categorical variables - (https://en.wikipedia.org/wiki/Categorical_variable). Using an attribute of - a floating point type (e.g., CPU time) in a sample can quickly make every - attribute value unique, defeating the purpose of the dictionary and - impractically increasing the profile size. - - Examples of attributes: - "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" - "abc.com/myattribute": true - "allocation_size": 128 bytes - - attribute_table[0] must always be zero value (KeyValueAndUnit{}) and present. - """ - @property - def stack_table(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Stack]: - """Stacks referenced by samples via Sample.stack_index. - - stack_table[0] must always be zero value (Stack{}) and present. - """ - def __init__( - self, - *, - mapping_table: collections.abc.Iterable[global___Mapping] | None = ..., - location_table: collections.abc.Iterable[global___Location] | None = ..., - function_table: collections.abc.Iterable[global___Function] | None = ..., - link_table: collections.abc.Iterable[global___Link] | None = ..., - string_table: collections.abc.Iterable[builtins.str] | None = ..., - attribute_table: collections.abc.Iterable[global___KeyValueAndUnit] | None = ..., - stack_table: collections.abc.Iterable[global___Stack] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attribute_table", b"attribute_table", "function_table", b"function_table", "link_table", b"link_table", "location_table", b"location_table", "mapping_table", b"mapping_table", "stack_table", b"stack_table", "string_table", b"string_table"]) -> None: ... - -global___ProfilesDictionary = ProfilesDictionary - -@typing_extensions.final -class ProfilesData(google.protobuf.message.Message): - """ProfilesData represents the profiles data that can be stored in persistent storage, - OR can be embedded by other protocols that transfer OTLP profiles data but do not - implement the OTLP protocol. - - The main difference between this message and collector protocol is that - in this message there will not be any "control" or "metadata" specific to - OTLP protocol. - - When new fields are added into this message, the OTLP request MUST be updated - as well. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_PROFILES_FIELD_NUMBER: builtins.int - DICTIONARY_FIELD_NUMBER: builtins.int - @property - def resource_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceProfiles]: - """An array of ResourceProfiles. - For data coming from an SDK profiler, this array will typically contain one - element. Host-level profilers will usually create one ResourceProfile per - container, as well as one additional ResourceProfile grouping all samples - from non-containerized processes. - Other resource groupings are possible as well and clarified via - Resource.attributes and semantic conventions. - Tools that visualize profiles should prefer displaying - resources_profiles[0].scope_profiles[0].profiles[0] by default. - """ - @property - def dictionary(self) -> global___ProfilesDictionary: - """One instance of ProfilesDictionary""" - def __init__( - self, - *, - resource_profiles: collections.abc.Iterable[global___ResourceProfiles] | None = ..., - dictionary: global___ProfilesDictionary | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["dictionary", b"dictionary", "resource_profiles", b"resource_profiles"]) -> None: ... - -global___ProfilesData = ProfilesData - -@typing_extensions.final -class ResourceProfiles(google.protobuf.message.Message): - """A collection of ScopeProfiles from a Resource.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_FIELD_NUMBER: builtins.int - SCOPE_PROFILES_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: - """The resource for the profiles in this message. - If this field is not set then no resource info is known. - """ - @property - def scope_profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeProfiles]: - """A list of ScopeProfiles that originate from a resource.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the resource data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "scope_profiles" field which have their own schema_url field. - """ - def __init__( - self, - *, - resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., - scope_profiles: collections.abc.Iterable[global___ScopeProfiles] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_profiles", b"scope_profiles"]) -> None: ... - -global___ResourceProfiles = ResourceProfiles - -@typing_extensions.final -class ScopeProfiles(google.protobuf.message.Message): - """A collection of Profiles produced by an InstrumentationScope.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SCOPE_FIELD_NUMBER: builtins.int - PROFILES_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: - """The instrumentation scope information for the profiles in this message. - Semantically when InstrumentationScope isn't set, it is equivalent with - an empty instrumentation scope name (unknown). - """ - @property - def profiles(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Profile]: - """A list of Profiles that originate from an instrumentation scope.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the profile data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "scope" field and all profiles in the - "profiles" field. - """ - def __init__( - self, - *, - scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., - profiles: collections.abc.Iterable[global___Profile] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["profiles", b"profiles", "schema_url", b"schema_url", "scope", b"scope"]) -> None: ... - -global___ScopeProfiles = ScopeProfiles - -@typing_extensions.final -class Profile(google.protobuf.message.Message): - """Profile is a common stacktrace profile format. - - Measurements represented with this format should follow the - following conventions: - - - Consumers should treat unset optional fields as if they had been - set with their default value. - - - When possible, measurements should be stored in "unsampled" form - that is most useful to humans. There should be enough - information present to determine the original sampled values. - - - The profile is represented as a set of samples, where each sample - references a stack trace which is a list of locations, each belonging - to a mapping. - - There is a N->1 relationship from Stack.location_indices entries to - locations. For every Stack.location_indices entry there must be a - unique Location with that index. - - There is an optional N->1 relationship from locations to - mappings. For every nonzero Location.mapping_id there must be a - unique Mapping with that index. - - Represents a complete profile, including sample types, samples, mappings to - binaries, stacks, locations, functions, string table, and additional - metadata. It modifies and annotates pprof Profile with OpenTelemetry - specific fields. - - Note that whilst fields in this message retain the name and field id from pprof in most cases - for ease of understanding data migration, it is not intended that pprof:Profile and - OpenTelemetry:Profile encoding be wire compatible. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SAMPLE_TYPE_FIELD_NUMBER: builtins.int - SAMPLES_FIELD_NUMBER: builtins.int - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - DURATION_NANO_FIELD_NUMBER: builtins.int - PERIOD_TYPE_FIELD_NUMBER: builtins.int - PERIOD_FIELD_NUMBER: builtins.int - PROFILE_ID_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - ORIGINAL_PAYLOAD_FORMAT_FIELD_NUMBER: builtins.int - ORIGINAL_PAYLOAD_FIELD_NUMBER: builtins.int - ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int - @property - def sample_type(self) -> global___ValueType: - """The type and unit of all Sample.values in this profile. - For a cpu or off-cpu profile this might be: - ["cpu","nanoseconds"] or ["off_cpu","nanoseconds"] - For a heap profile, this might be: - ["allocated_objects","count"] or ["allocated_space","bytes"], - """ - @property - def samples(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Sample]: - """The set of samples recorded in this profile.""" - time_unix_nano: builtins.int - """The following fields 3-12 are informational, do not affect - interpretation of results. - - Time of collection. Value is UNIX Epoch time in nanoseconds since 00:00:00 - UTC on 1 January 1970. - """ - duration_nano: builtins.int - """Duration of the profile. For instant profiles like live heap snapshot, the - duration can be zero but it may be preferable to set time_unix_nano to the - process start time and duration_nano to the relative time when the profile - was gathered. This ensures Sample.timestamps_unix_nano values such as - allocation timestamp fall into the profile time range. - """ - @property - def period_type(self) -> global___ValueType: - """The kind of events between sampled occurrences. - e.g [ "cpu","cycles" ] or [ "heap","bytes" ] - """ - period: builtins.int - """The number of events between sampled occurrences.""" - profile_id: builtins.bytes - """A globally unique identifier for a profile. The ID is a 16-byte array. An ID with - all zeroes is considered invalid. It may be used for deduplication and signal - correlation purposes. It is acceptable to treat two profiles with different values - in this field as not equal, even if they represented the same object at an earlier - time. - This field is optional; an ID may be assigned to an ID-less profile in a later step. - """ - dropped_attributes_count: builtins.int - """The number of attributes that were discarded. Attributes - can be discarded because their keys are too long or because there are too many - attributes. If this value is 0, then no attributes were dropped. - """ - original_payload_format: builtins.str - """The original payload format. See also original_payload. Optional, but the - format and the bytes must be set or unset together. - - The allowed values for the format string are defined by the OpenTelemetry - specification. Some examples are "jfr", "pprof", "linux_perf". - - The original payload may be optionally provided when the conversion to the - OLTP format was done from a different format with some loss of the fidelity - and the receiver may want to store the original payload to allow future - lossless export or reinterpretation. Some examples of the original format - are JFR (Java Flight Recorder), pprof, Linux perf. - - Even when the original payload is in a format that is semantically close to - OTLP, such as pprof, a conversion may still be lossy in some cases (e.g. if - the pprof file contains custom extensions or conventions). - - The original payload can be large in size, so including the original - payload should be configurable by the profiler or collector options. The - default behavior should be to not include the original payload. - """ - original_payload: builtins.bytes - """The original payload bytes. See also original_payload_format. Optional, but - format and the bytes must be set or unset together. - """ - @property - def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """References to attributes in attribute_table. [optional]""" - def __init__( - self, - *, - sample_type: global___ValueType | None = ..., - samples: collections.abc.Iterable[global___Sample] | None = ..., - time_unix_nano: builtins.int = ..., - duration_nano: builtins.int = ..., - period_type: global___ValueType | None = ..., - period: builtins.int = ..., - profile_id: builtins.bytes = ..., - dropped_attributes_count: builtins.int = ..., - original_payload_format: builtins.str = ..., - original_payload: builtins.bytes = ..., - attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["period_type", b"period_type", "sample_type", b"sample_type"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "dropped_attributes_count", b"dropped_attributes_count", "duration_nano", b"duration_nano", "original_payload", b"original_payload", "original_payload_format", b"original_payload_format", "period", b"period", "period_type", b"period_type", "profile_id", b"profile_id", "sample_type", b"sample_type", "samples", b"samples", "time_unix_nano", b"time_unix_nano"]) -> None: ... - -global___Profile = Profile - -@typing_extensions.final -class Link(google.protobuf.message.Message): - """A pointer from a profile Sample to a trace Span. - Connects a profile sample to a trace span, identified by unique trace and span IDs. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TRACE_ID_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - trace_id: builtins.bytes - """A unique identifier of a trace that this linked span is part of. The ID is a - 16-byte array. - """ - span_id: builtins.bytes - """A unique identifier for the linked span. The ID is an 8-byte array.""" - def __init__( - self, - *, - trace_id: builtins.bytes = ..., - span_id: builtins.bytes = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["span_id", b"span_id", "trace_id", b"trace_id"]) -> None: ... - -global___Link = Link - -@typing_extensions.final -class ValueType(google.protobuf.message.Message): - """ValueType describes the type and units of a value.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TYPE_STRINDEX_FIELD_NUMBER: builtins.int - UNIT_STRINDEX_FIELD_NUMBER: builtins.int - type_strindex: builtins.int - """Index into ProfilesDictionary.string_table.""" - unit_strindex: builtins.int - """Index into ProfilesDictionary.string_table.""" - def __init__( - self, - *, - type_strindex: builtins.int = ..., - unit_strindex: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["type_strindex", b"type_strindex", "unit_strindex", b"unit_strindex"]) -> None: ... - -global___ValueType = ValueType - -@typing_extensions.final -class Sample(google.protobuf.message.Message): - """Each Sample records values encountered in some program context. The program - context is typically a stack trace, perhaps augmented with auxiliary - information like the thread-id, some indicator of a higher level request - being handled etc. - - A Sample MUST have have at least one values or timestamps_unix_nano entry. If - both fields are populated, they MUST contain the same number of elements, and - the elements at the same index MUST refer to the same event. - - For the purposes of efficiently representing aggregated data observations, a Sample is regarded - as having a shared identity and an associated collection of per-observation data points. - Samples having the same identity SHOULD be combined by inserting timestamps and values to the data arrays. - - Examples of different ways ('shapes') of representing a sample with the total value of 10: - - Report of a stacktrace at 10 timestamps (consumers must assume the value is 1 for each point): - values: [] - timestamps_unix_nano: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - Report of a stacktrace with an aggregated value without timestamps: - values: [10] - timestamps_unix_nano: [] - - Report of a stacktrace at 4 timestamps where each point records a specific value: - values: [2, 2, 3, 3] - timestamps_unix_nano: [1, 2, 3, 4] - - All Samples for a Profile SHOULD have the same shape, i.e. all data observation series should consistently - adopt the same data recording style. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - STACK_INDEX_FIELD_NUMBER: builtins.int - ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int - LINK_INDEX_FIELD_NUMBER: builtins.int - VALUES_FIELD_NUMBER: builtins.int - TIMESTAMPS_UNIX_NANO_FIELD_NUMBER: builtins.int - stack_index: builtins.int - """A Sample's identity (i.e. 'primary key') is the tuple of {stack_index, set_of(attribute_indices), link_index} - - Reference to stack in ProfilesDictionary.stack_table. - """ - @property - def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """References to attributes in ProfilesDictionary.attribute_table. [optional]""" - link_index: builtins.int - """Reference to link in ProfilesDictionary.link_table. [optional] - It can be unset / set to 0 if no link exists, as link_table[0] is always a 'null' default value. - """ - @property - def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """The following fields may contain per-observation data and do not form part of the Sample's identity. - - The type and unit of each value is defined by Profile.sample_type. - """ - @property - def timestamps_unix_nano(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """Timestamps associated with Sample. Value is UNIX Epoch time in nanoseconds - since 00:00:00 UTC on 1 January 1970. The timestamps should fall within the - [Profile.time_unix_nano, Profile.time_unix_nano + Profile.duration_nano) - time range. - """ - def __init__( - self, - *, - stack_index: builtins.int = ..., - attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., - link_index: builtins.int = ..., - values: collections.abc.Iterable[builtins.int] | None = ..., - timestamps_unix_nano: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "link_index", b"link_index", "stack_index", b"stack_index", "timestamps_unix_nano", b"timestamps_unix_nano", "values", b"values"]) -> None: ... - -global___Sample = Sample - -@typing_extensions.final -class Mapping(google.protobuf.message.Message): - """Describes the mapping of a binary in memory, including its address range, - file offset, and metadata like build ID - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - MEMORY_START_FIELD_NUMBER: builtins.int - MEMORY_LIMIT_FIELD_NUMBER: builtins.int - FILE_OFFSET_FIELD_NUMBER: builtins.int - FILENAME_STRINDEX_FIELD_NUMBER: builtins.int - ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int - memory_start: builtins.int - """Address at which the binary (or DLL) is loaded into memory.""" - memory_limit: builtins.int - """The limit of the address range occupied by this mapping.""" - file_offset: builtins.int - """Offset in the binary that corresponds to the first mapped address.""" - filename_strindex: builtins.int - """The object this entry is loaded from. This can be a filename on - disk for the main binary and shared libraries, or virtual - abstractions like "[vdso]". - Index into ProfilesDictionary.string_table. - """ - @property - def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """References to attributes in ProfilesDictionary.attribute_table. [optional]""" - def __init__( - self, - *, - memory_start: builtins.int = ..., - memory_limit: builtins.int = ..., - file_offset: builtins.int = ..., - filename_strindex: builtins.int = ..., - attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attribute_indices", b"attribute_indices", "file_offset", b"file_offset", "filename_strindex", b"filename_strindex", "memory_limit", b"memory_limit", "memory_start", b"memory_start"]) -> None: ... - -global___Mapping = Mapping - -@typing_extensions.final -class Stack(google.protobuf.message.Message): - """A Stack represents a stack trace as a list of locations.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - LOCATION_INDICES_FIELD_NUMBER: builtins.int - @property - def location_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """References to locations in ProfilesDictionary.location_table. - The first location is the leaf frame. - """ - def __init__( - self, - *, - location_indices: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["location_indices", b"location_indices"]) -> None: ... - -global___Stack = Stack - -@typing_extensions.final -class Location(google.protobuf.message.Message): - """Describes function and line table debug information.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - MAPPING_INDEX_FIELD_NUMBER: builtins.int - ADDRESS_FIELD_NUMBER: builtins.int - LINES_FIELD_NUMBER: builtins.int - ATTRIBUTE_INDICES_FIELD_NUMBER: builtins.int - mapping_index: builtins.int - """Reference to mapping in ProfilesDictionary.mapping_table. - It can be unset / set to 0 if the mapping is unknown or not applicable for - this profile type, as mapping_table[0] is always a 'null' default mapping. - """ - address: builtins.int - """The instruction address for this location, if available. It - should be within [Mapping.memory_start...Mapping.memory_limit] - for the corresponding mapping. A non-leaf address may be in the - middle of a call instruction. It is up to display tools to find - the beginning of the instruction if necessary. - """ - @property - def lines(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Line]: - """Multiple line indicates this location has inlined functions, - where the last entry represents the caller into which the - preceding entries were inlined. - - E.g., if memcpy() is inlined into printf: - lines[0].function_name == "memcpy" - lines[1].function_name == "printf" - """ - @property - def attribute_indices(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: - """References to attributes in ProfilesDictionary.attribute_table. [optional]""" - def __init__( - self, - *, - mapping_index: builtins.int = ..., - address: builtins.int = ..., - lines: collections.abc.Iterable[global___Line] | None = ..., - attribute_indices: collections.abc.Iterable[builtins.int] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["address", b"address", "attribute_indices", b"attribute_indices", "lines", b"lines", "mapping_index", b"mapping_index"]) -> None: ... - -global___Location = Location - -@typing_extensions.final -class Line(google.protobuf.message.Message): - """Details a specific line in a source code, linked to a function.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - FUNCTION_INDEX_FIELD_NUMBER: builtins.int - LINE_FIELD_NUMBER: builtins.int - COLUMN_FIELD_NUMBER: builtins.int - function_index: builtins.int - """Reference to function in ProfilesDictionary.function_table.""" - line: builtins.int - """Line number in source code. 0 means unset.""" - column: builtins.int - """Column number in source code. 0 means unset.""" - def __init__( - self, - *, - function_index: builtins.int = ..., - line: builtins.int = ..., - column: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["column", b"column", "function_index", b"function_index", "line", b"line"]) -> None: ... - -global___Line = Line - -@typing_extensions.final -class Function(google.protobuf.message.Message): - """Describes a function, including its human-readable name, system name, - source file, and starting line number in the source. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - NAME_STRINDEX_FIELD_NUMBER: builtins.int - SYSTEM_NAME_STRINDEX_FIELD_NUMBER: builtins.int - FILENAME_STRINDEX_FIELD_NUMBER: builtins.int - START_LINE_FIELD_NUMBER: builtins.int - name_strindex: builtins.int - """The function name. Empty string if not available.""" - system_name_strindex: builtins.int - """Function name, as identified by the system. For instance, - it can be a C++ mangled name. Empty string if not available. - """ - filename_strindex: builtins.int - """Source file containing the function. Empty string if not available.""" - start_line: builtins.int - """Line number in source file. 0 means unset.""" - def __init__( - self, - *, - name_strindex: builtins.int = ..., - system_name_strindex: builtins.int = ..., - filename_strindex: builtins.int = ..., - start_line: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["filename_strindex", b"filename_strindex", "name_strindex", b"name_strindex", "start_line", b"start_line", "system_name_strindex", b"system_name_strindex"]) -> None: ... - -global___Function = Function - -@typing_extensions.final -class KeyValueAndUnit(google.protobuf.message.Message): - """A custom 'dictionary native' style of encoding attributes which is more convenient - for profiles than opentelemetry.proto.common.v1.KeyValue - Specifically, uses the string table for keys and allows optional unit information. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - KEY_STRINDEX_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - UNIT_STRINDEX_FIELD_NUMBER: builtins.int - key_strindex: builtins.int - """The index into the string table for the attribute's key.""" - @property - def value(self) -> opentelemetry.proto.common.v1.common_pb2.AnyValue: - """The value of the attribute.""" - unit_strindex: builtins.int - """The index into the string table for the attribute's unit. - zero indicates implicit (by semconv) or non-defined unit. - """ - def __init__( - self, - *, - key_strindex: builtins.int = ..., - value: opentelemetry.proto.common.v1.common_pb2.AnyValue | None = ..., - unit_strindex: builtins.int = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["key_strindex", b"key_strindex", "unit_strindex", b"unit_strindex", "value", b"value"]) -> None: ... - -global___KeyValueAndUnit = KeyValueAndUnit diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py index f7066fcf7ac..a63c51ed52e 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.py @@ -1,28 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/resource/v1/resource.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"\xa8\x01\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\r\x12=\n\x0b\x65ntity_refs\x18\x03 \x03(\x0b\x32(.opentelemetry.proto.common.v1.EntityRefB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.resource.v1.resource_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\001Z*go.opentelemetry.io/proto/otlp/resource/v1\252\002\037OpenTelemetry.Proto.Resource.V1' - _globals['_RESOURCE']._serialized_start=128 - _globals['_RESOURCE']._serialized_end=296 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.resource.v1.resource_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi deleted file mode 100644 index 61472c538e1..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/resource/v1/resource_pb2.pyi +++ /dev/null @@ -1,70 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.message -import opentelemetry.proto.common.v1.common_pb2 -import sys - -if sys.version_info >= (3, 8): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -@typing_extensions.final -class Resource(google.protobuf.message.Message): - """Resource information.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - ENTITY_REFS_FIELD_NUMBER: builtins.int - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """Set of attributes that describe the resource. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - """The number of dropped attributes. If the value is 0, then - no attributes were dropped. - """ - @property - def entity_refs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.EntityRef]: - """Set of entities that participate in this Resource. - - Note: keys in the references MUST exist in attributes of this message. - - Status: [Development] - """ - def __init__( - self, - *, - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - entity_refs: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.EntityRef] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "entity_refs", b"entity_refs"]) -> None: ... - -global___Resource = Resource diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py index 61a2d0fadd1..8e2aeea213d 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py +++ b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.py @@ -1,47 +1,5 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: opentelemetry/proto/trace/v1/trace.proto -# Protobuf Python Version: 5.26.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 -_sym_db = _symbol_database.Default() - -from opentelemetry.proto.common.v1 import common_pb2 as opentelemetry_dot_proto_dot_common_dot_v1_dot_common__pb2 -from opentelemetry.proto.resource.v1 import resource_pb2 as opentelemetry_dot_proto_dot_resource_dot_v1_dot_resource__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xa7\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x84\x08\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\r\n\x05\x66lags\x18\x10 \x01(\x07\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\xac\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\x12\r\n\x05\x66lags\x18\x06 \x01(\x07\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02*\x9c\x01\n\tSpanFlags\x12\x19\n\x15SPAN_FLAGS_DO_NOT_USE\x10\x00\x12 \n\x1bSPAN_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x12*\n%SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK\x10\x80\x02\x12&\n!SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK\x10\x80\x04\x42w\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1\xaa\x02\x1cOpenTelemetry.Proto.Trace.V1b\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opentelemetry.proto.trace.v1.trace_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'\n\037io.opentelemetry.proto.trace.v1B\nTraceProtoP\001Z\'go.opentelemetry.io/proto/otlp/trace/v1\252\002\034OpenTelemetry.Proto.Trace.V1' - _globals['_SPANFLAGS']._serialized_start=1782 - _globals['_SPANFLAGS']._serialized_end=1938 - _globals['_TRACESDATA']._serialized_start=166 - _globals['_TRACESDATA']._serialized_end=247 - _globals['_RESOURCESPANS']._serialized_start=250 - _globals['_RESOURCESPANS']._serialized_end=417 - _globals['_SCOPESPANS']._serialized_start=420 - _globals['_SCOPESPANS']._serialized_end=571 - _globals['_SPAN']._serialized_start=574 - _globals['_SPAN']._serialized_end=1602 - _globals['_SPAN_EVENT']._serialized_start=1131 - _globals['_SPAN_EVENT']._serialized_end=1271 - _globals['_SPAN_LINK']._serialized_start=1274 - _globals['_SPAN_LINK']._serialized_end=1446 - _globals['_SPAN_SPANKIND']._serialized_start=1449 - _globals['_SPAN_SPANKIND']._serialized_end=1602 - _globals['_STATUS']._serialized_start=1605 - _globals['_STATUS']._serialized_end=1779 - _globals['_STATUS_STATUSCODE']._serialized_start=1695 - _globals['_STATUS_STATUSCODE']._serialized_end=1773 -# @@protoc_insertion_point(module_scope) +from opentelemetry._proto.trace.v1.trace_pb2 import * # noqa: F401,F403 diff --git a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi b/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi deleted file mode 100644 index e21336f03c6..00000000000 --- a/opentelemetry-proto/src/opentelemetry/proto/trace/v1/trace_pb2.pyi +++ /dev/null @@ -1,586 +0,0 @@ -""" -@generated by mypy-protobuf. Do not edit manually! -isort:skip_file -Copyright 2019, OpenTelemetry Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -import builtins -import collections.abc -import google.protobuf.descriptor -import google.protobuf.internal.containers -import google.protobuf.internal.enum_type_wrapper -import google.protobuf.message -import opentelemetry.proto.common.v1.common_pb2 -import opentelemetry.proto.resource.v1.resource_pb2 -import sys -import typing - -if sys.version_info >= (3, 10): - import typing as typing_extensions -else: - import typing_extensions - -DESCRIPTOR: google.protobuf.descriptor.FileDescriptor - -class _SpanFlags: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - -class _SpanFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SpanFlags.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - SPAN_FLAGS_DO_NOT_USE: _SpanFlags.ValueType # 0 - """The zero value for the enum. Should not be used for comparisons. - Instead use bitwise "and" with the appropriate mask as shown above. - """ - SPAN_FLAGS_TRACE_FLAGS_MASK: _SpanFlags.ValueType # 255 - """Bits 0-7 are used for trace flags.""" - SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK: _SpanFlags.ValueType # 256 - """Bits 8 and 9 are used to indicate that the parent span or link span is remote. - Bit 8 (`HAS_IS_REMOTE`) indicates whether the value is known. - Bit 9 (`IS_REMOTE`) indicates whether the span or link is remote. - """ - SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK: _SpanFlags.ValueType # 512 - -class SpanFlags(_SpanFlags, metaclass=_SpanFlagsEnumTypeWrapper): - """SpanFlags represents constants used to interpret the - Span.flags field, which is protobuf 'fixed32' type and is to - be used as bit-fields. Each non-zero value defined in this enum is - a bit-mask. To extract the bit-field, for example, use an - expression like: - - (span.flags & SPAN_FLAGS_TRACE_FLAGS_MASK) - - See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. - - Note that Span flags were introduced in version 1.1 of the - OpenTelemetry protocol. Older Span producers do not set this - field, consequently consumers should not rely on the absence of a - particular flag bit to indicate the presence of a particular feature. - """ - -SPAN_FLAGS_DO_NOT_USE: SpanFlags.ValueType # 0 -"""The zero value for the enum. Should not be used for comparisons. -Instead use bitwise "and" with the appropriate mask as shown above. -""" -SPAN_FLAGS_TRACE_FLAGS_MASK: SpanFlags.ValueType # 255 -"""Bits 0-7 are used for trace flags.""" -SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK: SpanFlags.ValueType # 256 -"""Bits 8 and 9 are used to indicate that the parent span or link span is remote. -Bit 8 (`HAS_IS_REMOTE`) indicates whether the value is known. -Bit 9 (`IS_REMOTE`) indicates whether the span or link is remote. -""" -SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK: SpanFlags.ValueType # 512 -global___SpanFlags = SpanFlags - -@typing_extensions.final -class TracesData(google.protobuf.message.Message): - """TracesData represents the traces data that can be stored in a persistent storage, - OR can be embedded by other protocols that transfer OTLP traces data but do - not implement the OTLP protocol. - - The main difference between this message and collector protocol is that - in this message there will not be any "control" or "metadata" specific to - OTLP protocol. - - When new fields are added into this message, the OTLP request MUST be updated - as well. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_SPANS_FIELD_NUMBER: builtins.int - @property - def resource_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ResourceSpans]: - """An array of ResourceSpans. - For data coming from a single resource this array will typically contain - one element. Intermediary nodes that receive data from multiple origins - typically batch the data before forwarding further and in that case this - array will contain multiple elements. - """ - def __init__( - self, - *, - resource_spans: collections.abc.Iterable[global___ResourceSpans] | None = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["resource_spans", b"resource_spans"]) -> None: ... - -global___TracesData = TracesData - -@typing_extensions.final -class ResourceSpans(google.protobuf.message.Message): - """A collection of ScopeSpans from a Resource.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - RESOURCE_FIELD_NUMBER: builtins.int - SCOPE_SPANS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def resource(self) -> opentelemetry.proto.resource.v1.resource_pb2.Resource: - """The resource for the spans in this message. - If this field is not set then no resource info is known. - """ - @property - def scope_spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScopeSpans]: - """A list of ScopeSpans that originate from a resource.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the resource data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "resource" field. It does not apply - to the data in the "scope_spans" field which have their own schema_url field. - """ - def __init__( - self, - *, - resource: opentelemetry.proto.resource.v1.resource_pb2.Resource | None = ..., - scope_spans: collections.abc.Iterable[global___ScopeSpans] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["resource", b"resource"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["resource", b"resource", "schema_url", b"schema_url", "scope_spans", b"scope_spans"]) -> None: ... - -global___ResourceSpans = ResourceSpans - -@typing_extensions.final -class ScopeSpans(google.protobuf.message.Message): - """A collection of Spans produced by an InstrumentationScope.""" - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - SCOPE_FIELD_NUMBER: builtins.int - SPANS_FIELD_NUMBER: builtins.int - SCHEMA_URL_FIELD_NUMBER: builtins.int - @property - def scope(self) -> opentelemetry.proto.common.v1.common_pb2.InstrumentationScope: - """The instrumentation scope information for the spans in this message. - Semantically when InstrumentationScope isn't set, it is equivalent with - an empty instrumentation scope name (unknown). - """ - @property - def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: - """A list of Spans that originate from an instrumentation scope.""" - schema_url: builtins.str - """The Schema URL, if known. This is the identifier of the Schema that the span data - is recorded in. Notably, the last part of the URL path is the version number of the - schema: http[s]://server[:port]/path/. To learn more about Schema URL see - https://opentelemetry.io/docs/specs/otel/schemas/#schema-url - This schema_url applies to the data in the "scope" field and all spans and span - events in the "spans" field. - """ - def __init__( - self, - *, - scope: opentelemetry.proto.common.v1.common_pb2.InstrumentationScope | None = ..., - spans: collections.abc.Iterable[global___Span] | None = ..., - schema_url: builtins.str = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["scope", b"scope"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["schema_url", b"schema_url", "scope", b"scope", "spans", b"spans"]) -> None: ... - -global___ScopeSpans = ScopeSpans - -@typing_extensions.final -class Span(google.protobuf.message.Message): - """A Span represents a single operation performed by a single component of the system. - - The next available field id is 17. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - class _SpanKind: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - - class _SpanKindEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Span._SpanKind.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - SPAN_KIND_UNSPECIFIED: Span._SpanKind.ValueType # 0 - """Unspecified. Do NOT use as default. - Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. - """ - SPAN_KIND_INTERNAL: Span._SpanKind.ValueType # 1 - """Indicates that the span represents an internal operation within an application, - as opposed to an operation happening at the boundaries. Default value. - """ - SPAN_KIND_SERVER: Span._SpanKind.ValueType # 2 - """Indicates that the span covers server-side handling of an RPC or other - remote network request. - """ - SPAN_KIND_CLIENT: Span._SpanKind.ValueType # 3 - """Indicates that the span describes a request to some remote service.""" - SPAN_KIND_PRODUCER: Span._SpanKind.ValueType # 4 - """Indicates that the span describes a producer sending a message to a broker. - Unlike CLIENT and SERVER, there is often no direct critical path latency relationship - between producer and consumer spans. A PRODUCER span ends when the message was accepted - by the broker while the logical processing of the message might span a much longer time. - """ - SPAN_KIND_CONSUMER: Span._SpanKind.ValueType # 5 - """Indicates that the span describes consumer receiving a message from a broker. - Like the PRODUCER kind, there is often no direct critical path latency relationship - between producer and consumer spans. - """ - - class SpanKind(_SpanKind, metaclass=_SpanKindEnumTypeWrapper): - """SpanKind is the type of span. Can be used to specify additional relationships between spans - in addition to a parent/child relationship. - """ - - SPAN_KIND_UNSPECIFIED: Span.SpanKind.ValueType # 0 - """Unspecified. Do NOT use as default. - Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. - """ - SPAN_KIND_INTERNAL: Span.SpanKind.ValueType # 1 - """Indicates that the span represents an internal operation within an application, - as opposed to an operation happening at the boundaries. Default value. - """ - SPAN_KIND_SERVER: Span.SpanKind.ValueType # 2 - """Indicates that the span covers server-side handling of an RPC or other - remote network request. - """ - SPAN_KIND_CLIENT: Span.SpanKind.ValueType # 3 - """Indicates that the span describes a request to some remote service.""" - SPAN_KIND_PRODUCER: Span.SpanKind.ValueType # 4 - """Indicates that the span describes a producer sending a message to a broker. - Unlike CLIENT and SERVER, there is often no direct critical path latency relationship - between producer and consumer spans. A PRODUCER span ends when the message was accepted - by the broker while the logical processing of the message might span a much longer time. - """ - SPAN_KIND_CONSUMER: Span.SpanKind.ValueType # 5 - """Indicates that the span describes consumer receiving a message from a broker. - Like the PRODUCER kind, there is often no direct critical path latency relationship - between producer and consumer spans. - """ - - @typing_extensions.final - class Event(google.protobuf.message.Message): - """Event is a time-stamped annotation of the span, consisting of user-supplied - text description and key-value pairs. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - time_unix_nano: builtins.int - """The time the event occurred.""" - name: builtins.str - """The name of the event. - This field is semantically required to be set to non-empty string. - """ - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """A collection of attribute key/value pairs on the event. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - """The number of dropped attributes. If the value is 0, - then no attributes were dropped. - """ - def __init__( - self, - *, - time_unix_nano: builtins.int = ..., - name: builtins.str = ..., - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "name", b"name", "time_unix_nano", b"time_unix_nano"]) -> None: ... - - @typing_extensions.final - class Link(google.protobuf.message.Message): - """A pointer from the current span to another span in the same trace or in a - different trace. For example, this can be used in batching operations, - where a single batch handler processes multiple requests from different - traces or when the handler receives a request from a different project. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - TRACE_ID_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - TRACE_STATE_FIELD_NUMBER: builtins.int - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - trace_id: builtins.bytes - """A unique identifier of a trace that this linked span is part of. The ID is a - 16-byte array. - """ - span_id: builtins.bytes - """A unique identifier for the linked span. The ID is an 8-byte array.""" - trace_state: builtins.str - """The trace_state associated with the link.""" - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """A collection of attribute key/value pairs on the link. - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - """The number of dropped attributes. If the value is 0, - then no attributes were dropped. - """ - flags: builtins.int - """Flags, a bit field. - - Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace - Context specification. To read the 8-bit W3C trace flag, use - `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. - - See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. - - Bits 8 and 9 represent the 3 states of whether the link is remote. - The states are (unknown, is not remote, is remote). - To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. - To read whether the link is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. - - Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. - When creating new spans, bits 10-31 (most-significant 22-bits) MUST be zero. - - [Optional]. - """ - def __init__( - self, - *, - trace_id: builtins.bytes = ..., - span_id: builtins.bytes = ..., - trace_state: builtins.str = ..., - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - flags: builtins.int = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "flags", b"flags", "span_id", b"span_id", "trace_id", b"trace_id", "trace_state", b"trace_state"]) -> None: ... - - TRACE_ID_FIELD_NUMBER: builtins.int - SPAN_ID_FIELD_NUMBER: builtins.int - TRACE_STATE_FIELD_NUMBER: builtins.int - PARENT_SPAN_ID_FIELD_NUMBER: builtins.int - FLAGS_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int - KIND_FIELD_NUMBER: builtins.int - START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - END_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int - ATTRIBUTES_FIELD_NUMBER: builtins.int - DROPPED_ATTRIBUTES_COUNT_FIELD_NUMBER: builtins.int - EVENTS_FIELD_NUMBER: builtins.int - DROPPED_EVENTS_COUNT_FIELD_NUMBER: builtins.int - LINKS_FIELD_NUMBER: builtins.int - DROPPED_LINKS_COUNT_FIELD_NUMBER: builtins.int - STATUS_FIELD_NUMBER: builtins.int - trace_id: builtins.bytes - """A unique identifier for a trace. All spans from the same trace share - the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR - of length other than 16 bytes is considered invalid (empty string in OTLP/JSON - is zero-length and thus is also invalid). - - This field is required. - """ - span_id: builtins.bytes - """A unique identifier for a span within a trace, assigned when the span - is created. The ID is an 8-byte array. An ID with all zeroes OR of length - other than 8 bytes is considered invalid (empty string in OTLP/JSON - is zero-length and thus is also invalid). - - This field is required. - """ - trace_state: builtins.str - """trace_state conveys information about request position in multiple distributed tracing graphs. - It is a trace_state in w3c-trace-context format: https://www.w3.org/TR/trace-context/#tracestate-header - See also https://github.com/w3c/distributed-tracing for more details about this field. - """ - parent_span_id: builtins.bytes - """The `span_id` of this span's parent span. If this is a root span, then this - field must be empty. The ID is an 8-byte array. - """ - flags: builtins.int - """Flags, a bit field. - - Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace - Context specification. To read the 8-bit W3C trace flag, use - `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. - - See https://www.w3.org/TR/trace-context-2/#trace-flags for the flag definitions. - - Bits 8 and 9 represent the 3 states of whether a span's parent - is remote. The states are (unknown, is not remote, is remote). - To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. - To read whether the span is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. - - When creating span messages, if the message is logically forwarded from another source - with an equivalent flags fields (i.e., usually another OTLP span message), the field SHOULD - be copied as-is. If creating from a source that does not have an equivalent flags field - (such as a runtime representation of an OpenTelemetry span), the high 22 bits MUST - be set to zero. - Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. - - [Optional]. - """ - name: builtins.str - """A description of the span's operation. - - For example, the name can be a qualified method name or a file name - and a line number where the operation is called. A best practice is to use - the same display name at the same call point in an application. - This makes it easier to correlate spans in different traces. - - This field is semantically required to be set to non-empty string. - Empty value is equivalent to an unknown span name. - - This field is required. - """ - kind: global___Span.SpanKind.ValueType - """Distinguishes between spans generated in a particular context. For example, - two spans with the same name may be distinguished using `CLIENT` (caller) - and `SERVER` (callee) to identify queueing latency associated with the span. - """ - start_time_unix_nano: builtins.int - """The start time of the span. On the client side, this is the time - kept by the local machine where the span execution starts. On the server side, this - is the time when the server's application handler starts running. - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - - This field is semantically required and it is expected that end_time >= start_time. - """ - end_time_unix_nano: builtins.int - """The end time of the span. On the client side, this is the time - kept by the local machine where the span execution ends. On the server side, this - is the time when the server application handler stops running. - Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. - - This field is semantically required and it is expected that end_time >= start_time. - """ - @property - def attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[opentelemetry.proto.common.v1.common_pb2.KeyValue]: - """A collection of key/value pairs. Note, global attributes - like server name can be set using the resource API. Examples of attributes: - - "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" - "/http/server_latency": 300 - "example.com/myattribute": true - "example.com/score": 10.239 - - Attribute keys MUST be unique (it is not allowed to have more than one - attribute with the same key). - The behavior of software that receives duplicated keys can be unpredictable. - """ - dropped_attributes_count: builtins.int - """The number of attributes that were discarded. Attributes - can be discarded because their keys are too long or because there are too many - attributes. If this value is 0, then no attributes were dropped. - """ - @property - def events(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Event]: - """A collection of Event items.""" - dropped_events_count: builtins.int - """The number of dropped events. If the value is 0, then no - events were dropped. - """ - @property - def links(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span.Link]: - """A collection of Links, which are references from this span to a span - in the same or different trace. - """ - dropped_links_count: builtins.int - """The number of dropped links after the maximum size was - enforced. If this value is 0, then no links were dropped. - """ - @property - def status(self) -> global___Status: - """An optional final status for this span. Semantically when Status isn't set, it means - span's status code is unset, i.e. assume STATUS_CODE_UNSET (code = 0). - """ - def __init__( - self, - *, - trace_id: builtins.bytes = ..., - span_id: builtins.bytes = ..., - trace_state: builtins.str = ..., - parent_span_id: builtins.bytes = ..., - flags: builtins.int = ..., - name: builtins.str = ..., - kind: global___Span.SpanKind.ValueType = ..., - start_time_unix_nano: builtins.int = ..., - end_time_unix_nano: builtins.int = ..., - attributes: collections.abc.Iterable[opentelemetry.proto.common.v1.common_pb2.KeyValue] | None = ..., - dropped_attributes_count: builtins.int = ..., - events: collections.abc.Iterable[global___Span.Event] | None = ..., - dropped_events_count: builtins.int = ..., - links: collections.abc.Iterable[global___Span.Link] | None = ..., - dropped_links_count: builtins.int = ..., - status: global___Status | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["status", b"status"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["attributes", b"attributes", "dropped_attributes_count", b"dropped_attributes_count", "dropped_events_count", b"dropped_events_count", "dropped_links_count", b"dropped_links_count", "end_time_unix_nano", b"end_time_unix_nano", "events", b"events", "flags", b"flags", "kind", b"kind", "links", b"links", "name", b"name", "parent_span_id", b"parent_span_id", "span_id", b"span_id", "start_time_unix_nano", b"start_time_unix_nano", "status", b"status", "trace_id", b"trace_id", "trace_state", b"trace_state"]) -> None: ... - -global___Span = Span - -@typing_extensions.final -class Status(google.protobuf.message.Message): - """The Status type defines a logical error model that is suitable for different - programming environments, including REST APIs and RPC APIs. - """ - - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - class _StatusCode: - ValueType = typing.NewType("ValueType", builtins.int) - V: typing_extensions.TypeAlias = ValueType - - class _StatusCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Status._StatusCode.ValueType], builtins.type): - DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor - STATUS_CODE_UNSET: Status._StatusCode.ValueType # 0 - """The default status.""" - STATUS_CODE_OK: Status._StatusCode.ValueType # 1 - """The Span has been validated by an Application developer or Operator to - have completed successfully. - """ - STATUS_CODE_ERROR: Status._StatusCode.ValueType # 2 - """The Span contains an error.""" - - class StatusCode(_StatusCode, metaclass=_StatusCodeEnumTypeWrapper): - """For the semantics of status codes see - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#set-status - """ - - STATUS_CODE_UNSET: Status.StatusCode.ValueType # 0 - """The default status.""" - STATUS_CODE_OK: Status.StatusCode.ValueType # 1 - """The Span has been validated by an Application developer or Operator to - have completed successfully. - """ - STATUS_CODE_ERROR: Status.StatusCode.ValueType # 2 - """The Span contains an error.""" - - MESSAGE_FIELD_NUMBER: builtins.int - CODE_FIELD_NUMBER: builtins.int - message: builtins.str - """A developer-facing human readable error message.""" - code: global___Status.StatusCode.ValueType - """The status code.""" - def __init__( - self, - *, - message: builtins.str = ..., - code: global___Status.StatusCode.ValueType = ..., - ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["code", b"code", "message", b"message"]) -> None: ... - -global___Status = Status diff --git a/opentelemetry-proto/src/opentelemetry/proto/version/__init__.py b/opentelemetry-proto/src/opentelemetry/proto/version/__init__.py index 524a0260e55..1d05b21972c 100644 --- a/opentelemetry-proto/src/opentelemetry/proto/version/__init__.py +++ b/opentelemetry-proto/src/opentelemetry/proto/version/__init__.py @@ -1,4 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.45.0.dev" + +from opentelemetry._proto.version import __version__ + +__all__ = ["__version__"] diff --git a/opentelemetry-proto/tests/equivalence/__init__.py b/opentelemetry-proto/tests/equivalence/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/__init__.py b/opentelemetry-proto/tests/equivalence/collector/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/logs/__init__.py b/opentelemetry-proto/tests/equivalence/collector/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/logs/v1/__init__.py b/opentelemetry-proto/tests/equivalence/collector/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/logs/v1/test_logs_service_pypb2.py b/opentelemetry-proto/tests/equivalence/collector/logs/v1/test_logs_service_pypb2.py new file mode 100644 index 00000000000..6b44cfc66a8 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/collector/logs/v1/test_logs_service_pypb2.py @@ -0,0 +1,67 @@ +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest as ProtoExportLogsServiceRequest, + ExportLogsServiceResponse as ProtoExportLogsServiceResponse, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ( + ResourceLogs as ProtoResourceLogs, +) + +from opentelemetry._proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, + ExportLogsServiceResponse, +) +from opentelemetry._proto.logs.v1.logs_pb2 import ResourceLogs + + +def test_export_response_empty() -> None: + assert ExportLogsServiceResponse().SerializeToString() == b"" + + +def test_export_response_matches_proto() -> None: + assert ( + ExportLogsServiceResponse().SerializeToString() + == ProtoExportLogsServiceResponse().SerializeToString() + ) + + +def test_export_request_empty() -> None: + assert ExportLogsServiceRequest().SerializeToString() == b"" + + +def test_export_request_empty_matches_proto() -> None: + assert ( + ExportLogsServiceRequest().SerializeToString() + == ProtoExportLogsServiceRequest().SerializeToString() + ) + + +def test_export_request_with_empty_resource_logs() -> None: + our = ExportLogsServiceRequest(resource_logs=[ResourceLogs()]) + proto = ProtoExportLogsServiceRequest(resource_logs=[ProtoResourceLogs()]) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_with_schema_url() -> None: + our = ExportLogsServiceRequest( + resource_logs=[ResourceLogs(schema_url="https://example.com/schema")] + ) + proto = ProtoExportLogsServiceRequest( + resource_logs=[ProtoResourceLogs(schema_url="https://example.com/schema")] + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_multiple_resource_logs() -> None: + our = ExportLogsServiceRequest( + resource_logs=[ + ResourceLogs(schema_url="schema-a"), + ResourceLogs(schema_url="schema-b"), + ] + ) + proto = ProtoExportLogsServiceRequest( + resource_logs=[ + ProtoResourceLogs(schema_url="schema-a"), + ProtoResourceLogs(schema_url="schema-b"), + ] + ) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/collector/metrics/__init__.py b/opentelemetry-proto/tests/equivalence/collector/metrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/metrics/v1/__init__.py b/opentelemetry-proto/tests/equivalence/collector/metrics/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/metrics/v1/test_metrics_service_pypb2.py b/opentelemetry-proto/tests/equivalence/collector/metrics/v1/test_metrics_service_pypb2.py new file mode 100644 index 00000000000..3171161ef12 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/collector/metrics/v1/test_metrics_service_pypb2.py @@ -0,0 +1,67 @@ +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest as ProtoExportMetricsServiceRequest, + ExportMetricsServiceResponse as ProtoExportMetricsServiceResponse, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + ResourceMetrics as ProtoResourceMetrics, +) + +from opentelemetry._proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, + ExportMetricsServiceResponse, +) +from opentelemetry._proto.metrics.v1.metrics_pb2 import ResourceMetrics + + +def test_export_response_empty() -> None: + assert ExportMetricsServiceResponse().SerializeToString() == b"" + + +def test_export_response_matches_proto() -> None: + assert ( + ExportMetricsServiceResponse().SerializeToString() + == ProtoExportMetricsServiceResponse().SerializeToString() + ) + + +def test_export_request_empty() -> None: + assert ExportMetricsServiceRequest().SerializeToString() == b"" + + +def test_export_request_empty_matches_proto() -> None: + assert ( + ExportMetricsServiceRequest().SerializeToString() + == ProtoExportMetricsServiceRequest().SerializeToString() + ) + + +def test_export_request_with_empty_resource_metrics() -> None: + our = ExportMetricsServiceRequest(resource_metrics=[ResourceMetrics()]) + proto = ProtoExportMetricsServiceRequest(resource_metrics=[ProtoResourceMetrics()]) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_with_schema_url() -> None: + our = ExportMetricsServiceRequest( + resource_metrics=[ResourceMetrics(schema_url="https://example.com/schema")] + ) + proto = ProtoExportMetricsServiceRequest( + resource_metrics=[ProtoResourceMetrics(schema_url="https://example.com/schema")] + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_multiple_resource_metrics() -> None: + our = ExportMetricsServiceRequest( + resource_metrics=[ + ResourceMetrics(schema_url="schema-a"), + ResourceMetrics(schema_url="schema-b"), + ] + ) + proto = ProtoExportMetricsServiceRequest( + resource_metrics=[ + ProtoResourceMetrics(schema_url="schema-a"), + ProtoResourceMetrics(schema_url="schema-b"), + ] + ) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/collector/trace/__init__.py b/opentelemetry-proto/tests/equivalence/collector/trace/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/trace/v1/__init__.py b/opentelemetry-proto/tests/equivalence/collector/trace/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/collector/trace/v1/test_trace_service_pypb2.py b/opentelemetry-proto/tests/equivalence/collector/trace/v1/test_trace_service_pypb2.py new file mode 100644 index 00000000000..691c2ae2478 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/collector/trace/v1/test_trace_service_pypb2.py @@ -0,0 +1,67 @@ +from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest as ProtoExportTraceServiceRequest, + ExportTraceServiceResponse as ProtoExportTraceServiceResponse, +) +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as ProtoResourceSpans, +) + +from opentelemetry._proto.collector.trace.v1.trace_service_pb2 import ( + ExportTraceServiceRequest, + ExportTraceServiceResponse, +) +from opentelemetry._proto.trace.v1.trace_pb2 import ResourceSpans + + +def test_export_response_empty() -> None: + assert ExportTraceServiceResponse().SerializeToString() == b"" + + +def test_export_response_matches_proto() -> None: + assert ( + ExportTraceServiceResponse().SerializeToString() + == ProtoExportTraceServiceResponse().SerializeToString() + ) + + +def test_export_request_empty() -> None: + assert ExportTraceServiceRequest().SerializeToString() == b"" + + +def test_export_request_empty_matches_proto() -> None: + assert ( + ExportTraceServiceRequest().SerializeToString() + == ProtoExportTraceServiceRequest().SerializeToString() + ) + + +def test_export_request_with_empty_resource_spans() -> None: + our = ExportTraceServiceRequest(resource_spans=[ResourceSpans()]) + proto = ProtoExportTraceServiceRequest(resource_spans=[ProtoResourceSpans()]) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_with_schema_url() -> None: + our = ExportTraceServiceRequest( + resource_spans=[ResourceSpans(schema_url="https://example.com/schema")] + ) + proto = ProtoExportTraceServiceRequest( + resource_spans=[ProtoResourceSpans(schema_url="https://example.com/schema")] + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_export_request_multiple_resource_spans() -> None: + our = ExportTraceServiceRequest( + resource_spans=[ + ResourceSpans(schema_url="schema-a"), + ResourceSpans(schema_url="schema-b"), + ] + ) + proto = ProtoExportTraceServiceRequest( + resource_spans=[ + ProtoResourceSpans(schema_url="schema-a"), + ProtoResourceSpans(schema_url="schema-b"), + ] + ) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/common/__init__.py b/opentelemetry-proto/tests/equivalence/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/common/v1/__init__.py b/opentelemetry-proto/tests/equivalence/common/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/common/v1/test_common_pypb2.py b/opentelemetry-proto/tests/equivalence/common/v1/test_common_pypb2.py new file mode 100644 index 00000000000..67063117176 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/common/v1/test_common_pypb2.py @@ -0,0 +1,188 @@ +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue as ProtoAnyValue, + ArrayValue as ProtoArrayValue, + InstrumentationScope as ProtoInstrumentationScope, + KeyValue as ProtoKeyValue, + KeyValueList as ProtoKeyValueList, +) + +from opentelemetry._proto.common.v1.common_pb2 import ( + AnyValue, + ArrayValue, + InstrumentationScope, + KeyValue, + KeyValueList, +) + + +# ── AnyValue ────────────────────────────────────────────────────────────────── + +def test_any_value_empty() -> None: + assert AnyValue().SerializeToString() == b"" + + +def test_any_value_empty_matches_proto() -> None: + assert AnyValue().SerializeToString() == ProtoAnyValue().SerializeToString() + + +def test_any_value_string() -> None: + our = AnyValue(string_value="hello") + proto = ProtoAnyValue(string_value="hello") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_bool_true() -> None: + our = AnyValue(bool_value=True) + proto = ProtoAnyValue(bool_value=True) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_bool_false() -> None: + our = AnyValue(bool_value=False) + proto = ProtoAnyValue(bool_value=False) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_int() -> None: + our = AnyValue(int_value=42) + proto = ProtoAnyValue(int_value=42) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_int_negative() -> None: + our = AnyValue(int_value=-1) + proto = ProtoAnyValue(int_value=-1) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_double() -> None: + our = AnyValue(double_value=3.14) + proto = ProtoAnyValue(double_value=3.14) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_bytes() -> None: + our = AnyValue(bytes_value=b"\x01\x02\x03") + proto = ProtoAnyValue(bytes_value=b"\x01\x02\x03") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_array_empty() -> None: + our = AnyValue(array_value=ArrayValue()) + proto = ProtoAnyValue(array_value=ProtoArrayValue()) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_any_value_kvlist_empty() -> None: + our = AnyValue(kvlist_value=KeyValueList()) + proto = ProtoAnyValue(kvlist_value=ProtoKeyValueList()) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ArrayValue ──────────────────────────────────────────────────────────────── + +def test_array_value_empty() -> None: + assert ArrayValue().SerializeToString() == b"" + + +def test_array_value_empty_matches_proto() -> None: + assert ArrayValue().SerializeToString() == ProtoArrayValue().SerializeToString() + + +def test_array_value_with_elements() -> None: + our = ArrayValue(values=[AnyValue(string_value="a"), AnyValue(int_value=1)]) + proto = ProtoArrayValue(values=[ProtoAnyValue(string_value="a"), ProtoAnyValue(int_value=1)]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── KeyValueList ────────────────────────────────────────────────────────────── + +def test_kvlist_empty() -> None: + assert KeyValueList().SerializeToString() == b"" + + +def test_kvlist_empty_matches_proto() -> None: + assert KeyValueList().SerializeToString() == ProtoKeyValueList().SerializeToString() + + +def test_kvlist_with_values() -> None: + our = KeyValueList(values=[KeyValue(key="k", value=AnyValue(string_value="v"))]) + proto = ProtoKeyValueList(values=[ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v"))]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── KeyValue ────────────────────────────────────────────────────────────────── + +def test_key_value_empty() -> None: + assert KeyValue().SerializeToString() == b"" + + +def test_key_value_empty_matches_proto() -> None: + assert KeyValue().SerializeToString() == ProtoKeyValue().SerializeToString() + + +def test_key_value_key_only() -> None: + our = KeyValue(key="mykey") + proto = ProtoKeyValue(key="mykey") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_key_value_string() -> None: + our = KeyValue(key="k", value=AnyValue(string_value="v")) + proto = ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v")) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_key_value_int() -> None: + our = KeyValue(key="count", value=AnyValue(int_value=42)) + proto = ProtoKeyValue(key="count", value=ProtoAnyValue(int_value=42)) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_key_value_double() -> None: + our = KeyValue(key="ratio", value=AnyValue(double_value=0.5)) + proto = ProtoKeyValue(key="ratio", value=ProtoAnyValue(double_value=0.5)) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── InstrumentationScope ────────────────────────────────────────────────────── + +def test_instrumentation_scope_empty() -> None: + assert InstrumentationScope().SerializeToString() == b"" + + +def test_instrumentation_scope_empty_matches_proto() -> None: + assert ( + InstrumentationScope().SerializeToString() + == ProtoInstrumentationScope().SerializeToString() + ) + + +def test_instrumentation_scope_name() -> None: + our = InstrumentationScope(name="mylib") + proto = ProtoInstrumentationScope(name="mylib") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_instrumentation_scope_name_version() -> None: + our = InstrumentationScope(name="mylib", version="1.2.3") + proto = ProtoInstrumentationScope(name="mylib", version="1.2.3") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_instrumentation_scope_with_attributes() -> None: + attr = KeyValue(key="env", value=AnyValue(string_value="prod")) + proto_attr = ProtoKeyValue(key="env", value=ProtoAnyValue(string_value="prod")) + our = InstrumentationScope( + name="mylib", + version="1.0", + attributes=[attr], + dropped_attributes_count=3, + ) + proto = ProtoInstrumentationScope( + name="mylib", + version="1.0", + attributes=[proto_attr], + dropped_attributes_count=3, + ) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/conftest.py b/opentelemetry-proto/tests/equivalence/conftest.py new file mode 100644 index 00000000000..c556071941f --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/conftest.py @@ -0,0 +1,16 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# The equivalence tests import the real (google.protobuf) message classes via +# ``opentelemetry.proto.*`` and the pure-Python classes via +# ``opentelemetry._proto.*``. Both this distribution (as a re-export shim) and +# the real ``opentelemetry-proto`` provide ``opentelemetry.proto``; guard that +# the public path resolves to the real package, not this package's shim, so the +# comparisons are genuinely real-vs-pure-Python. +from opentelemetry.proto.trace.v1 import trace_pb2 as _public_trace_pb2 + +assert "pyproto" not in (_public_trace_pb2.__file__ or ""), ( + "opentelemetry.proto resolved to the pyproto shim " + f"({_public_trace_pb2.__file__}); the equivalence tests need the real " + "opentelemetry-proto package to own that path." +) diff --git a/opentelemetry-proto/tests/equivalence/logs/__init__.py b/opentelemetry-proto/tests/equivalence/logs/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/logs/v1/__init__.py b/opentelemetry-proto/tests/equivalence/logs/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/logs/v1/test_logs_pypb2.py b/opentelemetry-proto/tests/equivalence/logs/v1/test_logs_pypb2.py new file mode 100644 index 00000000000..0bf2871ad44 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/logs/v1/test_logs_pypb2.py @@ -0,0 +1,155 @@ +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue as ProtoAnyValue, + InstrumentationScope as ProtoInstrumentationScope, + KeyValue as ProtoKeyValue, +) +from opentelemetry.proto.logs.v1.logs_pb2 import ( + LogRecord as ProtoLogRecord, + ResourceLogs as ProtoResourceLogs, + ScopeLogs as ProtoScopeLogs, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource as ProtoResource + +from opentelemetry._proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.logs.v1.logs_pb2 import LogRecord, ResourceLogs, ScopeLogs +from opentelemetry._proto.resource.v1.resource_pb2 import Resource + + +# ── LogRecord ───────────────────────────────────────────────────────────────── + +def test_log_record_empty() -> None: + assert LogRecord().SerializeToString() == b"" + + +def test_log_record_empty_matches_proto() -> None: + assert LogRecord().SerializeToString() == ProtoLogRecord().SerializeToString() + + +def test_log_record_severity() -> None: + our = LogRecord(severity_number=9, severity_text="INFO") + proto = ProtoLogRecord(severity_number=9, severity_text="INFO") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_log_record_with_body() -> None: + our = LogRecord(body=AnyValue(string_value="hello world")) + proto = ProtoLogRecord(body=ProtoAnyValue(string_value="hello world")) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_log_record_with_trace_context() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + our = LogRecord( + time_unix_nano=1_000_000_000, + trace_id=trace_id, + span_id=span_id, + observed_time_unix_nano=2_000_000_000, + ) + proto = ProtoLogRecord( + time_unix_nano=1_000_000_000, + trace_id=trace_id, + span_id=span_id, + observed_time_unix_nano=2_000_000_000, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_log_record_with_attributes() -> None: + attr = KeyValue(key="env", value=AnyValue(string_value="prod")) + proto_attr = ProtoKeyValue(key="env", value=ProtoAnyValue(string_value="prod")) + our = LogRecord( + body=AnyValue(string_value="msg"), + attributes=[attr], + dropped_attributes_count=1, + ) + proto = ProtoLogRecord( + body=ProtoAnyValue(string_value="msg"), + attributes=[proto_attr], + dropped_attributes_count=1, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_log_record_event_name() -> None: + our = LogRecord(event_name="user.click") + proto = ProtoLogRecord(event_name="user.click") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_log_record_flags() -> None: + our = LogRecord(flags=1) + proto = ProtoLogRecord(flags=1) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ScopeLogs ───────────────────────────────────────────────────────────────── + +def test_scope_logs_empty() -> None: + assert ScopeLogs().SerializeToString() == b"" + + +def test_scope_logs_empty_matches_proto() -> None: + assert ScopeLogs().SerializeToString() == ProtoScopeLogs().SerializeToString() + + +def test_scope_logs_schema_url() -> None: + our = ScopeLogs(schema_url="https://example.com/schema") + proto = ProtoScopeLogs(schema_url="https://example.com/schema") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_scope_logs_with_scope() -> None: + scope = InstrumentationScope(name="mylib", version="1.0") + proto_scope = ProtoInstrumentationScope(name="mylib", version="1.0") + our = ScopeLogs(scope=scope) + proto = ProtoScopeLogs(scope=proto_scope) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_scope_logs_with_scope_and_records() -> None: + scope = InstrumentationScope(name="mylib", version="1.0") + proto_scope = ProtoInstrumentationScope(name="mylib", version="1.0") + rec = LogRecord(severity_text="WARN", severity_number=13) + proto_rec = ProtoLogRecord(severity_text="WARN", severity_number=13) + our = ScopeLogs(scope=scope, log_records=[rec], schema_url="s") + proto = ProtoScopeLogs(scope=proto_scope, log_records=[proto_rec], schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ResourceLogs ────────────────────────────────────────────────────────────── + +def test_resource_logs_empty() -> None: + assert ResourceLogs().SerializeToString() == b"" + + +def test_resource_logs_empty_matches_proto() -> None: + assert ResourceLogs().SerializeToString() == ProtoResourceLogs().SerializeToString() + + +def test_resource_logs_schema_url() -> None: + our = ResourceLogs(schema_url="https://example.com") + proto = ProtoResourceLogs(schema_url="https://example.com") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_logs_with_resource() -> None: + res = Resource(attributes=[KeyValue(key="k", value=AnyValue(string_value="v"))]) + proto_res = ProtoResource(attributes=[ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v"))]) + our = ResourceLogs(resource=res, schema_url="s") + proto = ProtoResourceLogs(resource=proto_res, schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_logs_with_scope_logs() -> None: + rec = LogRecord(severity_text="INFO", severity_number=9) + proto_rec = ProtoLogRecord(severity_text="INFO", severity_number=9) + sl = ScopeLogs(log_records=[rec]) + proto_sl = ProtoScopeLogs(log_records=[proto_rec]) + our = ResourceLogs(scope_logs=[sl]) + proto = ProtoResourceLogs(scope_logs=[proto_sl]) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/metrics/__init__.py b/opentelemetry-proto/tests/equivalence/metrics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/metrics/v1/__init__.py b/opentelemetry-proto/tests/equivalence/metrics/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/metrics/v1/test_metrics_pypb2.py b/opentelemetry-proto/tests/equivalence/metrics/v1/test_metrics_pypb2.py new file mode 100644 index 00000000000..ef3a850c43e --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/metrics/v1/test_metrics_pypb2.py @@ -0,0 +1,527 @@ +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue as ProtoAnyValue, + InstrumentationScope as ProtoInstrumentationScope, + KeyValue as ProtoKeyValue, +) +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + Exemplar as ProtoExemplar, + ExponentialHistogram as ProtoExponentialHistogram, + ExponentialHistogramDataPoint as ProtoExponentialHistogramDataPoint, + Gauge as ProtoGauge, + Histogram as ProtoHistogram, + HistogramDataPoint as ProtoHistogramDataPoint, + Metric as ProtoMetric, + NumberDataPoint as ProtoNumberDataPoint, + ResourceMetrics as ProtoResourceMetrics, + ScopeMetrics as ProtoScopeMetrics, + Sum as ProtoSum, + Summary as ProtoSummary, + SummaryDataPoint as ProtoSummaryDataPoint, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource as ProtoResource + +from opentelemetry._proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.metrics.v1.metrics_pb2 import ( + Exemplar, + ExponentialHistogram, + ExponentialHistogramDataPoint, + Gauge, + Histogram, + HistogramDataPoint, + Metric, + NumberDataPoint, + ResourceMetrics, + ScopeMetrics, + Sum, + Summary, + SummaryDataPoint, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource + + +# ── Exemplar ────────────────────────────────────────────────────────────────── + +def test_exemplar_empty() -> None: + assert Exemplar().SerializeToString() == b"" + + +def test_exemplar_empty_matches_proto() -> None: + assert Exemplar().SerializeToString() == ProtoExemplar().SerializeToString() + + +def test_exemplar_as_double() -> None: + our = Exemplar(as_double=1.5, time_unix_nano=1_000) + proto = ProtoExemplar(as_double=1.5, time_unix_nano=1_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_exemplar_as_int() -> None: + our = Exemplar(as_int=42, time_unix_nano=2_000) + proto = ProtoExemplar(as_int=42, time_unix_nano=2_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_exemplar_with_span_trace_id() -> None: + our = Exemplar(as_double=3.0, span_id=b"\x01" * 8, trace_id=b"\x02" * 16) + proto = ProtoExemplar(as_double=3.0, span_id=b"\x01" * 8, trace_id=b"\x02" * 16) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_exemplar_with_filtered_attributes() -> None: + # filtered_attributes is field 7; test it alone to avoid field-ordering + # differences between our fixed-order encoding and proto's ascending order. + attr = KeyValue(key="k", value=AnyValue(string_value="v")) + proto_attr = ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v")) + our = Exemplar(filtered_attributes=[attr]) + proto = ProtoExemplar(filtered_attributes=[proto_attr]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── NumberDataPoint ─────────────────────────────────────────────────────────── + +def test_number_data_point_empty() -> None: + assert NumberDataPoint().SerializeToString() == b"" + + +def test_number_data_point_empty_matches_proto() -> None: + assert NumberDataPoint().SerializeToString() == ProtoNumberDataPoint().SerializeToString() + + +def test_number_data_point_as_double() -> None: + our = NumberDataPoint(as_double=1.5, time_unix_nano=1_000_000) + proto = ProtoNumberDataPoint(as_double=1.5, time_unix_nano=1_000_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_number_data_point_as_int() -> None: + our = NumberDataPoint(as_int=42, time_unix_nano=1_000_000) + proto = ProtoNumberDataPoint(as_int=42, time_unix_nano=1_000_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_number_data_point_with_attributes() -> None: + # attributes is field 7, flags is field 8 — both high-numbered and in the + # right relative order in our encoding, so no ordering difference with proto. + attr = KeyValue(key="env", value=AnyValue(string_value="prod")) + proto_attr = ProtoKeyValue(key="env", value=ProtoAnyValue(string_value="prod")) + our = NumberDataPoint(attributes=[attr], flags=1) + proto = ProtoNumberDataPoint(attributes=[proto_attr], flags=1) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_number_data_point_with_exemplar() -> None: + our = NumberDataPoint( + as_double=5.0, + start_time_unix_nano=1_000, + time_unix_nano=2_000, + exemplars=[Exemplar(as_double=5.0)], + ) + proto = ProtoNumberDataPoint( + as_double=5.0, + start_time_unix_nano=1_000, + time_unix_nano=2_000, + exemplars=[ProtoExemplar(as_double=5.0)], + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Gauge ───────────────────────────────────────────────────────────────────── + +def test_gauge_empty() -> None: + assert Gauge().SerializeToString() == b"" + + +def test_gauge_empty_matches_proto() -> None: + assert Gauge().SerializeToString() == ProtoGauge().SerializeToString() + + +def test_gauge_with_data_point() -> None: + our = Gauge(data_points=[NumberDataPoint(as_double=1.5)]) + proto = ProtoGauge(data_points=[ProtoNumberDataPoint(as_double=1.5)]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Sum ─────────────────────────────────────────────────────────────────────── + +def test_sum_empty() -> None: + assert Sum().SerializeToString() == b"" + + +def test_sum_empty_matches_proto() -> None: + assert Sum().SerializeToString() == ProtoSum().SerializeToString() + + +def test_sum_with_data_and_temporality() -> None: + our = Sum( + data_points=[NumberDataPoint(as_int=10)], + aggregation_temporality=1, + is_monotonic=True, + ) + proto = ProtoSum( + data_points=[ProtoNumberDataPoint(as_int=10)], + aggregation_temporality=1, + is_monotonic=True, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── HistogramDataPoint ──────────────────────────────────────────────────────── + +def test_histogram_data_point_empty() -> None: + assert HistogramDataPoint().SerializeToString() == b"" + + +def test_histogram_data_point_empty_matches_proto() -> None: + assert ( + HistogramDataPoint().SerializeToString() + == ProtoHistogramDataPoint().SerializeToString() + ) + + +def test_histogram_data_point_with_count_and_buckets() -> None: + our = HistogramDataPoint( + time_unix_nano=1_000, + count=5, + sum=100.0, + bucket_counts=[2, 3], + explicit_bounds=[50.0], + ) + proto = ProtoHistogramDataPoint( + time_unix_nano=1_000, + count=5, + sum=100.0, + bucket_counts=[2, 3], + explicit_bounds=[50.0], + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_histogram_data_point_with_min_max() -> None: + our = HistogramDataPoint(time_unix_nano=1_000, count=3, min=0.0, max=10.0) + proto = ProtoHistogramDataPoint(time_unix_nano=1_000, count=3, min=0.0, max=10.0) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Histogram ───────────────────────────────────────────────────────────────── + +def test_histogram_empty() -> None: + assert Histogram().SerializeToString() == b"" + + +def test_histogram_empty_matches_proto() -> None: + assert Histogram().SerializeToString() == ProtoHistogram().SerializeToString() + + +def test_histogram_with_data_and_temporality() -> None: + our = Histogram( + data_points=[HistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=2, + ) + proto = ProtoHistogram( + data_points=[ProtoHistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=2, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ExponentialHistogramDataPoint.Buckets ───────────────────────────────────── + +def test_exp_histogram_buckets_empty() -> None: + assert ExponentialHistogramDataPoint.Buckets().SerializeToString() == b"" + + +def test_exp_histogram_buckets_empty_matches_proto() -> None: + assert ( + ExponentialHistogramDataPoint.Buckets().SerializeToString() + == ProtoExponentialHistogramDataPoint.Buckets().SerializeToString() + ) + + +def test_exp_histogram_buckets_with_data() -> None: + our = ExponentialHistogramDataPoint.Buckets(offset=2, bucket_counts=[1, 2, 3]) + proto = ProtoExponentialHistogramDataPoint.Buckets(offset=2, bucket_counts=[1, 2, 3]) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_exp_histogram_buckets_negative_offset() -> None: + our = ExponentialHistogramDataPoint.Buckets(offset=-3, bucket_counts=[4, 5]) + proto = ProtoExponentialHistogramDataPoint.Buckets(offset=-3, bucket_counts=[4, 5]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ExponentialHistogramDataPoint ───────────────────────────────────────────── + +def test_exp_histogram_data_point_empty() -> None: + assert ExponentialHistogramDataPoint().SerializeToString() == b"" + + +def test_exp_histogram_data_point_empty_matches_proto() -> None: + assert ( + ExponentialHistogramDataPoint().SerializeToString() + == ProtoExponentialHistogramDataPoint().SerializeToString() + ) + + +def test_exp_histogram_data_point_with_scale_and_count() -> None: + our = ExponentialHistogramDataPoint( + time_unix_nano=1_000, + count=10, + scale=-1, + zero_count=2, + ) + proto = ProtoExponentialHistogramDataPoint( + time_unix_nano=1_000, + count=10, + scale=-1, + zero_count=2, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_exp_histogram_data_point_with_buckets() -> None: + pos = ExponentialHistogramDataPoint.Buckets(offset=1, bucket_counts=[3, 4]) + neg = ExponentialHistogramDataPoint.Buckets(offset=-1, bucket_counts=[1]) + proto_pos = ProtoExponentialHistogramDataPoint.Buckets(offset=1, bucket_counts=[3, 4]) + proto_neg = ProtoExponentialHistogramDataPoint.Buckets(offset=-1, bucket_counts=[1]) + our = ExponentialHistogramDataPoint( + count=8, scale=2, positive=pos, negative=neg, time_unix_nano=1_000 + ) + proto = ProtoExponentialHistogramDataPoint( + count=8, scale=2, positive=proto_pos, negative=proto_neg, time_unix_nano=1_000 + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ExponentialHistogram ────────────────────────────────────────────────────── + +def test_exp_histogram_empty() -> None: + assert ExponentialHistogram().SerializeToString() == b"" + + +def test_exp_histogram_empty_matches_proto() -> None: + assert ( + ExponentialHistogram().SerializeToString() + == ProtoExponentialHistogram().SerializeToString() + ) + + +def test_exp_histogram_with_data() -> None: + our = ExponentialHistogram( + data_points=[ExponentialHistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=1, + ) + proto = ProtoExponentialHistogram( + data_points=[ProtoExponentialHistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=1, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── SummaryDataPoint.ValueAtQuantile ────────────────────────────────────────── + +def test_value_at_quantile_empty() -> None: + assert SummaryDataPoint.ValueAtQuantile().SerializeToString() == b"" + + +def test_value_at_quantile_empty_matches_proto() -> None: + assert ( + SummaryDataPoint.ValueAtQuantile().SerializeToString() + == ProtoSummaryDataPoint.ValueAtQuantile().SerializeToString() + ) + + +def test_value_at_quantile() -> None: + our = SummaryDataPoint.ValueAtQuantile(quantile=0.5, value=100.0) + proto = ProtoSummaryDataPoint.ValueAtQuantile(quantile=0.5, value=100.0) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_value_at_quantile_p99() -> None: + our = SummaryDataPoint.ValueAtQuantile(quantile=0.99, value=500.0) + proto = ProtoSummaryDataPoint.ValueAtQuantile(quantile=0.99, value=500.0) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── SummaryDataPoint ────────────────────────────────────────────────────────── + +def test_summary_data_point_empty() -> None: + assert SummaryDataPoint().SerializeToString() == b"" + + +def test_summary_data_point_empty_matches_proto() -> None: + assert ( + SummaryDataPoint().SerializeToString() + == ProtoSummaryDataPoint().SerializeToString() + ) + + +def test_summary_data_point_with_count_and_sum() -> None: + our = SummaryDataPoint(count=10, sum=500.0, time_unix_nano=1_000) + proto = ProtoSummaryDataPoint(count=10, sum=500.0, time_unix_nano=1_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_summary_data_point_with_quantiles() -> None: + our = SummaryDataPoint( + count=100, + sum=5000.0, + quantile_values=[ + SummaryDataPoint.ValueAtQuantile(quantile=0.5, value=45.0), + SummaryDataPoint.ValueAtQuantile(quantile=0.99, value=200.0), + ], + ) + proto = ProtoSummaryDataPoint( + count=100, + sum=5000.0, + quantile_values=[ + ProtoSummaryDataPoint.ValueAtQuantile(quantile=0.5, value=45.0), + ProtoSummaryDataPoint.ValueAtQuantile(quantile=0.99, value=200.0), + ], + ) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Summary ─────────────────────────────────────────────────────────────────── + +def test_summary_empty() -> None: + assert Summary().SerializeToString() == b"" + + +def test_summary_empty_matches_proto() -> None: + assert Summary().SerializeToString() == ProtoSummary().SerializeToString() + + +def test_summary_with_data_point() -> None: + our = Summary(data_points=[SummaryDataPoint(count=10, sum=500.0)]) + proto = ProtoSummary(data_points=[ProtoSummaryDataPoint(count=10, sum=500.0)]) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Metric ──────────────────────────────────────────────────────────────────── + +def test_metric_empty() -> None: + assert Metric().SerializeToString() == b"" + + +def test_metric_empty_matches_proto() -> None: + assert Metric().SerializeToString() == ProtoMetric().SerializeToString() + + +def test_metric_gauge() -> None: + our = Metric(name="cpu", description="CPU usage", unit="%", gauge=Gauge( + data_points=[NumberDataPoint(as_double=0.75)] + )) + proto = ProtoMetric(name="cpu", description="CPU usage", unit="%", gauge=ProtoGauge( + data_points=[ProtoNumberDataPoint(as_double=0.75)] + )) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_metric_sum() -> None: + our = Metric(name="requests", unit="1", sum=Sum( + data_points=[NumberDataPoint(as_int=100)], + aggregation_temporality=2, + is_monotonic=True, + )) + proto = ProtoMetric(name="requests", unit="1", sum=ProtoSum( + data_points=[ProtoNumberDataPoint(as_int=100)], + aggregation_temporality=2, + is_monotonic=True, + )) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_metric_histogram() -> None: + our = Metric(name="latency", histogram=Histogram( + data_points=[HistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=1, + )) + proto = ProtoMetric(name="latency", histogram=ProtoHistogram( + data_points=[ProtoHistogramDataPoint(count=5, time_unix_nano=1_000)], + aggregation_temporality=1, + )) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_metric_summary() -> None: + our = Metric(name="duration", summary=Summary( + data_points=[SummaryDataPoint(count=10, sum=500.0)], + )) + proto = ProtoMetric(name="duration", summary=ProtoSummary( + data_points=[ProtoSummaryDataPoint(count=10, sum=500.0)], + )) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_metric_exponential_histogram() -> None: + our = Metric(name="exp_lat", exponential_histogram=ExponentialHistogram( + data_points=[ExponentialHistogramDataPoint(count=3, time_unix_nano=1_000)], + aggregation_temporality=2, + )) + proto = ProtoMetric(name="exp_lat", exponential_histogram=ProtoExponentialHistogram( + data_points=[ProtoExponentialHistogramDataPoint(count=3, time_unix_nano=1_000)], + aggregation_temporality=2, + )) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ScopeMetrics ────────────────────────────────────────────────────────────── + +def test_scope_metrics_empty() -> None: + assert ScopeMetrics().SerializeToString() == b"" + + +def test_scope_metrics_empty_matches_proto() -> None: + assert ScopeMetrics().SerializeToString() == ProtoScopeMetrics().SerializeToString() + + +def test_scope_metrics_schema_url() -> None: + our = ScopeMetrics(schema_url="https://example.com") + proto = ProtoScopeMetrics(schema_url="https://example.com") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_scope_metrics_with_scope_and_metric() -> None: + scope = InstrumentationScope(name="mylib", version="1.0") + proto_scope = ProtoInstrumentationScope(name="mylib", version="1.0") + metric = Metric(name="cpu", gauge=Gauge(data_points=[NumberDataPoint(as_double=0.5)])) + proto_metric = ProtoMetric(name="cpu", gauge=ProtoGauge(data_points=[ProtoNumberDataPoint(as_double=0.5)])) + our = ScopeMetrics(scope=scope, metrics=[metric], schema_url="s") + proto = ProtoScopeMetrics(scope=proto_scope, metrics=[proto_metric], schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ResourceMetrics ─────────────────────────────────────────────────────────── + +def test_resource_metrics_empty() -> None: + assert ResourceMetrics().SerializeToString() == b"" + + +def test_resource_metrics_empty_matches_proto() -> None: + assert ResourceMetrics().SerializeToString() == ProtoResourceMetrics().SerializeToString() + + +def test_resource_metrics_schema_url() -> None: + our = ResourceMetrics(schema_url="https://example.com") + proto = ProtoResourceMetrics(schema_url="https://example.com") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_metrics_with_resource() -> None: + res = Resource(attributes=[KeyValue(key="k", value=AnyValue(string_value="v"))]) + proto_res = ProtoResource(attributes=[ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v"))]) + our = ResourceMetrics(resource=res, schema_url="s") + proto = ProtoResourceMetrics(resource=proto_res, schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_metrics_with_scope_metrics() -> None: + sm = ScopeMetrics(metrics=[Metric(name="cpu", gauge=Gauge())]) + proto_sm = ProtoScopeMetrics(metrics=[ProtoMetric(name="cpu", gauge=ProtoGauge())]) + our = ResourceMetrics(scope_metrics=[sm]) + proto = ProtoResourceMetrics(scope_metrics=[proto_sm]) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/resource/__init__.py b/opentelemetry-proto/tests/equivalence/resource/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/resource/v1/__init__.py b/opentelemetry-proto/tests/equivalence/resource/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/resource/v1/test_resource_pypb2.py b/opentelemetry-proto/tests/equivalence/resource/v1/test_resource_pypb2.py new file mode 100644 index 00000000000..5ee8ffc5b4f --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/resource/v1/test_resource_pypb2.py @@ -0,0 +1,64 @@ +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue as ProtoAnyValue, + KeyValue as ProtoKeyValue, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource as ProtoResource + +from opentelemetry._proto.common.v1.common_pb2 import AnyValue, KeyValue +from opentelemetry._proto.resource.v1.resource_pb2 import Resource + + +def test_resource_empty() -> None: + assert Resource().SerializeToString() == b"" + + +def test_resource_empty_matches_proto() -> None: + assert Resource().SerializeToString() == ProtoResource().SerializeToString() + + +def test_resource_dropped_attributes_count() -> None: + our = Resource(dropped_attributes_count=5) + proto = ProtoResource(dropped_attributes_count=5) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_with_one_attribute() -> None: + attr = KeyValue(key="service.name", value=AnyValue(string_value="my-service")) + proto_attr = ProtoKeyValue(key="service.name", value=ProtoAnyValue(string_value="my-service")) + our = Resource(attributes=[attr]) + proto = ProtoResource(attributes=[proto_attr]) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_with_multiple_attributes() -> None: + attrs = [ + KeyValue(key="service.name", value=AnyValue(string_value="my-service")), + KeyValue(key="service.version", value=AnyValue(string_value="1.0.0")), + KeyValue(key="deployment.environment", value=AnyValue(string_value="prod")), + ] + proto_attrs = [ + ProtoKeyValue(key="service.name", value=ProtoAnyValue(string_value="my-service")), + ProtoKeyValue(key="service.version", value=ProtoAnyValue(string_value="1.0.0")), + ProtoKeyValue(key="deployment.environment", value=ProtoAnyValue(string_value="prod")), + ] + our = Resource(attributes=attrs) + proto = ProtoResource(attributes=proto_attrs) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_full() -> None: + attrs = [ + KeyValue(key="k", value=AnyValue(string_value="v")), + ] + proto_attrs = [ + ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v")), + ] + our = Resource(attributes=attrs, dropped_attributes_count=2) + proto = ProtoResource(attributes=proto_attrs, dropped_attributes_count=2) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_with_int_attribute() -> None: + our = Resource(attributes=[KeyValue(key="pid", value=AnyValue(int_value=1234))]) + proto = ProtoResource(attributes=[ProtoKeyValue(key="pid", value=ProtoAnyValue(int_value=1234))]) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/equivalence/trace/__init__.py b/opentelemetry-proto/tests/equivalence/trace/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/trace/v1/__init__.py b/opentelemetry-proto/tests/equivalence/trace/v1/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/equivalence/trace/v1/test_trace_pypb2.py b/opentelemetry-proto/tests/equivalence/trace/v1/test_trace_pypb2.py new file mode 100644 index 00000000000..3c4f7cd2e22 --- /dev/null +++ b/opentelemetry-proto/tests/equivalence/trace/v1/test_trace_pypb2.py @@ -0,0 +1,245 @@ +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue as ProtoAnyValue, + InstrumentationScope as ProtoInstrumentationScope, + KeyValue as ProtoKeyValue, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource as ProtoResource +from opentelemetry.proto.trace.v1.trace_pb2 import ( + ResourceSpans as ProtoResourceSpans, + ScopeSpans as ProtoScopeSpans, + Span as ProtoSpan, + Status as ProtoStatus, +) + +from opentelemetry._proto.common.v1.common_pb2 import ( + AnyValue, + InstrumentationScope, + KeyValue, +) +from opentelemetry._proto.resource.v1.resource_pb2 import Resource +from opentelemetry._proto.trace.v1.trace_pb2 import ( + ResourceSpans, + ScopeSpans, + Span, + Status, +) + + +# ── Status ──────────────────────────────────────────────────────────────────── + +def test_status_empty() -> None: + assert Status().SerializeToString() == b"" + + +def test_status_empty_matches_proto() -> None: + assert Status().SerializeToString() == ProtoStatus().SerializeToString() + + +def test_status_code() -> None: + our = Status(code=2) + proto = ProtoStatus(code=2) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_status_code_and_message() -> None: + our = Status(code=2, message="internal error") + proto = ProtoStatus(code=2, message="internal error") + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Span.Event ──────────────────────────────────────────────────────────────── + +def test_span_event_empty() -> None: + assert Span.Event().SerializeToString() == b"" + + +def test_span_event_empty_matches_proto() -> None: + assert Span.Event().SerializeToString() == ProtoSpan.Event().SerializeToString() + + +def test_span_event_name_and_time() -> None: + our = Span.Event(name="button.click", time_unix_nano=1_000_000) + proto = ProtoSpan.Event(name="button.click", time_unix_nano=1_000_000) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_event_with_attributes() -> None: + attr = KeyValue(key="k", value=AnyValue(string_value="v")) + proto_attr = ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v")) + our = Span.Event(name="evt", attributes=[attr], dropped_attributes_count=1) + proto = ProtoSpan.Event(name="evt", attributes=[proto_attr], dropped_attributes_count=1) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Span.Link ───────────────────────────────────────────────────────────────── + +def test_span_link_empty() -> None: + assert Span.Link().SerializeToString() == b"" + + +def test_span_link_empty_matches_proto() -> None: + assert Span.Link().SerializeToString() == ProtoSpan.Link().SerializeToString() + + +def test_span_link_with_ids() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + our = Span.Link(trace_id=trace_id, span_id=span_id, trace_state="k=v") + proto = ProtoSpan.Link(trace_id=trace_id, span_id=span_id, trace_state="k=v") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_link_with_flags() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + our = Span.Link(trace_id=trace_id, span_id=span_id, flags=1) + proto = ProtoSpan.Link(trace_id=trace_id, span_id=span_id, flags=1) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── Span ────────────────────────────────────────────────────────────────────── + +def test_span_empty() -> None: + assert Span().SerializeToString() == b"" + + +def test_span_empty_matches_proto() -> None: + assert Span().SerializeToString() == ProtoSpan().SerializeToString() + + +def test_span_basic() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + our = Span( + trace_id=trace_id, + span_id=span_id, + name="my-span", + start_time_unix_nano=1_000_000_000, + end_time_unix_nano=2_000_000_000, + ) + proto = ProtoSpan( + trace_id=trace_id, + span_id=span_id, + name="my-span", + start_time_unix_nano=1_000_000_000, + end_time_unix_nano=2_000_000_000, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_with_status() -> None: + our = Span(name="err-span", status=Status(code=2, message="error")) + proto = ProtoSpan(name="err-span", status=ProtoStatus(code=2, message="error")) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_with_attributes() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + attr = KeyValue(key="http.method", value=AnyValue(string_value="GET")) + proto_attr = ProtoKeyValue(key="http.method", value=ProtoAnyValue(string_value="GET")) + our = Span( + trace_id=trace_id, + span_id=span_id, + name="http-span", + attributes=[attr], + dropped_attributes_count=2, + ) + proto = ProtoSpan( + trace_id=trace_id, + span_id=span_id, + name="http-span", + attributes=[proto_attr], + dropped_attributes_count=2, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_with_events_and_links() -> None: + trace_id = b"\x01" * 16 + span_id = b"\x02" * 8 + our = Span( + trace_id=trace_id, + span_id=span_id, + name="parent", + events=[Span.Event(name="click", time_unix_nano=500)], + links=[Span.Link(trace_id=trace_id, span_id=span_id)], + dropped_events_count=1, + dropped_links_count=2, + ) + proto = ProtoSpan( + trace_id=trace_id, + span_id=span_id, + name="parent", + events=[ProtoSpan.Event(name="click", time_unix_nano=500)], + links=[ProtoSpan.Link(trace_id=trace_id, span_id=span_id)], + dropped_events_count=1, + dropped_links_count=2, + ) + assert our.SerializeToString() == proto.SerializeToString() + + +def test_span_kind() -> None: + our = Span(name="server", kind=2) + proto = ProtoSpan(name="server", kind=2) + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ScopeSpans ──────────────────────────────────────────────────────────────── + +def test_scope_spans_empty() -> None: + assert ScopeSpans().SerializeToString() == b"" + + +def test_scope_spans_empty_matches_proto() -> None: + assert ScopeSpans().SerializeToString() == ProtoScopeSpans().SerializeToString() + + +def test_scope_spans_schema_url() -> None: + our = ScopeSpans(schema_url="https://example.com") + proto = ProtoScopeSpans(schema_url="https://example.com") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_scope_spans_with_scope_and_span() -> None: + scope = InstrumentationScope(name="mylib", version="1.0") + proto_scope = ProtoInstrumentationScope(name="mylib", version="1.0") + span = Span(name="test-span") + proto_span = ProtoSpan(name="test-span") + our = ScopeSpans(scope=scope, spans=[span], schema_url="s") + proto = ProtoScopeSpans(scope=proto_scope, spans=[proto_span], schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +# ── ResourceSpans ───────────────────────────────────────────────────────────── + +def test_resource_spans_empty() -> None: + assert ResourceSpans().SerializeToString() == b"" + + +def test_resource_spans_empty_matches_proto() -> None: + assert ResourceSpans().SerializeToString() == ProtoResourceSpans().SerializeToString() + + +def test_resource_spans_schema_url() -> None: + our = ResourceSpans(schema_url="https://example.com") + proto = ProtoResourceSpans(schema_url="https://example.com") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_spans_with_resource() -> None: + res = Resource(attributes=[KeyValue(key="k", value=AnyValue(string_value="v"))]) + proto_res = ProtoResource(attributes=[ProtoKeyValue(key="k", value=ProtoAnyValue(string_value="v"))]) + our = ResourceSpans(resource=res, schema_url="s") + proto = ProtoResourceSpans(resource=proto_res, schema_url="s") + assert our.SerializeToString() == proto.SerializeToString() + + +def test_resource_spans_with_scope_spans() -> None: + span = Span(name="s") + proto_span = ProtoSpan(name="s") + ss = ScopeSpans(spans=[span]) + proto_ss = ProtoScopeSpans(spans=[proto_span]) + our = ResourceSpans(scope_spans=[ss]) + proto = ProtoResourceSpans(scope_spans=[proto_ss]) + assert our.SerializeToString() == proto.SerializeToString() diff --git a/opentelemetry-proto/tests/performance/__init__.py b/opentelemetry-proto/tests/performance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/performance/test_benchmark.py b/opentelemetry-proto/tests/performance/test_benchmark.py new file mode 100644 index 00000000000..f562b001505 --- /dev/null +++ b/opentelemetry-proto/tests/performance/test_benchmark.py @@ -0,0 +1,569 @@ +# tests/_pyprotobuf/test_benchmark.py +# +# Benchmark: _pyprotobuf (pure-Python) vs google.protobuf (C extension) +# encoding speed. Four benchmark categories: +# +# Full message — one round-trip of a Record containing every field type. +# Per field — each field helper in isolation against google.protobuf. +# Scaling — string, bytes, and packed-repeated at 3 payload sizes. +# Varint — encode_varint at 1-, 2-, 3-, and 5-byte bit widths. +# +# All proto schemas are built at import time via the descriptor / message- +# factory API — no .proto files or protoc step required. +# +# Run: +# uv run pytest tests/_pyprotobuf/test_benchmark.py -v --benchmark-sort=mean + +from google.protobuf import descriptor_pb2, descriptor_pool, message_factory +from pytest import mark + +from opentelemetry._proto._pyprotobuf import encode_varint +from opentelemetry._proto._pyprotobuf.fields import ( + bool_field, + byt, + dbl, + fix32, + fix64, + msg, + packed_double, + packed_uint64, + sint32, + string, + u64, +) + +_T = descriptor_pb2.FieldDescriptorProto + + +# ── Proto builders ───────────────────────────────────────────────────────────── + +def _build_full_record_classes(): + """Record message with one field of every type (full-message benchmark).""" + fp = descriptor_pb2.FileDescriptorProto() + fp.name = "pyproto_benchmark.proto" + fp.syntax = "proto3" + + inner = fp.message_type.add() + inner.name = "Inner" + for name, number, tid in (("label", 1, _T.TYPE_STRING), ("seq", 2, _T.TYPE_UINT64)): + f = inner.field.add() + f.name = name; f.number = number; f.type = tid; f.label = _T.LABEL_OPTIONAL + + rec = fp.message_type.add() + rec.name = "Record" + for name, number, tid, lbl in ( + ("name", 1, _T.TYPE_STRING, _T.LABEL_OPTIONAL), + ("count", 2, _T.TYPE_UINT64, _T.LABEL_OPTIONAL), + ("value", 3, _T.TYPE_DOUBLE, _T.LABEL_OPTIONAL), + ("active", 4, _T.TYPE_BOOL, _T.LABEL_OPTIONAL), + ("data", 5, _T.TYPE_BYTES, _T.LABEL_OPTIONAL), + ("timestamp_ns", 6, _T.TYPE_FIXED64, _T.LABEL_OPTIONAL), + ("flags", 7, _T.TYPE_FIXED32, _T.LABEL_OPTIONAL), + ("bucket_counts", 9, _T.TYPE_UINT64, _T.LABEL_REPEATED), + ("bounds", 10, _T.TYPE_DOUBLE, _T.LABEL_REPEATED), + ): + f = rec.field.add() + f.name = name; f.number = number; f.type = tid; f.label = lbl + + mf = rec.field.add() + mf.name = "inner"; mf.number = 8; mf.type = _T.TYPE_MESSAGE + mf.label = _T.LABEL_OPTIONAL; mf.type_name = "Inner" + + pool = descriptor_pool.DescriptorPool() + pool.Add(fp) + return ( + message_factory.GetMessageClass(pool.FindMessageTypeByName("Inner")), + message_factory.GetMessageClass(pool.FindMessageTypeByName("Record")), + ) + + +def _build_field_msg_classes(): + """FieldMsg with one field per type — per-field and scaling benchmarks.""" + fp = descriptor_pb2.FileDescriptorProto() + fp.name = "pyproto_field_benchmark.proto" + fp.syntax = "proto3" + + fi = fp.message_type.add() + fi.name = "FieldInner" + f = fi.field.add() + f.name = "v"; f.number = 1; f.type = _T.TYPE_UINT64; f.label = _T.LABEL_OPTIONAL + + fm = fp.message_type.add() + fm.name = "FieldMsg" + for name, number, tid, lbl in ( + ("f_uint64", 1, _T.TYPE_UINT64, _T.LABEL_OPTIONAL), + ("f_string", 2, _T.TYPE_STRING, _T.LABEL_OPTIONAL), + ("f_bytes", 3, _T.TYPE_BYTES, _T.LABEL_OPTIONAL), + ("f_double", 4, _T.TYPE_DOUBLE, _T.LABEL_OPTIONAL), + ("f_bool", 5, _T.TYPE_BOOL, _T.LABEL_OPTIONAL), + ("f_fixed64", 6, _T.TYPE_FIXED64, _T.LABEL_OPTIONAL), + ("f_fixed32", 7, _T.TYPE_FIXED32, _T.LABEL_OPTIONAL), + ("f_sint32", 8, _T.TYPE_SINT32, _T.LABEL_OPTIONAL), + ("f_packed_u64", 9, _T.TYPE_UINT64, _T.LABEL_REPEATED), + ("f_packed_dbl", 10, _T.TYPE_DOUBLE, _T.LABEL_REPEATED), + ): + f = fm.field.add() + f.name = name; f.number = number; f.type = tid; f.label = lbl + + mf = fm.field.add() + mf.name = "f_msg"; mf.number = 11; mf.type = _T.TYPE_MESSAGE + mf.label = _T.LABEL_OPTIONAL; mf.type_name = "FieldInner" + + pool = descriptor_pool.DescriptorPool() + pool.Add(fp) + return ( + message_factory.GetMessageClass(pool.FindMessageTypeByName("FieldInner")), + message_factory.GetMessageClass(pool.FindMessageTypeByName("FieldMsg")), + ) + + +def _build_repeated_msg_classes(): + """Item + Container — repeated embedded message benchmark.""" + fp = descriptor_pb2.FileDescriptorProto() + fp.name = "pyproto_repeated_benchmark.proto" + fp.syntax = "proto3" + + item = fp.message_type.add() + item.name = "Item" + for name, number, tid in ( + ("label", 1, _T.TYPE_STRING), + ("count", 2, _T.TYPE_UINT64), + ("value", 3, _T.TYPE_DOUBLE), + ): + f = item.field.add() + f.name = name; f.number = number; f.type = tid; f.label = _T.LABEL_OPTIONAL + + container = fp.message_type.add() + container.name = "Container" + rf = container.field.add() + rf.name = "items"; rf.number = 1; rf.type = _T.TYPE_MESSAGE + rf.label = _T.LABEL_REPEATED; rf.type_name = "Item" + + pool = descriptor_pool.DescriptorPool() + pool.Add(fp) + return ( + message_factory.GetMessageClass(pool.FindMessageTypeByName("Item")), + message_factory.GetMessageClass(pool.FindMessageTypeByName("Container")), + ) + + +_Inner, _Record = _build_full_record_classes() +_FieldInner, _FieldMsg = _build_field_msg_classes() +_Item, _Container = _build_repeated_msg_classes() + + +# ── Shared benchmark data ────────────────────────────────────────────────────── + +_NAME = "benchmark.record.example" +_COUNT = 9_876_543_210 +_VALUE = 3.141592653589793 +_DATA = b"\xde\xad\xbe\xef" * 8 +_TS = 1_782_401_900_556_236_527 +_FLAGS = 0xDEAD +_INNER_LABEL = "inner.label" +_INNER_SEQ = 42 +_BUCKET_COUNTS = [0, 1, 4, 12, 35, 78, 120, 89, 42, 15, 4, 1, 0] +_BOUNDS = [0.0, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] + +_FIELD_INNER_BYTES = _FieldInner(v=42).SerializeToString() + +# Item data for repeated-message benchmarks: (label, count, value) tuples. +# All values are non-default so every field is serialized. +_REPEATED_SIZES = [1, 5, 20] +_ITEM_DATA = [ + (f"item.label.{i}", (i + 1) * 100, (i + 1) * 0.5) + for i in range(max(_REPEATED_SIZES)) +] + +# Pre-built google.protobuf objects — construction cost excluded from +# serialization-only benchmarks in section 7. +_PRE_BUILT_RECORD = _Record( + name=_NAME, count=_COUNT, value=_VALUE, active=True, + data=_DATA, timestamp_ns=_TS, flags=_FLAGS, + inner=_Inner(label=_INNER_LABEL, seq=_INNER_SEQ), + bucket_counts=_BUCKET_COUNTS, bounds=_BOUNDS, +) + +_PRE_BUILT_FIELD_PB = { + "uint64": _FieldMsg(f_uint64=_COUNT), + "string": _FieldMsg(f_string=_NAME), + "bytes": _FieldMsg(f_bytes=_DATA), + "double": _FieldMsg(f_double=_VALUE), + "bool": _FieldMsg(f_bool=True), + "fixed64": _FieldMsg(f_fixed64=_TS), + "fixed32": _FieldMsg(f_fixed32=_FLAGS), + "sint32": _FieldMsg(f_sint32=-12345), + "packed_uint64": _FieldMsg(f_packed_u64=_BUCKET_COUNTS), + "packed_double": _FieldMsg(f_packed_dbl=_BOUNDS), + "msg": _FieldMsg(f_msg=_FieldInner(v=42)), +} + + +# ══════════════════════════════════════════════════════════════════════════════ +# 1. Full-message benchmark +# ══════════════════════════════════════════════════════════════════════════════ + +def _pyproto_encode() -> bytes: + inner = string(1, _INNER_LABEL) + u64(2, _INNER_SEQ) + return ( + string(1, _NAME) + + u64(2, _COUNT) + + dbl(3, _VALUE) + + bool_field(4, True) + + byt(5, _DATA) + + fix64(6, _TS) + + fix32(7, _FLAGS) + + msg(8, inner) + + packed_uint64(9, _BUCKET_COUNTS) + + packed_double(10, _BOUNDS) + ) + + +def _pb_encode() -> bytes: + return _Record( + name=_NAME, + count=_COUNT, + value=_VALUE, + active=True, + data=_DATA, + timestamp_ns=_TS, + flags=_FLAGS, + inner=_Inner(label=_INNER_LABEL, seq=_INNER_SEQ), + bucket_counts=_BUCKET_COUNTS, + bounds=_BOUNDS, + ).SerializeToString() + + +def test_encode_outputs_identical() -> None: + assert _pyproto_encode() == _pb_encode() + + +def test_encode_pyproto(benchmark) -> None: + result = benchmark(_pyproto_encode) + assert len(result) > 0 + + +def test_encode_protobuf(benchmark) -> None: + result = benchmark(_pb_encode) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 2. Per-field-type benchmarks +# +# Each pyproto field helper is benchmarked against google.protobuf encoding +# the same single field. Both sides include message-construction overhead +# where applicable (there is no separate "build" vs "serialize" step in the +# pyproto API — it is a pure function call). +# ══════════════════════════════════════════════════════════════════════════════ + +_PER_FIELD = [ + ("uint64", lambda: u64(1, _COUNT), + lambda: _FieldMsg(f_uint64=_COUNT).SerializeToString()), + ("string", lambda: string(2, _NAME), + lambda: _FieldMsg(f_string=_NAME).SerializeToString()), + ("bytes", lambda: byt(3, _DATA), + lambda: _FieldMsg(f_bytes=_DATA).SerializeToString()), + ("double", lambda: dbl(4, _VALUE), + lambda: _FieldMsg(f_double=_VALUE).SerializeToString()), + ("bool", lambda: bool_field(5, True), + lambda: _FieldMsg(f_bool=True).SerializeToString()), + ("fixed64", lambda: fix64(6, _TS), + lambda: _FieldMsg(f_fixed64=_TS).SerializeToString()), + ("fixed32", lambda: fix32(7, _FLAGS), + lambda: _FieldMsg(f_fixed32=_FLAGS).SerializeToString()), + ("sint32", lambda: sint32(8, -12345), + lambda: _FieldMsg(f_sint32=-12345).SerializeToString()), + ("packed_uint64",lambda: packed_uint64(9, _BUCKET_COUNTS), + lambda: _FieldMsg(f_packed_u64=_BUCKET_COUNTS).SerializeToString()), + ("packed_double",lambda: packed_double(10, _BOUNDS), + lambda: _FieldMsg(f_packed_dbl=_BOUNDS).SerializeToString()), + ("msg", lambda: msg(11, _FIELD_INNER_BYTES), + lambda: _FieldMsg(f_msg=_FieldInner(v=42)).SerializeToString()), +] + +_FIELD_IDS = [name for name, _, _ in _PER_FIELD] + + +@mark.parametrize("name,pyproto_fn,pb_fn", _PER_FIELD, ids=_FIELD_IDS) +def test_field_outputs_identical(name, pyproto_fn, pb_fn) -> None: + assert pyproto_fn() == pb_fn(), f"encoding mismatch for field type: {name}" + + +@mark.parametrize("fn", [p for _, p, _ in _PER_FIELD], ids=_FIELD_IDS) +def test_field_pyproto(benchmark, fn) -> None: + result = benchmark(fn) + assert len(result) > 0 + + +@mark.parametrize("fn", [pb for _, _, pb in _PER_FIELD], ids=_FIELD_IDS) +def test_field_protobuf(benchmark, fn) -> None: + result = benchmark(fn) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 3. Scaling benchmarks +# +# How encoding time grows with payload size for the three field types whose +# cost is proportional to data length: string, bytes, and packed repeated. +# Data is pre-built outside the benchmark loop; only the encoding is timed. +# ══════════════════════════════════════════════════════════════════════════════ + +_STR_SIZES = [4, 256, 16_384] +_BYTES_SIZES = [4, 256, 16_384] +_PACKED_SIZES = [10, 100, 1_000] + + +@mark.parametrize("n", _STR_SIZES, ids=["4B", "256B", "16KB"]) +def test_scale_string_pyproto(benchmark, n) -> None: + s = "x" * n + result = benchmark(lambda: string(2, s)) + assert len(result) > 0 + + +@mark.parametrize("n", _STR_SIZES, ids=["4B", "256B", "16KB"]) +def test_scale_string_protobuf(benchmark, n) -> None: + s = "x" * n + result = benchmark(lambda: _FieldMsg(f_string=s).SerializeToString()) + assert len(result) > 0 + + +@mark.parametrize("n", _BYTES_SIZES, ids=["4B", "256B", "16KB"]) +def test_scale_bytes_pyproto(benchmark, n) -> None: + data = b"x" * n + result = benchmark(lambda: byt(3, data)) + assert len(result) > 0 + + +@mark.parametrize("n", _BYTES_SIZES, ids=["4B", "256B", "16KB"]) +def test_scale_bytes_protobuf(benchmark, n) -> None: + data = b"x" * n + result = benchmark(lambda: _FieldMsg(f_bytes=data).SerializeToString()) + assert len(result) > 0 + + +@mark.parametrize("n", _PACKED_SIZES, ids=["10", "100", "1000"]) +def test_scale_packed_uint64_pyproto(benchmark, n) -> None: + values = list(range(n)) + result = benchmark(lambda: packed_uint64(9, values)) + assert len(result) > 0 + + +@mark.parametrize("n", _PACKED_SIZES, ids=["10", "100", "1000"]) +def test_scale_packed_uint64_protobuf(benchmark, n) -> None: + values = list(range(n)) + result = benchmark(lambda: _FieldMsg(f_packed_u64=values).SerializeToString()) + assert len(result) > 0 + + +@mark.parametrize("n", _PACKED_SIZES, ids=["10", "100", "1000"]) +def test_scale_packed_double_pyproto(benchmark, n) -> None: + values = [float(i) * 0.1 for i in range(n)] + result = benchmark(lambda: packed_double(10, values)) + assert len(result) > 0 + + +@mark.parametrize("n", _PACKED_SIZES, ids=["10", "100", "1000"]) +def test_scale_packed_double_protobuf(benchmark, n) -> None: + values = [float(i) * 0.1 for i in range(n)] + result = benchmark(lambda: _FieldMsg(f_packed_dbl=values).SerializeToString()) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 4. Varint bit-width benchmarks +# +# encode_varint is the hottest path in _pyprotobuf: every tag and every +# varint-type field value goes through it. These benchmarks measure how +# encoding time scales with the number of continuation bytes (bit width). +# +# The google.protobuf column encodes the same integer as a uint64 field +# (including message-object construction and tag overhead), which is the +# closest available comparison point since protobuf does not expose a +# standalone varint encoder. Use the pyproto column to track raw varint +# speed; use the ratio to see the combined tag+field overhead gap. +# ══════════════════════════════════════════════════════════════════════════════ + +_VARINT_CASES = [ + ("1byte", 63), # fits in 1 byte (0x00–0x7f) + ("2byte", 300), # requires 2 bytes (0x80–0x3fff) + ("3byte", 100_000), # requires 3 bytes (0x4000–0x1fffff) + ("5byte", 2**32 - 1), # requires 5 bytes (max uint32) +] + +_VARINT_IDS = [c[0] for c in _VARINT_CASES] +_VARINT_VALUES = [c[1] for c in _VARINT_CASES] + + +@mark.parametrize("v", _VARINT_VALUES, ids=_VARINT_IDS) +def test_varint_pyproto(benchmark, v) -> None: + result = benchmark(lambda: encode_varint(v)) + assert len(result) > 0 + + +@mark.parametrize("v", _VARINT_VALUES, ids=_VARINT_IDS) +def test_varint_protobuf(benchmark, v) -> None: + result = benchmark(lambda: _FieldMsg(f_uint64=v).SerializeToString()) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 5. Concatenation strategy: `+` vs `b"".join()` +# +# The current SerializeToString() pattern chains field results with `+`, +# creating N-1 intermediate bytes objects (one per addition). b"".join() +# allocates the final buffer once and copies each part exactly once. +# These two tests use the same field data so the only variable is the +# concatenation strategy. +# ══════════════════════════════════════════════════════════════════════════════ + +def _pyproto_encode_join() -> bytes: + inner = b"".join([string(1, _INNER_LABEL), u64(2, _INNER_SEQ)]) + return b"".join([ + string(1, _NAME), + u64(2, _COUNT), + dbl(3, _VALUE), + bool_field(4, True), + byt(5, _DATA), + fix64(6, _TS), + fix32(7, _FLAGS), + msg(8, inner), + packed_uint64(9, _BUCKET_COUNTS), + packed_double(10, _BOUNDS), + ]) + + +def test_concat_strategy_outputs_identical() -> None: + assert _pyproto_encode() == _pyproto_encode_join() + + +def test_encode_concat_pyproto(benchmark) -> None: + result = benchmark(_pyproto_encode) + assert len(result) > 0 + + +def test_encode_join_pyproto(benchmark) -> None: + result = benchmark(_pyproto_encode_join) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 6. All-default fields (fast path) +# +# Proto3 omits fields whose value equals the type default (0, "", b"", False, +# 0.0, []). Each _pyprotobuf helper returns b"" immediately for defaults. +# This benchmark measures the minimum cost of SerializeToString() — calling +# every helper in a message when none produce any output. +# ══════════════════════════════════════════════════════════════════════════════ + +def _pyproto_encode_all_defaults() -> bytes: + # Embedded message field omitted: real SerializeToString() guards with + # `if self.field is not None`. Every helper here returns b"". + return ( + string(1, "") + + u64(2, 0) + + dbl(3, 0.0) + + bool_field(4, False) + + byt(5, b"") + + fix64(6, 0) + + fix32(7, 0) + + packed_uint64(9, []) + + packed_double(10, []) + ) + + +def test_all_defaults_pyproto(benchmark) -> None: + result = benchmark(_pyproto_encode_all_defaults) + assert result == b"" + + +def test_all_defaults_protobuf(benchmark) -> None: + result = benchmark(lambda: _Record().SerializeToString()) + assert result == b"" + + +# ══════════════════════════════════════════════════════════════════════════════ +# 7. google.protobuf: construction vs serialization split +# +# Previous benchmarks bundle message-object construction and serialization +# together for google.protobuf. pyproto has no construction phase — it is a +# pure function call. Separating the two phases shows where google.protobuf's +# time actually goes and gives a fairer encoding-only comparison. +# +# Read these alongside the existing test_encode_pyproto / test_encode_protobuf: +# test_encode_pyproto — pyproto: pure call, no object +# test_encode_protobuf_construct — google.protobuf: construction only +# test_encode_protobuf_serialize — google.protobuf: serialization only +# test_encode_protobuf — google.protobuf: construction + serialization +# ══════════════════════════════════════════════════════════════════════════════ + +def _pb_construct(): + return _Record( + name=_NAME, count=_COUNT, value=_VALUE, active=True, + data=_DATA, timestamp_ns=_TS, flags=_FLAGS, + inner=_Inner(label=_INNER_LABEL, seq=_INNER_SEQ), + bucket_counts=_BUCKET_COUNTS, bounds=_BOUNDS, + ) + + +def test_encode_protobuf_construct(benchmark) -> None: + result = benchmark(_pb_construct) + assert result is not None + + +def test_encode_protobuf_serialize(benchmark) -> None: + result = benchmark(_PRE_BUILT_RECORD.SerializeToString) + assert len(result) > 0 + + +@mark.parametrize("name", list(_PRE_BUILT_FIELD_PB), ids=list(_PRE_BUILT_FIELD_PB)) +def test_field_serialize_only_protobuf(benchmark, name) -> None: + result = benchmark(_PRE_BUILT_FIELD_PB[name].SerializeToString) + assert len(result) > 0 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 8. Repeated embedded messages +# +# Encodes a Container with N Item sub-messages. Each Item has a string, +# uint64, and double field. Unlike packed repeated scalars, repeated messages +# require one SerializeToString-equivalent call per element plus one msg() +# wrapper per element — this tests whether per-element overhead compounds +# or stays flat. +# +# pyproto side: b"".join(msg(1, string(1,l)+u64(2,c)+dbl(3,v)) for ...) +# protobuf side: _Container(items=[_Item(...), ...]).SerializeToString() +# ══════════════════════════════════════════════════════════════════════════════ + +def _pyproto_encode_repeated(n: int) -> bytes: + return b"".join( + msg(1, string(1, label) + u64(2, count) + dbl(3, value)) + for label, count, value in _ITEM_DATA[:n] + ) + + +def _pb_encode_repeated(n: int) -> bytes: + return _Container( + items=[ + _Item(label=label, count=count, value=value) + for label, count, value in _ITEM_DATA[:n] + ] + ).SerializeToString() + + +@mark.parametrize("n", _REPEATED_SIZES, ids=["1", "5", "20"]) +def test_repeated_msg_outputs_identical(n) -> None: + assert _pyproto_encode_repeated(n) == _pb_encode_repeated(n) + + +@mark.parametrize("n", _REPEATED_SIZES, ids=["1", "5", "20"]) +def test_repeated_msg_pyproto(benchmark, n) -> None: + result = benchmark(lambda: _pyproto_encode_repeated(n)) + assert len(result) > 0 + + +@mark.parametrize("n", _REPEATED_SIZES, ids=["1", "5", "20"]) +def test_repeated_msg_protobuf(benchmark, n) -> None: + result = benchmark(lambda: _pb_encode_repeated(n)) + assert len(result) > 0 diff --git a/opentelemetry-proto/tests/performance/test_benchmark_otlp.py b/opentelemetry-proto/tests/performance/test_benchmark_otlp.py new file mode 100644 index 00000000000..6a90aff9b3b --- /dev/null +++ b/opentelemetry-proto/tests/performance/test_benchmark_otlp.py @@ -0,0 +1,408 @@ +# tests/performance/test_benchmark_otlp.py +# +# Benchmark: pure-Python pyproto (opentelemetry._proto) vs the real +# google.protobuf implementation (opentelemetry.proto, upb/C backend) on +# *realistic, full OTLP export payloads* — the actual workload the exporters +# put through these message classes. +# +# test_benchmark.py measures the _pyprotobuf primitives (varint, single +# fields, scaling) against synthetic google.protobuf messages built from +# throw-away descriptors. This file instead exercises the real OTLP message +# trees — ExportTraceServiceRequest / ExportLogsServiceRequest / +# ExportMetricsServiceRequest, nested Resource → Scope → Span/LogRecord/Metric +# with attributes, events, links, status and data points — at several batch +# sizes, so the pyproto-vs-protobuf gap can be read off for payloads that look +# like what a real collector export sends. +# +# Both implementations expose an identical constructor + SerializeToString() +# API (the equivalence suite proves the bytes match), so a single builder, +# parameterized by a bundle of classes, drives both sides. +# +# BUILD + SERIALIZE benchmarks — one full encode of a logical payload from +# source data, i.e. the per-export cost an +# exporter actually pays. +# SERIALIZE-ONLY benchmarks — SerializeToString() on a pre-built message, +# isolating raw encoding from construction. +# +# The real (google.protobuf) classes are only available when the upstream +# ``opentelemetry-proto`` package owns the public ``opentelemetry.proto`` path +# (installed *after* this package so it wins the namespace). When it does not, +# ``opentelemetry.proto`` resolves to this package's pure-Python shim and the +# comparison would be pyproto-vs-pyproto — so the whole module skips, exactly +# like the equivalence suite's conftest guard. +# +# Run (install order matters — real protobuf must win opentelemetry.proto): +# uv pip install . && uv pip install protobuf opentelemetry-proto pytest-benchmark +# uv run pytest tests/performance/test_benchmark_otlp.py \ +# --benchmark-sort=fullname --benchmark-group-by=group + +from types import SimpleNamespace + +from pytest import mark, skip + +from opentelemetry._proto.collector.logs.v1 import logs_service_pb2 as _py_coll_logs +from opentelemetry._proto.collector.metrics.v1 import ( + metrics_service_pb2 as _py_coll_metrics, +) +from opentelemetry._proto.collector.trace.v1 import ( + trace_service_pb2 as _py_coll_trace, +) +from opentelemetry._proto.common.v1 import common_pb2 as _py_common +from opentelemetry._proto.logs.v1 import logs_pb2 as _py_logs +from opentelemetry._proto.metrics.v1 import metrics_pb2 as _py_metrics +from opentelemetry._proto.resource.v1 import resource_pb2 as _py_resource +from opentelemetry._proto.trace.v1 import trace_pb2 as _py_trace + +# Public path: the real opentelemetry-proto when installed, else this package's +# pure-Python shim. Guard that it is the real package before benchmarking. +from opentelemetry.proto.common.v1 import common_pb2 as _pb_common + +if "pyproto" in (_pb_common.__file__ or ""): + skip( + "opentelemetry.proto resolves to the pyproto shim " + f"({_pb_common.__file__}); install the real opentelemetry-proto " + "package (after this one, so it owns opentelemetry.proto) to compare " + "pure-Python pyproto against the real google.protobuf implementation.", + allow_module_level=True, + ) + +from opentelemetry.proto.collector.logs.v1 import logs_service_pb2 as _pb_coll_logs +from opentelemetry.proto.collector.metrics.v1 import ( + metrics_service_pb2 as _pb_coll_metrics, +) +from opentelemetry.proto.collector.trace.v1 import ( + trace_service_pb2 as _pb_coll_trace, +) +from opentelemetry.proto.logs.v1 import logs_pb2 as _pb_logs +from opentelemetry.proto.metrics.v1 import metrics_pb2 as _pb_metrics +from opentelemetry.proto.resource.v1 import resource_pb2 as _pb_resource +from opentelemetry.proto.trace.v1 import trace_pb2 as _pb_trace + + +# ── Class bundles ─────────────────────────────────────────────────────────── +# +# Both implementations share the same class names and constructor kwargs, so a +# builder written against one bundle works verbatim against the other. + +def _bundle(common, resource, trace, logs, metrics, coll_trace, coll_logs, coll_metrics): + return SimpleNamespace( + AnyValue=common.AnyValue, + KeyValue=common.KeyValue, + InstrumentationScope=common.InstrumentationScope, + Resource=resource.Resource, + Span=trace.Span, + Status=trace.Status, + ScopeSpans=trace.ScopeSpans, + ResourceSpans=trace.ResourceSpans, + LogRecord=logs.LogRecord, + ScopeLogs=logs.ScopeLogs, + ResourceLogs=logs.ResourceLogs, + NumberDataPoint=metrics.NumberDataPoint, + Sum=metrics.Sum, + HistogramDataPoint=metrics.HistogramDataPoint, + Histogram=metrics.Histogram, + Metric=metrics.Metric, + ScopeMetrics=metrics.ScopeMetrics, + ResourceMetrics=metrics.ResourceMetrics, + ExportTraceServiceRequest=coll_trace.ExportTraceServiceRequest, + ExportLogsServiceRequest=coll_logs.ExportLogsServiceRequest, + ExportMetricsServiceRequest=coll_metrics.ExportMetricsServiceRequest, + ) + + +PY = _bundle( + _py_common, _py_resource, _py_trace, _py_logs, _py_metrics, + _py_coll_trace, _py_coll_logs, _py_coll_metrics, +) +PB = _bundle( + _pb_common, _pb_resource, _pb_trace, _pb_logs, _pb_metrics, + _pb_coll_trace, _pb_coll_logs, _pb_coll_metrics, +) + + +# ── Deterministic source data ─────────────────────────────────────────────── +# +# No randomness (scripts/tests must be reproducible); ids and values are +# derived from the element index so every payload is stable across runs. + +_SCHEMA_URL = "https://opentelemetry.io/schemas/1.30.0" + + +def _trace_id(i: int) -> bytes: + return bytes(((i + b) % 256) for b in range(16)) + + +def _span_id(i: int) -> bytes: + return bytes(((i * 7 + b) % 256) for b in range(8)) + + +def _resource(M): + """A resource with the attribute set a real service emits.""" + return M.Resource( + attributes=[ + M.KeyValue(key="service.name", value=M.AnyValue(string_value="checkout-service")), + M.KeyValue(key="service.version", value=M.AnyValue(string_value="1.24.0")), + M.KeyValue(key="service.instance.id", value=M.AnyValue(string_value="pod-7f9c-abc123")), + M.KeyValue(key="process.pid", value=M.AnyValue(int_value=42317)), + M.KeyValue(key="host.name", value=M.AnyValue(string_value="ip-10-0-12-34")), + M.KeyValue(key="telemetry.sdk.language", value=M.AnyValue(string_value="python")), + M.KeyValue(key="telemetry.sdk.version", value=M.AnyValue(string_value="1.30.0")), + ] + ) + + +def _span_attributes(M, i: int): + return [ + M.KeyValue(key="http.request.method", value=M.AnyValue(string_value="GET")), + M.KeyValue(key="url.path", value=M.AnyValue(string_value=f"/api/orders/{i}")), + M.KeyValue(key="http.response.status_code", value=M.AnyValue(int_value=200)), + M.KeyValue(key="server.address", value=M.AnyValue(string_value="orders.internal")), + M.KeyValue(key="network.protocol.version", value=M.AnyValue(string_value="1.1")), + M.KeyValue(key="db.system", value=M.AnyValue(string_value="postgresql")), + ] + + +def _span(M, i: int): + base = 1_700_000_000_000_000_000 + i * 1_000_000 + return M.Span( + trace_id=_trace_id(i), + span_id=_span_id(i), + parent_span_id=_span_id(i + 1), + name="GET /api/orders/{id}", + kind=2, # SPAN_KIND_SERVER + start_time_unix_nano=base, + end_time_unix_nano=base + 4_200_000, + attributes=_span_attributes(M, i), + events=[ + M.Span.Event( + time_unix_nano=base + 1_000_000, + name="cache.miss", + attributes=[ + M.KeyValue(key="cache.key", value=M.AnyValue(string_value=f"order:{i}")), + ], + ), + M.Span.Event( + time_unix_nano=base + 2_500_000, + name="db.query", + attributes=[ + M.KeyValue(key="db.rows", value=M.AnyValue(int_value=17)), + ], + ), + ], + links=[ + M.Span.Link( + trace_id=_trace_id(i + 100), + span_id=_span_id(i + 100), + trace_state="vendor=abc", + ), + ], + status=M.Status(code=1, message=""), # STATUS_CODE_OK + flags=1, + ) + + +def _log_record(M, i: int): + base = 1_700_000_000_000_000_000 + i * 1_000_000 + return M.LogRecord( + time_unix_nano=base, + observed_time_unix_nano=base + 1_000, + severity_number=9, # INFO + severity_text="INFO", + body=M.AnyValue(string_value=f"request {i} completed in 4.2ms with status 200"), + attributes=[ + M.KeyValue(key="log.source", value=M.AnyValue(string_value="access")), + M.KeyValue(key="http.route", value=M.AnyValue(string_value="/api/orders/{id}")), + M.KeyValue(key="http.response.status_code", value=M.AnyValue(int_value=200)), + M.KeyValue(key="thread.id", value=M.AnyValue(int_value=140_234_567)), + ], + trace_id=_trace_id(i), + span_id=_span_id(i), + flags=1, + ) + + +def _metric(M, i: int, n_dp: int): + base = 1_700_000_000_000_000_000 + i * 1_000_000 + return M.Metric( + name=f"http.server.request.duration.{i}", + description="Duration of HTTP server requests.", + unit="s", + histogram=M.Histogram( + aggregation_temporality=2, # CUMULATIVE + data_points=[ + M.HistogramDataPoint( + attributes=[ + M.KeyValue(key="http.request.method", value=M.AnyValue(string_value="GET")), + M.KeyValue(key="http.response.status_code", value=M.AnyValue(int_value=200)), + ], + start_time_unix_nano=base, + time_unix_nano=base + 60_000_000_000, + count=1_234, + sum=456.789, + bucket_counts=[0, 12, 145, 402, 388, 210, 61, 14, 2, 0], + explicit_bounds=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5], + min=0.002, + max=3.1, + ) + for _ in range(n_dp) + ], + ), + ) + + +# ── Payload builders (batch of resources × scopes × items) ────────────────── + +def build_trace_request(M, n_res: int, n_scope: int, n_span: int): + return M.ExportTraceServiceRequest( + resource_spans=[ + M.ResourceSpans( + resource=_resource(M), + scope_spans=[ + M.ScopeSpans( + scope=M.InstrumentationScope(name="opentelemetry.instrumentation.flask", version="0.48b0"), + spans=[_span(M, i) for i in range(n_span)], + schema_url=_SCHEMA_URL, + ) + for _ in range(n_scope) + ], + schema_url=_SCHEMA_URL, + ) + for _ in range(n_res) + ] + ) + + +def build_logs_request(M, n_res: int, n_scope: int, n_log: int): + return M.ExportLogsServiceRequest( + resource_logs=[ + M.ResourceLogs( + resource=_resource(M), + scope_logs=[ + M.ScopeLogs( + scope=M.InstrumentationScope(name="opentelemetry.sdk._logs", version="1.30.0"), + log_records=[_log_record(M, i) for i in range(n_log)], + schema_url=_SCHEMA_URL, + ) + for _ in range(n_scope) + ], + schema_url=_SCHEMA_URL, + ) + for _ in range(n_res) + ] + ) + + +def build_metrics_request(M, n_res: int, n_metric: int, n_dp: int): + return M.ExportMetricsServiceRequest( + resource_metrics=[ + M.ResourceMetrics( + resource=_resource(M), + scope_metrics=[ + M.ScopeMetrics( + scope=M.InstrumentationScope(name="opentelemetry.sdk.metrics", version="1.30.0"), + metrics=[_metric(M, i, n_dp) for i in range(n_metric)], + schema_url=_SCHEMA_URL, + ) + ], + schema_url=_SCHEMA_URL, + ) + for _ in range(n_res) + ] + ) + + +# Scale points: (id, dims). Dims meaning is per-builder (see signatures above). +# "single" — one item, the smallest useful export. +# "scope_N" — one resource/scope, N items: isolates per-item encoding cost. +# "batch" — several resources/scopes: a full collector-sized export. +_TRACE_SCALES = [ + ("single", (1, 1, 1)), + ("scope_100", (1, 1, 100)), + ("batch_512", (4, 2, 64)), +] +_LOG_SCALES = [ + ("single", (1, 1, 1)), + ("scope_100", (1, 1, 100)), + ("batch_1000", (2, 1, 500)), +] +_METRIC_SCALES = [ + ("single", (1, 1, 1)), + ("scope_50", (1, 50, 1)), + ("batch_500", (2, 25, 10)), +] + +_SIGNALS = { + "trace": (build_trace_request, _TRACE_SCALES), + "logs": (build_logs_request, _LOG_SCALES), + "metrics": (build_metrics_request, _METRIC_SCALES), +} + + +# ── Equivalence guard: the two sides must encode to identical bytes ────────── +# +# The benchmark only means something if both implementations do the same work. +# Proto3 field-order serialization makes the byte streams comparable directly. + +_ALL_CASES = [ + (signal, label, dims) + for signal, (_builder, scales) in _SIGNALS.items() + for label, dims in scales +] +_ALL_IDS = [f"{signal}-{label}" for signal, label, _ in _ALL_CASES] + + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +def test_otlp_outputs_identical(signal, label, dims) -> None: + builder = _SIGNALS[signal][0] + py_bytes = builder(PY, *dims).SerializeToString() + pb_bytes = builder(PB, *dims).SerializeToString() + assert py_bytes == pb_bytes, ( + f"{signal}/{label}: pyproto and protobuf disagree " + f"({len(py_bytes)} vs {len(pb_bytes)} bytes)" + ) + + +# ── Build + serialize: the real per-export cost ───────────────────────────── +# +# One full encode of a logical payload from source data — construct the message +# tree and serialize it, as an exporter does on every export. + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +@mark.benchmark(group="build_serialize") +def test_build_serialize_pyproto(benchmark, signal, label, dims) -> None: + builder = _SIGNALS[signal][0] + result = benchmark(lambda: builder(PY, *dims).SerializeToString()) + assert len(result) > 0 + + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +@mark.benchmark(group="build_serialize") +def test_build_serialize_protobuf(benchmark, signal, label, dims) -> None: + builder = _SIGNALS[signal][0] + result = benchmark(lambda: builder(PB, *dims).SerializeToString()) + assert len(result) > 0 + + +# ── Serialize only: raw encoding of a pre-built message ───────────────────── +# +# Isolates SerializeToString() from message construction. pyproto builds nothing +# up front (its constructors just stash references), so its serialize-only and +# build+serialize numbers are close; google.protobuf does real work in both +# construction and serialization, so this split shows where its time goes. + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +@mark.benchmark(group="serialize_only") +def test_serialize_only_pyproto(benchmark, signal, label, dims) -> None: + builder = _SIGNALS[signal][0] + message = builder(PY, *dims) + result = benchmark(message.SerializeToString) + assert len(result) > 0 + + +@mark.parametrize("signal,label,dims", _ALL_CASES, ids=_ALL_IDS) +@mark.benchmark(group="serialize_only") +def test_serialize_only_protobuf(benchmark, signal, label, dims) -> None: + builder = _SIGNALS[signal][0] + message = builder(PB, *dims) + result = benchmark(message.SerializeToString) + assert len(result) > 0 diff --git a/opentelemetry-proto/tests/test_proto.py b/opentelemetry-proto/tests/test_proto.py deleted file mode 100644 index 2120c0a06a1..00000000000 --- a/opentelemetry-proto/tests/test_proto.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 -# type: ignore - -from importlib.util import find_spec -from unittest import TestCase - - -class TestInstrumentor(TestCase): - def test_proto(self): - if find_spec("opentelemetry.proto") is None: - self.fail("opentelemetry-proto not installed") diff --git a/opentelemetry-proto/tests/unit/__init__.py b/opentelemetry-proto/tests/unit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/__init__.py b/opentelemetry-proto/tests/unit/_pyprotobuf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/test_enum.py b/opentelemetry-proto/tests/unit/_pyprotobuf/test_enum.py new file mode 100644 index 00000000000..ac20d349ba9 --- /dev/null +++ b/opentelemetry-proto/tests/unit/_pyprotobuf/test_enum.py @@ -0,0 +1,96 @@ +# tests/test__enum.py +# +# encode_enum encodes an integer enum value using the same wire format as +# int32: non-negative values as a plain varint, negative values as a 64-bit +# two's-complement varint. These tests verify that contract using hand-computed +# expected byte literals. + +from google.protobuf import descriptor_pb2, descriptor_pool, message_factory +from pytest import mark + +from opentelemetry._proto._pyprotobuf import encode_enum, encode_int, encode_tag + + +def test_zero() -> None: + # The proto3 default enum value is always 0. + assert encode_enum(0) == b"\x00" + + +def test_one() -> None: + # Typical enum constant fits in a single varint byte. + assert encode_enum(1) == b"\x01" + + +def test_two() -> None: + assert encode_enum(2) == b"\x02" + + +def test_two_byte_value() -> None: + # A large enum constant that requires two varint bytes (same as encode_int(300)). + assert encode_enum(300) == b"\xac\x02" + + +def test_negative_value() -> None: + # Negative enum values use 64-bit two's-complement encoding. + # -1 → 10-byte varint for 2^64-1. + assert encode_enum(-1) == b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +def test_matches_encode_int_for_positive_values() -> None: + # encode_enum and encode_int must produce identical bytes for non-negative inputs. + for value in [0, 1, 2, 127, 128, 255, 300, 2**16]: + assert encode_enum(value) == encode_int(value) + + +def test_matches_encode_int_for_negative_values() -> None: + for value in [-1, -2, -(2**31)]: + assert encode_enum(value) == encode_int(value) + + +# ── oracle — byte-for-byte comparison with google.protobuf ──────────────────── +# +# A proto2 message with a Color enum field is used as the oracle. proto2 is +# chosen so the field is serialised even when its value is the default (0). +# +# The Color enum defines RED=0, GREEN=1, BLUE=2. Only those three values are +# used because proto2 rejects assignments of undefined enum constants. +# +# The serialised message is exactly encode_tag(field, 0) + encode_enum(value). + +_FIELD = 1 +_WT = 0 # wire type 0 — varint + + +def _build_enum_message_class(): + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = "opentelemetry_pyproto_test_enum.proto" + file_proto.syntax = "proto2" + + color = file_proto.enum_type.add() + color.name = "Color" + for name, number in [("RED", 0), ("GREEN", 1), ("BLUE", 2)]: + ev = color.value.add() + ev.name = name + ev.number = number + + msg_proto = file_proto.message_type.add() + msg_proto.name = "EnumMessage" + f = msg_proto.field.add() + f.name = "color_field" + f.number = _FIELD + f.type = descriptor_pb2.FieldDescriptorProto.TYPE_ENUM + f.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + f.type_name = ".Color" + + pool = descriptor_pool.DescriptorPool() + pool.Add(file_proto) + return message_factory.GetMessageClass(pool.FindMessageTypeByName("EnumMessage")) + + +_EnumMessage = _build_enum_message_class() + + +@mark.parametrize("value", [0, 1, 2]) +def test_encode_enum_matches_protobuf(value: int) -> None: + expected = encode_tag(_FIELD, _WT) + encode_enum(value) + assert _EnumMessage(color_field=value).SerializeToString() == expected diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/test_fields.py b/opentelemetry-proto/tests/unit/_pyprotobuf/test_fields.py new file mode 100644 index 00000000000..6327a842766 --- /dev/null +++ b/opentelemetry-proto/tests/unit/_pyprotobuf/test_fields.py @@ -0,0 +1,651 @@ +# tests/_pyprotobuf/test_fields.py +# +# Tests for the proto3 field-level encoding helpers in fields.py. +# +# Every test follows the same pattern: +# 1. Verify default-omission: proto3 defaults (zero, empty, None) produce b"". +# 2. Verify tag: the first byte(s) of the output encode the correct field +# number and wire type using the formula (field_number << 3) | wire_type. +# 3. Verify value: the bytes after the tag match the correct wire encoding. +# +# All expected byte literals are derived by hand from the protobuf wire-format +# specification and cross-checked against encode_tag + the primitive encoders. + +from math import inf, nan +from struct import pack + +from pytest import mark + +from opentelemetry._proto._pyprotobuf import encode_tag, encode_varint +from opentelemetry._proto._pyprotobuf.fields import ( + WT_32BIT, + WT_64BIT, + WT_LEN, + WT_VARINT, + bool_field, + byt, + dbl, + fix32, + fix64, + msg, + opt_dbl, + packed_double, + packed_fix64, + packed_uint64, + sint32, + string, + u64, +) + + +# ── Wire-type constants ──────────────────────────────────────────────────────── + + +def test_wt_varint_is_zero() -> None: + assert WT_VARINT == 0 + + +def test_wt_64bit_is_one() -> None: + assert WT_64BIT == 1 + + +def test_wt_len_is_two() -> None: + assert WT_LEN == 2 + + +def test_wt_32bit_is_five() -> None: + assert WT_32BIT == 5 + + +# ── msg ──────────────────────────────────────────────────────────────────────── +# +# msg always writes (no omission for empty content). The caller decides whether +# to guard on None. Wire layout: tag | varint(len(content)) | content. + + +def test_msg_empty_content() -> None: + # An empty sub-message produces tag + varint(0), not b"". + # tag(1, 2) = (1<<3)|2 = 10 = 0x0A; varint(0) = 0x00 + assert msg(1, b"") == b"\x0a\x00" + + +def test_msg_single_byte_content() -> None: + # tag(1, 2) = 0x0A; varint(1) = 0x01; content = 0xAB + assert msg(1, b"\xab") == b"\x0a\x01\xab" + + +def test_msg_two_byte_content() -> None: + # tag(1, 2) = 0x0A; varint(2) = 0x02; content = 0xDE 0xAD + assert msg(1, b"\xde\xad") == b"\x0a\x02\xde\xad" + + +def test_msg_field_number_2() -> None: + # tag(2, 2) = (2<<3)|2 = 18 = 0x12 + assert msg(2, b"\x01") == b"\x12\x01\x01" + + +def test_msg_tag_uses_wt_len() -> None: + result = msg(3, b"\xff") + assert result[0] == (3 << 3) | WT_LEN + + +def test_msg_length_prefix_correct_for_long_content() -> None: + # 128 bytes of content — length prefix needs two varint bytes (0x80 0x01) + content = b"\x00" * 128 + result = msg(1, content) + assert result[0:1] == b"\x0a" + assert result[1:3] == b"\x80\x01" + assert result[3:] == content + + +def test_msg_matches_formula() -> None: + content = b"\x42" * 5 + field = 4 + expected = encode_tag(field, WT_LEN) + encode_varint(len(content)) + content + assert msg(field, content) == expected + + +# ── string ───────────────────────────────────────────────────────────────────── +# +# Omit when empty. Wire layout: tag (wt=2) | varint(len(utf8)) | utf8 bytes. + + +def test_string_empty_is_omitted() -> None: + assert string(1, "") == b"" + + +def test_string_ascii() -> None: + # tag(1, 2) = 0x0A; varint(2) = 0x02; "hi" = 0x68 0x69 + assert string(1, "hi") == b"\x0a\x02hi" + + +def test_string_single_char() -> None: + assert string(1, "A") == b"\x0a\x01A" + + +def test_string_two_byte_utf8() -> None: + # "é" = U+00E9, UTF-8: C3 A9 (2 bytes) + assert string(1, "é") == b"\x0a\x02\xc3\xa9" + + +def test_string_three_byte_utf8() -> None: + # "中" = U+4E2D, UTF-8: E4 B8 AD (3 bytes) + assert string(1, "中") == b"\x0a\x03\xe4\xb8\xad" + + +def test_string_field_number_affects_tag() -> None: + # tag(3, 2) = (3<<3)|2 = 26 = 0x1A + assert string(3, "x") == b"\x1a\x01x" + + +def test_string_tag_uses_wt_len() -> None: + result = string(2, "hello") + assert result[0] == (2 << 3) | WT_LEN + + +def test_string_length_is_byte_count_not_char_count() -> None: + # "日" = 3 UTF-8 bytes; length prefix must be 3, not 1 + result = string(1, "日") + assert result[1] == 3 + + +@mark.parametrize("value", ["a", "hello", "café", "Ünïcödé", "日本語", "\U0001F600"]) +def test_string_matches_formula(value: str) -> None: + utf8 = value.encode("utf-8") + expected = encode_tag(1, WT_LEN) + encode_varint(len(utf8)) + utf8 + assert string(1, value) == expected + + +# ── byt ──────────────────────────────────────────────────────────────────────── +# +# Omit when empty. Wire layout: tag (wt=2) | varint(len) | raw bytes. +# Identical layout to string but no UTF-8 encoding step. + + +def test_byt_empty_is_omitted() -> None: + assert byt(1, b"") == b"" + + +def test_byt_single_byte() -> None: + # tag(1, 2) = 0x0A; varint(1) = 0x01; payload = 0x42 + assert byt(1, b"\x42") == b"\x0a\x01\x42" + + +def test_byt_two_bytes() -> None: + assert byt(1, b"\xde\xad") == b"\x0a\x02\xde\xad" + + +def test_byt_null_bytes_preserved() -> None: + assert byt(1, b"\x00\x00") == b"\x0a\x02\x00\x00" + + +def test_byt_high_bytes_preserved() -> None: + assert byt(1, b"\xff\x80") == b"\x0a\x02\xff\x80" + + +def test_byt_field_number_affects_tag() -> None: + # tag(7, 2) = (7<<3)|2 = 58 = 0x3A + assert byt(7, b"\x01") == b"\x3a\x01\x01" + + +def test_byt_tag_uses_wt_len() -> None: + result = byt(2, b"\x99") + assert result[0] == (2 << 3) | WT_LEN + + +@mark.parametrize("value", [b"\x00", b"\xff", b"hello", b"\x00\x01\x02", b"\x80\x81\x82"]) +def test_byt_matches_formula(value: bytes) -> None: + expected = encode_tag(1, WT_LEN) + encode_varint(len(value)) + value + assert byt(1, value) == expected + + +# ── u64 ──────────────────────────────────────────────────────────────────────── +# +# Omit when zero. Wire layout: tag (wt=0) | varint(value). + + +def test_u64_zero_is_omitted() -> None: + assert u64(1, 0) == b"" + + +def test_u64_one() -> None: + # tag(1, 0) = 0x08; varint(1) = 0x01 + assert u64(1, 1) == b"\x08\x01" + + +def test_u64_127() -> None: + assert u64(1, 127) == b"\x08\x7f" + + +def test_u64_128() -> None: + # varint(128) = 0x80 0x01 + assert u64(1, 128) == b"\x08\x80\x01" + + +def test_u64_field_number_affects_tag() -> None: + # tag(4, 0) = (4<<3)|0 = 32 = 0x20 + assert u64(4, 1) == b"\x20\x01" + + +def test_u64_tag_uses_wt_varint() -> None: + result = u64(5, 99) + assert result[0] == (5 << 3) | WT_VARINT + + +@mark.parametrize("value", [1, 127, 128, 255, 300, 2**32 - 1, 2**32, 2**64 - 1]) +def test_u64_matches_formula(value: int) -> None: + expected = encode_tag(1, WT_VARINT) + encode_varint(value) + assert u64(1, value) == expected + + +# ── bool_field ───────────────────────────────────────────────────────────────── +# +# Omit when False. True encodes as varint 1. Wire type is WT_VARINT. + + +def test_bool_false_is_omitted() -> None: + assert bool_field(1, False) == b"" + + +def test_bool_true() -> None: + # tag(1, 0) = 0x08; varint(1) = 0x01 + assert bool_field(1, True) == b"\x08\x01" + + +def test_bool_field_number_affects_tag() -> None: + # tag(6, 0) = (6<<3)|0 = 48 = 0x30 + assert bool_field(6, True) == b"\x30\x01" + + +def test_bool_tag_uses_wt_varint() -> None: + result = bool_field(2, True) + assert result[0] == (2 << 3) | WT_VARINT + + +def test_bool_true_always_encodes_as_one() -> None: + # bool True always produces a varint 1 regardless of the Python truthiness integer + assert bool_field(1, True) == encode_tag(1, WT_VARINT) + b"\x01" + + +# ── fix32 ────────────────────────────────────────────────────────────────────── +# +# Omit when zero. Wire layout: tag (wt=5) | 4-byte little-endian uint32. + + +def test_fix32_zero_is_omitted() -> None: + assert fix32(1, 0) == b"" + + +def test_fix32_one() -> None: + # tag(1, 5) = (1<<3)|5 = 13 = 0x0D; pack(" None: + # 2^32-1 → all bytes 0xFF + assert fix32(1, 2**32 - 1) == b"\x0d\xff\xff\xff\xff" + + +def test_fix32_field_number_affects_tag() -> None: + # tag(3, 5) = (3<<3)|5 = 29 = 0x1D + assert fix32(3, 1) == b"\x1d\x01\x00\x00\x00" + + +def test_fix32_tag_uses_wt_32bit() -> None: + result = fix32(2, 7) + assert result[0] == (2 << 3) | WT_32BIT + + +def test_fix32_always_four_value_bytes() -> None: + result = fix32(1, 42) + # 1 tag byte + 4 value bytes + assert len(result) == 5 + + +@mark.parametrize("value", [1, 255, 256, 2**16, 2**24, 2**32 - 1]) +def test_fix32_matches_formula(value: int) -> None: + expected = encode_tag(1, WT_32BIT) + pack(" None: + assert fix64(1, 0) == b"" + + +def test_fix64_one() -> None: + # tag(1, 1) = (1<<3)|1 = 9 = 0x09; pack(" None: + assert fix64(1, 2**64 - 1) == b"\x09\xff\xff\xff\xff\xff\xff\xff\xff" + + +def test_fix64_field_number_affects_tag() -> None: + # tag(2, 1) = (2<<3)|1 = 17 = 0x11 + assert fix64(2, 1) == b"\x11\x01\x00\x00\x00\x00\x00\x00\x00" + + +def test_fix64_tag_uses_wt_64bit() -> None: + result = fix64(3, 99) + assert result[0] == (3 << 3) | WT_64BIT + + +def test_fix64_always_eight_value_bytes() -> None: + result = fix64(1, 42) + # 1 tag byte + 8 value bytes + assert len(result) == 9 + + +@mark.parametrize("value", [1, 255, 2**32 - 1, 2**32, 2**63, 2**64 - 1]) +def test_fix64_matches_formula(value: int) -> None: + expected = encode_tag(1, WT_64BIT) + pack(" None: + assert dbl(1, 0.0) == b"" + + +def test_dbl_negative_zero_is_omitted() -> None: + # -0.0 == 0.0 in Python, so it is treated as the proto3 default and omitted. + assert dbl(1, -0.0) == b"" + + +def test_dbl_one() -> None: + # tag(1, 1) = 0x09; pack(" None: + assert dbl(1, -1.0) == b"\x09\x00\x00\x00\x00\x00\x00\xf0\xbf" + + +def test_dbl_infinity_is_encoded() -> None: + result = dbl(1, inf) + assert result == encode_tag(1, WT_64BIT) + pack(" None: + result = dbl(1, nan) + assert result == encode_tag(1, WT_64BIT) + pack(" None: + # tag(4, 1) = (4<<3)|1 = 33 = 0x21 + result = dbl(4, 1.0) + assert result[0] == (4 << 3) | WT_64BIT + + +def test_dbl_always_eight_value_bytes() -> None: + result = dbl(1, 3.14) + assert len(result) == 9 + + +@mark.parametrize("value", [1.0, -1.0, 0.5, 3.14, 1e100, -1e100, inf, -inf]) +def test_dbl_matches_formula(value: float) -> None: + expected = encode_tag(1, WT_64BIT) + pack(" None: + assert opt_dbl(1, None) == b"" + + +def test_opt_dbl_zero_is_NOT_omitted() -> None: + # This is the key difference from dbl: 0.0 is a valid measurement. + result = opt_dbl(1, 0.0) + assert result != b"" + assert result == encode_tag(1, WT_64BIT) + pack(" None: + result = opt_dbl(1, -0.0) + assert result != b"" + + +def test_opt_dbl_one() -> None: + assert opt_dbl(1, 1.0) == b"\x09\x00\x00\x00\x00\x00\x00\xf0\x3f" + + +def test_opt_dbl_negative_one() -> None: + assert opt_dbl(1, -1.0) == b"\x09\x00\x00\x00\x00\x00\x00\xf0\xbf" + + +def test_opt_dbl_infinity_is_encoded() -> None: + result = opt_dbl(1, inf) + assert result == encode_tag(1, WT_64BIT) + pack(" None: + result = opt_dbl(1, nan) + assert result == encode_tag(1, WT_64BIT) + pack(" None: + result = opt_dbl(5, 1.0) + assert result[0] == (5 << 3) | WT_64BIT + + +@mark.parametrize("value", [0.0, -0.0, 1.0, -1.0, 0.5, 3.14, 1e100, inf, -inf]) +def test_opt_dbl_matches_formula(value: float) -> None: + expected = encode_tag(1, WT_64BIT) + pack(" None: + # dbl omits 0.0; opt_dbl does not. + assert dbl(1, 0.0) == b"" + assert opt_dbl(1, 0.0) != b"" + + +# ── sint32 ───────────────────────────────────────────────────────────────────── +# +# Omit when zero. ZigZag encoding: n>=0 → 2n, n<0 → -2n-1. Wire type WT_VARINT. + + +def test_sint32_zero_is_omitted() -> None: + assert sint32(1, 0) == b"" + + +def test_sint32_positive_one() -> None: + # ZigZag(1) = 2; tag(1,0)=0x08; varint(2)=0x02 + assert sint32(1, 1) == b"\x08\x02" + + +def test_sint32_negative_one() -> None: + # ZigZag(-1) = 1; tag(1,0)=0x08; varint(1)=0x01 + assert sint32(1, -1) == b"\x08\x01" + + +def test_sint32_positive_two() -> None: + # ZigZag(2) = 4 + assert sint32(1, 2) == b"\x08\x04" + + +def test_sint32_negative_two() -> None: + # ZigZag(-2) = 3 + assert sint32(1, -2) == b"\x08\x03" + + +def test_sint32_field_number_affects_tag() -> None: + # tag(7, 0) = (7<<3)|0 = 56 = 0x38 + result = sint32(7, 1) + assert result[0] == (7 << 3) | WT_VARINT + + +def test_sint32_tag_uses_wt_varint() -> None: + result = sint32(3, -5) + assert result[0] == (3 << 3) | WT_VARINT + + +@mark.parametrize("value", [1, -1, 2, -2, 127, -128, 150, -150, 2**31 - 1, -(2**31)]) +def test_sint32_zigzag_formula(value: int) -> None: + zigzag = 2 * value if value >= 0 else -2 * value - 1 + expected = encode_tag(1, WT_VARINT) + encode_varint(zigzag) + assert sint32(1, value) == expected + + +# ── packed_uint64 ────────────────────────────────────────────────────────────── +# +# Omit when empty. Wire layout: tag (wt=2) | varint(payload_len) | varint*... + + +def test_packed_uint64_empty_is_omitted() -> None: + assert packed_uint64(1, []) == b"" + + +def test_packed_uint64_single_element() -> None: + # payload = varint(1) = 0x01; len=1 + # tag(1,2)=0x0A; varint(1)=0x01; payload=0x01 + assert packed_uint64(1, [1]) == b"\x0a\x01\x01" + + +def test_packed_uint64_three_elements() -> None: + # payload = varint(1)+varint(2)+varint(3) = 0x01 0x02 0x03; len=3 + assert packed_uint64(1, [1, 2, 3]) == b"\x0a\x03\x01\x02\x03" + + +def test_packed_uint64_element_zero() -> None: + # Zero element encodes as varint(0) = 0x00 — still written (not omitted within payload) + assert packed_uint64(1, [0]) == b"\x0a\x01\x00" + + +def test_packed_uint64_field_number_affects_tag() -> None: + result = packed_uint64(3, [1]) + assert result[0] == (3 << 3) | WT_LEN + + +def test_packed_uint64_tag_uses_wt_len() -> None: + result = packed_uint64(2, [42]) + assert result[0] == (2 << 3) | WT_LEN + + +def test_packed_uint64_length_prefix_covers_payload() -> None: + values = [1, 128, 300] + payload = b"".join(encode_varint(v) for v in values) + result = packed_uint64(1, values) + # byte at index 1 is varint(len(payload)); for short payloads this is 1 byte + assert result == encode_tag(1, WT_LEN) + encode_varint(len(payload)) + payload + + +@mark.parametrize("values", [[1], [0, 1, 2], [127, 128, 255], [2**32 - 1, 2**64 - 1]]) +def test_packed_uint64_matches_formula(values: list) -> None: + payload = b"".join(encode_varint(v) for v in values) + expected = encode_tag(1, WT_LEN) + encode_varint(len(payload)) + payload + assert packed_uint64(1, values) == expected + + +# ── packed_fix64 ─────────────────────────────────────────────────────────────── +# +# Omit when empty. Wire layout: tag (wt=2) | varint(payload_len) | fixed64*... +# Each element is 8 bytes little-endian regardless of value. + + +def test_packed_fix64_empty_is_omitted() -> None: + assert packed_fix64(1, []) == b"" + + +def test_packed_fix64_single_element() -> None: + # payload = pack(" None: + # payload = 16 bytes; varint(16)=0x10 + result = packed_fix64(1, [1, 2]) + expected = b"\x0a\x10" + pack(" None: + result = packed_fix64(1, [0]) + assert result == b"\x0a\x08" + b"\x00" * 8 + + +def test_packed_fix64_field_number_affects_tag() -> None: + result = packed_fix64(5, [1]) + assert result[0] == (5 << 3) | WT_LEN + + +def test_packed_fix64_payload_length_always_multiple_of_8() -> None: + for n in [1, 2, 3, 5]: + result = packed_fix64(1, list(range(n))) + # payload length is n*8; varint(n*8) occupies 1 byte for n<=15 + payload_len = result[1] + assert payload_len == n * 8 + + +@mark.parametrize("values", [[0], [1, 2], [2**32 - 1, 2**64 - 1], [0, 0, 0]]) +def test_packed_fix64_matches_formula(values: list) -> None: + payload = b"".join(pack(" None: + assert packed_double(1, []) == b"" + + +def test_packed_double_single_element() -> None: + # payload = pack(" None: + result = packed_double(1, [1.0, 2.0]) + expected = b"\x0a\x10" + pack("<2d", 1.0, 2.0) + assert result == expected + + +def test_packed_double_zero_element_is_written() -> None: + # 0.0 is valid inside a packed repeated field (only the field itself is omitted when empty) + result = packed_double(1, [0.0]) + assert result == b"\x0a\x08" + pack(" None: + result = packed_double(7, [1.0]) + assert result[0] == (7 << 3) | WT_LEN + + +def test_packed_double_payload_length_always_multiple_of_8() -> None: + for n in [1, 2, 3]: + result = packed_double(1, [float(i) for i in range(n)]) + payload_len = result[1] + assert payload_len == n * 8 + + +@mark.parametrize( + "values", + [[0.0], [1.0, -1.0], [0.5, 3.14, 2.71], [inf, -inf]], +) +def test_packed_double_matches_formula(values: list) -> None: + payload = pack(f"<{len(values)}d", *values) + expected = encode_tag(1, WT_LEN) + encode_varint(len(payload)) + payload + assert packed_double(1, values) == expected diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/test_scalars.py b/opentelemetry-proto/tests/unit/_pyprotobuf/test_scalars.py new file mode 100644 index 00000000000..442c8e51b9c --- /dev/null +++ b/opentelemetry-proto/tests/unit/_pyprotobuf/test_scalars.py @@ -0,0 +1,684 @@ +# tests/test__scalars.py +# +# Tests for all scalar encoders in _scalars.py. The oracle for fixed-width +# types is Python's struct module (independent standard-library implementation). +# Variable-width types (varint-backed) are verified against hand-computed +# expected byte literals derived from the protobuf wire-format spec. + +from math import e, inf, nan, pi, tau +from struct import pack, unpack + +from google.protobuf import descriptor_pb2, descriptor_pool, message_factory +from pytest import mark + +from opentelemetry._proto._pyprotobuf import ( + encode_bool, + encode_bytes, + encode_double, + encode_fixed32, + encode_fixed64, + encode_float, + encode_int, + encode_sfixed32, + encode_sfixed64, + encode_sint32, + encode_sint64, + encode_string, + encode_tag, + encode_uint32, + encode_uint64, + encode_varint, +) + + +# ── encode_uint32 ────────────────────────────────────────────────────────────── + + +def test_uint32_zero() -> None: + assert encode_uint32(0) == b"\x00" + + +def test_uint32_one() -> None: + assert encode_uint32(1) == b"\x01" + + +def test_uint32_max() -> None: + # 2^32-1 encodes as five varint bytes: all low bits set. + assert encode_uint32(2**32 - 1) == b"\xff\xff\xff\xff\x0f" + + +@mark.parametrize("value", [0, 1, 127, 128, 255, 300, 2**16 - 1, 2**16, 2**32 - 1]) +def test_uint32_matches_uint64_encoding(value: int) -> None: + # uint32 and uint64 share the same varint encoding for values in [0, 2^32-1]. + assert encode_uint32(value) == encode_uint64(value) + + +# ── encode_uint64 ────────────────────────────────────────────────────────────── + + +def test_uint64_zero() -> None: + assert encode_uint64(0) == b"\x00" + + +def test_uint64_max() -> None: + assert encode_uint64(2**64 - 1) == b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +@mark.parametrize( + "value", + [0, 1, 127, 128, 2**32 - 1, 2**32, 2**56, 2**63 - 1, 2**63, 2**64 - 1], +) +def test_uint64_is_varint(value: int) -> None: + assert encode_uint64(value) == encode_varint(value) + + +# ── encode_bool ──────────────────────────────────────────────────────────────── + + +def test_bool_false() -> None: + assert encode_bool(False) == b"\x00" + + +def test_bool_true() -> None: + assert encode_bool(True) == b"\x01" + + +def test_bool_truthy_int() -> None: + # Any truthy value must encode as 1, not as the integer itself. + assert encode_bool(42) == b"\x01" + + +def test_bool_falsy_int() -> None: + assert encode_bool(0) == b"\x00" + + +# ── encode_int ───────────────────────────────────────────────────────────────── +# +# int32 / int64: non-negative values encode like varint; negative values are +# sign-extended to 64 bits (two's complement) then encoded as a varint. + + +def test_int_zero() -> None: + assert encode_int(0) == b"\x00" + + +def test_int_positive_small() -> None: + assert encode_int(1) == b"\x01" + + +def test_int_positive_300() -> None: + assert encode_int(300) == b"\xac\x02" + + +def test_int_negative_one() -> None: + # -1 in 64-bit two's complement is 2^64-1, which requires 10 varint bytes. + assert encode_int(-1) == b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +def test_int_negative_two() -> None: + assert encode_int(-2) == b"\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +def test_int_int32_min() -> None: + # -2^31 in 64-bit two's complement: bits 31-63 all set, bits 0-30 clear. + assert encode_int(-(2**31)) == b"\x80\x80\x80\x80\xf8\xff\xff\xff\xff\x01" + + +def test_int_int64_min() -> None: + # -2^63: only bit 63 set. Nine groups of 0x80, then final byte 0x01. + assert encode_int(-(2**63)) == b"\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01" + + +# ── encode_sint32 ────────────────────────────────────────────────────────────── +# +# ZigZag32: n>=0 → 2n, n<0 → -2n-1. Result fits in 32 bits; encoded as varint. + + +def test_sint32_zero() -> None: + # ZigZag(0) = 0 + assert encode_sint32(0) == b"\x00" + + +def test_sint32_negative_one() -> None: + # ZigZag(-1) = 1 + assert encode_sint32(-1) == b"\x01" + + +def test_sint32_positive_one() -> None: + # ZigZag(1) = 2 + assert encode_sint32(1) == b"\x02" + + +def test_sint32_negative_two() -> None: + # ZigZag(-2) = 3 + assert encode_sint32(-2) == b"\x03" + + +def test_sint32_positive_two() -> None: + # ZigZag(2) = 4 + assert encode_sint32(2) == b"\x04" + + +def test_sint32_max() -> None: + # ZigZag(2^31-1) = 2^32-2 = 0xFFFFFFFE → 5-byte varint + assert encode_sint32(2**31 - 1) == b"\xfe\xff\xff\xff\x0f" + + +def test_sint32_min() -> None: + # ZigZag(-2^31) = 2^32-1 = 0xFFFFFFFF → 5-byte varint + assert encode_sint32(-(2**31)) == b"\xff\xff\xff\xff\x0f" + + +@mark.parametrize("value", [0, 1, -1, 2, -2, 150, -150, 2**31 - 1, -(2**31)]) +def test_sint32_zigzag(value: int) -> None: + # Verify ZigZag correctness using the arithmetic definition as oracle. + zigzag = 2 * value if value >= 0 else -2 * value - 1 + assert encode_sint32(value) == encode_varint(zigzag) + + +# ── encode_sint64 ────────────────────────────────────────────────────────────── +# +# ZigZag64: same interleaving as sint32 but over the 64-bit domain. + + +def test_sint64_zero() -> None: + assert encode_sint64(0) == b"\x00" + + +def test_sint64_negative_one() -> None: + assert encode_sint64(-1) == b"\x01" + + +def test_sint64_positive_one() -> None: + assert encode_sint64(1) == b"\x02" + + +def test_sint64_max() -> None: + # ZigZag64(2^63-1) = 2^64-2 = 0xFFFFFFFFFFFFFFFE → 10-byte varint + assert encode_sint64(2**63 - 1) == b"\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +def test_sint64_min() -> None: + # ZigZag64(-2^63) = 2^64-1 = 0xFFFFFFFFFFFFFFFF → 10-byte varint + assert encode_sint64(-(2**63)) == b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +@mark.parametrize( + "value", + [0, 1, -1, 2, -2, 150, -150, 2**31 - 1, -(2**31), 2**32, -(2**32), 2**63 - 1, -(2**63)], +) +def test_sint64_zigzag(value: int) -> None: + zigzag = 2 * value if value >= 0 else -2 * value - 1 + assert encode_sint64(value) == encode_varint(zigzag) + + +# ── encode_float ─────────────────────────────────────────────────────────────── + + +def test_float_zero() -> None: + assert encode_float(0.0) == b"\x00\x00\x00\x00" + + +def test_float_one() -> None: + # IEEE 754 single 1.0 = 0x3F800000; little-endian: 00 00 80 3F + assert encode_float(1.0) == b"\x00\x00\x80\x3f" + + +def test_float_negative_one() -> None: + # IEEE 754 single -1.0 = 0xBF800000; little-endian: 00 00 80 BF + assert encode_float(-1.0) == b"\x00\x00\x80\xbf" + + +def test_float_always_four_bytes() -> None: + assert len(encode_float(0.0)) == 4 + assert len(encode_float(1.0)) == 4 + assert len(encode_float(inf)) == 4 + + +@mark.parametrize( + "value", + [ + 0.0, 1.0, -1.0, 0.5, -0.5, 0.25, 2.0, -2.0, + unpack(" None: + assert encode_float(value) == pack(" None: + assert encode_float(nan) == pack(" None: + assert encode_double(0.0) == b"\x00\x00\x00\x00\x00\x00\x00\x00" + + +def test_double_one() -> None: + # IEEE 754 double 1.0 = 0x3FF0000000000000; little-endian: 00 00 00 00 00 00 F0 3F + assert encode_double(1.0) == b"\x00\x00\x00\x00\x00\x00\xf0\x3f" + + +def test_double_negative_one() -> None: + assert encode_double(-1.0) == b"\x00\x00\x00\x00\x00\x00\xf0\xbf" + + +def test_double_always_eight_bytes() -> None: + assert len(encode_double(0.0)) == 8 + assert len(encode_double(1.0)) == 8 + assert len(encode_double(inf)) == 8 + + +@mark.parametrize( + "value", + [ + 0.0, 1.0, -1.0, 0.5, -0.5, pi, e, tau, + 1e100, -1e100, 5e-324, 1.7976931348623157e308, inf, -inf, + ], +) +def test_double_matches_struct(value: float) -> None: + assert encode_double(value) == pack(" None: + assert encode_double(nan) == pack(" None: + assert encode_fixed32(0) == b"\x00\x00\x00\x00" + + +def test_fixed32_one() -> None: + assert encode_fixed32(1) == b"\x01\x00\x00\x00" + + +def test_fixed32_max() -> None: + assert encode_fixed32(2**32 - 1) == b"\xff\xff\xff\xff" + + +def test_fixed32_always_four_bytes() -> None: + assert len(encode_fixed32(0)) == 4 + assert len(encode_fixed32(2**32 - 1)) == 4 + + +@mark.parametrize("value", [0, 1, 127, 128, 255, 256, 2**16 - 1, 2**16, 2**24, 2**32 - 1]) +def test_fixed32_matches_struct(value: int) -> None: + assert encode_fixed32(value) == pack(" None: + assert encode_sfixed32(0) == b"\x00\x00\x00\x00" + + +def test_sfixed32_negative_one() -> None: + # -1 in 32-bit two's complement is 0xFFFFFFFF; all bytes 0xFF. + assert encode_sfixed32(-1) == b"\xff\xff\xff\xff" + + +def test_sfixed32_min() -> None: + # -2^31 = 0x80000000 little-endian: 00 00 00 80 + assert encode_sfixed32(-(2**31)) == b"\x00\x00\x00\x80" + + +def test_sfixed32_max() -> None: + # 2^31-1 = 0x7FFFFFFF little-endian: FF FF FF 7F + assert encode_sfixed32(2**31 - 1) == b"\xff\xff\xff\x7f" + + +def test_sfixed32_always_four_bytes() -> None: + assert len(encode_sfixed32(0)) == 4 + assert len(encode_sfixed32(-1)) == 4 + + +@mark.parametrize("value", [0, 1, -1, 127, -128, 2**15 - 1, -(2**15), 2**31 - 1, -(2**31)]) +def test_sfixed32_matches_struct(value: int) -> None: + assert encode_sfixed32(value) == pack(" None: + assert encode_fixed64(0) == b"\x00\x00\x00\x00\x00\x00\x00\x00" + + +def test_fixed64_one() -> None: + assert encode_fixed64(1) == b"\x01\x00\x00\x00\x00\x00\x00\x00" + + +def test_fixed64_max() -> None: + assert encode_fixed64(2**64 - 1) == b"\xff\xff\xff\xff\xff\xff\xff\xff" + + +def test_fixed64_always_eight_bytes() -> None: + assert len(encode_fixed64(0)) == 8 + assert len(encode_fixed64(2**64 - 1)) == 8 + + +@mark.parametrize( + "value", [0, 1, 255, 2**16 - 1, 2**32 - 1, 2**32, 2**48, 2**63, 2**64 - 1] +) +def test_fixed64_matches_struct(value: int) -> None: + assert encode_fixed64(value) == pack(" None: + assert encode_sfixed64(0) == b"\x00\x00\x00\x00\x00\x00\x00\x00" + + +def test_sfixed64_negative_one() -> None: + # -1 in 64-bit two's complement: all bytes 0xFF. + assert encode_sfixed64(-1) == b"\xff\xff\xff\xff\xff\xff\xff\xff" + + +def test_sfixed64_min() -> None: + # -2^63 = 0x8000000000000000 little-endian: 00 00 00 00 00 00 00 80 + assert encode_sfixed64(-(2**63)) == b"\x00\x00\x00\x00\x00\x00\x00\x80" + + +def test_sfixed64_max() -> None: + # 2^63-1 = 0x7FFFFFFFFFFFFFFF little-endian: FF FF FF FF FF FF FF 7F + assert encode_sfixed64(2**63 - 1) == b"\xff\xff\xff\xff\xff\xff\xff\x7f" + + +def test_sfixed64_always_eight_bytes() -> None: + assert len(encode_sfixed64(0)) == 8 + assert len(encode_sfixed64(-1)) == 8 + + +@mark.parametrize( + "value", + [0, 1, -1, 127, -128, 2**31 - 1, -(2**31), 2**32, -(2**32), 2**63 - 1, -(2**63)], +) +def test_sfixed64_matches_struct(value: int) -> None: + assert encode_sfixed64(value) == pack(" None: + assert encode_string("") == b"\x00" + + +def test_string_ascii_single_char() -> None: + assert encode_string("A") == b"\x01A" + + +def test_string_ascii_word() -> None: + assert encode_string("hi") == b"\x02hi" + + +def test_string_two_byte_utf8() -> None: + # "é" = U+00E9, UTF-8: 0xC3 0xA9 (2 bytes, not 1 character) + assert encode_string("é") == b"\x02\xc3\xa9" + + +def test_string_three_byte_utf8() -> None: + # "中" = U+4E2D, UTF-8: 0xE4 0xB8 0xAD (3 bytes) + assert encode_string("中") == b"\x03\xe4\xb8\xad" + + +def test_string_length_counts_bytes_not_chars() -> None: + # "日本語" is 3 characters but 9 UTF-8 bytes. + value = "日本語" + result = encode_string(value) + assert result[0] == 9 # byte count, not char count + assert result[1:] == value.encode("utf-8") + + +def test_string_two_byte_length_prefix() -> None: + # A string of 128 ASCII characters needs a 2-byte varint length prefix. + result = encode_string("a" * 128) + assert result[:2] == b"\x80\x01" + assert result[2:] == b"a" * 128 + + +@mark.parametrize( + "value", + ["", "a", "hello", "café", "Ünïcödé", "日本語", "中文", "\U0001F600", "hello 世界"], +) +def test_string_length_prefix_matches_utf8_bytecount(value: str) -> None: + utf8 = value.encode("utf-8") + result = encode_string(value) + assert result == encode_varint(len(utf8)) + utf8 + + +# ── encode_bytes ─────────────────────────────────────────────────────────────── + + +def test_bytes_empty() -> None: + assert encode_bytes(b"") == b"\x00" + + +def test_bytes_single_byte() -> None: + assert encode_bytes(b"\x42") == b"\x01\x42" + + +def test_bytes_high_bytes() -> None: + # Values >= 0x80 must pass through verbatim. + assert encode_bytes(b"\xff\x80") == b"\x02\xff\x80" + + +def test_bytes_null_bytes() -> None: + assert encode_bytes(b"\x00\x00\x00") == b"\x03\x00\x00\x00" + + +def test_bytes_all_byte_values() -> None: + # 256-byte payload; length prefix is encode_varint(256) = b'\x80\x02' + payload = bytes(range(256)) + result = encode_bytes(payload) + assert result[:2] == b"\x80\x02" + assert result[2:] == payload + + +def test_bytes_two_byte_length_prefix() -> None: + value = b"\xab" * 128 + result = encode_bytes(value) + assert result[:2] == b"\x80\x01" + assert result[2:] == value + + +@mark.parametrize( + "value", + [b"", b"\x00", b"\xff", b"hello", b"\x00\x01\x02\x03", b"\x80\x81\x82", b"\xfe\xff"], +) +def test_bytes_length_prefix_matches_payload_length(value: bytes) -> None: + result = encode_bytes(value) + assert result == encode_varint(len(value)) + value + + +# ── oracle — byte-for-byte comparison with google.protobuf ──────────────────── +# +# A proto2 message with one optional field per scalar type is used as the +# oracle. proto2 is chosen because it serialises every field that has been +# explicitly set, including fields whose value equals the type default (0, +# False, b"", ""), which proto3 would silently omit. +# +# For each function under test the pattern is: +# expected = encode_tag(field_number, wire_type) + encode_X(value) +# assert _ScalarMessage(**{field_name: value}).SerializeToString() == expected +# +# Field number assignments: +_F_UINT32 = 1 # wt 0 +_F_UINT64 = 2 # wt 0 +_F_BOOL = 3 # wt 0 +_F_INT32 = 4 # wt 0 +_F_INT64 = 5 # wt 0 +_F_SINT32 = 6 # wt 0 +_F_SINT64 = 7 # wt 0 +_F_FLOAT = 8 # wt 5 +_F_DOUBLE = 9 # wt 1 +_F_FIXED32 = 10 # wt 5 +_F_SFIXED32 = 11 # wt 5 +_F_FIXED64 = 12 # wt 1 +_F_SFIXED64 = 13 # wt 1 +_F_STRING = 14 # wt 2 +_F_BYTES = 15 # wt 2 + +_WT_VARINT = 0 +_WT_64BIT = 1 +_WT_LEN = 2 +_WT_32BIT = 5 + + +def _build_scalar_message_class(): + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = "opentelemetry_pyproto_test_scalars.proto" + file_proto.syntax = "proto2" + msg_proto = file_proto.message_type.add() + msg_proto.name = "ScalarMessage" + + T = descriptor_pb2.FieldDescriptorProto + + def _add(name, number, type_id): + f = msg_proto.field.add() + f.name = name + f.number = number + f.type = type_id + f.label = T.LABEL_OPTIONAL + + _add("uint32_field", _F_UINT32, T.TYPE_UINT32) + _add("uint64_field", _F_UINT64, T.TYPE_UINT64) + _add("bool_field", _F_BOOL, T.TYPE_BOOL) + _add("int32_field", _F_INT32, T.TYPE_INT32) + _add("int64_field", _F_INT64, T.TYPE_INT64) + _add("sint32_field", _F_SINT32, T.TYPE_SINT32) + _add("sint64_field", _F_SINT64, T.TYPE_SINT64) + _add("float_field", _F_FLOAT, T.TYPE_FLOAT) + _add("double_field", _F_DOUBLE, T.TYPE_DOUBLE) + _add("fixed32_field", _F_FIXED32, T.TYPE_FIXED32) + _add("sfixed32_field", _F_SFIXED32, T.TYPE_SFIXED32) + _add("fixed64_field", _F_FIXED64, T.TYPE_FIXED64) + _add("sfixed64_field", _F_SFIXED64, T.TYPE_SFIXED64) + _add("string_field", _F_STRING, T.TYPE_STRING) + _add("bytes_field", _F_BYTES, T.TYPE_BYTES) + + pool = descriptor_pool.DescriptorPool() + pool.Add(file_proto) + return message_factory.GetMessageClass(pool.FindMessageTypeByName("ScalarMessage")) + + +_ScalarMessage = _build_scalar_message_class() + + +def _s(field_name: str, value) -> bytes: + return _ScalarMessage(**{field_name: value}).SerializeToString() + + +# ── encode_uint32 oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, 127, 128, 255, 300, 2**16, 2**32 - 1]) +def test_encode_uint32_matches_protobuf(value: int) -> None: + assert _s("uint32_field", value) == encode_tag(_F_UINT32, _WT_VARINT) + encode_uint32(value) + + +# ── encode_uint64 oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, 127, 128, 2**32 - 1, 2**32, 2**63, 2**64 - 1]) +def test_encode_uint64_matches_protobuf(value: int) -> None: + assert _s("uint64_field", value) == encode_tag(_F_UINT64, _WT_VARINT) + encode_uint64(value) + + +# ── encode_bool oracle ───────────────────────────────────────────────────────── + +@mark.parametrize("value", [False, True]) +def test_encode_bool_matches_protobuf(value: bool) -> None: + assert _s("bool_field", value) == encode_tag(_F_BOOL, _WT_VARINT) + encode_bool(value) + + +# ── encode_int oracle (int32 / int64) ───────────────────────────────────────── + +@mark.parametrize("value", [0, 1, 127, 128, 2**31 - 1, -1, -2, -128, -(2**31)]) +def test_encode_int_int32_matches_protobuf(value: int) -> None: + assert _s("int32_field", value) == encode_tag(_F_INT32, _WT_VARINT) + encode_int(value) + + +@mark.parametrize("value", [0, 1, 2**63 - 1, -1, -(2**63)]) +def test_encode_int_int64_matches_protobuf(value: int) -> None: + assert _s("int64_field", value) == encode_tag(_F_INT64, _WT_VARINT) + encode_int(value) + + +# ── encode_sint32 oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, -1, 2, -2, 150, -150, 2**31 - 1, -(2**31)]) +def test_encode_sint32_matches_protobuf(value: int) -> None: + assert _s("sint32_field", value) == encode_tag(_F_SINT32, _WT_VARINT) + encode_sint32(value) + + +# ── encode_sint64 oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, -1, 150, -150, 2**63 - 1, -(2**63)]) +def test_encode_sint64_matches_protobuf(value: int) -> None: + assert _s("sint64_field", value) == encode_tag(_F_SINT64, _WT_VARINT) + encode_sint64(value) + + +# ── encode_float oracle ──────────────────────────────────────────────────────── + +@mark.parametrize("value", [0.0, 1.0, -1.0, 0.5, inf, -inf]) +def test_encode_float_matches_protobuf(value: float) -> None: + assert _s("float_field", value) == encode_tag(_F_FLOAT, _WT_32BIT) + encode_float(value) + + +# ── encode_double oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", [0.0, 1.0, -1.0, pi, 1e100, inf, -inf]) +def test_encode_double_matches_protobuf(value: float) -> None: + assert _s("double_field", value) == encode_tag(_F_DOUBLE, _WT_64BIT) + encode_double(value) + + +# ── encode_fixed32 oracle ────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, 255, 2**16, 2**32 - 1]) +def test_encode_fixed32_matches_protobuf(value: int) -> None: + assert _s("fixed32_field", value) == encode_tag(_F_FIXED32, _WT_32BIT) + encode_fixed32(value) + + +# ── encode_sfixed32 oracle ───────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, -1, 2**31 - 1, -(2**31)]) +def test_encode_sfixed32_matches_protobuf(value: int) -> None: + assert _s("sfixed32_field", value) == encode_tag(_F_SFIXED32, _WT_32BIT) + encode_sfixed32(value) + + +# ── encode_fixed64 oracle ────────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, 2**32, 2**64 - 1]) +def test_encode_fixed64_matches_protobuf(value: int) -> None: + assert _s("fixed64_field", value) == encode_tag(_F_FIXED64, _WT_64BIT) + encode_fixed64(value) + + +# ── encode_sfixed64 oracle ───────────────────────────────────────────────────── + +@mark.parametrize("value", [0, 1, -1, 2**63 - 1, -(2**63)]) +def test_encode_sfixed64_matches_protobuf(value: int) -> None: + assert _s("sfixed64_field", value) == encode_tag(_F_SFIXED64, _WT_64BIT) + encode_sfixed64(value) + + +# ── encode_string oracle ─────────────────────────────────────────────────────── + +@mark.parametrize("value", ["", "a", "hello", "café", "日本語", "\U0001F600", "x" * 128]) +def test_encode_string_matches_protobuf(value: str) -> None: + assert _s("string_field", value) == encode_tag(_F_STRING, _WT_LEN) + encode_string(value) + + +# ── encode_bytes oracle ──────────────────────────────────────────────────────── + +@mark.parametrize("value", [b"", b"\x00", b"\xff", b"hello", b"\x80\x81\x82", b"\xab" * 128]) +def test_encode_bytes_matches_protobuf(value: bytes) -> None: + assert _s("bytes_field", value) == encode_tag(_F_BYTES, _WT_LEN) + encode_bytes(value) diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/test_tag.py b/opentelemetry-proto/tests/unit/_pyprotobuf/test_tag.py new file mode 100644 index 00000000000..c4c31a816a3 --- /dev/null +++ b/opentelemetry-proto/tests/unit/_pyprotobuf/test_tag.py @@ -0,0 +1,152 @@ +# tests/test__tag.py +# +# encode_tag(field_number, wire_type) encodes (field_number << 3) | wire_type +# as a varint. These tests verify the formula and the varint encoding of the +# resulting tag integer, using hand-computed expected bytes. +# +# Wire type constants: +# 0 VARINT — int32, int64, uint32, uint64, bool, enum +# 1 64BIT — fixed64, sfixed64, double +# 2 LEN — string, bytes, embedded messages, packed repeated +# 5 32BIT — fixed32, sfixed32, float + +from google.protobuf import descriptor_pb2, descriptor_pool, message_factory +from pytest import mark + +from opentelemetry._proto._pyprotobuf import encode_tag, encode_varint + + +@mark.parametrize( + ("field_number", "wire_type"), + [ + (1, 0), # tag = 8 → 1-byte varint + (1, 1), # tag = 9 → 1-byte varint + (1, 2), # tag = 10 → 1-byte varint + (1, 5), # tag = 13 → 1-byte varint + (15, 0), # tag = 120 → last 1-byte tag for wire type 0 + (16, 0), # tag = 128 → first 2-byte tag for wire type 0 + (150, 0), # tag = 1200 → 2-byte varint + (1024, 0), # tag = 8192 → 2-byte varint + (1048576, 2), # tag = 8388610 → 4-byte varint + ], +) +def test_encode_tag_matches_formula(field_number: int, wire_type: int) -> None: + # encode_tag must produce the same bytes as encoding the tag integer directly. + tag_int = (field_number << 3) | wire_type + assert encode_tag(field_number, wire_type) == encode_varint(tag_int) + + +def test_field_1_wire_type_0() -> None: + # (1 << 3) | 0 = 8 → single byte 0x08 + assert encode_tag(1, 0) == b"\x08" + + +def test_field_1_wire_type_2() -> None: + # (1 << 3) | 2 = 10 → single byte 0x0A + assert encode_tag(1, 2) == b"\x0a" + + +def test_field_15_wire_type_0() -> None: + # (15 << 3) | 0 = 120 → single byte 0x78 (last 1-byte wt-0 tag) + assert encode_tag(15, 0) == b"\x78" + + +def test_field_16_wire_type_0() -> None: + # (16 << 3) | 0 = 128 → two bytes 0x80 0x01 (first 2-byte wt-0 tag) + assert encode_tag(16, 0) == b"\x80\x01" + + +def test_field_2_wire_type_1() -> None: + # (2 << 3) | 1 = 17 → single byte 0x11 + assert encode_tag(2, 1) == b"\x11" + + +def test_field_3_wire_type_5() -> None: + # (3 << 3) | 5 = 29 → single byte 0x1D + assert encode_tag(3, 5) == b"\x1d" + + +# ── oracle — byte-for-byte comparison with google.protobuf ──────────────────── +# +# A proto2 message with fields at distinct positions and wire types is used as +# the oracle. For each (field_number, wire_type) pair we serialise a message +# with only that field set and verify that encode_tag(field_number, wire_type) +# matches the first len(tag) bytes of the serialised output. +# +# Field number / tag varint length: +# field 1, wt 0: tag = 8 → 1-byte (last before multi-byte for wt 0) +# field 10, wt 1: tag = 81 → 1-byte (wire type 1) +# field 11, wt 2: tag = 90 → 1-byte (wire type 2) +# field 12, wt 5: tag = 101 → 1-byte (wire type 5) +# field 15, wt 0: tag = 120 → 1-byte (last 1-byte wt-0 tag) +# field 16, wt 0: tag = 128 → 2-byte (first 2-byte wt-0 tag) +# field 150, wt 0: tag = 1200 → 2-byte +# field 1048576, wt 2: tag = 8388610 → 4-byte + +_WT_VARINT = 0 +_WT_64BIT = 1 +_WT_LEN = 2 +_WT_32BIT = 5 + +_F_UINT64 = 1 +_F_FIXED64 = 10 +_F_STRING = 11 +_F_FIXED32 = 12 +_F_UINT64_15 = 15 +_F_UINT64_16 = 16 +_F_UINT64_150 = 150 +_F_STRING_BIG = 1048576 + + +def _build_tag_message_class(): + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = "opentelemetry_pyproto_test_tag.proto" + file_proto.syntax = "proto2" + msg_proto = file_proto.message_type.add() + msg_proto.name = "TagMessage" + + T = descriptor_pb2.FieldDescriptorProto + + def _add(name, number, type_id): + f = msg_proto.field.add() + f.name = name + f.number = number + f.type = type_id + f.label = T.LABEL_OPTIONAL + + _add("uint64_field", _F_UINT64, T.TYPE_UINT64) + _add("fixed64_field", _F_FIXED64, T.TYPE_FIXED64) + _add("string_field", _F_STRING, T.TYPE_STRING) + _add("fixed32_field", _F_FIXED32, T.TYPE_FIXED32) + _add("uint64_field_15", _F_UINT64_15, T.TYPE_UINT64) + _add("uint64_field_16", _F_UINT64_16, T.TYPE_UINT64) + _add("uint64_field_150", _F_UINT64_150, T.TYPE_UINT64) + _add("string_field_big", _F_STRING_BIG, T.TYPE_STRING) + + pool = descriptor_pool.DescriptorPool() + pool.Add(file_proto) + return message_factory.GetMessageClass(pool.FindMessageTypeByName("TagMessage")) + + +_TagMessage = _build_tag_message_class() + + +@mark.parametrize( + ("field_name", "field_number", "wire_type", "field_value"), + [ + ("uint64_field", _F_UINT64, _WT_VARINT, 1 ), + ("fixed64_field", _F_FIXED64, _WT_64BIT, 1 ), + ("string_field", _F_STRING, _WT_LEN, "x"), + ("fixed32_field", _F_FIXED32, _WT_32BIT, 1 ), + ("uint64_field_15", _F_UINT64_15, _WT_VARINT, 1 ), + ("uint64_field_16", _F_UINT64_16, _WT_VARINT, 1 ), + ("uint64_field_150", _F_UINT64_150, _WT_VARINT, 1 ), + ("string_field_big", _F_STRING_BIG, _WT_LEN, "x"), + ], +) +def test_encode_tag_matches_protobuf( + field_name: str, field_number: int, wire_type: int, field_value +) -> None: + serialized = _TagMessage(**{field_name: field_value}).SerializeToString() + tag = encode_tag(field_number, wire_type) + assert serialized[: len(tag)] == tag diff --git a/opentelemetry-proto/tests/unit/_pyprotobuf/test_varint.py b/opentelemetry-proto/tests/unit/_pyprotobuf/test_varint.py new file mode 100644 index 00000000000..d4f409d9f93 --- /dev/null +++ b/opentelemetry-proto/tests/unit/_pyprotobuf/test_varint.py @@ -0,0 +1,115 @@ +# tests/test__varint.py + +from google.protobuf import descriptor_pb2, descriptor_pool, message_factory +from pytest import mark, raises + +from opentelemetry._proto._pyprotobuf import encode_tag, encode_varint + + +def test_zero() -> None: + assert encode_varint(0) == b"\x00" + + +def test_single_byte_max() -> None: + assert encode_varint(127) == b"\x7f" + + +def test_first_two_byte_value() -> None: + assert encode_varint(128) == b"\x80\x01" + + +def test_150() -> None: + assert encode_varint(150) == b"\x96\x01" + + +def test_300() -> None: + assert encode_varint(300) == b"\xac\x02" + + +def test_uint32_max() -> None: + assert encode_varint(2**32 - 1) == b"\xff\xff\xff\xff\x0f" + + +def test_uint64_max() -> None: + assert encode_varint(2**64 - 1) == b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01" + + +def test_rejects_negative_values() -> None: + with raises(ValueError, match="varint values must be non-negative"): + encode_varint(-1) + + +def test_two_byte_boundary_low() -> None: + # 16383 = 0x3FFF — the largest value that fits in two varint bytes. + # 7-bit groups: 0x7F (lower), 0x7F (upper, no continuation bit). + assert encode_varint(16_383) == b"\xff\x7f" + + +def test_three_byte_boundary_low() -> None: + # 16384 = 0x4000 — the first value that requires three varint bytes. + assert encode_varint(16_384) == b"\x80\x80\x01" + + +def test_one() -> None: + # 1 fits in a single byte; no continuation bit needed. + assert encode_varint(1) == b"\x01" + + +# ── oracle — byte-for-byte comparison with google.protobuf ──────────────────── +# +# A proto2 message with a single uint64 field is used as the oracle. proto2 +# is chosen because it serialises the field even when its value is the type +# default (0). uint64 covers the full [0, 2^64-1] varint range. +# +# The serialised message for a single field is exactly: +# tag (varint) + value (varint) +# +# Asserting that encode_tag(field, wt) + encode_varint(value) equals the +# serialised message verifies that our varint encoder produces the exact same +# bytes as google.protobuf for every tested value. + +_FIELD = 1 +_WT = 0 # wire type 0 — varint + + +def _build_varint_message_class(): + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = "opentelemetry_pyproto_test_varint.proto" + file_proto.syntax = "proto2" + msg_proto = file_proto.message_type.add() + msg_proto.name = "VarintMessage" + f = msg_proto.field.add() + f.name = "uint64_field" + f.number = _FIELD + f.type = descriptor_pb2.FieldDescriptorProto.TYPE_UINT64 + f.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + pool = descriptor_pool.DescriptorPool() + pool.Add(file_proto) + return message_factory.GetMessageClass(pool.FindMessageTypeByName("VarintMessage")) + + +_VarintMessage = _build_varint_message_class() + + +@mark.parametrize( + "value", + [ + 0, + 1, + 127, + 128, + 150, + 300, + 16_383, + 16_384, + 2**16, + 2**28, + 2**32 - 1, + 2**32, + 2**63 - 1, + 2**64 - 1, + ], +) +def test_encode_varint_matches_protobuf(value: int) -> None: + expected = encode_tag(_FIELD, _WT) + encode_varint(value) + assert _VarintMessage(uint64_field=value).SerializeToString() == expected diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 17c91f0e807..1efbd186d74 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -402,15 +402,25 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER` provides `requests.Session` for the HTTP OTLP Log exporter. -Entry point providers should implement the following: +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER` provides a `urllib.request.OpenerDirector` for the HTTP OTLP Log exporter. +Entry point providers should return a :class:`urllib.request.OpenerDirector` +(for example one built with :func:`urllib.request.build_opener`), which the +exporter uses to send OTLP/HTTP requests: .. code-block:: python - import requests + from urllib.request import HTTPSHandler, build_opener # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. - def request_session_provder() -> requests.Session: + def opener_provider() -> "urllib.request.OpenerDirector": + return build_opener(HTTPSHandler()) + +.. deprecated:: + Returning a ``requests.Session`` (or passing one via the exporter's + ``session`` argument) is still accepted for backwards compatibility but + is deprecated and will be removed in a future release; it now emits a + ``DeprecationWarning``. ``requests`` is no longer a dependency of the + exporter. Note: This environment variable is experimental and subject to change. """ @@ -420,15 +430,25 @@ def request_session_provder() -> requests.Session: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER` provides `requests.Session` for all HTTP OTLP exporters. -Entry point providers should implement the following: +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER` provides a `urllib.request.OpenerDirector` for all HTTP OTLP exporters. +Entry point providers should return a :class:`urllib.request.OpenerDirector` +(for example one built with :func:`urllib.request.build_opener`), which the +exporter uses to send OTLP/HTTP requests: .. code-block:: python - import requests + from urllib.request import HTTPSHandler, build_opener # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. - def request_session_provder() -> requests.Session: + def opener_provider() -> "urllib.request.OpenerDirector": + return build_opener(HTTPSHandler()) + +.. deprecated:: + Returning a ``requests.Session`` (or passing one via the exporter's + ``session`` argument) is still accepted for backwards compatibility but + is deprecated and will be removed in a future release; it now emits a + ``DeprecationWarning``. ``requests`` is no longer a dependency of the + exporter. Note: This environment variable is experimental and subject to change. """ @@ -456,15 +476,25 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER` provides `requests.Session` to the HTTP OTLP Span exporter. -Entry point providers should implement the following: +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER` provides a `urllib.request.OpenerDirector` to the HTTP OTLP Span exporter. +Entry point providers should return a :class:`urllib.request.OpenerDirector` +(for example one built with :func:`urllib.request.build_opener`), which the +exporter uses to send OTLP/HTTP requests: .. code-block:: python - import requests + from urllib.request import HTTPSHandler, build_opener # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. - def request_session_provder() -> requests.Session: + def opener_provider() -> "urllib.request.OpenerDirector": + return build_opener(HTTPSHandler()) + +.. deprecated:: + Returning a ``requests.Session`` (or passing one via the exporter's + ``session`` argument) is still accepted for backwards compatibility but + is deprecated and will be removed in a future release; it now emits a + ``DeprecationWarning``. ``requests`` is no longer a dependency of the + exporter. Note: This environment variable is experimental and subject to change. """ @@ -492,15 +522,25 @@ def channel_credential_provider() -> grpc.ChannelCredentials: """ .. envvar:: OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER -The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER` provides `requests.Session` to the HTTP OTLP Metric exporter. -Entry point providers should implement the following: +The :envvar:`OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER` provides a `urllib.request.OpenerDirector` to the HTTP OTLP Metric exporter. +Entry point providers should return a :class:`urllib.request.OpenerDirector` +(for example one built with :func:`urllib.request.build_opener`), which the +exporter uses to send OTLP/HTTP requests: .. code-block:: python - import requests + from urllib.request import HTTPSHandler, build_opener # Add a reference to this function under the `opentelemetry_otlp_credential_provider` entry point. - def request_session_provder() -> requests.Session: + def opener_provider() -> "urllib.request.OpenerDirector": + return build_opener(HTTPSHandler()) + +.. deprecated:: + Returning a ``requests.Session`` (or passing one via the exporter's + ``session`` argument) is still accepted for backwards compatibility but + is deprecated and will be removed in a future release; it now emits a + ``DeprecationWarning``. ``requests`` is no longer a dependency of the + exporter. Note: This environment variable is experimental and subject to change. """