Skip to content

Commit 7e40c0b

Browse files
authored
Merge branch 'main' into feature/asyncio-instrumentation
2 parents 86f137c + b6d77f1 commit 7e40c0b

File tree

7 files changed

+281
-10
lines changed

7 files changed

+281
-10
lines changed

CHANGELOG.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- `opentelemetry-instrumentation-asyncio` Add support for asyncio
1212
([#1919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1943))
13+
- `opentelemetry-instrumentation` Added Otel semantic convention opt-in mechanism
14+
([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987))
1315

1416
## Version 1.21.0/0.42b0 (2023-11-01)
1517

16-
- `opentelemetry-instrumentation-aiohttp-server` Add instrumentor and auto instrumentation support for aiohttp-server
17-
([#1800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1800))
18-
1918
### Added
2019

20+
- `opentelemetry-instrumentation-aiohttp-server` Add instrumentor and auto instrumentation support for aiohttp-server
21+
([#1800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1800))
2122
- `opentelemetry-instrumentation-botocore` Include SNS topic ARN as a span attribute with name `messaging.destination.name` to uniquely identify the SNS topic
2223
([#1995](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1995))
2324
- `opentelemetry-instrumentation-system-metrics` Add support for collecting process metrics
@@ -61,6 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6162
([#1824](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1824))
6263
- Fix sqlalchemy instrumentation wrap methods to accept sqlcommenter options
6364
([#1873](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1873))
65+
- Exclude background task execution from root server span in ASGI middleware
66+
([#1952](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1952))
6467

6568
### Added
6669

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ async def __call__(self, scope, receive, send):
576576
if scope["type"] == "http":
577577
self.active_requests_counter.add(1, active_requests_count_attrs)
578578
try:
579-
with trace.use_span(span, end_on_exit=True) as current_span:
579+
with trace.use_span(span, end_on_exit=False) as current_span:
580580
if current_span.is_recording():
581581
for key, value in attributes.items():
582582
current_span.set_attribute(key, value)
@@ -630,6 +630,8 @@ async def __call__(self, scope, receive, send):
630630
)
631631
if token:
632632
context.detach(token)
633+
if span.is_recording():
634+
span.end()
633635

634636
# pylint: enable=too-many-branches
635637

@@ -653,8 +655,11 @@ async def otel_receive():
653655
def _get_otel_send(
654656
self, server_span, server_span_name, scope, send, duration_attrs
655657
):
658+
expecting_trailers = False
659+
656660
@wraps(send)
657661
async def otel_send(message):
662+
nonlocal expecting_trailers
658663
with self.tracer.start_as_current_span(
659664
" ".join((server_span_name, scope["type"], "send"))
660665
) as send_span:
@@ -668,6 +673,8 @@ async def otel_send(message):
668673
] = status_code
669674
set_status_code(server_span, status_code)
670675
set_status_code(send_span, status_code)
676+
677+
expecting_trailers = message.get("trailers", False)
671678
elif message["type"] == "websocket.send":
672679
set_status_code(server_span, 200)
673680
set_status_code(send_span, 200)
@@ -703,5 +710,15 @@ async def otel_send(message):
703710
pass
704711

705712
await send(message)
713+
if (
714+
not expecting_trailers
715+
and message["type"] == "http.response.body"
716+
and not message.get("more_body", False)
717+
) or (
718+
expecting_trailers
719+
and message["type"] == "http.response.trailers"
720+
and not message.get("more_trailers", False)
721+
):
722+
server_span.end()
706723

707724
return otel_send

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

+189-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import asyncio
1818
import sys
19+
import time
1920
import unittest
2021
from timeit import default_timer
2122
from unittest import mock
@@ -57,6 +58,8 @@
5758
"http.server.request.size": _duration_attrs,
5859
}
5960

61+
_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S = 0.01
62+
6063

6164
async def http_app(scope, receive, send):
6265
message = await receive()
@@ -99,6 +102,108 @@ async def simple_asgi(scope, receive, send):
99102
await websocket_app(scope, receive, send)
100103

101104

105+
async def long_response_asgi(scope, receive, send):
106+
assert isinstance(scope, dict)
107+
assert scope["type"] == "http"
108+
message = await receive()
109+
scope["headers"] = [(b"content-length", b"128")]
110+
assert scope["type"] == "http"
111+
if message.get("type") == "http.request":
112+
await send(
113+
{
114+
"type": "http.response.start",
115+
"status": 200,
116+
"headers": [
117+
[b"Content-Type", b"text/plain"],
118+
[b"content-length", b"1024"],
119+
],
120+
}
121+
)
122+
await send(
123+
{"type": "http.response.body", "body": b"*", "more_body": True}
124+
)
125+
await send(
126+
{"type": "http.response.body", "body": b"*", "more_body": True}
127+
)
128+
await send(
129+
{"type": "http.response.body", "body": b"*", "more_body": True}
130+
)
131+
await send(
132+
{"type": "http.response.body", "body": b"*", "more_body": False}
133+
)
134+
135+
136+
async def background_execution_asgi(scope, receive, send):
137+
assert isinstance(scope, dict)
138+
assert scope["type"] == "http"
139+
message = await receive()
140+
scope["headers"] = [(b"content-length", b"128")]
141+
assert scope["type"] == "http"
142+
if message.get("type") == "http.request":
143+
await send(
144+
{
145+
"type": "http.response.start",
146+
"status": 200,
147+
"headers": [
148+
[b"Content-Type", b"text/plain"],
149+
[b"content-length", b"1024"],
150+
],
151+
}
152+
)
153+
await send(
154+
{
155+
"type": "http.response.body",
156+
"body": b"*",
157+
}
158+
)
159+
time.sleep(_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S)
160+
161+
162+
async def background_execution_trailers_asgi(scope, receive, send):
163+
assert isinstance(scope, dict)
164+
assert scope["type"] == "http"
165+
message = await receive()
166+
scope["headers"] = [(b"content-length", b"128")]
167+
assert scope["type"] == "http"
168+
if message.get("type") == "http.request":
169+
await send(
170+
{
171+
"type": "http.response.start",
172+
"status": 200,
173+
"headers": [
174+
[b"Content-Type", b"text/plain"],
175+
[b"content-length", b"1024"],
176+
],
177+
"trailers": True,
178+
}
179+
)
180+
await send(
181+
{"type": "http.response.body", "body": b"*", "more_body": True}
182+
)
183+
await send(
184+
{"type": "http.response.body", "body": b"*", "more_body": False}
185+
)
186+
await send(
187+
{
188+
"type": "http.response.trailers",
189+
"headers": [
190+
[b"trailer", b"test-trailer"],
191+
],
192+
"more_trailers": True,
193+
}
194+
)
195+
await send(
196+
{
197+
"type": "http.response.trailers",
198+
"headers": [
199+
[b"trailer", b"second-test-trailer"],
200+
],
201+
"more_trailers": False,
202+
}
203+
)
204+
time.sleep(_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S)
205+
206+
102207
async def error_asgi(scope, receive, send):
103208
assert isinstance(scope, dict)
104209
assert scope["type"] == "http"
@@ -127,14 +232,19 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
127232
# Ensure modifiers is a list
128233
modifiers = modifiers or []
129234
# Check for expected outputs
130-
self.assertEqual(len(outputs), 2)
131235
response_start = outputs[0]
132-
response_body = outputs[1]
236+
response_final_body = [
237+
output
238+
for output in outputs
239+
if output["type"] == "http.response.body"
240+
][-1]
241+
133242
self.assertEqual(response_start["type"], "http.response.start")
134-
self.assertEqual(response_body["type"], "http.response.body")
243+
self.assertEqual(response_final_body["type"], "http.response.body")
244+
self.assertEqual(response_final_body.get("more_body", False), False)
135245

136246
# Check http response body
137-
self.assertEqual(response_body["body"], b"*")
247+
self.assertEqual(response_final_body["body"], b"*")
138248

139249
# Check http response start
140250
self.assertEqual(response_start["status"], 200)
@@ -153,7 +263,6 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
153263

154264
# Check spans
155265
span_list = self.memory_exporter.get_finished_spans()
156-
self.assertEqual(len(span_list), 4)
157266
expected = [
158267
{
159268
"name": "GET / http receive",
@@ -194,6 +303,7 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
194303
for modifier in modifiers:
195304
expected = modifier(expected)
196305
# Check that output matches
306+
self.assertEqual(len(span_list), len(expected))
197307
for span, expected in zip(span_list, expected):
198308
self.assertEqual(span.name, expected["name"])
199309
self.assertEqual(span.kind, expected["kind"])
@@ -232,6 +342,80 @@ def test_asgi_exc_info(self):
232342
outputs = self.get_all_output()
233343
self.validate_outputs(outputs, error=ValueError)
234344

345+
def test_long_response(self):
346+
"""Test that the server span is ended on the final response body message.
347+
348+
If the server span is ended early then this test will fail due
349+
to discrepancies in the expected list of spans and the emitted list of spans.
350+
"""
351+
app = otel_asgi.OpenTelemetryMiddleware(long_response_asgi)
352+
self.seed_app(app)
353+
self.send_default_request()
354+
outputs = self.get_all_output()
355+
356+
def add_more_body_spans(expected: list):
357+
more_body_span = {
358+
"name": "GET / http send",
359+
"kind": trace_api.SpanKind.INTERNAL,
360+
"attributes": {"type": "http.response.body"},
361+
}
362+
extra_spans = [more_body_span] * 3
363+
expected[2:2] = extra_spans
364+
return expected
365+
366+
self.validate_outputs(outputs, modifiers=[add_more_body_spans])
367+
368+
def test_background_execution(self):
369+
"""Test that the server span is ended BEFORE the background task is finished."""
370+
app = otel_asgi.OpenTelemetryMiddleware(background_execution_asgi)
371+
self.seed_app(app)
372+
self.send_default_request()
373+
outputs = self.get_all_output()
374+
self.validate_outputs(outputs)
375+
span_list = self.memory_exporter.get_finished_spans()
376+
server_span = span_list[-1]
377+
assert server_span.kind == SpanKind.SERVER
378+
span_duration_nanos = server_span.end_time - server_span.start_time
379+
self.assertLessEqual(
380+
span_duration_nanos,
381+
_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10**9,
382+
)
383+
384+
def test_trailers(self):
385+
"""Test that trailers are emitted as expected and that the server span is ended
386+
BEFORE the background task is finished."""
387+
app = otel_asgi.OpenTelemetryMiddleware(
388+
background_execution_trailers_asgi
389+
)
390+
self.seed_app(app)
391+
self.send_default_request()
392+
outputs = self.get_all_output()
393+
394+
def add_body_and_trailer_span(expected: list):
395+
body_span = {
396+
"name": "GET / http send",
397+
"kind": trace_api.SpanKind.INTERNAL,
398+
"attributes": {"type": "http.response.body"},
399+
}
400+
trailer_span = {
401+
"name": "GET / http send",
402+
"kind": trace_api.SpanKind.INTERNAL,
403+
"attributes": {"type": "http.response.trailers"},
404+
}
405+
expected[2:2] = [body_span]
406+
expected[4:4] = [trailer_span] * 2
407+
return expected
408+
409+
self.validate_outputs(outputs, modifiers=[add_body_and_trailer_span])
410+
span_list = self.memory_exporter.get_finished_spans()
411+
server_span = span_list[-1]
412+
assert server_span.kind == SpanKind.SERVER
413+
span_duration_nanos = server_span.end_time - server_span.start_time
414+
self.assertLessEqual(
415+
span_duration_nanos,
416+
_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10**9,
417+
)
418+
235419
def test_override_span_name(self):
236420
"""Test that default span_names can be overwritten by our callback function."""
237421
span_name = "Dymaxion"

instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ instruments = [
3838
test = [
3939
"opentelemetry-instrumentation-botocore[instruments]",
4040
"markupsafe==2.0.1",
41+
"botocore ~= 1.0, < 1.31.81",
4142
"moto[all] ~= 2.2.6",
4243
"opentelemetry-test-utils == 0.43b0.dev",
4344
]

instrumentation/opentelemetry-instrumentation-cassandra/README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
OpenTelemetry Cassandra Instrumentation
2-
===================================
2+
=======================================
33

44
|pypi|
55

opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
DependencyConflict,
2626
get_dependency_conflicts,
2727
)
28+
from opentelemetry.instrumentation.utils import (
29+
_OpenTelemetrySemanticConventionStability,
30+
)
2831

2932
_LOG = getLogger(__name__)
3033

@@ -105,6 +108,9 @@ def instrument(self, **kwargs):
105108
_LOG.error(conflict)
106109
return None
107110

111+
# initialize semantic conventions opt-in if needed
112+
_OpenTelemetrySemanticConventionStability._initialize()
113+
108114
result = self._instrument( # pylint: disable=assignment-from-no-return
109115
**kwargs
110116
)

0 commit comments

Comments
 (0)