Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changelog/26.added
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Logs: add public `Logger.enabled(context, severity_number, event_name)` to the
API and SDK, delegating to processor-level `enabled` hooks
38 changes: 38 additions & 0 deletions opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,32 @@ def emit(
) -> None:
"""Emits a :class:`LogRecord` representing a log to the processing pipeline."""

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns whether this `Logger` is enabled for the given arguments.

This is an optional performance optimization that lets callers avoid
the cost of constructing a :class:`LogRecord` when it would not be
processed. A returned value of ``True`` means the `Logger` is enabled
for the provided arguments; a returned value of ``False`` means the
`Logger` is disabled for them.

The parameters mirror the spec's ``EnabledParameters``:

Args:
context: The context associated with the log record.
severity_number: The severity number of the log record.
event_name: The event name of the log record.

The default implementation returns ``False``, matching the behavior of
the no-op API where no log records are ever emitted.
"""
return False


class NoOpLogger(Logger):
"""The default Logger used when no Logger implementation is available.
Expand Down Expand Up @@ -300,6 +326,18 @@ def emit(
exception=exception,
)

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
return self._logger.enabled(
context=context,
severity_number=severity_number,
event_name=event_name,
)


class LoggerProvider(ABC):
"""
Expand Down
49 changes: 49 additions & 0 deletions opentelemetry-api/tests/logs/test_logger_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

# pylint: disable=protected-access
import unittest
from unittest.mock import Mock

import opentelemetry._logs._internal as logs_internal
from opentelemetry._logs import NoOpLogger
from opentelemetry._logs.severity import SeverityNumber


class TestLoggerEnabled(unittest.TestCase):
def test_noop_logger_enabled_returns_false_by_default(self):
logger = NoOpLogger("noop-test")
self.assertFalse(logger.enabled())

def test_noop_logger_enabled_accepts_parameters(self):
logger = NoOpLogger("noop-test")
self.assertFalse(
logger.enabled(
context=None,
severity_number=SeverityNumber.INFO,
event_name="my.event",
)
)

def test_proxy_logger_delegates_enabled_to_real_logger(self):
logger = logs_internal.ProxyLogger("proxy-test")
real_logger = Mock()
real_logger.enabled.return_value = True
logger._real_logger = real_logger

result = logger.enabled(
severity_number=SeverityNumber.WARN, event_name="warn.event"
)

self.assertTrue(result)
real_logger.enabled.assert_called_once_with(
context=None,
severity_number=SeverityNumber.WARN,
event_name="warn.event",
)

def test_proxy_logger_enabled_falls_back_to_noop(self):
# No real logger and no provider set: proxy uses its no-op logger,
# whose enabled() returns False.
logger = logs_internal.ProxyLogger("proxy-test")
self.assertFalse(logger.enabled())
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,29 @@ def force_flush(self, timeout_millis: int = 30000) -> bool:
False if the timeout is exceeded, True otherwise.
"""

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns whether this processor would process a log record with the
given arguments.

This is an optional performance-optimization hook used by
:meth:`Logger.enabled`. Processors that can cheaply determine that a
log record will be dropped (for example, a severity-based filter) may
override this to return ``False``. The default implementation returns
``True`` because a processor cannot generally determine ahead of time
whether a record will be processed.

Args:
context: The context associated with the log record.
severity_number: The severity number of the log record.
event_name: The event name of the log record.
"""
return True


# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved
# pylint:disable=no-member
Expand Down Expand Up @@ -424,6 +447,23 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None:
for lp in self._log_record_processors:
lp.on_emit(log_record)

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns True if any registered processor is enabled for the given
arguments. Returns False when no processors are registered."""
for lp in self._log_record_processors:
if lp.enabled(
context=context,
severity_number=severity_number,
event_name=event_name,
):
return True
return False

