diff --git a/CHANGELOG.md b/CHANGELOG.md index 870884ee2c5..90fcb56cb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `rstcheck` to pre-commit to stop introducing invalid RST ([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755)) +- logs: extend Logger.emit to accept separated keyword arguments + ([#4737](https://github.com/open-telemetry/opentelemetry-python/pull/4737)) ## Version 1.37.0/0.58b0 (2025-09-11) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 0d22564c66a..bbcfcddc846 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -33,6 +33,8 @@ .. versionadded:: 1.15.0 """ +from __future__ import annotations + from abc import ABC, abstractmethod from logging import getLogger from os import environ @@ -143,8 +145,40 @@ def __init__( self._schema_url = schema_url self._attributes = attributes + @overload + def emit( + self, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: ... + + @overload + def emit( + self, + record: LogRecord, + ) -> None: ... + @abstractmethod - def emit(self, record: "LogRecord") -> None: + def emit( + self, + record: LogRecord | None = None, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" @@ -154,7 +188,39 @@ class NoOpLogger(Logger): All operations are no-op. """ - def emit(self, record: "LogRecord") -> None: + @overload + def emit( + self, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: ... + + @overload + def emit( # pylint:disable=arguments-differ + self, + record: LogRecord, + ) -> None: ... + + def emit( + self, + record: LogRecord | None = None, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: pass @@ -188,8 +254,52 @@ def _logger(self) -> Logger: return self._real_logger return self._noop_logger - def emit(self, record: LogRecord) -> None: - self._logger.emit(record) + @overload + def emit( + self, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: ... + + @overload + def emit( # pylint:disable=arguments-differ + self, + record: LogRecord, + ) -> None: ... + + def emit( + self, + record: LogRecord | None = None, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: + if record: + self._logger.emit(record) + else: + self._logger.emit( + timestamp=timestamp, + observed_timestamp=observed_timestamp, + context=context, + severity_number=severity_number, + severity_text=severity_text, + body=body, + attributes=attributes, + event_name=event_name, + ) class LoggerProvider(ABC): diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 64c024c3fa1..d72ccc7c6b2 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -34,7 +34,19 @@ def get_logger( class LoggerTest(_logs.NoOpLogger): - def emit(self, record: _logs.LogRecord) -> None: + def emit( + self, + record: typing.Optional[_logs.LogRecord] = None, + *, + timestamp=None, + observed_timestamp=None, + context=None, + severity_number=None, + severity_text=None, + body=None, + attributes=None, + event_name=None, + ) -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index aca3d50c130..521a705e47b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -599,7 +599,7 @@ def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes: ) return attributes - def _translate(self, record: logging.LogRecord) -> LogRecord: + def _translate(self, record: logging.LogRecord) -> dict: timestamp = int(record.created * 1e9) observered_timestamp = time_ns() attributes = self._get_attributes(record) @@ -633,17 +633,15 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: "WARN" if record.levelname == "WARNING" else record.levelname ) - logger = get_logger(record.name, logger_provider=self._logger_provider) - return LogRecord( - timestamp=timestamp, - observed_timestamp=observered_timestamp, - context=get_current() or None, - severity_text=level_name, - severity_number=severity_number, - body=body, - resource=logger.resource, - attributes=attributes, - ) + return { + "timestamp": timestamp, + "observed_timestamp": observered_timestamp, + "context": get_current() or None, + "severity_text": level_name, + "severity_number": severity_number, + "body": body, + "attributes": attributes, + } def emit(self, record: logging.LogRecord) -> None: """ @@ -653,7 +651,7 @@ def emit(self, record: logging.LogRecord) -> None: """ logger = get_logger(record.name, logger_provider=self._logger_provider) if not isinstance(logger, NoOpLogger): - logger.emit(self._translate(record)) + logger.emit(**self._translate(record)) def flush(self) -> None: """ @@ -692,16 +690,63 @@ def __init__( def resource(self): return self._resource - def emit(self, record: APILogRecord): + @overload + def emit( + self, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + severity_text: str | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ) -> None: ... + + @overload + def emit( # pylint:disable=arguments-differ + self, + record: APILogRecord, + ) -> None: ... + + def emit( + self, + record: APILogRecord | None = None, + *, + timestamp: int | None = None, + observed_timestamp: int | None = None, + context: Context | None = None, + severity_text: str | None = None, + severity_number: SeverityNumber | None = None, + body: AnyValue | None = None, + attributes: _ExtendedAttributes | None = None, + event_name: str | None = None, + ): """Emits the :class:`LogData` by associating :class:`LogRecord` and instrumentation info. """ - if not isinstance(record, LogRecord): + + if not record: + record = LogRecord( + timestamp=timestamp, + observed_timestamp=observed_timestamp, + context=context, + severity_text=severity_text, + severity_number=severity_number, + body=body, + attributes=attributes, + event_name=event_name, + resource=self._resource, + ) + elif not isinstance(record, LogRecord): # pylint:disable=protected-access record = LogRecord._from_api_log_record( record=record, resource=self._resource ) + log_data = LogData(record, self._instrumentation_scope) + self._multi_log_record_processor.on_emit(log_data) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 33983c4f737..e4849e07a2e 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -18,7 +18,13 @@ from unittest.mock import Mock, patch from opentelemetry._logs import LogRecord as APILogRecord -from opentelemetry.sdk._logs import Logger, LoggerProvider, LogRecord +from opentelemetry._logs import SeverityNumber +from opentelemetry.context import get_current +from opentelemetry.sdk._logs import ( + Logger, + LoggerProvider, + LogRecord, +) from opentelemetry.sdk._logs._internal import ( NoOpLogger, SynchronousMultiLogRecordProcessor, @@ -116,6 +122,7 @@ def test_can_emit_logrecord(self): log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] self.assertTrue(isinstance(log_data.log_record, LogRecord)) + self.assertTrue(log_data.log_record is log_record) def test_can_emit_api_logrecord(self): logger, log_record_processor_mock = self._get_logger() @@ -126,4 +133,41 @@ def test_can_emit_api_logrecord(self): logger.emit(api_log_record) log_record_processor_mock.on_emit.assert_called_once() log_data = log_record_processor_mock.on_emit.call_args.args[0] - self.assertTrue(isinstance(log_data.log_record, LogRecord)) + log_record = log_data.log_record + self.assertTrue(isinstance(log_record, LogRecord)) + self.assertEqual(log_record.timestamp, None) + self.assertEqual(log_record.observed_timestamp, 0) + self.assertEqual(log_record.context, {}) + self.assertEqual(log_record.severity_number, None) + self.assertEqual(log_record.severity_text, None) + self.assertEqual(log_record.body, "a log line") + self.assertEqual(log_record.attributes, {}) + self.assertEqual(log_record.event_name, None) + self.assertEqual(log_record.resource, logger.resource) + + def test_can_emit_with_keywords_arguments(self): + logger, log_record_processor_mock = self._get_logger() + + logger.emit( + timestamp=100, + observed_timestamp=101, + context=get_current(), + severity_number=SeverityNumber.WARN, + severity_text="warn", + body="a body", + attributes={"some": "attributes"}, + event_name="event_name", + ) + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + log_record = log_data.log_record + self.assertTrue(isinstance(log_record, LogRecord)) + self.assertEqual(log_record.timestamp, 100) + self.assertEqual(log_record.observed_timestamp, 101) + self.assertEqual(log_record.context, {}) + self.assertEqual(log_record.severity_number, SeverityNumber.WARN) + self.assertEqual(log_record.severity_text, "warn") + self.assertEqual(log_record.body, "a body") + self.assertEqual(log_record.attributes, {"some": "attributes"}) + self.assertEqual(log_record.event_name, "event_name") + self.assertEqual(log_record.resource, logger.resource) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 6e9221b124d..cdb50082614 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -113,7 +113,19 @@ def __init__(self, name, resource, processor): self.resource = resource self.processor = processor - def emit(self, record): + def emit( + self, + record=None, + *, + timestamp=None, + observed_timestamp=None, + context=None, + severity_number=None, + severity_text=None, + body=None, + attributes=None, + event_name=None, + ): self.processor.emit(record)