anytrace 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anytrace/__init__.py +50 -0
- anytrace/adapters/__init__.py +33 -0
- anytrace/adapters/base.py +36 -0
- anytrace/adapters/langfuse.py +80 -0
- anytrace/adapters/langsmith.py +88 -0
- anytrace/adapters/otlp.py +36 -0
- anytrace/adapters/phoenix.py +71 -0
- anytrace/adapters/processor.py +48 -0
- anytrace/config/__init__.py +10 -0
- anytrace/config/loader.py +282 -0
- anytrace/context/__init__.py +5 -0
- anytrace/context/middleware.py +79 -0
- anytrace/context/propagation.py +75 -0
- anytrace/core/__init__.py +44 -0
- anytrace/core/generation.py +72 -0
- anytrace/core/logs.py +41 -0
- anytrace/core/spans.py +295 -0
- anytrace/core/tracer.py +249 -0
- anytrace/integrations/__init__.py +8 -0
- anytrace/integrations/anthropic.py +74 -0
- anytrace/integrations/crewai.py +297 -0
- anytrace/integrations/langchain.py +257 -0
- anytrace/integrations/openai.py +110 -0
- anytrace/migrate/__init__.py +13 -0
- anytrace/migrate/checkpoint.py +35 -0
- anytrace/migrate/cli.py +159 -0
- anytrace/migrate/http.py +52 -0
- anytrace/migrate/model.py +34 -0
- anytrace/migrate/readers.py +273 -0
- anytrace/migrate/verify.py +125 -0
- anytrace/migrate/writer.py +116 -0
- anytrace/py.typed +0 -0
- anytrace/testing.py +47 -0
- anytrace-0.9.0.dist-info/METADATA +360 -0
- anytrace-0.9.0.dist-info/RECORD +38 -0
- anytrace-0.9.0.dist-info/WHEEL +4 -0
- anytrace-0.9.0.dist-info/entry_points.txt +2 -0
- anytrace-0.9.0.dist-info/licenses/LICENSE +202 -0
anytrace/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""anytrace — vendor-neutral agent tracing over OpenTelemetry.
|
|
2
|
+
|
|
3
|
+
Public API surface is only what is exported here. Keep it small.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from anytrace.context import extract_context, inject_context, use_context
|
|
7
|
+
from anytrace.core import (
|
|
8
|
+
add_event,
|
|
9
|
+
add_metadata,
|
|
10
|
+
add_tags,
|
|
11
|
+
init,
|
|
12
|
+
instrument_logging,
|
|
13
|
+
record_generation,
|
|
14
|
+
set_io,
|
|
15
|
+
set_session,
|
|
16
|
+
set_user,
|
|
17
|
+
shutdown,
|
|
18
|
+
stats,
|
|
19
|
+
trace_agent,
|
|
20
|
+
trace_chain,
|
|
21
|
+
trace_embedding,
|
|
22
|
+
trace_retriever,
|
|
23
|
+
trace_span,
|
|
24
|
+
trace_tool,
|
|
25
|
+
traced,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"add_event",
|
|
30
|
+
"add_metadata",
|
|
31
|
+
"add_tags",
|
|
32
|
+
"extract_context",
|
|
33
|
+
"init",
|
|
34
|
+
"inject_context",
|
|
35
|
+
"instrument_logging",
|
|
36
|
+
"record_generation",
|
|
37
|
+
"set_io",
|
|
38
|
+
"set_session",
|
|
39
|
+
"set_user",
|
|
40
|
+
"shutdown",
|
|
41
|
+
"stats",
|
|
42
|
+
"trace_agent",
|
|
43
|
+
"trace_chain",
|
|
44
|
+
"trace_embedding",
|
|
45
|
+
"trace_retriever",
|
|
46
|
+
"trace_span",
|
|
47
|
+
"trace_tool",
|
|
48
|
+
"traced",
|
|
49
|
+
"use_context",
|
|
50
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Backend adapters: per-backend OTLP endpoint, auth headers, and attribute mapping."""
|
|
2
|
+
|
|
3
|
+
from anytrace.adapters.base import BackendAdapter
|
|
4
|
+
from anytrace.config import ConfigError
|
|
5
|
+
|
|
6
|
+
__all__ = ["BackendAdapter", "get_adapter", "register_adapter"]
|
|
7
|
+
|
|
8
|
+
_REGISTRY: dict[str, BackendAdapter] = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_adapter(adapter: BackendAdapter) -> None:
|
|
12
|
+
_REGISTRY[adapter.name] = adapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_adapter(kind: str) -> BackendAdapter:
|
|
16
|
+
try:
|
|
17
|
+
return _REGISTRY[kind]
|
|
18
|
+
except KeyError:
|
|
19
|
+
raise ConfigError(
|
|
20
|
+
f"no adapter registered for backend kind {kind!r}; "
|
|
21
|
+
f"registered kinds: {sorted(_REGISTRY) or 'none'}"
|
|
22
|
+
) from None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from anytrace.adapters.langfuse import LangfuseAdapter # noqa: E402
|
|
26
|
+
from anytrace.adapters.langsmith import LangSmithAdapter # noqa: E402
|
|
27
|
+
from anytrace.adapters.otlp import OTLPAdapter # noqa: E402
|
|
28
|
+
from anytrace.adapters.phoenix import PhoenixAdapter # noqa: E402
|
|
29
|
+
|
|
30
|
+
register_adapter(LangfuseAdapter())
|
|
31
|
+
register_adapter(LangSmithAdapter())
|
|
32
|
+
register_adapter(PhoenixAdapter())
|
|
33
|
+
register_adapter(OTLPAdapter())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Adapter interface every backend must implement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from anytrace.config import BackendConfig as Config
|
|
9
|
+
|
|
10
|
+
Attributes = Mapping[str, Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class BackendAdapter(Protocol):
|
|
15
|
+
"""A tracing backend reachable via OTLP.
|
|
16
|
+
|
|
17
|
+
Adapters carry no vendor SDK dependencies: they only supply the OTLP
|
|
18
|
+
endpoint, auth headers, and a GenAI -> backend attribute mapping.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
"""Stable identifier used in configuration (e.g. "langsmith", "langfuse")."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def endpoint(self, cfg: Config) -> str:
|
|
27
|
+
"""OTLP HTTP endpoint URL for this backend."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def headers(self, cfg: Config) -> dict[str, str]:
|
|
31
|
+
"""Auth and transport headers for the OTLP exporter."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def map_attributes(self, attrs: Attributes) -> dict[str, Any]:
|
|
35
|
+
"""Translate GenAI semantic-convention attributes to backend-specific ones."""
|
|
36
|
+
...
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Langfuse adapter: OTLP endpoint, Basic auth, and attribute mapping.
|
|
2
|
+
|
|
3
|
+
Verified against https://langfuse.com/docs/opentelemetry/get-started (2026-07):
|
|
4
|
+
OTLP traces endpoint is ``{LANGFUSE_ENDPOINT}/api/public/otel/v1/traces``, auth is
|
|
5
|
+
HTTP Basic with ``public_key:secret_key``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from anytrace.adapters.base import Attributes
|
|
14
|
+
from anytrace.config import BackendConfig
|
|
15
|
+
|
|
16
|
+
_OTLP_PATH = "/api/public/otel/v1/traces"
|
|
17
|
+
|
|
18
|
+
# The FULL GenAI -> Langfuse mapping lives in this one table.
|
|
19
|
+
# Langfuse understands gen_ai.* natively, so originals are kept as-is;
|
|
20
|
+
# these entries ADD the Langfuse-native name alongside the original.
|
|
21
|
+
_ATTRIBUTE_MAP: dict[str, str] = {
|
|
22
|
+
"user.id": "langfuse.user.id",
|
|
23
|
+
"session.id": "langfuse.session.id",
|
|
24
|
+
"anytrace.release": "langfuse.release",
|
|
25
|
+
"anytrace.version": "langfuse.version",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# gen_ai.operation.name -> langfuse.observation.type. Falls back to the model
|
|
29
|
+
# marker. Langfuse supports tool/retriever/embedding/agent/... types since v3.
|
|
30
|
+
_OPERATION_TYPES: dict[str, str] = {
|
|
31
|
+
"chat": "generation",
|
|
32
|
+
"completion": "generation",
|
|
33
|
+
"execute_tool": "tool",
|
|
34
|
+
"retrieval": "retriever",
|
|
35
|
+
"embeddings": "embedding",
|
|
36
|
+
"invoke_agent": "agent",
|
|
37
|
+
"chain": "chain",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Spans carrying this attribute are LLM calls and get typed as generations.
|
|
41
|
+
_GENERATION_MARKER = "gen_ai.request.model"
|
|
42
|
+
|
|
43
|
+
# Custom attributes (add_metadata / trace_span kwargs) only become first-class
|
|
44
|
+
# Langfuse metadata when prefixed; mirror anything outside these namespaces
|
|
45
|
+
# into langfuse.observation.metadata.<key> (audit 2026-07-15).
|
|
46
|
+
_PASSTHROUGH_PREFIXES = ("gen_ai.", "langfuse.", "llm.", "http.", "url.", "anytrace.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LangfuseAdapter:
|
|
50
|
+
name = "langfuse"
|
|
51
|
+
|
|
52
|
+
def endpoint(self, cfg: BackendConfig) -> str:
|
|
53
|
+
base = cfg.endpoint.rstrip("/")
|
|
54
|
+
if base.endswith(_OTLP_PATH):
|
|
55
|
+
return base
|
|
56
|
+
return base + _OTLP_PATH
|
|
57
|
+
|
|
58
|
+
def headers(self, cfg: BackendConfig) -> dict[str, str]:
|
|
59
|
+
credentials = f"{cfg.auth['public_key']}:{cfg.auth['secret_key']}"
|
|
60
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
|
61
|
+
return {"Authorization": f"Basic {encoded}"}
|
|
62
|
+
|
|
63
|
+
def map_attributes(self, attrs: Attributes) -> dict[str, Any]:
|
|
64
|
+
mapped = dict(attrs)
|
|
65
|
+
for source, target in _ATTRIBUTE_MAP.items():
|
|
66
|
+
if source in mapped:
|
|
67
|
+
mapped[target] = mapped[source]
|
|
68
|
+
for key, value in attrs.items():
|
|
69
|
+
if key in _ATTRIBUTE_MAP or key.startswith(_PASSTHROUGH_PREFIXES):
|
|
70
|
+
continue
|
|
71
|
+
mapped[f"langfuse.observation.metadata.{key}"] = value
|
|
72
|
+
tags = mapped.get("anytrace.tags")
|
|
73
|
+
if tags:
|
|
74
|
+
mapped["langfuse.trace.tags"] = list(tags)
|
|
75
|
+
operation = mapped.get("gen_ai.operation.name")
|
|
76
|
+
if operation in _OPERATION_TYPES:
|
|
77
|
+
mapped["langfuse.observation.type"] = _OPERATION_TYPES[operation]
|
|
78
|
+
elif _GENERATION_MARKER in mapped:
|
|
79
|
+
mapped["langfuse.observation.type"] = "generation"
|
|
80
|
+
return mapped
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""LangSmith adapter: OTLP endpoint, x-api-key auth, and attribute mapping.
|
|
2
|
+
|
|
3
|
+
Verified against https://docs.langchain.com/langsmith/trace-with-opentelemetry (2026-07):
|
|
4
|
+
cloud OTLP endpoint is ``{base}/otel/v1/traces``; self-hosted appends ``/api/v1/otel``
|
|
5
|
+
to the base URL (so the full traces path is ``{base}/api/v1/otel/v1/traces``).
|
|
6
|
+
LangSmith natively understands ``gen_ai.*`` including ``gen_ai.usage.input_tokens`` /
|
|
7
|
+
``output_tokens``, so no OpenLLMetry renaming is needed — originals are preserved.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from anytrace.adapters.base import Attributes
|
|
15
|
+
from anytrace.config import BackendConfig
|
|
16
|
+
|
|
17
|
+
_CLOUD_HOSTS = ("api.smith.langchain.com",)
|
|
18
|
+
_TRACES_SUFFIX = "/v1/traces"
|
|
19
|
+
|
|
20
|
+
# The FULL GenAI -> LangSmith mapping lives in this one table.
|
|
21
|
+
# gen_ai.* passes through untouched; these entries ADD LangSmith-native names.
|
|
22
|
+
_ATTRIBUTE_MAP: dict[str, str] = {
|
|
23
|
+
"user.id": "langsmith.metadata.user_id",
|
|
24
|
+
"session.id": "langsmith.metadata.session_id",
|
|
25
|
+
# LangSmith has no cost attribute (it computes its own from model pricing);
|
|
26
|
+
# mirror the reported value into metadata so it isn't lost (audit 2026-07-15).
|
|
27
|
+
"gen_ai.usage.cost": "langsmith.metadata.cost",
|
|
28
|
+
"anytrace.release": "langsmith.metadata.release",
|
|
29
|
+
"anytrace.version": "langsmith.metadata.version",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# gen_ai.operation.name -> langsmith.span.kind. Falls back to the model marker.
|
|
33
|
+
_OPERATION_KINDS: dict[str, str] = {
|
|
34
|
+
"chat": "llm",
|
|
35
|
+
"completion": "llm",
|
|
36
|
+
"execute_tool": "tool",
|
|
37
|
+
"retrieval": "retriever",
|
|
38
|
+
"embeddings": "embedding",
|
|
39
|
+
"invoke_agent": "chain", # LangSmith has no agent run type
|
|
40
|
+
"chain": "chain",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Spans carrying this attribute are LLM calls and get langsmith.span.kind = "llm".
|
|
44
|
+
_LLM_MARKER = "gen_ai.request.model"
|
|
45
|
+
|
|
46
|
+
# Custom attributes (add_metadata / trace_span kwargs) are invisible to LangSmith
|
|
47
|
+
# unless prefixed; anything outside these namespaces gets mirrored into
|
|
48
|
+
# langsmith.metadata.<key> (audit 2026-07-15: status/eta_days/order_id were dropped).
|
|
49
|
+
_PASSTHROUGH_PREFIXES = ("gen_ai.", "langsmith.", "llm.", "http.", "url.", "anytrace.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LangSmithAdapter:
|
|
53
|
+
name = "langsmith"
|
|
54
|
+
|
|
55
|
+
def endpoint(self, cfg: BackendConfig) -> str:
|
|
56
|
+
base = cfg.endpoint.rstrip("/")
|
|
57
|
+
if "/otel" in base:
|
|
58
|
+
# Caller already supplied the OTLP base; just ensure the traces path.
|
|
59
|
+
return base if base.endswith(_TRACES_SUFFIX) else base + _TRACES_SUFFIX
|
|
60
|
+
if any(host in base for host in _CLOUD_HOSTS):
|
|
61
|
+
return base + "/otel" + _TRACES_SUFFIX
|
|
62
|
+
return base + "/api/v1/otel" + _TRACES_SUFFIX
|
|
63
|
+
|
|
64
|
+
def headers(self, cfg: BackendConfig) -> dict[str, str]:
|
|
65
|
+
headers = {"x-api-key": cfg.auth["api_key"]}
|
|
66
|
+
project = cfg.options.get("project")
|
|
67
|
+
if project:
|
|
68
|
+
headers["Langsmith-Project"] = project
|
|
69
|
+
return headers
|
|
70
|
+
|
|
71
|
+
def map_attributes(self, attrs: Attributes) -> dict[str, Any]:
|
|
72
|
+
mapped = dict(attrs)
|
|
73
|
+
for source, target in _ATTRIBUTE_MAP.items():
|
|
74
|
+
if source in mapped:
|
|
75
|
+
mapped[target] = mapped[source]
|
|
76
|
+
for key, value in attrs.items():
|
|
77
|
+
if key in _ATTRIBUTE_MAP or key.startswith(_PASSTHROUGH_PREFIXES):
|
|
78
|
+
continue
|
|
79
|
+
mapped[f"langsmith.metadata.{key}"] = value
|
|
80
|
+
tags = mapped.get("anytrace.tags")
|
|
81
|
+
if tags:
|
|
82
|
+
mapped["langsmith.span.tags"] = ",".join(str(t) for t in tags)
|
|
83
|
+
operation = mapped.get("gen_ai.operation.name")
|
|
84
|
+
if operation in _OPERATION_KINDS:
|
|
85
|
+
mapped["langsmith.span.kind"] = _OPERATION_KINDS[operation]
|
|
86
|
+
elif _LLM_MARKER in mapped:
|
|
87
|
+
mapped["langsmith.span.kind"] = "llm"
|
|
88
|
+
return mapped
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Generic OTLP adapter: any collector or OTLP-native receiver, no dialect.
|
|
2
|
+
|
|
3
|
+
Attributes pass through unchanged (neutral ``gen_ai.*`` / ``anytrace.*``) —
|
|
4
|
+
when routing through an OTel Collector to a vendor, do vendor mapping in the
|
|
5
|
+
collector, or configure that vendor's anytrace adapter directly instead.
|
|
6
|
+
Optional headers (e.g. collector auth) come from ``OTLP_HEADERS="k=v,k2=v2"``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from anytrace.adapters.base import Attributes
|
|
14
|
+
from anytrace.config import BackendConfig
|
|
15
|
+
|
|
16
|
+
_OTLP_PATH = "/v1/traces"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OTLPAdapter:
|
|
20
|
+
name = "otlp"
|
|
21
|
+
|
|
22
|
+
def endpoint(self, cfg: BackendConfig) -> str:
|
|
23
|
+
base = cfg.endpoint.rstrip("/")
|
|
24
|
+
return base if base.endswith(_OTLP_PATH) else base + _OTLP_PATH
|
|
25
|
+
|
|
26
|
+
def headers(self, cfg: BackendConfig) -> dict[str, str]:
|
|
27
|
+
raw = cfg.options.get("headers", "")
|
|
28
|
+
headers: dict[str, str] = {}
|
|
29
|
+
for pair in raw.split(","):
|
|
30
|
+
if "=" in pair:
|
|
31
|
+
key, _, value = pair.partition("=")
|
|
32
|
+
headers[key.strip()] = value.strip()
|
|
33
|
+
return headers
|
|
34
|
+
|
|
35
|
+
def map_attributes(self, attrs: Attributes) -> dict[str, Any]:
|
|
36
|
+
return dict(attrs)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Arize Phoenix adapter: OTLP endpoint and OpenInference attribute mapping.
|
|
2
|
+
|
|
3
|
+
Phoenix (OSS) ingests OTLP at ``{PHOENIX_ENDPOINT}/v1/traces`` — no auth for
|
|
4
|
+
self-hosted; Phoenix Cloud uses an ``api_key`` header (PHOENIX_API_KEY).
|
|
5
|
+
Phoenix's native dialect is OpenInference, so the mapping mirrors GenAI
|
|
6
|
+
attributes into OpenInference names; originals are preserved.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from anytrace.adapters.base import Attributes
|
|
14
|
+
from anytrace.config import BackendConfig
|
|
15
|
+
|
|
16
|
+
_OTLP_PATH = "/v1/traces"
|
|
17
|
+
|
|
18
|
+
# The FULL GenAI -> OpenInference mapping lives in these two tables.
|
|
19
|
+
_ATTRIBUTE_MAP: dict[str, str] = {
|
|
20
|
+
"gen_ai.request.model": "llm.model_name",
|
|
21
|
+
"gen_ai.usage.input_tokens": "llm.token_count.prompt",
|
|
22
|
+
"gen_ai.usage.output_tokens": "llm.token_count.completion",
|
|
23
|
+
"gen_ai.prompt": "input.value",
|
|
24
|
+
"gen_ai.completion": "output.value",
|
|
25
|
+
# user.id / session.id are OpenInference-native already — passed through.
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# gen_ai.operation.name -> openinference.span.kind (values are UPPERCASE).
|
|
29
|
+
_OPERATION_KINDS: dict[str, str] = {
|
|
30
|
+
"chat": "LLM",
|
|
31
|
+
"completion": "LLM",
|
|
32
|
+
"execute_tool": "TOOL",
|
|
33
|
+
"retrieval": "RETRIEVER",
|
|
34
|
+
"embeddings": "EMBEDDING",
|
|
35
|
+
"invoke_agent": "AGENT",
|
|
36
|
+
"chain": "CHAIN",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_LLM_MARKER = "gen_ai.request.model"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PhoenixAdapter:
|
|
43
|
+
name = "phoenix"
|
|
44
|
+
|
|
45
|
+
def endpoint(self, cfg: BackendConfig) -> str:
|
|
46
|
+
base = cfg.endpoint.rstrip("/")
|
|
47
|
+
return base if base.endswith(_OTLP_PATH) else base + _OTLP_PATH
|
|
48
|
+
|
|
49
|
+
def headers(self, cfg: BackendConfig) -> dict[str, str]:
|
|
50
|
+
api_key = cfg.auth.get("api_key")
|
|
51
|
+
if not api_key:
|
|
52
|
+
return {}
|
|
53
|
+
# Phoenix Cloud (app.phoenix.arize.com) authenticates via Bearer
|
|
54
|
+
# (live-verified 2026-07-16: api_key header alone gets 401); the
|
|
55
|
+
# legacy api_key header is included for older self-hosted deployments.
|
|
56
|
+
return {"Authorization": f"Bearer {api_key}", "api_key": api_key}
|
|
57
|
+
|
|
58
|
+
def map_attributes(self, attrs: Attributes) -> dict[str, Any]:
|
|
59
|
+
mapped = dict(attrs)
|
|
60
|
+
for source, target in _ATTRIBUTE_MAP.items():
|
|
61
|
+
if source in mapped:
|
|
62
|
+
mapped[target] = mapped[source]
|
|
63
|
+
tags = mapped.get("anytrace.tags")
|
|
64
|
+
if tags:
|
|
65
|
+
mapped["tag.tags"] = list(tags)
|
|
66
|
+
operation = mapped.get("gen_ai.operation.name")
|
|
67
|
+
if operation in _OPERATION_KINDS:
|
|
68
|
+
mapped["openinference.span.kind"] = _OPERATION_KINDS[operation]
|
|
69
|
+
elif _LLM_MARKER in mapped:
|
|
70
|
+
mapped["openinference.span.kind"] = "LLM"
|
|
71
|
+
return mapped
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Exporter wrapper that applies an adapter's attribute mapping just before export.
|
|
2
|
+
|
|
3
|
+
One recorded span fans out to N backends; each backend's exporter sees the span
|
|
4
|
+
rewritten in its own attribute dialect, while the original span is untouched.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
12
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
13
|
+
|
|
14
|
+
from anytrace.adapters.base import BackendAdapter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MappingSpanExporter(SpanExporter):
|
|
18
|
+
"""Delegates to ``wrapped`` after rewriting attributes via ``adapter.map_attributes``."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, adapter: BackendAdapter, wrapped: SpanExporter) -> None:
|
|
21
|
+
self._adapter = adapter
|
|
22
|
+
self._wrapped = wrapped
|
|
23
|
+
|
|
24
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
25
|
+
return self._wrapped.export([self._map(span) for span in spans])
|
|
26
|
+
|
|
27
|
+
def _map(self, span: ReadableSpan) -> ReadableSpan:
|
|
28
|
+
mapped = self._adapter.map_attributes(dict(span.attributes or {}))
|
|
29
|
+
return ReadableSpan(
|
|
30
|
+
name=span.name,
|
|
31
|
+
context=span.get_span_context(),
|
|
32
|
+
parent=span.parent,
|
|
33
|
+
resource=span.resource,
|
|
34
|
+
attributes=mapped,
|
|
35
|
+
events=span.events,
|
|
36
|
+
links=span.links,
|
|
37
|
+
kind=span.kind,
|
|
38
|
+
status=span.status,
|
|
39
|
+
start_time=span.start_time,
|
|
40
|
+
end_time=span.end_time,
|
|
41
|
+
instrumentation_scope=span.instrumentation_scope,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def shutdown(self) -> None:
|
|
45
|
+
self._wrapped.shutdown()
|
|
46
|
+
|
|
47
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
48
|
+
return self._wrapped.force_flush(timeout_millis)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Configuration loading: env vars first, optional YAML override, with validation."""
|
|
2
|
+
|
|
3
|
+
from anytrace.config.loader import (
|
|
4
|
+
BackendConfig,
|
|
5
|
+
ConfigError,
|
|
6
|
+
TracingConfig,
|
|
7
|
+
load_config,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = ["BackendConfig", "ConfigError", "TracingConfig", "load_config"]
|