Skip to content

Commit 1ae131e

Browse files
committed
Merge branch 'main' of https://github.com/open-telemetry/opentelemetry-python-contrib into weslayer/refactor-boto-core-span
2 parents 942fa6f + e2ba6d4 commit 1ae131e

File tree

38 files changed

+3431
-1358
lines changed

38 files changed

+3431
-1358
lines changed

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- `opentelemetry-instrumentation-system-metrics`: fix loading on Google Cloud Run
17+
([#3533](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3533))
1618
- `opentelemetry-instrumentation-fastapi`: fix wrapping of middlewares
1719
([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012))
20+
- `opentelemetry-instrumentation-starlette` Remove max version constraint on starlette
21+
([#3456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3456))
22+
- `opentelemetry-instrumentation-urllib3`: proper bucket boundaries in stable semconv http duration metrics
23+
([#3518](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3518))
24+
- `opentelemetry-instrumentation-urllib`: proper bucket boundaries in stable semconv http duration metrics
25+
([#3519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3519))
26+
- `opentelemetry-instrumentation-falcon`: proper bucket boundaries in stable semconv http duration
27+
([#3525](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3525))
28+
- `opentelemetry-instrumentation-wsgi`: add explicit http duration buckets for stable semconv
29+
([#3527](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3527))
30+
- `opentelemetry-instrumentation-asgi`: add explicit http duration buckets for stable semconv
31+
([#3526](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3526))
32+
- `opentelemetry-instrumentation-flask`: proper bucket boundaries in stable semconv http duration
33+
([#3523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3523))
34+
- `opentelemetry-instrumentation-django`: proper bucket boundaries in stable semconv http duration
35+
([#3524](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3524))
36+
- `opentelemetry-instrumentation-grpc`: support non-list interceptors
37+
([#3520](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3520))
38+
- `opentelemetry-instrumentation-botocore` Ensure spans end on early stream closure for Bedrock Streaming APIs
39+
([#3481](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3481))
1840

1941
### Breaking changes
2042

@@ -50,7 +72,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5072
- `opentelemetry-instrumentation-botocore` Capture server attributes for botocore API calls
5173
([#3448](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3448))
5274

53-
5475
## Version 1.32.0/0.53b0 (2025-04-10)
5576

5677
### Added

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration
4545
| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy >= 1.0.0, < 2.1.0 | Yes | development
4646
| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No | development
47-
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette >= 0.13, <0.15 | Yes | development
47+
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette >= 0.13 | Yes | development
4848
| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | development
4949
| [opentelemetry-instrumentation-threading](./opentelemetry-instrumentation-threading) | threading | No | development
5050
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes | development

instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,44 @@ async def hello(request):
5454
)
5555
from opentelemetry.propagate import extract
5656
from opentelemetry.propagators.textmap import Getter
57+
from opentelemetry.semconv._incubating.attributes.http_attributes import (
58+
HTTP_FLAVOR,
59+
HTTP_HOST,
60+
HTTP_METHOD,
61+
HTTP_ROUTE,
62+
HTTP_SCHEME,
63+
HTTP_SERVER_NAME,
64+
HTTP_STATUS_CODE,
65+
HTTP_TARGET,
66+
HTTP_URL,
67+
HTTP_USER_AGENT,
68+
)
69+
from opentelemetry.semconv._incubating.attributes.net_attributes import (
70+
NET_HOST_NAME,
71+
NET_HOST_PORT,
72+
)
5773
from opentelemetry.semconv.metrics import MetricInstruments
58-
from opentelemetry.semconv.trace import SpanAttributes
5974
from opentelemetry.trace.status import Status, StatusCode
6075
from opentelemetry.util.http import get_excluded_urls, remove_url_credentials
6176

6277
_duration_attrs = [
63-
SpanAttributes.HTTP_METHOD,
64-
SpanAttributes.HTTP_HOST,
65-
SpanAttributes.HTTP_SCHEME,
66-
SpanAttributes.HTTP_STATUS_CODE,
67-
SpanAttributes.HTTP_FLAVOR,
68-
SpanAttributes.HTTP_SERVER_NAME,
69-
SpanAttributes.NET_HOST_NAME,
70-
SpanAttributes.NET_HOST_PORT,
71-
SpanAttributes.HTTP_ROUTE,
78+
HTTP_METHOD,
79+
HTTP_HOST,
80+
HTTP_SCHEME,
81+
HTTP_STATUS_CODE,
82+
HTTP_FLAVOR,
83+
HTTP_SERVER_NAME,
84+
NET_HOST_NAME,
85+
NET_HOST_PORT,
86+
HTTP_ROUTE,
7287
]
7388

7489
_active_requests_count_attrs = [
75-
SpanAttributes.HTTP_METHOD,
76-
SpanAttributes.HTTP_HOST,
77-
SpanAttributes.HTTP_SCHEME,
78-
SpanAttributes.HTTP_FLAVOR,
79-
SpanAttributes.HTTP_SERVER_NAME,
90+
HTTP_METHOD,
91+
HTTP_HOST,
92+
HTTP_SCHEME,
93+
HTTP_FLAVOR,
94+
HTTP_SERVER_NAME,
8095
]
8196

8297
tracer = trace.get_tracer(__name__)
@@ -140,29 +155,27 @@ def collect_request_attributes(request: web.Request) -> Dict:
140155
http_url += "?" + urllib.parse.unquote(query_string)
141156

142157
result = {
143-
SpanAttributes.HTTP_SCHEME: request.scheme,
144-
SpanAttributes.HTTP_HOST: server_host,
145-
SpanAttributes.NET_HOST_PORT: port,
146-
SpanAttributes.HTTP_ROUTE: _get_view_func(request),
147-
SpanAttributes.HTTP_FLAVOR: f"{request.version.major}.{request.version.minor}",
148-
SpanAttributes.HTTP_TARGET: request.path,
149-
SpanAttributes.HTTP_URL: remove_url_credentials(http_url),
158+
HTTP_SCHEME: request.scheme,
159+
HTTP_HOST: server_host,
160+
NET_HOST_PORT: port,
161+
HTTP_ROUTE: _get_view_func(request),
162+
HTTP_FLAVOR: f"{request.version.major}.{request.version.minor}",
163+
HTTP_TARGET: request.path,
164+
HTTP_URL: remove_url_credentials(http_url),
150165
}
151166

152167
http_method = request.method
153168
if http_method:
154-
result[SpanAttributes.HTTP_METHOD] = http_method
169+
result[HTTP_METHOD] = http_method
155170

156171
http_host_value_list = (
157172
[request.host] if not isinstance(request.host, list) else request.host
158173
)
159174
if http_host_value_list:
160-
result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
161-
http_host_value_list
162-
)
175+
result[HTTP_SERVER_NAME] = ",".join(http_host_value_list)
163176
http_user_agent = request.headers.get("user-agent")
164177
if http_user_agent:
165-
result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent
178+
result[HTTP_USER_AGENT] = http_user_agent
166179

167180
# remove None values
168181
result = {k: v for k, v in result.items() if v is not None}
@@ -183,7 +196,7 @@ def set_status_code(span, status_code: int) -> None:
183196
)
184197
)
185198
else:
186-
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
199+
span.set_attribute(HTTP_STATUS_CODE, status_code)
187200
span.set_status(
188201
Status(http_status_to_status_code(status_code, server_span=True))
189202
)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
202202

203203
from opentelemetry import context, trace
204204
from opentelemetry.instrumentation._semconv import (
205+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
205206
_filter_semconv_active_request_count_attr,
206207
_filter_semconv_duration_attrs,
207208
_get_schema_url,
@@ -589,6 +590,7 @@ def __init__(
589590
name=HTTP_SERVER_REQUEST_DURATION,
590591
description="Duration of HTTP server requests.",
591592
unit="s",
593+
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
592594
)
593595
self.server_response_size_histogram = None
594596
if _report_old(sem_conv_opt_in_mode):

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import opentelemetry.instrumentation.asgi as otel_asgi
2424
from opentelemetry import trace as trace_api
2525
from opentelemetry.instrumentation._semconv import (
26+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
2627
OTEL_SEMCONV_STABILITY_OPT_IN,
2728
_OpenTelemetrySemanticConventionStability,
2829
_server_active_requests_count_attrs_new,
@@ -1245,6 +1246,7 @@ async def test_asgi_metrics(self):
12451246
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
12461247

12471248
async def test_asgi_metrics_new_semconv(self):
1249+
# pylint: disable=too-many-nested-blocks
12481250
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
12491251
self.seed_app(app)
12501252
await self.send_default_request()
@@ -1274,6 +1276,11 @@ async def test_asgi_metrics_new_semconv(self):
12741276
for point in data_points:
12751277
if isinstance(point, HistogramDataPoint):
12761278
self.assertEqual(point.count, 3)
1279+
if metric.name == "http.server.request.duration":
1280+
self.assertEqual(
1281+
point.explicit_bounds,
1282+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
1283+
)
12771284
histogram_data_point_seen = True
12781285
if isinstance(point, NumberDataPoint):
12791286
number_data_point_seen = True
@@ -1284,6 +1291,7 @@ async def test_asgi_metrics_new_semconv(self):
12841291
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
12851292

12861293
async def test_asgi_metrics_both_semconv(self):
1294+
# pylint: disable=too-many-nested-blocks
12871295
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
12881296
self.seed_app(app)
12891297
await self.send_default_request()
@@ -1313,6 +1321,11 @@ async def test_asgi_metrics_both_semconv(self):
13131321
for point in data_points:
13141322
if isinstance(point, HistogramDataPoint):
13151323
self.assertEqual(point.count, 3)
1324+
if metric.name == "http.server.request.duration":
1325+
self.assertEqual(
1326+
point.explicit_bounds,
1327+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
1328+
)
13161329
histogram_data_point_seen = True
13171330
if isinstance(point, NumberDataPoint):
13181331
number_data_point_seen = True

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -499,18 +499,20 @@ def _converse_on_success(
499499
[stop_reason],
500500
)
501501

502-
event_logger = instrumentor_context.event_logger
503-
choice = _Choice.from_converse(result, capture_content)
504-
# this path is used by streaming apis, in that case we are already out of the span
505-
# context so need to add the span context manually
506-
span_ctx = span.get_span_context()
507-
event_logger.emit(
508-
choice.to_choice_event(
509-
trace_id=span_ctx.trace_id,
510-
span_id=span_ctx.span_id,
511-
trace_flags=span_ctx.trace_flags,
502+
# In case of an early stream closure, the result may not contain outputs
503+
if self._stream_has_output_content(result):
504+
event_logger = instrumentor_context.event_logger
505+
choice = _Choice.from_converse(result, capture_content)
506+
# this path is used by streaming apis, in that case we are already out of the span
507+
# context so need to add the span context manually
508+
span_ctx = span.get_span_context()
509+
event_logger.emit(
510+
choice.to_choice_event(
511+
trace_id=span_ctx.trace_id,
512+
span_id=span_ctx.span_id,
513+
trace_flags=span_ctx.trace_flags,
514+
)
512515
)
513-
)
514516

515517
metrics = instrumentor_context.metrics
516518
metrics_attributes = self._extract_metrics_attributes()
@@ -602,11 +604,14 @@ def _on_stream_error_callback(
602604
span: Span,
603605
exception,
604606
instrumentor_context: _BotocoreInstrumentorContext,
607+
span_ended: bool,
605608
):
606609
span.set_status(Status(StatusCode.ERROR, str(exception)))
607610
if span.is_recording():
608611
span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
609-
span.end()
612+
613+
if not span_ended:
614+
span.end()
610615

611616
metrics = instrumentor_context.metrics
612617
metrics_attributes = {
@@ -638,15 +643,17 @@ def on_success(
638643
result["stream"], EventStream
639644
):
640645

641-
def stream_done_callback(response):
646+
def stream_done_callback(response, span_ended):
642647
self._converse_on_success(
643648
span, response, instrumentor_context, capture_content
644649
)
645-
span.end()
646650

647-
def stream_error_callback(exception):
651+
if not span_ended:
652+
span.end()
653+
654+
def stream_error_callback(exception, span_ended):
648655
self._on_stream_error_callback(
649-
span, exception, instrumentor_context
656+
span, exception, instrumentor_context, span_ended
650657
)
651658

652659
result["stream"] = ConverseStreamWrapper(
@@ -677,16 +684,17 @@ def stream_error_callback(exception):
677684
elif self._call_context.operation == "InvokeModelWithResponseStream":
678685
if "body" in result and isinstance(result["body"], EventStream):
679686

680-
def invoke_model_stream_done_callback(response):
687+
def invoke_model_stream_done_callback(response, span_ended):
681688
# the callback gets data formatted as the simpler converse API
682689
self._converse_on_success(
683690
span, response, instrumentor_context, capture_content
684691
)
685-
span.end()
692+
if not span_ended:
693+
span.end()
686694

687-
def invoke_model_stream_error_callback(exception):
695+
def invoke_model_stream_error_callback(exception, span_ended):
688696
self._on_stream_error_callback(
689-
span, exception, instrumentor_context
697+
span, exception, instrumentor_context, span_ended
690698
)
691699

692700
result["body"] = InvokeModelWithResponseStreamWrapper(
@@ -781,9 +789,11 @@ def _handle_amazon_nova_response(
781789
GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]
782790
)
783791

784-
event_logger = instrumentor_context.event_logger
785-
choice = _Choice.from_converse(response_body, capture_content)
786-
event_logger.emit(choice.to_choice_event())
792+
# In case of an early stream closure, the result may not contain outputs
793+
if self._stream_has_output_content(response_body):
794+
event_logger = instrumentor_context.event_logger
795+
choice = _Choice.from_converse(response_body, capture_content)
796+
event_logger.emit(choice.to_choice_event())
787797

788798
metrics = instrumentor_context.metrics
789799
metrics_attributes = self._extract_metrics_attributes()
@@ -1004,3 +1014,8 @@ def on_error(
10041014
duration,
10051015
attributes=metrics_attributes,
10061016
)
1017+
1018+
def _stream_has_output_content(self, response_body: dict[str, Any]):
1019+
return (
1020+
"output" in response_body and "message" in response_body["output"]
1021+
)

0 commit comments

Comments
 (0)