Skip to content
Merged
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.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