← Files HoneycombARCHIVED FILE

references/content-capture-setup.md

7.91 KB · Sep 30, 2026 · 22:51 UTC

↓ Download file

# Content Capture Setup

Enabling and controlling capture of GenAI message content — prompts, responses, tool
arguments, and tool results. Required for debugging tool call failures, excessive
planning loops, and agent deadlocks.

## Auto-Instrumentation (Python)

Set one environment variable before starting the application:

```bash
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
```

This enables capture of:
- `gen_ai.input.messages` — user prompts, tool call arguments, tool results
- `gen_ai.output.messages` — model responses, tool call requests
- `gen_ai.system_instructions` — system prompts
- `gen_ai.tool.definitions` — tool schemas

Must be set **before** calling `Instrumentor().instrument()`.

## Manual Instrumentation

For languages without auto-instrumentation, or when you need fine-grained control.

### Setting Content on Inference Spans

```python
import json

with tracer.start_as_current_span("chat gpt-4", kind=SpanKind.CLIENT) as span:
    # Set input messages (opt-in)
    span.set_attribute("gen_ai.input.messages", json.dumps([
        {
            "role": "system",
            "parts": [{"type": "text", "text": "You are a helpful assistant."}]
        },
        {
            "role": "user",
            "parts": [{"type": "text", "text": "What's the weather in NYC?"}]
        }
    ]))

    response = client.chat.completions.create(model="gpt-4", messages=messages)

    # Set output messages (opt-in)
    span.set_attribute("gen_ai.output.messages", json.dumps([
        {
            "role": "assistant",
            "parts": [
                {
                    "type": "tool_call",
                    "id": "call_abc123",
                    "name": "get_weather",
                    "arguments": '{"city": "NYC"}'
                }
            ]
        }
    ]))
```

### Node.js

```javascript
span.setAttribute("gen_ai.input.messages", JSON.stringify([
  {
    role: "user",
    parts: [{ type: "text", text: "What's the weather in NYC?" }],
  },
]));

// After response
span.setAttribute("gen_ai.output.messages", JSON.stringify([
  {
    role: "assistant",
    parts: [{ type: "text", text: "It's 72°F in NYC." }],
  },
]));
```

### Go

```go
inputJSON, _ := json.Marshal([]Message{
    {Role: "user", Parts: []Part{{Type: "text", Text: "What's the weather?"}}},
})
span.SetAttributes(attribute.String("gen_ai.input.messages", string(inputJSON)))

// After response
outputJSON, _ := json.Marshal([]Message{
    {Role: "assistant", Parts: []Part{{Type: "text", Text: "It's 72°F."}}},
})
span.SetAttributes(attribute.String("gen_ai.output.messages", string(outputJSON)))
```

### Setting Content on Tool Spans

```python
with tracer.start_as_current_span("execute_tool get_weather") as span:
    span.set_attribute("gen_ai.tool.call.arguments", json.dumps({
        "city": "NYC"
    }))

    result = get_weather(city="NYC")

    span.set_attribute("gen_ai.tool.call.result", json.dumps(result))
```

## Message JSON Schema

All content attributes use a JSON array of message objects:

```json
[
  {
    "role": "system" | "user" | "assistant" | "tool",
    "parts": [
      {"type": "text", "text": "..."},
      {"type": "tool_call", "id": "call_123", "name": "fn_name", "arguments": "{}"},
      {"type": "tool_call_response", "id": "call_123", "response": "{}"},
      {"type": "reasoning", "text": "..."}
    ]
  }
]
```

### Part Types

| Type | Used In | Description |
| :--- | :--- | :--- |
| `text` | input/output | Plain text content |
| `tool_call` | output | Model requesting a tool call |
| `tool_call_response` | input | Tool result fed back to model |
| `reasoning` | output | Model's chain-of-thought (if exposed) |

### Tool Call Part Fields

```json
{
  "type": "tool_call",
  "id": "call_abc123",
  "name": "get_weather",
  "arguments": "{\"city\": \"NYC\"}"
}
```

