Skip to content

feat(sdk): expose use_legacy_attributes via Traceloop.init()#4133

Merged
dvirski merged 4 commits into
mainfrom
dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init()
May 19, 2026
Merged

feat(sdk): expose use_legacy_attributes via Traceloop.init()#4133
dvirski merged 4 commits into
mainfrom
dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init()

Conversation

@dvirski
Copy link
Copy Markdown
Contributor

@dvirski dvirski commented May 12, 2026

Fixes #3236.

Problem: use_legacy_attributes existed on every instrumentation but was
unreachable through the SDK — users were locked to legacy gen_ai.prompt/
gen_ai.completion span attributes with no way to opt into the new
event-based format.

Fix: thread use_legacy_attributes=True (default, no behaviour change) from
Traceloop.init() through TracerWrapper, init_instrumentations(), and all
8 instrumentations that support the flag: openai, anthropic, bedrock,
sagemaker, groq, langchain, together, watsonx.

Summary by CodeRabbit

  • New Features

    • Added use_attributes (default: true) to control whether prompt/completion data is emitted as span attributes or as events; this setting now propagates across all instrumentations and the deprecated use_legacy_attributes alias is supported.
  • Bug Fixes

    • Passing both use_attributes and use_legacy_attributes now raises a TypeError; using use_legacy_attributes emits a DeprecationWarning.
  • Tests

    • Added initialization tests for defaults, propagation, deprecation warning, and mutual-exclusion error.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

Adds a use_attributes parameter (and deprecated use_legacy_attributes alias) to Traceloop.init, validates and resolves the flags, forwards the boolean through TracerWrapper -> init_instrumentations into each instrumentor constructor, and adds tests + fixture validating propagation, defaults, and deprecation warnings.

Changes

Attribute vs events configuration propagation

Layer / File(s) Summary
Public API: Traceloop.init signature
packages/traceloop-sdk/traceloop/sdk/__init__.py
Adds use_attributes and deprecated use_legacy_attributes kwarg; validates both are not provided, emits DeprecationWarning when legacy is used, defaults to True when unset, and forwards resolved value into TracerWrapper.
TracerWrapper propagation
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
Add use_attributes param on TracerWrapper.__new__ and forward it into init_instrumentations.
Tracing initializer functions
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
Update init_instrumentations and per-instrument init_* initializers to accept use_attributes and pass it into each OpenTelemetry instrumentor constructor.
Instrumentor constructors (per-package)
packages/opentelemetry-instrumentation-openai/..., .../anthropic/..., .../groq/..., .../langchain/..., .../bedrock/..., .../sagemaker/..., .../together/..., .../watsonx/...
Each instrumentor adds use_attributes (optional) plus deprecated use_legacy_attributes handling: error on both provided, warn on legacy usage, default to True when unset, and set Config.use_legacy_attributes from resolved value.
Tests and fixture
packages/traceloop-sdk/tests/test_sdk_initialization.py
Add isolated_tracer_wrapper fixture to isolate singleton state; tests verify default behavior, explicit use_attributes=False propagation to OpenAI/Anthropic, deprecated alias emits DeprecationWarning, and passing both kwargs raises TypeError.

Sequence Diagram

