Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 97a36ea

Browse files
authored
Capture exception information in log attributes (open-telemetry#2531)
1 parent f7b33c6 commit 97a36ea

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Fix incorrect installation of some exporter “convenience” packages into
1111
“site-packages/src”
1212
([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525))
13+
- Capture exception information as part of log attributes
14+
([#2531](https://github.com/open-telemetry/opentelemetry-python/pull/2531))
1315
- Change OTLPHandler to LoggingHandler
1416
([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528))
1517
- Fix delta histogram sum not being reset on collection

opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import os
2121
import threading
22+
import traceback
2223
from typing import Any, Callable, Optional, Tuple, Union, cast
2324

2425
from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp
@@ -28,6 +29,7 @@
2829
from opentelemetry.sdk.resources import Resource
2930
from opentelemetry.sdk.util import ns_to_iso_str
3031
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
32+
from opentelemetry.semconv.trace import SpanAttributes
3133
from opentelemetry.trace import (
3234
format_span_id,
3335
format_trace_id,
@@ -319,9 +321,27 @@ def __init__(
319321

320322
@staticmethod
321323
def _get_attributes(record: logging.LogRecord) -> Attributes:
322-
return {
324+
attributes = {
323325
k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS
324326
}
327+
if record.exc_info is not None:
328+
exc_type = ""
329+
message = ""
330+
stack_trace = ""
331+
exctype, value, tb = record.exc_info
332+
if exctype is not None:
333+
exc_type = exctype.__name__
334+
if value is not None and value.args:
335+
message = value.args[0]
336+
if tb is not None:
337+
# https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation
338+
stack_trace = "".join(
339+
traceback.format_exception(*record.exc_info)
340+
)
341+
attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type
342+
attributes[SpanAttributes.EXCEPTION_MESSAGE] = message
343+
attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace
344+
return attributes
325345

326346
def _translate(self, record: logging.LogRecord) -> LogRecord:
327347
timestamp = int(record.created * 1e9)

opentelemetry-sdk/tests/logs/test_handler.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
1514
import logging
1615
import unittest
1716
from unittest.mock import Mock
1817

1918
from opentelemetry.sdk import trace
2019
from opentelemetry.sdk._logs import LogEmitter, LoggingHandler
2120
from opentelemetry.sdk._logs.severity import SeverityNumber
21+
from opentelemetry.semconv.trace import SpanAttributes
2222
from opentelemetry.trace import INVALID_SPAN_CONTEXT
2323

2424

@@ -77,6 +77,36 @@ def test_log_record_user_attributes(self):
7777
self.assertIsNotNone(log_record)
7878
self.assertEqual(log_record.attributes, {"http.status_code": 200})
7979

80+
def test_log_record_exception(self):
81+
"""Exception information will be included in attributes"""
82+
emitter_mock = Mock(spec=LogEmitter)
83+
logger = get_logger(log_emitter=emitter_mock)
84+
try:
85+
raise ZeroDivisionError("division by zero")
86+
except ZeroDivisionError:
87+
logger.exception("Zero Division Error")
88+
args, _ = emitter_mock.emit.call_args_list[0]
89+
log_record = args[0]
90+
91+
self.assertIsNotNone(log_record)
92+
self.assertEqual(log_record.body, "Zero Division Error")
93+
self.assertEqual(
94+
log_record.attributes[SpanAttributes.EXCEPTION_TYPE],
95+
ZeroDivisionError.__name__,
96+
)
97+
self.assertEqual(
98+
log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE],
99+
"division by zero",
100+
)
101+
stack_trace = log_record.attributes[
102+
SpanAttributes.EXCEPTION_STACKTRACE
103+
]
104+
self.assertIsInstance(stack_trace, str)
105+
self.assertTrue("Traceback" in stack_trace)
106+
self.assertTrue("ZeroDivisionError" in stack_trace)
107+
self.assertTrue("division by zero" in stack_trace)
108+
self.assertTrue(__file__ in stack_trace)
109+
80110
def test_log_record_trace_correlation(self):
81111
emitter_mock = Mock(spec=LogEmitter)
82112
logger = get_logger(log_emitter=emitter_mock)

0 commit comments

Comments
 (0)