--- name: ak-dev-new-tracing-provider description: > Step-by-step guide for adding a new observability/tracing provider to Agent Kernel. Use this skill when you need to integrate a new tracing backend (beyond Langfuse, OpenLLMetry/Traceloop, Pydantic Logfire, and AWS CloudWatch). Covers implementing the BaseTrace interface, creating framework-specific traced runners, configuration, and testing. license: Apache-2.0 metadata: author: yaalalabs category: developer --- # Adding a New Tracing Provider This guide walks through adding a new observability/tracing provider to Agent Kernel. Use the Langfuse implementation (`ak-py/src/agentkernel/trace/langfuse/`) as the canonical reference. For a backend reached through plain OpenTelemetry (no vendor SDK), see the CloudWatch provider (`ak-py/src/agentkernel/trace/cloudwatch/`): it installs its own SDK `TracerProvider` and OTLP exporter once (or reuses one already installed, since OpenTelemetry honours only the first), wraps each run through a `span(name, session)` context manager on the provider class that its runners receive, and adds a provider-specific helper module (`sigv4.py`) beside the runners. ## Architecture Overview Agent Kernel's tracing system: 1. **`BaseTrace`** (`trace/base.py`) defines the interface — one method per supported framework that returns a traced `Runner` (or `None`) 2. **`Trace`** (`trace/trace.py`) is a factory that creates the appropriate trace instance based on `AKConfig.trace.type` 3. Each **framework Module** checks for a trace runner at initialization — if tracing is enabled, it uses the traced runner instead of the default 4. Traced runners **extend the base framework runner** and wrap execution with spans/traces ## Step-by-Step ### 1. Create the Trace Provider Directory ``` ak-py/src/agentkernel/trace// ├── __init__.py ├── .py # Main trace class ├── openai.py # Traced OpenAI runner ├── langgraph.py # Traced LangGraph runner ├── crewai.py # Traced CrewAI runner ├── adk.py # Traced Google ADK runner ├── smolagents.py # Traced Smolagents runner └── pydanticai.py # Traced Pydantic AI runner ``` ### 2. Implement the Main Trace Class In the main trace class, there should be a method each agentic framework. Each method should return a traced Runner if the framework is supported, or None if not. The traced Runner should extend the base Runner for that framework and wrap execution with tracing spans. ```python # ak-py/src/agentkernel/trace//.py import logging from agentkernel.core.base import Runner from agentkernel.trace.base import BaseTrace logger = logging.getLogger("ak.trace.") class (BaseTrace): """ tracing implementation for Agent Kernel.""" def __init__(self): logger.info("Initializing tracing") # Initialize the tracing client/SDK # e.g., self._client = ProviderClient() def init(self): """Initialize the tracing backend. Called once at startup.""" # Set up any global instrumentation # e.g., self._client.configure(api_key=os.getenv("PROVIDER_API_KEY")) pass def openai(self) -> Runner | None: """Return a traced runner for OpenAI framework, or None if not supported.""" try: from .openai import OpenAIRunner return OpenAIRunner(self._client) except ImportError: logger.warning("OpenAI tracing dependencies not available") return None def langgraph(self) -> Runner | None: try: from .langgraph import LangGraphRunner return LangGraphRunner(self._client) except ImportError: return None def crewai(self) -> Runner | None: try: from .crewai import CrewAIRunner return CrewAIRunner(self._client) except ImportError: return None def adk(self) -> Runner | None: try: from .adk import ADKRunner return ADKRunner(self._client) except ImportError: return None def smolagents(self) -> Runner: from .smolagents import SmolagentsRunner return SmolagentsRunner(self._client) ``` ### 3. Implement Framework-Specific Traced Runners Each traced runner **extends the base framework runner** and wraps execution with tracing spans. #### OpenAI Traced Runner ```python # ak-py/src/agentkernel/trace//openai.py from agentkernel.framework.openai.openai import OpenAIRunner from agentkernel.core.base import Session from agentkernel.core.model import AgentReply, AgentRequest class OpenAIRunner(OpenAIRunner): def __init__(self, client): super().__init__() self._trace_client = client async def run(self, agent, session: Session, requests: list[AgentRequest]) -> AgentReply: # Wrap the base runner's execution with a trace span with self._trace_client.start_span( name=f"agent.{agent.name}", attributes={ "framework": "openai", "session_id": session.id, "agent_name": agent.name, } ) as span: try: result = await super().run(agent, session, requests) span.set_attribute("output_length", len(result.response) if hasattr(result, 'response') else 0) span.set_status("OK") return result except Exception as e: span.set_status("ERROR") span.record_exception(e) raise ``` #### LangGraph Traced Runner ```python # ak-py/src/agentkernel/trace//langgraph.py from agentkernel.framework.langgraph.langgraph import LangGraphRunner class LangGraphRunner(LangGraphRunner): def __init__(self, client): super().__init__() self._trace_client = client async def run(self, agent, session, requests): with self._trace_client.start_span( name=f"agent.{agent.name}", attributes={"framework": "langgraph", "session_id": session.id} ): return await super().run(agent, session, requests) ``` Follow the same pattern for CrewAI, Google ADK, Smolagents, and Pydantic AI runners (see `trace/langfuse/smolagents.py` and `trace/openllmetry/smolagents.py` for reference). ### 4. Update the `__init__.py` ```python # ak-py/src/agentkernel/trace//__init__.py from . import ``` ### 5. Update the BaseTrace Interface Add the new provider as a recognized option. The `BaseTrace` class (`trace/base.py`) already defines the interface — your implementation just needs to conform to it. No changes to `base.py` are needed unless you're adding a new framework. Note that `init()` and all six framework methods (`openai`, `langgraph`, `crewai`, `adk`, `smolagents`, `pydanticai`) are declared `@abstractmethod` on `BaseTrace`, so every new provider must implement all seven — otherwise the class cannot be instantiated. ### 6. Register with the Trace Factory Update `ak-py/src/agentkernel/trace/trace.py`. The factory shares the house pluggable-backend shape from `core/util/factory.py` (`resolve_dotted`, `require_extra`, `AKConfigError` — the same pattern used by the guardrail, session/thread/multimodal store, and sandbox provider factories): `Trace.get()` builds an instance via `Trace._build()` only when tracing is enabled, each built-in's lazy import is wrapped in `require_extra` (so a missing optional dependency raises an actionable `ImportError` naming the pip extra), and anything that isn't a recognized short name is treated as a dotted path to a `BaseTrace` subclass (bring-your-own). When tracing is disabled, `instance` stays `None` and the factory returns `Trace(None)`, whose `init()` and framework methods no-op / return `None`: ```python _BUILTIN_TRACERS = ["langfuse", "openllmetry", "logfire", "cloudwatch"] class Trace(BaseTrace): @classmethod def get(cls) -> "Trace": config = AKConfig.get() instance = cls._build(config.trace.type) if config.trace.enabled else None trace = cls(instance) trace.init() return trace @staticmethod def _build(trace_type: str) -> BaseTrace: if trace_type == "langfuse": with require_extra("langfuse", "trace.type: langfuse"): from .langfuse.langfuse import LangFuse return LangFuse() if trace_type == "openllmetry": with require_extra("openllmetry", "trace.type: openllmetry"): from .openllmetry.openllmetry import OpenLLMetry return OpenLLMetry() if trace_type == "logfire": with require_extra("logfire", "trace.type: logfire"): from .logfire.logfire import Logfire return Logfire() if trace_type == "cloudwatch": with require_extra("cloudwatch", "trace.type: cloudwatch"): from .cloudwatch.cloudwatch import CloudWatch return CloudWatch() if trace_type == "": # ADD THIS with require_extra("", "trace.type: "): from .. import return () if "." not in trace_type: raise AKConfigError( f"unknown trace type '{trace_type}'; expected one of {_BUILTIN_TRACERS} or a dotted path to a BaseTrace subclass" ) return resolve_dotted(trace_type, base=BaseTrace)() # bring-your-own ``` A dotted `type` (e.g. `myorg.tracing.CustomTrace`) resolves via `resolve_dotted` without any factory edit at all — only add an `if` branch here for a first-party, in-repo provider you want addressable by a short name. ### 7. Add Configuration The existing `_TraceConfig.type` in `config.py` is a free-form string (no regex pattern) described as "a built-in short name (langfuse, openllmetry, logfire, cloudwatch) or a dotted path to a BaseTrace subclass" — do not add a `pattern=` constraint, since that would break the bring-your-own path. Your provider needs to respond to `type: ""`: ```yaml # config.yaml trace: enabled: true type: ``` Add provider-specific environment variables as needed (e.g., `PROVIDER_API_KEY`). ### 8. Add Optional Dependencies In `ak-py/pyproject.toml`: ```toml [project.optional-dependencies] = [ "provider-sdk>=x.y.z", # Add any framework-specific instrumentation packages ] ``` ### 9. Add Tests Create `ak-py/tests/test_trace_.py`, and add a missing-extra test to `tests/test_trace.py` asserting the friendly `agentkernel[]` `ImportError`. Two existing files show the two styles: `test_trace_logfire.py` injects a fake SDK module into `sys.modules` (the SDK is not a test dependency), and `test_trace_cloudwatch.py` records spans with a real OpenTelemetry SDK `TracerProvider` and `InMemorySpanExporter` while patching `trace.get_tracer_provider` / `set_tracer_provider` (the global provider is settable once per process, so a test must never install one). Cover: - Test that the factory creates the correct instance for `type: ""` - Test that traced runners properly wrap execution with spans - Test that errors are recorded in spans - Use mocks for the tracing client ### 10. Add Documentation Add `docs/docs/advanced/tracing-.md` covering: - Provider setup (API keys, dashboard URL) - Configuration - What gets traced (spans, attributes) - Dashboard screenshots (optional) Then update the landing page inventories in `docs/src/components/*/data.tsx`: add a tile to the **Observability, safety & testing** row in `IntegrationsMarquee/data.tsx` (role `Tracing`, `href` to the provider's docs page, logo under `docs/static/img/integrations/` or a `react-icons/si` glyph); add the provider to the **Tracing** card's `tags` and `description` under the Observe tab in `FeatureExplorer/data.tsx`; optionally add `pick("")` to the **Clouds & observability** card in `ArchitectureOverview/data.tsx` if it is a headline backend. Logo sourcing and the build check are in `ak-dev-sync-docs-from-branch`, *Docs-Site Landing and Features Pages*. Then add the provider to the docs-site features page (`docs/src/pages/features.tsx`): the Observability card's `highlights` list one entry per provider, and the Problem section's `rows` name the built-in tracing providers in a `with:` cell. Grep `docs/src/pages/*.tsx` for "Langfuse" to find every roll call. ## How Framework Modules Consume Tracing Each framework Module's constructor checks for tracing: ```python class OpenAIModule(Module): def __init__(self, agents): super().__init__() trace_runner = Trace.get().openai() # Returns traced Runner or None self.runner = trace_runner if trace_runner else OpenAIRunner() self.load(agents) ``` This means tracing is **transparent** — users don't change their agent code, they just add `trace` config. ## Checklist - [ ] `ak-py/src/agentkernel/trace//` directory with `__init__.py` and main class - [ ] Traced runners for each framework (OpenAI, LangGraph, CrewAI, ADK, Smolagents, Pydantic AI) - [ ] Registration in `trace/trace.py` factory - [ ] Configuration via `type: ""` in `config.yaml` - [ ] Optional dependencies in `pyproject.toml` - [ ] Tests for factory creation and span wrapping - [ ] Documentation in `docs/docs/advanced/tracing-.md` - [ ] Landing page inventories: marquee tile (`IntegrationsMarquee/data.tsx`), Tracing card tags (`FeatureExplorer/data.tsx`); features page Observability highlights and `with:` cells