Skip to content

Conversation

@krisztianfekete
Copy link
Contributor

@krisztianfekete krisztianfekete commented Aug 11, 2025

  • I have added tests that cover my changes.
  • If adding a new instrumentation or changing an existing one, I've added screenshots from some observability platform showing the change.
  • PR name follows conventional commits format: feat(instrumentation): ... or fix(instrumentation): ....
  • (If applicable) I have updated the documentation accordingly.

Fixes #3234

Testing:

# Spans
{
    "name": "openai.chat",
    "context": {
        "trace_id": "959173fc9afc5cff00a09add2e8c5562",
        "span_id": "fd1c0e64841e0c39",
        "trace_flags": "01"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": "0000000000000000",
    "start_time": "1754931979114935927",
    "end_time": "1754931979409526420",
    "status": {
        "status_code": "ERROR"
    },
    "attributes": {
        "llm.request.type": "chat",
        "gen_ai.system": "openai",
        "gen_ai.request.model": "gpt-3.5-turbo",
        "llm.headers": "None",
        "llm.is_streaming": "False",
        "gen_ai.openai.api_base": "https://api.openai.com/v1/",
        "error.type": "AuthenticationError",
    }
}

{
    "name": "openai.completion",
    "context": {
        "trace_id": "d09d3206f5017a46bed4c9f8e7ac132e",
        "span_id": "731e33ffab224cd3",
        "trace_flags": "01"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": "0000000000000000",
    "start_time": "1754931979409775957",
    "end_time": "1754931979544994620",
    "status": {
        "status_code": "ERROR"
    },
    "attributes": {
        "llm.request.type": "completion",
        "gen_ai.system": "openai",
        "gen_ai.request.model": "gpt-3.5-turbo-instruct",
        "llm.headers": "None",
        "llm.is_streaming": "False",
        "gen_ai.openai.api_base": "https://api.openai.com/v1/",
        "error.type": "AuthenticationError",
    }
}

{
    "name": "openai.embeddings",
    "context": {
        "trace_id": "f1a29cf4d96072c8ed483429d4ea6a72",
        "span_id": "5643360ed25a51a7",
        "trace_flags": "01"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": "0000000000000000",
    "start_time": "1754931979545094827",
    "end_time": "1754931979681955587",
    "status": {
        "status_code": "ERROR"
    },
    "attributes": {
        "llm.request.type": "embedding",
        "gen_ai.system": "openai",
        "gen_ai.request.model": "text-embedding-ada-002",
        "llm.headers": "None",
        "llm.is_streaming": "False",
        "gen_ai.openai.api_base": "https://api.openai.com/v1/",
        "error.type": "AuthenticationError",
    }
}

# Events

{
    "trace_id": "959173fc9afc5cff00a09add2e8c5562",
    "span_id": "fd1c0e64841e0c39",
    "trace_flags": "01",
    "severity_text": "NOTSET",
    "body": {'content': 'test chat'},
    "timestamp": "1754931979115121674",
    "attributes": {
        "gen_ai.system": "openai",
        "event.name": "gen_ai.user.message",
    }
}

{
    "trace_id": "d09d3206f5017a46bed4c9f8e7ac132e",
    "span_id": "731e33ffab224cd3",
    "trace_flags": "01",
    "severity_text": "NOTSET",
    "body": {'content': 'test completion'},
    "timestamp": "1754931979409878914",
    "attributes": {
        "gen_ai.system": "openai",
        "event.name": "gen_ai.user.message",
    }
}

{
    "trace_id": "f1a29cf4d96072c8ed483429d4ea6a72",
    "span_id": "5643360ed25a51a7",
    "trace_flags": "01",
    "severity_text": "NOTSET",
    "body": {'content': 'test embedding'},
    "timestamp": "1754931979545139868",
    "attributes": {
        "gen_ai.system": "openai",
        "event.name": "gen_ai.user.message",
    }
}

Important

Fixes span ID propagation in OpenAI instrumentation wrappers by setting the current context with trace.use_span() and ensuring proper span closure and error handling.

  • Behavior:
    • Use trace.use_span() to set the current context in chat_wrapper() and achat_wrapper() in chat_wrappers.py, completion_wrapper() and acompletion_wrapper() in completion_wrappers.py, and messages_list_wrapper() and runs_create_and_stream_wrapper() in assistant_wrappers.py.
    • Ensure spans are ended correctly after handling exceptions and responses.
  • Error Handling:
    • Record exceptions and set error status in spans in chat_wrappers.py, completion_wrappers.py, and assistant_wrappers.py.
  • Streaming:
    • Handle streaming responses with _build_from_streaming_response() and _abuild_from_streaming_response() in completion_wrappers.py and chat_wrappers.py.
    • Use EventHandleWrapper for event handling in runs_create_and_stream_wrapper() in assistant_wrappers.py.

This description was created by Ellipsis for 814e6f1. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • More reliable trace context propagation across chat, completion, and assistant flows, including streaming.
    • Deterministic streaming lifecycle with sync/async context manager and iterator support.
    • Adds richer telemetry: records system/model metadata and consistently emits events or span attributes.
  • Bug Fixes

    • Corrected span lifecycle: spans remain active through requests/streams, record exceptions, set error status, and finalize reliably with consistent metrics.
  • Tests

    • Added tests validating span and log context propagation for instrumented OpenAI interactions.

@CLAassistant
Copy link

CLAassistant commented Aug 11, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Walkthrough

Adds explicit OpenTelemetry span context management across OpenAI wrappers, moves streaming handling inside active span contexts, hardens ChatStream lifecycle with context/iterator hooks and thread-safe cleanup, and adds tests verifying emitted events carry span/trace IDs.

Changes

Cohort / File(s) Summary
Trace context propagation in shared wrappers
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py, packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py
Wrap core request/response and streaming flows in trace.use_span(span, end_on_exit=False); record exceptions/status on spans; move streaming lifecycle inside span context; integrate span lifecycle with streaming builders.
ChatStream lifecycle & streaming management
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
Add lifecycle hooks and iterator protocols (__del__, context managers, sync/async iter methods); add thread-safe idempotent cleanup (_cleanup_lock/_cleanup_completed); ensure partial metrics and span finalization on stream completion or cleanup.
Assistant wrappers & metadata handling
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py
Use trace.use_span around assistants flows; add assistant enrichment/caching, record LLM system/model attributes, restructure event vs attribute recording with prompt/completion indexing, and wrap handlers via EventHandleWrapper to propagate span.
Tests for span context propagation
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py
New tests and helper to assert that emitted LogData events contain trace_id/span_id matching finished spans for mocked OpenAI calls.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Wrapper as OpenAI Wrapper
  participant OTel as OTel Tracer
  participant OpenAI as OpenAI SDK

  App->>Wrapper: invoke()
  Wrapper->>OTel: start_span()
  Wrapper->>OTel: use_span(span, end_on_exit=false)
  Wrapper->>OpenAI: request (within active span)
  OpenAI-->>Wrapper: response / streaming handle
  Wrapper->>OTel: record attrs/events/errors
  Wrapper-->>App: response or ChatStream
  Note right of ChatStream: ChatStream operations keep span active until cleanup/end
Loading
sequenceDiagram
  participant App
  participant Wrapper
  participant ChatStream
  participant OTel
  participant OpenAI

  App->>Wrapper: invoke(stream=True)
  Wrapper->>OTel: start_span + use_span
  Wrapper->>OpenAI: start streaming
  Wrapper-->>App: return ChatStream (span-linked)
  loop consumer reads
    App->>ChatStream: next()/anext()
    ChatStream->>OpenAI: read chunk
    OpenAI-->>ChatStream: chunk
    ChatStream->>OTel: record partial metrics/events
  end
  ChatStream->>OTel: finalize metrics, set status, end span
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
Ensure OpenAI events include SpanID/TraceID when emitting prompts and outputs (#3234)
Use current-span management so EventLogger sees active span (#3234)
Maintain span context during streaming so streamed events/logs correlate (#3234)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Add assistant enrichment path and caching (.../v1/assistant_wrappers.py, multiple locations) Enrichment/caching is unrelated to span propagation objectives in #3234.
Record new LLM_SYSTEM and model attributes on spans (.../v1/assistant_wrappers.py, multiple lines) Attribute/schema additions are not necessary to fix missing span IDs on events.
Restructure event gating and prompt/completion indexing (.../v1/assistant_wrappers.py, multiple lines) Event routing/indexing changes extend beyond the goal of ensuring events carry span IDs.

Possibly related PRs

Suggested reviewers

  • nirga

Poem

I traced a whisker through the stream so bright,
Each event now hops with span ID in sight.
Tokens tick softly as ChatStream tends the flow,
Cleanup locks click — no stray spans to stow.
A rabbit logs the trail and watches traces grow. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@krisztianfekete krisztianfekete marked this pull request as ready for review August 11, 2025 18:04
Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

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

Important

Looks good to me! 👍

Reviewed everything up to 814e6f1 in 1 minute and 26 seconds. Click for details.
  • Reviewed 739 lines of code in 3 files
  • Skipped 0 files when reviewing.
  • Skipped posting 3 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py:92
  • Draft comment:
    Good use of 'with trace.use_span(span, end_on_exit=False)' to ensure the span is set in the current context so events pick up the proper span IDs. The error and streaming handling appear consistent.
  • Reason this comment was not posted:
    Confidence changes required: 0% <= threshold 50% None
2. packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py:59
  • Draft comment:
    The completion wrappers now wrap the processing inside a 'with trace.use_span(span, end_on_exit=False)' block. This ensures that events emitted via _handle_request/_handle_response have access to the current span context. Looks good.
  • Reason this comment was not posted:
    Confidence changes required: 0% <= threshold 50% None
3. packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py:312
  • Draft comment:
    In runs_create_and_stream_wrapper, the span is only ended in the exception branch. Please verify that for successful streaming responses the span is ended appropriately (perhaps via the event handler) to avoid leaving spans open.
  • Reason this comment was not posted:
    Confidence changes required: 33% <= threshold 50% None

Workflow ID: wflow_KcNOadfbkwjyKpvt

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

Copy link

@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: 5

🔭 Outside diff range comments (5)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (1)

264-273: Defensive iteration over choices to avoid NoneType errors

If an SDK chunk lacks 'choices', iterating over None raises. Guard with default empty list.

-    for choice in item.get("choices"):
+    for choice in (item.get("choices") or []):
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (4)

642-645: Async context manager does not trigger cleanup; spans may leak when using async with

aexit does not call _ensure_cleanup(); spans may remain open and metrics unfinalized for async context usage.

-    async def __aexit__(self, exc_type, exc_val, exc_tb):
-        await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        cleanup_exception = None
+        try:
+            self._ensure_cleanup()
+        except Exception as e:
+            cleanup_exception = e
+            logger.debug("Error during ChatStream cleanup in __aexit__: %s", cleanup_exception)
+        await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)

707-752: Keep span current during streaming finalization to ensure events carry span_id

emit_event in _process_complete_response runs after the outer with trace.use_span has exited. Wrap the body in a use_span context.

     def _process_complete_response(self):
-        _set_streaming_token_metrics(
+        with trace.use_span(self._span, end_on_exit=False):
+            _set_streaming_token_metrics(
                 self._request_kwargs,
                 self._complete_response,
                 self._span,
                 self._token_counter,
                 self._shared_attributes(),
             )
             # choice metrics
             if self._choice_counter and self._complete_response.get("choices"):
                 _set_choice_counter_metrics(
                     self._choice_counter,
                     self._complete_response.get("choices"),
                     self._shared_attributes(),
                 )
             # duration metrics
             if self._start_time and isinstance(self._start_time, (float, int)):
                 duration = time.time() - self._start_time
             else:
                 duration = None
             if duration and isinstance(duration, (float, int)) and self._duration_histogram:
                 self._duration_histogram.record(
                     duration, attributes=self._shared_attributes()
                 )
             if self._streaming_time_to_generate and self._time_of_first_token:
                 self._streaming_time_to_generate.record(
                     time.time() - self._time_of_first_token,
                     attributes=self._shared_attributes(),
                 )
             _set_response_attributes(self._span, self._complete_response)
             if should_emit_events():
                 for choice in self._complete_response.get("choices", []):
                     emit_event(_parse_choice_event(choice))
             else:
                 if should_send_prompts():
                     _set_completions(
                         self._span, self._complete_response.get("choices"))
             self._span.set_status(Status(StatusCode.OK))
             self._span.end()
             self._cleanup_completed = True

828-896: Wrap streaming builders in trace.use_span
The streaming builder functions in chat_wrappers.py emit events and record metrics outside of the span’s active context. Enclose their entire processing—from initializing complete_response through span.end()—in a with trace.use_span(span, end_on_exit=False): block to ensure that all child events and metrics are correctly attributed.

• File: packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py

  • Lines 828–896: _build_from_streaming_response
  • Lines 899–967: _abuild_from_streaming_response

Example diff for _build_from_streaming_response:

 def _build_from_streaming_response(
     span,
     response,
     instance=None,
     token_counter=None,
     choice_counter=None,
     duration_histogram=None,
     streaming_time_to_first_token=None,
     streaming_time_to_generate=None,
     start_time=None,
     request_kwargs=None,
 ):
-    complete_response = {"choices": [], "model": "", "id": ""}
+    with trace.use_span(span, end_on_exit=False):
+        complete_response = {"choices": [], "model": "", "id": ""}
         first_token = True
         time_of_first_token = start_time  # will be updated when first token is received

         for item in response:
@@
-    _set_response_attributes(span, complete_response)
+        _set_response_attributes(span, complete_response)
@@
-    span.set_status(Status(StatusCode.OK))
-    span.end()
+        span.set_status(Status(StatusCode.OK))
+        span.end()

Apply the same pattern to _abuild_from_streaming_response. Ensure you have from opentelemetry import trace imported at the top of the file.


651-663: Enhance exception recording in chat stream wrappers
Ensure that on streaming errors (non-StopIteration/StopAsyncIteration), the span records both the exception and its type before setting the error status.

Locations to update:

  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py, in both __next__ (lines ~651–663) and __anext__ (lines ~667–681).

Suggested diff:

--- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
+++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
@@ def __next__(self):
-                self._ensure_cleanup()
-                if self._span and self._span.is_recording():
-                    self._span.set_status(Status(StatusCode.ERROR, str(e)))
+                self._ensure_cleanup()
+                if self._span and self._span.is_recording():
+                    # Record exception type and details for better diagnostics
+                    self._span.set_attribute(ERROR_TYPE, e.__class__.__name__)
+                    self._span.record_exception(e)
+                    self._span.set_status(Status(StatusCode.ERROR, str(e)))
@@ async def __anext__(self):
-                self._ensure_cleanup()
-                if self._span and self._span.is_recording():
-                    self._span.set_status(Status(StatusCode.ERROR, str(e)))
+                self._ensure_cleanup()
+                if self._span and self._span.is_recording():
+                    # Record exception type and details for better diagnostics
+                    self._span.set_attribute(ERROR_TYPE, e.__class__.__name__)
+                    self._span.record_exception(e)
+                    self._span.set_status(Status(StatusCode.ERROR, str(e)))

These changes align with other OpenAI wrappers (e.g., completion_wrappers.py) and leverage the existing ERROR_TYPE import.

🧹 Nitpick comments (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (2)

76-79: Consider setting OK status for non-streaming path before ending

For consistency with streaming path, you may want to set span.set_status(Status(StatusCode.OK)) before span.end() when not streaming.

-    _handle_response(response, span, instance)
-    span.end()
+    _handle_response(response, span, instance)
+    span.set_status(Status(StatusCode.OK))
+    span.end()

Also applies to: 112-116


132-139: Optional: include role in prompt events for consistency

Completion prompts are emitted without a role. If helpful for consumers, emit with a "user" role for prompt strings.

-            emit_event(MessageEvent(content=p))
+            emit_event(MessageEvent(content=p, role="user"))
...
-        emit_event(MessageEvent(content=prompt))
+        emit_event(MessageEvent(content=prompt, role="user"))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between feb570b and 814e6f1.

📒 Files selected for processing (3)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (3)
  • _handle_request (267-288)
  • _build_from_streaming_response (828-895)
  • _abuild_from_streaming_response (899-966)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py (1)
  • is_streaming_response (331-339)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py (2)
  • run_async (154-165)
  • is_openai_v1 (20-21)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py (1)
  • is_streaming_response (331-339)
🔇 Additional comments (4)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (1)

59-71: Good: keep span current + robust error handling

Using trace.use_span with end_on_exit=False correctly makes the span current during request/response and streaming setup; exception path records ERROR_TYPE, status, and ends the span. This aligns with the PR objective.

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (2)

132-140: Good: span made current around message processing

Wrapping messages_list_wrapper logic in trace.use_span ensures all emit_event calls and span attribute mutations have the correct current span.


317-326: Good: error path ends span and re-raises

Exception logging and span finalization are correct here; the span is ended and the error is propagated.

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (1)

119-163: Good: use_span around request/response, metrics, and error handling

Both sync and async wrappers correctly keep span current and finalize on success/error. This addresses the core PR issue for non-streaming flows.

Also applies to: 190-218

Comment on lines +92 to +95
# Use the span as current context to ensure events get proper trace context
with trace.use_span(span, end_on_exit=False):
run_async(_handle_request(span, kwargs, instance))
try:
start_time = time.time()
response = wrapped(*args, **kwargs)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Context not propagated into new thread used by run_async; emit_event may still miss span_id

run_async executes the coroutine in a new thread when a loop is running. The current runtime context (and thus the current span set by trace.use_span) is not automatically propagated to that thread. emit_event inside _handle_request will then lack a current span.

Fix either by propagating the context into the thread here, or by updating utils.run_async to attach a captured context.

Minimal in-place fix:

-        run_async(_handle_request(span, kwargs, instance))
+        # Ensure current runtime context (with the span) flows into the helper thread
+        current_ctx = context_api.get_current()
+        def _runner():
+            token = context_api.attach(current_ctx)
+            try:
+                import asyncio
+                asyncio.run(_handle_request(span, kwargs, instance))
+            finally:
+                context_api.detach(token)
+        import threading
+        t = threading.Thread(target=_runner)
+        t.start()
+        t.join()

Alternatively, enhance utils.run_async to accept and attach a context (preferred, single place to maintain).

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +72 to 77
if is_streaming_response(response):
# span will be closed after the generator is done
return _build_from_streaming_response(span, kwargs, response)
else:
_handle_response(response, span, instance)

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Streaming path loses current-context for downstream events; wrap generator bodies with trace.use_span

When you return the streaming generator, the with trace.use_span context is exited. emit_event calls inside _build_from_streaming_response/_abuild_from_streaming_response will then run without a current span, reintroducing the missing span_id problem for streaming events.

Wrap the generator bodies with trace.use_span(span, end_on_exit=False) to keep the span current throughout iteration and finalization.

Apply:

 def _build_from_streaming_response(span, request_kwargs, response):
-    complete_response = {"choices": [], "model": "", "id": ""}
-    for item in response:
-        yield item
-        _accumulate_streaming_response(complete_response, item)
-    _set_response_attributes(span, complete_response)
-    _set_token_usage(span, request_kwargs, complete_response)
-    if should_emit_events():
-        _emit_streaming_response_events(complete_response)
-    else:
-        if should_send_prompts():
-            _set_completions(span, complete_response.get("choices"))
-    span.set_status(Status(StatusCode.OK))
-    span.end()
+    # Keep span as current during the whole streaming lifecycle
+    with trace.use_span(span, end_on_exit=False):
+        complete_response = {"choices": [], "model": "", "id": ""}
+        for item in response:
+            yield item
+            _accumulate_streaming_response(complete_response, item)
+        _set_response_attributes(span, complete_response)
+        _set_token_usage(span, request_kwargs, complete_response)
+        if should_emit_events():
+            _emit_streaming_response_events(complete_response)
+        else:
+            if should_send_prompts():
+                _set_completions(span, complete_response.get("choices"))
+        span.set_status(Status(StatusCode.OK))
+        span.end()
 
 async def _abuild_from_streaming_response(span, request_kwargs, response):
-    complete_response = {"choices": [], "model": "", "id": ""}
-    async for item in response:
-        yield item
-        _accumulate_streaming_response(complete_response, item)
-    _set_response_attributes(span, complete_response)
-    _set_token_usage(span, request_kwargs, complete_response)
-    if should_emit_events():
-        _emit_streaming_response_events(complete_response)
-    else:
-        if should_send_prompts():
-            _set_completions(span, complete_response.get("choices"))
-    span.set_status(Status(StatusCode.OK))
-    span.end()
+    # Keep span as current during the whole streaming lifecycle
+    with trace.use_span(span, end_on_exit=False):
+        complete_response = {"choices": [], "model": "", "id": ""}
+        async for item in response:
+            yield item
+            _accumulate_streaming_response(complete_response, item)
+        _set_response_attributes(span, complete_response)
+        _set_token_usage(span, request_kwargs, complete_response)
+        if should_emit_events():
+            _emit_streaming_response_events(complete_response)
+        else:
+            if should_send_prompts():
+                _set_completions(span, complete_response.get("choices"))
+        span.set_status(Status(StatusCode.OK))
+        span.end()

Run this quick grep to spot any remaining emit_event sites in streaming code that run outside a use_span context:

Also applies to: 108-115, 183-201, 204-222


🏁 Script executed:

#!/bin/bash
rg -n "emit_event\(" -A 4 -B 6 packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py

Length of output: 1359


I’d like to verify whether any trace.use_span wrappers already surround non-streaming paths or other emit sites. Let’s search for existing use_span usages in this file:


🏁 Script executed:

rg -n "use_span" -A 3 -B 3 packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py

Length of output: 582


Wrap streaming generators in trace.use_span to preserve current span context

Returning the raw generator exits the outer use_span (though the span remains open), so any emit_event calls inside the streaming builders run without a current span. Enclose the bodies of both streaming helpers in a nested with trace.use_span(span, end_on_exit=False): block so that events, status updates, and span.end() execute under the correct context.

Affected locations in
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py:

  • _build_from_streaming_response
  • _abuild_from_streaming_response

Suggested patch:

 def _build_from_streaming_response(span, request_kwargs, response):
-    complete_response = {"choices": [], "model": "", "id": ""}
-    for item in response:
-        yield item
-        _accumulate_streaming_response(complete_response, item)
-    _set_response_attributes(span, complete_response)
-    _set_token_usage(span, request_kwargs, complete_response)
-    if should_emit_events():
-        _emit_streaming_response_events(complete_response)
-    else:
-        if should_send_prompts():
-            _set_completions(span, complete_response.get("choices"))
-    span.set_status(Status(StatusCode.OK))
-    span.end()
+    # Keep span current for events and finalization
+    with trace.use_span(span, end_on_exit=False):
+        complete_response = {"choices": [], "model": "", "id": ""}
+        for item in response:
+            yield item
+            _accumulate_streaming_response(complete_response, item)
+        _set_response_attributes(span, complete_response)
+        _set_token_usage(span, request_kwargs, complete_response)
+        if should_emit_events():
+            _emit_streaming_response_events(complete_response)
+        elif should_send_prompts():
+            _set_completions(span, complete_response.get("choices"))
+        span.set_status(Status(StatusCode.OK))
+        span.end()
 
 async def _abuild_from_streaming_response(span, request_kwargs, response):
-    complete_response = {"choices": [], "model": "", "id": ""}
-    async for item in response:
-        yield item
-        _accumulate_streaming_response(complete_response, item)
-    _set_response_attributes(span, complete_response)
-    _set_token_usage(span, request_kwargs, complete_response)
-    if should_emit_events():
-        _emit_streaming_response_events(complete_response)
-    else:
-        if should_send_prompts():
-            _set_completions(span, complete_response.get("choices"))
-    span.set_status(Status(StatusCode.OK))
-    span.end()
+    # Keep span current for events and finalization
+    with trace.use_span(span, end_on_exit=False):
+        complete_response = {"choices": [], "model": "", "id": ""}
+        async for item in response:
+            yield item
+            _accumulate_streaming_response(complete_response, item)
+        _set_response_attributes(span, complete_response)
+        _set_token_usage(span, request_kwargs, complete_response)
+        if should_emit_events():
+            _emit_streaming_response_events(complete_response)
+        elif should_send_prompts():
+            _set_completions(span, complete_response.get("choices"))
+        span.set_status(Status(StatusCode.OK))
+        span.end()
📝 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
if is_streaming_response(response):
# span will be closed after the generator is done
return _build_from_streaming_response(span, kwargs, response)
else:
_handle_response(response, span, instance)
def _build_from_streaming_response(span, request_kwargs, response):
# Keep span current for events and finalization
with trace.use_span(span, end_on_exit=False):
complete_response = {"choices": [], "model": "", "id": ""}
for item in response:
yield item
_accumulate_streaming_response(complete_response, item)
_set_response_attributes(span, complete_response)
_set_token_usage(span, request_kwargs, complete_response)
if should_emit_events():
_emit_streaming_response_events(complete_response)
elif should_send_prompts():
_set_completions(span, complete_response.get("choices"))
span.set_status(Status(StatusCode.OK))
span.end()
async def _abuild_from_streaming_response(span, request_kwargs, response):
# Keep span current for events and finalization
with trace.use_span(span, end_on_exit=False):
complete_response = {"choices": [], "model": "", "id": ""}
async for item in response:
yield item
_accumulate_streaming_response(complete_response, item)
_set_response_attributes(span, complete_response)
_set_token_usage(span, request_kwargs, complete_response)
if should_emit_events():
_emit_streaming_response_events(complete_response)
elif should_send_prompts():
_set_completions(span, complete_response.get("choices"))
span.set_status(Status(StatusCode.OK))
span.end()
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py
around lines 72 to 77, the streaming generator helpers
(_build_from_streaming_response and _abuild_from_streaming_response) return raw
generators which causes code executed inside them (emit_event, set_status,
span.end()) to run without the current span context; wrap the entire body of
each streaming helper in a nested with trace.use_span(span, end_on_exit=False):
block so the yielded generator executes with the span active, ensuring
emit_event/status/span.end() run under the correct context (apply the same
change to both sync and async variants).

Comment on lines +194 to +210
message_content = content[0].get("text").get("value")
message_role = msg.get("role")
if message_role in ["user", "system"]:
if should_emit_events():
emit_event(MessageEvent(content=message_content, role=message_role))
else:
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
message_role,
)
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
message_content,
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Brittle access to message content; handle non-text and empty content safely

content is assumed to be a list where content[0]["text"]["value"] exists. This can KeyError when first item is not "text" or content is empty.

-            message_content = content[0].get("text").get("value")
+            message_content = None
+            if isinstance(content, list) and content:
+                first = content[0]
+                if isinstance(first, dict) and isinstance(first.get("text"), dict):
+                    message_content = first["text"].get("value")
+            if message_content is None:
+                # Fallback: best-effort stringification
+                message_content = str(content)
📝 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
message_content = content[0].get("text").get("value")
message_role = msg.get("role")
if message_role in ["user", "system"]:
if should_emit_events():
emit_event(MessageEvent(content=message_content, role=message_role))
else:
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
message_role,
)
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
message_content,
)
message_content = None
if isinstance(content, list) and content:
first = content[0]
if isinstance(first, dict) and isinstance(first.get("text"), dict):
message_content = first["text"].get("value")
if message_content is None:
# Fallback: best-effort stringification
message_content = str(content)
message_role = msg.get("role")
if message_role in ["user", "system"]:
if should_emit_events():
emit_event(MessageEvent(content=message_content, role=message_role))
else:
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
message_role,
)
_set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
message_content,
)

Comment on lines +312 to +316
kwargs["event_handler"] = EventHandleWrapper(
original_handler=kwargs["event_handler"],
span=span,
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard event_handler wrapping to avoid KeyError

kwargs["event_handler"] may be absent. Wrap only if provided.

-        kwargs["event_handler"] = EventHandleWrapper(
-            original_handler=kwargs["event_handler"],
-            span=span,
-        )
+        if kwargs.get("event_handler"):
+            kwargs["event_handler"] = EventHandleWrapper(
+                original_handler=kwargs["event_handler"],
+                span=span,
+            )
📝 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
kwargs["event_handler"] = EventHandleWrapper(
original_handler=kwargs["event_handler"],
span=span,
)
if kwargs.get("event_handler"):
kwargs["event_handler"] = EventHandleWrapper(
original_handler=kwargs["event_handler"],
span=span,
)
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py
around lines 312 to 315, the code unconditionally assigns
kwargs["event_handler"] = EventHandleWrapper(...), which raises a KeyError when
"event_handler" is not present; change this to only wrap when the key exists
(e.g., if "event_handler" in kwargs and kwargs["event_handler"] is not None) or
use kwargs.get and conditionally set the wrapped handler, leaving kwargs
unchanged when absent.

@nirga nirga changed the title fix(instrumentation): Fixes context in wrappers to propagate span IDs properly fix(openai): Fixes context in wrappers to propagate span IDs properly Aug 11, 2025
Copy link
Member

@nirga nirga left a comment

Choose a reason for hiding this comment

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

Looks good overall! 2 requests:

  1. fix the one comment from coderabbit
  2. add a test :)

@krisztianfekete krisztianfekete requested a review from nirga August 12, 2025 05:17
Copy link

@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: 0

♻️ Duplicate comments (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (2)

195-196: Brittle content access needs error handling

The code assumes content[0].get("text").get("value") structure exists, which can cause KeyError if the first item isn't "text" type or if content is empty.

The previous review comment about brittle access to message content still applies. Consider adding safe access pattern:

-            message_content = content[0].get("text").get("value")
+            message_content = None
+            if isinstance(content, list) and content:
+                first = content[0]
+                if isinstance(first, dict) and isinstance(first.get("text"), dict):
+                    message_content = first["text"].get("value")
+            if message_content is None:
+                message_content = str(content)

313-316: EventHandleWrapper assignment needs safety check

The code unconditionally assigns kwargs["event_handler"] which will fail if the key doesn't exist.

The previous review comment about guarding the event_handler wrapping still applies:

-        kwargs["event_handler"] = EventHandleWrapper(
-            original_handler=kwargs["event_handler"],
-            span=span,
-        )
+        if kwargs.get("event_handler"):
+            kwargs["event_handler"] = EventHandleWrapper(
+                original_handler=kwargs["event_handler"],
+                span=span,
+            )
🧹 Nitpick comments (2)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (2)

4-4: Remove unused import

The MagicMock import is not used anywhere in the test file.

-from unittest.mock import MagicMock

35-41: Use contextlib.suppress for cleaner exception handling

The try-except-pass block can be simplified using contextlib.suppress for better readability.

+import contextlib
+
-    try:
-        mock_openai_client.chat.completions.create(
-            model="gpt-3.5-turbo",
-            messages=[{"role": "user", "content": "Test span context"}],
-        )
-    except Exception:
-        pass
+    with contextlib.suppress(Exception):
+        mock_openai_client.chat.completions.create(
+            model="gpt-3.5-turbo",
+            messages=[{"role": "user", "content": "Test span context"}],
+        )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 814e6f1 and fb7cda4.

📒 Files selected for processing (2)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (3)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • EventAttributes (267-286)
packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
  • mock_openai_client (47-51)
packages/traceloop-sdk/traceloop/sdk/utils/in_memory_span_exporter.py (1)
  • get_finished_spans (40-43)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (5)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (1)
  • Config (6-15)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py (2)
  • model_as_dict (342-352)
  • _set_span_attribute (30-37)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • SpanAttributes (64-257)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py (1)
  • should_emit_events (174-181)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py (1)
  • EventHandleWrapper (13-132)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py

4-4: unittest.mock.MagicMock imported but unused

Remove unused import: unittest.mock.MagicMock

(F401)


35-41: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)

🪛 Flake8 (7.2.0)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py

[error] 4-4: 'unittest.mock.MagicMock' imported but unused

(F401)

🔇 Additional comments (7)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (2)

14-28: Well-structured helper function for event validation

The helper function correctly validates both the trace/span ID propagation and ensures the event belongs to the OpenAI system. The assertions provide clear error messages for debugging.


30-62: Test effectively validates span context propagation

The test correctly verifies the core PR objective: ensuring events contain proper trace and span IDs that match their parent spans. The conditional checks handle test environment variations gracefully with clear skip messages.

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (5)

5-5: Good addition of trace import for context management

This import enables the use of trace.use_span() to set the current context, which is essential for the span ID propagation fix.


132-139: Proper trace context management for event propagation

The use of trace.use_span(span, end_on_exit=False) correctly sets the current context so that events emitted within this scope will have the proper trace and span IDs. The exception handling within the context ensures spans are properly ended even on errors.


186-188: Conditional event emission properly implemented

The code correctly gates event emission based on should_emit_events(). This addresses the previous review feedback about unconditional event emission.


260-327: Comprehensive trace context implementation for streaming

The implementation properly wraps the entire streaming operation within trace.use_span(), ensuring events from the EventHandleWrapper have correct trace context. The exception handling correctly ends the span and re-raises errors.


141-188: No span indexing inconsistencies detected

The prompt_index in assistant_wrappers.py starts at 0, is incremented once for the assistant’s instructions and once for the run’s instructions, matching the pattern in responses_wrappers.py (which also initializes at 0 and increments per prompt). Tests in test_azure.py expect the first prompt’s index to be 0, aligning with this implementation—no changes required.

@nirga nirga changed the title fix(openai): Fixes context in wrappers to propagate span IDs properly fix(openai): propagate span IDs properly to events Aug 12, 2025
Copy link

@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: 0

♻️ Duplicate comments (3)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (3)

195-210: Brittle content access pattern needs defensive handling.

The code assumes content[0].get("text").get("value") exists without validation. This can raise KeyError or AttributeError when the content structure differs from expectations.

This issue was identified in previous reviews and needs to be addressed with defensive handling for non-text content or empty arrays.


313-316: Guard against missing event_handler to prevent KeyError.

The code unconditionally wraps kwargs["event_handler"] without checking if the key exists, which will raise a KeyError when no event handler is provided.

This issue was identified in previous reviews and needs conditional wrapping only when the event_handler is present.


185-188: Ensure mutual exclusivity between event emission and span attributes for run instructions

The current logic always calls _set_span_attribute(...content, run["instructions"]) and then conditionally emits an event. It should mirror the pattern used elsewhere—emit the event or set both role and content attributes, not both.

Locations to update:

  • File packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py
  • Around lines 181–188

Suggested diff:

-        _set_span_attribute(
-            span,
-            f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
-            run["instructions"],
-        )
-        if should_emit_events():
-            emit_event(MessageEvent(content=run["instructions"], role="system"))
-        prompt_index += 1
+        if should_emit_events():
+            emit_event(MessageEvent(content=run["instructions"], role="system"))
+        else:
+            _set_span_attribute(
+                span,
+                f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
+                "system"
+            )
+            _set_span_attribute(
+                span,
+                f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
+                run["instructions"],
+            )
+        prompt_index += 1
🧹 Nitpick comments (1)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (1)

34-40: Consider using contextlib.suppress for cleaner exception handling.

The static analysis tool correctly identifies that the try-except-pass pattern can be simplified using contextlib.suppress(Exception) for better readability.

Apply this diff to simplify the exception handling:

+from contextlib import suppress
+
    # The mock_openai_client fixture should trigger instrumentation but not make real calls
-    try:
+    with suppress(Exception):
         mock_openai_client.chat.completions.create(
             model="gpt-3.5-turbo",
             messages=[{"role": "user", "content": "Test span context"}],
         )
-    except Exception:
-        pass
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d4a66e and 4738519.

📒 Files selected for processing (4)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (3 hunks)
  • packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/completion_wrappers.py
  • packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (5)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (1)
  • Config (6-15)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/__init__.py (1)
  • model_as_dict (342-352)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • SpanAttributes (64-257)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/utils.py (1)
  • should_emit_events (174-181)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py (1)
  • EventHandleWrapper (13-132)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (3)
packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
  • EventAttributes (267-286)
packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
  • mock_openai_client (47-51)
packages/traceloop-sdk/traceloop/sdk/utils/in_memory_span_exporter.py (1)
  • get_finished_spans (40-43)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py

34-40: Use contextlib.suppress(Exception) instead of try-except-pass

Replace with contextlib.suppress(Exception)

(SIM105)

🔇 Additional comments (4)
packages/opentelemetry-instrumentation-openai/tests/traces/test_span_context_propagation.py (2)

1-62: LGTM! Well-structured test for span context propagation.

The test implementation correctly validates that events contain proper trace and span IDs, which addresses the core issue described in the PR objectives. The use of skips for missing spans or logs provides graceful handling of test environment variations.


13-27: LGTM! Comprehensive event validation logic.

The assert_event_has_span_context function properly validates both trace context propagation and OpenAI event categorization. The error messages provide clear diagnostics for debugging test failures.

packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py (2)

132-139: LGTM! Proper span context management with error handling.

The implementation correctly uses trace.use_span() to set the current context as required for event propagation, and properly handles exceptions by recording them and setting error status before ending the span.


260-327: LGTM! Consistent trace context management in streaming wrapper.

The implementation properly establishes span context for event propagation and handles the EventHandleWrapper integration correctly. The exception handling follows the same pattern as the non-streaming wrapper.

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: OpenAI events don't contain span IDs

3 participants