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

Add exception on console #168

Merged
merged 7 commits into from
May 13, 2024
Merged
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
27 changes: 26 additions & 1 deletion logfire/_internal/exporters/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
import sys
from collections.abc import Sequence
from datetime import datetime, timezone
from textwrap import indent as indent_text
from typing import Any, List, Literal, Mapping, TextIO, Tuple, cast

from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace import Event, ReadableSpan
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
from opentelemetry.util import types as otel_types
from rich.columns import Columns
Expand Down Expand Up @@ -125,6 +126,9 @@ def _print_span(self, span: ReadableSpan, indent: int = 0):
# in the rich case it uses syntax highlighting and columns for layout.
self._print_arguments(span, indent_str)

exc_event = next((event for event in span.events or [] if event.name == 'exception'), None)
self._print_exc_info(exc_event, indent_str)

def _span_text_parts(self, span: ReadableSpan, indent: int) -> tuple[str, TextParts]:
"""Return the formatted message or span name and parts containing basic span information.

Expand Down Expand Up @@ -255,6 +259,27 @@ def _print_arguments_plain(self, arguments: dict[str, Any], indent_str: str) ->
out += [f'{prefix}{line}']
print('\n'.join(out), file=self._output)

def _print_exc_info(self, exc_event: Event | None, indent_str: str) -> None:
"""Print exception information if an exception event is present."""
if exc_event is None or not exc_event.attributes:
return

exc_type = cast(str, exc_event.attributes.get('exception.type'))
exc_msg = cast(str, exc_event.attributes.get('exception.message'))
exc_tb = cast(str, exc_event.attributes.get('exception.stacktrace'))

if self._console:
barrier = Text(indent_str + '│ ', style='blue', end='')
exc_type = Text(f'{exc_type}: ', end='', style='bold red')
exc_msg = Text(exc_msg)
indented_code = indent_text(exc_tb, indent_str + '│ ')
exc_tb = Syntax(indented_code, 'python', background_color='default')
self._console.print(Group(barrier, exc_type, exc_msg), exc_tb)
else:
out = [f'{indent_str}│ {exc_type}: {exc_msg}']
out += [indent_text(exc_tb, indent_str + '│ ')]
print('\n'.join(out), file=self._output)

def force_flush(self, timeout_millis: int = 0) -> bool: # pragma: no cover
"""Force flush all spans, does nothing for this exporter."""
return True
Expand Down
99 changes: 99 additions & 0 deletions tests/test_console_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from __future__ import annotations

import io
import sys

import pytest
from dirty_equals import IsStr
from inline_snapshot import snapshot
from opentelemetry import trace
from opentelemetry.sdk.trace import ReadableSpan
Expand Down Expand Up @@ -689,3 +691,100 @@ def test_console_logging_to_stdout(capsys: pytest.CaptureFixture[str]):
' outer span log message',
]
)


def test_exception(exporter: TestExporter) -> None:
try:
1 / 0 # type: ignore
except ZeroDivisionError:
logfire.exception('error!!! {a}', a='test')

spans = exported_spans_as_models(exporter)
assert spans == snapshot(
[
ReadableSpanModel(
name='error!!! {a}',
context=SpanContextModel(trace_id=1, span_id=1, is_remote=False),
parent=None,
start_time=1000000000,
end_time=1000000000,
attributes={
'logfire.span_type': 'log',
'logfire.level_num': 17,
'logfire.msg_template': 'error!!! {a}',
'logfire.msg': 'error!!! test',
'code.filepath': 'test_console_exporter.py',
'code.function': 'test_exception',
'code.lineno': 123,
'a': 'test',
'logfire.json_schema': '{"type":"object","properties":{"a":{}}}',
},
events=[
{
'name': 'exception',
'timestamp': 2000000000,
'attributes': {
'exception.type': 'ZeroDivisionError',
'exception.message': 'division by zero',
'exception.stacktrace': 'ZeroDivisionError: division by zero',
'exception.escaped': 'False',
},
}
],
resource=None,
)
]
)

issue_lines = (
[' │ 1 / 0 # type: ignore', ' │ ~~^~~']
if sys.version_info >= (3, 11)
else [' │ 1 / 0 # type: ignore']
)
out = io.StringIO()
SimpleConsoleSpanExporter(output=out, colors='never').export(exporter.exported_spans)
assert out.getvalue().splitlines() == snapshot(
[
'00:00:01.000 error!!! test',
' │ ZeroDivisionError: division by zero',
' │ Traceback (most recent call last):',
IsStr(regex=rf' │ File "{__file__}", line \d+, in test_exception'),
*issue_lines,
' │ ZeroDivisionError: division by zero',
'',
]
)

issue_lines = (
[
'\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[91;49m~\x1b[0m\x1b[91;49m~\x1b[0m\x1b[91;49m^\x1b[0m\x1b[91;49m~\x1b[0m\x1b[91;49m~\x1b[0m',
]
if sys.version_info >= (3, 11)
else []
)

out = io.StringIO()
SimpleConsoleSpanExporter(output=out, colors='always').export(exporter.exported_spans)
assert out.getvalue().splitlines() == [
'\x1b[32m00:00:01.000\x1b[0m \x1b[31merror!!! test\x1b[0m',
'\x1b[34m │ \x1b[0m\x1b[1;31mZeroDivisionError: ' '\x1b[0mdivision by zero',
'\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mTraceback\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49m(\x1b[0m\x1b[97;49mmost\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mrecent\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mcall\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mlast\x1b[0m\x1b[97;49m)\x1b[0m\x1b[97;49m:\x1b[0m',
IsStr(),
'\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[37;49m1\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[91;49m/\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[37;49m0\x1b[0m\x1b[97;49m \x1b[0m\x1b[37;49m# type: '
'ignore\x1b[0m',
*issue_lines,
'\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[92;49mZeroDivisionError\x1b[0m\x1b[97;49m:\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mdivision\x1b[0m\x1b[97;49m '
'\x1b[0m\x1b[97;49mby\x1b[0m\x1b[97;49m \x1b[0m\x1b[97;49mzero\x1b[0m',
'',
]