From ef8e65e4dd843c8cc943ad82646407cd8884200e Mon Sep 17 00:00:00 2001 From: Gary <59334078+garrettallen14@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:18:09 -0700 Subject: [PATCH] Add layerlens.instrument documentation --- docs/SUMMARY.md | 7 + docs/api-reference/README.md | 18 +- docs/api-reference/instrumentation.md | 249 ++++++++++++++++++++++++++ docs/instrumentation/README.md | 75 ++++++++ docs/instrumentation/frameworks.md | 170 ++++++++++++++++++ docs/instrumentation/providers.md | 222 +++++++++++++++++++++++ docs/instrumentation/quickstart.md | 171 ++++++++++++++++++ 7 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 docs/api-reference/instrumentation.md create mode 100644 docs/instrumentation/README.md create mode 100644 docs/instrumentation/frameworks.md create mode 100644 docs/instrumentation/providers.md create mode 100644 docs/instrumentation/quickstart.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7d2c039..99166dd 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -19,8 +19,15 @@ * [Traces](api-reference/traces.md) * [Trace Evaluations](api-reference/trace-evaluations.md) * [Judge Optimizations](api-reference/judge-optimizations.md) + * [Instrumentation](api-reference/instrumentation.md) * [Error Handling](api-reference/errors.md) +## Instrumentation +* [Overview](instrumentation/README.md) + * [Quick Start](instrumentation/quickstart.md) + * [LLM Providers](instrumentation/providers.md) + * [Agent Frameworks](instrumentation/frameworks.md) + ## CLI * [Getting Started](cli/getting-started.md) * [Command Reference](cli/commands.md) diff --git a/docs/api-reference/README.md b/docs/api-reference/README.md index d91fcaa..f5461d7 100644 --- a/docs/api-reference/README.md +++ b/docs/api-reference/README.md @@ -1,2 +1,18 @@ -# api-reference +# API Reference + +Detailed documentation for every resource and method in the LayerLens Stratix Python SDK. + +## Resources + +- [Client Configuration](client.md) — `Stratix` and `AsyncStratix` setup +- [Public Client](public-client.md) — Public models, benchmarks, evaluations +- [Evaluations](evaluations.md) — Create and manage evaluations +- [Results](results.md) — Retrieve evaluation results +- [Models & Benchmarks](models-benchmarks.md) — Model and benchmark management +- [Judges](judges.md) — Evaluation judge CRUD +- [Traces](traces.md) — Upload and manage trace data +- [Trace Evaluations](trace-evaluations.md) — Run judges against traces +- [Judge Optimizations](judge-optimizations.md) — Optimize judge configurations +- [Instrumentation](instrumentation.md) — Tracing primitives and adapters +- [Error Handling](errors.md) — Exception hierarchy and handling patterns diff --git a/docs/api-reference/instrumentation.md b/docs/api-reference/instrumentation.md new file mode 100644 index 0000000..a5f4de3 --- /dev/null +++ b/docs/api-reference/instrumentation.md @@ -0,0 +1,249 @@ +# Instrumentation + +The `layerlens.instrument` module provides tracing primitives and provider/framework adapters for automatic LLM observability. + +## Overview + +### Using Synchronous Client + +```python +from layerlens import Stratix +from layerlens.instrument import trace, span + +client = Stratix() + +@trace(client) +def my_agent(query: str): + with span("process", kind="internal") as s: + result = do_work(query) + s.output = result + return result + +my_agent("Hello") +``` + +### Using Async Client + +```python +import asyncio +from layerlens import AsyncStratix +from layerlens.instrument import trace, span + +client = AsyncStratix() + +@trace(client) +async def my_agent(query: str): + with span("process") as s: + result = await do_work(query) + s.output = result + return result + +asyncio.run(my_agent("Hello")) +``` + +## Core API + +### `trace(client, name=None, metadata=None)` + +Decorator that creates a root span and uploads the trace on function completion. + +#### Parameters + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `client` | `Stratix \| AsyncStratix` | Yes | SDK client used to upload the trace | +| `name` | `str \| None` | No | Override span name (defaults to function name) | +| `metadata` | `dict \| None` | No | Arbitrary metadata attached to the root span | + +#### Behavior + +- Creates a `TraceRecorder` and root `SpanData` +- Sets `_current_recorder` and `_current_span` context variables +- Captures function arguments as `input` +- Captures return value as `output` +- On error: sets `status="error"` and records the error message +- On completion: serializes span tree to a temp JSON file, calls `client.traces.upload()`, deletes the temp file +- Resets context variables in a `finally` block +- Works with both sync and async functions + +#### Example + +```python +@trace(client) +def my_agent(query: str): + return process(query) + +@trace(client, name="custom-name") +async def my_async_agent(query: str): + return await process(query) +``` + +### `span(name, kind="internal", input=None, metadata=None)` + +Context manager that creates a child span under the current active span. + +#### Parameters + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | `str` | Yes | Display name for the span | +| `kind` | `str` | No | Span type: `"internal"`, `"llm"`, `"retriever"`, `"tool"`, `"chain"` | +| `input` | `Any` | No | Input data for the span | +| `metadata` | `dict \| None` | No | Arbitrary metadata attached to the span | + +#### Returns + +Returns a `SpanData` object (or a no-op dummy if no trace is active). + +#### Behavior + +- If called outside a `@trace` context, returns a no-op context manager +- Creates a `SpanData` with the given name and kind +- Appends the span to the current parent's `children` list +- Sets `_current_span` to the new span for the duration of the `with` block +- Restores the previous span on exit +- On error inside the block: sets `status="error"`, records error, re-raises + +#### Example + +```python +@trace(client) +def my_agent(query: str): + with span("step-1", kind="tool") as s: + s.input = query + result = tool_call(query) + s.output = result + s.metadata["tool_version"] = "1.0" + return result +``` + +### `SpanData` + +Dataclass representing a single span in the trace tree. + +#### Properties + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `name` | `str` | (required) | Span display name | +| `span_id` | `str` | auto-generated | Unique identifier (UUID hex, 16 chars) | +| `parent_id` | `str \| None` | `None` | Parent span ID | +| `start_time` | `float` | `time.time()` | Unix timestamp | +| `end_time` | `float \| None` | `None` | Unix timestamp when finished | +| `status` | `str` | `"ok"` | `"ok"` or `"error"` | +| `kind` | `str` | `"internal"` | Span type | +| `input` | `Any` | `None` | Input data | +| `output` | `Any` | `None` | Output data | +| `error` | `str \| None` | `None` | Error message | +| `metadata` | `dict` | `{}` | Arbitrary key-value metadata | +| `children` | `list[SpanData]` | `[]` | Child spans | + +#### Methods + +##### `finish(error=None)` + +Sets `end_time` to the current time. If `error` is provided, sets `status="error"` and records the error message. + +##### `to_dict()` + +Serializes the span tree to a JSON-compatible dictionary, recursively including all children. + +### `TraceRecorder` + +Collects the span tree and handles flushing to the LayerLens API. + +#### Methods + +##### `flush()` + +Serializes the root span tree to a temporary JSON file, calls `client.traces.upload(path)`, and deletes the temp file. Used by the `@trace` decorator for sync functions. + +##### `async_flush()` + +Async version of `flush()`. Used by the `@trace` decorator for async functions. + +## Provider Adapters + +### `instrument_openai(client)` + +Monkey-patches `client.chat.completions.create` on an OpenAI client instance. + +```python +from layerlens.instrument.adapters.providers.openai import instrument_openai + +instrument_openai(openai_client) +``` + +#### Classes + +| Class | Description | +| ----- | ----------- | +| `OpenAIProvider` | Provider adapter with `connect_client()` / `disconnect()` | + +### `instrument_anthropic(client)` + +Monkey-patches `client.messages.create` on an Anthropic client instance. + +```python +from layerlens.instrument.adapters.providers.anthropic import instrument_anthropic + +instrument_anthropic(anthropic_client) +``` + +#### Classes + +| Class | Description | +| ----- | ----------- | +| `AnthropicProvider` | Provider adapter with `connect_client()` / `disconnect()` | + +### `instrument_litellm()` + +Monkey-patches `litellm.completion` and `litellm.acompletion` at the module level. + +```python +from layerlens.instrument.adapters.providers.litellm import instrument_litellm, uninstrument_litellm + +instrument_litellm() # Patch +uninstrument_litellm() # Restore +``` + +## Framework Adapters + +### `LangChainCallbackHandler(client)` + +LangChain `BaseCallbackHandler` implementation that builds a span tree from chain/LLM/tool/retriever events. + +```python +from layerlens.instrument.adapters.frameworks.langchain import LangChainCallbackHandler + +handler = LangChainCallbackHandler(client) +chain.invoke(input, config={"callbacks": [handler]}) +``` + +#### Supported Callbacks + +| Callback | Span Kind | +| -------- | --------- | +| `on_chain_start` / `on_chain_end` / `on_chain_error` | `chain` | +| `on_llm_start` / `on_llm_end` / `on_llm_error` | `llm` | +| `on_chat_model_start` | `llm` | +| `on_tool_start` / `on_tool_end` / `on_tool_error` | `tool` | +| `on_retriever_start` / `on_retriever_end` / `on_retriever_error` | `retriever` | + +### `LangGraphCallbackHandler(client)` + +Extends `LangChainCallbackHandler` with LangGraph node name extraction. + +```python +from layerlens.instrument.adapters.frameworks.langgraph import LangGraphCallbackHandler + +handler = LangGraphCallbackHandler(client) +graph.invoke(input, config={"callbacks": [handler]}) +``` + +Extracts node names from `metadata.langgraph_node` or plain tags (skipping internal `graph:step:*` tags). + +## Next Steps + +- [Instrumentation Guide](../instrumentation/README.md) for usage patterns and examples +- [Traces API Reference](traces.md) for the underlying upload mechanism diff --git a/docs/instrumentation/README.md b/docs/instrumentation/README.md new file mode 100644 index 0000000..4d37402 --- /dev/null +++ b/docs/instrumentation/README.md @@ -0,0 +1,75 @@ +# Instrumentation + +The `layerlens.instrument` module provides automatic tracing for LLM applications. It captures execution spans — function calls, LLM requests, tool invocations — as a tree structure and uploads them as traces to LayerLens for evaluation. + +## How It Works + +1. **`@trace(client)`** wraps a function as the root of a trace. When the function completes, the span tree is serialized to JSON and uploaded via `client.traces.upload()`. +2. **`span()`** creates child spans inside a traced function. Spans nest automatically using Python's `contextvars`. +3. **Provider adapters** (OpenAI, Anthropic, LiteLLM) monkey-patch SDK methods to create LLM spans automatically — no code changes needed inside your functions. +4. **Framework adapters** (LangChain, LangGraph) plug in as callback handlers to capture chain/tool/retriever spans from agent frameworks. + +## Quick Example + +```python +from layerlens import Stratix +from layerlens.instrument import trace, span +from layerlens.instrument.adapters.providers.openai import instrument_openai + +client = Stratix() + +# Auto-instrument OpenAI — all chat.completions.create calls +# inside a @trace will generate LLM spans automatically +import openai +openai_client = openai.OpenAI() +instrument_openai(openai_client) + +@trace(client) +def my_agent(question: str): + with span("retrieve", kind="retriever") as s: + docs = search(question) + s.output = docs + + response = openai_client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": f"Context: {docs}"}, + {"role": "user", "content": question}, + ], + ) + return response.choices[0].message.content + +my_agent("What is retrieval-augmented generation?") +``` + +This produces a trace with three spans: + +``` +my_agent (root, kind=internal) +├── retrieve (kind=retriever) +└── openai.chat.completions.create (kind=llm, auto-captured) +``` + +## Guides + +- [Quick Start](quickstart.md) — `@trace`, `span()`, and manual instrumentation +- [LLM Providers](providers.md) — Auto-instrument OpenAI, Anthropic, and LiteLLM +- [Agent Frameworks](frameworks.md) — LangChain and LangGraph callback handlers + +## Key Concepts + +| Concept | Description | +| ------- | ----------- | +| **Trace** | A complete execution tree, rooted at a `@trace`-decorated function | +| **Span** | A single unit of work within a trace (function call, LLM request, tool use) | +| **Kind** | Span type: `internal`, `llm`, `retriever`, `tool`, `chain` | +| **Provider adapter** | Monkey-patches an LLM SDK to emit `llm` spans automatically | +| **Framework adapter** | Callback handler that captures spans from agent frameworks | + +## No-Op Safety + +All instrumentation is no-op safe: + +- Provider adapters pass through to the original SDK method when called outside a `@trace` context +- `span()` returns a dummy context manager when called outside a `@trace` context +- No performance overhead when instrumentation is not active diff --git a/docs/instrumentation/frameworks.md b/docs/instrumentation/frameworks.md new file mode 100644 index 0000000..6528ca9 --- /dev/null +++ b/docs/instrumentation/frameworks.md @@ -0,0 +1,170 @@ +# Agent Framework Instrumentation + +Framework adapters plug into agent frameworks as callback handlers. Unlike provider adapters (which monkey-patch SDK methods), framework adapters receive events from the framework and build span trees from them. + +## Supported Frameworks + +| Framework | Adapter | Integration | +| --------- | ------- | ----------- | +| LangChain | `LangChainCallbackHandler` | Pass as a callback handler | +| LangGraph | `LangGraphCallbackHandler` | Pass as a callback handler | + +## LangChain + +### Installation + +```bash +pip install layerlens[langchain] +``` + +### Usage + +```python +from layerlens import Stratix +from layerlens.instrument.adapters.frameworks.langchain import LangChainCallbackHandler + +client = Stratix() +handler = LangChainCallbackHandler(client) + +# Pass the handler to any LangChain runnable +chain = prompt | llm | parser +result = chain.invoke( + {"question": "What is RAG?"}, + config={"callbacks": [handler]}, +) +``` + +The handler automatically captures: + +| Event | Span Kind | Captured Data | +| ----- | --------- | ------------- | +| Chain start/end | `chain` | Chain name, input, output | +| LLM start/end | `llm` | Model name, prompts, response, token usage | +| Tool start/end | `tool` | Tool name, input query, output | +| Retriever start/end | `retriever` | Query, retrieved documents | + +### How It Works + +LangChain provides `run_id` (UUID) and `parent_run_id` for every callback event. The handler uses these to build a span tree: + +1. `on_chain_start` — creates a root span (or child span if `parent_run_id` exists) +2. `on_llm_start` / `on_tool_start` / `on_retriever_start` — creates child spans +3. `on_*_end` — finishes the span with output data +4. `on_*_error` — finishes the span with `status="error"` +5. When the root chain ends — the full span tree is flushed as a trace + +### Example: RAG Chain + +```python +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI +from langchain_core.output_parsers import StrOutputParser + +from layerlens import Stratix +from layerlens.instrument.adapters.frameworks.langchain import LangChainCallbackHandler + +client = Stratix() +handler = LangChainCallbackHandler(client) + +prompt = ChatPromptTemplate.from_template("Answer: {question}") +llm = ChatOpenAI(model="gpt-4o") +chain = prompt | llm | StrOutputParser() + +result = chain.invoke( + {"question": "What is retrieval-augmented generation?"}, + config={"callbacks": [handler]}, +) +``` + +This produces a trace like: + +``` +RunnableSequence (kind=chain) +├── ChatPromptTemplate (kind=chain) +├── ChatOpenAI (kind=llm) +│ metadata: {model: "gpt-4o", usage: {total_tokens: 150}} +└── StrOutputParser (kind=chain) +``` + +### Error Handling + +Chain and LLM errors are captured automatically: + +```python +handler = LangChainCallbackHandler(client) + +try: + chain.invoke(input, config={"callbacks": [handler]}) +except Exception: + pass # Trace still uploads with error spans +``` + +## LangGraph + +The LangGraph adapter extends the LangChain handler with graph node awareness. + +### Installation + +```bash +pip install layerlens[langchain] +``` + +### Usage + +```python +from layerlens import Stratix +from layerlens.instrument.adapters.frameworks.langgraph import LangGraphCallbackHandler + +client = Stratix() +handler = LangGraphCallbackHandler(client) + +# Use with a LangGraph compiled graph +result = graph.invoke( + {"messages": [{"role": "user", "content": "Hello"}]}, + config={"callbacks": [handler]}, +) +``` + +### Node Name Extraction + +LangGraph attaches metadata to chain events that identifies which graph node is executing. The adapter extracts this to produce cleaner span names: + +- Checks `metadata.langgraph_node` for the node name (highest priority) +- Falls back to the first plain tag (no colon), skipping internal `graph:step:*` tags +- Uses the chain name from `serialized` if neither is present + +This means your traces show meaningful names like `agent`, `tools`, `retrieve` instead of generic `RunnableSequence` spans. + +### Example Trace Output + +``` +StateGraph (kind=chain) +├── agent (kind=chain, node) +│ └── ChatOpenAI (kind=llm) +├── tools (kind=chain, node) +│ └── search (kind=tool) +└── agent (kind=chain, node) + └── ChatOpenAI (kind=llm) +``` + +## Framework vs Provider Adapters + +You can use both together. For example, use the LangChain callback handler for span tree structure, and a provider adapter to enrich LLM spans with token usage: + +```python +from layerlens.instrument.adapters.providers.openai import instrument_openai +from layerlens.instrument.adapters.frameworks.langchain import LangChainCallbackHandler + +# Both can be active simultaneously +instrument_openai(openai_client) +handler = LangChainCallbackHandler(client) + +chain.invoke(input, config={"callbacks": [handler]}) +``` + +Note: When using both, you may get duplicate LLM spans (one from the provider adapter, one from the framework callback). In most cases, using just the framework adapter is sufficient since it captures LLM events through callbacks. + +## Next Steps + +- [LLM Providers](providers.md) — Auto-instrument OpenAI, Anthropic, and LiteLLM +- [Quick Start](quickstart.md) — Manual instrumentation with `@trace` and `span()` diff --git a/docs/instrumentation/providers.md b/docs/instrumentation/providers.md new file mode 100644 index 0000000..4f34d0f --- /dev/null +++ b/docs/instrumentation/providers.md @@ -0,0 +1,222 @@ +# LLM Provider Instrumentation + +Provider adapters automatically capture LLM spans when SDK methods are called inside a `@trace` context. No changes to your LLM calling code are needed. + +## Supported Providers + +| Provider | Adapter | Wraps | +| -------- | ------- | ----- | +| OpenAI | `instrument_openai(client)` | `client.chat.completions.create` | +| Anthropic | `instrument_anthropic(client)` | `client.messages.create` | +| LiteLLM | `instrument_litellm()` | `litellm.completion`, `litellm.acompletion` | + +LiteLLM provides a unified interface to 100+ providers (Azure, Google, Cohere, Mistral, Bedrock, etc.), so `instrument_litellm()` covers all of them. + +## OpenAI + +### Installation + +```bash +pip install layerlens[openai] +``` + +### Usage + +```python +import openai +from layerlens import Stratix +from layerlens.instrument import trace +from layerlens.instrument.adapters.providers.openai import instrument_openai + +client = Stratix() +openai_client = openai.OpenAI() + +# Instrument the client instance +instrument_openai(openai_client) + +@trace(client) +def my_agent(question: str): + response = openai_client.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": question}], + ) + return response.choices[0].message.content + +my_agent("What is Python?") +``` + +The adapter captures: + +- **Span name**: `openai.chat.completions.create` +- **Kind**: `llm` +- **Input**: Messages array +- **Output**: Assistant message content +- **Metadata**: `model`, `temperature`, `max_tokens`, `usage` (prompt/completion/total tokens) + +### Disconnect + +```python +from layerlens.instrument.adapters.providers.openai import OpenAIProvider + +provider = OpenAIProvider() +provider.connect_client(openai_client) + +# Later, restore original methods: +provider.disconnect() +``` + +## Anthropic + +### Installation + +```bash +pip install layerlens[anthropic] +``` + +### Usage + +```python +import anthropic +from layerlens import Stratix +from layerlens.instrument import trace +from layerlens.instrument.adapters.providers.anthropic import instrument_anthropic + +client = Stratix() +anthropic_client = anthropic.Anthropic() + +instrument_anthropic(anthropic_client) + +@trace(client) +def my_agent(question: str): + response = anthropic_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": question}], + ) + return response.content[0].text + +my_agent("What is Python?") +``` + +The adapter captures: + +- **Span name**: `anthropic.messages.create` +- **Kind**: `llm` +- **Input**: Messages array +- **Output**: Response content blocks +- **Metadata**: `model`, `usage` (input/output tokens), `stop_reason` + +### Disconnect + +```python +from layerlens.instrument.adapters.providers.anthropic import AnthropicProvider + +provider = AnthropicProvider() +provider.connect_client(anthropic_client) +provider.disconnect() +``` + +## LiteLLM + +LiteLLM works differently from OpenAI/Anthropic — it patches module-level functions rather than client instances. + +### Installation + +```bash +pip install layerlens[litellm] +``` + +### Usage + +```python +import litellm +from layerlens import Stratix +from layerlens.instrument import trace +from layerlens.instrument.adapters.providers.litellm import instrument_litellm + +client = Stratix() + +# Patch litellm module (call once at startup) +instrument_litellm() + +@trace(client) +def my_agent(question: str): + response = litellm.completion( + model="gpt-4o", + messages=[{"role": "user", "content": question}], + ) + return response.choices[0].message.content + +my_agent("What is Python?") +``` + +Since LiteLLM supports 100+ providers, this single call instruments all of them: + +```python +instrument_litellm() + +@trace(client) +def multi_provider(): + # All of these generate LLM spans + litellm.completion(model="gpt-4o", messages=[...]) + litellm.completion(model="claude-sonnet-4-20250514", messages=[...]) + litellm.completion(model="gemini/gemini-pro", messages=[...]) +``` + +### Uninstrument + +```python +from layerlens.instrument.adapters.providers.litellm import uninstrument_litellm + +uninstrument_litellm() +``` + +## Captured Metadata + +All provider adapters capture these request parameters when present: + +| Parameter | Description | +| --------- | ----------- | +| `model` | Model name/ID | +| `temperature` | Sampling temperature | +| `max_tokens` | Maximum response tokens | +| `top_p` | Nucleus sampling parameter | +| `frequency_penalty` | Frequency penalty | +| `presence_penalty` | Presence penalty | +| `response_format` | Structured output format | + +Response metadata varies by provider but always includes token usage when available. + +## Passthrough Behavior + +When called **outside** a `@trace` context, all adapters pass through to the original SDK method with zero overhead. This means you can instrument at startup and leave it on — it only activates when a trace is running. + +```python +instrument_openai(openai_client) + +# No active trace — passes through directly to OpenAI +openai_client.chat.completions.create(model="gpt-4o", messages=[...]) + +@trace(client) +def traced_call(): + # Active trace — generates an LLM span + openai_client.chat.completions.create(model="gpt-4o", messages=[...]) +``` + +## Error Handling + +If an LLM call raises an exception inside a `@trace`, the adapter records the error on the span and re-raises the exception: + +```python +@trace(client) +def my_agent(): + try: + openai_client.chat.completions.create(model="gpt-4o", messages=[...]) + except openai.APIError: + pass # Span is recorded with status="error" +``` + +## Next Steps + +- [Agent Frameworks](frameworks.md) — LangChain and LangGraph callback handlers +- [Quick Start](quickstart.md) — Manual instrumentation with `@trace` and `span()` diff --git a/docs/instrumentation/quickstart.md b/docs/instrumentation/quickstart.md new file mode 100644 index 0000000..9c954ad --- /dev/null +++ b/docs/instrumentation/quickstart.md @@ -0,0 +1,171 @@ +# Instrumentation Quick Start + +This guide covers the core instrumentation API: the `@trace` decorator and the `span()` context manager. + +## Installation + +The instrumentation module is included in the base SDK — no extra dependencies needed: + +```bash +pip install layerlens --extra-index-url https://sdk.layerlens.ai/package +``` + +Provider adapters require their respective SDK as an optional dependency: + +```bash +pip install layerlens[openai] # OpenAI +pip install layerlens[anthropic] # Anthropic +pip install layerlens[litellm] # LiteLLM (100+ providers) +pip install layerlens[langchain] # LangChain / LangGraph +``` + +## The `@trace` Decorator + +`@trace(client)` marks a function as the root of a trace. When the function returns (or raises), the complete span tree is serialized and uploaded. + +### Using Synchronous Client + +```python +from layerlens import Stratix +from layerlens.instrument import trace + +client = Stratix() + +@trace(client) +def my_agent(query: str): + # Everything inside here is traced + return process(query) + +my_agent("Hello") +# → Trace uploaded automatically on return +``` + +### Using Async Client + +```python +import asyncio +from layerlens import AsyncStratix +from layerlens.instrument import trace + +client = AsyncStratix() + +@trace(client) +async def my_agent(query: str): + return await process(query) + +asyncio.run(my_agent("Hello")) +``` + +### Custom Trace Names + +By default the trace is named after the function. Override with the `name` parameter: + +```python +@trace(client, name="qa-pipeline") +def run_pipeline(query: str): + ... +``` + +## The `span()` Context Manager + +Use `span()` inside a traced function to create child spans: + +```python +from layerlens.instrument import trace, span + +@trace(client) +def my_agent(query: str): + with span("retrieve", kind="retriever") as s: + docs = search(query) + s.output = docs + + with span("generate", kind="llm") as s: + answer = call_llm(query, docs) + s.output = answer + + return answer +``` + +### Span Parameters + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `name` | `str` | (required) | Display name for the span | +| `kind` | `str` | `"internal"` | Span type: `internal`, `llm`, `retriever`, `tool`, `chain` | +| `input` | `Any` | `None` | Input data for the span | +| `metadata` | `dict \| None` | `None` | Arbitrary metadata attached to the span | + +### Setting Span Data + +Inside the `with` block, you can set properties on the span object: + +```python +with span("my-step", kind="tool") as s: + s.input = {"query": query} + result = do_work(query) + s.output = result + s.metadata["custom_key"] = "custom_value" +``` + +### Nesting Spans + +Spans nest automatically — the parent-child relationship is tracked via `contextvars`: + +```python +@trace(client) +def my_agent(query: str): + with span("outer") as outer: + with span("inner") as inner: + # inner is a child of outer + ... + with span("sibling"): + # sibling is a child of root, not outer + ... +``` + +This produces: + +``` +my_agent (root) +├── outer +│ └── inner +└── sibling +``` + +## Span Data Model + +Each span captures: + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `name` | `str` | Span name | +| `span_id` | `str` | Unique identifier (auto-generated) | +| `parent_id` | `str \| None` | Parent span ID | +| `start_time` | `float` | Unix timestamp when span started | +| `end_time` | `float \| None` | Unix timestamp when span ended | +| `status` | `str` | `"ok"` or `"error"` | +| `kind` | `str` | `"internal"`, `"llm"`, `"retriever"`, `"tool"`, `"chain"` | +| `input` | `Any` | Input data (set manually or captured by adapters) | +| `output` | `Any` | Output data | +| `error` | `str \| None` | Error message if status is `"error"` | +| `metadata` | `dict` | Arbitrary metadata (model name, token usage, etc.) | +| `children` | `list` | Child spans | + +## Error Handling + +Errors are captured automatically. If an exception is raised inside a traced function or span, the span's status is set to `"error"` and the error message is recorded. The exception still propagates normally. + +```python +@trace(client) +def my_agent(query: str): + with span("risky-step") as s: + raise ValueError("something broke") + # → span status="error", error="something broke" + # → trace still uploads with the error recorded + # → ValueError propagates to caller +``` + +## Next Steps + +- [LLM Providers](providers.md) — Auto-instrument OpenAI, Anthropic, and LiteLLM +- [Agent Frameworks](frameworks.md) — LangChain and LangGraph callback handlers