diff --git a/changelog/fixed/abstract-albatross.md b/changelog/fixed/abstract-albatross.md new file mode 100644 index 0000000000..e0fe3a2c1e --- /dev/null +++ b/changelog/fixed/abstract-albatross.md @@ -0,0 +1 @@ +Fix `contrib.opentelemetry` `ReplaySafeLoggerProvider.get_logger()` raising `TypeError` with opentelemetry-api 1.45, where `Logger.enabled` became abstract. `enabled()` now returns `False` while replaying history events, matching `emit()`. diff --git a/temporalio/contrib/opentelemetry/_logger_provider.py b/temporalio/contrib/opentelemetry/_logger_provider.py index 0d663bd976..31cd0d1e4c 100644 --- a/temporalio/contrib/opentelemetry/_logger_provider.py +++ b/temporalio/contrib/opentelemetry/_logger_provider.py @@ -52,6 +52,19 @@ def emit(self, *args: Any, **kwargs: Any) -> None: return self._logger.emit(*args, **kwargs) + def enabled(self, *args: Any, **kwargs: Any) -> bool: + # Abstract on Logger from opentelemetry-api 1.45, so without this + # override the wrapper cannot be instantiated there. Callers use it to + # skip building records, so it must agree with emit() during replay. + if _skip_emitting(): + return False + enabled = getattr(self._logger, "enabled", None) + if enabled is None: + # Loggers from opentelemetry-api before 1.45 have no enabled(), and + # their callers always emit, so leave the decision to emit(). + return True + return enabled(*args, **kwargs) + class ReplaySafeLoggerProvider(LoggerProvider): """A logger provider that is safe for use during workflow replay. diff --git a/tests/contrib/opentelemetry/test_logger_provider.py b/tests/contrib/opentelemetry/test_logger_provider.py index f26f8b508b..e3d1ac09d8 100644 --- a/tests/contrib/opentelemetry/test_logger_provider.py +++ b/tests/contrib/opentelemetry/test_logger_provider.py @@ -154,6 +154,73 @@ def test_replay_safe_logger_provider_emits_from_live_operations_during_replay(): assert [(r.event_name, r.body) for r in records] == [("live-query", "kept")] +class _EnabledLogger(NoOpLogger): + def __init__(self) -> None: + super().__init__("enabled-logger") + self.enabled_calls: list[dict[str, Any]] = [] + + def enabled(self, **kwargs: Any) -> bool: + self.enabled_calls.append(kwargs) + return True + + +class _EnabledLoggerProvider(NoOpLoggerProvider): + def __init__(self) -> None: + self.logger = _EnabledLogger() + + def get_logger(self, *args: Any, **kwargs: Any) -> Logger: # type: ignore[override] + return self.logger + + +def test_replay_safe_logger_provider_delegates_enabled_outside_workflow(): + inner_provider = _EnabledLoggerProvider() + logger: Any = ReplaySafeLoggerProvider(inner_provider).get_logger("test-logger") + + assert logger.enabled(event_name="event") + assert inner_provider.logger.enabled_calls == [{"event_name": "event"}] + + +def test_replay_safe_logger_provider_disabled_replaying_history_events(): + """Callers that check enabled() before building a record must skip it + during replay, since emit() would drop it.""" + inner_provider = _EnabledLoggerProvider() + logger: Any = ReplaySafeLoggerProvider(inner_provider).get_logger("test-logger") + + with _workflow_replay_state(replaying_history_events=True): + assert not logger.enabled(event_name="event") + assert not inner_provider.logger.enabled_calls + + +def test_replay_safe_logger_provider_enabled_for_live_operations_during_replay(): + inner_provider = _EnabledLoggerProvider() + logger: Any = ReplaySafeLoggerProvider(inner_provider).get_logger("test-logger") + + with _workflow_replay_state(replaying_history_events=False): + assert logger.enabled(event_name="live-query") + assert inner_provider.logger.enabled_calls == [{"event_name": "live-query"}] + + +def test_replay_safe_logger_provider_enabled_without_inner_enabled(): + """Loggers from opentelemetry-api before 1.45 have no enabled(), so + callers that probe for it must not get an AttributeError.""" + + class LoggerWithoutEnabled: + def emit(self, *_args: Any, **_kwargs: Any) -> None: + pass + + class LoggerWithoutEnabledProvider(NoOpLoggerProvider): + def get_logger(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[override] + return LoggerWithoutEnabled() + + logger: Any = ReplaySafeLoggerProvider(LoggerWithoutEnabledProvider()).get_logger( + "test-logger" + ) + + assert logger.enabled() + with _workflow_replay_state(replaying_history_events=True): + assert not logger.enabled() + + def test_replay_safe_logger_provider_delegates_other_attributes(): provider, _ = _sdk_provider() assert provider.force_flush()