From 6ec03c7fa29e64b2b8d986acb7b638ddd6b559f0 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 06:03:15 -0400 Subject: [PATCH 1/6] Add exception on console --- logfire/_internal/exporters/console.py | 27 +++++++++++- tests/test_console_exporter.py | 60 ++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/logfire/_internal/exporters/console.py b/logfire/_internal/exporters/console.py index 808fa22b4..f73d19825 100644 --- a/logfire/_internal/exporters/console.py +++ b/logfire/_internal/exporters/console.py @@ -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 @@ -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. @@ -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 diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index ee9f2a96d..3b54d7482 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -4,6 +4,7 @@ import io import pytest +from dirty_equals import IsStr from inline_snapshot import snapshot from opentelemetry import trace from opentelemetry.sdk.trace import ReadableSpan @@ -689,3 +690,62 @@ 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, + ) + ] + ) + + 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'), + ' │ 1 / 0 # type: ignore', + ' │ ~~^~~', + ' │ ZeroDivisionError: division by zero', + '', + ] + ) From 595ae162ee6115f00d7cb784db03e1d3c10762de Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 06:40:10 -0400 Subject: [PATCH 2/6] fix older versions --- tests/test_console_exporter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index 3b54d7482..7e3ce161f 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -2,6 +2,7 @@ from __future__ import annotations import io +import sys import pytest from dirty_equals import IsStr @@ -735,6 +736,11 @@ def test_exception(exporter: TestExporter) -> 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( @@ -743,8 +749,7 @@ def test_exception(exporter: TestExporter) -> None: ' │ ZeroDivisionError: division by zero', ' │ Traceback (most recent call last):', IsStr(regex=rf' │ File "{__file__}", line \d+, in test_exception'), - ' │ 1 / 0 # type: ignore', - ' │ ~~^~~', + *issue_lines, ' │ ZeroDivisionError: division by zero', '', ] From 5149455347571f50c17d2ada1a894e2d8b617a41 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 07:11:35 -0400 Subject: [PATCH 3/6] increase coverage --- tests/test_console_exporter.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index 7e3ce161f..aae47df10 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -754,3 +754,29 @@ def test_exception(exporter: TestExporter) -> None: '', ] ) + + 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', + '\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', + '\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', + '', + ] From 1601fb502ba4d06ee7994fd52970febb2fbc3d71 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 07:17:35 -0400 Subject: [PATCH 4/6] check if this works --- tests/test_console_exporter.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index aae47df10..49f1e88a3 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -755,6 +755,15 @@ def test_exception(exporter: TestExporter) -> None: ] ) + 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 ['\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '] + ) + out = io.StringIO() SimpleConsoleSpanExporter(output=out, colors='always').export(exporter.exported_spans) assert out.getvalue().splitlines() == [ @@ -772,8 +781,7 @@ def test_exception(exporter: TestExporter) -> None: '\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', - '\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', + *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 ' From 2b75d9828c719ac822e8759fa1bec0294b73b440 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 07:36:55 -0400 Subject: [PATCH 5/6] and now --- tests/test_console_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index 49f1e88a3..0808d9bab 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -761,7 +761,7 @@ def test_exception(exporter: TestExporter) -> None: '\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 ['\x1b[97;49m \x1b[0m\x1b[35;49m│\x1b[0m\x1b[97;49m '] + else [] ) out = io.StringIO() From cac7e897faf9ba46c8847022708b0e8d9be37299 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 17:54:52 +0200 Subject: [PATCH 6/6] Update console.py Co-authored-by: Alex Hall --- logfire/_internal/exporters/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logfire/_internal/exporters/console.py b/logfire/_internal/exporters/console.py index f73d19825..69b9335c4 100644 --- a/logfire/_internal/exporters/console.py +++ b/logfire/_internal/exporters/console.py @@ -270,7 +270,7 @@ def _print_exc_info(self, exc_event: Event | None, indent_str: str) -> None: if self._console: barrier = Text(indent_str + '│ ', style='blue', end='') - exc_type = Text(f'{exc_type}' + ': ', end='', style='bold red') + 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')