From fb0598fc981d71234eb5f8f8903a2fbff47bf8e6 Mon Sep 17 00:00:00 2001 From: Yohei Demachi <55367533+yoheidemachi@users.noreply.github.com> Date: Wed, 27 May 2026 11:29:16 +0900 Subject: [PATCH 1/3] feat(sdk): expose trace_content via Traceloop.init() Adds a `trace_content: Optional[bool] = None` argument to `Traceloop.init()` that mirrors the `TRACELOOP_TRACE_CONTENT` environment variable. When omitted, the env var stays authoritative; when set to True/False, the argument overrides the env var so that applications can disable prompt/completion capture without relying on process-level environment configuration. Closes #137 --- .../tests/test_sdk_initialization.py | 59 +++++++++++++++++++ .../traceloop-sdk/traceloop/sdk/__init__.py | 11 ++++ 2 files changed, 70 insertions(+) diff --git a/packages/traceloop-sdk/tests/test_sdk_initialization.py b/packages/traceloop-sdk/tests/test_sdk_initialization.py index 18ef5d7d0f..8efbcff421 100644 --- a/packages/traceloop-sdk/tests/test_sdk_initialization.py +++ b/packages/traceloop-sdk/tests/test_sdk_initialization.py @@ -1,9 +1,11 @@ import json +import os import warnings import pytest from unittest.mock import patch from openai import OpenAI from traceloop.sdk import Traceloop +from traceloop.sdk.config import is_content_tracing_enabled from traceloop.sdk.decorators import workflow from traceloop.sdk.tracing.tracing import TracerWrapper from opentelemetry.sdk.trace.export import SimpleSpanProcessor, BatchSpanProcessor @@ -358,3 +360,60 @@ def probe(): del TracerWrapper.instance if saved_instance is not None: TracerWrapper.instance = saved_instance + + +@pytest.fixture +def isolated_trace_content_env(): + """Save/restore TRACELOOP_TRACE_CONTENT so trace_content tests don't leak.""" + saved = os.environ.get("TRACELOOP_TRACE_CONTENT") + yield + if saved is None: + os.environ.pop("TRACELOOP_TRACE_CONTENT", None) + else: + os.environ["TRACELOOP_TRACE_CONTENT"] = saved + + +def test_trace_content_false_disables_content_tracing( + isolated_tracer_wrapper, isolated_trace_content_env +): + """trace_content=False must disable content capture regardless of how the env was set.""" + os.environ["TRACELOOP_TRACE_CONTENT"] = "true" + + Traceloop.init( + exporter=InMemorySpanExporter(), + disable_batch=True, + trace_content=False, + ) + + assert is_content_tracing_enabled() is False + assert os.environ["TRACELOOP_TRACE_CONTENT"] == "false" + + +def test_trace_content_true_overrides_env( + isolated_tracer_wrapper, isolated_trace_content_env +): + """An explicit trace_content=True must win over an env that disabled it — + otherwise the new arg would be weaker than the env var it is meant to expose.""" + os.environ["TRACELOOP_TRACE_CONTENT"] = "false" + + Traceloop.init( + exporter=InMemorySpanExporter(), + disable_batch=True, + trace_content=True, + ) + + assert is_content_tracing_enabled() is True + assert os.environ["TRACELOOP_TRACE_CONTENT"] == "true" + + +def test_trace_content_none_honors_env( + isolated_tracer_wrapper, isolated_trace_content_env +): + """When trace_content is omitted, the env var stays authoritative — existing + deployments that rely on TRACELOOP_TRACE_CONTENT must not change behavior.""" + os.environ["TRACELOOP_TRACE_CONTENT"] = "false" + + Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) + + assert is_content_tracing_enabled() is False + assert os.environ["TRACELOOP_TRACE_CONTENT"] == "false" diff --git a/packages/traceloop-sdk/traceloop/sdk/__init__.py b/packages/traceloop-sdk/traceloop/sdk/__init__.py index 6663429d99..91cd8ae996 100644 --- a/packages/traceloop-sdk/traceloop/sdk/__init__.py +++ b/packages/traceloop-sdk/traceloop/sdk/__init__.py @@ -73,6 +73,7 @@ def init( endpoint_is_traceloop: Optional[bool] = False, use_attributes: Optional[bool] = None, use_legacy_attributes: Optional[bool] = None, + trace_content: Optional[bool] = None, ) -> Optional[Client]: """Initialize Traceloop tracing, metrics, and instrumentation. @@ -88,6 +89,13 @@ def init( events have nowhere to go and no prompt/completion data will be recorded. use_legacy_attributes: Deprecated alias for ``use_attributes``. Will be removed in a future release. + trace_content: Controls whether prompts/completions are captured by the + bundled instrumentations. If ``None`` (default), the value of the + ``TRACELOOP_TRACE_CONTENT`` environment variable is honored (defaults + to enabled). If ``True`` or ``False``, the argument explicitly overrides + the environment variable for the lifetime of the process. Use + ``False`` to disable prompt/completion capture when sensitive content + must not leave the host. """ if use_attributes is not None and use_legacy_attributes is not None: raise TypeError( @@ -125,6 +133,9 @@ def init( print(Fore.YELLOW + "Tracing is disabled" + Fore.RESET) return + if trace_content is not None: + os.environ["TRACELOOP_TRACE_CONTENT"] = "true" if trace_content else "false" + enable_content_tracing = is_content_tracing_enabled() if exporter and processor: From 8e4f4e9ac931661746043742985b881625cf9ae8 Mon Sep 17 00:00:00 2001 From: Yohei Demachi <55367533+yoheidemachi@users.noreply.github.com> Date: Wed, 27 May 2026 11:36:34 +0900 Subject: [PATCH 2/3] docs(sdk): clarify trace_content side effects and pin sticky behavior Document the three non-obvious aspects of trace_content surfaced in review: (1) writing TRACELOOP_TRACE_CONTENT is process-wide and inherited by subprocesses, (2) the override is sticky across subsequent init() calls with trace_content=None, (3) per-span override via override_enable_content_tracing still works because instrumentations treat env and context as OR. Adds a test pinning (2) so a future save/restore refactor would force a docs update. Assisted-by: Claude Opus 4.7 --- .../tests/test_sdk_initialization.py | 23 +++++++++++++++++++ .../traceloop-sdk/traceloop/sdk/__init__.py | 13 +++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/traceloop-sdk/tests/test_sdk_initialization.py b/packages/traceloop-sdk/tests/test_sdk_initialization.py index 8efbcff421..6450fd1d68 100644 --- a/packages/traceloop-sdk/tests/test_sdk_initialization.py +++ b/packages/traceloop-sdk/tests/test_sdk_initialization.py @@ -417,3 +417,26 @@ def test_trace_content_none_honors_env( assert is_content_tracing_enabled() is False assert os.environ["TRACELOOP_TRACE_CONTENT"] == "false" + + +def test_trace_content_override_is_sticky_across_inits( + isolated_tracer_wrapper, isolated_trace_content_env +): + """An explicit trace_content setting must persist across subsequent init() + calls that omit the arg. Once the env has been overwritten, trace_content=None + cannot restore the pre-init env value — the most recent explicit setting wins + until something external resets the env. This pins the documented behavior so + a future "save/restore env on init exit" refactor would force a docs update.""" + os.environ["TRACELOOP_TRACE_CONTENT"] = "false" + + Traceloop.init( + exporter=InMemorySpanExporter(), + disable_batch=True, + trace_content=True, + ) + assert os.environ["TRACELOOP_TRACE_CONTENT"] == "true" + + Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) + + assert is_content_tracing_enabled() is True + assert os.environ["TRACELOOP_TRACE_CONTENT"] == "true" diff --git a/packages/traceloop-sdk/traceloop/sdk/__init__.py b/packages/traceloop-sdk/traceloop/sdk/__init__.py index 91cd8ae996..12036cc6c4 100644 --- a/packages/traceloop-sdk/traceloop/sdk/__init__.py +++ b/packages/traceloop-sdk/traceloop/sdk/__init__.py @@ -96,6 +96,19 @@ def init( the environment variable for the lifetime of the process. Use ``False`` to disable prompt/completion capture when sensitive content must not leave the host. + + Notes: + * This is implemented by writing ``TRACELOOP_TRACE_CONTENT`` into + ``os.environ``, so child processes and any other code reading + that variable will observe the override. + * The override is sticky: a subsequent ``Traceloop.init()`` call + with ``trace_content=None`` does not restore the previous + env-driven value — the most recent explicit ``True``/``False`` + keeps winning until the env var is changed externally. + * Per-span enablement via the ``override_enable_content_tracing`` + OTel context value still works when ``trace_content=False`` — + instrumentations treat env and context as OR, so individual + spans can opt back in while the global default stays off. """ if use_attributes is not None and use_legacy_attributes is not None: raise TypeError( From 376f59e03ca88643e9c312a99cbee56e5132e60c Mon Sep 17 00:00:00 2001 From: Yohei Demachi <55367533+yoheidemachi@users.noreply.github.com> Date: Wed, 27 May 2026 11:47:29 +0900 Subject: [PATCH 3/3] docs(sdk): note trace_content is skipped in init no-op paths, harden test fixture Address self-review nits on the trace_content init arg: * Docstring: clarify that the env override is not applied when enabled=False or tracing is disabled via TRACELOOP_TRACING_ENABLED, since init() returns early before the write would happen. Pins the documented surface to the actual code path. * Test fixture: clear TRACELOOP_TRACE_CONTENT before yielding (not just on teardown) so trace_content tests start from a known-unset state. A CI env that pre-sets the var would otherwise mask bugs in tests that exercise the "env not set" default path. No behavior change. Assisted-by: Claude Opus 4.7 --- packages/traceloop-sdk/tests/test_sdk_initialization.py | 8 ++++++-- packages/traceloop-sdk/traceloop/sdk/__init__.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/traceloop-sdk/tests/test_sdk_initialization.py b/packages/traceloop-sdk/tests/test_sdk_initialization.py index 6450fd1d68..5f8969aa51 100644 --- a/packages/traceloop-sdk/tests/test_sdk_initialization.py +++ b/packages/traceloop-sdk/tests/test_sdk_initialization.py @@ -364,8 +364,12 @@ def probe(): @pytest.fixture def isolated_trace_content_env(): - """Save/restore TRACELOOP_TRACE_CONTENT so trace_content tests don't leak.""" - saved = os.environ.get("TRACELOOP_TRACE_CONTENT") + """Save/restore TRACELOOP_TRACE_CONTENT so trace_content tests don't leak. + + Also clears the var before yielding so tests start from a known-unset state — + otherwise a CI env that pre-sets TRACELOOP_TRACE_CONTENT could mask bugs in + tests that exercise the "env not set" default path.""" + saved = os.environ.pop("TRACELOOP_TRACE_CONTENT", None) yield if saved is None: os.environ.pop("TRACELOOP_TRACE_CONTENT", None) diff --git a/packages/traceloop-sdk/traceloop/sdk/__init__.py b/packages/traceloop-sdk/traceloop/sdk/__init__.py index 12036cc6c4..fca71903bf 100644 --- a/packages/traceloop-sdk/traceloop/sdk/__init__.py +++ b/packages/traceloop-sdk/traceloop/sdk/__init__.py @@ -109,6 +109,10 @@ def init( OTel context value still works when ``trace_content=False`` — instrumentations treat env and context as OR, so individual spans can opt back in while the global default stays off. + * Not applied when ``enabled=False`` or when tracing is disabled + via ``TRACELOOP_TRACING_ENABLED``: ``init()`` returns early + before the override would take effect, so the env var is left + untouched in those no-op paths. """ if use_attributes is not None and use_legacy_attributes is not None: raise TypeError(