diff --git a/.gitlab/scripts/build-wheel-helpers.sh b/.gitlab/scripts/build-wheel-helpers.sh index 64184624255..946a7522e36 100755 --- a/.gitlab/scripts/build-wheel-helpers.sh +++ b/.gitlab/scripts/build-wheel-helpers.sh @@ -331,7 +331,7 @@ test_wheel() { uv pip freeze echo "=== site-packages contents ===" "${VENV_PATH}/bin/python" -c "import site; print('site-packages:', site.getsitepackages())" - ls -la "${VENV_PATH}/lib/"*/site-packages/ | head -30 + ls -la "${VENV_PATH}/lib/"*/site-packages/ | sed -n '1,30p' echo "=== Testing direct import ===" "${VENV_PATH}/bin/python" -c "import ddtrace; print('✓ ddtrace import successful')" || echo "✗ ddtrace import failed" diff --git a/ddtrace/internal/openfeature/_flageval_metrics.py b/ddtrace/internal/openfeature/_flageval_metrics.py index 2c54eccf93c..03bc3b53c8f 100644 --- a/ddtrace/internal/openfeature/_flageval_metrics.py +++ b/ddtrace/internal/openfeature/_flageval_metrics.py @@ -66,7 +66,7 @@ def __init__(self) -> None: return try: - from opentelemetry import metrics as otel_metrics + import opentelemetry.metrics as otel_metrics # Get the global meter provider (set up by ddtrace OTel metrics infrastructure) meter = otel_metrics.get_meter(METER_NAME) diff --git a/ddtrace/internal/opentelemetry/exporters/__init__.py b/ddtrace/internal/opentelemetry/exporters/__init__.py new file mode 100644 index 00000000000..fae7f18adbf --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/__init__.py @@ -0,0 +1,139 @@ +from importlib import import_module +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version +from typing import Any + +from ddtrace.internal.logger import get_logger +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import GRPCLogsExporter +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import GRPCMetricsExporter +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import HTTPLogsExporter +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import HTTPMetricsExporter + + +log = get_logger(__name__) + +_MINIMUM_SUPPORTED_VERSION = (1, 15, 0) +_MINIMUM_LIGHTWEIGHT_VERSION = (1, 18, 0) + + +def get_metrics_exporter(protocol: str) -> type[Any] | None: + """Return the metrics exporter class for protocol, if one is available.""" + try: + exporter_version = _exporter_version() + if not _is_supported(exporter_version): + log.warning( + "OpenTelemetry Metrics exporter for %s requires version %r or higher, but found version %r.", + protocol, + _MINIMUM_SUPPORTED_VERSION, + exporter_version, + ) + return None + + if protocol == "grpc": + if _supports_lightweight_exporter(exporter_version): + try: + from ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter import ( + OTLPMetricExporter as GRPCMetricExporter, + ) + + return GRPCMetricExporter + except ImportError: + pass + import_module("opentelemetry.exporter.otlp.proto.grpc.metric_exporter") + return GRPCMetricsExporter + + if protocol == "http/protobuf": + if _supports_lightweight_exporter(exporter_version): + from ddtrace.internal.opentelemetry.exporters.http_metric_exporter import ( + OTLPMetricExporter as HTTPMetricExporter, + ) + + return HTTPMetricExporter + import_module("opentelemetry.exporter.otlp.proto.http.metric_exporter") + return HTTPMetricsExporter + + log.warning( + "OpenTelemetry Metrics exporter protocol '%s' is not supported. Use 'grpc' or 'http/protobuf'.", + protocol, + ) + except ImportError as e: + log.warning( + "OpenTelemetry Metrics exporter for %s is not available. " + "Install ddtrace[opentelemetry] before enabling OpenTelemetry Metrics support: %s", + protocol, + str(e), + ) + return None + + +def get_logs_exporter(protocol: str) -> type[Any] | None: + """Return the logs exporter class for protocol, if one is available.""" + try: + exporter_version = _exporter_version() + if not _is_supported(exporter_version): + log.warning( + "OpenTelemetry Logs exporter for %s requires version %r or higher, but found version %r. " + "Please upgrade the appropriate opentelemetry-exporter package.", + protocol, + _MINIMUM_SUPPORTED_VERSION, + exporter_version, + ) + return None + + if protocol == "grpc": + if _supports_lightweight_exporter(exporter_version): + try: + from ddtrace.internal.opentelemetry.exporters.grpclib_log_exporter import ( + OTLPLogExporter as GRPCLogExporter, + ) + + return GRPCLogExporter + except ImportError: + pass + import_module("opentelemetry.exporter.otlp.proto.grpc._log_exporter") + return GRPCLogsExporter + + if protocol == "http/protobuf": + if _supports_lightweight_exporter(exporter_version): + from ddtrace.internal.opentelemetry.exporters.http_log_exporter import ( + OTLPLogExporter as HTTPLogExporter, + ) + + return HTTPLogExporter + import_module("opentelemetry.exporter.otlp.proto.http._log_exporter") + return HTTPLogsExporter + + log.warning( + "OpenTelemetry Logs exporter protocol '%s' is not supported. Use 'grpc' or 'http/protobuf'.", + protocol, + ) + except ImportError as e: + log.warning( + "OpenTelemetry Logs exporter for %s is not available. " + "Install ddtrace[opentelemetry] before enabling OpenTelemetry Logs support: %s", + protocol, + str(e), + ) + return None + + +def _exporter_version() -> str: + try: + return version("opentelemetry-exporter-otlp-proto-common") + except PackageNotFoundError: + from opentelemetry.exporter.otlp.proto.http.version import __version__ + + return str(__version__) + + +def _is_supported(exporter_version: str) -> bool: + return _parse_version(exporter_version) >= _MINIMUM_SUPPORTED_VERSION + + +def _supports_lightweight_exporter(exporter_version: str) -> bool: + return _parse_version(exporter_version) >= _MINIMUM_LIGHTWEIGHT_VERSION + + +def _parse_version(exporter_version: str) -> tuple[int, int, int]: + major, minor, patch = exporter_version.split(".")[:3] + return int(major), int(minor), int(patch) diff --git a/ddtrace/internal/opentelemetry/exporters/exporter_telemetry.py b/ddtrace/internal/opentelemetry/exporters/exporter_telemetry.py new file mode 100644 index 00000000000..cfbd8fbf84d --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/exporter_telemetry.py @@ -0,0 +1,103 @@ +import logging +from typing import Any + +from ddtrace.internal.telemetry import telemetry_writer +from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE + + +_ENCODING = "protobuf" +log = logging.getLogger(__name__) + + +def record_log_records(count: int, protocol: str) -> None: + telemetry_writer.add_count_metric( + TELEMETRY_NAMESPACE.TRACERS, + "otel.log_records", + count, + (("protocol", protocol), ("encoding", _ENCODING)), + ) + + +def record_metrics_export_attempt(protocol: str) -> None: + # TODO: Count the number of unique metrics streams in this export. + telemetry_writer.add_count_metric( + TELEMETRY_NAMESPACE.TRACERS, + "otel.metrics_export_attempts", + 1, + (("protocol", protocol), ("encoding", _ENCODING)), + ) + + +def record_metrics_export_result(result: Any, protocol: str) -> None: + if result.value not in (0, 1): + return + + telemetry_writer.add_count_metric( + TELEMETRY_NAMESPACE.TRACERS, + "otel.metrics_export_successes" if result.value == 0 else "otel.metrics_export_failures", + 1, + (("protocol", protocol), ("encoding", _ENCODING)), + ) + + +class _ExporterProxy: + _exporter: Any + + def __getattr__(self, name: str) -> Any: + return getattr(self._exporter, name) + + +class _MetricsExporterProxy(_ExporterProxy): + _protocol: str + + def export(self, metrics_data: Any, timeout_millis: Any = 10_000, *args: Any, **kwargs: Any) -> Any: + record_metrics_export_attempt(self._protocol) + log.debug("Exporting OpenTelemetry Metrics with %s protocol and protobuf encoding", self._protocol) + result = self._exporter.export(metrics_data, timeout_millis, *args, **kwargs) + record_metrics_export_result(result, self._protocol) + return result + + +class GRPCMetricsExporter(_MetricsExporterProxy): + _protocol = "grpc" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter + + self._exporter = OTLPMetricExporter(*args, **kwargs) + + +class HTTPMetricsExporter(_MetricsExporterProxy): + _protocol = "http" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter + + self._exporter = OTLPMetricExporter(*args, **kwargs) + + +class _LogsExporterProxy(_ExporterProxy): + _protocol: str + + def export(self, batch: Any, *args: Any, **kwargs: Any) -> Any: + record_log_records(len(batch), self._protocol) + log.debug("Exporting %d OpenTelemetry Logs with %s protocol and protobuf encoding", len(batch), self._protocol) + return self._exporter.export(batch, *args, **kwargs) + + +class GRPCLogsExporter(_LogsExporterProxy): + _protocol = "grpc" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter + + self._exporter = OTLPLogExporter(*args, **kwargs) + + +class HTTPLogsExporter(_LogsExporterProxy): + _protocol = "http" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter + + self._exporter = OTLPLogExporter(*args, **kwargs) diff --git a/ddtrace/internal/opentelemetry/exporters/grpclib_exporter.py b/ddtrace/internal/opentelemetry/exporters/grpclib_exporter.py new file mode 100644 index 00000000000..846e2d4cd9b --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/grpclib_exporter.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from collections.abc import Sequence +import logging +import os +import ssl +from time import monotonic +from time import sleep +from typing import Any +from urllib.parse import ParseResult +from urllib.parse import urlparse +import warnings + +from grpclib.client import Channel +from grpclib.client import UnaryUnaryMethod +from grpclib.const import Status +from grpclib.exceptions import GRPCError +from opentelemetry.util.re import parse_env_headers + +from ddtrace.internal import forksafe +from ddtrace.internal.settings import env +from ddtrace.internal.threads import Lock + + +log = logging.getLogger(__name__) + +_DEFAULT_ENDPOINT = "http://localhost:4317" +_MAX_RETRY_DELAY = 64.0 +_RETRYABLE_STATUSES = { + Status.CANCELLED, + Status.DEADLINE_EXCEEDED, + Status.RESOURCE_EXHAUSTED, + Status.ABORTED, + Status.OUT_OF_RANGE, + Status.UNAVAILABLE, + Status.DATA_LOSS, +} + + +class GrpclibExporter: + def __init__( + self, + signal: str, + method: str, + request_type: type[Any], + response_type: type[Any], + endpoint: str | None = None, + insecure: bool | None = None, + headers: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + ) -> None: + self._signal = signal + prefix = f"OTEL_EXPORTER_OTLP_{signal.upper()}" + self._endpoint = endpoint or _environment( + f"{prefix}_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", _DEFAULT_ENDPOINT + ) + self._insecure = _resolve_insecure(self._endpoint, insecure, f"{prefix}_INSECURE") + self._headers = _resolve_headers(headers, f"{prefix}_HEADERS") + self._timeout = timeout if timeout is not None else _environment_float(f"{prefix}_TIMEOUT", 10.0) + configured_compression = compression or _environment( + f"{prefix}_COMPRESSION", "OTEL_EXPORTER_OTLP_COMPRESSION", "none" + ) + compression_name = str(getattr(configured_compression, "value", configured_compression)).lower() + if compression_name not in ("", "none"): + raise ValueError(f"The grpclib OTLP {signal} exporter does not support compression") + + self._ssl_context = ( + None + if self._insecure + else _build_ssl_context( + prefix, + certificate_file, + client_key_file, + client_certificate_file, + ) + ) + self._method_name = method + self._request_type = request_type + self._response_type = response_type + self._lock = Lock() + self._pid = os.getpid() + self._shutdown = False + self._initialize_transport() + forksafe.register(self._after_fork) + + def _initialize_transport(self) -> None: + parsed = _parse_endpoint(self._endpoint) + self._loop = asyncio.new_event_loop() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="The loop argument is deprecated", category=DeprecationWarning) + self._channel = Channel( + parsed.hostname, + parsed.port or (443 if not self._insecure else 4317), + loop=self._loop, + ssl=self._ssl_context, + ) + self._method: UnaryUnaryMethod[Any, Any] = UnaryUnaryMethod( + self._channel, + self._method_name, + self._request_type, + self._response_type, + ) + + def _after_fork(self) -> None: + self._pid = os.getpid() + self._lock = Lock() + self._initialize_transport() + + def _export(self, request: Any, success: Any, failure: Any, timeout_millis: float | None) -> Any: + if self._shutdown: + return failure + + timeout = self._timeout if timeout_millis is None else min(self._timeout, timeout_millis / 1000) + deadline = monotonic() + timeout + delay = 1.0 + while True: + try: + with self._lock: + if self._pid != os.getpid(): + self._after_fork() + remaining = deadline - monotonic() + if remaining <= 0: + return failure + self._loop.run_until_complete(self._method(request, timeout=remaining, metadata=self._headers)) + return success + except GRPCError as error: + remaining = deadline - monotonic() + if error.status not in _RETRYABLE_STATUSES or remaining <= delay: + log.error("Failed to export OpenTelemetry %s over gRPC: %s", self._signal, error) + return failure + log.warning("Transient gRPC error exporting OpenTelemetry %s; retrying in %ss", self._signal, delay) + sleep(delay) + delay = min(delay * 2, _MAX_RETRY_DELAY) + except RuntimeError as error: + if "after interpreter shutdown" in str(error): + log.debug("Skipped final OpenTelemetry %s export during interpreter shutdown", self._signal) + return failure + log.exception("Failed to export OpenTelemetry %s over gRPC", self._signal) + return failure + except Exception: + log.exception("Failed to export OpenTelemetry %s over gRPC", self._signal) + return failure + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + if self._shutdown: + return + self._shutdown = True + forksafe.unregister(self._after_fork) + with self._lock: + self._channel.close() + if not self._loop.is_closed(): + self._loop.close() + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + +def _parse_endpoint(endpoint: str) -> ParseResult: + parsed = urlparse(endpoint if "://" in endpoint else f"//{endpoint}") + if not parsed.hostname or parsed.scheme not in ("", "http", "https"): + raise ValueError(f"Invalid OTLP gRPC endpoint: {endpoint!r}") + if parsed.path not in ("", "/"): + raise ValueError("OTLP gRPC endpoints cannot include a path") + return parsed + + +def _environment(signal_name: str, global_name: str, default: str) -> str: + return env.get(signal_name) or env.get(global_name) or default + + +def _resolve_insecure(endpoint: str, insecure: bool | None, signal_name: str) -> bool: + if insecure is not None: + return insecure + configured = env.get(signal_name) + if configured is not None: + return configured.strip().lower() == "true" + return _parse_endpoint(endpoint).scheme != "https" + + +def _resolve_headers( + headers: Sequence[tuple[str, str]] | Mapping[str, str] | str | None, + signal_name: str, +) -> tuple[tuple[str, str], ...]: + configured = headers if headers is not None else _environment(signal_name, "OTEL_EXPORTER_OTLP_HEADERS", "") + if isinstance(configured, str): + return tuple(parse_env_headers(configured).items()) + if isinstance(configured, Mapping): + return tuple(configured.items()) + return tuple(configured) + + +def _environment_float(signal_name: str, default: float) -> float: + configured = env.get(signal_name) or env.get("OTEL_EXPORTER_OTLP_TIMEOUT") + return float(configured) if configured is not None else default + + +def _build_ssl_context( + prefix: str, + certificate_file: str | None, + client_key_file: str | None, + client_certificate_file: str | None, +) -> ssl.SSLContext: + certificate = certificate_file or env.get(f"{prefix}_CERTIFICATE") or env.get("OTEL_EXPORTER_OTLP_CERTIFICATE") + context = ssl.create_default_context(cafile=certificate) + client_certificate = client_certificate_file or env.get(f"{prefix}_CLIENT_CERTIFICATE") + client_key = client_key_file or env.get(f"{prefix}_CLIENT_KEY") + if client_certificate: + context.load_cert_chain(client_certificate, client_key) + return context diff --git a/ddtrace/internal/opentelemetry/exporters/grpclib_log_exporter.py b/ddtrace/internal/opentelemetry/exporters/grpclib_log_exporter.py new file mode 100644 index 00000000000..eee127891b9 --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/grpclib_log_exporter.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +import logging +from typing import Any + + +try: + from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +except ImportError: + from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import encode_logs +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceResponse + + +try: + from opentelemetry.sdk._logs.export import LogRecordExporter as LogExporter + from opentelemetry.sdk._logs.export import LogRecordExportResult as LogExportResult +except ImportError: + from opentelemetry.sdk._logs.export import LogExporter + from opentelemetry.sdk._logs.export import LogExportResult + +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_log_records +from ddtrace.internal.opentelemetry.exporters.grpclib_exporter import GrpclibExporter + + +log = logging.getLogger(__name__) + +_METHOD = "/opentelemetry.proto.collector.logs.v1.LogsService/Export" +_PROTOCOL = "grpc" + + +class OTLPLogExporter(LogExporter, GrpclibExporter): # type: ignore[misc] + """Export OTLP logs over gRPC without depending on grpcio.""" + + 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: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + insecure: bool | None = None, + **kwargs: Any, + ) -> None: + GrpclibExporter.__init__( + self, + "logs", + _METHOD, + ExportLogsServiceRequest, + ExportLogsServiceResponse, + endpoint, + insecure, + headers, + timeout, + compression, + certificate_file, + client_key_file, + client_certificate_file, + ) + + def export(self, batch: Sequence[Any], *args: Any, **kwargs: Any) -> Any: + record_log_records(len(batch), _PROTOCOL) + log.debug("Exporting %d OpenTelemetry Logs with %s protocol and protobuf encoding", len(batch), _PROTOCOL) + try: + request = encode_logs(batch) + except Exception: + log.exception("Failed to encode OpenTelemetry logs") + return LogExportResult.FAILURE + timeout_millis = kwargs.get("timeout_millis") + return self._export(request, LogExportResult.SUCCESS, LogExportResult.FAILURE, timeout_millis) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + GrpclibExporter.shutdown(self, timeout_millis, **kwargs) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return GrpclibExporter.force_flush(self, timeout_millis) diff --git a/ddtrace/internal/opentelemetry/exporters/grpclib_metric_exporter.py b/ddtrace/internal/opentelemetry/exporters/grpclib_metric_exporter.py new file mode 100644 index 00000000000..868aed07855 --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/grpclib_metric_exporter.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +import logging +from typing import Any + +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import OTLPMetricExporterMixin +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.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceResponse +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.export import MetricExporter +from opentelemetry.sdk.metrics.export import MetricExportResult +from opentelemetry.sdk.metrics.export import MetricsData + +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_metrics_export_attempt +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_metrics_export_result +from ddtrace.internal.opentelemetry.exporters.grpclib_exporter import GrpclibExporter + + +log = logging.getLogger(__name__) + +_METHOD = "/opentelemetry.proto.collector.metrics.v1.MetricsService/Export" +_PROTOCOL = "grpc" + + +class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin, GrpclibExporter): # type: ignore[misc] + """Export OTLP metrics over gRPC without depending on grpcio.""" + + def __init__( + self, + endpoint: str | None = None, + insecure: bool | None = None, + headers: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + preferred_temporality: dict[type, AggregationTemporality] | None = None, + preferred_aggregation: dict[type, Any] | None = None, + **kwargs: Any, + ) -> None: + self._common_configuration(preferred_temporality) + if preferred_aggregation: + self._preferred_aggregation.update(preferred_aggregation) + GrpclibExporter.__init__( + self, + "metrics", + _METHOD, + ExportMetricsServiceRequest, + ExportMetricsServiceResponse, + endpoint, + insecure, + headers, + timeout, + compression, + ) + + def export( + self, metrics_data: MetricsData, timeout_millis: float | None = 10_000, **kwargs: Any + ) -> MetricExportResult: + record_metrics_export_attempt(_PROTOCOL) + log.debug("Exporting OpenTelemetry Metrics with %s protocol and protobuf encoding", _PROTOCOL) + try: + request = encode_metrics(metrics_data) + except Exception: + log.exception("Failed to encode OpenTelemetry metrics") + result = MetricExportResult.FAILURE + else: + result = self._export(request, MetricExportResult.SUCCESS, MetricExportResult.FAILURE, timeout_millis) + record_metrics_export_result(result, _PROTOCOL) + return result + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + GrpclibExporter.shutdown(self, timeout_millis, **kwargs) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return GrpclibExporter.force_flush(self, timeout_millis) diff --git a/ddtrace/internal/opentelemetry/exporters/http_exporter.py b/ddtrace/internal/opentelemetry/exporters/http_exporter.py new file mode 100644 index 00000000000..c5f6e60dddf --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/http_exporter.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +from datetime import timezone +from email.utils import parsedate_to_datetime +import gzip +import logging +import math +from secrets import SystemRandom +import ssl +from threading import Event +from time import monotonic +from time import time +from typing import Any +from urllib.error import HTTPError +from urllib.parse import urlsplit +from urllib.request import HTTPSHandler +from urllib.request import OpenerDirector +from urllib.request import Request +from urllib.request import build_opener +import zlib + +from opentelemetry.util.re import parse_env_headers + +from ddtrace.internal.http_client import HTTPClient +from ddtrace.internal.settings import env + + +log = logging.getLogger(__name__) + +_DEFAULT_ENDPOINT = "http://localhost:4318" +_MAX_RETRIES = 6 +_MAX_REQUEST_SIZE = 64 * 1024 * 1024 +_RETRYABLE_STATUS_CODES = frozenset({429, 502, 503, 504}) +_RANDOM = SystemRandom() + + +class HttpExporter: + """Send OTLP protobuf payloads without the upstream HTTP transport dependencies.""" + + def __init__( + self, + signal: str, + path: str, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + max_request_size: int | None = None, + ) -> None: + prefix = f"OTEL_EXPORTER_OTLP_{signal.upper()}" + self._signal = signal + self._endpoint = endpoint or _resolve_endpoint(prefix, path) + self._headers = list(_resolve_headers(headers, f"{prefix}_HEADERS")) + self._timeout = timeout if timeout is not None else _environment_float(f"{prefix}_TIMEOUT", 10.0) + self._compression = _resolve_compression(compression, f"{prefix}_COMPRESSION") + self._max_request_size = _MAX_REQUEST_SIZE if max_request_size is None else max_request_size + self._shutdown = Event() + self._client: HTTPClient | None + self._opener: OpenerDirector | None + + if not _has_header(self._headers, "content-type"): + self._headers.insert(0, ("Content-Type", "application/x-protobuf")) + if self._compression != "none" and not _has_header(self._headers, "content-encoding"): + self._headers.append(("Content-Encoding", self._compression)) + + certificate = certificate_file or _environment(f"{prefix}_CERTIFICATE", "OTEL_EXPORTER_OTLP_CERTIFICATE", "") + client_certificate = client_certificate_file or env.get(f"{prefix}_CLIENT_CERTIFICATE") or "" + client_key = client_key_file or env.get(f"{prefix}_CLIENT_KEY") or "" + if certificate or client_certificate or client_key: + self._client = None + context = ssl.create_default_context(cafile=certificate or None) + if client_certificate: + context.load_cert_chain(client_certificate, client_key or None) + self._opener = build_opener(HTTPSHandler(context=context)) + self._path = "" + else: + parsed = urlsplit(self._endpoint) + if parsed.scheme not in ("http", "https") or not parsed.netloc: + raise ValueError(f"Invalid OTLP HTTP endpoint: {self._endpoint!r}") + base_url = f"{parsed.scheme}://{parsed.netloc}" + self._path = parsed.path or "/" + if parsed.query: + self._path += f"?{parsed.query}" + self._client = HTTPClient( + base_url, + timeout_ms=max(1, int(self._timeout * 1000)), + headers=self._headers, + treat_http_errors_as_errors=False, + ) + self._opener = None + + def _export(self, payload: bytes, success: Any, failure: Any, timeout_millis: float | None) -> Any: + if self._shutdown.is_set(): + return failure + if self._max_request_size > 0 and len(payload) > self._max_request_size: + log.error( + "Failed to export OpenTelemetry %s: payload size %d exceeds the %d byte limit", + self._signal, + len(payload), + self._max_request_size, + ) + return failure + + payload = _compress(payload, self._compression) + timeout = self._timeout if timeout_millis is None else min(self._timeout, timeout_millis / 1000) + deadline = monotonic() + timeout + + for attempt in range(_MAX_RETRIES): + remaining = deadline - monotonic() + if remaining <= 0: + break + try: + response_status, retry_after = self._send(payload, remaining) + except Exception as error: + retry_after = None + failure_reason: Any = error + else: + if 200 <= response_status < 400: + return success + failure_reason = f"HTTP {response_status}" + if response_status not in _RETRYABLE_STATUS_CODES: + log.error("Failed to export OpenTelemetry %s: %s", self._signal, failure_reason) + return failure + + delay = retry_after if retry_after is not None else 2**attempt * _RANDOM.uniform(0.8, 1.2) + if attempt + 1 == _MAX_RETRIES or delay > deadline - monotonic(): + break + log.warning( + "Transient error exporting OpenTelemetry %s (%s); retrying in %.2fs", + self._signal, + failure_reason, + delay, + ) + if self._shutdown.wait(delay): + return failure + + log.error("Failed to export OpenTelemetry %s before the deadline", self._signal) + return failure + + def _send(self, payload: bytes, timeout: float) -> tuple[int, float | None]: + if self._client is not None: + native_response = self._client.post(self._path, body=payload, timeout_ms=max(1, int(timeout * 1000))) + return native_response.status_code, _parse_retry_after(native_response.header("retry-after")) + + request = Request(self._endpoint, data=payload, headers=dict(self._headers), method="POST") + try: + opener = self._opener + if opener is None: + raise RuntimeError("OTLP HTTP exporter has no configured transport") + with opener.open(request, timeout=timeout) as url_response: + return url_response.status, _parse_retry_after(url_response.headers.get("retry-after")) + except HTTPError as error: + return error.code, _parse_retry_after(error.headers.get("retry-after")) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + self._shutdown.set() + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return True + + +def _resolve_endpoint(prefix: str, path: str) -> str: + signal_endpoint = env.get(f"{prefix}_ENDPOINT") + if signal_endpoint: + return signal_endpoint + return (env.get("OTEL_EXPORTER_OTLP_ENDPOINT") or _DEFAULT_ENDPOINT).rstrip("/") + path + + +def _resolve_headers( + headers: Sequence[tuple[str, str]] | Mapping[str, str] | str | None, + signal_name: str, +) -> tuple[tuple[str, str], ...]: + configured = headers if headers is not None else _environment(signal_name, "OTEL_EXPORTER_OTLP_HEADERS", "") + if isinstance(configured, str): + return tuple(parse_env_headers(configured).items()) + if isinstance(configured, Mapping): + return tuple(configured.items()) + return tuple(configured) + + +def _resolve_compression(compression: Any, signal_name: str) -> str: + configured = compression or _environment(signal_name, "OTEL_EXPORTER_OTLP_COMPRESSION", "none") + name = str(getattr(configured, "value", configured)).lower() + if name not in ("none", "gzip", "deflate"): + raise ValueError(f"Unsupported OTLP HTTP compression: {name!r}") + return name + + +def _compress(payload: bytes, compression: str) -> bytes: + if compression == "gzip": + return gzip.compress(payload) + if compression == "deflate": + return zlib.compress(payload) + return payload + + +def _environment(signal_name: str, global_name: str, default: str) -> str: + return env.get(signal_name) or env.get(global_name) or default + + +def _environment_float(signal_name: str, default: float) -> float: + configured = env.get(signal_name) or env.get("OTEL_EXPORTER_OTLP_TIMEOUT") + return float(configured) if configured is not None else default + + +def _has_header(headers: Sequence[tuple[str, str]], name: str) -> bool: + return any(key.lower() == name for key, _ in headers) + + +def _parse_retry_after(value: str | None) -> float | None: + if value is None: + return None + try: + seconds = float(value) + except ValueError: + try: + retry_at = parsedate_to_datetime(value) + except (TypeError, ValueError): + return None + if retry_at.tzinfo is None: + retry_at = retry_at.replace(tzinfo=timezone.utc) + return max(retry_at.timestamp() - time(), 0.0) + return max(seconds, 0.0) if math.isfinite(seconds) else None diff --git a/ddtrace/internal/opentelemetry/exporters/http_log_exporter.py b/ddtrace/internal/opentelemetry/exporters/http_log_exporter.py new file mode 100644 index 00000000000..ae49a37d985 --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/http_log_exporter.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +import logging +from typing import Any + + +try: + from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +except ImportError: + from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import encode_logs + +try: + from opentelemetry.sdk._logs.export import LogRecordExporter as LogExporter + from opentelemetry.sdk._logs.export import LogRecordExportResult as LogExportResult +except ImportError: + from opentelemetry.sdk._logs.export import LogExporter + from opentelemetry.sdk._logs.export import LogExportResult + +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_log_records +from ddtrace.internal.opentelemetry.exporters.http_exporter import HttpExporter + + +log = logging.getLogger(__name__) + +_PROTOCOL = "http" + + +class OTLPLogExporter(LogExporter, HttpExporter): # type: ignore[misc] + """Export OTLP logs over HTTP without third-party HTTP packages.""" + + 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: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + max_request_size: int | None = None, + **kwargs: Any, + ) -> None: + HttpExporter.__init__( + self, + "logs", + "/v1/logs", + endpoint, + certificate_file, + client_key_file, + client_certificate_file, + headers, + timeout, + compression, + max_request_size, + ) + + def export(self, batch: Sequence[Any], *args: Any, **kwargs: Any) -> Any: + record_log_records(len(batch), _PROTOCOL) + log.debug("Exporting %d OpenTelemetry Logs with %s protocol and protobuf encoding", len(batch), _PROTOCOL) + try: + payload = encode_logs(batch).SerializeToString() + except Exception: + log.exception("Failed to encode OpenTelemetry logs") + return LogExportResult.FAILURE + return self._export(payload, LogExportResult.SUCCESS, LogExportResult.FAILURE, kwargs.get("timeout_millis")) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + HttpExporter.shutdown(self, timeout_millis, **kwargs) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return HttpExporter.force_flush(self, timeout_millis) diff --git a/ddtrace/internal/opentelemetry/exporters/http_metric_exporter.py b/ddtrace/internal/opentelemetry/exporters/http_metric_exporter.py new file mode 100644 index 00000000000..9a9961c33e5 --- /dev/null +++ b/ddtrace/internal/opentelemetry/exporters/http_metric_exporter.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from collections.abc import Sequence +import logging +from typing import Any + +from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import OTLPMetricExporterMixin +from opentelemetry.exporter.otlp.proto.common.metrics_encoder import encode_metrics +from opentelemetry.sdk.metrics.export import AggregationTemporality +from opentelemetry.sdk.metrics.export import MetricExporter +from opentelemetry.sdk.metrics.export import MetricExportResult +from opentelemetry.sdk.metrics.export import MetricsData + +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_metrics_export_attempt +from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import record_metrics_export_result +from ddtrace.internal.opentelemetry.exporters.http_exporter import HttpExporter + + +log = logging.getLogger(__name__) + +_PROTOCOL = "http" + + +class OTLPMetricExporter(MetricExporter, OTLPMetricExporterMixin, HttpExporter): # type: ignore[misc] + """Export OTLP metrics over HTTP without third-party HTTP packages.""" + + 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: Sequence[tuple[str, str]] | Mapping[str, str] | str | None = None, + timeout: float | None = None, + compression: Any = None, + preferred_temporality: dict[type, AggregationTemporality] | None = None, + preferred_aggregation: dict[type, Any] | None = None, + max_request_size: int | None = None, + **kwargs: Any, + ) -> None: + self._common_configuration(preferred_temporality) + if preferred_aggregation: + self._preferred_aggregation.update(preferred_aggregation) + HttpExporter.__init__( + self, + "metrics", + "/v1/metrics", + endpoint, + certificate_file, + client_key_file, + client_certificate_file, + headers, + timeout, + compression, + max_request_size, + ) + + def export( + self, metrics_data: MetricsData, timeout_millis: float | None = 10_000, **kwargs: Any + ) -> MetricExportResult: + record_metrics_export_attempt(_PROTOCOL) + log.debug("Exporting OpenTelemetry Metrics with %s protocol and protobuf encoding", _PROTOCOL) + try: + payload = encode_metrics(metrics_data).SerializeToString() + except Exception: + log.exception("Failed to encode OpenTelemetry metrics") + result = MetricExportResult.FAILURE + else: + result = self._export(payload, MetricExportResult.SUCCESS, MetricExportResult.FAILURE, timeout_millis) + record_metrics_export_result(result, _PROTOCOL) + return result + + def shutdown(self, timeout_millis: float = 30_000, **kwargs: Any) -> None: + HttpExporter.shutdown(self, timeout_millis, **kwargs) + + def force_flush(self, timeout_millis: float = 10_000) -> bool: + return HttpExporter.force_flush(self, timeout_millis) diff --git a/ddtrace/internal/opentelemetry/logs.py b/ddtrace/internal/opentelemetry/logs.py index 1a063da6e07..7be77528f40 100644 --- a/ddtrace/internal/opentelemetry/logs.py +++ b/ddtrace/internal/opentelemetry/logs.py @@ -7,11 +7,10 @@ from ddtrace import config from ddtrace.internal.hostname import get_hostname from ddtrace.internal.logger import get_logger +from ddtrace.internal.opentelemetry.exporters import get_logs_exporter from ddtrace.internal.settings import env from ddtrace.internal.settings._agentless import config as agentless_config from ddtrace.internal.settings._opentelemetry import otel_config -from ddtrace.internal.telemetry import telemetry_writer -from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE log = get_logger(__name__) @@ -33,7 +32,7 @@ def set_otel_logs_provider() -> None: return protocol = otel_config.exporter.LOGS_PROTOCOL - exporter_class = _import_exporter(protocol) + exporter_class = get_logs_exporter(protocol) if exporter_class is None: return @@ -106,70 +105,6 @@ def _build_resource() -> Optional[Any]: return None -def _dd_logs_exporter(otel_exporter: type[Any], protocol: str, encoding: str) -> type[Any]: - """Create a custom OpenTelemetry Logs exporter that adds telemetry metrics and debug logs.""" - - class DDLogsExporter(otel_exporter): - """A custom OpenTelemetry Logs exporter that adds telemetry metrics and debug logs.""" - - def export(self, batch: Any, *args: Any, **kwargs: Any) -> Any: - """Export logs and queues telemetry metrics.""" - telemetry_writer.add_count_metric( - TELEMETRY_NAMESPACE.TRACERS, - "otel.log_records", - len(batch), - ( - ("protocol", protocol), - ("encoding", encoding), - ), - ) - log.debug( - "Exporting %d OpenTelemetry Logs with %s protocol and %s encoding", len(batch), protocol, encoding - ) - return super().export(batch, *args, **kwargs) - - return DDLogsExporter - - -def _import_exporter(protocol): - """Import the appropriate OpenTelemetry Logs exporter based on the set protocol""" - try: - if protocol == "grpc": - from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter - from opentelemetry.exporter.otlp.proto.grpc.version import __version__ as exporter_version - elif protocol == "http/protobuf": - from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter - from opentelemetry.exporter.otlp.proto.http.version import __version__ as exporter_version - else: - log.warning( - "OpenTelemetry Logs exporter protocol '%s' is not supported. Use 'grpc' or 'http/protobuf'.", - protocol, - ) - return None - - if tuple(int(x) for x in exporter_version.split(".")[:3]) < MINIMUM_SUPPORTED_VERSION: - log.warning( - "OpenTelemetry Logs exporter for %s requires version %r or higher, but found version %r. " - "Please upgrade the appropriate opentelemetry-exporter package.", - protocol, - MINIMUM_SUPPORTED_VERSION, - exporter_version, - ) - return None - - return _dd_logs_exporter(OTLPLogExporter, protocol.split("/")[0], "protobuf") - - except ImportError as e: - log.warning( - "OpenTelemetry Logs exporter for %s is not available. " - "Please install a supported package (ex: opentelemetry-exporter-otlp-proto-%s): %s", - protocol, - "grpc" if protocol == "grpc" else "http", - str(e), - ) - return None - - class _SelfTelemetryLogFilter(logging.Filter): """Reject log records emitted by ddtrace's own loggers and by the OpenTelemetry SDK/exporter loggers. diff --git a/ddtrace/internal/opentelemetry/metrics.py b/ddtrace/internal/opentelemetry/metrics.py index d9f085a369e..e9c1d731002 100644 --- a/ddtrace/internal/opentelemetry/metrics.py +++ b/ddtrace/internal/opentelemetry/metrics.py @@ -6,11 +6,10 @@ from ddtrace import config from ddtrace.internal.hostname import get_hostname from ddtrace.internal.logger import get_logger +from ddtrace.internal.opentelemetry.exporters import get_metrics_exporter from ddtrace.internal.settings import env from ddtrace.internal.settings._agentless import config as agentless_config from ddtrace.internal.settings._opentelemetry import otel_config -from ddtrace.internal.telemetry import telemetry_writer -from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE log = get_logger(__name__) @@ -31,7 +30,7 @@ def set_otel_meter_provider(): return protocol = otel_config.exporter.METRICS_PROTOCOL - exporter_class = _import_exporter(protocol) + exporter_class = get_metrics_exporter(protocol) if exporter_class is None: return @@ -79,8 +78,9 @@ def _build_resource() -> Optional[Any]: """Build an OpenTelemetry Resource using DD_TAGS and OTEL_RESOURCE_ATTRIBUTES.""" try: from opentelemetry.sdk.resources import Resource + from opentelemetry.util.types import AttributeValue - resource_attributes = { + resource_attributes: dict[str, AttributeValue | None] = { **config.tags, "service.name": config.service, "service.version": config.version, @@ -90,90 +90,11 @@ def _build_resource() -> Optional[Any]: if config._report_hostname and "host.name" not in resource_attributes: resource_attributes["host.name"] = get_hostname() - resource_attributes = {k: str(v) if v is not None else "" for k, v in resource_attributes.items()} - - return Resource.create(resource_attributes) + return Resource.create({key: value for key, value in resource_attributes.items() if value is not None}) except ImportError: log.warning( "OpenTelemetry SDK is not installed, opentelemetry metrics will not be enabled. " - "Please install the OpenTelemetry SDK before enabling ddtrace OpenTelemetry Metrics support." - ) - return None - - -def _dd_metrics_exporter(otel_exporter: type[Any], protocol: str, encoding: str) -> type[Any]: - """Create a custom OpenTelemetry Metrics exporter that adds telemetry metrics and debug logs.""" - - class DDMetricsExporter(otel_exporter): - """A custom OpenTelemetry Metrics exporter that adds telemetry metrics and debug logs.""" - - def export(self, metrics_data: Any, timeout_millis: Any, *args: Any, **kwargs: Any) -> Any: - """Export metrics and queues telemetry metrics.""" - telemetry_writer.add_count_metric( - TELEMETRY_NAMESPACE.TRACERS, - "otel.metrics_export_attempts", - 1, - ( - ("protocol", protocol), - ("encoding", encoding), - ), - ) - # TODO: Count the number of unique metrics streams in this export - log.debug("Exporting OpenTelemetry Metrics with %s protocol and %s encoding", protocol, encoding) - result = super().export(metrics_data, timeout_millis, *args, **kwargs) - - if result.value == 0 or result.value == 1: - telemetry_writer.add_count_metric( - TELEMETRY_NAMESPACE.TRACERS, - "otel.metrics_export_successes" if result.value == 0 else "otel.metrics_export_failures", - 1, - ( - ("protocol", protocol), - ("encoding", encoding), - ), - ) - - return result - - return DDMetricsExporter - - -def _import_exporter(protocol): - """Import the appropriate OpenTelemetry Metrics exporter based on the set protocol""" - try: - if protocol == "grpc": - from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter - from opentelemetry.exporter.otlp.proto.grpc.version import __version__ as exporter_version - elif protocol == "http/protobuf": - from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter - from opentelemetry.exporter.otlp.proto.http.version import __version__ as exporter_version - else: - log.warning( - "OpenTelemetry Metrics exporter protocol '%s' is not supported. Use 'grpc' or 'http/protobuf'.", - protocol, - ) - return None - - if tuple(int(x) for x in exporter_version.split(".")[:3]) < MINIMUM_SUPPORTED_VERSION: - log.warning( - "OpenTelemetry Metrics exporter for %s requires version %r or higher, but found version %r. " - "Please upgrade the appropriate opentelemetry-exporter package.", - protocol, - MINIMUM_SUPPORTED_VERSION, - exporter_version, - ) - return None - - protocol_name = "grpc" if protocol == "grpc" else "http" - return _dd_metrics_exporter(OTLPMetricExporter, protocol_name, "protobuf") - - except ImportError as e: - log.warning( - "OpenTelemetry Metrics exporter for %s is not available. " - "Please install a supported package (ex: opentelemetry-exporter-otlp-proto-%s): %s", - protocol, - "grpc" if protocol == "grpc" else "http", - str(e), + "Install ddtrace[opentelemetry] before enabling OpenTelemetry Metrics support." ) return None diff --git a/docs/configuration.rst b/docs/configuration.rst index 3779b906c3b..202b13cd51b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -580,7 +580,8 @@ Metrics description: | When used with ``ddtrace-run`` this configuration enables support for exporting OTLP metrics generated - by the OpenTelemetry Metrics API. The application must also include its own OTLP metrics exporter. + by the OpenTelemetry Metrics API. Install ``ddtrace[opentelemetry]`` to include the lightweight + gRPC and HTTP/protobuf exporters. version_added: v3.11.0: @@ -1273,7 +1274,8 @@ Logs description: | When used with ``ddtrace-run`` this configuration enables support for exporting OTLP logs generated - by the OpenTelemetry Logging API. The application must also include its own OTLP logs exporter. + by the OpenTelemetry Logging API. Install ``ddtrace[opentelemetry]`` to include the lightweight + gRPC and HTTP/protobuf exporters. version_added: v3.12.0: Adds support for submitting logs via an OTLP Exporter. diff --git a/lib-injection/sources/requirements.csv b/lib-injection/sources/requirements.csv index 27ee2d58138..e577f1d8480 100644 --- a/lib-injection/sources/requirements.csv +++ b/lib-injection/sources/requirements.csv @@ -10,5 +10,7 @@ envier,~=0.6.1, opentelemetry-api,">=1,<2", wrapt,">=1,!=2.2.0,<3", opentracing,">=2,<3", -opentelemetry-exporter-otlp,">=1,<2", +grpclib,">=0.4.8,<0.5", +opentelemetry-exporter-otlp-proto-common,">=1.18,<2", +opentelemetry-sdk,">=1.18,<2", openfeature-sdk,">=0.8,<1", diff --git a/pyproject.toml b/pyproject.toml index 3aba9e97047..7b917a5bb79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,9 @@ opentracing = [ "opentracing>=2,<3", ] opentelemetry = [ - "opentelemetry-exporter-otlp>=1,<2", + "grpclib>=0.4.8,<0.5", + "opentelemetry-exporter-otlp-proto-common>=1.18,<2", + "opentelemetry-sdk>=1.18,<2", ] openfeature = [ "openfeature-sdk>=0.8,<1", diff --git a/releasenotes/notes/built-in-otel-metrics-exporters-4f809ffac6a523c9.yaml b/releasenotes/notes/built-in-otel-metrics-exporters-4f809ffac6a523c9.yaml new file mode 100644 index 00000000000..135f7aebe24 --- /dev/null +++ b/releasenotes/notes/built-in-otel-metrics-exporters-4f809ffac6a523c9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + OpenTelemetry: The ``opentelemetry`` extra now provides OTLP metrics and logs export over HTTP/protobuf and lightweight gRPC without requiring ``grpcio``. diff --git a/requirements.csv b/requirements.csv index 27ee2d58138..e577f1d8480 100644 --- a/requirements.csv +++ b/requirements.csv @@ -10,5 +10,7 @@ envier,~=0.6.1, opentelemetry-api,">=1,<2", wrapt,">=1,!=2.2.0,<3", opentracing,">=2,<3", -opentelemetry-exporter-otlp,">=1,<2", +grpclib,">=0.4.8,<0.5", +opentelemetry-exporter-otlp-proto-common,">=1.18,<2", +opentelemetry-sdk,">=1.18,<2", openfeature-sdk,">=0.8,<1", diff --git a/tests/contrib/suitespec.yml b/tests/contrib/suitespec.yml index edd30a30392..33b78eeeeb7 100644 --- a/tests/contrib/suitespec.yml +++ b/tests/contrib/suitespec.yml @@ -1999,22 +1999,22 @@ suites: dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, markupsafe, flask, gevent, 'requests==2.28.1', opentelemetry-api] - name: opentelemetry python: ['3.9', '3.10', '3.11', '3.12', '3.13'] - dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', 'opentelemetry-exporter-otlp~=1.15.0'] + dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', 'opentelemetry-exporter-otlp~=1.15.0', 'opentelemetry-sdk~=1.34.0', 'grpclib>=0.4.8,<0.5'] env: SDK_EXPORTER_INSTALLED: '1' - name: opentelemetry python: ['3.9', '3.10', '3.11', '3.12', '3.13'] - dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', 'opentelemetry-exporter-otlp~=1.34.0'] + dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', 'opentelemetry-exporter-otlp-proto-common~=1.34.0', 'opentelemetry-sdk~=1.34.0', 'grpclib>=0.4.8,<0.5'] env: SDK_EXPORTER_INSTALLED: '1' - name: opentelemetry python: ['3.9', '3.10', '3.11', '3.12', '3.13'] - dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', opentelemetry-exporter-otlp] + dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, 'markupsafe==2.0.1', flask, gevent, 'requests==2.28.1', opentelemetry-exporter-otlp-proto-common, opentelemetry-sdk, 'grpclib>=0.4.8,<0.5'] env: SDK_EXPORTER_INSTALLED: '1' - name: opentelemetry python: ['3.14'] - dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, markupsafe, flask, gevent, 'requests==2.28.1', opentelemetry-exporter-otlp] + dependencies: [pytest-randomly, 'pytest-asyncio==0.21.1', opentelemetry-instrumentation-flask, markupsafe, flask, gevent, 'requests==2.28.1', opentelemetry-exporter-otlp-proto-common, opentelemetry-sdk, 'grpclib>=0.4.8,<0.5'] env: SDK_EXPORTER_INSTALLED: '1' protobuf: diff --git a/tests/opentelemetry/test_logs.py b/tests/opentelemetry/test_logs.py index 3d44c4f2eb6..d80f76db69f 100644 --- a/tests/opentelemetry/test_logs.py +++ b/tests/opentelemetry/test_logs.py @@ -1,5 +1,14 @@ -from concurrent import futures +import asyncio +from http.server import BaseHTTPRequestHandler +from http.server import ThreadingHTTPServer +from importlib import import_module +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version import os +import sys +from threading import Event +from threading import Thread +from types import SimpleNamespace from opentelemetry.version import __version__ as api_version_string import pytest @@ -9,34 +18,113 @@ try: - from opentelemetry.exporter.otlp.version import __version__ as exporter_version - + exporter_version = package_version("opentelemetry-exporter-otlp-proto-common") EXPORTER_VERSION = tuple(int(x) for x in exporter_version.split(".")[:3]) -except ImportError: - EXPORTER_VERSION = (0, 0, 0) +except PackageNotFoundError: + try: + from opentelemetry.exporter.otlp.proto.http.version import __version__ as exporter_version + except ImportError: + EXPORTER_VERSION = (0, 0, 0) + else: + EXPORTER_VERSION = tuple(int(x) for x in exporter_version.split(".")[:3]) def create_mock_grpc_server(): """Create a mock gRPC server for testing OpenTelemetry logs exporter.""" - import grpc - from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import LogsServiceServicer - from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import add_LogsServiceServicer_to_server + from grpclib.const import Cardinality + from grpclib.const import Handler + from grpclib.server import Server + from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceRequest + from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceResponse + + method = "/opentelemetry.proto.collector.logs.v1.LogsService/Export" - class MockLogsService(LogsServiceServicer): + class MockLogsService: def __init__(self): self.received_requests = [] - def Export(self, request, context): - from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ExportLogsServiceResponse - + async def export(self, stream): + request = await stream.recv_message() self.received_requests.append(request) - return ExportLogsServiceResponse() + await stream.send_message(ExportLogsServiceResponse()) + + def __mapping__(self): + return { + method: Handler( + self.export, + Cardinality.UNARY_UNARY, + ExportLogsServiceRequest, + ExportLogsServiceResponse, + ) + } + + class MockServer: + def __init__(self, service): + self._service = service + self._started = Event() + self._error = None + + def start(self): + self._thread = Thread(target=self._run, daemon=True) + self._thread.start() + assert self._started.wait(5), "Timed out starting the mock OTLP gRPC server" + if self._error is not None: + raise self._error + + def _run(self): + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self._server = Server([self._service], loop=self._loop) + try: + self._loop.run_until_complete(self._server.start("127.0.0.1", 4317)) + except Exception as error: + self._error = error + self._started.set() + return + self._started.set() + self._loop.run_forever() + self._server.close() + self._loop.run_until_complete(self._server.wait_closed()) + self._loop.close() + + def stop(self, timeout): + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join(timeout) mock_service = MockLogsService() - server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) - add_LogsServiceServicer_to_server(mock_service, server) - server.add_insecure_port("[::]:4317") - return mock_service, server + return mock_service, MockServer(mock_service) + + +def create_mock_http_server(port): + """Create a mock HTTP server for testing OpenTelemetry logs exporter.""" + requests = [] + + class MockHTTPServer(ThreadingHTTPServer): + allow_reuse_address = True + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length", "0")) + requests.append((self.path, self.rfile.read(length))) + self.send_response(200) + self.end_headers() + + def log_message(self, format_string, *args): + pass + + server = MockHTTPServer(("127.0.0.1", port), Handler) + thread = Thread(target=server.serve_forever, daemon=True) + + class MockServer: + def start(self): + thread.start() + + def stop(self, timeout): + server.shutdown() + server.server_close() + thread.join(timeout) + + return requests, MockServer() def decode_logs_request(request_body: bytes): @@ -133,6 +221,129 @@ def test_otel_logs_support_enabled(): ) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18, 0), reason="The lightweight gRPC exporter requires OpenTelemetry 1.18") +def test_grpc_protocol_selects_grpclib_exporter(): + from ddtrace.internal.opentelemetry.exporters import get_logs_exporter + + assert get_logs_exporter("grpc").__module__ == "ddtrace.internal.opentelemetry.exporters.grpclib_log_exporter" + + +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18, 0), reason="The lightweight HTTP exporter requires OpenTelemetry 1.18") +def test_http_protocol_selects_lightweight_exporter(): + from ddtrace.internal.opentelemetry.exporters import get_logs_exporter + + assert get_logs_exporter("http/protobuf").__module__ == "ddtrace.internal.opentelemetry.exporters.http_log_exporter" + + +@pytest.mark.parametrize("protocol", ["grpc", "http/protobuf"]) +def test_exporter_resolution_without_proto_package_returns_none(monkeypatch, protocol): + from ddtrace.internal.opentelemetry import exporters + + def missing_exporter_version(): + raise ImportError("OpenTelemetry exporter is not installed") + + monkeypatch.setattr(exporters, "_exporter_version", missing_exporter_version) + + assert exporters.get_logs_exporter(protocol) is None + + +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="Test requires the lightweight exporter selection path") +def test_grpc_protocol_without_grpclib_falls_back_to_upstream_exporter(monkeypatch): + from ddtrace.internal.opentelemetry import exporters + from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import GRPCLogsExporter + + monkeypatch.setitem(sys.modules, "ddtrace.internal.opentelemetry.exporters.grpclib_log_exporter", None) + monkeypatch.setattr(exporters, "import_module", lambda name: SimpleNamespace()) + + assert exporters.get_logs_exporter("grpc") is GRPCLogsExporter + + +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="Test requires the lightweight exporter selection path") +def test_grpc_protocol_without_grpclib_or_upstream_exporter_returns_none(monkeypatch): + from ddtrace.internal.opentelemetry import exporters + + def missing_upstream_exporter(name): + raise ImportError("OpenTelemetry gRPC exporter is not installed") + + monkeypatch.setitem(sys.modules, "ddtrace.internal.opentelemetry.exporters.grpclib_log_exporter", None) + monkeypatch.setattr(exporters, "import_module", missing_upstream_exporter) + + assert exporters.get_logs_exporter("grpc") is None + + +@pytest.mark.skipif( + not MINIMUM_SUPPORTED_VERSION <= EXPORTER_VERSION < (1, 18), + reason="Test requires the upstream exporter fallback", +) +def test_upstream_exporter_fallback_records_telemetry(monkeypatch): + try: + from opentelemetry.sdk._logs.export import LogRecordExportResult as LogExportResult + except ImportError: + from opentelemetry.sdk._logs.export import LogExportResult + + from ddtrace.internal.opentelemetry.exporters import exporter_telemetry + from ddtrace.internal.opentelemetry.exporters import get_logs_exporter + + telemetry = [] + monkeypatch.setattr( + exporter_telemetry.telemetry_writer, + "add_count_metric", + lambda namespace, name, value, tags: telemetry.append((name, value, tags)), + ) + + exporter = get_logs_exporter("grpc")(endpoint="http://127.0.0.1:4317") + monkeypatch.setattr(exporter._exporter, "export", lambda *args, **kwargs: LogExportResult.SUCCESS) + try: + assert exporter.export([object(), object()]) is LogExportResult.SUCCESS + finally: + exporter.shutdown() + + assert telemetry == [("otel.log_records", 2, (("protocol", "grpc"), ("encoding", "protobuf")))] + + +@pytest.mark.parametrize( + ("module_name", "protocol"), + [ + ("ddtrace.internal.opentelemetry.exporters.grpclib_log_exporter", "grpc"), + ("ddtrace.internal.opentelemetry.exporters.http_log_exporter", "http"), + ], +) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight exporters require OpenTelemetry 1.18") +def test_lightweight_exporter_records_telemetry(monkeypatch, module_name, protocol): + try: + from opentelemetry.sdk._logs.export import LogRecordExportResult as LogExportResult + except ImportError: + from opentelemetry.sdk._logs.export import LogExportResult + + from ddtrace.internal.opentelemetry.exporters import exporter_telemetry + from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE + + telemetry = [] + monkeypatch.setattr( + exporter_telemetry.telemetry_writer, + "add_count_metric", + lambda namespace, name, value, tags: telemetry.append((namespace, name, value, tags)), + ) + + module = import_module(module_name) + monkeypatch.setattr(module, "encode_logs", lambda batch: SimpleNamespace(SerializeToString=lambda: b"payload")) + exporter = module.OTLPLogExporter(endpoint="http://127.0.0.1:4318") + monkeypatch.setattr(exporter, "_export", lambda *args, **kwargs: LogExportResult.SUCCESS) + try: + assert exporter.export([object(), object()]) is LogExportResult.SUCCESS + finally: + exporter.shutdown() + + assert telemetry == [ + ( + TELEMETRY_NAMESPACE.TRACERS, + "otel.log_records", + 2, + (("protocol", protocol), ("encoding", "protobuf")), + ) + ] + + @pytest.mark.skipif( EXPORTER_VERSION < MINIMUM_SUPPORTED_VERSION, reason=f"OpenTelemetry exporter version {MINIMUM_SUPPORTED_VERSION} is required to export logs", @@ -189,40 +400,32 @@ def test_otel_logs_support_not_enabled(): "DD_TRACE_REPORT_HOSTNAME": "true", "DD_HOSTNAME": "ddhost", "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:14318", }, err=None, ) def test_otel_logs_exporter_auto_configured_http(): """Test OpenTelemetry logs exporter auto-configuration for HTTP protocol.""" from logging import getLogger - from unittest.mock import Mock - from unittest.mock import patch from opentelemetry._logs import get_logger_provider + from tests.opentelemetry.test_logs import create_mock_http_server from tests.opentelemetry.test_logs import decode_logs_request from tests.opentelemetry.test_logs import extract_log_correlation_attributes log = getLogger() - with patch("requests.sessions.Session.request") as mock_request: - mock_response = Mock(status_code=200) - mock_request.return_value = mock_response - + requests, server = create_mock_http_server(14318) + try: + server.start() log.error("test_otel_logs_exporter_auto_configured_http") - logger_provider = get_logger_provider() logger_provider.force_flush() + finally: + server.stop(2) - request_body = None - for call in mock_request.call_args_list: - method, url = call[0][:2] - if method == "POST" and "/v1/logs" in url: - request_body = call[1].get("data", None) - break - assert request_body is not None, ( - "Expected a request body to be present in the " - f"OpenTelemetry logs exporter request {mock_request.call_args_list}" - ) + request_body = next((body for path, body in requests if "/v1/logs" in path), None) + assert request_body is not None, f"Expected an OpenTelemetry logs exporter request, found {requests}" captured_logs = decode_logs_request(request_body) assert len(captured_logs.resource_logs) > 0, "Expected at least one resource log in the OpenTelemetry logs request" @@ -569,7 +772,6 @@ def test_otel_logs_exporter_excludes_self_telemetry(): EXPORTER_VERSION < MINIMUM_SUPPORTED_VERSION, reason=f"OpenTelemetry exporter version {MINIMUM_SUPPORTED_VERSION} is required to export logs", ) -@pytest.mark.snapshot() @pytest.mark.subprocess(ddtrace_run=True, env={"DD_LOGS_OTEL_ENABLED": "true"}) def test_otel_logs_does_not_generate_client_grpc_spans(): """ @@ -579,10 +781,14 @@ def test_otel_logs_does_not_generate_client_grpc_spans(): from opentelemetry._logs import get_logger_provider + from ddtrace import tracer from tests.opentelemetry.test_logs import create_mock_grpc_server + from tests.utils import DummyWriter logger = getLogger() mock_service, server = create_mock_grpc_server() + writer = DummyWriter() + tracer._span_aggregator.writer = writer try: server.start() @@ -592,33 +798,41 @@ def test_otel_logs_does_not_generate_client_grpc_spans(): server.stop(2) assert mock_service.received_requests, "Expected gRPC log export requests but received none" + assert writer.pop_traces() == [] @pytest.mark.skipif( EXPORTER_VERSION < MINIMUM_SUPPORTED_VERSION, reason=f"OpenTelemetry exporter version {MINIMUM_SUPPORTED_VERSION} is required to export logs", ) -@pytest.mark.snapshot() @pytest.mark.subprocess( - ddtrace_run=True, env={"DD_LOGS_OTEL_ENABLED": "true", "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf"} + ddtrace_run=True, + env={ + "DD_LOGS_OTEL_ENABLED": "true", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:14319", + }, ) def test_otel_logs_does_not_generate_client_http_spans(): """Test that OpenTelemetry http logs exporter does not generate client spans.""" from logging import getLogger - from unittest.mock import Mock - from unittest.mock import patch from opentelemetry._logs import get_logger_provider - logger = getLogger() - with patch("requests.sessions.Session.request") as mock_request: - mock_request.return_value = Mock(status_code=200) + from ddtrace import tracer + from tests.opentelemetry.test_logs import create_mock_http_server + from tests.utils import DummyWriter + logger = getLogger() + requests, server = create_mock_http_server(14319) + writer = DummyWriter() + tracer._span_aggregator.writer = writer + try: + server.start() logger.error("test_otel_logs_http") get_logger_provider().force_flush() + finally: + server.stop(2) - log_request_found = any( - len(call[0]) >= 2 and call[0][0] == "POST" and "/v1/logs" in call[0][1] - for call in mock_request.call_args_list - ) - assert log_request_found, f"Expected HTTP log export request but found none: {mock_request.call_args_list}" + assert any("/v1/logs" in path for path, _ in requests), f"Expected HTTP log export request, found {requests}" + assert writer.pop_traces() == [] diff --git a/tests/opentelemetry/test_metrics.py b/tests/opentelemetry/test_metrics.py index ed10925646e..e05f2704f25 100644 --- a/tests/opentelemetry/test_metrics.py +++ b/tests/opentelemetry/test_metrics.py @@ -1,4 +1,9 @@ +from importlib import import_module +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version import os +import sys +from types import SimpleNamespace from opentelemetry import version import pytest @@ -7,39 +12,41 @@ OTEL_VERSION = tuple(int(x) for x in version.__version__.split(".")[:3]) +def _exporter_version(): + try: + version = package_version("opentelemetry-exporter-otlp-proto-common") + except PackageNotFoundError: + try: + from opentelemetry.exporter.otlp.proto.http.version import __version__ as version + except ImportError: + return (0, 0, 0) + return tuple(int(x) for x in version.split(".")[:3]) + + +EXPORTER_VERSION = _exporter_version() + + def skipif( - exporter_installed: bool = False, exporter_not_installed: bool = False, unsupported_otel_version: bool = False + exporter_installed: bool = False, + exporter_not_installed: bool = False, + unsupported_otel_version: bool = False, ): - """ - Returns a pytest skip marker based on OpenTelemetry version and exporter installation. - Parameters: - - exporter_installed: If True, skip tests that require OpenTelemetry exporters. - - exporter_not_installed: If True, skip tests that do not require OpenTelemetry exporters. - - unsupported_otel_version: If True, skip tests that require OpenTelemetry version 1.12 or higher. - - v1.12.0 is the first version that exposes metrics in the public API - """ if unsupported_otel_version and OTEL_VERSION < (1, 12): - return pytest.mark.skipif(True, reason="OpenTelemetry version 1.12 or higher is required for these tests") - + return pytest.mark.skipif(True, reason="OpenTelemetry 1.12 or newer is required") has_exporter = os.getenv("SDK_EXPORTER_INSTALLED", "").lower() in ("true", "1") if exporter_installed and has_exporter: - return pytest.mark.skipif(True, reason="Tests not compatible with the opentelemetry exporters") - elif exporter_not_installed and not has_exporter: - return pytest.mark.skipif(True, reason="Tests only compatible with the opentelemetry exporters") - return pytest.mark.skipif(False, reason="No skip condition met for OpenTelemetry logs exporter tests") + return pytest.mark.skipif(True, reason="Test requires an API-only OpenTelemetry environment") + if exporter_not_installed and not has_exporter: + return pytest.mark.skipif(True, reason="Test requires the OpenTelemetry metrics exporters") + return pytest.mark.skipif(False, reason="OpenTelemetry dependency set is compatible") @skipif(exporter_installed=True, unsupported_otel_version=True) def test_otel_metrics_sdk_not_installed_by_default(): - """ - Test that the OpenTelemetry metrics exporter can be set up correctly. - """ from ddtrace.internal.opentelemetry.metrics import set_otel_meter_provider - # This should not raise an ImportError set_otel_meter_provider() - # If the OpenTelemetry SDK is not installed with pytest.raises(ImportError): from opentelemetry.sdk.resources import Resource # noqa: F401 @@ -47,29 +54,12 @@ def test_otel_metrics_sdk_not_installed_by_default(): @skipif(exporter_not_installed=True, unsupported_otel_version=True) @pytest.mark.subprocess() def test_otel_metrics_exporter_installed(): - """ - Test that the OpenTelemetry metrics exporter can be set up correctly. - """ - from ddtrace.internal.opentelemetry.metrics import set_otel_meter_provider - - # This should not raise an ImportError - set_otel_meter_provider() + from ddtrace.internal.opentelemetry.exporters import get_metrics_exporter - # Check if the GRPC/protobuf exporter is available - try: - from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter - - assert OTLPMetricExporter() is not None - except ImportError: - pytest.fail("OTLPMetricExporter for gRPC protobuf should be available") - - # Check if HTTP/protobuf exporter is available - try: - from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter - - assert OTLPMetricExporter() is not None - except ImportError: - pytest.fail("OTLPMetricExporter for HTTP/protobuf should be available") + grpc_exporter = get_metrics_exporter("grpc")() + http_exporter = get_metrics_exporter("http/protobuf")() + grpc_exporter.shutdown() + http_exporter.shutdown() @skipif(exporter_not_installed=True, unsupported_otel_version=True) @@ -109,6 +99,298 @@ def test_otel_metrics_disabled_and_unset(): ) +def _metrics_data(): + from opentelemetry.sdk.metrics import MeterProvider + from opentelemetry.sdk.metrics.export import InMemoryMetricReader + from opentelemetry.sdk.resources import Resource + + reader = InMemoryMetricReader() + provider = MeterProvider(metric_readers=(reader,), resource=Resource.create({"service.name": "test"})) + provider.get_meter("test").create_counter("requests").add( + 2, + { + "route": "/items", + "cached": True, + "status": 200, + "ratio": 0.5, + "regions": ("us", "eu"), + }, + ) + return provider, reader.get_metrics_data() + + +def _attributes(request): + point = request.resource_metrics[0].scope_metrics[0].metrics[0].sum.data_points[0] + return {attribute.key: attribute.value for attribute in point.attributes} + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight gRPC exporter requires OpenTelemetry 1.18") +def test_grpclib_exporter_preserves_attribute_types_and_headers(monkeypatch): + from opentelemetry.sdk.metrics.export import MetricExportResult + + from ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter import OTLPMetricExporter + + requests = [] + calls = [] + + async def export(request, *, timeout, metadata): + requests.append(request) + calls.append((timeout, metadata)) + + provider, metrics_data = _metrics_data() + exporter = OTLPMetricExporter( + endpoint="http://127.0.0.1:4317", + headers="authorization=Bearer%20token,x-test=value", + timeout=3, + ) + monkeypatch.setattr(exporter, "_method", export) + try: + assert exporter.export(metrics_data) is MetricExportResult.SUCCESS + finally: + exporter.shutdown() + provider.shutdown() + + attributes = _attributes(requests[0]) + assert attributes["route"].string_value == "/items" + assert attributes["cached"].bool_value is True + assert attributes["status"].int_value == 200 + assert attributes["ratio"].double_value == 0.5 + assert [value.string_value for value in attributes["regions"].array_value.values] == ["us", "eu"] + assert calls[0][0] == pytest.approx(3, abs=0.1) + assert calls[0][1] == (("authorization", "Bearer token"), ("x-test", "value")) + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight HTTP exporter requires OpenTelemetry 1.18") +def test_http_exporter_preserves_attribute_types_headers_and_compression(monkeypatch): + import gzip + + from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ExportMetricsServiceRequest + from opentelemetry.sdk.metrics.export import MetricExportResult + + from ddtrace.internal.opentelemetry.exporters.http_metric_exporter import OTLPMetricExporter + + calls = [] + + def send(payload, timeout): + calls.append((payload, timeout)) + return 200, None + + provider, metrics_data = _metrics_data() + exporter = OTLPMetricExporter( + endpoint="http://127.0.0.1:4318/custom/metrics", + headers="authorization=Bearer%20token,x-test=value", + timeout=3, + compression="gzip", + ) + monkeypatch.setattr(exporter, "_send", send) + try: + assert exporter.export(metrics_data) is MetricExportResult.SUCCESS + finally: + exporter.shutdown() + provider.shutdown() + + request = ExportMetricsServiceRequest.FromString(gzip.decompress(calls[0][0])) + attributes = _attributes(request) + assert attributes["route"].string_value == "/items" + assert attributes["cached"].bool_value is True + assert attributes["status"].int_value == 200 + assert attributes["ratio"].double_value == 0.5 + assert [value.string_value for value in attributes["regions"].array_value.values] == ["us", "eu"] + assert calls[0][1] == pytest.approx(3, abs=0.1) + assert ("authorization", "Bearer token") in exporter._headers + assert ("x-test", "value") in exporter._headers + assert ("Content-Encoding", "gzip") in exporter._headers + assert exporter._path == "/custom/metrics" + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight gRPC exporter requires OpenTelemetry 1.18") +def test_grpclib_exporter_uses_otlp_temporality_preference(monkeypatch): + from opentelemetry.sdk.metrics._internal.instrument import Counter + from opentelemetry.sdk.metrics.export import AggregationTemporality + + from ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter import OTLPMetricExporter + + monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "delta") + exporter = OTLPMetricExporter() + try: + assert exporter._preferred_temporality[Counter] is AggregationTemporality.DELTA + finally: + exporter.shutdown() + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight gRPC exporter requires OpenTelemetry 1.18") +def test_grpclib_exporter_ignores_interpreter_shutdown_error(monkeypatch, caplog): + from opentelemetry.sdk.metrics.export import MetricExportResult + + from ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter import OTLPMetricExporter + + async def export(request, *, timeout, metadata): + raise RuntimeError("cannot schedule new futures after interpreter shutdown") + + provider, metrics_data = _metrics_data() + exporter = OTLPMetricExporter(endpoint="http://127.0.0.1:4317") + monkeypatch.setattr(exporter, "_method", export) + try: + assert exporter.export(metrics_data) is MetricExportResult.FAILURE + finally: + exporter.shutdown() + provider.shutdown() + + assert not [record for record in caplog.records if record.levelno >= 30] + + +@skipif(exporter_not_installed=True) +def test_resource_attributes_preserve_types(monkeypatch): + from ddtrace.internal.opentelemetry import metrics + + monkeypatch.setattr( + metrics, + "config", + SimpleNamespace( + tags={"enabled": True, "retries": 2, "regions": ("us", "eu")}, + service="test", + version=None, + env=None, + _report_hostname=False, + ), + ) + attributes = metrics._build_resource().attributes + + assert attributes["enabled"] is True + assert attributes["retries"] == 2 + assert attributes["regions"] == ("us", "eu") + + +@pytest.mark.parametrize( + ("protocol", "module"), + [ + ("grpc", "ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter"), + ("http/protobuf", "ddtrace.internal.opentelemetry.exporters.http_metric_exporter"), + ], +) +@skipif(exporter_not_installed=True) +def test_protocol_selects_exporter(protocol, module): + from ddtrace.internal.opentelemetry.exporters import get_metrics_exporter + + if EXPORTER_VERSION < (1, 18): + module = "ddtrace.internal.opentelemetry.exporters.exporter_telemetry" + assert get_metrics_exporter(protocol).__module__ == module + + +@pytest.mark.parametrize("protocol", ["grpc", "http/protobuf"]) +def test_exporter_resolution_without_proto_package_returns_none(monkeypatch, protocol): + from ddtrace.internal.opentelemetry import exporters + + def missing_exporter_version(): + raise ImportError("OpenTelemetry exporter is not installed") + + monkeypatch.setattr(exporters, "_exporter_version", missing_exporter_version) + + assert exporters.get_metrics_exporter(protocol) is None + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="Test requires the lightweight exporter selection path") +def test_grpc_protocol_without_grpclib_falls_back_to_upstream_exporter(monkeypatch): + from ddtrace.internal.opentelemetry import exporters + from ddtrace.internal.opentelemetry.exporters.exporter_telemetry import GRPCMetricsExporter + + monkeypatch.setitem(sys.modules, "ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter", None) + monkeypatch.setattr(exporters, "import_module", lambda name: SimpleNamespace()) + + assert exporters.get_metrics_exporter("grpc") is GRPCMetricsExporter + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="Test requires the lightweight exporter selection path") +def test_grpc_protocol_without_grpclib_or_upstream_exporter_returns_none(monkeypatch): + from ddtrace.internal.opentelemetry import exporters + + def missing_upstream_exporter(name): + raise ImportError("OpenTelemetry gRPC exporter is not installed") + + monkeypatch.setitem(sys.modules, "ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter", None) + monkeypatch.setattr(exporters, "import_module", missing_upstream_exporter) + + assert exporters.get_metrics_exporter("grpc") is None + + +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION >= (1, 18), reason="Test requires the upstream exporter fallback") +def test_upstream_exporter_fallback_records_telemetry(monkeypatch): + from opentelemetry.sdk.metrics.export import MetricExportResult + + from ddtrace.internal.opentelemetry.exporters import exporter_telemetry + from ddtrace.internal.opentelemetry.exporters import get_metrics_exporter + + telemetry = [] + monkeypatch.setattr( + exporter_telemetry.telemetry_writer, + "add_count_metric", + lambda namespace, name, value, tags: telemetry.append((name, value, tags)), + ) + + exporter = get_metrics_exporter("grpc")(endpoint="http://127.0.0.1:4317") + monkeypatch.setattr(exporter._exporter, "export", lambda *args, **kwargs: MetricExportResult.SUCCESS) + try: + assert exporter.export(object()) is MetricExportResult.SUCCESS + finally: + exporter.shutdown() + + tags = (("protocol", "grpc"), ("encoding", "protobuf")) + assert telemetry == [ + ("otel.metrics_export_attempts", 1, tags), + ("otel.metrics_export_successes", 1, tags), + ] + + +@pytest.mark.parametrize( + ("module_name", "protocol"), + [ + ("ddtrace.internal.opentelemetry.exporters.grpclib_metric_exporter", "grpc"), + ("ddtrace.internal.opentelemetry.exporters.http_metric_exporter", "http"), + ], +) +@skipif(exporter_not_installed=True) +@pytest.mark.skipif(EXPORTER_VERSION < (1, 18), reason="The lightweight exporters require OpenTelemetry 1.18") +def test_lightweight_exporter_records_telemetry(monkeypatch, module_name, protocol): + from opentelemetry.sdk.metrics.export import MetricExportResult + + from ddtrace.internal.opentelemetry.exporters import exporter_telemetry + from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE + + telemetry = [] + monkeypatch.setattr( + exporter_telemetry.telemetry_writer, + "add_count_metric", + lambda namespace, name, value, tags: telemetry.append((namespace, name, value, tags)), + ) + + module = import_module(module_name) + provider, metrics_data = _metrics_data() + exporter = module.OTLPMetricExporter(endpoint="http://127.0.0.1:4318") + results = iter((MetricExportResult.SUCCESS, MetricExportResult.FAILURE)) + monkeypatch.setattr(exporter, "_export", lambda *args, **kwargs: next(results)) + try: + assert exporter.export(metrics_data) is MetricExportResult.SUCCESS + assert exporter.export(metrics_data) is MetricExportResult.FAILURE + finally: + exporter.shutdown() + provider.shutdown() + + tags = (("protocol", protocol), ("encoding", "protobuf")) + assert telemetry == [ + (TELEMETRY_NAMESPACE.TRACERS, "otel.metrics_export_attempts", 1, tags), + (TELEMETRY_NAMESPACE.TRACERS, "otel.metrics_export_successes", 1, tags), + (TELEMETRY_NAMESPACE.TRACERS, "otel.metrics_export_attempts", 1, tags), + (TELEMETRY_NAMESPACE.TRACERS, "otel.metrics_export_failures", 1, tags), + ] + + @pytest.mark.subprocess( ddtrace_run=True, env={ diff --git a/tests/requirements_locks/1064582.txt b/tests/requirements_locks/1064582.txt deleted file mode 100644 index 19af4073418..00000000000 --- a/tests/requirements_locks/1064582.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/1064582.in -# -attrs==25.3.0 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 -opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.34.1 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==5.29.5 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/10d4104.txt b/tests/requirements_locks/10d4104.txt new file mode 100644 index 00000000000..e66595a1111 --- /dev/null +++ b/tests/requirements_locks/10d4104.txt @@ -0,0 +1,49 @@ +attrs==26.1.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +exceptiongroup==1.3.1 +flask==2.1.3 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.45.0 +opentelemetry-exporter-otlp-proto-common==1.45.0 +opentelemetry-instrumentation==0.66b0 +opentelemetry-instrumentation-flask==0.66b0 +opentelemetry-instrumentation-wsgi==0.66b0 +opentelemetry-proto==1.45.0 +opentelemetry-sdk==1.45.0 +opentelemetry-semantic-conventions==0.66b0 +opentelemetry-util-http==0.66b0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==7.36.2 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==2.5.0 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/52cc04c.txt b/tests/requirements_locks/10fe944.txt similarity index 62% rename from tests/requirements_locks/52cc04c.txt rename to tests/requirements_locks/10fe944.txt index becd362fa91..fb20cf2774b 100644 --- a/tests/requirements_locks/52cc04c.txt +++ b/tests/requirements_locks/10fe944.txt @@ -1,33 +1,27 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/52cc04c.in -# attrs==26.1.0 certifi==2026.7.22 charset-normalizer==2.1.1 -click==8.4.2 -coverage[toml]==7.15.4 +click==8.5.0 +coverage[toml]==7.16.2 exceptiongroup==1.3.1 flask==2.1.3 -gevent==26.8.0 -googleapis-common-protos==1.75.0 -greenlet==3.5.5 -grpcio==1.83.0 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 hypothesis==6.45.0 -idna==3.19 +idna==3.20 importlib-metadata==8.7.1 iniconfig==2.3.0 itsdangerous==2.2.0 jinja2==3.1.6 markupsafe==2.0.1 mock==5.2.0 +multidict==7.0.0 opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 opentelemetry-instrumentation==0.55b1 opentelemetry-instrumentation-flask==0.55b1 opentelemetry-instrumentation-wsgi==0.55b1 @@ -43,8 +37,8 @@ pygments==2.21.0 pytest==9.1.1 pytest-asyncio==0.21.1 pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 requests==2.28.1 sortedcontainers==2.4.0 tomli==2.4.1 @@ -52,6 +46,6 @@ typing-extensions==4.16.0 urllib3==1.26.20 werkzeug==2.1.2 wrapt==1.17.3 -zipp==4.1.0 +zipp==4.1.1 zope-event==6.2 -zope-interface==8.5 +zope-interface==8.6 diff --git a/tests/requirements_locks/14ddd29.txt b/tests/requirements_locks/14ddd29.txt new file mode 100644 index 00000000000..f4be7ee7999 --- /dev/null +++ b/tests/requirements_locks/14ddd29.txt @@ -0,0 +1,54 @@ +attrs==26.1.0 +backoff==2.2.1 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.6 +grpcio==1.84.0 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.34.1 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-exporter-otlp-proto-grpc==1.15.0 +opentelemetry-exporter-otlp-proto-http==1.15.0 +opentelemetry-instrumentation==0.55b1 +opentelemetry-instrumentation-flask==0.55b1 +opentelemetry-instrumentation-wsgi==0.55b1 +opentelemetry-proto==1.15.0 +opentelemetry-sdk==1.34.1 +opentelemetry-semantic-conventions==0.55b1 +opentelemetry-util-http==0.55b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==4.25.9 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==1.17.3 +zipp==4.1.1 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/16728b4.txt b/tests/requirements_locks/16728b4.txt new file mode 100644 index 00000000000..f4be7ee7999 --- /dev/null +++ b/tests/requirements_locks/16728b4.txt @@ -0,0 +1,54 @@ +attrs==26.1.0 +backoff==2.2.1 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.6 +grpcio==1.84.0 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.34.1 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-exporter-otlp-proto-grpc==1.15.0 +opentelemetry-exporter-otlp-proto-http==1.15.0 +opentelemetry-instrumentation==0.55b1 +opentelemetry-instrumentation-flask==0.55b1 +opentelemetry-instrumentation-wsgi==0.55b1 +opentelemetry-proto==1.15.0 +opentelemetry-sdk==1.34.1 +opentelemetry-semantic-conventions==0.55b1 +opentelemetry-util-http==0.55b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==4.25.9 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==1.17.3 +zipp==4.1.1 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/1a59a5f.txt b/tests/requirements_locks/1a59a5f.txt deleted file mode 100644 index 3e116ca1d29..00000000000 --- a/tests/requirements_locks/1a59a5f.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/1a59a5f.in -# -attrs==25.3.0 -backoff==2.2.1 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.15.0 -opentelemetry-exporter-otlp-proto-grpc==1.15.0 -opentelemetry-exporter-otlp-proto-http==1.15.0 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.15.0 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==4.25.8 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/1ca3564.txt b/tests/requirements_locks/1ca3564.txt deleted file mode 100644 index 18e6830736e..00000000000 --- a/tests/requirements_locks/1ca3564.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/1ca3564.in -# -attrs==25.3.0 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 -opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.34.1 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==5.29.5 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/1dc7dc3.txt b/tests/requirements_locks/1dc7dc3.txt new file mode 100644 index 00000000000..498533e6135 --- /dev/null +++ b/tests/requirements_locks/1dc7dc3.txt @@ -0,0 +1,48 @@ +attrs==26.1.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.45.0 +opentelemetry-exporter-otlp-proto-common==1.45.0 +opentelemetry-instrumentation==0.66b0 +opentelemetry-instrumentation-flask==0.66b0 +opentelemetry-instrumentation-wsgi==0.66b0 +opentelemetry-proto==1.45.0 +opentelemetry-sdk==1.45.0 +opentelemetry-semantic-conventions==0.66b0 +opentelemetry-util-http==0.66b0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==7.36.2 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==2.5.0 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/1df916a.txt b/tests/requirements_locks/1df916a.txt deleted file mode 100644 index 00175bdbf63..00000000000 --- a/tests/requirements_locks/1df916a.txt +++ /dev/null @@ -1,60 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/1df916a.in -# -attrs==25.3.0 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.1.8 -coverage[toml]==7.9.1 -exceptiongroup==1.3.0 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 -opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.34.1 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==5.29.5 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -tomli==2.2.1 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/c9de0b6.txt b/tests/requirements_locks/26721f5.txt similarity index 69% rename from tests/requirements_locks/c9de0b6.txt rename to tests/requirements_locks/26721f5.txt index 32942364d60..cf15b4d0342 100644 --- a/tests/requirements_locks/c9de0b6.txt +++ b/tests/requirements_locks/26721f5.txt @@ -1,9 +1,3 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/c9de0b6.in -# attrs==26.1.0 certifi==2026.7.22 charset-normalizer==2.1.1 @@ -12,22 +6,22 @@ coverage[toml]==7.10.7 exceptiongroup==1.3.1 flask==2.1.3 gevent==26.7.0 -googleapis-common-protos==1.75.0 greenlet==3.2.5 -grpcio==1.80.0 +grpclib==0.4.8 +h2==4.3.0 +hpack==4.1.0 +hyperframe==6.1.0 hypothesis==6.45.0 -idna==3.19 +idna==3.20 importlib-metadata==8.7.1 iniconfig==2.1.0 itsdangerous==2.2.0 jinja2==3.1.6 markupsafe==2.0.1 mock==5.2.0 +multidict==6.7.1 opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 opentelemetry-instrumentation==0.55b1 opentelemetry-instrumentation-flask==0.55b1 opentelemetry-instrumentation-wsgi==0.55b1 @@ -46,6 +40,7 @@ pytest-cov==7.1.0 pytest-mock==3.15.1 pytest-randomly==4.0.1 requests==2.28.1 +setuptools==82.0.1 sortedcontainers==2.4.0 tomli==2.4.1 typing-extensions==4.16.0 @@ -55,6 +50,3 @@ wrapt==1.17.3 zipp==3.23.1 zope-event==6.0 zope-interface==8.0.1 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==82.0.1 diff --git a/tests/requirements_locks/3b2045e.txt b/tests/requirements_locks/3b2045e.txt new file mode 100644 index 00000000000..c9ab0273cbc --- /dev/null +++ b/tests/requirements_locks/3b2045e.txt @@ -0,0 +1,55 @@ +attrs==26.1.0 +backoff==2.2.1 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.6 +grpcio==1.84.0 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.34.1 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-exporter-otlp-proto-grpc==1.15.0 +opentelemetry-exporter-otlp-proto-http==1.15.0 +opentelemetry-instrumentation==0.55b1 +opentelemetry-instrumentation-flask==0.55b1 +opentelemetry-instrumentation-wsgi==0.55b1 +opentelemetry-proto==1.15.0 +opentelemetry-sdk==1.34.1 +opentelemetry-semantic-conventions==0.55b1 +opentelemetry-util-http==0.55b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==4.25.9 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==1.17.3 +zipp==4.1.1 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/58ea88b.txt b/tests/requirements_locks/58ea88b.txt new file mode 100644 index 00000000000..b0d1643ebbd --- /dev/null +++ b/tests/requirements_locks/58ea88b.txt @@ -0,0 +1,57 @@ +attrs==26.1.0 +backoff==2.2.1 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.1.8 +coverage[toml]==7.10.7 +exceptiongroup==1.3.1 +flask==2.1.3 +gevent==26.7.0 +googleapis-common-protos==1.75.0 +greenlet==3.2.5 +grpcio==1.80.0 +grpclib==0.4.8 +h2==4.3.0 +hpack==4.1.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.1.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==6.7.1 +opentelemetry-api==1.34.1 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-exporter-otlp-proto-grpc==1.15.0 +opentelemetry-exporter-otlp-proto-http==1.15.0 +opentelemetry-instrumentation==0.55b1 +opentelemetry-instrumentation-flask==0.55b1 +opentelemetry-instrumentation-wsgi==0.55b1 +opentelemetry-proto==1.15.0 +opentelemetry-sdk==1.34.1 +opentelemetry-semantic-conventions==0.55b1 +opentelemetry-util-http==0.55b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==4.25.9 +pygments==2.21.0 +pytest==8.4.2 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +pytest-randomly==4.0.1 +requests==2.28.1 +setuptools==82.0.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==1.17.3 +zipp==3.23.1 +zope-event==6.0 +zope-interface==8.0.1 diff --git a/tests/requirements_locks/662817c.txt b/tests/requirements_locks/662817c.txt deleted file mode 100644 index 6c2173f1e5f..00000000000 --- a/tests/requirements_locks/662817c.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/662817c.in -# -attrs==25.3.0 -backoff==2.2.1 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.15.0 -opentelemetry-exporter-otlp-proto-grpc==1.15.0 -opentelemetry-exporter-otlp-proto-http==1.15.0 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.15.0 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==4.25.8 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/674c887.txt b/tests/requirements_locks/674c887.txt new file mode 100644 index 00000000000..9363c71e174 --- /dev/null +++ b/tests/requirements_locks/674c887.txt @@ -0,0 +1,52 @@ +attrs==26.1.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.1.8 +coverage[toml]==7.10.7 +exceptiongroup==1.3.1 +flask==2.1.3 +gevent==26.7.0 +greenlet==3.2.5 +grpclib==0.4.8 +h2==4.3.0 +hpack==4.1.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.1.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==6.7.1 +opentelemetry-api==1.41.1 +opentelemetry-exporter-otlp-proto-common==1.41.1 +opentelemetry-instrumentation==0.62b1 +opentelemetry-instrumentation-flask==0.62b1 +opentelemetry-instrumentation-wsgi==0.62b1 +opentelemetry-proto==1.41.1 +opentelemetry-sdk==1.41.1 +opentelemetry-semantic-conventions==0.62b1 +opentelemetry-util-http==0.62b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==6.33.6 +pygments==2.21.0 +pytest==8.4.2 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +pytest-randomly==4.0.1 +requests==2.28.1 +setuptools==82.0.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==2.5.0 +zipp==3.23.1 +zope-event==6.0 +zope-interface==8.0.1 diff --git a/tests/requirements_locks/6980d7a.txt b/tests/requirements_locks/6980d7a.txt deleted file mode 100644 index 89c4d32024a..00000000000 --- a/tests/requirements_locks/6980d7a.txt +++ /dev/null @@ -1,60 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/6980d7a.in -# -attrs==25.3.0 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -exceptiongroup==1.3.0 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 -opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.34.1 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==5.29.5 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -tomli==2.2.1 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/4b40218.txt b/tests/requirements_locks/862257a.txt similarity index 61% rename from tests/requirements_locks/4b40218.txt rename to tests/requirements_locks/862257a.txt index 8541ae46b15..34d2baac11d 100644 --- a/tests/requirements_locks/4b40218.txt +++ b/tests/requirements_locks/862257a.txt @@ -1,32 +1,26 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/4b40218.in -# attrs==26.1.0 certifi==2026.7.22 charset-normalizer==2.1.1 -click==8.4.2 -coverage[toml]==7.15.4 +click==8.5.0 +coverage[toml]==7.16.2 flask==2.1.3 -gevent==26.8.0 -googleapis-common-protos==1.75.0 -greenlet==3.5.5 -grpcio==1.83.0 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 hypothesis==6.45.0 -idna==3.19 +idna==3.20 importlib-metadata==8.7.1 iniconfig==2.3.0 itsdangerous==2.2.0 jinja2==3.1.6 markupsafe==2.0.1 mock==5.2.0 +multidict==7.0.0 opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 opentelemetry-instrumentation==0.55b1 opentelemetry-instrumentation-flask==0.55b1 opentelemetry-instrumentation-wsgi==0.55b1 @@ -42,14 +36,14 @@ pygments==2.21.0 pytest==9.1.1 pytest-asyncio==0.21.1 pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 requests==2.28.1 sortedcontainers==2.4.0 typing-extensions==4.16.0 urllib3==1.26.20 werkzeug==2.1.2 wrapt==1.17.3 -zipp==4.1.0 +zipp==4.1.1 zope-event==6.2 -zope-interface==8.5 +zope-interface==8.6 diff --git a/tests/requirements_locks/89f632a.txt b/tests/requirements_locks/89f632a.txt deleted file mode 100644 index 50a5131b04e..00000000000 --- a/tests/requirements_locks/89f632a.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/89f632a.in -# -attrs==25.3.0 -backoff==2.2.1 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.15.0 -opentelemetry-exporter-otlp-proto-grpc==1.15.0 -opentelemetry-exporter-otlp-proto-http==1.15.0 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.15.0 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==4.25.8 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/8cd7168.txt b/tests/requirements_locks/8cd7168.txt deleted file mode 100644 index 403cc21f4d4..00000000000 --- a/tests/requirements_locks/8cd7168.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/8cd7168.in -# -attrs==25.3.0 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 -opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.34.1 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==5.29.5 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/8f4eb1c.txt b/tests/requirements_locks/8f4eb1c.txt new file mode 100644 index 00000000000..b446b9ca067 --- /dev/null +++ b/tests/requirements_locks/8f4eb1c.txt @@ -0,0 +1,56 @@ +attrs==26.1.0 +backoff==2.2.1 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +exceptiongroup==1.3.1 +flask==2.1.3 +gevent==26.9.0 +googleapis-common-protos==1.75.0 +greenlet==3.5.6 +grpcio==1.84.0 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +importlib-metadata==8.7.1 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.34.1 +opentelemetry-exporter-otlp==1.15.0 +opentelemetry-exporter-otlp-proto-grpc==1.15.0 +opentelemetry-exporter-otlp-proto-http==1.15.0 +opentelemetry-instrumentation==0.55b1 +opentelemetry-instrumentation-flask==0.55b1 +opentelemetry-instrumentation-wsgi==0.55b1 +opentelemetry-proto==1.15.0 +opentelemetry-sdk==1.34.1 +opentelemetry-semantic-conventions==0.55b1 +opentelemetry-util-http==0.55b1 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==4.25.9 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +tomli==2.4.1 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==1.17.3 +zipp==4.1.1 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/1a8f71a.txt b/tests/requirements_locks/95d66a5.txt similarity index 61% rename from tests/requirements_locks/1a8f71a.txt rename to tests/requirements_locks/95d66a5.txt index 79ba36c1054..34d2baac11d 100644 --- a/tests/requirements_locks/1a8f71a.txt +++ b/tests/requirements_locks/95d66a5.txt @@ -1,32 +1,26 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/1a8f71a.in -# attrs==26.1.0 certifi==2026.7.22 charset-normalizer==2.1.1 -click==8.4.2 -coverage[toml]==7.15.4 +click==8.5.0 +coverage[toml]==7.16.2 flask==2.1.3 -gevent==26.8.0 -googleapis-common-protos==1.75.0 -greenlet==3.5.5 -grpcio==1.83.0 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 hypothesis==6.45.0 -idna==3.19 +idna==3.20 importlib-metadata==8.7.1 iniconfig==2.3.0 itsdangerous==2.2.0 jinja2==3.1.6 markupsafe==2.0.1 mock==5.2.0 +multidict==7.0.0 opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 opentelemetry-instrumentation==0.55b1 opentelemetry-instrumentation-flask==0.55b1 opentelemetry-instrumentation-wsgi==0.55b1 @@ -42,14 +36,14 @@ pygments==2.21.0 pytest==9.1.1 pytest-asyncio==0.21.1 pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 requests==2.28.1 sortedcontainers==2.4.0 typing-extensions==4.16.0 urllib3==1.26.20 werkzeug==2.1.2 wrapt==1.17.3 -zipp==4.1.0 +zipp==4.1.1 zope-event==6.2 -zope-interface==8.5 +zope-interface==8.6 diff --git a/tests/requirements_locks/a310e10.txt b/tests/requirements_locks/a310e10.txt new file mode 100644 index 00000000000..7a11726e330 --- /dev/null +++ b/tests/requirements_locks/a310e10.txt @@ -0,0 +1,47 @@ +attrs==26.1.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.45.0 +opentelemetry-exporter-otlp-proto-common==1.45.0 +opentelemetry-instrumentation==0.66b0 +opentelemetry-instrumentation-flask==0.66b0 +opentelemetry-instrumentation-wsgi==0.66b0 +opentelemetry-proto==1.45.0 +opentelemetry-sdk==1.45.0 +opentelemetry-semantic-conventions==0.66b0 +opentelemetry-util-http==0.66b0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==7.36.2 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==2.5.0 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/af0131c.txt b/tests/requirements_locks/af0131c.txt new file mode 100644 index 00000000000..fa01a760117 --- /dev/null +++ b/tests/requirements_locks/af0131c.txt @@ -0,0 +1,48 @@ +attrs==26.1.0 +blinker==1.9.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==3.1.3 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==3.0.4 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.45.0 +opentelemetry-exporter-otlp-proto-common==1.45.0 +opentelemetry-instrumentation==0.66b0 +opentelemetry-instrumentation-flask==0.66b0 +opentelemetry-instrumentation-wsgi==0.66b0 +opentelemetry-proto==1.45.0 +opentelemetry-sdk==1.45.0 +opentelemetry-semantic-conventions==0.66b0 +opentelemetry-util-http==0.66b0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==7.36.2 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==3.1.9 +wrapt==2.5.0 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/requirements_locks/cb2ca5e.txt b/tests/requirements_locks/cb2ca5e.txt deleted file mode 100644 index 4f926f7c3d1..00000000000 --- a/tests/requirements_locks/cb2ca5e.txt +++ /dev/null @@ -1,54 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.14 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/cb2ca5e.in -# -attrs==26.1.0 -blinker==1.9.0 -certifi==2026.7.22 -charset-normalizer==2.1.1 -click==8.4.2 -coverage[toml]==7.15.4 -flask==3.1.3 -gevent==26.8.0 -googleapis-common-protos==1.75.1 -greenlet==3.5.5 -grpcio==1.83.0 -hypothesis==6.45.0 -idna==3.19 -iniconfig==2.3.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==3.0.3 -mock==5.2.0 -opentelemetry-api==1.44.0 -opentelemetry-exporter-otlp==1.44.0 -opentelemetry-exporter-otlp-proto-common==1.44.0 -opentelemetry-exporter-otlp-proto-grpc==1.44.0 -opentelemetry-exporter-otlp-proto-http==1.44.0 -opentelemetry-instrumentation==0.65b0 -opentelemetry-instrumentation-flask==0.65b0 -opentelemetry-instrumentation-wsgi==0.65b0 -opentelemetry-proto==1.44.0 -opentelemetry-sdk==1.44.0 -opentelemetry-semantic-conventions==0.65b0 -opentelemetry-util-http==0.65b0 -opentracing==2.4.0 -packaging==26.3 -pluggy==1.6.0 -protobuf==7.35.1 -pygments==2.21.0 -pytest==9.1.1 -pytest-asyncio==0.21.1 -pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 -requests==2.28.1 -sortedcontainers==2.4.0 -typing-extensions==4.16.0 -urllib3==1.26.20 -werkzeug==3.1.8 -wrapt==2.3.0 -zope-event==6.2 -zope-interface==8.5 diff --git a/tests/requirements_locks/8e6df85.txt b/tests/requirements_locks/d1c30a4.txt similarity index 61% rename from tests/requirements_locks/8e6df85.txt rename to tests/requirements_locks/d1c30a4.txt index 0c881628cd8..3979825c67f 100644 --- a/tests/requirements_locks/8e6df85.txt +++ b/tests/requirements_locks/d1c30a4.txt @@ -1,32 +1,26 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/8e6df85.in -# attrs==26.1.0 certifi==2026.7.22 charset-normalizer==2.1.1 -click==8.4.2 -coverage[toml]==7.15.4 +click==8.5.0 +coverage[toml]==7.16.2 flask==2.1.3 -gevent==26.8.0 -googleapis-common-protos==1.75.0 -greenlet==3.5.5 -grpcio==1.83.0 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 hypothesis==6.45.0 -idna==3.19 +idna==3.20 importlib-metadata==8.7.1 iniconfig==2.3.0 itsdangerous==2.2.0 jinja2==3.1.6 markupsafe==2.0.1 mock==5.2.0 +multidict==7.0.0 opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.34.1 opentelemetry-exporter-otlp-proto-common==1.34.1 -opentelemetry-exporter-otlp-proto-grpc==1.34.1 -opentelemetry-exporter-otlp-proto-http==1.34.1 opentelemetry-instrumentation==0.55b1 opentelemetry-instrumentation-flask==0.55b1 opentelemetry-instrumentation-wsgi==0.55b1 @@ -42,14 +36,15 @@ pygments==2.21.0 pytest==9.1.1 pytest-asyncio==0.21.1 pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 requests==2.28.1 sortedcontainers==2.4.0 +tomli==2.4.1 typing-extensions==4.16.0 urllib3==1.26.20 werkzeug==2.1.2 wrapt==1.17.3 -zipp==4.1.0 +zipp==4.1.1 zope-event==6.2 -zope-interface==8.5 +zope-interface==8.6 diff --git a/tests/requirements_locks/e4226c8.txt b/tests/requirements_locks/e4226c8.txt deleted file mode 100644 index 816ff233485..00000000000 --- a/tests/requirements_locks/e4226c8.txt +++ /dev/null @@ -1,60 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/e4226c8.in -# -attrs==25.3.0 -backoff==2.2.1 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.2.1 -coverage[toml]==7.9.1 -exceptiongroup==1.3.0 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.15.0 -opentelemetry-exporter-otlp-proto-grpc==1.15.0 -opentelemetry-exporter-otlp-proto-http==1.15.0 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.15.0 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==4.25.8 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -tomli==2.2.1 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/eb2f2a5.txt b/tests/requirements_locks/eb2f2a5.txt deleted file mode 100644 index f243b9687d5..00000000000 --- a/tests/requirements_locks/eb2f2a5.txt +++ /dev/null @@ -1,60 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate tests/requirements_locks/eb2f2a5.in -# -attrs==25.3.0 -backoff==2.2.1 -certifi==2025.6.15 -charset-normalizer==2.1.1 -click==8.1.8 -coverage[toml]==7.9.1 -exceptiongroup==1.3.0 -flask==2.1.3 -gevent==25.5.1 -googleapis-common-protos==1.70.0 -greenlet==3.2.3 -grpcio==1.73.1 -hypothesis==6.45.0 -idna==3.10 -importlib-metadata==8.7.0 -iniconfig==2.1.0 -itsdangerous==2.2.0 -jinja2==3.1.6 -markupsafe==2.0.1 -mock==5.2.0 -opentelemetry-api==1.34.1 -opentelemetry-exporter-otlp==1.15.0 -opentelemetry-exporter-otlp-proto-grpc==1.15.0 -opentelemetry-exporter-otlp-proto-http==1.15.0 -opentelemetry-instrumentation==0.55b1 -opentelemetry-instrumentation-flask==0.55b1 -opentelemetry-instrumentation-wsgi==0.55b1 -opentelemetry-proto==1.15.0 -opentelemetry-sdk==1.34.1 -opentelemetry-semantic-conventions==0.55b1 -opentelemetry-util-http==0.55b1 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -protobuf==4.25.8 -pygments==2.19.2 -pytest==8.4.1 -pytest-asyncio==0.21.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -pytest-randomly==3.16.0 -requests==2.28.1 -sortedcontainers==2.4.0 -tomli==2.2.1 -typing-extensions==4.14.0 -urllib3==1.26.20 -werkzeug==2.1.2 -wrapt==1.17.2 -zipp==3.23.0 -zope-event==5.1 -zope-interface==7.2 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 diff --git a/tests/requirements_locks/fe4d7e7.txt b/tests/requirements_locks/fe4d7e7.txt new file mode 100644 index 00000000000..7a11726e330 --- /dev/null +++ b/tests/requirements_locks/fe4d7e7.txt @@ -0,0 +1,47 @@ +attrs==26.1.0 +certifi==2026.7.22 +charset-normalizer==2.1.1 +click==8.5.0 +coverage[toml]==7.16.2 +flask==2.1.3 +gevent==26.9.0 +greenlet==3.5.6 +grpclib==0.4.9 +h2==4.4.1 +hpack==4.2.0 +hyperframe==6.1.0 +hypothesis==6.45.0 +idna==3.20 +iniconfig==2.3.0 +itsdangerous==2.2.0 +jinja2==3.1.6 +markupsafe==2.0.1 +mock==5.2.0 +multidict==7.0.0 +opentelemetry-api==1.45.0 +opentelemetry-exporter-otlp-proto-common==1.45.0 +opentelemetry-instrumentation==0.66b0 +opentelemetry-instrumentation-flask==0.66b0 +opentelemetry-instrumentation-wsgi==0.66b0 +opentelemetry-proto==1.45.0 +opentelemetry-sdk==1.45.0 +opentelemetry-semantic-conventions==0.66b0 +opentelemetry-util-http==0.66b0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +protobuf==7.36.2 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==0.21.1 +pytest-cov==7.1.0 +pytest-mock==3.16.0 +pytest-randomly==5.0.0 +requests==2.28.1 +sortedcontainers==2.4.0 +typing-extensions==4.16.0 +urllib3==1.26.20 +werkzeug==2.1.2 +wrapt==2.5.0 +zope-event==6.2 +zope-interface==8.6 diff --git a/tests/snapshots/tests.opentelemetry.test_logs.test_otel_logs_does_not_generate_client_grpc_spans.json b/tests/snapshots/tests.opentelemetry.test_logs.test_otel_logs_does_not_generate_client_grpc_spans.json deleted file mode 100644 index 8c89cf25775..00000000000 --- a/tests/snapshots/tests.opentelemetry.test_logs.test_otel_logs_does_not_generate_client_grpc_spans.json +++ /dev/null @@ -1,37 +0,0 @@ -[[ - { - "name": "grpc", - "service": "grpc-server", - "resource": "/opentelemetry.proto.collector.logs.v1.LogsService/Export", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "grpc", - "error": 0, - "meta": { - "_dd.base_service": "ddtrace_subprocess_dir", - "_dd.p.dm": "-0", - "_dd.p.tid": "69f39d6500000000", - "_dd.svc_src": "grpc_server", - "_dd.tags.process": "entrypoint.basedir:ddtrace_subprocess_dir,entrypoint.name:tmpbie0i_uo,entrypoint.type:script,entrypoint.workdir:project,svc.auto:ddtrace_subprocess_dir", - "component": "grpc_server", - "grpc.method.kind": "unary", - "grpc.method.name": "Export", - "grpc.method.package": "opentelemetry.proto.collector.logs.v1", - "grpc.method.path": "/opentelemetry.proto.collector.logs.v1.LogsService/Export", - "grpc.method.service": "LogsService", - "language": "python", - "rpc.service": "opentelemetry.proto.collector.logs.v1.LogsService", - "runtime-id": "365ee0ef1260468f881674ad7bbc49eb", - "span.kind": "server" - }, - "metrics": { - "_dd.measured": 1.0, - "_dd.top_level": 1.0, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1.0, - "process_id": 60674.0 - }, - "duration": 98375, - "start": 1777573221412419960 - }]]