fiddler-adk 1.0.0rc1__tar.gz
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.
- fiddler_adk-1.0.0rc1/PKG-INFO +96 -0
- fiddler_adk-1.0.0rc1/README.md +61 -0
- fiddler_adk-1.0.0rc1/VERSION +1 -0
- fiddler_adk-1.0.0rc1/pyproject.toml +113 -0
- fiddler_adk-1.0.0rc1/setup.cfg +4 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk/__init__.py +17 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk/instrumentation.py +121 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk/span_processor.py +109 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk.egg-info/PKG-INFO +96 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk.egg-info/SOURCES.txt +13 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk.egg-info/dependency_links.txt +1 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk.egg-info/requires.txt +10 -0
- fiddler_adk-1.0.0rc1/src/fiddler_adk.egg-info/top_level.txt +1 -0
- fiddler_adk-1.0.0rc1/tests/test_adk_instrumentation.py +74 -0
- fiddler_adk-1.0.0rc1/tests/test_adk_span_processor.py +64 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fiddler-adk
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Fiddler SDK for Google ADK (Agent Development Kit) instrumentation with OpenTelemetry
|
|
5
|
+
Author-email: Fiddler AI <support@fiddler.ai>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://fiddler.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.fiddler.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/fiddler-labs/fiddler-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/fiddler-labs/fiddler-sdk/issues
|
|
11
|
+
Keywords: fiddler,ai,genai,llm,monitoring,observability,instrumentation,google-adk,adk,opentelemetry
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: fiddler-otel<2.0.0,>=1.0.0
|
|
27
|
+
Requires-Dist: google-adk>=1.34.2
|
|
28
|
+
Requires-Dist: opentelemetry-api>=1.37.0
|
|
29
|
+
Requires-Dist: opentelemetry-sdk>=1.37.0
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation>=0.58b0
|
|
31
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.37.0
|
|
32
|
+
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Provides-Extra: examples
|
|
34
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "examples"
|
|
35
|
+
|
|
36
|
+
# fiddler-adk
|
|
37
|
+
|
|
38
|
+
Fiddler SDK for instrumenting [Google ADK](https://github.com/google/adk-python) (Agent Development Kit) with OpenTelemetry.
|
|
39
|
+
|
|
40
|
+
This package lives in the [fiddler-sdk](https://github.com/fiddler-labs/fiddler-sdk) monorepo under `packages/integrations/fiddler-adk/`. It depends on [`fiddler-otel`](../../fiddler-otel) for the OTel pipeline, span attributes, and span processing.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install fiddler-adk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fiddler_otel import FiddlerClient
|
|
52
|
+
from fiddler_adk import GoogleADKInstrumentor
|
|
53
|
+
|
|
54
|
+
client = FiddlerClient(
|
|
55
|
+
api_key='YOUR_API_KEY',
|
|
56
|
+
application_id='YOUR_APPLICATION_ID', # UUID4 from the Fiddler GenAI Applications page
|
|
57
|
+
url='https://your-instance.fiddler.ai',
|
|
58
|
+
)
|
|
59
|
+
GoogleADKInstrumentor(client).instrument()
|
|
60
|
+
|
|
61
|
+
# Build and run ADK agents as usual — traces flow to Fiddler automatically.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
Google ADK emits OpenTelemetry spans natively (`invoke_agent`, `call_llm`,
|
|
67
|
+
`execute_tool`, ...) through a tracer that resolves against the **global**
|
|
68
|
+
tracer provider. `GoogleADKInstrumentor.instrument()`:
|
|
69
|
+
|
|
70
|
+
1. **Routes spans to Fiddler.** If no global tracer provider is configured,
|
|
71
|
+
the Fiddler client's provider (span processor + OTLP exporter) is promoted
|
|
72
|
+
to global. If your application (or `adk web`) already configured one, the
|
|
73
|
+
Fiddler span processor and exporter are attached to it instead.
|
|
74
|
+
2. **Enriches span attributes.** An `ADKSpanProcessor` reads the JSON attributes
|
|
75
|
+
that ADK natively sets on spans (`gcp.vertex.agent.llm_request`, etc.) and
|
|
76
|
+
maps them to `gen_ai.llm.input.user`, `gen_ai.llm.input.system`,
|
|
77
|
+
`gen_ai.llm.output`, `gen_ai.tool.input`, and `gen_ai.tool.output` — the
|
|
78
|
+
attributes Fiddler's evaluations consume. No monkey-patching is involved.
|
|
79
|
+
|
|
80
|
+
### Conversation and session context
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import uuid
|
|
84
|
+
from fiddler_adk import add_session_attributes, set_conversation_id
|
|
85
|
+
|
|
86
|
+
set_conversation_id(str(uuid.uuid4())) # gen_ai.conversation.id on all spans
|
|
87
|
+
add_session_attributes('user_tier', 'premium') # fiddler.session.user.user_tier on all spans
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Content capture
|
|
91
|
+
|
|
92
|
+
ADK includes full LLM request/response payloads in span attributes by default.
|
|
93
|
+
Set `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false` to disable ADK's payload
|
|
94
|
+
capture if your prompts may contain PII.
|
|
95
|
+
|
|
96
|
+
See `examples/` in this directory for a runnable agent.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# fiddler-adk
|
|
2
|
+
|
|
3
|
+
Fiddler SDK for instrumenting [Google ADK](https://github.com/google/adk-python) (Agent Development Kit) with OpenTelemetry.
|
|
4
|
+
|
|
5
|
+
This package lives in the [fiddler-sdk](https://github.com/fiddler-labs/fiddler-sdk) monorepo under `packages/integrations/fiddler-adk/`. It depends on [`fiddler-otel`](../../fiddler-otel) for the OTel pipeline, span attributes, and span processing.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install fiddler-adk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fiddler_otel import FiddlerClient
|
|
17
|
+
from fiddler_adk import GoogleADKInstrumentor
|
|
18
|
+
|
|
19
|
+
client = FiddlerClient(
|
|
20
|
+
api_key='YOUR_API_KEY',
|
|
21
|
+
application_id='YOUR_APPLICATION_ID', # UUID4 from the Fiddler GenAI Applications page
|
|
22
|
+
url='https://your-instance.fiddler.ai',
|
|
23
|
+
)
|
|
24
|
+
GoogleADKInstrumentor(client).instrument()
|
|
25
|
+
|
|
26
|
+
# Build and run ADK agents as usual — traces flow to Fiddler automatically.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
Google ADK emits OpenTelemetry spans natively (`invoke_agent`, `call_llm`,
|
|
32
|
+
`execute_tool`, ...) through a tracer that resolves against the **global**
|
|
33
|
+
tracer provider. `GoogleADKInstrumentor.instrument()`:
|
|
34
|
+
|
|
35
|
+
1. **Routes spans to Fiddler.** If no global tracer provider is configured,
|
|
36
|
+
the Fiddler client's provider (span processor + OTLP exporter) is promoted
|
|
37
|
+
to global. If your application (or `adk web`) already configured one, the
|
|
38
|
+
Fiddler span processor and exporter are attached to it instead.
|
|
39
|
+
2. **Enriches span attributes.** An `ADKSpanProcessor` reads the JSON attributes
|
|
40
|
+
that ADK natively sets on spans (`gcp.vertex.agent.llm_request`, etc.) and
|
|
41
|
+
maps them to `gen_ai.llm.input.user`, `gen_ai.llm.input.system`,
|
|
42
|
+
`gen_ai.llm.output`, `gen_ai.tool.input`, and `gen_ai.tool.output` — the
|
|
43
|
+
attributes Fiddler's evaluations consume. No monkey-patching is involved.
|
|
44
|
+
|
|
45
|
+
### Conversation and session context
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import uuid
|
|
49
|
+
from fiddler_adk import add_session_attributes, set_conversation_id
|
|
50
|
+
|
|
51
|
+
set_conversation_id(str(uuid.uuid4())) # gen_ai.conversation.id on all spans
|
|
52
|
+
add_session_attributes('user_tier', 'premium') # fiddler.session.user.user_tier on all spans
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Content capture
|
|
56
|
+
|
|
57
|
+
ADK includes full LLM request/response payloads in span attributes by default.
|
|
58
|
+
Set `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false` to disable ADK's payload
|
|
59
|
+
capture if your prompts may contain PII.
|
|
60
|
+
|
|
61
|
+
See `examples/` in this directory for a runnable agent.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0rc1
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0,<82.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fiddler-adk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Fiddler AI", email = "support@fiddler.ai"},
|
|
10
|
+
]
|
|
11
|
+
description = "Fiddler SDK for Google ADK (Agent Development Kit) instrumentation with OpenTelemetry"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "Apache-2.0"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: System :: Monitoring",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
]
|
|
29
|
+
keywords = ["fiddler", "ai", "genai", "llm", "monitoring", "observability", "instrumentation", "google-adk", "adk", "opentelemetry"]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"fiddler-otel>=1.0.0,<2.0.0",
|
|
32
|
+
"google-adk>=1.34.2",
|
|
33
|
+
"opentelemetry-api>=1.37.0",
|
|
34
|
+
"opentelemetry-sdk>=1.37.0",
|
|
35
|
+
"opentelemetry-instrumentation>=0.58b0",
|
|
36
|
+
"opentelemetry-exporter-otlp>=1.37.0",
|
|
37
|
+
"pydantic>=2.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
examples = [
|
|
42
|
+
"python-dotenv>=1.0.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"setuptools>=61.0,<82.0.0",
|
|
48
|
+
"pytest>=9.0.3",
|
|
49
|
+
"pytest-asyncio>=0.24.0",
|
|
50
|
+
"pytest-cov>=6.1.1",
|
|
51
|
+
"mypy>=1.16.1",
|
|
52
|
+
"ruff>=0.15.10",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://fiddler.ai"
|
|
57
|
+
Documentation = "https://docs.fiddler.ai"
|
|
58
|
+
Repository = "https://github.com/fiddler-labs/fiddler-sdk"
|
|
59
|
+
Issues = "https://github.com/fiddler-labs/fiddler-sdk/issues"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.setuptools.dynamic]
|
|
65
|
+
version = {file = ["VERSION"]}
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.package-data]
|
|
68
|
+
fiddler_adk = ["VERSION"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
73
|
+
python_classes = ["Test*"]
|
|
74
|
+
python_functions = ["test_*"]
|
|
75
|
+
asyncio_mode = "auto"
|
|
76
|
+
|
|
77
|
+
[tool.ruff]
|
|
78
|
+
line-length = 100
|
|
79
|
+
target-version = "py310"
|
|
80
|
+
extend-exclude = ["tests"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.format]
|
|
83
|
+
quote-style = "single"
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
select = [
|
|
87
|
+
"E", "F", "I", "N", "UP", "B", "C4", "DTZ", "T10", "RET", "SIM", "PTH", "ERA", "RUF",
|
|
88
|
+
]
|
|
89
|
+
ignore = [
|
|
90
|
+
"E501", "B008", "B904", "SIM108", "E402", "ERA001",
|
|
91
|
+
]
|
|
92
|
+
fixable = ["ALL"]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.isort]
|
|
95
|
+
known-first-party = ["fiddler_adk", "fiddler_otel"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"__init__.py" = ["F401", "E402"]
|
|
99
|
+
"tests/*" = ["S101"]
|
|
100
|
+
|
|
101
|
+
[tool.mypy]
|
|
102
|
+
python_version = "3.10"
|
|
103
|
+
warn_return_any = true
|
|
104
|
+
warn_unused_configs = true
|
|
105
|
+
disallow_untyped_defs = true
|
|
106
|
+
|
|
107
|
+
[[tool.mypy.overrides]]
|
|
108
|
+
module = [
|
|
109
|
+
"google.adk.*",
|
|
110
|
+
"google.genai.*",
|
|
111
|
+
"pydantic",
|
|
112
|
+
]
|
|
113
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Fiddler ADK - Google ADK (Agent Development Kit) instrumentation for Fiddler."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from fiddler_adk.instrumentation import GoogleADKInstrumentor
|
|
6
|
+
from fiddler_adk.span_processor import ADKSpanProcessor
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version('fiddler-adk')
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = 'unknown'
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'ADKSpanProcessor',
|
|
15
|
+
'GoogleADKInstrumentor',
|
|
16
|
+
'__version__',
|
|
17
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for Google ADK (Agent Development Kit).
|
|
2
|
+
|
|
3
|
+
Google ADK emits OpenTelemetry spans natively through a module-level tracer
|
|
4
|
+
named ``gcp.vertex.agent`` that resolves against the **global** tracer
|
|
5
|
+
provider. This instrumentor sets up a Fiddler-managed tracer provider (via
|
|
6
|
+
:class:`~fiddler_otel.client.FiddlerClient`) and promotes it to global so
|
|
7
|
+
ADK's tracer resolves to it.
|
|
8
|
+
|
|
9
|
+
The SDK operates in standalone mode: it initialises its own isolated tracer,
|
|
10
|
+
provider, processor, and exporter via ``FiddlerClient``. It does not interact
|
|
11
|
+
with customer-configured tracers or providers.
|
|
12
|
+
|
|
13
|
+
Content extraction and attribute mapping are handled by the Fiddler backend's
|
|
14
|
+
``GoogleADKSpanMapper`` — not by this SDK.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
from fiddler_otel import FiddlerClient
|
|
19
|
+
from fiddler_adk import GoogleADKInstrumentor
|
|
20
|
+
|
|
21
|
+
client = FiddlerClient(url="...", api_key="...", application_id="...")
|
|
22
|
+
GoogleADKInstrumentor(client).instrument()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from collections.abc import Collection
|
|
29
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from opentelemetry import trace
|
|
33
|
+
from opentelemetry.instrumentation.instrumentor import ( # type: ignore[attr-defined]
|
|
34
|
+
BaseInstrumentor,
|
|
35
|
+
)
|
|
36
|
+
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
|
|
37
|
+
|
|
38
|
+
from fiddler_adk.span_processor import ADKSpanProcessor
|
|
39
|
+
from fiddler_otel.client import FiddlerClient
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_package_version(package_name: str) -> str:
|
|
45
|
+
"""Return the installed version of a package, or ``'unknown'``."""
|
|
46
|
+
try:
|
|
47
|
+
return version(package_name)
|
|
48
|
+
except PackageNotFoundError:
|
|
49
|
+
return 'unknown'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GoogleADKInstrumentor(BaseInstrumentor):
|
|
53
|
+
"""OpenTelemetry instrumentor for Google ADK agents.
|
|
54
|
+
|
|
55
|
+
Sets up the Fiddler tracing pipeline via ``FiddlerClient`` and promotes
|
|
56
|
+
it to the global tracer provider so ADK's ``gcp.vertex.agent`` tracer
|
|
57
|
+
resolves to it. Adds ``ADKSpanProcessor`` for root-span conversation-id
|
|
58
|
+
backfill.
|
|
59
|
+
|
|
60
|
+
The SDK operates in standalone mode — it initialises its own isolated
|
|
61
|
+
tracer, provider, processor, and exporter. It does not interact with
|
|
62
|
+
customer-configured tracers.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, client: FiddlerClient):
|
|
66
|
+
"""Initialise the Google ADK instrumentor.
|
|
67
|
+
|
|
68
|
+
:param client: The ``FiddlerClient`` instance that owns the tracing
|
|
69
|
+
pipeline (tracer, provider, processor, exporter).
|
|
70
|
+
"""
|
|
71
|
+
self._client = client
|
|
72
|
+
self._provider_configured = False
|
|
73
|
+
|
|
74
|
+
self._client.update_resource(
|
|
75
|
+
{
|
|
76
|
+
'lib.google-adk.version': _get_package_version('google-adk'),
|
|
77
|
+
'lib.fiddler-adk.version': _get_package_version('fiddler-adk'),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
83
|
+
"""Return the packages required for this instrumentation."""
|
|
84
|
+
return ['google-adk']
|
|
85
|
+
|
|
86
|
+
def _instrument(self, **kwargs: Any) -> None:
|
|
87
|
+
"""Enable instrumentation: set up tracer provider with ADKSpanProcessor."""
|
|
88
|
+
self._setup_tracer_provider()
|
|
89
|
+
logger.info('Google ADK instrumentation enabled')
|
|
90
|
+
|
|
91
|
+
def _uninstrument(self, **kwargs: Any) -> None:
|
|
92
|
+
"""Disable instrumentation.
|
|
93
|
+
|
|
94
|
+
The tracer provider wiring is left in place: OpenTelemetry does not
|
|
95
|
+
support unsetting the global provider or removing span processors.
|
|
96
|
+
"""
|
|
97
|
+
logger.info('Google ADK instrumentation disabled')
|
|
98
|
+
|
|
99
|
+
def _setup_tracer_provider(self) -> None:
|
|
100
|
+
"""Set up the Fiddler tracer provider and promote it to global.
|
|
101
|
+
|
|
102
|
+
Uses ``FiddlerClient.get_tracer()`` which initialises the complete
|
|
103
|
+
tracing pipeline (provider, processor, exporter) in isolation.
|
|
104
|
+
The client's provider is then promoted to the global provider so
|
|
105
|
+
ADK's ``gcp.vertex.agent`` tracer resolves to it.
|
|
106
|
+
"""
|
|
107
|
+
if self._provider_configured:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Initialise the Fiddler tracing pipeline
|
|
111
|
+
self._client.get_tracer()
|
|
112
|
+
provider = self._client.get_tracer_provider()
|
|
113
|
+
|
|
114
|
+
# Add ADK-specific span processor for root-span conversation-id backfill.
|
|
115
|
+
if isinstance(provider, SDKTracerProvider):
|
|
116
|
+
provider.add_span_processor(ADKSpanProcessor())
|
|
117
|
+
|
|
118
|
+
# Promote to global so ADK's tracer resolves to Fiddler's provider
|
|
119
|
+
trace.set_tracer_provider(provider)
|
|
120
|
+
|
|
121
|
+
self._provider_configured = True
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ADK span processor: extends fiddler-otel with ADK-specific span enrichment.
|
|
2
|
+
|
|
3
|
+
This is a pure ``SpanProcessor`` approach — no monkey-patching.
|
|
4
|
+
|
|
5
|
+
Responsibilities:
|
|
6
|
+
|
|
7
|
+
* ``on_end``: backfill ``gen_ai.conversation.id`` onto the root ``invocation``
|
|
8
|
+
span, which ADK leaves attribute-less (it only stamps conversation id on
|
|
9
|
+
child spans).
|
|
10
|
+
|
|
11
|
+
All other enrichment (span-type classification, agent ID, content extraction,
|
|
12
|
+
attribute normalization) is handled by the Fiddler backend's
|
|
13
|
+
``GoogleADKSpanMapper``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
|
|
21
|
+
from opentelemetry import context
|
|
22
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
23
|
+
from opentelemetry.sdk.trace import Span as SDKSpan
|
|
24
|
+
from opentelemetry.trace import Span
|
|
25
|
+
|
|
26
|
+
from fiddler_otel.attributes import FiddlerSpanAttributes
|
|
27
|
+
from fiddler_otel.span_processor import FiddlerSpanProcessor as CoreFiddlerSpanProcessor
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ADKSpanProcessor(CoreFiddlerSpanProcessor):
|
|
33
|
+
"""Backfills ``gen_ai.conversation.id`` onto the root ADK span.
|
|
34
|
+
|
|
35
|
+
Extends :class:`CoreFiddlerSpanProcessor` (contextvar injection, attribute
|
|
36
|
+
denormalization). ADK sets ``gen_ai.conversation.id`` only on child spans
|
|
37
|
+
(``invoke_agent``, ``call_llm``, etc.); the root ``invocation`` span gets no
|
|
38
|
+
attributes. This processor copies the conversation id onto the still-open
|
|
39
|
+
root span when the first child carrying it ends.
|
|
40
|
+
|
|
41
|
+
All other enrichment (span-type classification, agent ID computation,
|
|
42
|
+
content extraction, attribute normalization) is delegated to the Fiddler
|
|
43
|
+
backend's ``GoogleADKSpanMapper``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
"""Initialise the processor and the live-root-span registry."""
|
|
48
|
+
super().__init__()
|
|
49
|
+
# Maps trace_id -> the still-open root span, so a child's
|
|
50
|
+
# ``gen_ai.conversation.id`` can be backfilled onto the root on end.
|
|
51
|
+
self._root_spans: dict[int, Span] = {}
|
|
52
|
+
self._root_lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
def on_start(self, span: Span, parent_context: context.Context | None = None) -> None:
|
|
55
|
+
"""Track root spans, then delegate to core."""
|
|
56
|
+
self._track_root_span(span)
|
|
57
|
+
super().on_start(span, parent_context)
|
|
58
|
+
|
|
59
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
60
|
+
"""Backfill ``gen_ai.conversation.id`` onto the root span.
|
|
61
|
+
|
|
62
|
+
ADK only sets ``gen_ai.conversation.id`` on child spans (``invoke_agent``,
|
|
63
|
+
``call_llm``, etc.); the root ``invocation`` span gets no attributes. The
|
|
64
|
+
root span stays open for the whole run, so when a child ends we copy its
|
|
65
|
+
conversation id onto the still-recording root span.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
self._backfill_root_conversation_id(span)
|
|
69
|
+
except Exception as e: # pylint: disable=broad-except
|
|
70
|
+
logger.debug('Failed to backfill root conversation id: %s', e, exc_info=True)
|
|
71
|
+
super().on_end(span)
|
|
72
|
+
|
|
73
|
+
def _track_root_span(self, span: Span) -> None:
|
|
74
|
+
"""Remember the root span of each trace so children can backfill onto it."""
|
|
75
|
+
if getattr(span, 'parent', None) is not None:
|
|
76
|
+
return
|
|
77
|
+
span_context = span.get_span_context()
|
|
78
|
+
if span_context is None:
|
|
79
|
+
return
|
|
80
|
+
with self._root_lock:
|
|
81
|
+
self._root_spans[span_context.trace_id] = span
|
|
82
|
+
|
|
83
|
+
def _backfill_root_conversation_id(self, span: ReadableSpan) -> None:
|
|
84
|
+
"""Copy a child's conversation id onto the open root span; clean up on root end."""
|
|
85
|
+
span_context = span.context
|
|
86
|
+
if span_context is None:
|
|
87
|
+
return
|
|
88
|
+
trace_id = span_context.trace_id
|
|
89
|
+
|
|
90
|
+
with self._root_lock:
|
|
91
|
+
root = self._root_spans.get(trace_id)
|
|
92
|
+
# The root span itself is ending — drop the reference and stop.
|
|
93
|
+
if root is not None and span_context.span_id == root.get_span_context().span_id:
|
|
94
|
+
del self._root_spans[trace_id]
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if not isinstance(root, SDKSpan):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
conversation_id = (span.attributes or {}).get(FiddlerSpanAttributes.CONVERSATION_ID)
|
|
101
|
+
if not conversation_id:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if not (root.is_recording() and root.attributes is not None):
|
|
105
|
+
return
|
|
106
|
+
if FiddlerSpanAttributes.CONVERSATION_ID in root.attributes:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
root.set_attribute(FiddlerSpanAttributes.CONVERSATION_ID, conversation_id)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fiddler-adk
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Fiddler SDK for Google ADK (Agent Development Kit) instrumentation with OpenTelemetry
|
|
5
|
+
Author-email: Fiddler AI <support@fiddler.ai>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://fiddler.ai
|
|
8
|
+
Project-URL: Documentation, https://docs.fiddler.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/fiddler-labs/fiddler-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/fiddler-labs/fiddler-sdk/issues
|
|
11
|
+
Keywords: fiddler,ai,genai,llm,monitoring,observability,instrumentation,google-adk,adk,opentelemetry
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: fiddler-otel<2.0.0,>=1.0.0
|
|
27
|
+
Requires-Dist: google-adk>=1.34.2
|
|
28
|
+
Requires-Dist: opentelemetry-api>=1.37.0
|
|
29
|
+
Requires-Dist: opentelemetry-sdk>=1.37.0
|
|
30
|
+
Requires-Dist: opentelemetry-instrumentation>=0.58b0
|
|
31
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.37.0
|
|
32
|
+
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Provides-Extra: examples
|
|
34
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "examples"
|
|
35
|
+
|
|
36
|
+
# fiddler-adk
|
|
37
|
+
|
|
38
|
+
Fiddler SDK for instrumenting [Google ADK](https://github.com/google/adk-python) (Agent Development Kit) with OpenTelemetry.
|
|
39
|
+
|
|
40
|
+
This package lives in the [fiddler-sdk](https://github.com/fiddler-labs/fiddler-sdk) monorepo under `packages/integrations/fiddler-adk/`. It depends on [`fiddler-otel`](../../fiddler-otel) for the OTel pipeline, span attributes, and span processing.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install fiddler-adk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fiddler_otel import FiddlerClient
|
|
52
|
+
from fiddler_adk import GoogleADKInstrumentor
|
|
53
|
+
|
|
54
|
+
client = FiddlerClient(
|
|
55
|
+
api_key='YOUR_API_KEY',
|
|
56
|
+
application_id='YOUR_APPLICATION_ID', # UUID4 from the Fiddler GenAI Applications page
|
|
57
|
+
url='https://your-instance.fiddler.ai',
|
|
58
|
+
)
|
|
59
|
+
GoogleADKInstrumentor(client).instrument()
|
|
60
|
+
|
|
61
|
+
# Build and run ADK agents as usual — traces flow to Fiddler automatically.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
Google ADK emits OpenTelemetry spans natively (`invoke_agent`, `call_llm`,
|
|
67
|
+
`execute_tool`, ...) through a tracer that resolves against the **global**
|
|
68
|
+
tracer provider. `GoogleADKInstrumentor.instrument()`:
|
|
69
|
+
|
|
70
|
+
1. **Routes spans to Fiddler.** If no global tracer provider is configured,
|
|
71
|
+
the Fiddler client's provider (span processor + OTLP exporter) is promoted
|
|
72
|
+
to global. If your application (or `adk web`) already configured one, the
|
|
73
|
+
Fiddler span processor and exporter are attached to it instead.
|
|
74
|
+
2. **Enriches span attributes.** An `ADKSpanProcessor` reads the JSON attributes
|
|
75
|
+
that ADK natively sets on spans (`gcp.vertex.agent.llm_request`, etc.) and
|
|
76
|
+
maps them to `gen_ai.llm.input.user`, `gen_ai.llm.input.system`,
|
|
77
|
+
`gen_ai.llm.output`, `gen_ai.tool.input`, and `gen_ai.tool.output` — the
|
|
78
|
+
attributes Fiddler's evaluations consume. No monkey-patching is involved.
|
|
79
|
+
|
|
80
|
+
### Conversation and session context
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import uuid
|
|
84
|
+
from fiddler_adk import add_session_attributes, set_conversation_id
|
|
85
|
+
|
|
86
|
+
set_conversation_id(str(uuid.uuid4())) # gen_ai.conversation.id on all spans
|
|
87
|
+
add_session_attributes('user_tier', 'premium') # fiddler.session.user.user_tier on all spans
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Content capture
|
|
91
|
+
|
|
92
|
+
ADK includes full LLM request/response payloads in span attributes by default.
|
|
93
|
+
Set `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false` to disable ADK's payload
|
|
94
|
+
capture if your prompts may contain PII.
|
|
95
|
+
|
|
96
|
+
See `examples/` in this directory for a runnable agent.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
VERSION
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/fiddler_adk/__init__.py
|
|
5
|
+
src/fiddler_adk/instrumentation.py
|
|
6
|
+
src/fiddler_adk/span_processor.py
|
|
7
|
+
src/fiddler_adk.egg-info/PKG-INFO
|
|
8
|
+
src/fiddler_adk.egg-info/SOURCES.txt
|
|
9
|
+
src/fiddler_adk.egg-info/dependency_links.txt
|
|
10
|
+
src/fiddler_adk.egg-info/requires.txt
|
|
11
|
+
src/fiddler_adk.egg-info/top_level.txt
|
|
12
|
+
tests/test_adk_instrumentation.py
|
|
13
|
+
tests/test_adk_span_processor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fiddler_adk
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Tests for the GoogleADKInstrumentor."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
|
|
7
|
+
|
|
8
|
+
from fiddler_adk import GoogleADKInstrumentor
|
|
9
|
+
from fiddler_adk.span_processor import ADKSpanProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def client():
|
|
14
|
+
"""Mock FiddlerClient."""
|
|
15
|
+
mock_client = Mock()
|
|
16
|
+
mock_client.api_key = 'test-key'
|
|
17
|
+
mock_client.url = 'https://test.fiddler.ai'
|
|
18
|
+
mock_client.application_id = '00000000-0000-4000-8000-000000000000'
|
|
19
|
+
return mock_client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def instrumentor(client):
|
|
24
|
+
"""Fresh instrumentor."""
|
|
25
|
+
adk_instrumentor = GoogleADKInstrumentor(client)
|
|
26
|
+
adk_instrumentor._provider_configured = False
|
|
27
|
+
yield adk_instrumentor
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestGoogleADKInstrumentor:
|
|
31
|
+
def test_instrumentation_dependencies(self, instrumentor):
|
|
32
|
+
assert 'google-adk' in instrumentor.instrumentation_dependencies()
|
|
33
|
+
|
|
34
|
+
def test_init_updates_client_resource(self, client):
|
|
35
|
+
GoogleADKInstrumentor(client)
|
|
36
|
+
attributes = client.update_resource.call_args[0][0]
|
|
37
|
+
assert 'lib.google-adk.version' in attributes
|
|
38
|
+
assert 'lib.fiddler-adk.version' in attributes
|
|
39
|
+
|
|
40
|
+
def test_requires_fiddler_client(self):
|
|
41
|
+
"""Must be initialised with a FiddlerClient — no standalone mode."""
|
|
42
|
+
with pytest.raises(TypeError):
|
|
43
|
+
GoogleADKInstrumentor() # type: ignore[call-arg]
|
|
44
|
+
|
|
45
|
+
def test_setup_uses_client_provider(self, instrumentor, client):
|
|
46
|
+
"""Uses FiddlerClient's own provider, promoted to global."""
|
|
47
|
+
with patch('fiddler_adk.instrumentation.trace') as mock_trace:
|
|
48
|
+
instrumentor._setup_tracer_provider()
|
|
49
|
+
|
|
50
|
+
client.get_tracer.assert_called_once()
|
|
51
|
+
mock_trace.set_tracer_provider.assert_called_once_with(
|
|
52
|
+
client.get_tracer_provider.return_value
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_setup_is_idempotent(self, instrumentor, client):
|
|
56
|
+
with patch('fiddler_adk.instrumentation.trace'):
|
|
57
|
+
instrumentor._setup_tracer_provider()
|
|
58
|
+
instrumentor._setup_tracer_provider()
|
|
59
|
+
|
|
60
|
+
client.get_tracer.assert_called_once()
|
|
61
|
+
|
|
62
|
+
def test_no_monkey_patching(self, instrumentor):
|
|
63
|
+
assert not hasattr(instrumentor, '_patched')
|
|
64
|
+
assert not hasattr(instrumentor, '_wrapped_trace_call_llm')
|
|
65
|
+
|
|
66
|
+
def test_no_standalone_mode(self):
|
|
67
|
+
"""No env-var-only / standalone pattern — always requires client."""
|
|
68
|
+
import inspect
|
|
69
|
+
|
|
70
|
+
sig = inspect.signature(GoogleADKInstrumentor.__init__)
|
|
71
|
+
params = list(sig.parameters.keys())
|
|
72
|
+
assert 'client' in params
|
|
73
|
+
# client should not have a default value (not Optional)
|
|
74
|
+
assert sig.parameters['client'].default is inspect.Parameter.empty
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Tests for the ADKSpanProcessor."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
5
|
+
|
|
6
|
+
from fiddler_adk.span_processor import ADKSpanProcessor
|
|
7
|
+
from fiddler_otel.attributes import FiddlerSpanAttributes
|
|
8
|
+
|
|
9
|
+
CID = FiddlerSpanAttributes.CONVERSATION_ID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def tracer():
|
|
14
|
+
"""A tracer wired to an ADKSpanProcessor, mimicking the ADK span tree."""
|
|
15
|
+
provider = TracerProvider()
|
|
16
|
+
provider.add_span_processor(ADKSpanProcessor())
|
|
17
|
+
return provider.get_tracer('test')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestRootConversationIdBackfill:
|
|
21
|
+
def test_root_gets_conversation_id_from_child(self, tracer):
|
|
22
|
+
"""ADK leaves the root ``invocation`` span attribute-less; a child's
|
|
23
|
+
conversation id should be backfilled onto it."""
|
|
24
|
+
with tracer.start_as_current_span('invocation') as root:
|
|
25
|
+
with tracer.start_as_current_span('invoke_agent time_agent') as child:
|
|
26
|
+
child.set_attribute(CID, 'session-abc')
|
|
27
|
+
assert root.attributes.get(CID) == 'session-abc'
|
|
28
|
+
|
|
29
|
+
def test_backfill_happens_while_root_open(self, tracer):
|
|
30
|
+
"""The id should land on the root as soon as the first child ends."""
|
|
31
|
+
with tracer.start_as_current_span('invocation') as root:
|
|
32
|
+
with tracer.start_as_current_span('invoke_agent x') as child:
|
|
33
|
+
child.set_attribute(CID, 'session-xyz')
|
|
34
|
+
assert root.attributes.get(CID) == 'session-xyz'
|
|
35
|
+
|
|
36
|
+
def test_root_without_conversation_id_children_unchanged(self, tracer):
|
|
37
|
+
"""No child carries a conversation id → root stays without one."""
|
|
38
|
+
with tracer.start_as_current_span('invocation') as root:
|
|
39
|
+
with tracer.start_as_current_span('invoke_agent x'):
|
|
40
|
+
pass
|
|
41
|
+
assert CID not in (root.attributes or {})
|
|
42
|
+
|
|
43
|
+
def test_first_child_conversation_id_wins(self, tracer):
|
|
44
|
+
"""Once set, the root id is not overwritten by later children."""
|
|
45
|
+
with tracer.start_as_current_span('invocation') as root:
|
|
46
|
+
with tracer.start_as_current_span('invoke_agent a') as first:
|
|
47
|
+
first.set_attribute(CID, 'session-1')
|
|
48
|
+
with tracer.start_as_current_span('invoke_agent b') as second:
|
|
49
|
+
second.set_attribute(CID, 'session-2')
|
|
50
|
+
assert root.attributes.get(CID) == 'session-1'
|
|
51
|
+
|
|
52
|
+
def test_no_span_type_set(self, tracer):
|
|
53
|
+
"""Span-type classification is delegated to the backend; the processor
|
|
54
|
+
must not set ``fiddler.span.type``."""
|
|
55
|
+
with tracer.start_as_current_span('invocation') as root:
|
|
56
|
+
with tracer.start_as_current_span('call_llm') as child:
|
|
57
|
+
child.set_attribute(CID, 'session-abc')
|
|
58
|
+
assert FiddlerSpanAttributes.TYPE not in (root.attributes or {})
|
|
59
|
+
assert FiddlerSpanAttributes.TYPE not in (child.attributes or {})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestGeneral:
|
|
63
|
+
def test_force_flush(self):
|
|
64
|
+
assert ADKSpanProcessor().force_flush() is True
|