Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ npm-debug.log
yarn-error.log
testem.log
/typings
.tool-versions

# System Files
.DS_Store
Expand Down
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,35 @@ Thanks for taking the time to contribute! 😃 🚀

Please refer to our [Contributing Guide](https://traceloop.com/docs/openllmetry/contributing/overview) for instructions on how to contribute.

## Local Testing and Linting in this Repo

A few steps to set up this repo locally.

Run the following at repo root to setup the yarn dependencies.
```shell
npm ci
```

Install `poetry` (version 2) at the system-level (e.g. using `pipx` or `brew`).

For setting up and testing an individual package, run the following from repo root.

```shell
npx nx run opentelemetry-instrumentation-openai:install --with dev,test
npx nx run opentelemetry-instrumentation-openai:lint
npx nx run opentelemetry-instrumentation-openai:test
```

Or you can run the following to automatically set up all affected packages.
```shell
npx nx affected -t install --with dev,test
npx nx affected -t lint
npx nx affected -t test
```

## Build Errors

If encountering local build errors in environment setup, try a lower python version, e.g. 3.11.
There are `.python-version` files in individual package directories, but poetry may not reliably respect it.
Depending on your python installation, you may need to set your python version accordingly at repo root (e.g. with a `.python-version` or `.tool-versions`).
The `.venv` dir within each package must be deleted before you retry the poetry/nx command, as it will not clear the existing venv and pick up the updated, lower version specified until the existing venv has been deleted manually.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Hook-based instrumentation for OpenAI Agents using the SDK's native callback system."""

from typing import Dict, Any
from typing import Dict, Any, Final
import json
import time
from collections import OrderedDict
Expand All @@ -11,7 +11,7 @@
gen_ai_attributes as GenAIAttributes
)
from agents.tracing.processors import TracingProcessor
from .utils import dont_throw
from .utils import dont_throw, should_send_prompts

try:
# Attempt to import once, so that we aren't looking for it repeatedly.
Expand Down Expand Up @@ -239,6 +239,7 @@ def on_span_end(self, span):
if span in self._otel_spans:
otel_span = self._otel_spans[span]
span_data = getattr(span, 'span_data', None)
should_trace_content: Final[bool] = should_send_prompts()
if span_data and (
type(span_data).__name__ == 'ResponseSpanData' or isinstance(
span_data,
Expand Down Expand Up @@ -304,7 +305,7 @@ def on_span_end(self, span):
otel_span.set_attribute(f"{prefix}.role", role)

# Set content attribute
if content is not None:
if should_trace_content and content is not None:
if not isinstance(content, str):
content = json.dumps(content)
otel_span.set_attribute(f"{prefix}.content", content)
Expand Down Expand Up @@ -405,7 +406,7 @@ def on_span_end(self, span):
if hasattr(response, 'output') and response.output:
for i, output in enumerate(response.output):
# Handle different output types
if hasattr(output, 'content') and output.content:
if should_trace_content and hasattr(output, 'content') and output.content:
# Text message with content array (ResponseOutputMessage)
content_text = ""
for content_item in output.content:
Expand Down Expand Up @@ -436,7 +437,7 @@ def on_span_end(self, span):
otel_span.set_attribute(
f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.tool_calls.0.id", tool_call_id)

elif hasattr(output, 'text'):
elif should_trace_content and hasattr(output, 'text'):
# Direct text content
otel_span.set_attribute(f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.content", output.text)
otel_span.set_attribute(
Expand Down Expand Up @@ -472,7 +473,7 @@ def on_span_end(self, span):
elif span_data:
# Extract prompt data from input and add to response span (legacy support)
input_data = getattr(span_data, 'input', [])
if input_data:
if should_trace_content and input_data:
for i, message in enumerate(input_data):
if hasattr(message, 'role') and hasattr(message, 'content'):
otel_span.set_attribute(f"gen_ai.prompt.{i}.role", message.role)
Expand Down Expand Up @@ -514,7 +515,7 @@ def on_span_end(self, span):
if hasattr(response, 'output') and response.output:
for i, output in enumerate(response.output):
# Handle different output types
if hasattr(output, 'content') and output.content:
if should_trace_content and hasattr(output, 'content') and output.content:
# Text message with content array (ResponseOutputMessage)
content_text = ""
for content_item in output.content:
Expand Down Expand Up @@ -545,7 +546,7 @@ def on_span_end(self, span):
otel_span.set_attribute(
f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.tool_calls.0.id", tool_call_id)

elif hasattr(output, 'text'):
elif should_trace_content and hasattr(output, 'text'):
# Direct text content
otel_span.set_attribute(f"{GenAIAttributes.GEN_AI_COMPLETION}.{i}.content", output.text)
otel_span.set_attribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import traceback
from opentelemetry import context as context_api

_TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"


def set_span_attribute(span, name, value):
if value is not None:
Expand All @@ -18,10 +20,14 @@ def _is_truthy(value):
return str(value).strip().lower() in ("true", "1", "yes", "on")


def should_send_prompts():
env_setting = os.getenv("TRACELOOP_TRACE_CONTENT", "true")
def should_send_prompts() -> bool:
"""Determine if LLM content tracing should be enabled.

Content includes not only prompts, but also responses.
"""
env_setting = os.getenv(_TRACELOOP_TRACE_CONTENT, "true")
override = context_api.get_value("override_enable_content_tracing")
return _is_truthy(env_setting) or bool(override)
return _is_truthy(env_setting) or _is_truthy(override)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context_api is not typed. This flag isn't documented and users could set it to any value 1, True, "true", "yes", "on" etc. Having a single implementation of _is_truthy reduces surprises and provides better compatibility with a range of possible values. The rationale is the same as that for the environment variable.



class JSONEncoder(json.JSONEncoder):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,18 @@ def run_async(method):
asyncio.run(method)


def should_send_prompts():
return (
os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
def _is_truthy(value):
return str(value).strip().lower() in ("true", "1", "yes", "on")


def should_send_prompts() -> bool:
"""Determine if LLM content tracing should be enabled.

Content includes not only prompts, but also responses.
"""
env_setting = os.getenv(TRACELOOP_TRACE_CONTENT, "true")
override = context_api.get_value("override_enable_content_tracing")
return _is_truthy(env_setting) or _is_truthy(override)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here - we set the override flag so it will always be a boolean

Copy link
Contributor Author

@duanyutong duanyutong Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we set the override

I see that packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py sets it, but this flag is also needed by general users to apply fine control over content tracing at the per-request level, when they are using simple otel without traceloop-sdk necessarily.

The _is_truthy call here adds extra safety, without breaking any existing behaviour.



def should_emit_events() -> bool:
Expand Down