Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
118 changes: 114 additions & 4 deletions opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""


Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand Down
14 changes: 13 additions & 1 deletion opentelemetry-api/tests/logs/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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)


Expand Down
48 changes: 46 additions & 2 deletions opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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)
14 changes: 13 additions & 1 deletion opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down