def shutdown(self) -> None:
"""Shutdown the log processors one by one"""
for lp in self._log_record_processors:
Expand Down Expand Up @@ -498,6 +538,23 @@ def _submit_and_wait(
def on_emit(self, log_record: ReadWriteLogRecord) -> None:
self._submit_and_wait(lambda lp: lp.on_emit, log_record)

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns True if any registered processor is enabled for the given
arguments. Returns False when no processors are registered."""
for lp in self._log_record_processors:
if lp.enabled(
context=context,
severity_number=severity_number,
event_name=event_name,
):
return True
return False

def shutdown(self) -> None:
self._submit_and_wait(lambda lp: lp.shutdown)

Expand Down Expand Up @@ -719,6 +776,29 @@ def __init__(
def _is_enabled(self) -> bool:
return self._logger_config.is_enabled

def enabled(
self,
context: Context | None = None,
severity_number: SeverityNumber | None = None,
event_name: str | None = None,
) -> bool:
"""Returns whether this `Logger` is enabled for the given arguments.

The `Logger` is enabled when it has not been disabled by the logger
configuration and at least one registered log record processor is
enabled for the provided arguments. Processors that do not override
their :meth:`LogRecordProcessor.enabled` hook report enabled by
default, so this returns ``True`` whenever at least one processor is
registered and the logger is not disabled.
"""
if not self._is_enabled():
return False
return self._multi_log_record_processor.enabled(
context=context,
severity_number=severity_number,
event_name=event_name,
)

def _set_logger_config(self, logger_config: _LoggerConfig) -> None:
self._logger_config = logger_config

Expand Down
84 changes: 84 additions & 0 deletions opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
ReadWriteLogRecord,
)
from opentelemetry.sdk._logs._internal import (
ConcurrentMultiLogRecordProcessor,
LogRecordProcessor,
NoOpLogger,
SynchronousMultiLogRecordProcessor,
_disable_logger_configurator,
Expand Down Expand Up @@ -551,3 +553,85 @@ def test_emit_readwrite_logrecord_uses_exception(self):
self.assertEqual(
attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError"
)


class _RecordingProcessor(LogRecordProcessor):
def __init__(self, enabled: bool = True):
self._enabled = enabled

def on_emit(self, log_record) -> None:
pass

def shutdown(self) -> None:
pass

def force_flush(self, timeout_millis: int = 30000) -> bool:
return True

def enabled(
self, context=None, severity_number=None, event_name=None
) -> bool:
return self._enabled


class TestLoggerEnabled(unittest.TestCase):
def test_enabled_false_when_no_processors(self):
provider = LoggerProvider()
logger = provider.get_logger("test")
self.assertFalse(logger.enabled())

def test_enabled_true_with_default_processor(self):
provider = LoggerProvider()
provider.add_log_record_processor(_RecordingProcessor(enabled=True))
logger = provider.get_logger("test")
self.assertTrue(logger.enabled())

def test_enabled_false_when_processor_reports_disabled(self):
provider = LoggerProvider()
provider.add_log_record_processor(_RecordingProcessor(enabled=False))
logger = provider.get_logger("test")
self.assertFalse(logger.enabled())

def test_enabled_true_when_any_processor_enabled(self):
provider = LoggerProvider()
provider.add_log_record_processor(_RecordingProcessor(enabled=False))
provider.add_log_record_processor(_RecordingProcessor(enabled=True))
logger = provider.get_logger("test")
self.assertTrue(logger.enabled())

def test_enabled_false_when_logger_config_disabled(self):
provider = LoggerProvider(
_logger_configurator=_disable_logger_configurator
)
provider.add_log_record_processor(_RecordingProcessor(enabled=True))
logger = provider.get_logger("test")
self.assertFalse(logger.enabled())

def test_enabled_forwards_parameters_to_processor(self):
provider = LoggerProvider()
processor = Mock(spec=LogRecordProcessor)
processor.enabled.return_value = True
provider.add_log_record_processor(processor)
logger = provider.get_logger("test")

result = logger.enabled(
severity_number=SeverityNumber.ERROR, event_name="err.event"
)

self.assertTrue(result)
processor.enabled.assert_called_once_with(
context=None,
severity_number=SeverityNumber.ERROR,
event_name="err.event",
)

def test_default_processor_enabled_hook_returns_true(self):
self.assertTrue(_RecordingProcessor().enabled())

def test_concurrent_multi_processor_enabled(self):
multi = ConcurrentMultiLogRecordProcessor()
self.assertFalse(multi.enabled())
multi.add_log_record_processor(_RecordingProcessor(enabled=False))
self.assertFalse(multi.enabled())
multi.add_log_record_processor(_RecordingProcessor(enabled=True))
self.assertTrue(multi.enabled())
Loading