6
from datetime import datetime, timezone
7
from pathlib import Path
8
from test.tracing.utils import SpyingTracer
9
from unittest.mock import ANY
12
from _pytest.capture import CaptureFixture
13
from _pytest.logging import LogCaptureFixture
14
from _pytest.monkeypatch import MonkeyPatch
16
import haystack.utils.jupyter
17
from haystack import logging as haystack_logging
20
@pytest.fixture(autouse=True)
21
def reset_logging_config() -> None:
22
old_handlers = logging.root.handlers.copy()
24
# Reset the logging configuration after each test to avoid impacting other tests
25
logging.root.handlers = old_handlers
28
class TestSkipLoggingConfiguration:
29
def test_skip_logging_configuration(
30
self, monkeypatch: MonkeyPatch, capfd: CaptureFixture, caplog: LogCaptureFixture
32
monkeypatch.setenv("HAYSTACK_LOGGING_IGNORE_STRUCTLOG", "true")
33
haystack_logging.configure_logging()
35
logger = logging.getLogger(__name__)
36
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
38
# the pytest fixture caplog only captures logs being rendered from the stdlib logging module
39
assert caplog.messages == ["Hello, structured logging!"]
41
# Nothing should be captured by capfd since structlog is not configured
42
assert capfd.readouterr().err == ""
45
class TestStructuredLoggingConsoleRendering:
46
def test_log_filtering_when_using_debug(self, capfd: CaptureFixture) -> None:
47
haystack_logging.configure_logging(use_json=False)
49
logger = logging.getLogger(__name__)
50
logger.debug("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
52
# Use `capfd` to capture the output of the final structlog rendering result
53
output = capfd.readouterr().err
56
def test_log_filtering_when_using_debug_and_log_level_is_debug(self, capfd: CaptureFixture) -> None:
57
haystack_logging.configure_logging(use_json=False)
59
logger = logging.getLogger(__name__)
60
logger.setLevel(logging.DEBUG)
62
logger.debug("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
64
# Use `capfd` to capture the output of the final structlog rendering result
65
output = capfd.readouterr().err
66
assert "Hello, structured logging" in output
67
assert "{" not in output, "Seems JSON rendering is enabled when it should not be"
69
def test_console_rendered_structured_log_even_if_no_tty_but_python_config(
70
self, capfd: CaptureFixture, monkeypatch: MonkeyPatch
72
monkeypatch.setattr(sys.stderr, "isatty", lambda: False)
74
haystack_logging.configure_logging(use_json=False)
76
logger = logging.getLogger(__name__)
77
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
79
# Use `capfd` to capture the output of the final structlog rendering result
80
output = capfd.readouterr().err
82
assert "Hello, structured logging!" in output
83
assert "{" not in output, "Seems JSON rendering is enabled when it should not be"
85
def test_console_rendered_structured_log_if_in_ipython(
86
self, capfd: CaptureFixture, monkeypatch: MonkeyPatch
88
monkeypatch.setattr(builtins, "__IPYTHON__", "true", raising=False)
90
haystack_logging.configure_logging()
92
logger = logging.getLogger(__name__)
93
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
95
# Use `capfd` to capture the output of the final structlog rendering result
96
output = capfd.readouterr().err
98
assert "Hello, structured logging!" in output
99
assert "{" not in output, "Seems JSON rendering is enabled when it should not be"
101
def test_console_rendered_structured_log_even_in_jupyter(
102
self, capfd: CaptureFixture, monkeypatch: MonkeyPatch
104
monkeypatch.setattr(haystack.utils.jupyter, haystack.utils.jupyter.is_in_jupyter.__name__, lambda: True)
106
haystack_logging.configure_logging()
108
logger = logging.getLogger(__name__)
109
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
111
# Use `capfd` to capture the output of the final structlog rendering result
112
output = capfd.readouterr().err
114
assert "Hello, structured logging!" in output
115
assert "{" not in output, "Seems JSON rendering is enabled when it should not be"
117
def test_console_rendered_structured_log_even_if_no_tty_but_forced_through_env(
118
self, capfd: CaptureFixture, monkeypatch: MonkeyPatch
120
monkeypatch.setenv("HAYSTACK_LOGGING_USE_JSON", "false")
122
haystack_logging.configure_logging()
124
logger = logging.getLogger(__name__)
125
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
127
# Use `capfd` to capture the output of the final structlog rendering result
128
output = capfd.readouterr().err
130
assert "Hello, structured logging!" in output
131
assert "{" not in output, "Seems JSON rendering is enabled when it should not be"
133
def test_console_rendered_structured_log(self, capfd: CaptureFixture) -> None:
134
haystack_logging.configure_logging()
136
logger = logging.getLogger(__name__)
137
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
139
# Use `capfd` to capture the output of the final structlog rendering result
140
output = capfd.readouterr().err
142
# Only check for the minute to be a bit more robust
143
today = datetime.now(tz=timezone.utc).isoformat(timespec="minutes").replace("+00:00", "")
144
assert today in output
146
log_level = "warning"
147
assert log_level in output
149
assert "Hello, structured logging!" in output
151
assert "key1" in output
152
assert "value1" in output
154
def test_logging_exceptions(self, capfd: CaptureFixture) -> None:
155
haystack_logging.configure_logging()
157
logger = logging.getLogger(__name__)
159
def function_that_raises_and_adds_to_stack_trace():
160
raise ValueError("This is an error")
163
function_that_raises_and_adds_to_stack_trace()
165
logger.exception("An error happened")
167
# Use `capfd` to capture the output of the final structlog rendering result
168
output = capfd.readouterr().err
170
assert "An error happened" in output
173
class TestStructuredLoggingJSONRendering:
174
def test_logging_as_json_if_not_atty(self, capfd: CaptureFixture, monkeypatch: MonkeyPatch) -> None:
175
monkeypatch.setattr(sys.stderr, "isatty", lambda: False)
176
haystack_logging.configure_logging()
178
logger = logging.getLogger(__name__)
179
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
181
# Use `capfd` to capture the output of the final structlog rendering result
182
output = capfd.readouterr().err
183
parsed_output = json.loads(output) # should not raise an error
185
assert parsed_output == {
186
"event": "Hello, structured logging!",
192
"module": "test.test_logging",
195
def test_logging_as_json(self, capfd: CaptureFixture) -> None:
196
haystack_logging.configure_logging(use_json=True)
198
logger = logging.getLogger(__name__)
199
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
201
# Use `capfd` to capture the output of the final structlog rendering result
202
output = capfd.readouterr().err
203
parsed_output = json.loads(output) # should not raise an error
205
assert parsed_output == {
206
"event": "Hello, structured logging!",
212
"module": "test.test_logging",
215
def test_logging_as_json_enabling_via_env(self, capfd: CaptureFixture, monkeypatch: MonkeyPatch) -> None:
216
monkeypatch.setenv("HAYSTACK_LOGGING_USE_JSON", "true")
217
haystack_logging.configure_logging()
219
logger = logging.getLogger(__name__)
220
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
222
# Use `capfd` to capture the output of the final structlog rendering result
223
output = capfd.readouterr().err
224
parsed_output = json.loads(output) # should not raise an error
226
assert parsed_output == {
227
"event": "Hello, structured logging!",
233
"module": "test.test_logging",
236
def test_logging_exceptions_json(self, capfd: CaptureFixture) -> None:
237
haystack_logging.configure_logging(use_json=True)
239
logger = logging.getLogger(__name__)
241
def function_that_raises_and_adds_to_stack_trace():
242
my_local_variable = "my_local_variable" # noqa: F841
243
raise ValueError("This is an error")
246
function_that_raises_and_adds_to_stack_trace()
248
logger.exception("An error happened ")
250
# Use `capfd` to capture the output of the final structlog rendering result
251
output = capfd.readouterr().err
252
parsed_output = json.loads(output)
253
assert parsed_output == {
254
"event": "An error happened ",
258
"module": "test.test_logging",
261
"exc_type": "ValueError",
262
"exc_value": "This is an error",
263
"syntax_error": None,
267
"filename": str(Path.cwd() / "test" / "test_logging.py"),
268
"lineno": ANY, # otherwise the test breaks if you add a line :-)
269
"name": "test_logging_exceptions_json",
274
"filename": str(Path.cwd() / "test" / "test_logging.py"),
275
"lineno": ANY, # otherwise the test breaks if you add a line :-)
276
"name": "function_that_raises_and_adds_to_stack_trace",
286
class TestLogTraceCorrelation:
287
def test_trace_log_correlation_python_logs_with_console_rendering(
288
self, spying_tracer: SpyingTracer, capfd: CaptureFixture
290
haystack_logging.configure_logging(use_json=False)
292
with spying_tracer.trace("test-operation"):
293
logger = logging.getLogger(__name__)
294
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
296
output = capfd.readouterr().err
297
assert "trace_id" not in output
299
def test_trace_log_correlation_python_logs(self, spying_tracer: SpyingTracer, capfd: CaptureFixture) -> None:
300
haystack_logging.configure_logging(use_json=True)
302
with spying_tracer.trace("test-operation") as span:
303
logger = logging.getLogger(__name__)
304
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
306
output = capfd.readouterr().err
307
parsed_output = json.loads(output)
309
assert parsed_output == {
310
"event": "Hello, structured logging!",
315
"trace_id": span.trace_id,
316
"span_id": span.span_id,
318
"module": "test.test_logging",
321
def test_trace_log_correlation_no_span(self, spying_tracer: SpyingTracer, capfd: CaptureFixture) -> None:
322
haystack_logging.configure_logging(use_json=True)
324
logger = logging.getLogger(__name__)
326
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
328
output = capfd.readouterr().err
329
parsed_output = json.loads(output)
331
assert parsed_output == {
332
"event": "Hello, structured logging!",
338
"module": "test.test_logging",
341
def test_trace_log_correlation_no_tracer(self, capfd: CaptureFixture) -> None:
342
haystack_logging.configure_logging(use_json=True)
344
logger = logging.getLogger(__name__)
346
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
348
output = capfd.readouterr().err
349
parsed_output = json.loads(output)
351
assert parsed_output == {
352
"event": "Hello, structured logging!",
358
"module": "test.test_logging",
362
class TestCompositeLogger:
363
def test_correct_stack_level_with_stdlib_rendering(
364
self, monkeypatch: MonkeyPatch, capfd: CaptureFixture, caplog: LogCaptureFixture
366
monkeypatch.setenv("HAYSTACK_LOGGING_IGNORE_STRUCTLOG", "true")
367
haystack_logging.configure_logging()
369
logger = logging.getLogger(__name__)
370
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
372
# the pytest fixture caplog only captures logs being rendered from the stdlib logging module
373
assert caplog.messages == ["Hello, structured logging!"]
374
assert caplog.records[0].name == "test.test_logging"
375
assert caplog.records[0].lineno == 370
377
# Nothing should be captured by capfd since structlog is not configured
378
assert capfd.readouterr().err == ""
380
def test_correct_stack_level_with_consoler_rendering(self, capfd: CaptureFixture) -> None:
381
haystack_logging.configure_logging(use_json=False)
383
logger = haystack_logging.getLogger(__name__)
384
logger.warning("Hello, structured logging!", extra={"key1": "value1", "key2": "value2"})
386
output = capfd.readouterr().err
387
assert "test.test_logging" in output
388
assert "384" in output
390
@pytest.mark.parametrize(
391
"method, expected_level",
395
("warning", "warning"),
397
("fatal", "critical"),
398
("exception", "error"),
399
("critical", "critical"),
402
def test_various_levels(self, capfd: LogCaptureFixture, method: str, expected_level: str) -> None:
403
haystack_logging.configure_logging(use_json=True)
405
logger = haystack_logging.getLogger(__name__)
407
logger.setLevel(logging.DEBUG)
409
getattr(logger, method)("Hello, structured {key}!", key="logging", key1="value1", key2="value2")
411
output = capfd.readouterr().err
412
parsed_output = json.loads(output) # should not raise an error
414
assert parsed_output == {
415
"event": "Hello, structured logging!",
419
"level": expected_level,
422
"module": "test.test_logging",
425
def test_log(self, capfd: LogCaptureFixture) -> None:
426
haystack_logging.configure_logging(use_json=True)
428
logger = haystack_logging.getLogger(__name__)
429
logger.setLevel(logging.DEBUG)
431
logger.log(logging.DEBUG, "Hello, structured '{key}'!", key="logging", key1="value1", key2="value2")
433
output = capfd.readouterr().err
434
parsed_output = json.loads(output)
436
assert parsed_output == {
437
"event": "Hello, structured 'logging'!",
444
"module": "test.test_logging",
447
def test_log_with_string_cast(self, capfd: LogCaptureFixture) -> None:
448
haystack_logging.configure_logging(use_json=True)
450
logger = haystack_logging.getLogger(__name__)
451
logger.setLevel(logging.DEBUG)
453
logger.log(logging.DEBUG, "Hello, structured '{key}'!", key=LogCaptureFixture, key1="value1", key2="value2")
455
output = capfd.readouterr().err
456
parsed_output = json.loads(output)
458
assert parsed_output == {
459
"event": "Hello, structured '<class '_pytest.logging.LogCaptureFixture'>'!",
460
"key": "<class '_pytest.logging.LogCaptureFixture'>",
466
"module": "test.test_logging",
469
@pytest.mark.parametrize(
470
"method, expected_level",
474
("warning", "warning"),
476
("fatal", "critical"),
477
("exception", "exception"),
478
("critical", "critical"),
481
def test_haystack_logger_with_positional_args(self, method: str, expected_level: str) -> None:
482
haystack_logging.configure_logging(use_json=True)
484
logger = haystack_logging.getLogger(__name__)
485
logger.setLevel(logging.DEBUG)
487
with pytest.raises(TypeError):
488
getattr(logger, method)("Hello, structured logging %s!", "logging")
490
@pytest.mark.parametrize(
491
"method, expected_level",
495
("warning", "warning"),
497
("fatal", "critical"),
498
("exception", "exception"),
499
("critical", "critical"),
502
def test_haystack_logger_with_old_interpolation(self, method: str, expected_level: str) -> None:
503
haystack_logging.configure_logging(use_json=True)
505
logger = haystack_logging.getLogger(__name__)
506
logger.setLevel(logging.DEBUG)
508
# does not raise - hence we need to check this separately
509
getattr(logger, method)("Hello, structured logging %s!", key="logging")
511
def test_that_haystack_logger_is_used(self) -> None:
512
"""Forces the usage of the Haystack logger instead of the standard library logger."""
513
allowed_list = [Path("haystack") / "logging.py"]
514
for root, dirs, files in os.walk("haystack"):
516
path = Path(root) / file
518
if not path.suffix.endswith(".py"):
521
if path in allowed_list:
524
content = path.read_text(encoding="utf-8")
526
# that looks like somebody is using our standard logger
527
if " logging.getLogger" in content:
528
haystack_logger_in_content = " haystack import logging" in content or ", logging" in content
529
assert haystack_logger_in_content, (
530
f"{path} doesn't use the Haystack logger. Please use the Haystack logger instead of the "
531
f"standard library logger and add plenty of keyword arguments."