chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:28 +08:00
commit c56bef871b
9296 changed files with 1854228 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
+200
View File
@@ -0,0 +1,200 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import logging
import threading
import pytest
from haystack import Pipeline, component, tracing
from haystack.tracing.logging_tracer import LoggingTracer
@component
class Hello:
@component.output_types(output=str)
def run(self, word: str | None) -> dict[str, str]:
return {"output": f"Hello, {word}!"}
@component
class FailingComponent:
@component.output_types(output=str)
def run(self, word: str | None) -> dict[str, str]:
raise Exception("Failing component")
class TestLoggingTracer:
def test_init(self) -> None:
tracer = LoggingTracer()
assert tracer.tags_color_strings == {}
tracer = LoggingTracer(tags_color_strings={"tag_name": "color_string"})
assert tracer.tags_color_strings == {"tag_name": "color_string"}
def test_logging_tracer(self, caplog: pytest.LogCaptureFixture) -> None:
tracer = LoggingTracer()
caplog.set_level(logging.DEBUG)
with tracer.trace("test") as span:
span.set_tag("key", "value")
assert "Operation: test" in caplog.text
assert "key=value" in caplog.text
assert len(caplog.records) == 2
# structured logging - LoggingTracer dynamically adds these attributes to LogRecord
assert caplog.records[0].operation_name == "test" # type: ignore[attr-defined]
assert caplog.records[1].tag_name == "key" # type: ignore[attr-defined]
assert caplog.records[1].tag_value == "value" # type: ignore[attr-defined]
def test_concurrent_spans_do_not_interleave(self, caplog: pytest.LogCaptureFixture) -> None:
"""Spans closing from parallel threads keep each span's operation-name and tag records contiguous."""
tracer = LoggingTracer()
caplog.set_level(logging.DEBUG)
num_threads = 4
num_tags = 3
# Line every thread up so their span-exit emissions race, maximizing the chance of interleaving without a lock.
barrier = threading.Barrier(num_threads)
def emit_span(idx: int) -> None:
with tracer.trace(f"op-{idx}") as span:
barrier.wait()
for tag_i in range(num_tags):
span.set_tag(f"tag-{tag_i}", str(idx))
threads = [threading.Thread(target=emit_span, args=(i,)) for i in range(num_threads)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
# Bucket each tag record under the operation record that precedes it.
groups: list[tuple[str, list[str]]] = []
for record in caplog.records:
if hasattr(record, "operation_name"):
groups.append((record.operation_name, []))
elif hasattr(record, "tag_name") and groups:
groups[-1][1].append(record.tag_value) # type: ignore[attr-defined]
# One contiguous group per span, and every tag inside a group belongs to that span (no interleaving).
assert len(groups) == num_threads
for operation_name, tag_values in groups:
idx = operation_name.removeprefix("op-")
assert tag_values == [idx] * num_tags
def test_tracing_complex_values(self, caplog: pytest.LogCaptureFixture) -> None:
tracer = LoggingTracer()
caplog.set_level(logging.DEBUG)
with tracer.trace("test") as span:
span.set_tag("key", {"a": 1, "b": [2, 3, 4]})
assert "Operation: test" in caplog.text
assert 'key={"a": 1, "b": [2, 3, 4]}' in caplog.text
assert len(caplog.records) == 2
# structured logging - LoggingTracer dynamically adds these attributes to LogRecord
assert caplog.records[0].operation_name == "test" # type: ignore[attr-defined]
assert caplog.records[1].tag_name == "key" # type: ignore[attr-defined]
assert caplog.records[1].tag_value == '{"a": 1, "b": [2, 3, 4]}' # type: ignore[attr-defined]
def test_apply_color_strings(self, caplog: pytest.LogCaptureFixture) -> None:
tracer = LoggingTracer(tags_color_strings={"key": "color_string"})
caplog.set_level(logging.DEBUG)
with tracer.trace("test") as span:
span.set_tag("key", "value")
assert "color_string" in caplog.text
def test_logging_pipeline(self, caplog: pytest.LogCaptureFixture) -> None:
pipeline = Pipeline()
pipeline.add_component("hello", Hello())
pipeline.add_component("hello2", Hello())
pipeline.connect("hello.output", "hello2.word")
tracing.enable_tracing(LoggingTracer())
caplog.set_level(logging.DEBUG)
pipeline.run(data={"word": "world"})
records = caplog.records
assert any(
record.operation_name == "haystack.component.run" for record in records if hasattr(record, "operation_name")
)
assert any(
record.operation_name == "haystack.pipeline.run" for record in records if hasattr(record, "operation_name")
)
tags_records = [record for record in records if hasattr(record, "tag_name")]
assert any(record.tag_name == "haystack.component.name" for record in tags_records)
assert any(record.tag_value == "hello" for record in tags_records) # type: ignore[attr-defined]
tracing.disable_tracing()
def test_logging_pipeline_with_content_tracing(self, caplog: pytest.LogCaptureFixture) -> None:
pipeline = Pipeline()
pipeline.add_component("hello", Hello())
tracing.tracer.is_content_tracing_enabled = True
tracing.enable_tracing(LoggingTracer())
caplog.set_level(logging.DEBUG)
pipeline.run(data={"word": "world"})
records = caplog.records
tags_records = [record for record in records if hasattr(record, "tag_name")]
input_tag_value = [
record.tag_value # type: ignore[attr-defined]
for record in tags_records
if record.tag_name == "haystack.component.input"
][0]
assert input_tag_value == '{"word": "world"}'
output_tag_value = [
record.tag_value # type: ignore[attr-defined]
for record in tags_records
if record.tag_name == "haystack.component.output"
][0]
assert output_tag_value == '{"output": "Hello, world!"}'
tracing.tracer.is_content_tracing_enabled = False
tracing.disable_tracing()
def test_logging_pipeline_on_failure(self, caplog: pytest.LogCaptureFixture) -> None:
"""
Test that the LoggingTracer also logs events when a component fails.
"""
pipeline = Pipeline()
pipeline.add_component("failing_component", FailingComponent())
tracing.enable_tracing(LoggingTracer())
caplog.set_level(logging.DEBUG)
try:
pipeline.run(data={"word": "world"})
except Exception:
pass
records = caplog.records
assert any(
record.operation_name == "haystack.component.run" for record in records if hasattr(record, "operation_name")
)
assert any(
record.operation_name == "haystack.pipeline.run" for record in records if hasattr(record, "operation_name")
)
tags_records = [record for record in records if hasattr(record, "tag_name")]
assert any(record.tag_name == "haystack.component.name" for record in tags_records)
assert any(record.tag_value == "failing_component" for record in tags_records) # type: ignore[attr-defined]
tracing.disable_tracing()
+93
View File
@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from unittest.mock import Mock
from _pytest.monkeypatch import MonkeyPatch
from haystack.tracing.tracer import (
HAYSTACK_CONTENT_TRACING_ENABLED_ENV_VAR,
NullSpan,
NullTracer,
ProxyTracer,
Tracer,
disable_tracing,
enable_tracing,
is_tracing_enabled,
tracer,
)
from test.tracing.utils import SpyingSpan, SpyingTracer
class TestNullTracer:
def test_tracing(self) -> None:
assert isinstance(tracer.actual_tracer, NullTracer)
# None of this raises
with tracer.trace("operation", {"key": "value"}, parent_span=None) as span:
span.set_tag("key", "value")
span.set_tags({"key": "value"})
assert isinstance(tracer.current_span(), NullSpan)
current_span = tracer.current_span()
assert current_span is not None
assert isinstance(current_span.raw_span(), NullSpan)
class TestProxyTracer:
def test_tracing(self) -> None:
spying_tracer = SpyingTracer()
my_tracer = ProxyTracer(provided_tracer=spying_tracer)
parent_span = Mock(spec=SpyingSpan)
with my_tracer.trace("operation", {"key": "value"}, parent_span=parent_span) as span:
span.set_tag("key", "value")
span.set_tags({"key2": "value2"})
assert len(spying_tracer.spans) == 1
assert spying_tracer.spans[0].operation_name == "operation"
assert spying_tracer.spans[0].parent_span == parent_span
assert spying_tracer.spans[0].tags == {"key": "value", "key2": "value2"}
class TestConfigureTracer:
def test_enable_tracer(self) -> None:
my_tracer = Mock(spec=Tracer) # anything else than `NullTracer` works for this test
enable_tracing(my_tracer)
assert isinstance(tracer, ProxyTracer)
assert tracer.actual_tracer is my_tracer
assert is_tracing_enabled()
def test_disable_tracing(self) -> None:
my_tracker = Mock(spec=Tracer) # anything else than `NullTracer` works for this test
enable_tracing(my_tracker)
assert tracer.actual_tracer is my_tracker
disable_tracing()
assert isinstance(tracer.actual_tracer, NullTracer)
assert is_tracing_enabled() is False
class TestTracingContent:
def test_set_content_tag_with_enabled_content_tracing(self, spying_tracer: SpyingTracer) -> None:
# SpyingTracer supports content tracing by default
enable_tracing(spying_tracer)
with tracer.trace("test") as span:
span.set_content_tag("my_content", "my_content")
assert len(spying_tracer.spans) == 1
span = spying_tracer.spans[0]
assert span.tags == {"my_content": "my_content"}
def test_set_content_tag_when_disabled_via_env_variable(self, monkeypatch: MonkeyPatch) -> None:
# we test if content tracing is disabled when the env variable is set to false
monkeypatch.setenv(HAYSTACK_CONTENT_TRACING_ENABLED_ENV_VAR, "false")
proxy_tracer = ProxyTracer(provided_tracer=SpyingTracer())
assert proxy_tracer.is_content_tracing_enabled is False
+121
View File
@@ -0,0 +1,121 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import Any
import pytest
from haystack import Document
from haystack.tracing import utils
from haystack.tracing.utils import _serializable_value
class NonSerializableClass:
def __str__(self) -> str:
return "NonSerializableClass"
class ClassWithToDict:
def __init__(self, value: str):
self.value = value
def to_dict(self) -> dict:
return {"value": self.value}
class ClassWithBothMethods:
"""Class with both to_dict and _to_trace_dict methods."""
def __init__(self, data: bytes):
self.data = data
def to_dict(self) -> dict:
return {"data": list(self.data)}
def _to_trace_dict(self) -> dict:
return {"data": f"Binary ({len(self.data)} bytes)"}
class TestSerializableValue:
@pytest.mark.parametrize("value", [1, 1.0, True, False, "string", None])
def test_primitive_types(self, value: Any) -> None:
assert _serializable_value(value) == value
def test_list_serialized_recursively(self) -> None:
result = _serializable_value([1, "two", 3.0])
assert result == [1, "two", 3.0]
def test_dict_serialized_recursively(self) -> None:
result = _serializable_value({"a": 1, "b": "two"})
assert result == {"a": 1, "b": "two"}
def test_nested_list_and_dict(self) -> None:
value = {"items": [1, 2, {"nested": "value"}]}
result = _serializable_value(value)
assert result == {"items": [1, 2, {"nested": "value"}]}
def test_object_with_to_dict(self) -> None:
obj = ClassWithToDict("test")
result = _serializable_value(obj)
assert result == {"value": "test"}
def test_object_with_to_trace_dict_placeholders(self) -> None:
obj = ClassWithBothMethods(b"hello")
result = _serializable_value(obj, use_placeholders=True)
assert result == {"data": "Binary (5 bytes)"}
def test_object_with_to_trace_dict_no_placeholders(self) -> None:
obj = ClassWithBothMethods(b"hello")
result = _serializable_value(obj, use_placeholders=False)
assert result == {"data": [104, 101, 108, 108, 111]}
def test_list_of_objects_with_to_dict(self) -> None:
objs = [ClassWithToDict("a"), ClassWithToDict("b")]
result = _serializable_value(objs)
assert result == [{"value": "a"}, {"value": "b"}]
def test_dict_with_object_values(self) -> None:
value = {"obj": ClassWithToDict("test")}
result = _serializable_value(value)
assert result == {"obj": {"value": "test"}}
def test_object_without_serialization_methods(self) -> None:
obj = NonSerializableClass()
result = _serializable_value(obj)
assert result is obj
class TestTypeCoercion:
@pytest.mark.parametrize(
"raw_value,expected_tag_value",
[
(1, 1),
(1.0, 1.0),
(True, True),
(None, ""),
("string", "string"),
([1, 2, 3], "[1, 2, 3]"),
({"key": "value"}, '{"key": "value"}'),
(NonSerializableClass(), "NonSerializableClass"),
(
Document(id="1", content="text"),
'{"id": "1", "content": "text", "blob": null, "score": null, "embedding": null, '
'"sparse_embedding": null}',
),
(
[Document(id="1", content="text")],
'[{"id": "1", "content": "text", "blob": null, "score": null, "embedding": null, '
'"sparse_embedding": null}]',
),
(
{"key": Document(id="1", content="text")},
'{"key": {"id": "1", "content": "text", "blob": null, "score": null, "embedding": null, '
'"sparse_embedding": null}}',
),
],
)
def test_type_coercion(self, raw_value: Any, expected_tag_value: bool | str | int | float) -> None:
coerced_value = utils.coerce_tag_value(raw_value)
assert coerced_value == expected_tag_value
+54
View File
@@ -0,0 +1,54 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import contextlib
import dataclasses
import uuid
from collections.abc import Iterator
from typing import Any
from haystack.tracing import Span, Tracer
@dataclasses.dataclass
class SpyingSpan(Span):
operation_name: str
parent_span: Span | None = None
tags: dict[str, Any] = dataclasses.field(default_factory=dict)
trace_id: str | None = dataclasses.field(default_factory=lambda: str(uuid.uuid4()))
span_id: str | None = dataclasses.field(default_factory=lambda: str(uuid.uuid4()))
def set_tag(self, key: str, value: Any) -> None:
self.tags[key] = value
def get_correlation_data_for_logs(self) -> dict[str, Any]:
return {"trace_id": self.trace_id, "span_id": self.span_id}
def set_content_tag(self, key: str, value: Any) -> None:
"""
Set a content tag, but only if content tracing is enabled in the tracer.
"""
self.set_tag(key, value)
class SpyingTracer(Tracer):
def current_span(self) -> Span | None:
return self.spans[-1] if self.spans else None
def __init__(self) -> None:
self.spans: list[SpyingSpan] = []
@contextlib.contextmanager
def trace(
self, operation_name: str, tags: dict[str, Any] | None = None, parent_span: Span | None = None
) -> Iterator[Span]:
new_span = SpyingSpan(operation_name, parent_span)
for key, value in (tags or {}).items():
new_span.set_tag(key, value)
self.spans.append(new_span)
yield new_span