From 34a3445842b21cc22be78b8dd53c7eaaf5a6ff96 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 2 May 2024 15:54:20 +0200 Subject: [PATCH 1/5] Add `min_log_level` to console options --- logfire/_internal/config.py | 7 +++++++ logfire/_internal/config_params.py | 5 ++++- logfire/_internal/exporters/console.py | 14 ++++++++++++-- tests/test_console_exporter.py | 19 ++++++++++++++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index 3a911181..81605fcb 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -58,6 +58,7 @@ OTLP_MAX_BODY_SIZE, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS, SUPPRESS_INSTRUMENTATION_CONTEXT_KEY, + LevelName, ) from .exporters.console import ( ConsoleColorsValues, @@ -101,7 +102,11 @@ class ConsoleOptions: span_style: Literal['simple', 'indented', 'show-parents'] = 'show-parents' """How spans are shown in the console.""" include_timestamps: bool = True + """Whether to include timestamps in the console output.""" verbose: bool = False + """Whether to show verbose output.""" + min_log_level: LevelName = 'info' + """The minimum log level to show in the console.""" @dataclass @@ -368,6 +373,7 @@ def _load_configuration( span_style=param_manager.load_param('console_span_style'), include_timestamps=param_manager.load_param('console_include_timestamp'), verbose=param_manager.load_param('console_verbose'), + min_log_level=param_manager.load_param('console_min_log_level'), ) if isinstance(pydantic_plugin, dict): @@ -586,6 +592,7 @@ def add_span_processor(span_processor: SpanProcessor) -> None: colors=self.console.colors, include_timestamp=self.console.include_timestamps, verbose=self.console.verbose, + min_log_level=self.console.min_log_level, ), ) ) diff --git a/logfire/_internal/config_params.py b/logfire/_internal/config_params.py index abc7f635..d687b03b 100644 --- a/logfire/_internal/config_params.py +++ b/logfire/_internal/config_params.py @@ -13,7 +13,7 @@ from logfire.exceptions import LogfireConfigError from . import config -from .constants import LOGFIRE_BASE_URL +from .constants import LOGFIRE_BASE_URL, LevelName from .exporters.console import ConsoleColorsValues from .utils import read_toml_file @@ -81,6 +81,8 @@ class ConfigParam: """Whether to include the timestamp in the console.""" CONSOLE_VERBOSE = ConfigParam(env_vars=['LOGFIRE_CONSOLE_VERBOSE'], allow_file_config=True, default=False, tp=bool) """Whether to log in verbose mode in the console.""" +CONSOLE_MIN_LOG_LEVEL = ConfigParam(env_vars=['LOGFIRE_CONSOLE_MIN_LOG_LEVEL'], allow_file_config=True, default='info', tp=LevelName) +"""Minimum log level to show in the console.""" PYDANTIC_PLUGIN_RECORD = ConfigParam(env_vars=['LOGFIRE_PYDANTIC_PLUGIN_RECORD'], allow_file_config=True, default='off', tp=PydanticPluginRecordValues) """Whether instrument Pydantic validation..""" PYDANTIC_PLUGIN_INCLUDE = ConfigParam(env_vars=['LOGFIRE_PYDANTIC_PLUGIN_INCLUDE'], allow_file_config=True, default=set(), tp=Set[str]) @@ -107,6 +109,7 @@ class ConfigParam: 'console_span_style': CONSOLE_SPAN_STYLE, 'console_include_timestamp': CONSOLE_INCLUDE_TIMESTAMP, 'console_verbose': CONSOLE_VERBOSE, + 'console_min_log_level': CONSOLE_MIN_LOG_LEVEL, 'pydantic_plugin_record': PYDANTIC_PLUGIN_RECORD, 'pydantic_plugin_include': PYDANTIC_PLUGIN_INCLUDE, 'pydantic_plugin_exclude': PYDANTIC_PLUGIN_EXCLUDE, diff --git a/logfire/_internal/exporters/console.py b/logfire/_internal/exporters/console.py index 5abf8d89..f88c8aa0 100644 --- a/logfire/_internal/exporters/console.py +++ b/logfire/_internal/exporters/console.py @@ -31,10 +31,12 @@ LEVEL_NUMBERS, NUMBER_TO_LEVEL, ONE_SECOND_IN_NANOSECONDS, + LevelName, ) from ..json_formatter import json_args_value_formatter ConsoleColorsValues = Literal['auto', 'always', 'never'] +_INFO_LEVEL = LEVEL_NUMBERS['info'] _WARN_LEVEL = LEVEL_NUMBERS['warn'] _ERROR_LEVEL = LEVEL_NUMBERS['error'] @@ -56,6 +58,7 @@ def __init__( colors: ConsoleColorsValues = 'auto', include_timestamp: bool = True, verbose: bool = False, + min_log_level: LevelName = 'info', ) -> None: self._output = output or sys.stdout if colors == 'auto': @@ -78,10 +81,15 @@ def __init__( # timestamp len('12:34:56.789') 12 + space (1) self._timestamp_indent = 13 if include_timestamp else 0 self._verbose = verbose + self._min_log_level_num = LEVEL_NUMBERS[min_log_level] def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: """Export the spans to the console.""" for span in spans: + if span.attributes: + log_level: int = span.attributes.get(ATTRIBUTES_LOG_LEVEL_NUM_KEY, _INFO_LEVEL) # type: ignore + if log_level < self._min_log_level_num: + continue self._log_span(span) return SpanExportResult.SUCCESS @@ -265,8 +273,9 @@ def __init__( colors: ConsoleColorsValues = 'auto', include_timestamp: bool = True, verbose: bool = False, + min_log_level: LevelName = 'info', ) -> None: - super().__init__(output, colors, include_timestamp, verbose) + super().__init__(output, colors, include_timestamp, verbose, min_log_level) # lookup from span ID to indent level self._indent_level: dict[int, int] = {} @@ -312,8 +321,9 @@ def __init__( colors: ConsoleColorsValues = 'auto', include_timestamp: bool = True, verbose: bool = False, + min_log_level: LevelName = 'info', ) -> None: - super().__init__(output, colors, include_timestamp, verbose) + super().__init__(output, colors, include_timestamp, verbose, min_log_level) # lookup from span_id to `(indent, span message, parent id)` self._span_history: dict[int, tuple[int, str, int]] = {} diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index e5ceebc4..62c3c580 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -4,6 +4,7 @@ import io import pytest +from inline_snapshot import snapshot from opentelemetry import trace from opentelemetry.sdk.trace import ReadableSpan @@ -598,7 +599,7 @@ def test_levels(exporter: TestExporter): ] out = io.StringIO() - SimpleConsoleSpanExporter(output=out, colors='never').export(spans) # type: ignore + SimpleConsoleSpanExporter(output=out, colors='never', min_log_level='trace').export(spans) # type: ignore # insert_assert(out.getvalue().splitlines()) assert out.getvalue().splitlines() == [ '00:00:01.000 trace message', @@ -611,7 +612,7 @@ def test_levels(exporter: TestExporter): ] out = io.StringIO() - SimpleConsoleSpanExporter(output=out, colors='never', verbose=True).export(spans) # type: ignore + SimpleConsoleSpanExporter(output=out, colors='never', verbose=True, min_log_level='trace').export(spans) # type: ignore # insert_assert(out.getvalue().splitlines()) assert out.getvalue().splitlines() == [ '00:00:01.000 trace message', @@ -631,7 +632,7 @@ def test_levels(exporter: TestExporter): ] out = io.StringIO() - SimpleConsoleSpanExporter(output=out, colors='always').export(spans) # type: ignore + SimpleConsoleSpanExporter(output=out, colors='always', min_log_level='trace').export(spans) # type: ignore # insert_assert(out.getvalue().splitlines()) assert out.getvalue().splitlines() == [ '\x1b[32m00:00:01.000\x1b[0m trace message', @@ -643,6 +644,18 @@ def test_levels(exporter: TestExporter): '\x1b[32m00:00:07.000\x1b[0m \x1b[31mfatal message\x1b[0m', ] + out = io.StringIO() + SimpleConsoleSpanExporter(output=out, min_log_level='info').export(spans) # type: ignore + assert out.getvalue().splitlines() == snapshot( + [ + '00:00:03.000 info message', + '00:00:04.000 notice message', + '00:00:05.000 warn message', + '00:00:06.000 error message', + '00:00:07.000 fatal message', + ] + ) + def test_console_logging_to_stdout(capsys: pytest.CaptureFixture[str]): # This is essentially a basic integration test, the other tests using an exporter From 4b0dcc35242928d136be166617eb19c452d3af81 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 2 May 2024 15:55:29 +0200 Subject: [PATCH 2/5] Revert "Add log level on `on_start` for ASGI send and receive messages" This reverts commit b3fd2b3b0f7c6d26ef6d7c4fcfe60c33aef03d3f. --- .../_internal/exporters/processor_wrapper.py | 80 +++++++++---------- tests/otel_integrations/test_fastapi.py | 4 - 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index f07b492e..a2592e9d 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -4,7 +4,6 @@ from opentelemetry import context from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor -from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.semconv.trace import SpanAttributes from ..constants import ( @@ -36,14 +35,13 @@ def on_start( ) -> None: if context.get_value('suppress_instrumentation'): # pragma: no cover return - _set_log_level_on_asgi_send_receive_spans(span) self.processor.on_start(span, parent_context) def on_end(self, span: ReadableSpan) -> None: if context.get_value('suppress_instrumentation'): # pragma: no cover return span_dict = span_to_dict(span) - _tweak_asgi_send_receive_spans(span_dict) + _tweak_asgi_send_recieve_spans(span_dict) _tweak_http_spans(span_dict) self.scrubber.scrub_span(span_dict) span = ReadableSpan(**span_dict) @@ -56,51 +54,53 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return self.processor.force_flush(timeout_millis) # pragma: no cover -def _set_log_level_on_asgi_send_receive_spans(span: Span) -> None: - """Set the log level of ASGI send/receive spans to debug. - - If a span doesn't have a level set, it defaults to 'info'. This is too high for ASGI send/receive spans, - which are generated for every request and are not particularly interesting. - """ - if _is_asgi_send_receive_span(span.name, span.instrumentation_scope): - span.set_attributes(log_level_attributes('debug')) - - -def _tweak_asgi_send_receive_spans(span: ReadableSpanDict) -> None: - """Make the name/message of spans generated by OTEL's ASGI middleware more useful. +def _tweak_asgi_send_recieve_spans(span: ReadableSpanDict): + """Make the name/message/level of spans generated by OTEL's ASGI middleware more useful. For example, a single request will typically generate two 'send' spans with the same message, e.g. 'GET /foo http send'. This function may add part of the ASGI event type to the name to make it more useful, so instead it shows e.g. 'http send response.start' and 'http send response.body'. - """ - name = span['name'] - if _is_asgi_send_receive_span(name, span['instrumentation_scope']): - attributes = span['attributes'] - # The attribute name should be `asgi.event.type` after this is merged and released: - # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300 - typ = attributes.get('asgi.event.type') or attributes.get('type') - if not ( - isinstance(typ, str) - and typ.startswith(('http.', 'websocket.')) - and attributes.get(ATTRIBUTES_MESSAGE_KEY) == name - ): # pragma: no cover - return - # Strip the 'http.' or 'websocket.' prefix from the event type and add it to the span name. - if typ in ('websocket.send', 'websocket.receive'): - # No point in adding anything in this case, otherwise it'd say e.g. 'websocket send send'. - # No other event types in https://asgi.readthedocs.io/en/latest/specs/www.html are redundant like this. - new_name = name - else: - span['name'] = new_name = f'{name} {typ.split(".", 1)[1]}' + The log level of these spans is also set to debug, as they are not usually interesting to the user. + """ + instrumentation_scope = span['instrumentation_scope'] + if not (instrumentation_scope and instrumentation_scope.name == 'opentelemetry.instrumentation.asgi'): + return - span['attributes'] = {**attributes, ATTRIBUTES_MESSAGE_KEY: new_name} + if not (name := span['name']).endswith( + ( + ' http send', + ' http receive', + ' websocket send', + ' websocket receive', + ) + ): + return + attributes = span['attributes'] + # The attribute name should be `asgi.event.type` after this is merged and released: + # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300 + typ = attributes.get('asgi.event.type') or attributes.get('type') + if not ( + isinstance(typ, str) + and typ.startswith(('http.', 'websocket.')) + and attributes.get(ATTRIBUTES_MESSAGE_KEY) == name + ): # pragma: no cover + return -def _is_asgi_send_receive_span(name: str, instrumentation_scope: InstrumentationScope | None) -> bool: - return ( - instrumentation_scope is not None and instrumentation_scope.name == 'opentelemetry.instrumentation.asgi' - ) and (name.endswith((' http send', ' http receive', ' websocket send', ' websocket receive'))) + # Strip the 'http.' or 'websocket.' prefix from the event type and add it to the span name. + if typ in ('websocket.send', 'websocket.receive'): + # No point in adding anything in this case, otherwise it'd say e.g. 'websocket send send'. + # No other event types in https://asgi.readthedocs.io/en/latest/specs/www.html are redundant like this. + new_name = name + else: + span['name'] = new_name = f'{name} {typ.split(".", 1)[1]}' + + span['attributes'] = { + **attributes, + ATTRIBUTES_MESSAGE_KEY: new_name, + **log_level_attributes('debug'), + } def _tweak_http_spans(span: ReadableSpanDict): diff --git a/tests/otel_integrations/test_fastapi.py b/tests/otel_integrations/test_fastapi.py index dcd8c6dc..9458a98d 100644 --- a/tests/otel_integrations/test_fastapi.py +++ b/tests/otel_integrations/test_fastapi.py @@ -234,7 +234,6 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.msg': 'GET /with_path_param/{param} http send', - 'logfire.level_num': 5, 'logfire.pending_parent_id': '0000000000000001', }, }, @@ -261,7 +260,6 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.msg': 'GET /with_path_param/{param} http send', - 'logfire.level_num': 5, 'logfire.pending_parent_id': '0000000000000001', }, }, @@ -423,7 +421,6 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.pending_parent_id': '0000000000000003', - 'logfire.level_num': 5, 'logfire.msg': 'GET / http send', }, }, @@ -450,7 +447,6 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.pending_parent_id': '0000000000000003', - 'logfire.level_num': 5, 'logfire.msg': 'GET / http send', }, }, From ce546497c15a34225344a4610524a65e072e6bd5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 2 May 2024 16:20:00 +0200 Subject: [PATCH 3/5] Add more info --- logfire/_internal/config.py | 5 ++++- tests/test_console_exporter.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index 81605fcb..f07081ca 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -104,7 +104,10 @@ class ConsoleOptions: include_timestamps: bool = True """Whether to include timestamps in the console output.""" verbose: bool = False - """Whether to show verbose output.""" + """Whether to show verbose output. + + It includes the filename, log level, and line number. + """ min_log_level: LevelName = 'info' """The minimum log level to show in the console.""" diff --git a/tests/test_console_exporter.py b/tests/test_console_exporter.py index 62c3c580..31fb89a2 100644 --- a/tests/test_console_exporter.py +++ b/tests/test_console_exporter.py @@ -645,7 +645,8 @@ def test_levels(exporter: TestExporter): ] out = io.StringIO() - SimpleConsoleSpanExporter(output=out, min_log_level='info').export(spans) # type: ignore + # The `min_log_level` is set to 'info' by default, so only 'info' and higher levels are logged. + SimpleConsoleSpanExporter(output=out).export(spans) # type: ignore assert out.getvalue().splitlines() == snapshot( [ '00:00:03.000 info message', From 502f452c1a4834ba98a989d5e6b6c1074d731480 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 3 May 2024 09:41:47 +0200 Subject: [PATCH 4/5] Add pragma: no branch --- 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 f88c8aa0..808fa22b 100644 --- a/logfire/_internal/exporters/console.py +++ b/logfire/_internal/exporters/console.py @@ -86,7 +86,7 @@ def __init__( def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: """Export the spans to the console.""" for span in spans: - if span.attributes: + if span.attributes: # pragma: no branch log_level: int = span.attributes.get(ATTRIBUTES_LOG_LEVEL_NUM_KEY, _INFO_LEVEL) # type: ignore if log_level < self._min_log_level_num: continue From 79a2326b9193b1a7b546d67573f1359a4ab1efb4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 3 May 2024 09:43:17 +0200 Subject: [PATCH 5/5] Revert "Revert "Add log level on `on_start` for ASGI send and receive messages"" This reverts commit 4b0dcc35242928d136be166617eb19c452d3af81. --- .../_internal/exporters/processor_wrapper.py | 80 +++++++++---------- tests/otel_integrations/test_fastapi.py | 4 + 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index a2592e9d..f07b492e 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -4,6 +4,7 @@ from opentelemetry import context from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.semconv.trace import SpanAttributes from ..constants import ( @@ -35,13 +36,14 @@ def on_start( ) -> None: if context.get_value('suppress_instrumentation'): # pragma: no cover return + _set_log_level_on_asgi_send_receive_spans(span) self.processor.on_start(span, parent_context) def on_end(self, span: ReadableSpan) -> None: if context.get_value('suppress_instrumentation'): # pragma: no cover return span_dict = span_to_dict(span) - _tweak_asgi_send_recieve_spans(span_dict) + _tweak_asgi_send_receive_spans(span_dict) _tweak_http_spans(span_dict) self.scrubber.scrub_span(span_dict) span = ReadableSpan(**span_dict) @@ -54,53 +56,51 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return self.processor.force_flush(timeout_millis) # pragma: no cover -def _tweak_asgi_send_recieve_spans(span: ReadableSpanDict): - """Make the name/message/level of spans generated by OTEL's ASGI middleware more useful. +def _set_log_level_on_asgi_send_receive_spans(span: Span) -> None: + """Set the log level of ASGI send/receive spans to debug. + + If a span doesn't have a level set, it defaults to 'info'. This is too high for ASGI send/receive spans, + which are generated for every request and are not particularly interesting. + """ + if _is_asgi_send_receive_span(span.name, span.instrumentation_scope): + span.set_attributes(log_level_attributes('debug')) + + +def _tweak_asgi_send_receive_spans(span: ReadableSpanDict) -> None: + """Make the name/message of spans generated by OTEL's ASGI middleware more useful. For example, a single request will typically generate two 'send' spans with the same message, e.g. 'GET /foo http send'. This function may add part of the ASGI event type to the name to make it more useful, so instead it shows e.g. 'http send response.start' and 'http send response.body'. - - The log level of these spans is also set to debug, as they are not usually interesting to the user. """ - instrumentation_scope = span['instrumentation_scope'] - if not (instrumentation_scope and instrumentation_scope.name == 'opentelemetry.instrumentation.asgi'): - return + name = span['name'] + if _is_asgi_send_receive_span(name, span['instrumentation_scope']): + attributes = span['attributes'] + # The attribute name should be `asgi.event.type` after this is merged and released: + # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300 + typ = attributes.get('asgi.event.type') or attributes.get('type') + if not ( + isinstance(typ, str) + and typ.startswith(('http.', 'websocket.')) + and attributes.get(ATTRIBUTES_MESSAGE_KEY) == name + ): # pragma: no cover + return - if not (name := span['name']).endswith( - ( - ' http send', - ' http receive', - ' websocket send', - ' websocket receive', - ) - ): - return + # Strip the 'http.' or 'websocket.' prefix from the event type and add it to the span name. + if typ in ('websocket.send', 'websocket.receive'): + # No point in adding anything in this case, otherwise it'd say e.g. 'websocket send send'. + # No other event types in https://asgi.readthedocs.io/en/latest/specs/www.html are redundant like this. + new_name = name + else: + span['name'] = new_name = f'{name} {typ.split(".", 1)[1]}' + + span['attributes'] = {**attributes, ATTRIBUTES_MESSAGE_KEY: new_name} - attributes = span['attributes'] - # The attribute name should be `asgi.event.type` after this is merged and released: - # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300 - typ = attributes.get('asgi.event.type') or attributes.get('type') - if not ( - isinstance(typ, str) - and typ.startswith(('http.', 'websocket.')) - and attributes.get(ATTRIBUTES_MESSAGE_KEY) == name - ): # pragma: no cover - return - # Strip the 'http.' or 'websocket.' prefix from the event type and add it to the span name. - if typ in ('websocket.send', 'websocket.receive'): - # No point in adding anything in this case, otherwise it'd say e.g. 'websocket send send'. - # No other event types in https://asgi.readthedocs.io/en/latest/specs/www.html are redundant like this. - new_name = name - else: - span['name'] = new_name = f'{name} {typ.split(".", 1)[1]}' - - span['attributes'] = { - **attributes, - ATTRIBUTES_MESSAGE_KEY: new_name, - **log_level_attributes('debug'), - } +def _is_asgi_send_receive_span(name: str, instrumentation_scope: InstrumentationScope | None) -> bool: + return ( + instrumentation_scope is not None and instrumentation_scope.name == 'opentelemetry.instrumentation.asgi' + ) and (name.endswith((' http send', ' http receive', ' websocket send', ' websocket receive'))) def _tweak_http_spans(span: ReadableSpanDict): diff --git a/tests/otel_integrations/test_fastapi.py b/tests/otel_integrations/test_fastapi.py index 9458a98d..dcd8c6dc 100644 --- a/tests/otel_integrations/test_fastapi.py +++ b/tests/otel_integrations/test_fastapi.py @@ -234,6 +234,7 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.msg': 'GET /with_path_param/{param} http send', + 'logfire.level_num': 5, 'logfire.pending_parent_id': '0000000000000001', }, }, @@ -260,6 +261,7 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.msg': 'GET /with_path_param/{param} http send', + 'logfire.level_num': 5, 'logfire.pending_parent_id': '0000000000000001', }, }, @@ -421,6 +423,7 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.pending_parent_id': '0000000000000003', + 'logfire.level_num': 5, 'logfire.msg': 'GET / http send', }, }, @@ -447,6 +450,7 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'attributes': { 'logfire.span_type': 'pending_span', 'logfire.pending_parent_id': '0000000000000003', + 'logfire.level_num': 5, 'logfire.msg': 'GET / http send', }, },