ASGI: Conditionally create SERVER spans (#843)

This commit is contained in:
Jakub Wach
2022-01-07 22:12:58 +01:00
committed by GitHub
parent 3de29868a9
commit 4c813c47e4
3 changed files with 80 additions and 15 deletions

View File

@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-flask` Flask: Conditionally create SERVER spans - `opentelemetry-instrumentation-flask` Flask: Conditionally create SERVER spans
([#828](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/828)) ([#828](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/828))
- `opentelemetry-instrumentation-asgi` ASGI: Conditionally create SERVER spans
([#843](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/843))
## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17 ## [1.8.0-0.27b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.8.0-0.27b0) - 2021-12-17
### Added ### Added

View File

@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# pylint: disable=too-many-locals
""" """
The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used
@ -110,7 +111,12 @@ from opentelemetry.instrumentation.utils import http_status_to_status_code
from opentelemetry.propagate import extract from opentelemetry.propagate import extract
from opentelemetry.propagators.textmap import Getter, Setter from opentelemetry.propagators.textmap import Getter, Setter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, set_span_in_context from opentelemetry.trace import (
INVALID_SPAN,
Span,
SpanKind,
set_span_in_context,
)
from opentelemetry.trace.status import Status, StatusCode from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.http import remove_url_credentials from opentelemetry.util.http import remove_url_credentials
@ -321,39 +327,48 @@ class OpenTelemetryMiddleware:
if self.excluded_urls and self.excluded_urls.url_disabled(url): if self.excluded_urls and self.excluded_urls.url_disabled(url):
return await self.app(scope, receive, send) return await self.app(scope, receive, send)
token = context.attach(extract(scope, getter=asgi_getter)) token = ctx = span_kind = None
server_span_name, additional_attributes = self.default_span_details(
scope if trace.get_current_span() is INVALID_SPAN:
) ctx = extract(scope, getter=asgi_getter)
token = context.attach(ctx)
span_kind = SpanKind.SERVER
else:
ctx = context.get_current()
span_kind = SpanKind.INTERNAL
span_name, additional_attributes = self.default_span_details(scope)
try: try:
with self.tracer.start_as_current_span( with self.tracer.start_as_current_span(
server_span_name, span_name,
kind=trace.SpanKind.SERVER, context=ctx,
) as server_span: kind=span_kind,
if server_span.is_recording(): ) as current_span:
if current_span.is_recording():
attributes = collect_request_attributes(scope) attributes = collect_request_attributes(scope)
attributes.update(additional_attributes) attributes.update(additional_attributes)
for key, value in attributes.items(): for key, value in attributes.items():
server_span.set_attribute(key, value) current_span.set_attribute(key, value)
if callable(self.server_request_hook): if callable(self.server_request_hook):
self.server_request_hook(server_span, scope) self.server_request_hook(current_span, scope)
otel_receive = self._get_otel_receive( otel_receive = self._get_otel_receive(
server_span_name, scope, receive span_name, scope, receive
) )
otel_send = self._get_otel_send( otel_send = self._get_otel_send(
server_span, current_span,
server_span_name, span_name,
scope, scope,
send, send,
) )
await self.app(scope, otel_receive, otel_send) await self.app(scope, otel_receive, otel_send)
finally: finally:
context.detach(token) if token:
context.detach(token)
def _get_otel_receive(self, server_span_name, scope, receive): def _get_otel_receive(self, server_span_name, scope, receive):
@wraps(receive) @wraps(receive)

View File

@ -19,6 +19,7 @@ import fastapi
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import opentelemetry.instrumentation.fastapi as otel_fastapi import opentelemetry.instrumentation.fastapi as otel_fastapi
from opentelemetry import trace
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
@ -329,3 +330,48 @@ class TestAutoInstrumentationLogic(unittest.TestCase):
should_be_original = fastapi.FastAPI should_be_original = fastapi.FastAPI
self.assertIs(original, should_be_original) self.assertIs(original, should_be_original)
class TestWrappedApplication(TestBase):
def setUp(self):
super().setUp()
self.app = fastapi.FastAPI()
@self.app.get("/foobar")
async def _():
return {"message": "hello world"}
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
self.client = TestClient(self.app)
self.tracer = self.tracer_provider.get_tracer(__name__)
def tearDown(self) -> None:
super().tearDown()
with self.disable_logging():
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
with self.tracer.start_as_current_span(
"test", kind=trace.SpanKind.SERVER
) as parent_span:
resp = self.client.get("/foobar")
self.assertEqual(200, resp.status_code)
span_list = self.memory_exporter.get_finished_spans()
for span in span_list:
print(str(span.__class__) + ": " + str(span.__dict__))
# there should be 4 spans - single SERVER "test" and three INTERNAL "FastAPI"
self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind)
self.assertEqual(trace.SpanKind.INTERNAL, span_list[1].kind)
# main INTERNAL span - child of test
self.assertEqual(trace.SpanKind.INTERNAL, span_list[2].kind)
self.assertEqual(
parent_span.context.span_id, span_list[2].parent.span_id
)
# SERVER "test"
self.assertEqual(trace.SpanKind.SERVER, span_list[3].kind)
self.assertEqual(
parent_span.context.span_id, span_list[3].context.span_id
)