diff --git a/changelog/fixed/typing-tapir.md b/changelog/fixed/typing-tapir.md new file mode 100644 index 000000000..f9979ad37 --- /dev/null +++ b/changelog/fixed/typing-tapir.md @@ -0,0 +1 @@ +Fix OpenTelemetry logger attribute typing and logger-provider test collection with OpenTelemetry 1.45 and newer by accepting string-keyed mappings without restricting attribute value types. diff --git a/temporalio/contrib/opentelemetry/_logger_provider.py b/temporalio/contrib/opentelemetry/_logger_provider.py index 31cd0d1e4..141004312 100644 --- a/temporalio/contrib/opentelemetry/_logger_provider.py +++ b/temporalio/contrib/opentelemetry/_logger_provider.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Mapping +from typing import Any # The OpenTelemetry Logs API specification is stable, but opentelemetry-python # exposes the logs bridge API only under the private opentelemetry._logs @@ -13,11 +14,6 @@ from temporalio import workflow -if TYPE_CHECKING: - # _ExtendedAttributes is the annotation OpenTelemetry's own public - # get_logger signature uses; there is no public alias. - from opentelemetry.util.types import _ExtendedAttributes - def _skip_emitting() -> bool: # in_workflow() must be evaluated first: is_replaying_history_events() @@ -123,7 +119,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: _ExtendedAttributes | None = None, + attributes: Mapping[str, Any] | None = None, ) -> Logger: """Get a replay-safe logger from the underlying provider. @@ -132,6 +128,8 @@ def get_logger( version: The version string of the instrumenting library. schema_url: The schema URL for the logger. attributes: Instrumentation scope attributes for the logger. + Values are typed permissively to support different OpenTelemetry + attribute types across supported versions. Returns: A replay-safe logger instance. diff --git a/tests/contrib/opentelemetry/test_logger_provider.py b/tests/contrib/opentelemetry/test_logger_provider.py index e3d1ac09d..400802a17 100644 --- a/tests/contrib/opentelemetry/test_logger_provider.py +++ b/tests/contrib/opentelemetry/test_logger_provider.py @@ -1,10 +1,11 @@ """Unit tests for ReplaySafeLoggerProvider.""" -from collections.abc import Iterator +from collections.abc import Iterator, Mapping from contextlib import contextmanager from typing import Any from unittest.mock import patch +import pytest from opentelemetry._logs import ( Logger, LoggerProvider, @@ -17,7 +18,6 @@ InMemoryLogRecordExporter, SimpleLogRecordProcessor, ) -from opentelemetry.util.types import _ExtendedAttributes from temporalio import workflow from temporalio.contrib.opentelemetry import ReplaySafeLoggerProvider @@ -45,7 +45,13 @@ def test_replay_safe_logger_provider_emit_passes_through_outside_workflow(): assert records[1].attributes and dict(records[1].attributes) == {"attr": "val"} -def test_replay_safe_logger_provider_delegates_get_logger_arguments(): +@pytest.mark.parametrize( + "attributes", + [None, {"attr": "val"}, {"nested": {"values": [1, b"bytes", None]}}], +) +def test_replay_safe_logger_provider_delegates_get_logger_arguments( + attributes: Mapping[str, Any] | None, +): class RecordingLoggerProvider(LoggerProvider): def __init__(self) -> None: self.calls: list[tuple] = [] @@ -56,7 +62,7 @@ def get_logger( name: str, version: str | None = None, schema_url: str | None = None, - attributes: _ExtendedAttributes | None = None, + attributes: Mapping[str, Any] | None = None, ) -> Logger: self.calls.append((name, version, schema_url, attributes)) return self._inner.get_logger(name, version, schema_url, attributes) @@ -67,11 +73,11 @@ def get_logger( "test-logger", version="1.2.3", schema_url="https://example.com/schema", - attributes={"attr": "val"}, + attributes=attributes, ) assert inner_provider.calls == [ - ("test-logger", "1.2.3", "https://example.com/schema", {"attr": "val"}) + ("test-logger", "1.2.3", "https://example.com/schema", attributes) ]