Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 (
code_attributes,

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.

I think code_attributes are already stable no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/attributes/code_attributes.py, it exists both in the stable and experimental folder. I can change the import to import from stable path. This attribute was already being imported. Not part of this change.

event_attributes,
)
from opentelemetry.semconv.attributes import (
exception_attributes,
otel_attributes,
)
from opentelemetry.util.types import AnyValue

_internal_logger = logging.getLogger(__name__ + ".internal")
Expand Down Expand Up @@ -132,11 +138,25 @@ 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?

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 +178,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 +188,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 +208,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