feat(tracing): add optional Langfuse support (#1717)

* feat(tracing): add optional Langfuse support

* Fix tracing fail-fast behavior for explicitly enabled providers

* fix(lint)
This commit is contained in:
totoyang
2026-04-02 13:06:10 +08:00
committed by GitHub
parent 3a672b39c7
commit 2d1f90d5dc
13 changed files with 667 additions and 67 deletions
+13 -2
View File
@@ -73,7 +73,7 @@ def _patch_factory(monkeypatch, app_config: AppConfig, model_class=FakeChatModel
"""Patch get_app_config, resolve_class, and tracing for isolated unit tests."""
monkeypatch.setattr(factory_module, "get_app_config", lambda: app_config)
monkeypatch.setattr(factory_module, "resolve_class", lambda path, base: model_class)
monkeypatch.setattr(factory_module, "is_tracing_enabled", lambda: False)
monkeypatch.setattr(factory_module, "build_tracing_callbacks", lambda: [])
# ---------------------------------------------------------------------------
@@ -95,12 +95,23 @@ def test_uses_first_model_when_name_is_none(monkeypatch):
def test_raises_when_model_not_found(monkeypatch):
cfg = _make_app_config([_make_model("only-model")])
monkeypatch.setattr(factory_module, "get_app_config", lambda: cfg)
monkeypatch.setattr(factory_module, "is_tracing_enabled", lambda: False)
monkeypatch.setattr(factory_module, "build_tracing_callbacks", lambda: [])
with pytest.raises(ValueError, match="ghost-model"):
factory_module.create_chat_model(name="ghost-model")
def test_appends_all_tracing_callbacks(monkeypatch):
cfg = _make_app_config([_make_model("alpha")])
_patch_factory(monkeypatch, cfg)
monkeypatch.setattr(factory_module, "build_tracing_callbacks", lambda: ["smith-callback", "langfuse-callback"])
FakeChatModel.captured_kwargs = {}
model = factory_module.create_chat_model(name="alpha")
assert model.callbacks == ["smith-callback", "langfuse-callback"]
# ---------------------------------------------------------------------------
# thinking_enabled=True
# ---------------------------------------------------------------------------
+85 -10
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
import pytest
from deerflow.config import tracing_config as tracing_module
@@ -9,6 +11,29 @@ def _reset_tracing_cache() -> None:
tracing_module._tracing_config = None
@pytest.fixture(autouse=True)
def clear_tracing_env(monkeypatch):
for name in (
"LANGSMITH_TRACING",
"LANGCHAIN_TRACING_V2",
"LANGCHAIN_TRACING",
"LANGSMITH_API_KEY",
"LANGCHAIN_API_KEY",
"LANGSMITH_PROJECT",
"LANGCHAIN_PROJECT",
"LANGSMITH_ENDPOINT",
"LANGCHAIN_ENDPOINT",
"LANGFUSE_TRACING",
"LANGFUSE_PUBLIC_KEY",
"LANGFUSE_SECRET_KEY",
"LANGFUSE_BASE_URL",
):
monkeypatch.delenv(name, raising=False)
_reset_tracing_cache()
yield
_reset_tracing_cache()
def test_prefers_langsmith_env_names(monkeypatch):
monkeypatch.setenv("LANGSMITH_TRACING", "true")
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_key")
@@ -18,11 +43,12 @@ def test_prefers_langsmith_env_names(monkeypatch):
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.enabled is True
assert cfg.api_key == "lsv2_key"
assert cfg.project == "smith-project"
assert cfg.endpoint == "https://smith.example.com"
assert cfg.langsmith.enabled is True
assert cfg.langsmith.api_key == "lsv2_key"
assert cfg.langsmith.project == "smith-project"
assert cfg.langsmith.endpoint == "https://smith.example.com"
assert tracing_module.is_tracing_enabled() is True
assert tracing_module.get_enabled_tracing_providers() == ["langsmith"]
def test_falls_back_to_langchain_env_names(monkeypatch):
@@ -39,11 +65,12 @@ def test_falls_back_to_langchain_env_names(monkeypatch):
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.enabled is True
assert cfg.api_key == "legacy-key"
assert cfg.project == "legacy-project"
assert cfg.endpoint == "https://legacy.example.com"
assert cfg.langsmith.enabled is True
assert cfg.langsmith.api_key == "legacy-key"
assert cfg.langsmith.project == "legacy-project"
assert cfg.langsmith.endpoint == "https://legacy.example.com"
assert tracing_module.is_tracing_enabled() is True
assert tracing_module.get_enabled_tracing_providers() == ["langsmith"]
def test_langsmith_tracing_false_overrides_langchain_tracing_v2_true(monkeypatch):
@@ -55,8 +82,9 @@ def test_langsmith_tracing_false_overrides_langchain_tracing_v2_true(monkeypatch
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.enabled is False
assert cfg.langsmith.enabled is False
assert tracing_module.is_tracing_enabled() is False
assert tracing_module.get_enabled_tracing_providers() == []
def test_defaults_when_project_not_set(monkeypatch):
@@ -68,4 +96,51 @@ def test_defaults_when_project_not_set(monkeypatch):
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.project == "deer-flow"
assert cfg.langsmith.project == "deer-flow"
def test_langfuse_config_is_loaded(monkeypatch):
monkeypatch.setenv("LANGFUSE_TRACING", "true")
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
monkeypatch.setenv("LANGFUSE_BASE_URL", "https://langfuse.example.com")
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.langfuse.enabled is True
assert cfg.langfuse.public_key == "pk-lf-test"
assert cfg.langfuse.secret_key == "sk-lf-test"
assert cfg.langfuse.host == "https://langfuse.example.com"
assert tracing_module.get_enabled_tracing_providers() == ["langfuse"]
def test_dual_provider_config_is_loaded(monkeypatch):
monkeypatch.setenv("LANGSMITH_TRACING", "true")
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_key")
monkeypatch.setenv("LANGFUSE_TRACING", "true")
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
_reset_tracing_cache()
cfg = tracing_module.get_tracing_config()
assert cfg.langsmith.is_configured is True
assert cfg.langfuse.is_configured is True
assert tracing_module.is_tracing_enabled() is True
assert tracing_module.get_enabled_tracing_providers() == ["langsmith", "langfuse"]
def test_langfuse_enabled_requires_public_and_secret_keys(monkeypatch):
monkeypatch.setenv("LANGFUSE_TRACING", "true")
monkeypatch.delenv("LANGFUSE_PUBLIC_KEY", raising=False)
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
_reset_tracing_cache()
assert tracing_module.get_tracing_config().is_configured is False
assert tracing_module.get_enabled_tracing_providers() == []
assert tracing_module.get_tracing_config().explicitly_enabled_providers == ["langfuse"]
with pytest.raises(ValueError, match="LANGFUSE_PUBLIC_KEY"):
tracing_module.validate_enabled_tracing_providers()
+173
View File
@@ -0,0 +1,173 @@
"""Tests for deerflow.tracing.factory."""
from __future__ import annotations
import sys
import types
import pytest
from deerflow.tracing import factory as tracing_factory
@pytest.fixture(autouse=True)
def clear_tracing_env(monkeypatch):
from deerflow.config import tracing_config as tracing_module
for name in (
"LANGSMITH_TRACING",
"LANGCHAIN_TRACING_V2",
"LANGCHAIN_TRACING",
"LANGSMITH_API_KEY",
"LANGCHAIN_API_KEY",
"LANGSMITH_PROJECT",
"LANGCHAIN_PROJECT",
"LANGSMITH_ENDPOINT",
"LANGCHAIN_ENDPOINT",
"LANGFUSE_TRACING",
"LANGFUSE_PUBLIC_KEY",
"LANGFUSE_SECRET_KEY",
"LANGFUSE_BASE_URL",
):
monkeypatch.delenv(name, raising=False)
tracing_module._tracing_config = None
yield
tracing_module._tracing_config = None
def test_build_tracing_callbacks_returns_empty_list_when_disabled(monkeypatch):
monkeypatch.setattr(tracing_factory, "validate_enabled_tracing_providers", lambda: None)
monkeypatch.setattr(tracing_factory, "get_enabled_tracing_providers", lambda: [])
callbacks = tracing_factory.build_tracing_callbacks()
assert callbacks == []
def test_build_tracing_callbacks_creates_langsmith_and_langfuse(monkeypatch):
class FakeLangSmithTracer:
def __init__(self, *, project_name: str):
self.project_name = project_name
class FakeLangfuseHandler:
def __init__(self, *, public_key: str):
self.public_key = public_key
monkeypatch.setattr(tracing_factory, "get_enabled_tracing_providers", lambda: ["langsmith", "langfuse"])
monkeypatch.setattr(tracing_factory, "validate_enabled_tracing_providers", lambda: None)
monkeypatch.setattr(
tracing_factory,
"get_tracing_config",
lambda: type(
"Cfg",
(),
{
"langsmith": type("LangSmithCfg", (), {"project": "smith-project"})(),
"langfuse": type(
"LangfuseCfg",
(),
{
"secret_key": "sk-lf-test",
"public_key": "pk-lf-test",
"host": "https://langfuse.example.com",
},
)(),
},
)(),
)
monkeypatch.setattr(tracing_factory, "_create_langsmith_tracer", lambda cfg: FakeLangSmithTracer(project_name=cfg.project))
monkeypatch.setattr(
tracing_factory,
"_create_langfuse_handler",
lambda cfg: FakeLangfuseHandler(public_key=cfg.public_key),
)
callbacks = tracing_factory.build_tracing_callbacks()
assert len(callbacks) == 2
assert callbacks[0].project_name == "smith-project"
assert callbacks[1].public_key == "pk-lf-test"
def test_build_tracing_callbacks_raises_when_enabled_provider_fails(monkeypatch):
monkeypatch.setattr(tracing_factory, "get_enabled_tracing_providers", lambda: ["langfuse"])
monkeypatch.setattr(tracing_factory, "validate_enabled_tracing_providers", lambda: None)
monkeypatch.setattr(
tracing_factory,
"get_tracing_config",
lambda: type(
"Cfg",
(),
{
"langfuse": type(
"LangfuseCfg",
(),
{"secret_key": "sk-lf-test", "public_key": "pk-lf-test", "host": "https://langfuse.example.com"},
)(),
},
)(),
)
monkeypatch.setattr(tracing_factory, "_create_langfuse_handler", lambda cfg: (_ for _ in ()).throw(RuntimeError("boom")))
with pytest.raises(RuntimeError, match="Langfuse tracing initialization failed"):
tracing_factory.build_tracing_callbacks()
def test_build_tracing_callbacks_raises_for_explicitly_enabled_misconfigured_provider(monkeypatch):
from deerflow.config import tracing_config as tracing_module
monkeypatch.setenv("LANGFUSE_TRACING", "true")
monkeypatch.delenv("LANGFUSE_PUBLIC_KEY", raising=False)
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
tracing_module._tracing_config = None
with pytest.raises(ValueError, match="LANGFUSE_PUBLIC_KEY"):
tracing_factory.build_tracing_callbacks()
def test_create_langfuse_handler_initializes_client_before_handler(monkeypatch):
calls: list[tuple[str, dict]] = []
class FakeLangfuse:
def __init__(self, **kwargs):
calls.append(("client", kwargs))
class FakeCallbackHandler:
def __init__(self, **kwargs):
calls.append(("handler", kwargs))
fake_langfuse_module = types.ModuleType("langfuse")
fake_langfuse_module.Langfuse = FakeLangfuse
fake_langfuse_langchain_module = types.ModuleType("langfuse.langchain")
fake_langfuse_langchain_module.CallbackHandler = FakeCallbackHandler
monkeypatch.setitem(sys.modules, "langfuse", fake_langfuse_module)
monkeypatch.setitem(sys.modules, "langfuse.langchain", fake_langfuse_langchain_module)
cfg = type(
"LangfuseCfg",
(),
{
"secret_key": "sk-lf-test",
"public_key": "pk-lf-test",
"host": "https://langfuse.example.com",
},
)()
tracing_factory._create_langfuse_handler(cfg)
assert calls == [
(
"client",
{
"secret_key": "sk-lf-test",
"public_key": "pk-lf-test",
"host": "https://langfuse.example.com",
},
),
(
"handler",
{
"public_key": "pk-lf-test",
},
),
]