diff --git a/CHANGELOG.md b/CHANGELOG.md index cd690d30..548b70c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Changed + +- Logfire spans carry `service.version` and a per-process `service.name` (`haiku-ingester`, `haiku-rag`, `haiku-rag-app`); `OTEL_SERVICE_NAME` / `LOGFIRE_SERVICE_NAME` override the default. + ## [0.65.0] - 2026-07-09 ### Added diff --git a/app/backend/main.py b/app/backend/main.py index cd157ccf..21793881 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -20,6 +20,7 @@ from haiku.rag.client import HaikuRAG from haiku.rag.config import load_yaml_config from haiku.rag.config.models import AppConfig from haiku.rag.skills.rag import create_skill, get_agent_preamble +from haiku.rag.telemetry import configure as configure_telemetry from haiku.rag.utils import get_model from haiku.skills import ( SkillDeps, @@ -30,14 +31,7 @@ from haiku.skills.prompts import build_system_prompt load_dotenv(find_dotenv(usecwd=True)) -# Configure logfire (only sends data if LOGFIRE_TOKEN is present) -try: - import logfire - - logfire.configure(send_to_logfire="if-token-present", console=False) - logfire.instrument_pydantic_ai() -except Exception: - pass +configure_telemetry(service_name="haiku-rag-app") logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" diff --git a/haiku_rag_slim/haiku/rag/chat/app.py b/haiku_rag_slim/haiku/rag/chat/app.py index 98015ed4..70439286 100644 --- a/haiku_rag_slim/haiku/rag/chat/app.py +++ b/haiku_rag_slim/haiku/rag/chat/app.py @@ -40,7 +40,7 @@ from haiku.skills.agent import ( from haiku.skills.models import Skill from haiku.skills.prompts import build_system_prompt -configure_telemetry() +configure_telemetry(service_name="haiku-rag") if TYPE_CHECKING: from textual.app import ComposeResult diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 7b201dd4..a14e5bfe 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -137,7 +137,9 @@ def main( from haiku.rag.telemetry import configure as configure_telemetry is_production = get_config().environment != "development" - configure_telemetry(console=False if is_production else None) + configure_telemetry( + service_name="haiku-rag", console=False if is_production else None + ) if get_config().environment != "development": # Suppress warnings in production diff --git a/haiku_rag_slim/haiku/rag/telemetry.py b/haiku_rag_slim/haiku/rag/telemetry.py index 0a34fcbb..7ad3024c 100644 --- a/haiku_rag_slim/haiku/rag/telemetry.py +++ b/haiku_rag_slim/haiku/rag/telemetry.py @@ -1,3 +1,5 @@ +import os +from importlib import metadata from typing import Literal from logfire import Logfire, attach_context, get_context @@ -25,8 +27,10 @@ def configure( Silently no-ops on failure so a missing/misconfigured LOGFIRE_TOKEN never crashes the app. - - service_name: identifies the process in the Logfire UI (e.g. - "haiku-ingester"). Falls back to logfire's default when None. + - service_name: the default name for this process in the Logfire UI + (e.g. "haiku-ingester"). The OTEL_SERVICE_NAME / LOGFIRE_SERVICE_NAME + env vars, when set, take precedence so operators can distinguish + concurrent processes. - console: False (default) suppresses span lines on stderr so they don't interleave with RichHandler logs. Pass None to let logfire decide (its own default applies). @@ -34,8 +38,21 @@ def configure( try: import logfire as _lf + # An explicit service_name arg would beat the env vars in logfire's + # precedence; deferring to None when an env var is set lets the + # operator's OTEL_SERVICE_NAME / LOGFIRE_SERVICE_NAME win over our + # per-process default. + env_service = os.environ.get("OTEL_SERVICE_NAME") or os.environ.get( + "LOGFIRE_SERVICE_NAME" + ) + try: + service_version = metadata.version("haiku.rag-slim") + except metadata.PackageNotFoundError: # pragma: no cover + service_version = None + _lf.configure( - service_name=service_name, + service_name=None if env_service else service_name, + service_version=service_version, send_to_logfire="if-token-present", console=console, ) diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py new file mode 100644 index 00000000..594210b7 --- /dev/null +++ b/tests/test_telemetry.py @@ -0,0 +1,57 @@ +from importlib import metadata + +import logfire +import pytest + +from haiku.rag import telemetry + + +@pytest.fixture +def captured_configure(monkeypatch): + """Capture the kwargs telemetry.configure() passes to logfire.configure, + and no-op the instrumentation so tests don't touch a real exporter.""" + captured: dict = {} + + def _fake_configure(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(logfire, "configure", _fake_configure) + monkeypatch.setattr(logfire, "instrument_pydantic_ai", lambda: None) + return captured + + +def test_default_service_name_used_when_env_unset(captured_configure, monkeypatch): + monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False) + monkeypatch.delenv("LOGFIRE_SERVICE_NAME", raising=False) + + telemetry.configure(service_name="haiku-ingester") + + assert captured_configure["service_name"] == "haiku-ingester" + + +def test_otel_service_name_overrides_default(captured_configure, monkeypatch): + monkeypatch.setenv("OTEL_SERVICE_NAME", "customer-ingester") + + telemetry.configure(service_name="haiku-ingester") + + # Deferring to logfire (service_name=None) lets it read the env var, + # so the customer's OTEL_SERVICE_NAME wins over our default. + assert captured_configure["service_name"] is None + + +def test_logfire_service_name_overrides_default(captured_configure, monkeypatch): + monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False) + monkeypatch.setenv("LOGFIRE_SERVICE_NAME", "customer-ingester") + + telemetry.configure(service_name="haiku-ingester") + + assert captured_configure["service_name"] is None + + +def test_service_version_is_package_version(captured_configure, monkeypatch): + monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False) + monkeypatch.delenv("LOGFIRE_SERVICE_NAME", raising=False) + + telemetry.configure(service_name="haiku-rag") + + assert captured_configure["service_version"] == metadata.version("haiku.rag-slim")