Skip to content

Commit 077b410

Browse files
Merge branch 'main' into feature/http-route-in-metric
2 parents 335657e + 6bc48be commit 077b410

File tree

12 files changed

+700
-84
lines changed

12 files changed

+700
-84
lines changed

CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638))
2424
- `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions
2525
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
26+
- `opentelemetry-instrumentation-fastapi` Implement new semantic convention opt-in with stable http semantic conventions
27+
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
2628
- `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions
2729
([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631))
2830
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
@@ -34,9 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3436

3537
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware
3638
([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580))
37-
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope
39+
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware
3840
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
39-
41+
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware
42+
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
4043

4144
### Fixed
4245

@@ -63,6 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6366
([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153))
6467
- `opentelemetry-instrumentation-asgi` Removed `NET_HOST_NAME` AND `NET_HOST_PORT` from active requests count attribute
6568
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
69+
- `opentelemetry-instrumentation-asgi` Bugfix: Middleware did not set status code attribute on duration metrics for non-recording spans.
70+
([#2627](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2627))
6671

6772

6873
## Version 1.25.0/0.46b0 (2024-05-31)

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Meeting notes are available as a public [Google doc](https://docs.google.com/doc
110110
Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telemetry/teams/python-approvers)):
111111

112112
- [Aaron Abbott](https://github.com/aabmass), Google
113+
- [Emídio Neto](https://github.com/emdneto), Zenvia
113114
- [Jeremy Voss](https://github.com/jeremydvoss), Microsoft
114115
- [Owais Lone](https://github.com/owais), Splunk
115116
- [Pablo Collins](https://github.com/pmcollins), Splunk

exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
asgiref==3.7.2
2-
certifi==2024.2.2
2+
certifi==2024.7.4
33
charset-normalizer==3.3.2
44
# We can drop this after bumping baseline to pypy-39
55
cramjam==2.1.0; platform_python_implementation == "PyPy"

instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
2020
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
2121
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental
22-
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental
22+
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | migration
2323
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
2424
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental
2525
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration

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

+21-29
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,6 @@ def set_status_code(
438438
sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT,
439439
):
440440
"""Adds HTTP response attributes to span using the status_code argument."""
441-
if not span.is_recording():
442-
return
443441
status_code_str = str(status_code)
444442

445443
try:
@@ -836,36 +834,16 @@ async def otel_send(message: dict[str, Any]):
836834
) as send_span:
837835
if callable(self.client_response_hook):
838836
self.client_response_hook(send_span, scope, message)
837+
838+
status_code = None
839+
if message["type"] == "http.response.start":
840+
status_code = message["status"]
841+
elif message["type"] == "websocket.send":
842+
status_code = 200
843+
839844
if send_span.is_recording():
840845
if message["type"] == "http.response.start":
841-
status_code = message["status"]
842-
# We record metrics only once
843-
set_status_code(
844-
server_span,
845-
status_code,
846-
duration_attrs,
847-
self._sem_conv_opt_in_mode,
848-
)
849-
set_status_code(
850-
send_span,
851-
status_code,
852-
None,
853-
self._sem_conv_opt_in_mode,
854-
)
855846
expecting_trailers = message.get("trailers", False)
856-
elif message["type"] == "websocket.send":
857-
set_status_code(
858-
server_span,
859-
200,
860-
duration_attrs,
861-
self._sem_conv_opt_in_mode,
862-
)
863-
set_status_code(
864-
send_span,
865-
200,
866-
None,
867-
self._sem_conv_opt_in_mode,
868-
)
869847
send_span.set_attribute("asgi.event.type", message["type"])
870848
if (
871849
server_span.is_recording()
@@ -886,6 +864,20 @@ async def otel_send(message: dict[str, Any]):
886864
server_span.set_attributes(
887865
custom_response_attributes
888866
)
867+
if status_code:
868+
# We record metrics only once
869+
set_status_code(
870+
server_span,
871+
status_code,
872+
duration_attrs,
873+
self._sem_conv_opt_in_mode,
874+
)
875+
set_status_code(
876+
send_span,
877+
status_code,
878+
None,
879+
self._sem_conv_opt_in_mode,
880+
)
889881

890882
propagator = get_global_response_propagator()
891883
if propagator:

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

+62-1
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,9 @@ def test_asgi_not_recording(self):
514514
mock_span = mock.Mock()
515515
mock_span.is_recording.return_value = False
516516
mock_tracer.start_as_current_span.return_value = mock_span
517-
mock_tracer.start_as_current_span.return_value.__enter__ = mock_span
517+
mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(
518+
return_value=mock_span
519+
)
518520
mock_tracer.start_as_current_span.return_value.__exit__ = mock_span
519521
with mock.patch("opentelemetry.trace.get_tracer") as tracer:
520522
tracer.return_value = mock_tracer
@@ -1342,6 +1344,65 @@ def test_basic_metric_success(self):
13421344
)
13431345
self.assertEqual(point.value, 0)
13441346

1347+
def test_basic_metric_success_nonrecording_span(self):
1348+
mock_tracer = mock.Mock()
1349+
mock_span = mock.Mock()
1350+
mock_span.is_recording.return_value = False
1351+
mock_tracer.start_as_current_span.return_value = mock_span
1352+
mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(
1353+
return_value=mock_span
1354+
)
1355+
mock_tracer.start_as_current_span.return_value.__exit__ = mock_span
1356+
with mock.patch("opentelemetry.trace.get_tracer") as tracer:
1357+
tracer.return_value = mock_tracer
1358+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
1359+
self.seed_app(app)
1360+
start = default_timer()
1361+
self.send_default_request()
1362+
duration = max(round((default_timer() - start) * 1000), 0)
1363+
expected_duration_attributes = {
1364+
"http.method": "GET",
1365+
"http.host": "127.0.0.1",
1366+
"http.scheme": "http",
1367+
"http.flavor": "1.0",
1368+
"net.host.port": 80,
1369+
"http.status_code": 200,
1370+
}
1371+
expected_requests_count_attributes = {
1372+
"http.method": "GET",
1373+
"http.host": "127.0.0.1",
1374+
"http.scheme": "http",
1375+
"http.flavor": "1.0",
1376+
}
1377+
metrics_list = self.memory_metrics_reader.get_metrics_data()
1378+
# pylint: disable=too-many-nested-blocks
1379+
for resource_metric in metrics_list.resource_metrics:
1380+
for scope_metrics in resource_metric.scope_metrics:
1381+
for metric in scope_metrics.metrics:
1382+
for point in list(metric.data.data_points):
1383+
if isinstance(point, HistogramDataPoint):
1384+
self.assertDictEqual(
1385+
expected_duration_attributes,
1386+
dict(point.attributes),
1387+
)
1388+
self.assertEqual(point.count, 1)
1389+
if metric.name == "http.server.duration":
1390+
self.assertAlmostEqual(
1391+
duration, point.sum, delta=5
1392+
)
1393+
elif (
1394+
metric.name == "http.server.response.size"
1395+
):
1396+
self.assertEqual(1024, point.sum)
1397+
elif metric.name == "http.server.request.size":
1398+
self.assertEqual(128, point.sum)
1399+
elif isinstance(point, NumberDataPoint):
1400+
self.assertDictEqual(
1401+
expected_requests_count_attributes,
1402+
dict(point.attributes),
1403+
)
1404+
self.assertEqual(point.value, 0)
1405+
13451406
def test_basic_metric_success_new_semconv(self):
13461407
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
13471408
self.seed_app(app)

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

+28-6
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
177177
import fastapi
178178
from starlette.routing import Match
179179

180+
from opentelemetry.instrumentation._semconv import (
181+
_get_schema_url,
182+
_HTTPStabilityMode,
183+
_OpenTelemetrySemanticConventionStability,
184+
_OpenTelemetryStabilitySignalType,
185+
)
180186
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
181187
from opentelemetry.instrumentation.asgi.types import (
182188
ClientRequestHook,
@@ -189,7 +195,11 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
189195
from opentelemetry.metrics import get_meter
190196
from opentelemetry.semconv.trace import SpanAttributes
191197
from opentelemetry.trace import get_tracer
192-
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
198+
from opentelemetry.util.http import (
199+
get_excluded_urls,
200+
parse_excluded_urls,
201+
sanitize_method,
202+
)
193203

194204
_excluded_urls_from_env = get_excluded_urls("FASTAPI")
195205
_logger = logging.getLogger(__name__)
@@ -218,6 +228,11 @@ def instrument_app(
218228
app._is_instrumented_by_opentelemetry = False
219229

220230
if not getattr(app, "_is_instrumented_by_opentelemetry", False):
231+
# initialize semantic conventions opt-in if needed
232+
_OpenTelemetrySemanticConventionStability._initialize()
233+
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
234+
_OpenTelemetryStabilitySignalType.HTTP,
235+
)
221236
if excluded_urls is None:
222237
excluded_urls = _excluded_urls_from_env
223238
else:
@@ -226,13 +241,13 @@ def instrument_app(
226241
__name__,
227242
__version__,
228243
tracer_provider,
229-
schema_url="https://opentelemetry.io/schemas/1.11.0",
244+
schema_url=_get_schema_url(sem_conv_opt_in_mode),
230245
)
231246
meter = get_meter(
232247
__name__,
233248
__version__,
234249
meter_provider,
235-
schema_url="https://opentelemetry.io/schemas/1.11.0",
250+
schema_url=_get_schema_url(sem_conv_opt_in_mode),
236251
)
237252

238253
app.add_middleware(
@@ -303,20 +318,25 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
303318
_client_request_hook: ClientRequestHook = None
304319
_client_response_hook: ClientResponseHook = None
305320
_instrumented_fastapi_apps = set()
321+
_sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT
306322

307323
def __init__(self, *args, **kwargs):
308324
super().__init__(*args, **kwargs)
309325
tracer = get_tracer(
310326
__name__,
311327
__version__,
312328
_InstrumentedFastAPI._tracer_provider,
313-
schema_url="https://opentelemetry.io/schemas/1.11.0",
329+
schema_url=_get_schema_url(
330+
_InstrumentedFastAPI._sem_conv_opt_in_mode
331+
),
314332
)
315333
meter = get_meter(
316334
__name__,
317335
__version__,
318336
_InstrumentedFastAPI._meter_provider,
319-
schema_url="https://opentelemetry.io/schemas/1.11.0",
337+
schema_url=_get_schema_url(
338+
_InstrumentedFastAPI._sem_conv_opt_in_mode
339+
),
320340
)
321341
self.add_middleware(
322342
OpenTelemetryMiddleware,
@@ -373,8 +393,10 @@ def _get_default_span_details(scope):
373393
A tuple of span name and attributes
374394
"""
375395
route = _get_route_details(scope)
376-
method = scope.get("method", "")
396+
method = sanitize_method(scope.get("method", "").strip())
377397
attributes = {}
398+
if method == "_OTHER":
399+
method = "HTTP"
378400
if route:
379401
attributes[SpanAttributes.HTTP_ROUTE] = route
380402
if method and route: # http

instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/package.py

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@
1616
_instruments = ("fastapi ~= 0.58",)
1717

1818
_supports_metrics = True
19+
20+
_semconv_status = "migration"

0 commit comments

Comments
 (0)