Skip to content

Commit 2186ba1

Browse files
feat(sdk): Add telemetry package to support sinks (#164)
## Summary Added a new shared top-level `telemetry/` package to hold common telemetry contracts and trace-context primitives used across AgentControl. Refactored the SDK observability write path to use the shared `ControlEventSink` contract internally while preserving the existing default OSS behavior: queue-backed batching and delivery to `/api/v1/observability/events`. Aligned telemetry packaging with the existing models/engine bundling pattern so telemetry is vendored into SDK and server builds rather than treated as a separate runtime-only dependency. ## Scope ### User-facing / API changes - No intended user-facing behavior changes. - No change to default OSS event delivery behavior. - No new public sink-selection API in this story. ### Internal changes - Introduced shared telemetry types: `ControlEventSink`, `SinkResult`, `BaseControlEventSink`, and shared trace-context helpers. - Removed SDK-local ownership of trace-context internals and pointed SDK code/tests to the shared telemetry package. - Updated SDK observability internals so event writes flow through the shared sink contract. - Updated build/package wiring so telemetry is bundled like models and engine. ### Out of scope - Server write-path refactor to the shared sink contract. - Config-driven sink selection. - OTEL sink implementation and OTEL settings. - Any changes to `/observability/events` server ingestion behavior. ## Risk and Rollout **Risk level:** Medium **Rollback plan:** 1. Revert the telemetry package introduction and SDK observability refactor. 2. Restore the previous SDK-local telemetry module and direct batcher-based event writes. 3. Remove telemetry vendoring from SDK/server build configuration. ## Testing - [x] Added or updated automated tests - [x] Ran `make check` (or explained why not) > Focused validation is still pending locally; mypy surfaced follow-up packaging/type issues during development and we have been resolving those incrementally. - [ ] Manually verified behavior ## Checklist - [ ] Linked issue/spec (if applicable) - [x] Updated docs/examples for user-facing changes > No docs/example updates were needed because this story is internal-only and no repo examples referenced the old telemetry module path. - [ ] Included any required follow-up tasks
1 parent 92b2d13 commit 2186ba1

25 files changed

Lines changed: 571 additions & 49 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
- name: Upload coverage to Codecov
6363
uses: codecov/codecov-action@v4
6464
with:
65-
files: coverage-models.xml,coverage-engine.xml,coverage-server.xml,coverage-sdk.xml
65+
files: coverage-models.xml,coverage-engine.xml,coverage-telemetry.xml,coverage-server.xml,coverage-sdk.xml
6666
fail_ci_if_error: false
6767
token: ${{ secrets.CODECOV_TOKEN }}
6868

‎Makefile‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
.PHONY: help sync openapi-spec openapi-spec-check test test-extras test-all models-test test-models test-sdk lint lint-fix typecheck check build build-models build-server build-sdk publish publish-models publish-server publish-sdk hooks-install hooks-uninstall prepush evaluators-test evaluators-lint evaluators-lint-fix evaluators-typecheck evaluators-build galileo-test galileo-lint galileo-lint-fix galileo-typecheck galileo-build sdk-ts-generate sdk-ts-overlay-test sdk-ts-name-check sdk-ts-generate-check sdk-ts-build sdk-ts-test sdk-ts-lint sdk-ts-typecheck sdk-ts-release-check sdk-ts-publish-dry-run sdk-ts-publish
1+
.PHONY: help sync openapi-spec openapi-spec-check test test-extras test-all models-test test-models test-sdk lint lint-fix typecheck check build build-models build-server build-sdk publish publish-models publish-server publish-sdk hooks-install hooks-uninstall prepush evaluators-test evaluators-lint evaluators-lint-fix evaluators-typecheck evaluators-build galileo-test galileo-lint galileo-lint-fix galileo-typecheck galileo-build sdk-ts-generate sdk-ts-overlay-test sdk-ts-name-check sdk-ts-generate-check sdk-ts-build sdk-ts-test sdk-ts-lint sdk-ts-typecheck sdk-ts-release-check sdk-ts-publish-dry-run sdk-ts-publish telemetry-test telemetry-lint telemetry-lint-fix telemetry-typecheck telemetry-build telemetry-publish
22

33
# Workspace package names
44
PACK_MODELS := agent-control-models
55
PACK_SERVER := agent-control-server
66
PACK_SDK := agent-control
77
PACK_ENGINE := agent-control-engine
8+
PACK_TELEMETRY := agent-control-telemetry
89
PACK_EVALUATORS := agent-control-evaluators
910
OPENAPI_SPEC_PATH := server/.generated/openapi.json
1011

@@ -14,6 +15,7 @@ SERVER_DIR := server
1415
SDK_DIR := sdks/python
1516
TS_SDK_DIR := sdks/typescript
1617
ENGINE_DIR := engine
18+
TELEMETRY_DIR := telemetry
1719
EVALUATORS_DIR := evaluators/builtin
1820
GALILEO_DIR := evaluators/contrib/galileo
1921
UI_DIR := ui
@@ -31,7 +33,7 @@ help:
3133
@echo " make openapi-spec-check - verify OpenAPI generation succeeds"
3234
@echo ""
3335
@echo "Test:"
34-
@echo " make test - run tests for core packages (models, server, engine, sdk, evaluators)"
36+
@echo " make test - run tests for core packages (models, telemetry, server, engine, sdk, evaluators)"
3537
@echo " make models-test - run shared model tests with coverage"
3638
@echo " make test-extras - run tests for contrib evaluators (galileo, etc.)"
3739
@echo " make test-all - run all tests (core + extras)"
@@ -82,13 +84,16 @@ openapi-spec-check: openapi-spec
8284
# Test
8385
# ---------------------------
8486

85-
test: models-test server-test engine-test sdk-test evaluators-test
87+
test: models-test telemetry-test server-test engine-test sdk-test evaluators-test
8688

8789
models-test:
8890
cd $(MODELS_DIR) && uv run pytest --cov=src --cov-report=xml:../coverage-models.xml -q
8991

9092
test-models: models-test
9193

94+
telemetry-test:
95+
$(MAKE) -C $(TELEMETRY_DIR) test
96+
9297
# Run tests for contrib evaluators (not included in default test target)
9398
test-extras: galileo-test
9499

@@ -102,26 +107,35 @@ check: test lint typecheck
102107
# Quality
103108
# ---------------------------
104109

105-
lint: engine-lint evaluators-lint
110+
lint: engine-lint telemetry-lint evaluators-lint
106111
uv run --package $(PACK_MODELS) ruff check --config pyproject.toml models/src
107112
uv run --package $(PACK_SERVER) ruff check --config pyproject.toml server/src
108113
uv run --package $(PACK_SDK) ruff check --config pyproject.toml sdks/python/src
109114

110-
lint-fix: engine-lint-fix evaluators-lint-fix
115+
lint-fix: engine-lint-fix telemetry-lint-fix evaluators-lint-fix
111116
uv run --package $(PACK_MODELS) ruff check --config pyproject.toml --fix models/src
112117
uv run --package $(PACK_SERVER) ruff check --config pyproject.toml --fix server/src
113118
uv run --package $(PACK_SDK) ruff check --config pyproject.toml --fix sdks/python/src
114119

115-
typecheck: engine-typecheck evaluators-typecheck
120+
typecheck: engine-typecheck telemetry-typecheck evaluators-typecheck
116121
uv run --package $(PACK_MODELS) mypy --config-file pyproject.toml models/src
117122
uv run --package $(PACK_SERVER) mypy --config-file pyproject.toml server/src
118123
uv run --package $(PACK_SDK) mypy --config-file pyproject.toml sdks/python/src
119124

125+
telemetry-lint:
126+
$(MAKE) -C $(TELEMETRY_DIR) lint
127+
128+
telemetry-lint-fix:
129+
$(MAKE) -C $(TELEMETRY_DIR) lint-fix
130+
131+
telemetry-typecheck:
132+
$(MAKE) -C $(TELEMETRY_DIR) typecheck
133+
120134
# ---------------------------
121135
# Build / Publish
122136
# ---------------------------
123137

124-
build: build-models build-server build-sdk engine-build evaluators-build
138+
build: build-models build-server build-sdk engine-build telemetry-build evaluators-build
125139

126140
build-models:
127141
cd $(MODELS_DIR) && uv build
@@ -132,7 +146,10 @@ build-server:
132146
build-sdk:
133147
cd $(SDK_DIR) && uv build
134148

135-
publish: publish-models publish-server publish-sdk engine-publish
149+
telemetry-build:
150+
cd $(TELEMETRY_DIR) && uv build
151+
152+
publish: publish-models publish-server publish-sdk engine-publish telemetry-publish
136153

137154
publish-models:
138155
cd $(MODELS_DIR) && uv publish
@@ -143,6 +160,9 @@ publish-server:
143160
publish-sdk:
144161
cd $(SDK_DIR) && uv publish
145162

163+
telemetry-publish:
164+
cd $(TELEMETRY_DIR) && uv publish
165+
146166
# ---------------------------
147167
# Git hooks
148168
# ---------------------------

‎codecov.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ fixes:
33
- "agent_control_engine/::engine/src/agent_control_engine/"
44
- "agent_control_models/::models/src/agent_control_models/"
55
- "agent_control_evaluators/::evaluators/builtin/src/agent_control_evaluators/"
6+
- "agent_control_telemetry/::telemetry/src/agent_control_telemetry/"
67
- "agent_control/::sdks/python/src/agent_control/"

‎pyproject.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"models",
1919
"server",
2020
"sdks/python",
21+
"telemetry",
2122
"engine",
2223
"evaluators/builtin",
2324
# NOTE: evaluators/contrib/* excluded - install separately when needed
@@ -36,6 +37,7 @@ required-environments = [
3637
agent-control-models = { workspace = true }
3738
agent-control-engine = { workspace = true }
3839
agent-control-evaluators = { workspace = true }
40+
agent-control-telemetry = { workspace = true }
3941

4042
[tool.ruff]
4143
line-length = 100
@@ -68,6 +70,7 @@ version_toml = [
6870
"models/pyproject.toml:project.version",
6971
"engine/pyproject.toml:project.version",
7072
"sdks/python/pyproject.toml:project.version",
73+
"telemetry/pyproject.toml:project.version",
7174
"server/pyproject.toml:project.version",
7275
"evaluators/builtin/pyproject.toml:project.version",
7376
"evaluators/contrib/galileo/pyproject.toml:project.version",

‎scripts/build.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"""Build packages for PyPI distribution.
33
44
This script builds all publishable packages. For SDK and server, it copies internal
5-
packages (models, engine) into the source directories before building, then cleans up
6-
afterward. This allows the published wheels to be self-contained.
5+
packages (models, engine, telemetry) into the source directories before building,
6+
then cleans up afterward. This allows the published wheels to be self-contained.
77
88
Usage:
99
python scripts/build.py [models|evaluators|sdk|server|galileo|all]
@@ -81,7 +81,7 @@ def build_sdk() -> None:
8181
print(f"Building agent-control-sdk v{version}")
8282

8383
# Clean previous builds and vendored code
84-
for pkg in ["agent_control_models", "agent_control_engine"]:
84+
for pkg in ["agent_control_models", "agent_control_engine", "agent_control_telemetry"]:
8585
target = sdk_src / pkg
8686
if target.exists():
8787
shutil.rmtree(target)
@@ -99,6 +99,10 @@ def build_sdk() -> None:
9999
ROOT / "engine" / "src" / "agent_control_engine",
100100
sdk_src / "agent_control_engine",
101101
)
102+
shutil.copytree(
103+
ROOT / "telemetry" / "src" / "agent_control_telemetry",
104+
sdk_src / "agent_control_telemetry",
105+
)
102106

103107
# Inject bundle metadata for conflict detection
104108
inject_bundle_metadata(
@@ -111,6 +115,11 @@ def build_sdk() -> None:
111115
"agent-control-sdk",
112116
version,
113117
)
118+
inject_bundle_metadata(
119+
sdk_src / "agent_control_telemetry" / "__init__.py",
120+
"agent-control-sdk",
121+
version,
122+
)
114123

115124
# Set version
116125
set_package_version(sdk_dir / "pyproject.toml", version)
@@ -120,7 +129,7 @@ def build_sdk() -> None:
120129
print(f" Built agent-control-sdk v{version}")
121130
finally:
122131
# Clean up vendored code (don't commit it)
123-
for pkg in ["agent_control_models", "agent_control_engine"]:
132+
for pkg in ["agent_control_models", "agent_control_engine", "agent_control_telemetry"]:
124133
target = sdk_src / pkg
125134
if target.exists():
126135
shutil.rmtree(target)
@@ -139,7 +148,7 @@ def build_server() -> None:
139148
print(f"Building agent-control-server v{version}")
140149

141150
# Clean previous builds and vendored code
142-
for pkg in ["agent_control_models", "agent_control_engine"]:
151+
for pkg in ["agent_control_models", "agent_control_engine", "agent_control_telemetry"]:
143152
target = server_src / pkg
144153
if target.exists():
145154
shutil.rmtree(target)
@@ -148,7 +157,7 @@ def build_server() -> None:
148157
if dist_dir.exists():
149158
shutil.rmtree(dist_dir)
150159

151-
# Copy vendored packages (models and engine only, NOT evaluators)
160+
# Copy vendored packages (models, engine, and telemetry only, NOT evaluators)
152161
shutil.copytree(
153162
ROOT / "models" / "src" / "agent_control_models",
154163
server_src / "agent_control_models",
@@ -157,6 +166,10 @@ def build_server() -> None:
157166
ROOT / "engine" / "src" / "agent_control_engine",
158167
server_src / "agent_control_engine",
159168
)
169+
shutil.copytree(
170+
ROOT / "telemetry" / "src" / "agent_control_telemetry",
171+
server_src / "agent_control_telemetry",
172+
)
160173

161174
# Inject bundle metadata for conflict detection
162175
inject_bundle_metadata(
@@ -169,6 +182,11 @@ def build_server() -> None:
169182
"agent-control-server",
170183
version,
171184
)
185+
inject_bundle_metadata(
186+
server_src / "agent_control_telemetry" / "__init__.py",
187+
"agent-control-server",
188+
version,
189+
)
172190

173191
# Set version
174192
set_package_version(server_dir / "pyproject.toml", version)
@@ -178,7 +196,7 @@ def build_server() -> None:
178196
print(f" Built agent-control-server v{version}")
179197
finally:
180198
# Clean up vendored code (don't commit it)
181-
for pkg in ["agent_control_models", "agent_control_engine"]:
199+
for pkg in ["agent_control_models", "agent_control_engine", "agent_control_telemetry"]:
182200
target = server_src / pkg
183201
if target.exists():
184202
shutil.rmtree(target)

‎sdks/python/pyproject.toml‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name = "agent-control-sdk"
33
version = "7.3.2"
44
description = "Python SDK for Agent Control - protect your AI agents with controls"
55
requires-python = ">=3.12"
6-
# Note: agent-control-models and agent-control-engine are bundled at build time
6+
# Note: agent-control-models, agent-control-engine, and agent-control-telemetry
7+
# are bundled at build time
78
# Note: agent-control-evaluators is a runtime dependency (NOT vendored) to avoid
89
# duplicate module conflict when galileo extras are installed
910
dependencies = [
@@ -48,6 +49,7 @@ dev = [
4849
"mypy>=1.8.0",
4950
"agent-control-models",
5051
"agent-control-engine",
52+
"agent-control-telemetry",
5153
"agent-control-evaluators",
5254
"strands-agents>=1.26.0", # For strands integration tests
5355
]
@@ -57,8 +59,14 @@ requires = ["hatchling"]
5759
build-backend = "hatchling.build"
5860

5961
[tool.hatch.build.targets.wheel]
60-
# Note: agent_control_models and agent_control_engine are copied by scripts/build.py
61-
packages = ["src/agent_control", "src/agent_control_models", "src/agent_control_engine"]
62+
# Note: agent_control_models, agent_control_engine, and agent_control_telemetry
63+
# are copied by scripts/build.py
64+
packages = [
65+
"src/agent_control",
66+
"src/agent_control_models",
67+
"src/agent_control_engine",
68+
"src/agent_control_telemetry",
69+
]
6270

6371
[tool.ruff]
6472
line-length = 100
@@ -80,6 +88,7 @@ known-first-party = ["agent_control"]
8088
[tool.uv.sources]
8189
agent-control-models = { workspace = true }
8290
agent-control-engine = { workspace = true }
91+
agent-control-telemetry = { workspace = true }
8392
agent-control-evaluators = { workspace = true }
8493
# For local dev: use local galileo package instead of PyPI
8594
agent-control-evaluator-galileo = { path = "../../evaluators/contrib/galileo", editable = true }

‎sdks/python/src/agent_control/__init__.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ async def handle_input(user_message: str) -> str:
6868
Step,
6969
StepSchema,
7070
)
71+
from agent_control_telemetry.trace_context import (
72+
clear_trace_context_provider,
73+
get_trace_context_from_provider,
74+
set_trace_context_provider,
75+
)
7176

7277
from . import agents, controls, evaluation, evaluators, policies
7378
from ._control_registry import (
@@ -86,18 +91,15 @@ async def handle_input(user_message: str) -> str:
8691
add_event,
8792
configure_logging,
8893
get_event_batcher,
94+
get_event_sink,
8995
get_log_config,
9096
get_logger,
9197
init_observability,
9298
is_observability_enabled,
9399
log_control_evaluation,
94100
shutdown_observability,
95101
sync_shutdown_observability,
96-
)
97-
from .telemetry import (
98-
clear_trace_context_provider,
99-
get_trace_context_from_provider,
100-
set_trace_context_provider,
102+
write_events,
101103
)
102104
from .tracing import (
103105
get_current_span_id,
@@ -1310,9 +1312,11 @@ async def main():
13101312
# Observability
13111313
"init_observability",
13121314
"add_event",
1315+
"write_events",
13131316
"shutdown_observability",
13141317
"is_observability_enabled",
13151318
"get_event_batcher",
1319+
"get_event_sink",
13161320
"configure_logging",
13171321
"get_log_config",
13181322
"log_control_evaluation",

‎sdks/python/src/agent_control/evaluation_events.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
EvaluationResponse,
1212
)
1313

14-
from .observability import add_event, get_logger, is_observability_enabled
14+
from .observability import get_logger, is_observability_enabled, write_events
1515

1616
_logger = get_logger(__name__)
1717

@@ -210,5 +210,4 @@ def enqueue_observability_events(events: list[ControlExecutionEvent]) -> None:
210210
if not is_observability_enabled():
211211
return
212212

213-
for event in events:
214-
add_event(event)
213+
write_events(events)

0 commit comments

Comments
 (0)