diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index 3a911181..f07081ca 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,14 @@ 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. + + It includes the filename, log level, and line number. + """ + min_log_level: LevelName = 'info' + """The minimum log level to show in the console.""" @dataclass @@ -368,6 +376,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 +595,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..808fa22b 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: # 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 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..31fb89a2 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,19 @@ def test_levels(exporter: TestExporter): '\x1b[32m00:00:07.000\x1b[0m \x1b[31mfatal message\x1b[0m', ] + out = io.StringIO() + # 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', + '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