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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ 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
([#2525](https://github.com/open-telemetry/opentelemetry-python/pull/2525))

## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10



- Docs rework: [non-API docs are
moving](https://github.com/open-telemetry/opentelemetry-python/issues/2172) to
[opentelemetry.io](https://opentelemetry.io). For details, including a list of
Expand Down
24 changes: 23 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 @@ -33,6 +34,7 @@
format_trace_id,
get_current_span,
)
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.span import TraceFlags
from opentelemetry.util._providers import _load_provider
from opentelemetry.util._time import _time_ns
Expand Down Expand Up @@ -318,9 +320,29 @@ 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:
lzchen marked this conversation as resolved.
Show resolved Hide resolved
exctype, value, tb = record.exc_info
if exctype is not None:
exc_type = exctype.__name__
if value is not None:
lzchen marked this conversation as resolved.
Show resolved Hide resolved
message = value.args[0]
lzchen marked this conversation as resolved.
Show resolved Hide resolved
callstack = []
if tb is not None:
for fileName, line, method, text in traceback.extract_tb(tb):
callstack.append({
lzchen marked this conversation as resolved.
Show resolved Hide resolved
'method': method,
'fileName': fileName,
'line': line,
'text': text,
})
callstack.reverse()
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] = json.dumps(callstack)
return attributes

def _translate(self, record: logging.LogRecord) -> LogRecord:
timestamp = int(record.created * 1e9)
Expand Down
25 changes: 25 additions & 0 deletions opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
import unittest
from unittest.mock import Mock

from opentelemetry.sdk import trace
from opentelemetry.sdk._logs import LogEmitter, OTLPHandler
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 +79,29 @@ 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:
div = 1/0
except:
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")
print(log_record.attributes[SpanAttributes.EXCEPTION_STACKTRACE])
stack_trace = json.loads(log_record.attributes[SpanAttributes.EXCEPTION_STACKTRACE])
self.assertEqual(len(stack_trace), 1)
self.assertEqual(stack_trace[0]["method"], "test_log_record_exception")
self.assertEqual(stack_trace[0]["fileName"], __file__)
# self.assertEqual(stack_trace[0]["line"], 87)
lzchen marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(stack_trace[0]["text"], "div = 1/0")

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