sequenceDiagram
  participant Client
  participant TraceloopInit
  participant TracerWrapper
  participant InitInstrumentations
  participant OpenAIInstrumentor
  participant AnthropicInstrumentor
  Client->>TraceloopInit: Traceloop.init(use_attributes=...)
  TraceloopInit->>TracerWrapper: TracerWrapper(..., use_attributes=...)
  TracerWrapper->>InitInstrumentations: init_instrumentations(use_attributes)
  InitInstrumentations->>OpenAIInstrumentor: OpenAIInstrumentor(use_attributes=...)
  InitInstrumentations->>AnthropicInstrumentor: AnthropicInstrumentor(use_attributes=...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • galzilber
  • doronkopit5
  • max-deygin-traceloop
  • netanel-tl

Poem

"I hopped through flags and sentinels small,
From init to tracer I carried them all.
I nudged a warning when the old name came near,
Now attributes or events both behave clear.
🐰✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: exposing the use_legacy_attributes flag via Traceloop.init(), though it uses the old name rather than the new use_attributes.
Linked Issues check ✅ Passed All coding requirements from issue #3236 are met: use_attributes flag is exposed via Traceloop.init(), threaded through TracerWrapper and init_instrumentations, and propagated to all eight instrumentors with proper defaults and deprecation handling.
Out of Scope Changes check ✅ Passed All changes are directly related to resolving issue #3236; no extraneous modifications detected outside the scope of exposing use_attributes via Traceloop.init() and refactoring legacy naming.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init()

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 12, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/traceloop-sdk/tests/test_sdk_initialization.py (1)

232-253: 💤 Low value

Test correctly verifies propagation to OpenAI instrumentor.

The test properly isolates itself by saving/restoring the TracerWrapper instance and verifies that use_legacy_attributes=False reaches the OpenAI instrumentor configuration.

Consider adding complementary test coverage:

Optional test improvements
  1. Verify the default case: Test that when use_legacy_attributes is not specified (or set to True), the config defaults to True.

  2. Test multiple instrumentors: While testing OpenAI is representative, you could verify that the flag propagates to at least one other instrumentor (e.g., Anthropic or Groq) to ensure the pattern holds across different code paths.

Example for default case:

def test_use_legacy_attributes_defaults_to_true():
    """Verify use_legacy_attributes defaults to True for backward compatibility."""
    from opentelemetry.instrumentation.openai.shared.config import Config as OpenAIConfig
    
    _instance = None
    if hasattr(TracerWrapper, "instance"):
        _instance = TracerWrapper.instance
        del TracerWrapper.instance
    
    exporter = InMemorySpanExporter()
    Traceloop.init(
        exporter=exporter,
        disable_batch=True,
        # use_legacy_attributes not specified, should default to True
    )
    
    assert OpenAIConfig.use_legacy_attributes is True
    
    if _instance is not None:
        TracerWrapper.instance = _instance
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/tests/test_sdk_initialization.py` around lines 232 -
253, Add complementary tests: create
test_use_legacy_attributes_defaults_to_true() that mirrors
test_use_legacy_attributes_false_propagates_to_instrumentors but omits the
use_legacy_attributes argument when calling
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) and assert
OpenAIConfig.use_legacy_attributes is True; also add an additional test (e.g.,
test_use_legacy_attributes_propagates_to_other_instrumentor) that initializes
the SDK with use_legacy_attributes=False and asserts the same flag reached
another instrumentor's Config (e.g., AnthropicConfig or GroqConfig) to confirm
propagation; keep the same TracerWrapper instance save/restore pattern used in
the existing test to avoid global state leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/traceloop-sdk/tests/test_sdk_initialization.py`:
- Around line 232-253: Add complementary tests: create
test_use_legacy_attributes_defaults_to_true() that mirrors
test_use_legacy_attributes_false_propagates_to_instrumentors but omits the
use_legacy_attributes argument when calling
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) and assert
OpenAIConfig.use_legacy_attributes is True; also add an additional test (e.g.,
test_use_legacy_attributes_propagates_to_other_instrumentor) that initializes
the SDK with use_legacy_attributes=False and asserts the same flag reached
another instrumentor's Config (e.g., AnthropicConfig or GroqConfig) to confirm
propagation; keep the same TracerWrapper instance save/restore pattern used in
the existing test to avoid global state leaks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc9db1b3-3297-4a1c-bd8b-32022889f21c

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3e696 and 7541daa.