### Tool Call Response Part Fields

```json
{
  "type": "tool_call_response",
  "id": "call_abc123",
  "response": "{\"temperature\": 72, \"unit\": \"F\"}"
}
```

## Privacy Controls

Content capture includes sensitive data (user prompts, model outputs). Apply these
controls based on environment.

### Filtering: Select Which Messages to Capture

```python
def filter_messages(messages):
    """Capture tool calls but redact user content."""
    filtered = []
    for msg in messages:
        if msg["role"] == "user":
            filtered.append({
                "role": "user",
                "parts": [{"type": "text", "text": "[REDACTED]"}]
            })
        else:
            filtered.append(msg)
    return filtered

# Use filtered content
span.set_attribute("gen_ai.input.messages", json.dumps(filter_messages(messages)))
```

### Truncation: Limit Content Size

```python
MAX_CONTENT_LENGTH = 4096  # bytes

def truncate_content(content_json):
    """Truncate content to prevent oversized spans."""
    if len(content_json) > MAX_CONTENT_LENGTH:
        return content_json[:MAX_CONTENT_LENGTH] + '...[truncated]"'
    return content_json

span.set_attribute("gen_ai.input.messages", truncate_content(json.dumps(messages)))
```

### Hooks: Route to Separate Storage

Use an OTel SpanProcessor to intercept content attributes and route them to
access-controlled storage:

```python
from opentelemetry.sdk.trace import SpanProcessor

CONTENT_ATTRS = {
    "gen_ai.input.messages",
    "gen_ai.output.messages",
    "gen_ai.system_instructions",
    "gen_ai.tool.definitions",
    "gen_ai.tool.call.arguments",
    "gen_ai.tool.call.result",
}

class ContentRedactionProcessor(SpanProcessor):
    def __init__(self, content_store):
        self.content_store = content_store

    def on_end(self, span):
        attrs = span.attributes or {}
        content = {}
        for key in CONTENT_ATTRS:
            if key in attrs:
                content[key] = attrs[key]

        if content:
            # Store content separately with access controls
            ref = self.content_store.store(
                trace_id=span.context.trace_id,
                span_id=span.context.span_id,
                content=content,
            )
            # Replace content with reference
            # Note: span attributes are immutable after export;
            # this processor runs before the exporter
```

### Environment-Based Strategy

| Environment | Strategy |
| :--- | :--- |
| Development | Full content capture — all fields enabled |
| Staging | Full content capture — matches production behavior |
| Production | Filtered capture — redact PII, truncate large payloads |
| Regulated (HIPAA/SOC2) | Separate storage — content routed to access-controlled store |

**Recommendation**: Enable content capture everywhere. Use filtering in production,
full content in non-prod. The debugging value of content capture far outweighs the
cost when failures occur.

## Provider-Specific Notes

### OpenAI

```python
# OpenAI auto-instrumentation captures tool_calls in finish_reason
# and includes function call details in output messages automatically
OpenAIInstrumentor().instrument()
```

### Anthropic

```python
# Anthropic responses include tool_use blocks
# Auto-instrumentation maps these to tool_call parts
AnthropicInstrumentor().instrument()
```

Cache tokens appear as `gen_ai.usage.cache_creation_input_tokens` and
`gen_ai.usage.cache_read_input_tokens` — these are captured regardless of content
capture settings (they're usage attributes, not content).

## Troubleshooting

**Content fields empty with auto-instrumentation:**
- Verify `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true` is set
- Must be set before `Instrumentor().instrument()` is called
- Check env var is visible to the process (not just the shell)

**Content too large:**
- Span attribute size limits vary by exporter (OTLP default: 128KB)
- Use truncation for large prompts/responses
- Consider storing large content separately and referencing by ID

**JSON parsing errors in Honeycomb:**
- Ensure content is valid JSON when setting attributes
- Use `json.dumps()` / `JSON.stringify()` — don't manually construct JSON strings

SHA-256: c3849e6b945076d0ff544d274023dad9013a741a8c27f0ea6263fd1ad11b6597