catalyst-tracing 0.0.1__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.
- catalyst_tracing/__init__.py +94 -0
- catalyst_tracing/agent_span.py +199 -0
- catalyst_tracing/anthropic.py +17 -0
- catalyst_tracing/claude_agent_sdk.py +21 -0
- catalyst_tracing/constants.py +122 -0
- catalyst_tracing/env.py +67 -0
- catalyst_tracing/errors.py +134 -0
- catalyst_tracing/exporter.py +60 -0
- catalyst_tracing/installers/__init__.py +0 -0
- catalyst_tracing/installers/anthropic.py +49 -0
- catalyst_tracing/installers/base.py +48 -0
- catalyst_tracing/installers/claude_agent_sdk.py +51 -0
- catalyst_tracing/installers/langchain.py +49 -0
- catalyst_tracing/installers/langgraph.py +42 -0
- catalyst_tracing/installers/langsmith.py +125 -0
- catalyst_tracing/installers/openai.py +60 -0
- catalyst_tracing/installers/openai_agents.py +62 -0
- catalyst_tracing/installers/pydantic_ai.py +43 -0
- catalyst_tracing/instrumentation/__init__.py +0 -0
- catalyst_tracing/instrumentation/anthropic.py +400 -0
- catalyst_tracing/instrumentation/claude_agent_sdk.py +274 -0
- catalyst_tracing/instrumentation/langchain.py +474 -0
- catalyst_tracing/instrumentation/openai.py +573 -0
- catalyst_tracing/instrumentation/stream_accumulator.py +387 -0
- catalyst_tracing/langchain.py +17 -0
- catalyst_tracing/langgraph.py +7 -0
- catalyst_tracing/langsmith.py +7 -0
- catalyst_tracing/openai.py +21 -0
- catalyst_tracing/openai_agents.py +21 -0
- catalyst_tracing/pydantic_ai.py +21 -0
- catalyst_tracing/semconv.py +181 -0
- catalyst_tracing/setup.py +288 -0
- catalyst_tracing-0.0.1.dist-info/METADATA +26 -0
- catalyst_tracing-0.0.1.dist-info/RECORD +35 -0
- catalyst_tracing-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""``catalyst_tracing`` — first-party tracing for LLM apps.
|
|
2
|
+
|
|
3
|
+
Drop-in OpenInference-shaped instrumentation for the SDKs your app
|
|
4
|
+
uses (openai, anthropic, langchain, langgraph, langsmith,
|
|
5
|
+
openai-agents, claude-agent-sdk, pydantic-ai), plus a small ergonomic helper layer for cases where
|
|
6
|
+
you need to author spans by hand.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
from catalyst_tracing import setup
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
|
|
13
|
+
tracing = setup()
|
|
14
|
+
client = OpenAI(api_key=...)
|
|
15
|
+
client.chat.completions.create(model=..., messages=...)
|
|
16
|
+
tracing.shutdown()
|
|
17
|
+
|
|
18
|
+
What this package owns
|
|
19
|
+
----------------------
|
|
20
|
+
|
|
21
|
+
- **OTLP plumbing** — endpoint, token, service-name resource, batch
|
|
22
|
+
vs. simple processor selection, env-var resolution with sensible
|
|
23
|
+
defaults including legacy ``OTLP_*`` fallback.
|
|
24
|
+
- **First-party OpenInference instrumentation** for openai, anthropic,
|
|
25
|
+
langchain, langgraph, openai-agents, claude-agent-sdk, plus bridges
|
|
26
|
+
for langsmith and pydantic-ai's native OTel. No
|
|
27
|
+
``openinference-instrumentation-*`` runtime dependency.
|
|
28
|
+
- **Helpers**: :func:`agent_span` for manual AGENT spans;
|
|
29
|
+
:class:`Attr` and :class:`SpanKindValues` for the OI semantic-
|
|
30
|
+
convention constants you'd want when authoring custom spans.
|
|
31
|
+
|
|
32
|
+
What this package doesn't own
|
|
33
|
+
-----------------------------
|
|
34
|
+
|
|
35
|
+
- The SDKs themselves. They're optional extras — pull in only the
|
|
36
|
+
ones you use::
|
|
37
|
+
|
|
38
|
+
pip install 'catalyst-tracing[openai,anthropic]'
|
|
39
|
+
|
|
40
|
+
- Shipping spans somewhere other than OTLP/HTTP/JSON. If you need a
|
|
41
|
+
different transport, build your own exporter and add it to
|
|
42
|
+
``tracing.provider``.
|
|
43
|
+
|
|
44
|
+
Public API
|
|
45
|
+
----------
|
|
46
|
+
|
|
47
|
+
* :func:`setup` — bootstrap tracing, returns a
|
|
48
|
+
:class:`CatalystTracing` handle.
|
|
49
|
+
* :func:`agent_span` — context manager that wraps work in an
|
|
50
|
+
OI-shaped AGENT span.
|
|
51
|
+
* :class:`Attr`, :class:`SpanKindValues` — wire-format constants
|
|
52
|
+
shared across instrumentation and consumer-authored spans.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from .agent_span import AgentSpanHandle, agent_span
|
|
56
|
+
from .errors import (
|
|
57
|
+
CatalystTracingError,
|
|
58
|
+
CatalystTracingErrorCode,
|
|
59
|
+
CatalystTracingErrorCodes,
|
|
60
|
+
InvalidSdkInputError,
|
|
61
|
+
InvalidTracerProviderError,
|
|
62
|
+
)
|
|
63
|
+
from .semconv import Attr, MessageRole, OpenInferenceAttribute, SpanKindValues
|
|
64
|
+
from .setup import CatalystTracing, InstrumentResult, setup
|
|
65
|
+
|
|
66
|
+
__version__ = "0.0.1"
|
|
67
|
+
PACKAGE_NAME = "catalyst-tracing"
|
|
68
|
+
IMPORT_NAME = "catalyst_tracing"
|
|
69
|
+
COMPANY_NAME = "Inference"
|
|
70
|
+
PLATFORM_NAME = "Catalyst"
|
|
71
|
+
PRODUCT_NAME = "Catalyst by Inference.net"
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"AgentSpanHandle",
|
|
75
|
+
"Attr",
|
|
76
|
+
"CatalystTracing",
|
|
77
|
+
"CatalystTracingError",
|
|
78
|
+
"CatalystTracingErrorCode",
|
|
79
|
+
"CatalystTracingErrorCodes",
|
|
80
|
+
"COMPANY_NAME",
|
|
81
|
+
"IMPORT_NAME",
|
|
82
|
+
"InstrumentResult",
|
|
83
|
+
"InvalidSdkInputError",
|
|
84
|
+
"InvalidTracerProviderError",
|
|
85
|
+
"MessageRole",
|
|
86
|
+
"OpenInferenceAttribute",
|
|
87
|
+
"PACKAGE_NAME",
|
|
88
|
+
"PLATFORM_NAME",
|
|
89
|
+
"PRODUCT_NAME",
|
|
90
|
+
"SpanKindValues",
|
|
91
|
+
"__version__",
|
|
92
|
+
"agent_span",
|
|
93
|
+
"setup",
|
|
94
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
""":func:`agent_span` — context manager that wraps work in an
|
|
2
|
+
OpenInference-flavored AGENT span.
|
|
3
|
+
|
|
4
|
+
Use this when you have a chunk of "agent work" — a CLI subprocess
|
|
5
|
+
call, a multi-step Pydantic AI run, the outer wrapper around an
|
|
6
|
+
``agents-sdk`` ``Runner.run()`` — and you want it to show up in
|
|
7
|
+
the trace viewer as a single AGENT-kind row with input/output and
|
|
8
|
+
optional token counts.
|
|
9
|
+
|
|
10
|
+
The context manager:
|
|
11
|
+
|
|
12
|
+
* Sets ``openinference.span.kind=AGENT``, ``agent.name``,
|
|
13
|
+
``gen_ai.system`` automatically.
|
|
14
|
+
* Runs the body inside the span's active OTel context, so child
|
|
15
|
+
spans created by instrumented LLM SDKs auto-parent to this
|
|
16
|
+
AGENT span. (That's how a full trace tree forms.)
|
|
17
|
+
* Ends with ``OK`` on success, records the exception and ends with
|
|
18
|
+
``ERROR`` on failure (re-raising so the caller's error handling
|
|
19
|
+
is unaffected).
|
|
20
|
+
|
|
21
|
+
Example
|
|
22
|
+
-------
|
|
23
|
+
Wrapping a CLI subprocess::
|
|
24
|
+
|
|
25
|
+
from catalyst_tracing import agent_span, setup
|
|
26
|
+
|
|
27
|
+
tracing = setup()
|
|
28
|
+
with agent_span(tracing.tracer, name="ClaudeCode", system="anthropic") as span:
|
|
29
|
+
span.set_input(prompt)
|
|
30
|
+
answer = run_cli(prompt)
|
|
31
|
+
span.set_output(answer)
|
|
32
|
+
span.record_tokens(prompt=120, completion=40)
|
|
33
|
+
|
|
34
|
+
Wrapping an `openai-agents` runner so its LLM-call spans nest
|
|
35
|
+
under the AGENT span::
|
|
36
|
+
|
|
37
|
+
from catalyst_tracing import agent_span, setup
|
|
38
|
+
from agents import Agent, Runner
|
|
39
|
+
|
|
40
|
+
tracing = setup()
|
|
41
|
+
agent = Agent(name="SupportAgent", instructions=..., tools=[...])
|
|
42
|
+
|
|
43
|
+
with agent_span(tracing.tracer, name="SupportAgent", system="openai") as span:
|
|
44
|
+
span.set_input(user_message)
|
|
45
|
+
result = await Runner.run(agent, input=user_message)
|
|
46
|
+
span.set_output(str(result.final_output))
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
import json
|
|
52
|
+
from collections.abc import Iterator
|
|
53
|
+
from contextlib import contextmanager
|
|
54
|
+
from dataclasses import dataclass
|
|
55
|
+
from typing import Any
|
|
56
|
+
|
|
57
|
+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode, Tracer
|
|
58
|
+
|
|
59
|
+
from .semconv import Attr, SpanKindValues
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class AgentSpanHandle:
|
|
64
|
+
"""The handle yielded by :func:`agent_span`.
|
|
65
|
+
|
|
66
|
+
Provides type-safe helpers for the OI attribute set the agent
|
|
67
|
+
span should carry, plus a ``span`` escape hatch for callers
|
|
68
|
+
that need the underlying OTel span (e.g. to add custom
|
|
69
|
+
attributes outside the OI vocabulary).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
#: The underlying OTel span — escape hatch for advanced use.
|
|
73
|
+
span: Span
|
|
74
|
+
|
|
75
|
+
def set_input(self, value: Any) -> None:
|
|
76
|
+
"""Record the agent's input.
|
|
77
|
+
|
|
78
|
+
Strings are stored as-is on ``input.value``. Non-strings
|
|
79
|
+
are JSON-stringified.
|
|
80
|
+
"""
|
|
81
|
+
self.span.set_attribute(
|
|
82
|
+
Attr.INPUT_VALUE,
|
|
83
|
+
value if isinstance(value, str) else json.dumps(value),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def set_output(self, value: Any) -> None:
|
|
87
|
+
"""Record the agent's final output.
|
|
88
|
+
|
|
89
|
+
Same string-vs-JSON rule as :meth:`set_input`.
|
|
90
|
+
"""
|
|
91
|
+
self.span.set_attribute(
|
|
92
|
+
Attr.OUTPUT_VALUE,
|
|
93
|
+
value if isinstance(value, str) else json.dumps(value),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def record_tokens(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
prompt: int | None = None,
|
|
100
|
+
completion: int | None = None,
|
|
101
|
+
total: int | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Record token usage on the agent span.
|
|
104
|
+
|
|
105
|
+
Any of ``prompt``, ``completion``, ``total`` may be omitted;
|
|
106
|
+
only the provided fields are written. Use this when you have
|
|
107
|
+
aggregated counts across multiple LLM calls inside the agent
|
|
108
|
+
loop — per-call counts get captured by the LLM-level
|
|
109
|
+
instrumentation separately.
|
|
110
|
+
"""
|
|
111
|
+
if prompt is not None:
|
|
112
|
+
self.span.set_attribute(Attr.TOKEN_COUNT_PROMPT, prompt)
|
|
113
|
+
if completion is not None:
|
|
114
|
+
self.span.set_attribute(Attr.TOKEN_COUNT_COMPLETION, completion)
|
|
115
|
+
if total is not None:
|
|
116
|
+
self.span.set_attribute(Attr.TOKEN_COUNT_TOTAL, total)
|
|
117
|
+
|
|
118
|
+
def set_model(self, model: str) -> None:
|
|
119
|
+
"""Record the model name (becomes ``llm.model_name``)."""
|
|
120
|
+
self.span.set_attribute(Attr.MODEL_NAME, model)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def agent_span(
|
|
125
|
+
tracer: Tracer,
|
|
126
|
+
*,
|
|
127
|
+
name: str,
|
|
128
|
+
system: str | None = None,
|
|
129
|
+
span_kind: SpanKindValues | str = SpanKindValues.AGENT,
|
|
130
|
+
otel_kind: SpanKind = SpanKind.INTERNAL,
|
|
131
|
+
span_name: str | None = None,
|
|
132
|
+
) -> Iterator[AgentSpanHandle]:
|
|
133
|
+
"""Wrap a chunk of agent work in an OI-shaped AGENT span.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
tracer : Tracer
|
|
138
|
+
OpenTelemetry tracer to author the span from. Use the one
|
|
139
|
+
on :class:`CatalystTracing.tracer`.
|
|
140
|
+
name : str
|
|
141
|
+
Logical agent name — e.g. ``"ClaudeCode"``,
|
|
142
|
+
``"WeatherAgent"``. Becomes the ``agent.name`` attribute
|
|
143
|
+
and the prefix of the default span name.
|
|
144
|
+
system : str, optional
|
|
145
|
+
Identifier of the LLM provider this agent runs on, e.g.
|
|
146
|
+
``"openai"`` or ``"anthropic"``. Becomes the
|
|
147
|
+
``gen_ai.system`` attribute. Optional — omit if the agent
|
|
148
|
+
is provider-agnostic.
|
|
149
|
+
span_kind : SpanKindValues or str, default AGENT
|
|
150
|
+
OpenInference span kind. Pass another
|
|
151
|
+
:class:`SpanKindValues` (e.g. ``CHAIN``) when this helper
|
|
152
|
+
is used outside a strictly agent context.
|
|
153
|
+
otel_kind : SpanKind, default INTERNAL
|
|
154
|
+
OTel-level span kind. ``INTERNAL`` because the work itself
|
|
155
|
+
is internal to this process; the LLM-call children get
|
|
156
|
+
their own ``CLIENT`` kind from the per-SDK patchers.
|
|
157
|
+
span_name : str, optional
|
|
158
|
+
Override the auto-generated span name (``f"{name}.run"``).
|
|
159
|
+
Useful when "run" doesn't fit — e.g. our examples use
|
|
160
|
+
``"claude-code.invocation"`` for one-shot CLI calls.
|
|
161
|
+
|
|
162
|
+
Yields
|
|
163
|
+
------
|
|
164
|
+
AgentSpanHandle
|
|
165
|
+
Helpers to set input/output/tokens/model on the span.
|
|
166
|
+
|
|
167
|
+
Notes
|
|
168
|
+
-----
|
|
169
|
+
The span is closed automatically when the ``with`` block
|
|
170
|
+
exits. On unhandled exceptions the span ends with ``ERROR``
|
|
171
|
+
status and an ``exception`` event recording the traceback,
|
|
172
|
+
then the exception re-raises so caller error handling is
|
|
173
|
+
unaffected.
|
|
174
|
+
"""
|
|
175
|
+
resolved_span_name = span_name or f"{name}.run"
|
|
176
|
+
resolved_kind_value = (
|
|
177
|
+
span_kind.value if isinstance(span_kind, SpanKindValues) else span_kind
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
attributes: dict[str, str] = {
|
|
181
|
+
Attr.SPAN_KIND: resolved_kind_value,
|
|
182
|
+
Attr.AGENT_NAME: name,
|
|
183
|
+
}
|
|
184
|
+
if system is not None:
|
|
185
|
+
attributes[Attr.SYSTEM] = system
|
|
186
|
+
|
|
187
|
+
with tracer.start_as_current_span(
|
|
188
|
+
resolved_span_name,
|
|
189
|
+
kind=otel_kind,
|
|
190
|
+
attributes=attributes,
|
|
191
|
+
) as span:
|
|
192
|
+
handle = AgentSpanHandle(span=span)
|
|
193
|
+
try:
|
|
194
|
+
yield handle
|
|
195
|
+
span.set_status(Status(StatusCode.OK))
|
|
196
|
+
except Exception as err:
|
|
197
|
+
span.record_exception(err)
|
|
198
|
+
span.set_status(Status(StatusCode.ERROR, str(err)))
|
|
199
|
+
raise
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""``catalyst_tracing.anthropic`` — granular entry-point import for the
|
|
2
|
+
Anthropic integration.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
from catalyst_tracing import setup
|
|
7
|
+
from catalyst_tracing.anthropic import install_anthropic
|
|
8
|
+
|
|
9
|
+
tracing = setup()
|
|
10
|
+
install_anthropic(tracing.provider)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .installers.anthropic import install_anthropic
|
|
16
|
+
|
|
17
|
+
__all__ = ["install_anthropic"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""``catalyst_tracing.claude_agent_sdk`` — granular entry-point import
|
|
2
|
+
for the Claude Agent SDK integration.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
from catalyst_tracing import setup
|
|
7
|
+
from catalyst_tracing.claude_agent_sdk import install_claude_agent_sdk
|
|
8
|
+
|
|
9
|
+
tracing = setup()
|
|
10
|
+
install_claude_agent_sdk(tracing.provider)
|
|
11
|
+
|
|
12
|
+
# `from claude_agent_sdk import query` continues to read the
|
|
13
|
+
# wrapped function. The patch covers both the package-level and
|
|
14
|
+
# submodule bindings.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .installers.claude_agent_sdk import install_claude_agent_sdk
|
|
20
|
+
|
|
21
|
+
__all__ = ["install_claude_agent_sdk"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Package-level constants shared across setup, installers, and patchers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
PACKAGE_VERSION = "0.0.1"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DefaultConfig:
|
|
9
|
+
OTLP_ENDPOINT = "http://localhost:8799"
|
|
10
|
+
SERVICE_NAME_PREFIX = "catalyst-app"
|
|
11
|
+
SERVICE_VERSION = PACKAGE_VERSION
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnvVar:
|
|
15
|
+
CATALYST_OTLP_ENDPOINT = "CATALYST_OTLP_ENDPOINT"
|
|
16
|
+
LEGACY_OTLP_ENDPOINT = "OTLP_ENDPOINT"
|
|
17
|
+
CATALYST_OTLP_TOKEN = "CATALYST_OTLP_TOKEN"
|
|
18
|
+
LEGACY_OTLP_INGEST_TOKEN = "OTLP_INGEST_TOKEN"
|
|
19
|
+
CATALYST_SERVICE_NAME = "CATALYST_SERVICE_NAME"
|
|
20
|
+
LEGACY_SERVICE_NAME = "SERVICE_NAME"
|
|
21
|
+
CATALYST_SERVICE_VERSION = "CATALYST_SERVICE_VERSION"
|
|
22
|
+
CATALYST_DEBUG = "CATALYST_DEBUG"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MimeType:
|
|
26
|
+
JSON = "application/json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HttpHeader:
|
|
30
|
+
AUTHORIZATION = "Authorization"
|
|
31
|
+
CONTENT_TYPE = "Content-Type"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OtlpPath:
|
|
35
|
+
TRACES = "/v1/traces"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BatchingMode:
|
|
39
|
+
BATCH = "batch"
|
|
40
|
+
SIMPLE = "simple"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SpanProcessorDefaults:
|
|
44
|
+
MAX_EXPORT_BATCH_SIZE = 64
|
|
45
|
+
SCHEDULE_DELAY_MILLIS = 1000
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ResourceAttribute:
|
|
49
|
+
SERVICE_NAME = "service.name"
|
|
50
|
+
SERVICE_VERSION = "service.version"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OpenTelemetryRuntime:
|
|
54
|
+
PROXY_TRACER_PROVIDER = "ProxyTracerProvider"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class IntegrationId:
|
|
58
|
+
OPENAI = "openai"
|
|
59
|
+
ANTHROPIC = "anthropic"
|
|
60
|
+
LANGCHAIN = "langchain"
|
|
61
|
+
LANGGRAPH = "langgraph"
|
|
62
|
+
LANGSMITH = "langsmith"
|
|
63
|
+
OPENAI_AGENTS = "openai-agents"
|
|
64
|
+
CLAUDE_AGENT_SDK = "claude-agent-sdk"
|
|
65
|
+
PYDANTIC_AI = "pydantic-ai"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InstallerId:
|
|
69
|
+
"""Function-name-compatible integration identifiers for errors."""
|
|
70
|
+
|
|
71
|
+
OPENAI = "openai"
|
|
72
|
+
ANTHROPIC = "anthropic"
|
|
73
|
+
LANGCHAIN = "langchain"
|
|
74
|
+
LANGGRAPH = "langgraph"
|
|
75
|
+
LANGSMITH = "langsmith"
|
|
76
|
+
OPENAI_AGENTS = "openai_agents"
|
|
77
|
+
CLAUDE_AGENT_SDK = "claude_agent_sdk"
|
|
78
|
+
PYDANTIC_AI = "pydantic_ai"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ImportName:
|
|
82
|
+
OPENAI = "openai"
|
|
83
|
+
ANTHROPIC = "anthropic"
|
|
84
|
+
LANGCHAIN_CORE = "langchain_core"
|
|
85
|
+
LANGGRAPH = "langgraph"
|
|
86
|
+
LANGSMITH = "langsmith"
|
|
87
|
+
LANGSMITH_CLIENT = "langsmith.client"
|
|
88
|
+
OPENAI_AGENTS = "agents"
|
|
89
|
+
CLAUDE_AGENT_SDK = "claude_agent_sdk"
|
|
90
|
+
CLAUDE_AGENT_SDK_QUERY = "claude_agent_sdk.query"
|
|
91
|
+
PYDANTIC_AI = "pydantic_ai"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class GenAISystem:
|
|
95
|
+
OPENAI = "openai"
|
|
96
|
+
ANTHROPIC = "anthropic"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TracerName:
|
|
100
|
+
ROOT = "catalyst_tracing"
|
|
101
|
+
OPENAI = "catalyst_tracing.openai"
|
|
102
|
+
ANTHROPIC = "catalyst_tracing.anthropic"
|
|
103
|
+
LANGCHAIN = "catalyst_tracing.langchain"
|
|
104
|
+
CLAUDE_AGENT_SDK = "catalyst_tracing.claude_agent_sdk"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SpanName:
|
|
108
|
+
OPENAI_CHAT_COMPLETIONS = "OpenAI Chat Completions"
|
|
109
|
+
OPENAI_RESPONSES = "OpenAI Responses"
|
|
110
|
+
ANTHROPIC_MESSAGES = "Anthropic Messages"
|
|
111
|
+
CLAUDE_AGENT_QUERY = "ClaudeAgentSDK.query"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AgentName:
|
|
115
|
+
CLAUDE_AGENT_SDK = "ClaudeAgentSDK"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PatchedMethod:
|
|
119
|
+
CREATE = "create"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
PATCH_MARKER_ATTR = "_catalyst_wrapped"
|
catalyst_tracing/env.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Resolve catalyst-tracing config from kwargs + env vars."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from .constants import DefaultConfig, EnvVar
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ResolvedConfig:
|
|
14
|
+
endpoint: str
|
|
15
|
+
token: str | None
|
|
16
|
+
service_name: str
|
|
17
|
+
service_version: str
|
|
18
|
+
debug: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _truthy(value: str | None) -> bool:
|
|
22
|
+
if value is None or value == "":
|
|
23
|
+
return False
|
|
24
|
+
return value.lower() not in {"0", "false", "no"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_config(
|
|
28
|
+
*,
|
|
29
|
+
endpoint: str | None = None,
|
|
30
|
+
token: str | None = None,
|
|
31
|
+
service_name: str | None = None,
|
|
32
|
+
service_version: str | None = None,
|
|
33
|
+
debug: bool | None = None,
|
|
34
|
+
) -> ResolvedConfig:
|
|
35
|
+
env = os.environ
|
|
36
|
+
resolved_endpoint = (
|
|
37
|
+
endpoint
|
|
38
|
+
or env.get(EnvVar.CATALYST_OTLP_ENDPOINT)
|
|
39
|
+
or env.get(EnvVar.LEGACY_OTLP_ENDPOINT)
|
|
40
|
+
or DefaultConfig.OTLP_ENDPOINT
|
|
41
|
+
)
|
|
42
|
+
resolved_token = (
|
|
43
|
+
token
|
|
44
|
+
or env.get(EnvVar.CATALYST_OTLP_TOKEN)
|
|
45
|
+
or env.get(EnvVar.LEGACY_OTLP_INGEST_TOKEN)
|
|
46
|
+
)
|
|
47
|
+
resolved_service_name = (
|
|
48
|
+
service_name
|
|
49
|
+
or env.get(EnvVar.CATALYST_SERVICE_NAME)
|
|
50
|
+
or env.get(EnvVar.LEGACY_SERVICE_NAME)
|
|
51
|
+
or f"{DefaultConfig.SERVICE_NAME_PREFIX}-{uuid.uuid4().hex[:8]}"
|
|
52
|
+
)
|
|
53
|
+
resolved_service_version = (
|
|
54
|
+
service_version
|
|
55
|
+
or env.get(EnvVar.CATALYST_SERVICE_VERSION)
|
|
56
|
+
or DefaultConfig.SERVICE_VERSION
|
|
57
|
+
)
|
|
58
|
+
resolved_debug = (
|
|
59
|
+
debug if debug is not None else _truthy(env.get(EnvVar.CATALYST_DEBUG))
|
|
60
|
+
)
|
|
61
|
+
return ResolvedConfig(
|
|
62
|
+
endpoint=resolved_endpoint,
|
|
63
|
+
token=resolved_token,
|
|
64
|
+
service_name=resolved_service_name,
|
|
65
|
+
service_version=resolved_service_version,
|
|
66
|
+
debug=resolved_debug,
|
|
67
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Catalyst-tracing error types.
|
|
2
|
+
|
|
3
|
+
Errors raised by this package always extend :class:`CatalystTracingError`
|
|
4
|
+
and carry a stable :attr:`~CatalystTracingError.code` string for
|
|
5
|
+
programmatic handling. Messages are written for humans — they say what
|
|
6
|
+
went wrong AND how to fix it.
|
|
7
|
+
|
|
8
|
+
Use ``isinstance(err, CatalystTracingError)`` for catch-and-log
|
|
9
|
+
policies and the ``code`` field for fine-grained branching.
|
|
10
|
+
|
|
11
|
+
Example::
|
|
12
|
+
|
|
13
|
+
from catalyst_tracing import CatalystTracingError, install_openai
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
install_openai(provider)
|
|
17
|
+
except CatalystTracingError as err:
|
|
18
|
+
if err.code == "ERR_INVALID_TRACER_PROVIDER":
|
|
19
|
+
... # actionable handling
|
|
20
|
+
raise
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Final, Literal
|
|
26
|
+
|
|
27
|
+
from .constants import InstallerId
|
|
28
|
+
|
|
29
|
+
CatalystTracingErrorCode = Literal[
|
|
30
|
+
"ERR_INVALID_TRACER_PROVIDER",
|
|
31
|
+
"ERR_INVALID_SDK_INPUT",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CatalystTracingErrorCodes:
|
|
36
|
+
INVALID_TRACER_PROVIDER: CatalystTracingErrorCode = "ERR_INVALID_TRACER_PROVIDER"
|
|
37
|
+
INVALID_SDK_INPUT: CatalystTracingErrorCode = "ERR_INVALID_SDK_INPUT"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CatalystTracingError(Exception):
|
|
41
|
+
"""Base class for every error raised by ``catalyst_tracing``.
|
|
42
|
+
|
|
43
|
+
Catch this when you only need "did the package raise?". Subclasses
|
|
44
|
+
exist for the specific failure modes.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
#: Stable, machine-readable code for ``except``-and-branch logic.
|
|
48
|
+
code: CatalystTracingErrorCode
|
|
49
|
+
|
|
50
|
+
def __init__(self, code: CatalystTracingErrorCode, message: str) -> None:
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
self.code = code
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InvalidTracerProviderError(CatalystTracingError):
|
|
56
|
+
"""Raised when an ``install_*`` helper is given something that
|
|
57
|
+
isn't a TracerProvider.
|
|
58
|
+
|
|
59
|
+
The Python install helpers consistently take a TracerProvider
|
|
60
|
+
(matching the OTel SDK's idiom). If you pass a Tracer, a string,
|
|
61
|
+
or ``None``, you get this error pointing at the call site rather
|
|
62
|
+
than a confusing AttributeError deep in the patcher.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
INTEGRATION_HINTS: Final[dict[str, str]] = {
|
|
66
|
+
InstallerId.OPENAI: "install_openai(provider)",
|
|
67
|
+
InstallerId.ANTHROPIC: "install_anthropic(provider)",
|
|
68
|
+
InstallerId.LANGCHAIN: "install_langchain(provider)",
|
|
69
|
+
InstallerId.LANGGRAPH: "install_langgraph(provider)",
|
|
70
|
+
InstallerId.LANGSMITH: "install_langsmith(provider)",
|
|
71
|
+
InstallerId.OPENAI_AGENTS: "install_openai_agents(provider)",
|
|
72
|
+
InstallerId.CLAUDE_AGENT_SDK: "install_claude_agent_sdk(provider)",
|
|
73
|
+
InstallerId.PYDANTIC_AI: "install_pydantic_ai(provider)",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def __init__(self, integration: str, received: Any) -> None:
|
|
77
|
+
hint = self.INTEGRATION_HINTS.get(
|
|
78
|
+
integration,
|
|
79
|
+
f"install_{integration}(provider)",
|
|
80
|
+
)
|
|
81
|
+
super().__init__(
|
|
82
|
+
CatalystTracingErrorCodes.INVALID_TRACER_PROVIDER,
|
|
83
|
+
f"install_{integration}: expected a TracerProvider "
|
|
84
|
+
f"(from `opentelemetry.sdk.trace`), got {_describe(received)}.\n"
|
|
85
|
+
f"Hint: pass `tracing.provider` from `setup()`, or your own "
|
|
86
|
+
f"`TracerProvider`: `{hint}`.",
|
|
87
|
+
)
|
|
88
|
+
self.integration = integration
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InvalidSdkInputError(CatalystTracingError):
|
|
92
|
+
"""Raised when an ``install_*`` helper that takes an SDK module
|
|
93
|
+
is given a non-module / wrong-shape argument."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
integration: str,
|
|
99
|
+
expectation: str,
|
|
100
|
+
received: Any,
|
|
101
|
+
fix_hint: str,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__(
|
|
104
|
+
CatalystTracingErrorCodes.INVALID_SDK_INPUT,
|
|
105
|
+
f"install_{integration}: {expectation}, got {_describe(received)}.\n"
|
|
106
|
+
f"Hint: {fix_hint}",
|
|
107
|
+
)
|
|
108
|
+
self.integration = integration
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _describe(value: Any) -> str:
|
|
112
|
+
"""Short, human-friendly description of a value for error text.
|
|
113
|
+
|
|
114
|
+
We avoid logging the full value (potentially huge / contains
|
|
115
|
+
secrets); just enough context for the user to recognize what they
|
|
116
|
+
passed.
|
|
117
|
+
"""
|
|
118
|
+
if value is None:
|
|
119
|
+
return "None"
|
|
120
|
+
if isinstance(value, type):
|
|
121
|
+
return f"class {value.__name__}"
|
|
122
|
+
if callable(value):
|
|
123
|
+
name = getattr(value, "__name__", None) or "anonymous"
|
|
124
|
+
return f"callable `{name}`"
|
|
125
|
+
if isinstance(value, dict):
|
|
126
|
+
keys = list(value)[:4]
|
|
127
|
+
if not keys:
|
|
128
|
+
return "an empty dict"
|
|
129
|
+
suffix = ", ..." if len(value) > 4 else ""
|
|
130
|
+
return f"dict with keys {keys}{suffix}"
|
|
131
|
+
type_name = type(value).__name__
|
|
132
|
+
if isinstance(value, (str, int, float, bool)):
|
|
133
|
+
return f"{type_name} {value!r}"
|
|
134
|
+
return type_name
|