⛔ Files ignored due to path filters (2)
  • packages/sample-app/uv.lock is excluded by !**/*.lock
  • packages/traceloop-sdk/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • packages/traceloop-sdk/tests/test_sdk_initialization.py
  • packages/traceloop-sdk/traceloop/sdk/__init__.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

@doronkopit5
Copy link
Copy Markdown
Member

doronkopit5 commented May 13, 2026

The PR cleanly threads the flag through the SDK, and the mechanical scope is right (all 8 instrumentors that accept the kwarg are covered). Before merging though, I want to flag that the world shifted under issue #3236 in a way that affects what this flag should be named and what it should do.

The OTel semconv decision reversed

When #3236 was filed, the assumption was: "legacy attributes (gen_ai.prompt/gen_ai.completion) → events" is the migration path. That's no longer true. Semconv issue #2010 was re-litigated and closed in favor of attributes on spans, not events. The new spec-compliant attributes are gen_ai.input.messages / gen_ai.output.messages / gen_ai.system_instructions — and events failed to gain adoption across the GenAI tracing ecosystem.

OpenLLMetry has already migrated

Spot-checking the instrumentors, they all emit the new attributes today by default:

  • opentelemetry-instrumentation-openai/.../shared/chat_wrappers.py:552, 612GEN_AI_INPUT_MESSAGES / GEN_AI_OUTPUT_MESSAGES
  • opentelemetry-instrumentation-anthropic/.../span_utils.py:147, 318
  • opentelemetry-instrumentation-groq/.../span_utils.py:140, 183
  • opentelemetry-instrumentation-bedrock/.../span_utils.py:185, 376
  • opentelemetry-instrumentation-langchain/.../span_utils.py:231, 379

So use_legacy_attributes=True (the default) is already the current spec-compliant behavior.

What use_legacy_attributes=False actually does

Looking at opentelemetry-instrumentation-openai/.../utils.py:191-198 and the call sites in chat_wrappers.py:273-287:

if should_emit_events():        # not Config.use_legacy_attributes AND event_logger set
    emit_event(...)             # emit prompts as OTel log events
elif should_send_prompts():
    await _set_prompts(...)     # emit gen_ai.input.messages on the span

So the real semantics today are:

Flag value What gets emitted Status
use_legacy_attributes=True (default) gen_ai.input.messages on span Current spec
use_legacy_attributes=False (this PR exposes) OTel log events instead Abandoned direction

The name is now actively misleading: a user reading the SDK signature would reasonably assume False means "non-legacy / modern", but it actually opts them off the current spec and onto the events path.

Proposal: rename to use_attributes

This matches @nirga's original suggestion in #3236 ("use_attributes (or use_events)") and reflects reality without flipping defaults:

Old New
use_legacy_attributes: bool = True use_attributes: bool = True

Default stays True (no behavior change for existing users). The "legacy" word — which is the inaccurate part — goes away. False continues to mean "emit as events instead."

To keep the SDK and instrumentor-level APIs consistent, I'd suggest renaming at both layers in this PR:

  1. Rename the kwarg on the 8 instrumentor __init__s to use_attributes, accepting use_legacy_attributes as a deprecated alias that emits a DeprecationWarning and forwards to the new name.
  2. Same on Traceloop.init().
  3. Remove the alias in the next major version.

This adds maybe ~15 lines of deprecation-shim code but gives us a clean, accurate public API without breaking anyone.

Two related items worth deciding before merge

  1. EventLoggerProvider plumbing. The original issue also asked how to configure the event logger when opting into events. Today, use_legacy_attributes=False silently relies on the global logger provider being configured. Worth documenting clearly that users must configure one themselves — otherwise setting the flag silently does nothing useful.

  2. Test robustness. The new test in tests/test_sdk_initialization.py:232-253 manually mutates TracerWrapper.instance instead of using the existing fixture pattern from conftest.py. It also only verifies one instrumentor (OpenAI). Worth parametrizing across 2-3 instrumentors and using the fixture pattern to avoid global-state leakage if Traceloop.init() ever raises mid-call.

@dvirski dvirski force-pushed the dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init() branch from 7541daa to b7c4747 Compare May 18, 2026 11:19
@dvirski
Copy link
Copy Markdown
Contributor Author

dvirski commented May 18, 2026

@doronkopit5 Your suggestions were on point.

What has changed:

  1. Renamed kwarg use_legacy_attributes → use_attributes across 8 instrumentor init methods (openai, anthropic, bedrock, sagemaker, groq, langchain, together, watsonx).

  2. Added deprecation shim — each init still accepts use_legacy_attributes via a sentinel, emits DeprecationWarning, and forwards to use_attributes.

  3. Same rename + deprecation shim on Traceloop.init() at the SDK layer.

  4. Threaded use_attributes through the SDK call chain — TracerWrapper.new, init_instrumentations, and all 8 init_*_instrumentor helpers in tracing.py.

  5. Added docstring to Traceloop.init() documenting use_attributes semantics and the requirement that use_attributes=False needs an EventLoggerProvider configured.

  6. Rewrote the test using a reusable isolated_tracer_wrapper fixture (replaces manual TracerWrapper.instance mutation) and expanded coverage to 3 tests: default-is-True across OpenAI + Anthropic, use_attributes=False propagates to OpenAI + Anthropic, deprecated alias still works and emits
    DeprecationWarning.

Default behavior unchanged; existing callers using use_legacy_attributes keep working (with a warning)

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/traceloop-sdk/traceloop/sdk/__init__.py (1)

57-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid mutable defaults in Traceloop.init.

Line 57 and Line 70 use {} defaults. Those dicts are shared across calls and can retain mutated state unexpectedly.

Suggested fix
-        headers: Dict[str, str] = {},
+        headers: Optional[Dict[str, str]] = None,
...
-        resource_attributes: dict = {},
+        resource_attributes: Optional[dict] = None,
...
-        headers = os.getenv("TRACELOOP_HEADERS") or headers
+        headers = os.getenv("TRACELOOP_HEADERS") or headers or {}
...
-        resource_attributes.update({SERVICE_NAME: app_name})
+        resource_attributes = resource_attributes or {}
+        resource_attributes.update({SERVICE_NAME: app_name})

Also applies to: 70-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/traceloop/sdk/__init__.py` at line 57, The init method
uses mutable default dicts (headers: Dict[str, str] = {} and the other {} at
line 70) which can retain state across calls; change those parameter defaults to
None (e.g., headers: Optional[Dict[str,str]] = None) and inside Traceloop.init
initialize them with an empty dict if None (headers = headers or {}) so each
call gets a fresh dict; update any docstrings and type hints accordingly and
ensure callers are unaffected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py`:
- Around line 648-669: The _emit_response_events function currently has a
mismatched signature vs its call sites, causing a TypeError when callers pass an
event_logger; update the _emit_response_events definition to accept an optional
second parameter (e.g., def _emit_response_events(responses, event_logger=None))
and make its body robust to both single-response dicts and lists of responses
and to a None event_logger, so both callers (the dont_throw-decorated call that
does _emit_response_events(responses, event_logger) and the other call that
passes only responses) succeed without raising; ensure the function emits events
using event_logger when provided and safely no-ops or logs appropriately when it
is None.

---

Outside diff comments:
In `@packages/traceloop-sdk/traceloop/sdk/__init__.py`:
- Line 57: The init method uses mutable default dicts (headers: Dict[str, str] =
{} and the other {} at line 70) which can retain state across calls; change
those parameter defaults to None (e.g., headers: Optional[Dict[str,str]] = None)
and inside Traceloop.init initialize them with an empty dict if None (headers =
headers or {}) so each call gets a fresh dict; update any docstrings and type
hints accordingly and ensure callers are unaffected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a0e3f6f-4554-45f6-9761-037b35fb8bfd

📥 Commits

Reviewing files that changed from the base of the PR and between 7541daa and b7c4747.

📒 Files selected for processing (11)
  • packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py
  • packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py
  • packages/opentelemetry-instrumentation-groq/opentelemetry/instrumentation/groq/__init__.py
  • packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py
  • packages/opentelemetry-instrumentation-sagemaker/opentelemetry/instrumentation/sagemaker/__init__.py
  • packages/opentelemetry-instrumentation-together/opentelemetry/instrumentation/together/__init__.py
  • packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py
  • packages/traceloop-sdk/tests/test_sdk_initialization.py
  • packages/traceloop-sdk/traceloop/sdk/__init__.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

Comment on lines +648 to 669
def __init__(
self,
exception_logger=None,
use_attributes: bool = True,
use_legacy_attributes=_USE_ATTRIBUTES_UNSET,
):
super().__init__()
if use_legacy_attributes is not _USE_ATTRIBUTES_UNSET:
warnings.warn(
"`use_legacy_attributes` is deprecated and will be removed in a "
"future release; use `use_attributes` instead. The current OTel "
"GenAI spec emits prompts/completions as span attributes "
"(`gen_ai.input.messages` / `gen_ai.output.messages`), which is "
"what `use_attributes=True` (the default) does. "
"`use_attributes=False` opts into the events path instead.",
DeprecationWarning,
stacklevel=2,
)
use_attributes = use_legacy_attributes
Config.exception_logger = exception_logger
Config.use_legacy_attributes = use_legacy_attributes
Config.use_legacy_attributes = use_attributes

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify definition/call mismatch for _emit_response_events
rg -n -C2 'def _emit_response_events\(|_emit_response_events\(' \
  packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 525


🏁 Script executed:

# Get full context around function definition (line 365)
sed -n '360,375p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 537


🏁 Script executed:

# Get full context around the first call site (line 520)
sed -n '515,535p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 616


🏁 Script executed:

# Get full context around the second call site (line 539)
sed -n '535,555p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 585


🏁 Script executed:

# Search for emit_event function to see what parameters it expects
rg -n -A 5 'def emit_event' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 47


🏁 Script executed:

# Search for `@dont_throw` decorator usage around the calls
rg -n -B 3 '_handle_response|_emit_response_events' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py | head -40

Repository: traceloop/openllmetry

Length of output: 668


🏁 Script executed:

# Get the full _emit_response_events function to see all emit_event calls
sed -n '365,375p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 424


🏁 Script executed:

# Verify that the second call site at line 539 also shows the mismatch
sed -n '538,547p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 386


🏁 Script executed:

# Check if there are more lines after 547 for the second call site
sed -n '538,550p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py

Repository: traceloop/openllmetry

Length of output: 469


🏁 Script executed:

# Verify the emit_event signature to confirm it expects event_logger
rg -n -B 2 -A 3 'def emit_event' packages/opentelemetry-instrumentation-watsonx/

Repository: traceloop/openllmetry

Length of output: 811


🏁 Script executed:

# Check if there's a second argument after the comma at line 546
sed -n '539,550p' packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py | cat -A

Repository: traceloop/openllmetry

Length of output: 435


use_attributes=False now exposes a broken Watsonx events path for non-stream responses.

With this constructor change, events mode is easier to reach, but Line 520 calls _emit_response_events(responses, event_logger) while the function at line 365 is defined with a single parameter. Line 539 similarly calls it with a response dict but omits the event_logger. Both calls occur within @dont_throw-decorated functions, so the resulting TypeError is silently swallowed, causing completion events to never be emitted.

💡 Proposed fix
-def _emit_response_events(response: dict):
+def _emit_response_events(response: dict, event_logger):
     for i, message in enumerate(response.get("results", [])):
         emit_event(
             ChoiceEvent(
                 index=i,
                 message={"content": message.get("generated_text"), "role": "assistant"},
                 finish_reason=message.get("stop_reason", "unknown"),
-            )
+            ),
+            event_logger,
         )
-        _emit_response_events(
+        _emit_response_events(
             {
                 "results": [
                     {
                         "stop_reason": stream_stop_reason,
                         "generated_text": stream_generated_text,
                     }
                 ]
             },
+            event_logger,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py`
around lines 648 - 669, The _emit_response_events function currently has a
mismatched signature vs its call sites, causing a TypeError when callers pass an
event_logger; update the _emit_response_events definition to accept an optional
second parameter (e.g., def _emit_response_events(responses, event_logger=None))
and make its body robust to both single-response dicts and lists of responses
and to a None event_logger, so both callers (the dont_throw-decorated call that
does _emit_response_events(responses, event_logger) and the other call that
passes only responses) succeed without raising; ensure the function emits events
using event_logger when provided and safely no-ops or logs appropriately when it
is None.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid finding but pre-existing — not introduced by this PR. _emit_response_events had the broken signature on main before this branch was cut, and the bug only fires when use_attributes=False (events path) + a 2-arg call site is hit; even then it's silently swallowed by the surrounding
@dont_throw. This PR is scoped to the kwarg rename, so I'll leave the watsonx event-emission bug for a follow-up that can also fix the matching issue inside _emit_response_events itself (the emit_event(ChoiceEvent(...)) call also passes only one arg and would TypeError on its own).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@doronkopit5
Copy link
Copy Markdown
Member

Thanks @dvirski — the rename, the deprecation shim, the docstring, the fixture-based tests are all on point. Two concrete issues with the implementation though:

1. CI lint will fail — _USE_ATTRIBUTES_UNSET = object() placement triggers E402

In 8 of the 9 modified files the sentinel sits between stdlib imports and the rest, which ruff flags as E402 Module level import not at top of file. I ran ruff locally on the branch:

File Ruff errors
opentelemetry-instrumentation-anthropic/__init__.py 19
opentelemetry-instrumentation-bedrock/__init__.py 19
opentelemetry-instrumentation-groq/__init__.py 17
opentelemetry-instrumentation-langchain/__init__.py 15
opentelemetry-instrumentation-sagemaker/__init__.py 14
opentelemetry-instrumentation-together/__init__.py 14
opentelemetry-instrumentation-watsonx/__init__.py 15
traceloop-sdk/sdk/__init__.py + tracing.py 41 combined
opentelemetry-instrumentation-openai/__init__.py 0 (sentinel placed correctly below imports)

2. The whole sentinel machinery is overengineering — Optional[bool] = None is the idiomatic fix

None isn't a valid value for a bool flag, so it's a natural sentinel. Drops 9 module-level objects, sidesteps the import-ordering trap entirely, and reads cleaner:

def __init__(
    self,
    ...
    use_attributes: bool = True,
    use_legacy_attributes: Optional[bool] = None,
):
    if use_legacy_attributes is not None:
        warnings.warn(...)
        use_attributes = use_legacy_attributes

This also addresses a subtle behavior issue with the current shim: if a caller passes both kwargs — Traceloop.init(use_attributes=False, use_legacy_attributes=True) — the deprecated one silently clobbers the explicit new one. Worth deciding which should win (I'd vote: the new kwarg wins, or raise TypeError), and either way add a one-line test.

Nit

The same ~12-line warnings.warn(...) block is copy-pasted 9 times. A small helper (e.g. traceloop.sdk._deprecation.warn_use_legacy_attributes()) would keep the message in one place if it ever needs to change.

Everything else looks good to merge once these are addressed.

@dvirski dvirski force-pushed the dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init() branch from b7c4747 to 4f9b300 Compare May 18, 2026 16:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/traceloop-sdk/traceloop/sdk/__init__.py`:
- Around line 86-88: The docs incorrectly reference an EventLoggerProvider and
opentelemetry._events.set_event_logger_provider; update the text to say
Traceloop emits through the OpenTelemetry logging API
(opentelemetry._logs.get_logger(...)) and instruct users to configure a
LoggerProvider via opentelemetry._logs.set_logger_provider (or their
OpenTelemetry logging SDK) so log-backed events and prompt/completion data are
recorded when use_attributes=False; change the mention of
EventLoggerProvider/set_event_logger_provider to
LoggerProvider/set_logger_provider and call out opentelemetry._logs.get_logger
as the emission point.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c5a168d-51ba-4c0a-9493-235880cf2ac5

📥 Commits

Reviewing files that changed from the base of the PR and between b7c4747 and 4f9b300.

📒 Files selected for processing (11)
  • packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py
  • packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py
  • packages/opentelemetry-instrumentation-groq/opentelemetry/instrumentation/groq/__init__.py
  • packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py
  • packages/opentelemetry-instrumentation-sagemaker/opentelemetry/instrumentation/sagemaker/__init__.py
  • packages/opentelemetry-instrumentation-together/opentelemetry/instrumentation/together/__init__.py
  • packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py
  • packages/traceloop-sdk/tests/test_sdk_initialization.py
  • packages/traceloop-sdk/traceloop/sdk/__init__.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/traceloop-sdk/tests/test_sdk_initialization.py

Comment thread packages/traceloop-sdk/traceloop/sdk/__init__.py
@dvirski
Copy link
Copy Markdown
Contributor Author

dvirski commented May 19, 2026

@doronkopit5

Fixed last comments:

summary of what was fixed according to your comment numbers:

  1. CI lint failure — _USE_ATTRIBUTES_UNSET = object() triggering E402
    Fixed. Removed all 9 module-level _USE_ATTRIBUTES_UNSET = object() sentinels by switching to the Optional[bool] = None pattern (see chore: nx migration #2). ruff check now passes clean across all 9 modified files. While I was in tests/test_sdk_initialization.py I also cleaned up two pre-existing unused/redefined
    imports (SimpleSpanProcessor, BatchSpanProcessor) that were also tripping ruff.

  2. Sentinel machinery is overengineering — Optional[bool] = None is idiomatic
    Fixed. Both kwargs are now Optional[bool] = None. Resolution logic in all 9 sites:
    if use_attributes is not None and use_legacy_attributes is not None:
    raise TypeError(
    "Cannot pass both use_attributes and use_legacy_attributes; "
    "use_legacy_attributes is deprecated, use use_attributes instead."
    )
    if use_legacy_attributes is not None:
    warnings.warn(..., DeprecationWarning, stacklevel=2)
    use_attributes = use_legacy_attributes
    if use_attributes is None:
    use_attributes = True

On the both-kwargs question: went with raise TypeError rather than "new wins". Passing both is a programmer error, and a loud failure beats either kwarg silently winning. Added a test (test_passing_both_kwargs_raises_type_error) asserting pytest.raises(TypeError, match="Cannot pass both").

Nit — duplicated 12-line warning across 9 sites
Not extracted, intentionally. Two reasons:

  1. Dependency direction. Today the dependency flow is one-way: traceloop-sdk depends on the instrumentor packages. Putting a shared helper in traceloop.sdk._deprecation would force the 8 instrumentor packages to depend on the SDK, which inverts the current dependency direction and would create a
    cycle.
  2. Package overhead for a new package only for this,
    The duplication isn't free, but in this case the cure (new files or a circular dep) is worse than the disease. Once the alias is removed in the next major, all 9 copies go away together.

stacklevel=2,
)
use_attributes = use_legacy_attributes
if use_attributes is None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This means the use_attributes should have a default value of True - it should not be optional

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

▎ Good catch on the signature — fair to question why use_attributes is typed Optional[bool] when conceptually it's a bool defaulting to True.

▎ The None default isn't because None is a meaningful runtime value — it's a marker for "caller didn't pass this kwarg." I need that marker to detect when a caller passes both use_attributes and the deprecated use_legacy_attributes in the same call, and raise TypeError (per Doron's earlier ask —
▎ silent clobber of an explicit kwarg is sneaky). The check is:

▎ if use_attributes is not None and use_legacy_attributes is not None:
▎ raise TypeError("Cannot pass both ...")

▎ If I default use_attributes to True, I lose the ability to tell apart init(use_legacy_attributes=False) from init(use_attributes=True, use_legacy_attributes=False) — both look the same inside the method body, and the both-passed TypeError becomes impossible to detect cleanly.

▎ The alternatives I considered:
▎ 1. Module-level _UNSET = object() sentinel — what I originally had; Doron flagged it as overengineering (and it triggered E402 in 8 of 9 files).
▎ 2. **kwargs to absorb the deprecated alias — kills IDE autocomplete + discoverability of use_legacy_attributes.
▎ 3. Lie in the annotation (bool = True but actually default to a sentinel) — breaks inspect.signature and mypy.

▎ So Optional[bool] = None ended up being the least-bad option: it's a known Python idiom for "unset", costs no extra symbols, and matches what Optional semantically means here ("either a bool, or the absence of one"). Once we remove the use_legacy_attributes alias in the next major, this can
▎ collapse to use_attributes: bool = True.

▎ Happy to add an inline code comment explaining this if you think it'd help future readers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I get the argument. But still I think you are aming to the final situation after the deprecation that param looks like:
use_attributes: bool = True

If this is the case then the logic should be to check if the legacy is defined, warn about it and deal with it..

Your choice

@dvirski dvirski force-pushed the dr/feat(sdk)-expose-use_legacy_attributes-via-Traceloop.init() branch from 4f9b300 to 3da01e5 Compare May 19, 2026 09:26
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/traceloop-sdk/tests/test_sdk_initialization.py`:
- Around line 249-259: The test
test_use_legacy_attributes_false_propagates_to_instrumentors should ensure
TracerWrapper singleton is reset before calling Traceloop.init to avoid
order-dependent flakes; wrap the initialization/assertions using the existing
isolated_tracer_wrapper fixture (or explicitly reset TracerWrapper.instance) so
Traceloop.init executes its init path, then call
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) and assert
OpenAIConfig.use_legacy_attributes and AnthropicConfig.use_legacy_attributes are
as expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e9e9601-d694-495b-9ea3-baf290f8bce8

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9b300 and 3da01e5.

⛔ Files ignored due to path filters (2)
  • packages/opentelemetry-instrumentation-langchain/uv.lock is excluded by !**/*.lock
  • packages/traceloop-sdk/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py
  • packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/__init__.py
  • packages/opentelemetry-instrumentation-groq/opentelemetry/instrumentation/groq/__init__.py
  • packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/__init__.py
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py
  • packages/opentelemetry-instrumentation-sagemaker/opentelemetry/instrumentation/sagemaker/__init__.py
  • packages/opentelemetry-instrumentation-together/opentelemetry/instrumentation/together/__init__.py
  • packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/__init__.py
  • packages/traceloop-sdk/tests/test_sdk_initialization.py
  • packages/traceloop-sdk/traceloop/sdk/__init__.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/opentelemetry-instrumentation-sagemaker/opentelemetry/instrumentation/sagemaker/init.py
  • packages/opentelemetry-instrumentation-bedrock/opentelemetry/instrumentation/bedrock/init.py
  • packages/traceloop-sdk/traceloop/sdk/init.py
  • packages/opentelemetry-instrumentation-langchain/opentelemetry/instrumentation/langchain/init.py
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/init.py
  • packages/opentelemetry-instrumentation-groq/opentelemetry/instrumentation/groq/init.py
  • packages/opentelemetry-instrumentation-together/opentelemetry/instrumentation/together/init.py
  • packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/init.py
  • packages/opentelemetry-instrumentation-watsonx/opentelemetry/instrumentation/watsonx/init.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

