Coverage for haystack/tracing/logging_tracer.py: 100%
36 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5import contextlib
6import dataclasses
7import threading
8from collections.abc import Iterator
9from typing import Any
11from haystack import logging
12from haystack.tracing import Span, Tracer
13from haystack.tracing.utils import coerce_tag_value
15logger = logging.getLogger(__name__)
17RESET_COLOR = "\033[0m"
20@dataclasses.dataclass
21class LoggingSpan(Span):
22 operation_name: str
23 tags: dict[str, Any] = dataclasses.field(default_factory=dict)
25 def set_tag(self, key: str, value: Any) -> None:
26 """
27 Set a single tag on the span.
29 :param key: the name of the tag.
30 :param value: the value of the tag.
31 """
32 self.tags[key] = value
35class LoggingTracer(Tracer):
36 """
37 A simple tracer that logs the operation name and tags of a span.
38 """
40 def __init__(self, tags_color_strings: dict[str, str] | None = None) -> None:
41 """
42 Initialize the LoggingTracer.
44 :param tags_color_strings:
45 A dictionary that maps tag names to color strings that should be used when logging the tags.
46 The color strings should be in the format of
47 [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors).
48 For example, to color the tag "haystack.component.input" in red, you would pass
49 `tags_color_strings={"haystack.component.input": "\x1b[1;31m"}`.
50 """
52 self.tags_color_strings = tags_color_strings or {}
53 # Spans can be created and closed from parallel worker threads (e.g. one span per tool call in the Agent).
54 # A span's operation name and its tags are emitted as separate log records, so we serialize the emission to
55 # keep each span's records contiguous instead of interleaved with those of concurrently-closing spans.
56 self._emit_lock = threading.Lock()
58 @contextlib.contextmanager
59 def trace(
60 self,
61 operation_name: str,
62 tags: dict[str, Any] | None = None,
63 parent_span: Span | None = None, # noqa: ARG002
64 ) -> Iterator[Span]:
65 """
66 Trace the execution of a block of code.
68 :param operation_name: the name of the operation being traced.
69 :param tags: tags to apply to the newly created span.
70 :param parent_span: the parent span to use for the newly created span. Not used in this simple tracer.
71 :returns: the newly created span.
72 """
74 custom_span = LoggingSpan(operation_name, tags=tags or {})
76 try:
77 yield custom_span
78 except Exception as e: # noqa: TRY203
79 raise e
80 # we make sure to log the operation name and tags of the span when the context manager exits
81 # both in case of success and error
82 finally:
83 operation_name = custom_span.operation_name
84 tags = custom_span.tags or {}
85 with self._emit_lock:
86 logger.debug("Operation: {operation_name}", operation_name=operation_name)
87 for tag_name, tag_value in tags.items():
88 color_string = self.tags_color_strings.get(tag_name, "")
89 coerced_value = coerce_tag_value(tag_value)
90 logger.debug(
91 color_string + "{tag_name}={tag_value}" + RESET_COLOR,
92 tag_name=tag_name,
93 tag_value=coerced_value,
94 )
96 def current_span(self) -> Span | None:
97 """Return the current active span, if any."""
98 # we don't store spans in this simple tracer
99 return None