Skip to content

Commit 529a9a0

Browse files
bendruckerxrmx
authored andcommitted
botocore: document threading instrumentation for S3 multipart operations (open-telemetry#3999)
* botocore: document threading instrumentation for S3 multipart operations Document that users need to enable ThreadingInstrumentor alongside BotocoreInstrumentor for proper trace context propagation with S3 upload_file and download_file methods. * botocore: move thread context docs to module docstring only README.rst is not built into the documentation, so keep the threading context propagation docs only in __init__.py where autodoc will pick them up. * Update __init__.py * Update __init__.py --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 61641aa commit 529a9a0

File tree

6 files changed

+108
-6
lines changed

6 files changed

+108
-6
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import os
2222
import time
2323
from typing import Any, AsyncIterator, Awaitable, Iterator, Optional, Union
24+
import collections.abc
2425

2526
from google.genai.models import AsyncModels, Models
2627
from google.genai.models import t as transformers
@@ -44,6 +45,7 @@
4445
_OpenTelemetryStabilitySignalType,
4546
_StabilityMode,
4647
)
48+
from opentelemetry.instrumentation.utils import is_instrumentation_enabled
4749
from opentelemetry.semconv._incubating.attributes import (
4850
code_attributes,
4951
gen_ai_attributes,
@@ -708,6 +710,11 @@ def instrumented_generate_content(
708710
config: Optional[GenerateContentConfigOrDict] = None,
709711
**kwargs: Any,
710712
) -> GenerateContentResponse:
713+
if not is_instrumentation_enabled():
714+
return wrapped_func(
715+
self, model=model, contents=contents, config=config, **kwargs
716+
)
717+
711718
candidates = []
712719
helper = _GenerateContentInstrumentationHelper(
713720
self,
@@ -783,6 +790,11 @@ def instrumented_generate_content_stream(
783790
config: Optional[GenerateContentConfigOrDict] = None,
784791
**kwargs: Any,
785792
) -> Iterator[GenerateContentResponse]:
793+
if not is_instrumentation_enabled():
794+
for resp in wrapped_func(self, model=model, contents=contents, config=config, **kwargs):
795+
yield resp
796+
return
797+
786798
candidates: list[Candidate] = []
787799
helper = _GenerateContentInstrumentationHelper(
788800
self,
@@ -858,6 +870,10 @@ async def instrumented_generate_content(
858870
config: Optional[GenerateContentConfigOrDict] = None,
859871
**kwargs: Any,
860872
) -> GenerateContentResponse:
873+
if not is_instrumentation_enabled():
874+
return await wrapped_func(
875+
self, model=model, contents=contents, config=config, **kwargs
876+
)
861877
helper = _GenerateContentInstrumentationHelper(
862878
self,
863879
otel_wrapper,
@@ -933,6 +949,9 @@ async def instrumented_generate_content_stream(
933949
config: Optional[GenerateContentConfigOrDict] = None,
934950
**kwargs: Any,
935951
) -> Awaitable[AsyncIterator[GenerateContentResponse]]: # type: ignore
952+
if not is_instrumentation_enabled():
953+
return await wrapped_func(self, model=model, contents=contents, config=config, **kwargs)
954+
936955
helper = _GenerateContentInstrumentationHelper(
937956
self,
938957
otel_wrapper,

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
_OpenTelemetryStabilitySignalType,
3030
_StabilityMode,
3131
)
32+
from opentelemetry.instrumentation.utils import is_instrumentation_enabled
3233
from opentelemetry.semconv._incubating.attributes import (
3334
code_attributes,
3435
)
@@ -168,6 +169,9 @@ def _wrap_sync_tool_function(
168169
):
169170
@functools.wraps(tool_function)
170171
def wrapped_function(*args, **kwargs):
172+
if not is_instrumentation_enabled():
173+
return tool_function(*args, **kwargs)
174+
171175
span_name = _create_function_span_name(tool_function)
172176
attributes = _create_function_span_attributes(
173177
tool_function, args, kwargs, extra_span_attributes
@@ -193,6 +197,9 @@ def _wrap_async_tool_function(
193197
):
194198
@functools.wraps(tool_function)
195199
async def wrapped_function(*args, **kwargs):
200+
if not is_instrumentation_enabled():
201+
return await tool_function(*args, **kwargs)
202+
196203
span_name = _create_function_span_name(tool_function)
197204
attributes = _create_function_span_attributes(
198205
tool_function, args, kwargs, extra_span_attributes

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from google.genai.types import GenerateContentConfig
2121
from pydantic import BaseModel, Field
2222

23+
from opentelemetry.instrumentation import utils
2324
from opentelemetry.instrumentation._semconv import (
2425
_OpenTelemetrySemanticConventionStability,
2526
_OpenTelemetryStabilitySignalType,
@@ -448,3 +449,14 @@ def test_records_metrics_data(self):
448449
self.otel.assert_has_metrics_data_named(
449450
"gen_ai.client.operation.duration"
450451
)
452+
453+
def test_suppress_instrumentation(self):
454+
self.configure_valid_response(text="Yep, it works!")
455+
with utils.suppress_instrumentation():
456+
response = self.generate_content(
457+
model="gemini-2.0-flash", contents="Does this work?"
458+
)
459+
self.assertEqual(response.text, "Yep, it works!")
460+
self.otel.assert_does_not_have_span_named(
461+
"generate_content gemini-2.0-flash"
462+
)

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/streaming_base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import unittest
1616

17+
from opentelemetry.instrumentation import utils
18+
1719
from .base import TestCase
1820

1921

@@ -70,3 +72,16 @@ def test_includes_token_counts_in_span_aggregated_from_responses(self):
7072
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
7173
self.assertEqual(span.attributes["gen_ai.usage.input_tokens"], 9)
7274
self.assertEqual(span.attributes["gen_ai.usage.output_tokens"], 12)
75+
76+
def test_suppress_instrumentation(self):
77+
self.configure_valid_response(text="Yep, it works!")
78+
with utils.suppress_instrumentation():
79+
responses = self.generate_content(
80+
model="gemini-2.0-flash", contents="Does this work?"
81+
)
82+
self.assertEqual(len(responses), 1)
83+
response = responses[0]
84+
self.assertEqual(response.text, "Yep, it works!")
85+
self.otel.assert_does_not_have_span_named(
86+
"generate_content gemini-2.0-flash"
87+
)

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import google.genai.types as genai_types
1818

19+
from opentelemetry.instrumentation import utils
1920
from opentelemetry.instrumentation._semconv import (
2021
_OpenTelemetrySemanticConventionStability,
2122
_OpenTelemetryStabilitySignalType,
@@ -440,3 +441,35 @@ def somefunction(x, y=2):
440441
generated_span.attributes,
441442
)
442443
self.tearDown()
444+
445+
def test_suppress_tool_call_instrumentation(self):
446+
calls = []
447+
448+
def handle(*args, **kwargs):
449+
calls.append((args, kwargs))
450+
return "some result"
451+
452+
def somefunction(somearg):
453+
print("somearg=%s", somearg)
454+
455+
self.mock_generate_content.side_effect = handle
456+
self.client.models.generate_content(
457+
model="some-model-name",
458+
contents="Some content",
459+
config={
460+
"tools": [somefunction],
461+
},
462+
)
463+
self.assertEqual(len(calls), 1)
464+
config = calls[0][1]["config"]
465+
tools = config.tools
466+
wrapped_somefunction = tools[0]
467+
468+
self.assertIsNone(
469+
self.otel.get_span_named("execute_tool somefunction")
470+
)
471+
472+
with utils.suppress_instrumentation():
473+
wrapped_somefunction("someparam")
474+
475+
self.otel.assert_does_not_have_span_named("execute_tool somefunction")

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,36 @@
4242
ec2 = session.create_client("ec2", region_name="us-west-2")
4343
ec2.describe_instances()
4444
45+
Thread Context Propagation
46+
--------------------------
47+
48+
boto3's S3 ``upload_file`` and ``download_file`` methods use background threads
49+
for multipart transfers. To ensure trace context is propagated to these threads,
50+
also enable the threading instrumentation:
51+
52+
.. code:: python
53+
54+
from opentelemetry.instrumentation.threading import ThreadingInstrumentor
55+
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
56+
57+
ThreadingInstrumentor().instrument()
58+
BotocoreInstrumentor().instrument()
59+
60+
When using auto-instrumentation (``opentelemetry-instrument``), both instrumentors
61+
are enabled automatically if their packages are installed.
62+
4563
API
4664
---
4765
4866
The `instrument` method accepts the following keyword args:
4967
50-
tracer_provider (TracerProvider) - an optional tracer provider
51-
request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request
52-
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None
53-
response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
54-
this function signature is: def response_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
68+
* tracer_provider (``TracerProvider``) - an optional tracer provider
69+
* request_hook (``Callable[[Span, str, str, dict], None]``) - a function with extra user-defined logic to be performed before performing the request
70+
* response_hook (``Callable[[Span, str, str, dict], None]``) - a function with extra user-defined logic to be performed after performing the request
5571
5672
for example:
5773
58-
.. code: python
74+
.. code:: python
5975
6076
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
6177
import botocore.session

0 commit comments

Comments
 (0)