Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture exception information in log attributes #2531

Merged
merged 16 commits into from
Mar 21, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix incorrect installation of some exporter “convenience” packages into
“site-packages/src”
([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525))
- Capture exception information as part of log attributes
([#2531](https://github.com/open-telemetry/opentelemetry-python/pull/2531))
- Change OTLPHandler to LoggingHandler
([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528))
- Fix delta histogram sum not being reset on collection
Expand Down
22 changes: 21 additions & 1 deletion opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging
import os
import threading
import traceback
from typing import Any, Callable, Optional, Tuple, Union, cast

from opentelemetry.sdk._logs.severity import SeverityNumber, std_to_otlp
Expand All @@ -28,6 +29,7 @@
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util import ns_to_iso_str
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import (
format_span_id,
format_trace_id,
Expand Down Expand Up @@ -319,9 +321,27 @@ def __init__(

@staticmethod
def _get_attributes(record: logging.LogRecord) -> Attributes:
return {
attributes = {
k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS
}
if record.exc_info is not None:
exc_type = ""
message = ""
stack_trace = ""
exctype, value, tb = record.exc_info
if exctype is not None:
exc_type = exctype.__name__
if value is not None and value.args:
message = value.args[0]
lzchen marked this conversation as resolved.
Show resolved Hide resolved
if tb is not None:
# https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation
stack_trace = "".join(
traceback.format_exception(*record.exc_info)
lzchen marked this conversation as resolved.
Show resolved Hide resolved
)
attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type
attributes[SpanAttributes.EXCEPTION_MESSAGE] = message
lzchen marked this conversation as resolved.
Show resolved Hide resolved
attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace
return attributes

def _translate(self, record: logging.LogRecord) -> LogRecord:
timestamp = int(record.created * 1e9)
Expand Down
32 changes: 31 additions & 1 deletion opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import unittest
from unittest.mock import Mock

from opentelemetry.sdk import trace
from opentelemetry.sdk._logs import LogEmitter, LoggingHandler
from opentelemetry.sdk._logs.severity import SeverityNumber
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import INVALID_SPAN_CONTEXT


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

def test_log_record_exception(self):
"""Exception information will be included in attributes"""
emitter_mock = Mock(spec=LogEmitter)
logger = get_logger(log_emitter=emitter_mock)
try:
raise ZeroDivisionError("division by zero")
except ZeroDivisionError:
logger.exception("Zero Division Error")
args, _ = emitter_mock.emit.call_args_list[0]
log_record = args[0]

self.assertIsNotNone(log_record)
self.assertEqual(log_record.body, "Zero Division Error")
self.assertEqual(
log_record.attributes[SpanAttributes.EXCEPTION_TYPE],
ZeroDivisionError.__name__,
)
self.assertEqual(
log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE],
"division by zero",
)
stack_trace = log_record.attributes[
SpanAttributes.EXCEPTION_STACKTRACE
]
self.assertIsInstance(stack_trace, str)
self.assertTrue("Traceback" in stack_trace)
self.assertTrue("ZeroDivisionError" in stack_trace)
self.assertTrue("division by zero" in stack_trace)
self.assertTrue(__file__ in stack_trace)

def test_log_record_trace_correlation(self):
emitter_mock = Mock(spec=LogEmitter)
logger = get_logger(log_emitter=emitter_mock)
Expand Down