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
1 change: 1 addition & 0 deletions .changelog/4864.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-instrumentation-logging`: Promote otel.event.name to LogRecord.event_name in _translate
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import traceback
from contextvars import ContextVar
from time import time_ns
from typing import Callable, Mapping
from typing import Callable

from opentelemetry._logs import (
LoggerProvider,
Expand All @@ -20,8 +20,14 @@
)
from opentelemetry.context import get_current
from opentelemetry.instrumentation.log_utils import std_to_otel
from opentelemetry.semconv._incubating.attributes import code_attributes
from opentelemetry.semconv.attributes import exception_attributes
from opentelemetry.semconv._incubating.attributes import (
event_attributes,
)
from opentelemetry.semconv.attributes import (
code_attributes,
exception_attributes,
otel_attributes,
)
from opentelemetry.util.types import AnyValue

_internal_logger = logging.getLogger(__name__ + ".internal")
Expand Down Expand Up @@ -132,11 +138,26 @@ def __init__(

def _get_attributes(
self, record: logging.LogRecord
) -> Mapping[str, AnyValue]:
) -> tuple[dict[str, AnyValue], str | None]:
attributes = {
k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS
}

# Promote otel.event.name (stable) or event.name (deprecated) to the
# first-class LogRecord.event_name field instead of leaving it as a
# plain attribute. otel.event.name takes precedence; event.name is a
# deprecated fallback per the OTel semantic conventions.
# Both keys are always popped so neither leaks into attributes.
event_name: str | None = attributes.pop(
otel_attributes.OTEL_EVENT_NAME, None
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you leave a todo comment to remove the deprecated branch path before making the package stable?

# TODO: Remove the deprecated branch path before marking logs stable
deprecated_event_name: str | None = attributes.pop(
event_attributes.EVENT_NAME, None
)
if event_name is None:
event_name = deprecated_event_name

if self._log_code_attributes:
# Add standard code attributes for logs.
attributes[code_attributes.CODE_FILE_PATH] = record.pathname
Expand All @@ -158,7 +179,7 @@ def _get_attributes(
attributes[exception_attributes.EXCEPTION_STACKTRACE] = (
"".join(traceback.format_exception(*record.exc_info))
)
return attributes
return attributes, event_name

def _translate(self, record: logging.LogRecord) -> LogRecord:
timestamp = int(record.created * 1e9)
Expand All @@ -168,7 +189,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord:
body = self.format(record)
else:
body = record.getMessage()
attributes = self._get_attributes(record)
attributes, event_name = self._get_attributes(record)

# Map Python log level names to OTel severity text as defined in
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#displaying-severity
Expand All @@ -188,6 +209,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord:
severity_number=severity_number,
body=body,
attributes=attributes,
event_name=event_name,
)

def emit(self, record: logging.LogRecord) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,92 @@ def test_logging_handler_without_env_var_uses_default_limit(self):
f"Should have 22 dropped attributes, got {record.dropped_attributes}",
)

# --- event_name promotion tests (issue #4743) ---

def test_otel_event_name_promoted_to_event_name_field(self):
"""otel.event.name in extra is promoted to LogRecord.event_name, not left as an attribute."""
processor, logger, handler = set_up_test_logging(logging.WARNING)

with self.assertLogs(level=logging.WARNING):
logger.warning(
"something happened",
extra={"otel.event.name": "my.event"},
)

record = processor.get_log_record(0)
self.assertEqual(record.log_record.event_name, "my.event")
self.assertNotIn("otel.event.name", record.log_record.attributes)

logger.removeHandler(handler)

def test_deprecated_event_name_promoted_as_fallback(self):
"""event.name (deprecated) is promoted to LogRecord.event_name when otel.event.name is absent."""
processor, logger, handler = set_up_test_logging(logging.WARNING)

with self.assertLogs(level=logging.WARNING):
logger.warning(
"something happened",
extra={"event.name": "legacy.event"},
)

record = processor.get_log_record(0)
self.assertEqual(record.log_record.event_name, "legacy.event")
self.assertNotIn("event.name", record.log_record.attributes)

logger.removeHandler(handler)

def test_otel_event_name_takes_precedence_over_deprecated_event_name(self):
"""otel.event.name wins over event.name when both are present."""
processor, logger, handler = set_up_test_logging(logging.WARNING)

with self.assertLogs(level=logging.WARNING):
logger.warning(
"something happened",
extra={
"otel.event.name": "stable.event",
"event.name": "legacy.event",
},
)

record = processor.get_log_record(0)
self.assertEqual(record.log_record.event_name, "stable.event")
self.assertNotIn("otel.event.name", record.log_record.attributes)
self.assertNotIn("event.name", record.log_record.attributes)

logger.removeHandler(handler)

def test_event_name_is_none_when_not_provided(self):
"""event_name is None when neither otel.event.name nor event.name is passed."""
processor, logger, handler = set_up_test_logging(logging.WARNING)

with self.assertLogs(level=logging.WARNING):
logger.warning("plain log, no event name")

record = processor.get_log_record(0)
self.assertIsNone(record.log_record.event_name)

logger.removeHandler(handler)

def test_other_extra_attributes_unaffected_by_event_name_promotion(self):
"""Unrelated extra attributes still land in the attributes dict normally."""
processor, logger, handler = set_up_test_logging(logging.WARNING)

with self.assertLogs(level=logging.WARNING):
logger.warning(
"something happened",
extra={
"otel.event.name": "my.event",
"http.status_code": 200,
},
)

record = processor.get_log_record(0)
self.assertEqual(record.log_record.event_name, "my.event")
self.assertEqual(record.log_record.attributes["http.status_code"], 200)
self.assertNotIn("otel.event.name", record.log_record.attributes)

logger.removeHandler(handler)


# pylint: disable=invalid-name
class SetupLoggingHandlerTestCase(unittest.TestCase):
Expand Down
Loading