Comment on lines +249 to +259
def test_use_legacy_attributes_false_propagates_to_instrumentors():
"""use_legacy_attributes=False passed to Traceloop.init() must reach each
instrumentor's Config — otherwise users have no way to opt into the new
event-based format through the SDK."""
from opentelemetry.instrumentation.openai.shared.config import Config as OpenAIConfig
from opentelemetry.instrumentation.anthropic.config import Config as AnthropicConfig

Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True)

assert OpenAIConfig.use_legacy_attributes is True
assert AnthropicConfig.use_legacy_attributes is True
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add singleton isolation to this init test to avoid order-dependent flakes.

This test calls Traceloop.init() but does not use isolated_tracer_wrapper, unlike the other new init-flag tests. If TracerWrapper.instance is already set by a prior test, this can silently bypass the init path and make assertions non-deterministic.

Suggested fix
-def test_use_legacy_attributes_false_propagates_to_instrumentors():
+def test_use_legacy_attributes_false_propagates_to_instrumentors(isolated_tracer_wrapper):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_use_legacy_attributes_false_propagates_to_instrumentors():
"""use_legacy_attributes=False passed to Traceloop.init() must reach each
instrumentor's Configotherwise users have no way to opt into the new
event-based format through the SDK."""
from opentelemetry.instrumentation.openai.shared.config import Config as OpenAIConfig
from opentelemetry.instrumentation.anthropic.config import Config as AnthropicConfig
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True)
assert OpenAIConfig.use_legacy_attributes is True
assert AnthropicConfig.use_legacy_attributes is True
def test_use_legacy_attributes_false_propagates_to_instrumentors(isolated_tracer_wrapper):
"""use_legacy_attributes=False passed to Traceloop.init() must reach each
instrumentor's Configotherwise users have no way to opt into the new
event-based format through the SDK."""
from opentelemetry.instrumentation.openai.shared.config import Config as OpenAIConfig
from opentelemetry.instrumentation.anthropic.config import Config as AnthropicConfig
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True)
assert OpenAIConfig.use_legacy_attributes is True
assert AnthropicConfig.use_legacy_attributes is True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/tests/test_sdk_initialization.py` around lines 249 -
259, The test test_use_legacy_attributes_false_propagates_to_instrumentors
should ensure TracerWrapper singleton is reset before calling Traceloop.init to
avoid order-dependent flakes; wrap the initialization/assertions using the
existing isolated_tracer_wrapper fixture (or explicitly reset
TracerWrapper.instance) so Traceloop.init executes its init path, then call
Traceloop.init(exporter=InMemorySpanExporter(), disable_batch=True) and assert
OpenAIConfig.use_legacy_attributes and AnthropicConfig.use_legacy_attributes are
as expected.

@dvirski dvirski merged commit b39151b into main May 19, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Bug Report: No way to set use_legacy_attributes using TraceLoop.init()

4 participants