From 95c4abf336c09b2c531b7a6c71aa7077055198da Mon Sep 17 00:00:00 2001 From: jayeshhire Date: Wed, 1 Apr 2026 22:16:48 +0530 Subject: [PATCH 1/3] Enabled the flake8-type-checking plugin rules for the ruff linter --- .../opentelemetry/codegen/json/generator.py | 6 ++++-- .../src/opentelemetry/codegen/json/writer.py | 6 ++++-- .../otlp/proto/common/_internal/__init__.py | 9 ++++++--- .../proto/grpc/metric_exporter/__init__.py | 8 +++++--- .../proto/http/metric_exporter/__init__.py | 13 ++++++++----- .../exporter/prometheus/__init__.py | 6 ++++-- .../src/opentelemetry/_events/__init__.py | 6 ++++-- .../opentelemetry/_logs/_internal/__init__.py | 12 +++++++----- .../src/opentelemetry/context/__init__.py | 4 +++- .../src/opentelemetry/context/context.py | 4 +++- .../metrics/_internal/instrument.py | 5 ++++- .../src/opentelemetry/util/_providers.py | 4 ++-- .../sdk/_configuration/_propagator.py | 18 ++++++++++-------- .../sdk/_logs/_internal/__init__.py | 16 +++++++++++++--- .../sdk/metrics/_internal/__init__.py | 6 ++++-- .../sdk/metrics/_internal/export/__init__.py | 8 +++++--- .../sdk/metrics/_internal/instrument.py | 8 +++++--- .../metrics/_internal/measurement_consumer.py | 6 ++++-- .../metrics/_internal/metric_reader_storage.py | 6 ++++-- .../sdk/metrics/_internal/point.py | 6 ++++-- .../sdk/metrics/_internal/sdk_configuration.py | 6 ++++-- .../opentelemetry/sdk/resources/__init__.py | 2 +- .../src/opentelemetry/sdk/trace/__init__.py | 2 +- .../_sampling_experimental/_always_off.py | 11 ++++++----- .../trace/_sampling_experimental/_always_on.py | 11 ++++++----- .../_sampling_experimental/_composable.py | 9 +++++---- .../_parent_threshold.py | 8 +++++--- .../_sampling_experimental/_rule_based.py | 11 ++++++----- .../trace/_sampling_experimental/_sampler.py | 11 +++++++---- .../_sampling_experimental/_trace_state.py | 7 ++++--- .../_sampling_experimental/_traceid_ratio.py | 11 ++++++----- .../opentelemetry/sdk/trace/_tracer_metrics.py | 10 +++++++--- .../src/opentelemetry/sdk/trace/sampling.py | 6 ++++-- ...exponential_bucket_histogram_aggregation.py | 9 ++++++--- .../test_periodic_exporting_metric_reader.py | 2 +- opentelemetry-sdk/tests/test_configurator.py | 14 ++++++++------ pyproject.toml | 9 +++++++++ .../shim/opentracing_shim/__init__.py | 9 ++++++--- 38 files changed, 195 insertions(+), 110 deletions(-) diff --git a/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/generator.py b/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/generator.py index 5893dbd4453..eadd2961ce8 100644 --- a/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/generator.py +++ b/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/generator.py @@ -18,9 +18,8 @@ import logging from collections import defaultdict -from collections.abc import Iterable from pathlib import Path -from typing import Callable, Final, Optional, Set +from typing import TYPE_CHECKING, Callable, Final, Optional, Set from google.protobuf import descriptor_pb2 as descriptor from google.protobuf.compiler import plugin_pb2 as plugin @@ -37,6 +36,9 @@ from opentelemetry.codegen.json.version import __version__ as GENERATOR_VERSION from opentelemetry.codegen.json.writer import CodeWriter +if TYPE_CHECKING: + from collections.abc import Iterable + _logger = logging.getLogger(__name__) CODEC_MODULE_NAME: Final[str] = "_json_codec" diff --git a/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/writer.py b/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/writer.py index 687de172d5b..aafc287d35d 100644 --- a/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/writer.py +++ b/codegen/opentelemetry-codegen-json/src/opentelemetry/codegen/json/writer.py @@ -14,9 +14,11 @@ from __future__ import annotations -from collections.abc import Iterable from contextlib import contextmanager -from typing import Any, Generator, Optional, Union +from typing import TYPE_CHECKING, Any, Generator, Optional, Union + +if TYPE_CHECKING: + from collections.abc import Iterable # pylint: disable-next=too-many-public-methods diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 200644368df..97a8777356c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -18,6 +18,7 @@ import logging from collections.abc import Sequence from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -41,9 +42,11 @@ from opentelemetry.proto.resource.v1.resource_pb2 import ( Resource as PB2Resource, ) -from opentelemetry.sdk.trace import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope -from opentelemetry.util.types import _ExtendedAttributes + +if TYPE_CHECKING: + from opentelemetry.sdk.trace import Resource + from opentelemetry.sdk.util.instrumentation import InstrumentationScope + from opentelemetry.util.types import _ExtendedAttributes _logger = logging.getLogger(__name__) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py index af77f6d1239..bded41e9636 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/metric_exporter/__init__.py @@ -16,10 +16,9 @@ from dataclasses import replace from logging import getLogger from os import environ -from typing import Iterable, List, Tuple, Union +from typing import TYPE_CHECKING, Iterable, List, Tuple, Union from typing import Sequence as TypingSequence -from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import ( OTLPMetricExporterMixin, ) @@ -53,7 +52,6 @@ OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, ) -from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, DataPointT, @@ -73,6 +71,10 @@ Histogram as HistogramType, ) +if TYPE_CHECKING: + from grpc import ChannelCredentials, Compression + from opentelemetry.sdk.metrics._internal.aggregation import Aggregation + _logger = getLogger(__name__) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 4ad5caee6a0..845b5318ade 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -21,6 +21,7 @@ from os import environ from time import time from typing import ( # noqa: F401 + TYPE_CHECKING, Any, Callable, Dict, @@ -62,9 +63,6 @@ ) from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 -from opentelemetry.proto.resource.v1.resource_pb2 import ( - Resource as PB2Resource, -) from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, @@ -82,7 +80,6 @@ OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, ) -from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 AggregationTemporality, Gauge, @@ -94,9 +91,15 @@ from opentelemetry.sdk.metrics.export import ( # noqa: F401 Histogram as HistogramType, ) -from opentelemetry.sdk.resources import Resource as SDKResource from opentelemetry.util.re import parse_env_headers +if TYPE_CHECKING: + from opentelemetry.proto.resource.v1.resource_pb2 import ( + Resource as PB2Resource, + ) + from opentelemetry.sdk.metrics._internal.aggregation import Aggregation + from opentelemetry.sdk.resources import Resource as SDKResource + _logger = logging.getLogger(__name__) diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 608d8f6d302..a73ffb8e14c 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -67,7 +67,7 @@ from json import dumps from logging import getLogger from os import environ -from typing import Deque, Dict, Iterable, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Deque, Dict, Iterable, Sequence, Tuple, Union from prometheus_client import start_http_server from prometheus_client.core import ( @@ -108,7 +108,9 @@ from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) -from opentelemetry.util.types import Attributes + +if TYPE_CHECKING: + from opentelemetry.util.types import Attributes _logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index d86fd12721d..f7ef8833092 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -15,7 +15,7 @@ from abc import ABC, abstractmethod from logging import getLogger from os import environ -from typing import Optional, cast +from typing import TYPE_CHECKING, Optional, cast from typing_extensions import deprecated @@ -24,11 +24,13 @@ from opentelemetry.environment_variables import ( _OTEL_PYTHON_EVENT_LOGGER_PROVIDER, ) -from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider from opentelemetry.util.types import AnyValue, _ExtendedAttributes +if TYPE_CHECKING: + from opentelemetry.trace.span import TraceFlags + _logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index bbcfcddc846..0f054116eab 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -39,19 +39,21 @@ from logging import getLogger from os import environ from time import time_ns -from typing import Optional, cast, overload +from typing import TYPE_CHECKING, Optional, cast, overload from typing_extensions import deprecated -from opentelemetry._logs.severity import SeverityNumber from opentelemetry.context import get_current -from opentelemetry.context.context import Context from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace import get_current_span -from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider -from opentelemetry.util.types import AnyValue, _ExtendedAttributes + +if TYPE_CHECKING: + from opentelemetry._logs.severity import SeverityNumber + from opentelemetry.context.context import Context + from opentelemetry.trace.span import TraceFlags + from opentelemetry.util.types import AnyValue, _ExtendedAttributes _logger = getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 39772554d5a..dc030c0a4f1 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -16,7 +16,6 @@ import logging import typing -from contextvars import Token from os import environ from uuid import uuid4 @@ -25,6 +24,9 @@ from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT from opentelemetry.util._importlib_metadata import entry_points +if typing.TYPE_CHECKING: + from contextvars import Token + logger = logging.getLogger(__name__) diff --git a/opentelemetry-api/src/opentelemetry/context/context.py b/opentelemetry-api/src/opentelemetry/context/context.py index c1ef9cfbb6b..32593b2fd05 100644 --- a/opentelemetry-api/src/opentelemetry/context/context.py +++ b/opentelemetry-api/src/opentelemetry/context/context.py @@ -16,7 +16,9 @@ import typing from abc import ABC, abstractmethod -from contextvars import Token + +if typing.TYPE_CHECKING: + from contextvars import Token class Context(typing.Dict[str, object]): diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index cfd7a1526c6..0efea702661 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -20,6 +20,7 @@ from logging import getLogger from re import compile as re_compile from typing import ( + TYPE_CHECKING, Callable, Dict, Generator, @@ -32,13 +33,15 @@ ) # pylint: disable=unused-import; needed for typing and sphinx -from opentelemetry import metrics from opentelemetry.context import Context from opentelemetry.metrics._internal.observation import Observation from opentelemetry.util.types import ( Attributes, ) +if TYPE_CHECKING: + from opentelemetry import metrics + _logger = getLogger(__name__) _name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}") diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index b748eadfe0a..030ad4a2a5d 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -32,12 +32,12 @@ def _load_provider( ) -> Provider: # type: ignore[type-var] try: provider_name = cast( - str, + "str", environ.get(provider_environment_variable, f"default_{provider}"), ) return cast( - Provider, + "Provider", next( # type: ignore iter( # type: ignore entry_points( # type: ignore diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py index 60cc904f137..7e6c8312249 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py @@ -14,24 +14,26 @@ from __future__ import annotations -from typing import Optional +from typing import TYPE_CHECKING, Optional from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.composite import CompositePropagator -from opentelemetry.propagators.textmap import TextMapPropagator from opentelemetry.sdk._configuration._exceptions import ConfigurationError -from opentelemetry.sdk._configuration.models import ( - Propagator as PropagatorConfig, -) -from opentelemetry.sdk._configuration.models import ( - TextMapPropagator as TextMapPropagatorConfig, -) from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) from opentelemetry.util._importlib_metadata import entry_points +if TYPE_CHECKING: + from opentelemetry.propagators.textmap import TextMapPropagator + from opentelemetry.sdk._configuration.models import ( + Propagator as PropagatorConfig, + ) + from opentelemetry.sdk._configuration.models import ( + TextMapPropagator as TextMapPropagatorConfig, + ) + def _load_entry_point_propagator(name: str) -> TextMapPropagator: """Load a propagator by name from the opentelemetry_propagator entry point group.""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index d6a4aa16ab0..b26f268a902 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -26,7 +26,15 @@ from os import environ from threading import Lock from time import time_ns -from typing import Any, Callable, Tuple, Union, cast, overload # noqa +from typing import ( # noqa + TYPE_CHECKING, + Any, + Callable, + Tuple, + Union, + cast, + overload, +) from typing_extensions import deprecated @@ -41,7 +49,6 @@ ) from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes from opentelemetry.context import get_current -from opentelemetry.context.context import Context from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.sdk._logs._internal._logger_metrics import LoggerMetrics from opentelemetry.sdk.environment_variables import ( @@ -58,7 +65,10 @@ format_span_id, format_trace_id, ) -from opentelemetry.util.types import AnyValue, _ExtendedAttributes + +if TYPE_CHECKING: + from opentelemetry.context.context import Context + from opentelemetry.util.types import AnyValue, _ExtendedAttributes _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 039f1dfb7d2..3943ec1feb9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -18,10 +18,9 @@ from os import environ from threading import Lock from time import time_ns -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk.metrics from opentelemetry.metrics import Counter as APICounter from opentelemetry.metrics import Histogram as APIHistogram from opentelemetry.metrics import Meter as APIMeter @@ -68,6 +67,9 @@ Attributes, ) +if TYPE_CHECKING: + import opentelemetry.sdk.metrics + _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 72b59280289..44efde20c7b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -23,12 +23,11 @@ from sys import stdout from threading import Event, Lock, RLock, Thread from time import perf_counter, time_ns -from typing import IO, Callable, Iterable, Optional +from typing import IO, TYPE_CHECKING, Callable, Iterable, Optional from typing_extensions import final # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk.metrics._internal from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, @@ -61,7 +60,6 @@ _ObservableUpDownCounter, _UpDownCounter, ) -from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, ) @@ -69,6 +67,10 @@ from ._metric_reader_metrics import MetricReaderMetrics +if TYPE_CHECKING: + import opentelemetry.sdk.metrics._internal + from opentelemetry.sdk.metrics._internal.point import MetricsData + _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index b01578f47ca..88f7077d6f3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -17,10 +17,9 @@ from logging import getLogger from time import time_ns -from typing import Generator, Iterable, List, Sequence, Union +from typing import TYPE_CHECKING, Generator, Iterable, List, Sequence, Union # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk.metrics from opentelemetry.context import Context, get_current from opentelemetry.metrics import CallbackT from opentelemetry.metrics import Counter as APICounter @@ -37,7 +36,10 @@ _MetricsHistogramAdvisory, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement -from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +if TYPE_CHECKING: + import opentelemetry.sdk.metrics + from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index 43ebc3d345e..33dea470c15 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -17,12 +17,11 @@ from abc import ABC, abstractmethod from threading import Lock from time import time_ns -from typing import List, Mapping, Optional +from typing import TYPE_CHECKING, List, Mapping, Optional # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics import opentelemetry.sdk.metrics._internal.instrument -import opentelemetry.sdk.metrics._internal.sdk_configuration from opentelemetry.metrics._internal.instrument import CallbackOptions from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.measurement import Measurement @@ -31,6 +30,9 @@ ) from opentelemetry.sdk.metrics._internal.point import MetricsData +if TYPE_CHECKING: + import opentelemetry.sdk.metrics._internal.sdk_configuration + class MeasurementConsumer(ABC): @abstractmethod diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py index 317fda0b420..739ba6ca7cb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py @@ -15,7 +15,7 @@ from logging import getLogger from threading import RLock from time import time_ns -from typing import Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from opentelemetry.metrics import ( Asynchronous, @@ -51,7 +51,9 @@ SdkConfiguration, ) from opentelemetry.sdk.metrics._internal.view import View -from opentelemetry.sdk.util.instrumentation import InstrumentationScope + +if TYPE_CHECKING: + from opentelemetry.sdk.util.instrumentation import InstrumentationScope _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py index 8c7e3469772..044f2d8785e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/point.py @@ -16,15 +16,17 @@ from dataclasses import asdict, dataclass, field from json import dumps, loads -from typing import Optional, Sequence, Union +from typing import TYPE_CHECKING, Optional, Sequence, Union # This kind of import is needed to avoid Sphinx errors. -import opentelemetry.sdk.metrics._internal from opentelemetry.sdk.metrics._internal.exemplar import Exemplar from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util.types import Attributes +if TYPE_CHECKING: + import opentelemetry.sdk.metrics._internal + @dataclass(frozen=True) class NumberDataPoint: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py index 3d88facb0c3..09002f31c55 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py @@ -15,11 +15,13 @@ # pylint: disable=unused-import from dataclasses import dataclass -from typing import Sequence +from typing import TYPE_CHECKING, Sequence # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics -import opentelemetry.sdk.resources + +if TYPE_CHECKING: + import opentelemetry.sdk.resources @dataclass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index a04d27e9ab1..3485e4e461a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -237,7 +237,7 @@ def create( if not resource.attributes.get(SERVICE_NAME, None): default_service_name = "unknown_service" process_executable_name = cast( - Optional[str], + "Optional[str]", resource.attributes.get(PROCESS_EXECUTABLE_NAME, None), ) if process_executable_name: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e0b639d81cf..d60903d3490 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -885,7 +885,7 @@ def _new_links(self, links: Sequence[trace_api.Link]): return BoundedList.from_seq(self._limits.max_links, valid_links) def get_span_context(self) -> trace_api.SpanContext: - return typing.cast(trace_api.SpanContext, self._context) + return typing.cast("trace_api.SpanContext", self._context) def set_attributes( self, attributes: Mapping[str, types.AttributeValue] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_off.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_off.py index eaafe164161..e86a42a57ed 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_off.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_off.py @@ -14,15 +14,16 @@ from __future__ import annotations -from typing import Sequence - -from opentelemetry.context import Context -from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import Attributes +from typing import TYPE_CHECKING, Sequence from ._composable import ComposableSampler, SamplingIntent from ._util import INVALID_THRESHOLD +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.trace import Link, SpanKind, TraceState + from opentelemetry.util.types import Attributes + _intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_on.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_on.py index 88ac61c5d37..ae8ceac579e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_on.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_on.py @@ -14,15 +14,16 @@ from __future__ import annotations -from typing import Sequence - -from opentelemetry.context import Context -from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import Attributes +from typing import TYPE_CHECKING, Sequence from ._composable import ComposableSampler, SamplingIntent from ._util import MIN_THRESHOLD +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.trace import Link, SpanKind, TraceState + from opentelemetry.util.types import Attributes + _intent = SamplingIntent(threshold=MIN_THRESHOLD) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_composable.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_composable.py index 5829601e30d..1e6081e18b6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_composable.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_composable.py @@ -15,11 +15,12 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Callable, Protocol, Sequence +from typing import TYPE_CHECKING, Callable, Protocol, Sequence -from opentelemetry.context import Context -from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import Attributes +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.trace import Link, SpanKind, TraceState + from opentelemetry.util.types import Attributes @dataclass(frozen=True) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_parent_threshold.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_parent_threshold.py index 83b7b7d3005..09c1e7ae220 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_parent_threshold.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_parent_threshold.py @@ -14,11 +14,9 @@ from __future__ import annotations -from typing import Sequence +from typing import TYPE_CHECKING, Sequence -from opentelemetry.context import Context from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span -from opentelemetry.util.types import Attributes from ._composable import ComposableSampler, SamplingIntent from ._trace_state import OtelTraceState @@ -28,6 +26,10 @@ is_valid_threshold, ) +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.util.types import Attributes + class _ComposableParentThreshold(ComposableSampler): def __init__(self, root_sampler: ComposableSampler): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_rule_based.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_rule_based.py index f03e3086527..0406f20f67f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_rule_based.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_rule_based.py @@ -14,15 +14,16 @@ from __future__ import annotations -from typing import Protocol, Sequence - -from opentelemetry.context import Context -from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import AnyValue, Attributes +from typing import TYPE_CHECKING, Protocol, Sequence from ._composable import ComposableSampler, SamplingIntent from ._util import INVALID_THRESHOLD +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.trace import Link, SpanKind, TraceState + from opentelemetry.util.types import AnyValue, Attributes + class PredicateT(Protocol): def __call__( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py index 989cc36019d..3c91570e806 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py @@ -14,17 +14,20 @@ from __future__ import annotations -from typing import Sequence +from typing import TYPE_CHECKING, Sequence -from opentelemetry.context import Context from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import Attributes -from ._composable import ComposableSampler, SamplingIntent from ._trace_state import OTEL_TRACE_STATE_KEY, OtelTraceState from ._util import INVALID_THRESHOLD, is_valid_random_value, is_valid_threshold +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.util.types import Attributes + + from ._composable import ComposableSampler, SamplingIntent + class _CompositeSampler(Sampler): def __init__(self, delegate: ComposableSampler): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py index b2fe9fd80af..735a4926835 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py @@ -15,9 +15,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Sequence - -from opentelemetry.trace import TraceState +from typing import TYPE_CHECKING, Sequence from ._util import ( INVALID_RANDOM_VALUE, @@ -27,6 +25,9 @@ is_valid_threshold, ) +if TYPE_CHECKING: + from opentelemetry.trace import TraceState + OTEL_TRACE_STATE_KEY = "ot" _TRACE_STATE_SIZE_LIMIT = 256 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_traceid_ratio.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_traceid_ratio.py index d63b6f8a8d7..1cc85e81c52 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_traceid_ratio.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_traceid_ratio.py @@ -14,16 +14,17 @@ from __future__ import annotations -from typing import Sequence - -from opentelemetry.context import Context -from opentelemetry.trace import Link, SpanKind, TraceState -from opentelemetry.util.types import Attributes +from typing import TYPE_CHECKING, Sequence from ._composable import ComposableSampler, SamplingIntent from ._trace_state import serialize_th from ._util import INVALID_THRESHOLD, MAX_THRESHOLD, calculate_threshold +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.trace import Link, SpanKind, TraceState + from opentelemetry.util.types import Attributes + class ComposableTraceIDRatioBased(ComposableSampler): _threshold: int diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py index ad7de330c78..feebdba284f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py @@ -14,9 +14,8 @@ from __future__ import annotations -from collections.abc import Callable +from typing import TYPE_CHECKING -from opentelemetry import metrics as metrics_api from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_SPAN_PARENT_ORIGIN, @@ -27,7 +26,12 @@ create_otel_sdk_span_live, create_otel_sdk_span_started, ) -from opentelemetry.trace.span import SpanContext + +if TYPE_CHECKING: + from collections.abc import Callable + + from opentelemetry import metrics as metrics_api + from opentelemetry.trace.span import SpanContext class TracerMetrics: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 68466eb1018..761771ac681 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -137,7 +137,7 @@ def get_sampler(sampler_argument): import os from logging import getLogger from types import MappingProxyType -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence # pylint: disable=unused-import from opentelemetry.context import Context @@ -146,9 +146,11 @@ def get_sampler(sampler_argument): OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.trace import Link, SpanKind, get_current_span -from opentelemetry.trace.span import TraceState from opentelemetry.util.types import Attributes +if TYPE_CHECKING: + from opentelemetry.trace.span import TraceState + _logger = getLogger(__name__) diff --git a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py index 820eb1070ab..6f92a1db8e2 100644 --- a/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponential_bucket_histogram_aggregation.py @@ -23,6 +23,7 @@ from sys import float_info, maxsize from time import time_ns from types import MethodType +from typing import TYPE_CHECKING from unittest.mock import Mock, patch from opentelemetry.context import Context @@ -44,15 +45,17 @@ LogarithmMapping, ) from opentelemetry.sdk.metrics._internal.measurement import Measurement -from opentelemetry.sdk.metrics._internal.point import ( - ExponentialHistogramDataPoint, -) from opentelemetry.sdk.metrics._internal.view import _default_reservoir_factory from opentelemetry.sdk.metrics.view import ( ExponentialBucketHistogramAggregation, ) from opentelemetry.test import TestCase +if TYPE_CHECKING: + from opentelemetry.sdk.metrics._internal.point import ( + ExponentialHistogramDataPoint, + ) + def get_counts(buckets: Buckets) -> int: counts = [] diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 3e47e577689..4ff5735bacb 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -348,7 +348,7 @@ def test_metric_reader_metrics(self): metric = reader_metrics[0] point = metric.data.data_points[0] - histogram = cast(HistogramDataPoint, point) + histogram = cast("HistogramDataPoint", point) self.assertEqual(histogram.count, 1) attrs = histogram.attributes assert attrs is not None diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 333494df746..78f9f91af99 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -20,14 +20,13 @@ import logging.config from logging import WARNING, getLogger from os import environ -from typing import Iterable, Optional, Sequence +from typing import TYPE_CHECKING, Iterable, Optional, Sequence from unittest import TestCase, mock from unittest.mock import Mock, patch from pytest import raises from opentelemetry import trace -from opentelemetry.context import Context from opentelemetry.environment_variables import OTEL_PYTHON_ID_GENERATOR from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, @@ -66,7 +65,6 @@ MetricExporter, MetricReader, ) -from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import SpanProcessor, _RuleBasedTracerConfigurator from opentelemetry.sdk.trace.export import ( @@ -83,9 +81,13 @@ TraceIdRatioBased, ) from opentelemetry.test.mock_test_classes import IterEntryPoint -from opentelemetry.trace import Link, SpanKind -from opentelemetry.trace.span import TraceState -from opentelemetry.util.types import Attributes + +if TYPE_CHECKING: + from opentelemetry.context import Context + from opentelemetry.sdk.metrics.view import Aggregation + from opentelemetry.trace import Link, SpanKind + from opentelemetry.trace.span import TraceState + from opentelemetry.util.types import Attributes class Provider: diff --git a/pyproject.toml b/pyproject.toml index 3182c69e19b..65ad1b6245d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,10 +85,19 @@ select = [ "PLE", # pylint error "Q", # flake8-quotes "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "TCH", # flake8-type-checking ] ignore = [ "E501", # line-too-long + + # flake8-type-checking rules: + # This rule enforces to add quotes for type aliases. + # This rule is unsafe as no static type checker can determine + # the exact behavior of runtime typing libraries. + "TC007", + # This rule is unstable and in preview + "TC008", ] [tool.ruff.lint.per-file-ignores] diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index e7261a0d92f..dafb480cca8 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -87,8 +87,7 @@ from __future__ import annotations import logging -from types import TracebackType -from typing import Type, TypeVar +from typing import TYPE_CHECKING, Type, TypeVar from opentracing import ( Format, @@ -124,7 +123,11 @@ ) from opentelemetry.trace import SpanContext as OtelSpanContext from opentelemetry.trace import Tracer as OtelTracer -from opentelemetry.util.types import Attributes + +if TYPE_CHECKING: + from types import TracebackType + + from opentelemetry.util.types import Attributes ValueT = TypeVar("ValueT", int, float, bool, str) logger = logging.getLogger(__name__) From a77ba74fc13fd92607a0344e3e611ab091ca0682 Mon Sep 17 00:00:00 2001 From: jayeshhire Date: Thu, 2 Apr 2026 21:00:32 +0530 Subject: [PATCH 2/3] Changes added in the CHANGELOG.md file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab44ceb635..b771afbd1f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Enabled the flake8-type-checking plugin rules for ruff linter. These rules do not allow the import of python objects outside the type-checking block, if they are only used for type annotations and are not used at runtime. ([#5029](https://github.com/open-telemetry/opentelemetry-python/pull/5029)) - `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job - `opentelemetry-sdk`: Add `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) From f9e0c8d21a3935907d59fd7e054f9730b38a8fdb Mon Sep 17 00:00:00 2001 From: jayeshhire Date: Thu, 2 Apr 2026 21:51:15 +0530 Subject: [PATCH 3/3] resolved merge conflicts --- CHANGELOG.md | 8 + docs/conf.py | 10 +- .../otlp/proto/common/_internal/__init__.py | 2 +- .../exporter/otlp/proto/grpc/exporter.py | 4 +- .../otlp/proto/http/_common/__init__.py | 9 +- .../proto/http/metric_exporter/__init__.py | 46 +-- .../metrics/test_otlp_metrics_exporter.py | 47 +-- .../opentelemetry/_logs/_internal/__init__.py | 6 +- .../src/opentelemetry/context/__init__.py | 4 +- .../metrics/test_benchmark_metrics.py | 47 +++ .../benchmarks/trace/test_benchmark_trace.py | 14 +- .../sdk/_configuration/__init__.py | 59 +++- .../sdk/_configuration/_common.py | 49 +++ .../sdk/environment_variables/__init__.py | 12 + .../sdk/metrics/_internal/__init__.py | 146 ++++++++- .../sdk/metrics/_internal/export/__init__.py | 14 +- .../sdk/metrics/_internal/instrument.py | 45 ++- .../metrics/_internal/measurement_consumer.py | 4 +- .../src/opentelemetry/sdk/trace/__init__.py | 109 ++++--- .../_sampling_experimental/_trace_state.py | 4 +- .../sdk/trace/export/__init__.py | 4 +- .../opentelemetry/sdk/util/instrumentation.py | 15 +- .../tests/_configuration/test_common.py | 81 +++++ .../tests/metrics/test_metrics.py | 294 +++++++++++++++++- opentelemetry-sdk/tests/test_configurator.py | 84 ++++- opentelemetry-sdk/tests/trace/test_trace.py | 81 ++++- pyproject.toml | 7 + scripts/update_sha.py | 2 +- .../opentelemetry/shim/opencensus/_patch.py | 2 +- .../shim/opentracing_shim/__init__.py | 22 +- 30 files changed, 1051 insertions(+), 180 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py create mode 100644 opentelemetry-sdk/tests/_configuration/test_common.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b771afbd1f9..0fe46157a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Enabled the flake8-type-checking plugin rules for ruff linter. These rules do not allow the import of python objects outside the type-checking block, if they are only used for type annotations and are not used at runtime. ([#5029](https://github.com/open-telemetry/opentelemetry-python/pull/5029)) +- `opentelemetry-sdk`: Add shared `_parse_headers` helper for declarative config OTLP exporters + ([#5021](https://github.com/open-telemetry/opentelemetry-python/pull/5021)) - `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job - `opentelemetry-sdk`: Add `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) @@ -42,6 +44,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4910](https://github.com/open-telemetry/opentelemetry-python/pull/4910)) - Add configurable `max_export_batch_size` to OTLP HTTP metrics exporter ([#4576](https://github.com/open-telemetry/opentelemetry-python/pull/4576)) +- `opentelemetry-sdk`: Implement experimental Meter configurator + ([#4966](https://github.com/open-telemetry/opentelemetry-python/pull/4966)) +- `opentelemetry-exporter-otlp-proto-http`: use consistent protobuf for export request + ([#5015](https://github.com/open-telemetry/opentelemetry-python/pull/5015)) +- `opentelemetry-sdk`: cache TracerConfig into the tracer, this changes an internal interface. Only one Tracer with the same instrumentation scope will be created + ([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007)) ## Version 1.40.0/0.61b0 (2026-03-04) diff --git a/docs/conf.py b/docs/conf.py index 25b890aa3d1..8ca6a83b5b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -228,11 +228,11 @@ scm_web = "https://github.com/" + REPO + "blob/" + branch # Store variables in the epilogue so they are globally available. -rst_epilog = """ -.. |SCM_WEB| replace:: {s} -.. |SCM_RAW_WEB| replace:: {sr} -.. |SCM_BRANCH| replace:: {b} -""".format(s=scm_web, sr=scm_raw_web, b=branch) +rst_epilog = f""" +.. |SCM_WEB| replace:: {scm_web} +.. |SCM_RAW_WEB| replace:: {scm_raw_web} +.. |SCM_BRANCH| replace:: {branch} +""" # used to have links to repo files extlinks = { diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py index 97a8777356c..5da0b750d43 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py @@ -173,7 +173,7 @@ def _get_resource_data( resource_class( **{ "resource": collector_resource, - "scope_{}".format(name): scope_data.values(), + f"scope_{name}": scope_data.values(), } ) ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 89c2608c30a..d52f61c8c85 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -155,9 +155,7 @@ class InvalidCompressionValueException(Exception): def __init__(self, environ_key: str, environ_value: str): super().__init__( - 'Invalid value "{}" for compression envvar {}'.format( - environ_value, environ_key - ) + f'Invalid value "{environ_value}" for compression envvar {environ_key}' ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py index 0658d0968e6..1bdb7d228c2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_common/__init__.py @@ -19,9 +19,6 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, ) from opentelemetry.util._importlib_metadata import entry_points @@ -36,9 +33,9 @@ def _is_retryable(resp: requests.Response) -> bool: def _load_session_from_envvar( cred_envvar: Literal[ - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", + "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", ], ) -> Optional[requests.Session]: _credential_env = environ.get( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 845b5318ade..cc5fa9b55e5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -51,7 +51,7 @@ _is_retryable, _load_session_from_envvar, ) -from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( # noqa: F401 +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( ExportMetricsServiceRequest, ) from opentelemetry.proto.common.v1.common_pb2 import ( # noqa: F401 @@ -61,7 +61,7 @@ KeyValue, KeyValueList, ) -from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 # noqa: F401 +from opentelemetry.proto.metrics.v1 import metrics_pb2 as pb2 from opentelemetry.proto.resource.v1.resource_pb2 import Resource # noqa: F401 from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, @@ -246,18 +246,19 @@ def _export( def _export_with_retries( self, - serialized_data: bytes, + export_request: ExportMetricsServiceRequest, deadline_sec: float, ) -> MetricExportResult: """Export serialized data with retry logic until success, non-transient error, or exponential backoff maxed out. Args: - serialized_data: serialized metrics data to export + export_request: ExportMetricsServiceRequest object containing metrics data to export deadline_sec: timestamp deadline for the export Returns: MetricExportResult: SUCCESS if export succeeded, FAILURE otherwise """ + serialized_data = export_request.SerializeToString() for retry_num in range(_MAX_RETRYS): # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2) @@ -313,23 +314,21 @@ def export( _logger.warning("Exporter already shutdown, ignoring batch") return MetricExportResult.FAILURE - serialized_data = encode_metrics(metrics_data) + export_request = encode_metrics(metrics_data) deadline_sec = time() + self._timeout # If no batch size configured, export as single batch with retries as configured if self._max_export_batch_size is None: - return self._export_with_retries( - serialized_data.SerializeToString(), deadline_sec - ) + return self._export_with_retries(export_request, deadline_sec) # Else, export in batches of configured size - split_metrics_batches = list( - _split_metrics_data(serialized_data, self._max_export_batch_size) + batched_export_requests = _split_metrics_data( + export_request, self._max_export_batch_size ) - for split_metrics_data in split_metrics_batches: + for split_metrics_data in batched_export_requests: export_result = self._export_with_retries( - split_metrics_data.SerializeToString(), + split_metrics_data, deadline_sec, ) if export_result != MetricExportResult.SUCCESS: @@ -356,18 +355,18 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: def _split_metrics_data( - metrics_data: pb2.MetricsData, + metrics_data: ExportMetricsServiceRequest, max_export_batch_size: int | None = None, -) -> Iterable[pb2.MetricsData]: - """Splits metrics data into several MetricsData (copies protobuf originals), +) -> Iterable[ExportMetricsServiceRequest]: + """Splits metrics data into several ExportMetricsServiceRequest (copies protobuf originals), based on configured data point max export batch size. Args: metrics_data: metrics object based on HTTP protocol buffer definition Returns: - Iterable[pb2.MetricsData]: An iterable of pb2.MetricsData objects containing - pb2.ResourceMetrics, pb2.ScopeMetrics, pb2.Metrics, and data points + Iterable[ExportMetricsServiceRequest]: An iterable of ExportMetricsServiceRequest objects containing + ExportMetricsServiceRequest.ResourceMetrics, ExportMetricsServiceRequest.ScopeMetrics, ExportMetricsServiceRequest.Metrics, and data points """ if not max_export_batch_size: return metrics_data @@ -433,7 +432,7 @@ def _split_metrics_data( batch_size += 1 if batch_size >= max_export_batch_size: - yield pb2.MetricsData( + yield ExportMetricsServiceRequest( resource_metrics=_get_split_resource_metrics_pb2( split_resource_metrics ) @@ -447,6 +446,11 @@ def _split_metrics_data( # Rebuild metric dict generically using same approach as initial creation field_name = metric.WhichOneof("data") + if field_name is None: + _logger.warning( + "Tried to split and export an unsupported metric type. Skipping." + ) + continue data_container = getattr(metric, field_name) metric_dict = { "name": metric.name, @@ -494,7 +498,7 @@ def _split_metrics_data( split_resource_metrics.pop() if batch_size > 0: - yield pb2.MetricsData( + yield ExportMetricsServiceRequest( resource_metrics=_get_split_resource_metrics_pb2( split_resource_metrics ) @@ -556,13 +560,13 @@ def _get_split_resource_metrics_pb2( new_resource_metrics = pb2.ResourceMetrics( resource=resource_metrics.get("resource"), scope_metrics=[], - schema_url=resource_metrics.get("schema_url"), + schema_url=resource_metrics.get("schema_url") or "", ) for scope_metrics in resource_metrics.get("scope_metrics", []): new_scope_metrics = pb2.ScopeMetrics( scope=scope_metrics.get("scope"), metrics=[], - schema_url=scope_metrics.get("schema_url"), + schema_url=scope_metrics.get("schema_url") or "", ) for metric in scope_metrics.get("metrics", []): diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 14a5cd5f40f..75b42e60b3a 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -40,6 +40,9 @@ _split_metrics_data, ) from opentelemetry.exporter.otlp.proto.http.version import __version__ +from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import ( + ExportMetricsServiceRequest, +) from opentelemetry.proto.common.v1.common_pb2 import ( InstrumentationScope, KeyValue, @@ -374,7 +377,7 @@ def test_serialization(self, mock_post): ) def test_split_metrics_data_many_data_points(self): - metrics_data = pb2.MetricsData( + metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -396,7 +399,7 @@ def test_split_metrics_data_many_data_points(self): ), ] ) - split_metrics_data: List[MetricsData] = list( + split_metrics_data: List[ExportMetricsServiceRequest] = list( # pylint: disable=protected-access _split_metrics_data( metrics_data=metrics_data, @@ -406,7 +409,7 @@ def test_split_metrics_data_many_data_points(self): self.assertEqual( [ - pb2.MetricsData( + ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -427,7 +430,7 @@ def test_split_metrics_data_many_data_points(self): ), ] ), - pb2.MetricsData( + ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -452,7 +455,7 @@ def test_split_metrics_data_many_data_points(self): ) def test_split_metrics_data_nb_data_points_equal_batch_size(self): - metrics_data = pb2.MetricsData( + metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -475,7 +478,7 @@ def test_split_metrics_data_nb_data_points_equal_batch_size(self): ] ) - split_metrics_data: List[MetricsData] = list( + split_metrics_data: List[ExportMetricsServiceRequest] = list( # pylint: disable=protected-access _split_metrics_data( metrics_data=metrics_data, @@ -485,7 +488,7 @@ def test_split_metrics_data_nb_data_points_equal_batch_size(self): self.assertEqual( [ - pb2.MetricsData( + ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -513,7 +516,7 @@ def test_split_metrics_data_nb_data_points_equal_batch_size(self): def test_split_metrics_data_many_resources_scopes_metrics(self): # GIVEN - metrics_data = pb2.MetricsData( + metrics_data = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -567,7 +570,7 @@ def test_split_metrics_data_many_resources_scopes_metrics(self): ] ) - split_metrics_data: List[MetricsData] = list( + split_metrics_data: List[ExportMetricsServiceRequest] = list( # pylint: disable=protected-access _split_metrics_data( metrics_data=metrics_data, @@ -577,7 +580,7 @@ def test_split_metrics_data_many_resources_scopes_metrics(self): self.assertEqual( [ - pb2.MetricsData( + ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -603,7 +606,7 @@ def test_split_metrics_data_many_resources_scopes_metrics(self): ), ] ), - pb2.MetricsData( + ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -858,7 +861,7 @@ def test_export_retries_with_batching_success( MagicMock(ok=True), MagicMock(ok=True), ] - mock_encode_metrics.return_value = pb2.MetricsData( + mock_encode_metrics.return_value = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -880,7 +883,7 @@ def test_export_retries_with_batching_success( ), ] ) - batch_1 = pb2.MetricsData( + batch_1 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -901,7 +904,7 @@ def test_export_retries_with_batching_success( ), ] ) - batch_2 = pb2.MetricsData( + batch_2 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -954,7 +957,7 @@ def test_export_retries_with_batching_failure_first( MagicMock(ok=True), MagicMock(ok=True), ] - mock_encode_metrics.return_value = pb2.MetricsData( + mock_encode_metrics.return_value = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -976,7 +979,7 @@ def test_export_retries_with_batching_failure_first( ), ] ) - batch_1 = pb2.MetricsData( + batch_1 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1031,7 +1034,7 @@ def test_export_retries_with_batching_failure_last( # Non-retryable MagicMock(ok=False, status_code=400, reason="bad request"), ] - mock_encode_metrics.return_value = pb2.MetricsData( + mock_encode_metrics.return_value = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1053,7 +1056,7 @@ def test_export_retries_with_batching_failure_last( ), ] ) - batch_1 = pb2.MetricsData( + batch_1 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1074,7 +1077,7 @@ def test_export_retries_with_batching_failure_last( ), ] ) - batch_2 = pb2.MetricsData( + batch_2 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1131,7 +1134,7 @@ def test_export_retries_with_batching_failure_retryable( # Then success MagicMock(ok=True), ] - mock_encode_metrics.return_value = pb2.MetricsData( + mock_encode_metrics.return_value = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1153,7 +1156,7 @@ def test_export_retries_with_batching_failure_retryable( ), ] ) - batch_1 = pb2.MetricsData( + batch_1 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, @@ -1174,7 +1177,7 @@ def test_export_retries_with_batching_failure_retryable( ), ] ) - batch_2 = pb2.MetricsData( + batch_2 = ExportMetricsServiceRequest( resource_metrics=[ _resource_metrics( index=1, diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 0f054116eab..f67cff67694 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -91,7 +91,7 @@ def __init__( observed_timestamp: Optional[int] = None, trace_id: Optional[int] = None, span_id: Optional[int] = None, - trace_flags: Optional["TraceFlags"] = None, + trace_flags: Optional[TraceFlags] = None, severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, @@ -106,7 +106,7 @@ def __init__( context: Optional[Context] = None, trace_id: Optional[int] = None, span_id: Optional[int] = None, - trace_flags: Optional["TraceFlags"] = None, + trace_flags: Optional[TraceFlags] = None, severity_text: Optional[str] = None, severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, @@ -432,7 +432,7 @@ def get_logger( logger_provider: Optional[LoggerProvider] = None, schema_url: Optional[str] = None, attributes: Optional[_ExtendedAttributes] = None, -) -> "Logger": +) -> Logger: """Returns a `Logger` for use within a python process. This function is a convenience wrapper for diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index dc030c0a4f1..a36e5ca5ab4 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -84,7 +84,7 @@ def create_key(keyname: str) -> str: return keyname + "-" + str(uuid4()) -def get_value(key: str, context: typing.Optional[Context] = None) -> "object": +def get_value(key: str, context: typing.Optional[Context] = None) -> object: """To access the local state of a concern, the RuntimeContext API provides a function which takes a context and a key as input, and returns a value. @@ -100,7 +100,7 @@ def get_value(key: str, context: typing.Optional[Context] = None) -> "object": def set_value( - key: str, value: "object", context: typing.Optional[Context] = None + key: str, value: object, context: typing.Optional[Context] = None ) -> Context: """To record the local state of a cross-cutting concern, the RuntimeContext API provides a function which takes a context, a diff --git a/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics.py b/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics.py index 7b062ce2c26..007481ae9ea 100644 --- a/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics.py +++ b/opentelemetry-sdk/benchmarks/metrics/test_benchmark_metrics.py @@ -14,10 +14,17 @@ import pytest from opentelemetry.sdk.metrics import Counter, MeterProvider +from opentelemetry.sdk.metrics._internal import ( + _default_meter_configurator, + _disable_meter_configurator, + _MeterConfig, + _RuleBasedMeterConfigurator, +) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, InMemoryMetricReader, ) +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob reader_cumulative = InMemoryMetricReader() reader_delta = InMemoryMetricReader( @@ -77,3 +84,43 @@ def benchmark_up_down_counter_add(): udcounter.add(1, labels) benchmark(benchmark_up_down_counter_add) + + +@pytest.fixture(params=[None, 0, 1, 10, 50]) +def num_meter_configurator_rules(request): + return request.param + + +# pylint: disable=protected-access,redefined-outer-name +def test_counter_add_with_meter_configurator_rules( + benchmark, num_meter_configurator_rules +): + def benchmark_counter_add(): + counter_cumulative.add(1, {}) + + if num_meter_configurator_rules is None: + provider_reader_cumulative._set_meter_configurator( + meter_configurator=_disable_meter_configurator + ) + else: + + def meter_configurator(meter_scope): + return _RuleBasedMeterConfigurator( + rules=[ + ( + _scope_name_matches_glob(glob_pattern=str(i)), + _MeterConfig(is_enabled=True), + ) + for i in range(num_meter_configurator_rules) + ], + default_config=_MeterConfig(is_enabled=True), + )(meter_scope) + + provider_reader_cumulative._set_meter_configurator( + meter_configurator=meter_configurator + ) + + benchmark(benchmark_counter_add) + provider_reader_cumulative._set_meter_configurator( + meter_configurator=_default_meter_configurator + ) diff --git a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py index c2d5590144c..51a321466c2 100644 --- a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py +++ b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py @@ -21,12 +21,12 @@ TracerProvider, _default_tracer_configurator, _RuleBasedTracerConfigurator, - _scope_name_matches_glob, _TracerConfig, sampling, ) +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob -tracer = TracerProvider( +tracer_provider = TracerProvider( sampler=sampling.DEFAULT_ON, resource=Resource( { @@ -35,10 +35,11 @@ "service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12", } ), -).get_tracer("sdk_tracer_provider") +) +tracer = tracer_provider.get_tracer("sdk_tracer_provider") -@pytest.fixture(params=[None, 0, 1, 10, 50]) +@pytest.fixture(params=[0, 1, 10, 50]) def num_tracer_configurator_rules(request): return request.param @@ -81,18 +82,13 @@ def tracer_configurator(tracer_scope): default_config=_TracerConfig(is_enabled=True), )(tracer_scope=tracer_scope) - tracer_provider = tracer._tracer_provider tracer_provider._set_tracer_configurator( tracer_configurator=tracer_configurator ) - if num_tracer_configurator_rules is None: - tracer._tracer_provider = None benchmark(benchmark_start_span) tracer_provider._set_tracer_configurator( tracer_configurator=_default_tracer_configurator ) - if num_tracer_configurator_rules is None: - tracer._tracer_provider = tracer_provider def test_simple_start_as_current_span(benchmark): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 602b105ca7c..e75c0b1446b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -25,7 +25,16 @@ import warnings from abc import ABC, abstractmethod from os import environ -from typing import Any, Callable, Mapping, Protocol, Sequence, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Mapping, + Protocol, + Sequence, + Type, + Union, +) from typing_extensions import Literal @@ -52,6 +61,7 @@ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_PYTHON_METER_CONFIGURATOR, OTEL_PYTHON_TRACER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, @@ -75,6 +85,10 @@ from opentelemetry.trace import set_tracer_provider from opentelemetry.util._importlib_metadata import entry_points +if TYPE_CHECKING: + from opentelemetry.sdk.metrics._internal import _MeterConfiguratorT + + _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" _EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http" @@ -171,6 +185,10 @@ def _get_tracer_configurator() -> str | None: return environ.get(OTEL_PYTHON_TRACER_CONFIGURATOR, None) +def _get_meter_configurator() -> str | None: + return environ.get(OTEL_PYTHON_METER_CONFIGURATOR, None) + + def _get_exporter_entry_point( exporter_name: str, signal_type: Literal["traces", "metrics", "logs"] ): @@ -267,6 +285,7 @@ def _init_metrics( ], resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, + meter_configurator: _MeterConfiguratorT | None = None, ): metric_readers = [] @@ -282,7 +301,11 @@ def _init_metrics( ) ) - provider = MeterProvider(resource=resource, metric_readers=metric_readers) + provider = MeterProvider( + resource=resource, + metric_readers=metric_readers, + _meter_configurator=meter_configurator, + ) set_meter_provider(provider) @@ -387,6 +410,27 @@ def _import_tracer_configurator( return tracer_configurator_impl +def _import_meter_configurator( + meter_configurator_name: str | None, +) -> _MeterConfiguratorT | None: + if not meter_configurator_name: + return None + + try: + _, meter_configurator_impl = _import_config_components( + [meter_configurator_name.strip()], + "_opentelemetry_meter_configurator", + )[0] + except Exception as exc: # pylint: disable=broad-exception-caught + _logger.warning( + "Using default meter configurator. Failed to load meter configurator, %s: %s", + meter_configurator_name, + exc, + ) + return None + return meter_configurator_impl + + def _import_exporters( trace_exporter_names: Sequence[str], metric_exporter_names: Sequence[str], @@ -507,6 +551,7 @@ def _initialize_components( export_log_record_processor: _ConfigurationExporterLogRecordProcessorT | None = None, tracer_configurator: _TracerConfiguratorT | None = None, + meter_configurator: _MeterConfiguratorT | None = None, ): # pylint: disable=too-many-locals if trace_exporter_names is None: @@ -538,6 +583,11 @@ def _initialize_components( tracer_configurator = _import_tracer_configurator( tracer_configurator_name ) + if meter_configurator is None: + meter_configurator_name = _get_meter_configurator() + meter_configurator = _import_meter_configurator( + meter_configurator_name + ) # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name # from the env variable else defaults to "unknown_service" @@ -554,7 +604,10 @@ def _initialize_components( tracer_configurator=tracer_configurator, ) _init_metrics( - metric_exporters, resource, exporter_args_map=exporter_args_map + exporters_or_readers=metric_exporters, + resource=resource, + exporter_args_map=exporter_args_map, + meter_configurator=meter_configurator, ) if setup_logging_handler is None: setup_logging_handler = ( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py new file mode 100644 index 00000000000..152be1ea01d --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py @@ -0,0 +1,49 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging +from typing import Optional + +_logger = logging.getLogger(__name__) + + +def _parse_headers( + headers: Optional[list], + headers_list: Optional[str], +) -> Optional[dict[str, str]]: + """Merge headers struct and headers_list into a dict. + + Returns None if neither is set, letting the exporter read env vars. + headers struct takes priority over headers_list for the same key. + """ + if headers is None and headers_list is None: + return None + result: dict[str, str] = {} + if headers_list: + for item in headers_list.split(","): + item = item.strip() + if "=" in item: + key, value = item.split("=", 1) + result[key.strip()] = value.strip() + elif item: + _logger.warning( + "Invalid header pair in headers_list (missing '='): %s", + item, + ) + if headers: + for pair in headers: + result[pair.name] = pair.value or "" + return result diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index f049415a15b..edf91da3a69 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -814,3 +814,15 @@ def channel_credential_provider() -> grpc.ChannelCredentials: This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ + +OTEL_PYTHON_METER_CONFIGURATOR = "OTEL_PYTHON_METER_CONFIGURATOR" +""" +.. envvar:: OTEL_PYTHON_METER_CONFIGURATOR + +The :envvar:`OTEL_PYTHON_METER_CONFIGURATOR` environment variable allows users to set a +custom Meter Configurator function. +Default: opentelemetry.sdk.metrics._internal._default_meter_configurator + +This is an experimental environment variable and the name of this variable and its behavior can +change in a non-backwards compatible way. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 3943ec1feb9..321d98c15c4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -14,11 +14,12 @@ import weakref from atexit import register, unregister +from dataclasses import dataclass from logging import getLogger from os import environ from threading import Lock from time import time_ns -from typing import TYPE_CHECKING, Optional, Sequence +from typing import TYPE_CHECKING, Callable, Optional, Sequence # This kind of import is needed to avoid Sphinx errors. from opentelemetry.metrics import Counter as APICounter @@ -61,7 +62,10 @@ SdkConfiguration, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope, + _InstrumentationScopePredicateT, +) from opentelemetry.util._once import Once from opentelemetry.util.types import ( Attributes, @@ -73,6 +77,27 @@ _logger = getLogger(__name__) +@dataclass +class _MeterConfig: + is_enabled: bool = True + + @classmethod + def default(cls) -> "_MeterConfig": + return _MeterConfig() + + +class _ProxyMeterConfig: + def __init__(self, config: _MeterConfig): + self._config = config + + @property + def is_enabled(self) -> bool: + return self._config.is_enabled + + def update(self, config: _MeterConfig) -> None: + self._config = config + + class Meter(APIMeter): """See `opentelemetry.metrics.Meter`.""" @@ -80,6 +105,8 @@ def __init__( self, instrumentation_scope: InstrumentationScope, measurement_consumer: MeasurementConsumer, + *, + _meter_config: Optional[_MeterConfig] = None, ): super().__init__( name=instrumentation_scope.name, @@ -90,6 +117,15 @@ def __init__( self._measurement_consumer = measurement_consumer self._instrument_id_instrument = {} self._instrument_registration_lock = Lock() + self._meter_config = _ProxyMeterConfig( + _meter_config or _MeterConfig.default() + ) + + def _is_enabled(self) -> bool: + return self._meter_config.is_enabled + + def _set_meter_config(self, meter_config: _MeterConfig) -> None: + self._meter_config.update(meter_config) def create_counter(self, name, unit="", description="") -> APICounter: with self._instrument_registration_lock: @@ -104,6 +140,7 @@ def create_counter(self, name, unit="", description="") -> APICounter: self._measurement_consumer, unit, description, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -136,6 +173,7 @@ def create_up_down_counter( self._measurement_consumer, unit, description, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -173,6 +211,7 @@ def create_observable_counter( callbacks, unit, description, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -241,6 +280,7 @@ def create_histogram( unit, description, explicit_bucket_boundaries_advisory, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -268,6 +308,7 @@ def create_gauge(self, name, unit="", description="") -> APIGauge: self._measurement_consumer, unit, description, + _meter_config=self._meter_config, ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -300,6 +341,7 @@ def create_observable_gauge( callbacks, unit, description, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -338,6 +380,7 @@ def create_observable_up_down_counter( callbacks, unit, description, + _meter_config=self._meter_config, ) ) instrument = self._instrument_id_instrument[status.instrument_id] @@ -372,6 +415,45 @@ def _get_exemplar_filter(exemplar_filter: str) -> ExemplarFilter: raise ValueError(msg) +_MeterConfiguratorT = Callable[[InstrumentationScope], _MeterConfig] +_MeterConfiguratorRulesT = Sequence[ + tuple[_InstrumentationScopePredicateT, _MeterConfig] +] + + +def _default_meter_configurator( + _meter_scope: InstrumentationScope, +) -> _MeterConfig: + return _MeterConfig.default() + + +def _disable_meter_configurator( + _meter_scope: InstrumentationScope, +) -> _MeterConfig: + return _MeterConfig(is_enabled=False) + + +class _RuleBasedMeterConfigurator: + def __init__( + self, + *, + rules: _MeterConfiguratorRulesT, + default_config: _MeterConfig, + ): + self._rules = rules + self._default_config = default_config + + def __call__(self, meter_scope: InstrumentationScope) -> _MeterConfig: + for predicate, meter_config in self._rules: + if predicate(meter_scope): + return meter_config + # by default return default config + return self._default_config + + def update_rules(self, rules: _MeterConfiguratorRulesT) -> None: + self._rules = rules + + class MeterProvider(APIMeterProvider): r"""See `opentelemetry.metrics.MeterProvider`. @@ -428,6 +510,8 @@ def __init__( exemplar_filter: Optional[ExemplarFilter] = None, shutdown_on_exit: bool = True, views: Sequence["opentelemetry.sdk.metrics.view.View"] = (), + *, + _meter_configurator: Optional[_MeterConfiguratorT] = None, ): self._lock = Lock() self._meter_lock = Lock() @@ -454,9 +538,12 @@ def __init__( if shutdown_on_exit: self._atexit_handler = register(self.shutdown) - self._meters = {} + self._meters: dict[InstrumentationScope, Meter] = {} self._shutdown_once = Once() self._shutdown = False + self._meter_configurator = ( + _meter_configurator or _default_meter_configurator + ) for metric_reader in self._sdk_config.metric_readers: with self._all_metric_readers_lock: @@ -474,6 +561,36 @@ def __init__( ) metric_reader._set_meter_provider(self) + def _set_meter_configurator( + self, *, meter_configurator: _MeterConfiguratorT + ): + """Set a new MeterConfigurator for this MeterProvider. + + Setting a new MeterConfigurator will result in the configurator being called + for each outstanding Meter and for any newly created meters thereafter. + Therefore, it is important that the provided function returns quickly. + """ + with self._meter_lock: + self._meter_configurator = meter_configurator + for instrumentation_scope, meter in self._meters.items(): + # pylint: disable-next=protected-access + meter._set_meter_config( + self._apply_meter_configurator(instrumentation_scope) + ) + + def _apply_meter_configurator( + self, instrumentation_scope: InstrumentationScope + ) -> _MeterConfig: + try: + return self._meter_configurator(instrumentation_scope) + # pylint: disable-next=broad-exception-caught + except Exception: + _logger.exception( + "meter configurator failed for scope '%s', using default config", + instrumentation_scope.name, + ) + return _MeterConfig.default() + def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = time_ns() + timeout_millis * 10**6 @@ -554,11 +671,9 @@ def _shutdown(): # pylint: disable=broad-exception-raised raise Exception( - ( - "MeterProvider.shutdown failed because the following " - "metric readers failed during shutdown:\n" - f"{metric_reader_error_string}" - ) + "MeterProvider.shutdown failed because the following " + "metric readers failed during shutdown:\n" + f"{metric_reader_error_string}" ) def get_meter( @@ -581,13 +696,18 @@ def get_meter( _logger.warning("Meter name cannot be None or empty.") return NoOpMeter(name, version=version, schema_url=schema_url) - info = InstrumentationScope(name, version, schema_url, attributes) + instrumentation_scope = InstrumentationScope( + name, version, schema_url, attributes + ) with self._meter_lock: - if not self._meters.get(info): + if not self._meters.get(instrumentation_scope): # FIXME #2558 pass SDKConfig object to meter so that the meter # has access to views. - self._meters[info] = Meter( - info, + self._meters[instrumentation_scope] = Meter( + instrumentation_scope, self._measurement_consumer, + _meter_config=self._apply_meter_configurator( + instrumentation_scope + ), ) - return self._meters[info] + return self._meters[instrumentation_scope] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 44efde20c7b..7f71b3bdb72 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -103,7 +103,7 @@ def __init__( preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ - type, "opentelemetry.sdk.metrics.view.Aggregation" + type, opentelemetry.sdk.metrics.view.Aggregation ] | None = None, ) -> None: @@ -158,7 +158,7 @@ def __init__( preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ - type, "opentelemetry.sdk.metrics.view.Aggregation" + type, opentelemetry.sdk.metrics.view.Aggregation ] | None = None, ): @@ -225,7 +225,7 @@ def __init__( preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ - type, "opentelemetry.sdk.metrics.view.Aggregation" + type, opentelemetry.sdk.metrics.view.Aggregation ] | None = None, *, @@ -233,10 +233,10 @@ def __init__( ) -> None: self._collect: Callable[ [ - "opentelemetry.sdk.metrics.export.MetricReader", + opentelemetry.sdk.metrics.export.MetricReader, AggregationTemporality, ], - Iterable["opentelemetry.sdk.metrics.export.Metric"], + Iterable[opentelemetry.sdk.metrics.export.Metric], ] = None self._instrument_class_temporality = { @@ -373,7 +373,7 @@ def _set_collect_callback( self, func: Callable[ [ - "opentelemetry.sdk.metrics.export.MetricReader", + opentelemetry.sdk.metrics.export.MetricReader, AggregationTemporality, ], MetricsData, @@ -425,7 +425,7 @@ def __init__( preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ - type, "opentelemetry.sdk.metrics.view.Aggregation" + type, opentelemetry.sdk.metrics.view.Aggregation ] | None = None, ) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index 88f7077d6f3..2f6e47a178c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -38,9 +38,13 @@ from opentelemetry.sdk.metrics._internal.measurement import Measurement if TYPE_CHECKING: - import opentelemetry.sdk.metrics + from opentelemetry.sdk.metrics._internal import ( + MeasurementConsumer, + _ProxyMeterConfig, + ) from opentelemetry.sdk.util.instrumentation import InstrumentationScope + _logger = getLogger(__name__) @@ -54,9 +58,11 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "opentelemetry.sdk.metrics.MeasurementConsumer", + measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", + *, + _meter_config: _ProxyMeterConfig | None = None, ): # pylint: disable=no-member result = self._check_name_unit_description(name, unit, description) @@ -78,18 +84,24 @@ def __init__( self.description = description self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer + self._meter_config = _meter_config super().__init__(name, unit=unit, description=description) + def _is_enabled(self) -> bool: + return self._meter_config is None or self._meter_config.is_enabled + class _Asynchronous: def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "opentelemetry.sdk.metrics.MeasurementConsumer", + measurement_consumer: MeasurementConsumer, callbacks: Iterable[CallbackT] | None = None, unit: str = "", description: str = "", + *, + _meter_config: _ProxyMeterConfig | None = None, ): # pylint: disable=no-member result = self._check_name_unit_description(name, unit, description) @@ -111,6 +123,7 @@ def __init__( self.description = description self.instrumentation_scope = instrumentation_scope self._measurement_consumer = measurement_consumer + self._meter_config = _meter_config super().__init__(name, callbacks, unit=unit, description=description) self._callbacks: List[CallbackT] = [] @@ -134,9 +147,14 @@ def inner( else: self._callbacks.append(callback) + def _is_enabled(self) -> bool: + return self._meter_config is None or self._meter_config.is_enabled + def callback( self, callback_options: CallbackOptions ) -> Iterable[Measurement]: + if not self._is_enabled(): + return for callback in self._callbacks: try: for api_measurement in callback(callback_options): @@ -165,6 +183,10 @@ def add( attributes: dict[str, str] | None = None, context: Context | None = None, ): + if not self._is_enabled(): + super().add(amount, attributes=attributes, context=context) + return + if amount < 0: _logger.warning( "Add amount must be non-negative on Counter %s.", self.name @@ -194,6 +216,10 @@ def add( attributes: dict[str, str] | None = None, context: Context | None = None, ): + if not self._is_enabled(): + super().add(amount, attributes=attributes, context=context) + return + time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( @@ -229,10 +255,12 @@ def __init__( self, name: str, instrumentation_scope: InstrumentationScope, - measurement_consumer: "opentelemetry.sdk.metrics.MeasurementConsumer", + measurement_consumer: MeasurementConsumer, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, + *, + _meter_config: _ProxyMeterConfig | None = None, ): super().__init__( name, @@ -240,6 +268,7 @@ def __init__( description=description, instrumentation_scope=instrumentation_scope, measurement_consumer=measurement_consumer, + _meter_config=_meter_config, ) self._advisory = _MetricsHistogramAdvisory( explicit_bucket_boundaries=explicit_bucket_boundaries_advisory @@ -256,6 +285,10 @@ def record( attributes: dict[str, str] | None = None, context: Context | None = None, ): + if not self._is_enabled(): + super().record(amount, attributes=attributes, context=context) + return + if amount < 0: _logger.warning( "Record amount must be non-negative on Histogram %s.", @@ -286,6 +319,10 @@ def set( attributes: dict[str, str] | None = None, context: Context | None = None, ): + if not self._is_enabled(): + super().set(amount, attributes=attributes, context=context) + return + time_unix_nano = time_ns() self._measurement_consumer.consume_measurement( Measurement( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py index 33dea470c15..49c310f00a1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py @@ -66,7 +66,7 @@ def __init__( self._sdk_config = sdk_config # should never be mutated self._reader_storages: Mapping[ - "opentelemetry.sdk.metrics.MetricReader", MetricReaderStorage + opentelemetry.sdk.metrics.export.MetricReader, MetricReaderStorage ] = { reader: MetricReaderStorage( sdk_config, @@ -76,7 +76,7 @@ def __init__( for reader in sdk_config.metric_readers } self._async_instruments: List[ - "opentelemetry.sdk.metrics._internal.instrument._Asynchronous" + opentelemetry.sdk.metrics._internal.instrument._Asynchronous ] = [] def consume_measurement(self, measurement: Measurement) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index d60903d3490..2a876ade90e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,7 +16,6 @@ import abc import atexit import concurrent.futures -import fnmatch import json import logging import os @@ -25,7 +24,6 @@ import typing import weakref from dataclasses import dataclass -from functools import lru_cache from os import environ from time import time_ns from types import MappingProxyType, TracebackType @@ -70,6 +68,7 @@ from opentelemetry.sdk.util.instrumentation import ( InstrumentationInfo, InstrumentationScope, + _InstrumentationScopePredicateT, ) from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_ESCAPED, @@ -1103,6 +1102,10 @@ class _Span(Span): class _TracerConfig: is_enabled: bool + @classmethod + def default(cls): + return cls(is_enabled=True) + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" @@ -1120,7 +1123,7 @@ def __init__( instrumentation_scope: InstrumentationScope, *, meter_provider: Optional[metrics_api.MeterProvider] = None, - _tracer_provider: Optional["TracerProvider"] = None, + _tracer_config: Optional[_TracerConfig] = None, ) -> None: self.sampler = sampler self.resource = resource @@ -1129,20 +1132,17 @@ def __init__( self.instrumentation_info = instrumentation_info self._span_limits = span_limits self._instrumentation_scope = instrumentation_scope - self._tracer_provider = _tracer_provider + self._tracer_config = _tracer_config or _TracerConfig.default() meter_provider = meter_provider or metrics_api.get_meter_provider() self._tracer_metrics = TracerMetrics(meter_provider) + def _set_tracer_config(self, tracer_config: _TracerConfig): + self._tracer_config = tracer_config + def _is_enabled(self) -> bool: """If the tracer is not enabled, start_span will create a NonRecordingSpan""" - - if not self._tracer_provider: - return True - tracer_config = self._tracer_provider._tracer_configurator( # pylint: disable=protected-access - self._instrumentation_scope - ) - return tracer_config.is_enabled + return self._tracer_config.is_enabled @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span( @@ -1262,22 +1262,11 @@ def start_span( # pylint: disable=too-many-locals _TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig] -_InstrumentationScopePredicateT = Callable[[InstrumentationScope], bool] _TracerConfiguratorRulesT = Sequence[ tuple[_InstrumentationScopePredicateT, _TracerConfig] ] -# TODO: share this with configurators for other signals -def _scope_name_matches_glob( - glob_pattern: str, -) -> _InstrumentationScopePredicateT: - def inner(scope: InstrumentationScope) -> bool: - return fnmatch.fnmatch(scope.name, glob_pattern) - - return inner - - class _RuleBasedTracerConfigurator: def __init__( self, @@ -1297,7 +1286,6 @@ def __call__(self, tracer_scope: InstrumentationScope) -> _TracerConfig: return self._default_config -@lru_cache def _default_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: @@ -1308,11 +1296,10 @@ def _default_tracer_configurator( implementing this interface returning a Tracer Config.""" return _RuleBasedTracerConfigurator( rules=[], - default_config=_TracerConfig(is_enabled=True), + default_config=_TracerConfig.default(), )(tracer_scope=tracer_scope) -@lru_cache def _disable_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: @@ -1365,28 +1352,42 @@ def __init__( self._tracer_configurator = ( _tracer_configurator or _default_tracer_configurator ) + self._tracers_lock = threading.Lock() + self._tracers: dict[InstrumentationScope, Tracer] = {} def _set_tracer_configurator( self, *, tracer_configurator: _TracerConfiguratorT ): """This is the function used to update the TracerProvider TracerConfigurator - Setting a new TracerConfigurator for a TracerProvider will make all the Tracers created from - this TracerProvider reference the new TracerConfigurator. - - The tracer checks its configuration at span creation time. Since this is an hot path - it's important that it'll execute quickly so it is suggested to memoize it with - functools.lru_cache. - If your TracerConfigurator is using some dynamic rules you can still use functools.lru_cache - decorator if you remember to clear its cache with the decorator cache_clear() function when - the rules change. + Setting a new TracerConfigurator for a TracerProvider will update the + TracerConfig of all Tracers create by this TracerProvider. """ self._tracer_configurator = tracer_configurator + with self._tracers_lock: + for instrumentation_scope, tracer in self._tracers.items(): + tracer_config = self._apply_tracer_configurator( + instrumentation_scope + ) + # pylint: disable-next=protected-access + tracer._set_tracer_config(tracer_config) @property def resource(self) -> Resource: return self._resource + def _apply_tracer_configurator( + self, instrumentation_scope: InstrumentationScope + ): + try: + return self._tracer_configurator(instrumentation_scope) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to create a Tracer Config for %s, using default Tracer config", + instrumentation_scope, + ) + return _TracerConfig.default() + def get_tracer( self, instrumenting_module_name: str, @@ -1417,23 +1418,33 @@ def get_tracer( schema_url, ) - tracer = Tracer( - self.sampler, - self.resource, - self._active_span_processor, - self.id_generator, - instrumentation_info, - self._span_limits, - InstrumentationScope( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - attributes, - ), - meter_provider=self._meter_provider, - _tracer_provider=self, + instrumentation_scope = InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, ) + with self._tracers_lock: + if instrumentation_scope in self._tracers: + return self._tracers[instrumentation_scope] + + tracer_config = self._apply_tracer_configurator( + instrumentation_scope + ) + tracer = Tracer( + self.sampler, + self.resource, + self._active_span_processor, + self.id_generator, + instrumentation_info, + self._span_limits, + instrumentation_scope, + meter_provider=self._meter_provider, + _tracer_config=tracer_config, + ) + self._tracers[instrumentation_scope] = tracer + return tracer def add_span_processor(self, span_processor: SpanProcessor) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py index 735a4926835..8cfd4abbced 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_trace_state.py @@ -46,11 +46,11 @@ class OtelTraceState: rest: Sequence[str] @staticmethod - def invalid() -> "OtelTraceState": + def invalid() -> OtelTraceState: return OtelTraceState(INVALID_RANDOM_VALUE, INVALID_THRESHOLD, ()) @staticmethod - def parse(trace_state: TraceState | None) -> "OtelTraceState": + def parse(trace_state: TraceState | None) -> OtelTraceState: if not trace_state: return OtelTraceState.invalid() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index d853dfd6c40..c3ca81542a9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -61,9 +61,7 @@ class SpanExporter: `SimpleSpanProcessor` or a `BatchSpanProcessor`. """ - def export( - self, spans: typing.Sequence[ReadableSpan] - ) -> "SpanExportResult": # pyright: ignore[reportReturnType] + def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult: # pyright: ignore[reportReturnType] """Exports a batch of telemetry data. Args: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index cdee837f669..fd8af277f58 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import fnmatch from json import dumps -from typing import Optional +from typing import Callable, Optional from typing_extensions import deprecated @@ -167,3 +168,15 @@ def to_json(self, indent: Optional[int] = 4) -> str: }, indent=indent, ) + + +_InstrumentationScopePredicateT = Callable[[InstrumentationScope], bool] + + +def _scope_name_matches_glob( + glob_pattern: str, +) -> _InstrumentationScopePredicateT: + def inner(scope: InstrumentationScope) -> bool: + return fnmatch.fnmatch(scope.name, glob_pattern) + + return inner diff --git a/opentelemetry-sdk/tests/_configuration/test_common.py b/opentelemetry-sdk/tests/_configuration/test_common.py new file mode 100644 index 00000000000..5c3fcf112b8 --- /dev/null +++ b/opentelemetry-sdk/tests/_configuration/test_common.py @@ -0,0 +1,81 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from types import SimpleNamespace + +from opentelemetry.sdk._configuration._common import _parse_headers + + +class TestParseHeaders(unittest.TestCase): + def test_both_none_returns_none(self): + self.assertIsNone(_parse_headers(None, None)) + + def test_empty_headers_list_returns_empty_dict(self): + self.assertEqual(_parse_headers(None, ""), {}) + + def test_headers_list_single_pair(self): + self.assertEqual( + _parse_headers(None, "x-api-key=secret"), + {"x-api-key": "secret"}, + ) + + def test_headers_list_multiple_pairs(self): + self.assertEqual( + _parse_headers(None, "x-api-key=secret,env=prod"), + {"x-api-key": "secret", "env": "prod"}, + ) + + def test_headers_list_strips_whitespace(self): + self.assertEqual( + _parse_headers(None, " x-api-key = secret , env = prod "), + {"x-api-key": "secret", "env": "prod"}, + ) + + def test_headers_list_value_with_equals(self): + # value contains '=' — only split on the first one + self.assertEqual( + _parse_headers(None, "auth=Bearer tok=en"), + {"auth": "Bearer tok=en"}, + ) + + def test_headers_list_invalid_pair_ignored(self): + # malformed entry (no '=') should be skipped with a warning + result = _parse_headers(None, "bad,x-key=val") + self.assertEqual(result, {"x-key": "val"}) + + def test_struct_headers_only(self): + headers = [ + SimpleNamespace(name="x-api-key", value="secret"), + SimpleNamespace(name="env", value="prod"), + ] + self.assertEqual( + _parse_headers(headers, None), + {"x-api-key": "secret", "env": "prod"}, + ) + + def test_struct_header_none_value_becomes_empty_string(self): + headers = [SimpleNamespace(name="x-key", value=None)] + self.assertEqual(_parse_headers(headers, None), {"x-key": ""}) + + def test_struct_headers_override_headers_list(self): + # struct takes priority over headers_list for same key + headers = [SimpleNamespace(name="x-api-key", value="from-struct")] + self.assertEqual( + _parse_headers(headers, "x-api-key=from-list,env=prod"), + {"x-api-key": "from-struct", "env": "prod"}, + ) + + def test_both_empty_struct_and_none_list_returns_empty_dict(self): + self.assertEqual(_parse_headers([], None), {}) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 0dc6d4ddf08..d2f475faa00 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=protected-access,no-self-use +# pylint: disable=protected-access,no-self-use,too-many-lines import weakref from collections.abc import Callable @@ -36,7 +36,14 @@ UpDownCounter, _Gauge, ) -from opentelemetry.sdk.metrics._internal import SynchronousMeasurementConsumer +from opentelemetry.sdk.metrics._internal import ( + SynchronousMeasurementConsumer, + _default_meter_configurator, + _disable_meter_configurator, + _MeterConfig, + _ProxyMeterConfig, + _RuleBasedMeterConfigurator, +) from opentelemetry.sdk.metrics.export import ( Metric, MetricExporter, @@ -46,6 +53,10 @@ ) from opentelemetry.sdk.metrics.view import SumAggregation, View from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationScope, + _scope_name_matches_glob, +) from opentelemetry.test import TestCase from opentelemetry.test.concurrency_test import ConcurrencyTestBase, MockFunc @@ -66,6 +77,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True +# pylint: disable=too-many-public-methods class TestMeterProvider(ConcurrencyTestBase, TestCase): def tearDown(self): MeterProvider._all_metric_readers = weakref.WeakSet() @@ -416,6 +428,65 @@ def test_consume_measurement_histogram( sync_consumer_instance.consume_measurement.assert_called() + def test_meter_provider_with_disabled_configurator(self): + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + meter = mp.get_meter("test") + self.assertFalse(meter._is_enabled()) + + def test_meter_provider_with_custom_configurator(self): + def configurator(scope): + if scope.name == "disabled_meter": + return _MeterConfig(is_enabled=False) + return _MeterConfig.default() + + mp = MeterProvider(_meter_configurator=configurator) + enabled = mp.get_meter("enabled_meter") + disabled = mp.get_meter("disabled_meter") + self.assertTrue(enabled._is_enabled()) + self.assertFalse(disabled._is_enabled()) + + def test_set_meter_configurator_updates_existing_meters(self): + mp = MeterProvider() + meter = mp.get_meter("test") + self.assertTrue(meter._is_enabled()) + + mp._set_meter_configurator( + meter_configurator=_disable_meter_configurator + ) + self.assertFalse(meter._is_enabled()) + + def test_set_meter_configurator_affects_new_meters(self): + mp = MeterProvider() + mp._set_meter_configurator( + meter_configurator=_disable_meter_configurator + ) + meter = mp.get_meter("new_meter") + self.assertFalse(meter._is_enabled()) + + def test_buggy_configurator_falls_back_to_default_on_get_meter(self): + def raising_configurator(_scope): + raise RuntimeError("configurator error") + + mp = MeterProvider(_meter_configurator=raising_configurator) + with self.assertLogs(level="ERROR"): + meter = mp.get_meter("test") + self.assertTrue(meter._is_enabled()) + + def test_buggy_configurator_falls_back_to_default_on_set_configurator( + self, + ): + mp = MeterProvider() + meter = mp.get_meter("test") + self.assertTrue(meter._is_enabled()) + + def raising_configurator(_scope): + raise ValueError("bad config") + + with self.assertLogs(level="ERROR"): + mp._set_meter_configurator(meter_configurator=raising_configurator) + # Should still be enabled (default config) despite the error + self.assertTrue(meter._is_enabled()) + @patch( "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" ) @@ -636,6 +707,225 @@ def test_get_meter_with_sdk_disabled(self): meter_provider = MeterProvider() self.assertIsInstance(meter_provider.get_meter(Mock()), NoOpMeter) + def test_meter_config_default(self): + config = _MeterConfig.default() + self.assertTrue(config.is_enabled) + + def test_meter_config_disabled(self): + config = _MeterConfig(is_enabled=False) + self.assertFalse(config.is_enabled) + + def test_proxy_meter_config_delegates(self): + proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True)) + self.assertTrue(proxy.is_enabled) + proxy_disabled = _ProxyMeterConfig(_MeterConfig(is_enabled=False)) + self.assertFalse(proxy_disabled.is_enabled) + + def test_proxy_meter_config_update(self): + proxy = _ProxyMeterConfig(_MeterConfig(is_enabled=True)) + self.assertTrue(proxy.is_enabled) + proxy.update(_MeterConfig(is_enabled=False)) + self.assertFalse(proxy.is_enabled) + proxy.update(_MeterConfig(is_enabled=True)) + self.assertTrue(proxy.is_enabled) + + def test_default_meter_configurator(self): + scope = InstrumentationScope("any_name", "1.0") + config = _default_meter_configurator(scope) + self.assertTrue(config.is_enabled) + + def test_disable_meter_configurator(self): + scope = InstrumentationScope("any_name", "1.0") + config = _disable_meter_configurator(scope) + self.assertFalse(config.is_enabled) + + def test_rule_based_configurator_first_match_wins(self): + disabled_config = _MeterConfig(is_enabled=False) + enabled_config = _MeterConfig(is_enabled=True) + configurator = _RuleBasedMeterConfigurator( + rules=[ + (lambda s: s.name == "foo", disabled_config), + (lambda s: s.name == "foo", enabled_config), + ], + default_config=enabled_config, + ) + scope = InstrumentationScope("foo", "1.0") + result = configurator(scope) + self.assertFalse(result.is_enabled) + + def test_rule_based_configurator_default_when_no_match(self): + disabled_config = _MeterConfig(is_enabled=False) + configurator = _RuleBasedMeterConfigurator( + rules=[ + ( + lambda s: s.name == "specific", + _MeterConfig(is_enabled=True), + ), + ], + default_config=disabled_config, + ) + scope = InstrumentationScope("other", "1.0") + result = configurator(scope) + self.assertFalse(result.is_enabled) + + def test_rule_based_configurator_with_glob_predicate(self): + disabled_config = _MeterConfig(is_enabled=False) + configurator = _RuleBasedMeterConfigurator( + rules=[ + (_scope_name_matches_glob("opentelemetry.*"), disabled_config), + ], + default_config=_MeterConfig.default(), + ) + self.assertFalse( + configurator( + InstrumentationScope("opentelemetry.sdk", "1.0") + ).is_enabled + ) + self.assertTrue( + configurator(InstrumentationScope("custom.name", "1.0")).is_enabled + ) + + def test_scope_name_matches_glob_exact(self): + predicate = _scope_name_matches_glob("my.meter") + self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0"))) + + def test_scope_name_matches_glob_wildcard(self): + predicate = _scope_name_matches_glob("my.*") + self.assertTrue(predicate(InstrumentationScope("my.meter", "1.0"))) + self.assertTrue(predicate(InstrumentationScope("my.other", "1.0"))) + self.assertFalse(predicate(InstrumentationScope("other.meter", "1.0"))) + + def test_scope_name_matches_glob_no_match(self): + predicate = _scope_name_matches_glob("no.match") + self.assertFalse(predicate(InstrumentationScope("my.meter", "1.0"))) + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_disabled_meter_counter_skips_measurement( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + counter = mp.get_meter("test").create_counter("c") + counter.add(1) + sync_consumer_instance.consume_measurement.assert_not_called() + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_disabled_meter_up_down_counter_skips_measurement( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + counter = mp.get_meter("test").create_up_down_counter("udc") + counter.add(1) + sync_consumer_instance.consume_measurement.assert_not_called() + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_disabled_meter_histogram_skips_measurement( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + histogram = mp.get_meter("test").create_histogram("h") + histogram.record(1) + sync_consumer_instance.consume_measurement.assert_not_called() + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_disabled_meter_gauge_skips_measurement( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + gauge = mp.get_meter("test").create_gauge("g") + gauge.set(1) + sync_consumer_instance.consume_measurement.assert_not_called() + + def test_disabled_meter_observable_counter_skips_callback(self): + cb = Mock() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + oc = mp.get_meter("test").create_observable_counter( + "oc", callbacks=[cb] + ) + # Trigger callback collection + list(oc.callback(Mock())) + cb.assert_not_called() + + def test_disabled_meter_observable_gauge_skips_callback(self): + cb = Mock() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + og = mp.get_meter("test").create_observable_gauge("og", callbacks=[cb]) + list(og.callback(Mock())) + cb.assert_not_called() + + def test_disabled_meter_observable_up_down_counter_skips_callback(self): + cb = Mock() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + oudc = mp.get_meter("test").create_observable_up_down_counter( + "oudc", callbacks=[cb] + ) + list(oudc.callback(Mock())) + cb.assert_not_called() + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_counter_noop_after_meter_disabled( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider() + meter = mp.get_meter("test") + counter = meter.create_counter("c") + + counter.add(1) + self.assertEqual( + sync_consumer_instance.consume_measurement.call_count, 1 + ) + + counter.add(2) + self.assertEqual( + sync_consumer_instance.consume_measurement.call_count, 2 + ) + + mp._set_meter_configurator( + meter_configurator=_disable_meter_configurator + ) + self.assertFalse(meter._is_enabled()) + + counter.add(3) + counter.add(4) + self.assertEqual( + sync_consumer_instance.consume_measurement.call_count, 2 + ) + + @patch( + "opentelemetry.sdk.metrics._internal.SynchronousMeasurementConsumer" + ) + def test_reenable_meter_after_disable( + self, mock_sync_measurement_consumer + ): + sync_consumer_instance = mock_sync_measurement_consumer() + mp = MeterProvider(_meter_configurator=_disable_meter_configurator) + meter = mp.get_meter("test") + counter = meter.create_counter("c") + + counter.add(1) + sync_consumer_instance.consume_measurement.assert_not_called() + + mp._set_meter_configurator( + meter_configurator=_default_meter_configurator + ) + self.assertTrue(meter._is_enabled()) + counter.add(1) + sync_consumer_instance.consume_measurement.assert_called_once() + class InMemoryMetricExporter(MetricExporter): def __init__(self): diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 78f9f91af99..fba0f7eb312 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -34,11 +34,13 @@ _EXPORTER_OTLP_PROTO_HTTP, _get_exporter_names, _get_id_generator, + _get_meter_configurator, _get_sampler, _get_tracer_configurator, _import_config_components, _import_exporters, _import_id_generator, + _import_meter_configurator, _import_sampler, _import_tracer_configurator, _init_logging, @@ -54,10 +56,15 @@ SimpleLogRecordProcessor, ) from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_METER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics._internal import ( + _default_meter_configurator, + _RuleBasedMeterConfigurator, +) from opentelemetry.sdk.metrics.export import ( AggregationTemporality, ConsoleMetricExporter, @@ -263,14 +270,14 @@ def get_description(self) -> str: def should_sample( self, - parent_context: Optional["Context"], + parent_context: Optional[Context], trace_id: int, name: str, kind: SpanKind = None, attributes: Attributes = None, links: Sequence[Link] = None, trace_state: TraceState = None, - ) -> "SamplingResult": + ) -> SamplingResult: return SamplingResult( Decision.RECORD_AND_SAMPLE, None, @@ -292,14 +299,14 @@ def get_description(self) -> str: def should_sample( self, - parent_context: "Context" | None, + parent_context: Context | None, trace_id: int, name: str, kind: SpanKind | None = None, attributes: Attributes = None, links: Sequence[Link] | None = None, trace_state: TraceState | None = None, - ) -> "SamplingResult": + ) -> SamplingResult: return SamplingResult( Decision.RECORD_AND_SAMPLE, None, @@ -921,7 +928,7 @@ def test_initialize_components_resource( _, _, kwargs = tracing_mock.mock_calls[0] tracing_resource = kwargs["resource"] _, args, _ = metrics_mock.mock_calls[0] - metrics_resource = args[1] + metrics_resource = kwargs["resource"] self.assertEqual(logging_resource, tracing_resource) self.assertEqual(logging_resource, metrics_resource) self.assertEqual(tracing_resource, metrics_resource) @@ -984,6 +991,7 @@ def test_initialize_components_kwargs( "log_record_processors": [], "span_processors": [], "tracer_configurator": "tracer_configurator_test", + "meter_configurator": "meter_configurator_test", } _initialize_components(**kwargs) @@ -1023,9 +1031,10 @@ def test_initialize_components_kwargs( tracer_configurator="tracer_configurator_test", ) metrics_mock.assert_called_once_with( - "TEST_METRICS_EXPORTERS_DICT", - "TEST_RESOURCE", + exporters_or_readers="TEST_METRICS_EXPORTERS_DICT", + resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, + meter_configurator="meter_configurator_test", ) logging_mock.assert_called_once_with( "TEST_LOG_EXPORTERS_DICT", @@ -1219,6 +1228,67 @@ def test_metrics_init_exporter_uses_exporter_args_map(self): reader = provider._sdk_config.metric_readers[0] self.assertEqual(reader.exporter.compression, "gzip") + def test_metrics_init_meter_configurator_none_by_default(self): + _init_metrics({}) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertEqual( + provider._meter_configurator, _default_meter_configurator + ) + + def test_metrics_init_meter_configurator_passed_directly(self): + mock_configurator = Mock() + _init_metrics({}, meter_configurator=mock_configurator) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertEqual(provider._meter_configurator, mock_configurator) + + @patch.dict( + "os.environ", + {OTEL_PYTHON_METER_CONFIGURATOR: "non_existent_entry_point"}, + ) + def test_metrics_init_custom_meter_configurator_with_env_non_existent_entry_point( + self, + ): + meter_configurator_name = _get_meter_configurator() + with self.assertLogs(level=WARNING): + meter_configurator = _import_meter_configurator( + meter_configurator_name + ) + _init_metrics({}, meter_configurator=meter_configurator) + + @patch("opentelemetry.sdk._configuration.entry_points") + @patch.dict( + "os.environ", + {OTEL_PYTHON_METER_CONFIGURATOR: "custom_meter_configurator"}, + ) + def test_metrics_init_custom_meter_configurator_with_env( + self, mock_entry_points + ): + def custom_meter_configurator(meter_scope): + return mock.Mock(spec=_RuleBasedMeterConfigurator)( + meter_scope=meter_scope + ) + + mock_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_meter_configurator", + custom_meter_configurator, + ) + ] + ) + + meter_configurator_name = _get_meter_configurator() + meter_configurator = _import_meter_configurator( + meter_configurator_name + ) + _init_metrics({}, meter_configurator=meter_configurator) + provider = self.set_provider_mock.call_args[0][0] + self.assertEqual( + provider._meter_configurator, custom_meter_configurator + ) + class TestExporterNames(TestCase): @patch.dict( diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0f617523163..d4ee490f397 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -16,6 +16,7 @@ # pylint: disable=no-member import copy +import dataclasses import shutil import subprocess import unittest @@ -48,7 +49,6 @@ Resource, TracerProvider, _RuleBasedTracerConfigurator, - _scope_name_matches_glob, _TracerConfig, ) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator @@ -60,7 +60,10 @@ StaticSampler, ) from opentelemetry.sdk.util import BoundedDict, BoundedList, ns_to_iso_str -from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.util.instrumentation import ( + InstrumentationInfo, + _scope_name_matches_glob, +) from opentelemetry.test.spantestutil import ( get_span_with_dropped_attributes_events_links, new_tracer, @@ -196,6 +199,43 @@ def test_get_tracer_sdk(self): {"key1": "value1", "key2": 6}, ) + def test_get_tracer_sdk_returns_same_tracer_when_called_with_same_instrumentation_scope( + self, + ): + tracer_provider = trace.TracerProvider() + tracer1 = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {"key1": "value1", "key2": 6}, + ) + + tracer2 = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {"key1": "value1", "key2": 6}, + ) + + self.assertEqual(tracer1, tracer2) + self.assertTrue(tracer1 is tracer2) + + def test_get_tracer_sdk_sets_default_tracer_config_if_configurator_raises( + self, + ): + def raising_tracer_configurator(tracer_scope): + raise ValueError() + + tracer_provider = trace.TracerProvider( + _tracer_configurator=raising_tracer_configurator + ) + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + ) + # pylint: disable=protected-access + self.assertEqual(tracer._tracer_config, _TracerConfig.default()) + @mock.patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_tracer_with_sdk_disabled(self): tracer_provider = trace.TracerProvider() @@ -2259,6 +2299,23 @@ def test_child_parent_span_exception(self): self.assertTupleEqual(parent_span.events, ()) +class TestTracerConfig(unittest.TestCase): + def test_default(self): + self.assertEqual( + _TracerConfig.default(), + _TracerConfig(is_enabled=True), + ) + + def test_equality(self): + config = _TracerConfig(is_enabled=True) + same_config = _TracerConfig(is_enabled=True) + other_config = _TracerConfig(is_enabled=False) + + self.assertEqual(config, same_config) + self.assertNotEqual(config, other_config) + self.assertNotEqual(config, "string") + + # pylint: disable=protected-access class TestTracerProvider(unittest.TestCase): @patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default") @@ -2297,6 +2354,26 @@ def test_default_tracer_configurator(self): self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), True) + def test_set_tracer_configurator_sets_default_tracer_config_if_configurator_raises( + self, + ): + def raising_tracer_configurator(tracer_scope): + raise ValueError() + + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + ) + tracer_provider._set_tracer_configurator( + tracer_configurator=raising_tracer_configurator + ) + # pylint: disable=protected-access + self.assertEqual( + dataclasses.asdict(tracer._tracer_config), + dataclasses.asdict(_TracerConfig.default()), + ) + def test_rule_based_tracer_configurator(self): # pylint: disable=protected-access rules = [ diff --git a/pyproject.toml b/pyproject.toml index 65ad1b6245d..12f108901fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,11 @@ select = [ "Q", # flake8-quotes "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g "TCH", # flake8-type-checking + "UP011", # lru-cache-without-parameters + "UP015", # redundant-open-modes + "UP032", # f-string + "UP034", # extraneous-parentheses + "UP037", # quoted-annotation ] ignore = [ @@ -124,6 +129,7 @@ include = [ "opentelemetry-sdk", "opentelemetry-proto-json", "exporter/opentelemetry-exporter-otlp-proto-grpc", + "exporter/opentelemetry-exporter-otlp-proto-http", "codegen/opentelemetry-codegen-json" ] @@ -133,6 +139,7 @@ exclude = [ "opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/", "opentelemetry-sdk/benchmarks", "exporter/opentelemetry-exporter-otlp-proto-grpc/tests", + "exporter/opentelemetry-exporter-otlp-proto-http/tests", ] # When packages are correct typed add them to the strict list diff --git a/scripts/update_sha.py b/scripts/update_sha.py index a0bf76f8b74..9bcde6fedc0 100644 --- a/scripts/update_sha.py +++ b/scripts/update_sha.py @@ -40,7 +40,7 @@ def update_sha(sha): yaml = YAML() yaml.preserve_quotes = True for workflow_file in workflow_files: - with open(workflow_file, "r") as file: + with open(workflow_file) as file: workflow = yaml.load(file) workflow["env"]["CONTRIB_REPO_SHA"] = sha with open(workflow_file, "w") as file: diff --git a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py index c3c6e810371..71d09486616 100644 --- a/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py +++ b/shim/opentelemetry-opencensus-shim/src/opentelemetry/shim/opencensus/_patch.py @@ -36,7 +36,7 @@ def install_shim( tracer_provider=tracer_provider, ) - @lru_cache() + @lru_cache def cached_shim_tracer(span_context: SpanContext) -> ShimTracer: return ShimTracer( NoopTracer(), diff --git a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py index dafb480cca8..79904031ce7 100644 --- a/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py +++ b/shim/opentelemetry-opentracing-shim/src/opentelemetry/shim/opentracing_shim/__init__.py @@ -134,7 +134,7 @@ _SHIM_KEY = create_key("scope_shim") -def create_tracer(otel_tracer_provider: TracerProvider) -> "TracerShim": +def create_tracer(otel_tracer_provider: TracerProvider) -> TracerShim: """Creates a :class:`TracerShim` object from the provided OpenTelemetry :class:`opentelemetry.trace.TracerProvider`. @@ -209,7 +209,7 @@ def unwrap(self): return self._otel_span - def set_operation_name(self, operation_name: str) -> "SpanShim": + def set_operation_name(self, operation_name: str) -> SpanShim: """Updates the name of the wrapped OpenTelemetry span. Args: @@ -244,7 +244,7 @@ def finish(self, finish_time: float | None = None): end_time = util.time_seconds_to_ns(finish_time) self._otel_span.end(end_time=end_time) - def set_tag(self, key: str, value: ValueT) -> "SpanShim": + def set_tag(self, key: str, value: ValueT) -> SpanShim: """Sets an OpenTelemetry attribute on the wrapped OpenTelemetry span. Args: @@ -260,7 +260,7 @@ def set_tag(self, key: str, value: ValueT) -> "SpanShim": def log_kv( self, key_values: Attributes, timestamp: float | None = None - ) -> "SpanShim": + ) -> SpanShim: """Logs an event for the wrapped OpenTelemetry span. Note: @@ -359,7 +359,7 @@ class ScopeShim(Scope): """ def __init__( - self, manager: "ScopeManagerShim", span: SpanShim, span_cm=None + self, manager: ScopeManagerShim, span: SpanShim, span_cm=None ): super().__init__(manager, span) self._span_cm = span_cm @@ -368,7 +368,7 @@ def __init__( # TODO: Change type of `manager` argument to `opentracing.ScopeManager`? We # need to get rid of `manager.tracer` for this. @classmethod - def from_context_manager(cls, manager: "ScopeManagerShim", span_cm): + def from_context_manager(cls, manager: ScopeManagerShim, span_cm): """Constructs a :class:`ScopeShim` from an OpenTelemetry `opentelemetry.trace.Span` context manager. @@ -455,14 +455,14 @@ class ScopeManagerShim(ScopeManager): span state. """ - def __init__(self, tracer: "TracerShim"): + def __init__(self, tracer: TracerShim): # The only thing the ``__init__()``` method on the base class does is # initialize `self._noop_span` and `self._noop_scope` with no-op # objects. Therefore, it doesn't seem useful to call it. # pylint: disable=super-init-not-called self._tracer = tracer - def activate(self, span: SpanShim, finish_on_close: bool) -> "ScopeShim": + def activate(self, span: SpanShim, finish_on_close: bool) -> ScopeShim: """Activates a :class:`SpanShim` and returns a :class:`ScopeShim` which represents the active span. @@ -480,7 +480,7 @@ def activate(self, span: SpanShim, finish_on_close: bool) -> "ScopeShim": return ScopeShim.from_context_manager(self, span_cm=span_cm) @property - def active(self) -> "ScopeShim": + def active(self) -> ScopeShim: """Returns a :class:`ScopeShim` object representing the currently-active span in the OpenTelemetry tracer. @@ -508,7 +508,7 @@ def active(self) -> "ScopeShim": return ScopeShim(self, span=wrapped_span) @property - def tracer(self) -> "TracerShim": + def tracer(self) -> TracerShim: """Returns the :class:`TracerShim` reference used by this :class:`ScopeManagerShim` for setting and getting the active span from the OpenTelemetry tracer. @@ -570,7 +570,7 @@ def start_active_span( start_time: float | None = None, ignore_active_span: bool = False, finish_on_close: bool = True, - ) -> "ScopeShim": + ) -> ScopeShim: """Starts and activates a span. In terms of functionality, this method behaves exactly like the same method on a "regular" OpenTracing tracer. See :meth:`opentracing.Tracer.start_active_span` for more details.