Honor OTEL_SERVICE_NAME and set service name/version for all processes

This commit is contained in:
Yiorgis Gozadinos 2026-07-10 11:11:38 +03:00
parent da0748916b
commit da79f92af1
No known key found for this signature in database
6 changed files with 87 additions and 13 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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,
)

57
tests/test_telemetry.py Normal file
View file

@ -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")