Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion tests/apps/fastapi_app/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# (c) Copyright IBM Corp. 2021
# (c) Copyright Instana Inc. 2020

from ...helpers import testenv

from fastapi import FastAPI, HTTPException, Response
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from fastapi.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException as StarletteHTTPException
import requests

fastapi_server = FastAPI()

Expand Down Expand Up @@ -46,4 +50,18 @@ async def five_hundred():

@fastapi_server.get("/starlette_exception")
async def starlette_exception():
raise StarletteHTTPException(status_code=500, detail="500 response")
raise StarletteHTTPException(status_code=500, detail="500 response")

def trigger_outgoing_call():
response = requests.get(testenv["fastapi_server"]+"/users/1")
return response.json()

@fastapi_server.get("/non_async_simple")
def non_async_complex_call():
response = trigger_outgoing_call()
return response

@fastapi_server.get("/non_async_threadpool")
def non_async_threadpool():
run_in_threadpool(trigger_outgoing_call)
return {"message": "non async functions executed on a thread pool can't be followed through thread boundaries"}
159 changes: 141 additions & 18 deletions tests/frameworks/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import requests

from instana.singletons import tracer
from instana.singletons import async_tracer
from tests.apps.fastapi_app import launch_fastapi
from ..helpers import testenv
from ..helpers import get_first_span_by_filter
Expand All @@ -33,19 +33,19 @@ def test_vanilla_get(self):
self.assertEqual(result.headers["X-INSTANA-L"], "1")
self.assertIn("Server-Timing", result.headers)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
# FastAPI instrumentation (like all instrumentation) _always_ traces unless told otherwise
self.assertEqual(len(spans), 1)
self.assertEqual(spans[0].n, "asgi")

def test_basic_get(self):
result = None
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/")

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -95,12 +95,12 @@ def test_basic_get(self):

def test_400(self):
result = None
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/400")

self.assertEqual(result.status_code, 400)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -150,12 +150,12 @@ def test_400(self):

def test_500(self):
result = None
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/500")

self.assertEqual(result.status_code, 500)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -205,12 +205,12 @@ def test_500(self):

def test_path_templates(self):
result = None
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/users/1")

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -260,12 +260,12 @@ def test_path_templates(self):

def test_secret_scrubbing(self):
result = None
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/?secret=shhh")

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -315,14 +315,14 @@ def test_secret_scrubbing(self):

def test_synthetic_request(self):
request_headers = {"X-INSTANA-SYNTHETIC": "1"}
with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(
testenv["fastapi_server"] + "/", headers=request_headers
)

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -381,14 +381,14 @@ def test_request_header_capture(self):

request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"}

with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(
testenv["fastapi_server"] + "/", headers=request_headers
)

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -446,12 +446,12 @@ def test_response_header_capture(self):

# The background FastAPI server is pre-configured with custom headers to capture

with tracer.start_active_span("test"):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/response_headers")

self.assertEqual(result.status_code, 200)

spans = tracer.recorder.queued_spans()
spans = async_tracer.recorder.queued_spans()
self.assertEqual(len(spans), 3)

span_filter = (
Expand Down Expand Up @@ -503,3 +503,126 @@ def test_response_header_capture(self):
self.assertEqual("this too", asgi_span.data["http"]["header"]["X-Capture-This-Too"])
self.assertIn("X-Capture-That-Too", asgi_span.data["http"]["header"])
self.assertEqual("that too", asgi_span.data["http"]["header"]["X-Capture-That-Too"])

def test_non_async_simple(self):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/non_async_simple")

self.assertEqual(result.status_code, 200)

spans = async_tracer.recorder.queued_spans()
self.assertEqual(5, len(spans))

span_filter = (
lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test"
)
test_span = get_first_span_by_filter(spans, span_filter)
self.assertTrue(test_span)

span_filter = (
lambda span: span.n == "urllib3" and span.p == test_span.s
)
urllib3_span1 = get_first_span_by_filter(spans, span_filter)
self.assertTrue(urllib3_span1)

span_filter = (
lambda span: span.n == "asgi" and span.p == urllib3_span1.s
)
asgi_span1 = get_first_span_by_filter(spans, span_filter)
self.assertTrue(asgi_span1)

span_filter = (
lambda span: span.n == "urllib3" and span.p == asgi_span1.s
)
urllib3_span2 = get_first_span_by_filter(spans, span_filter)
self.assertTrue(urllib3_span2)

span_filter = (
lambda span: span.n == "asgi" and span.p == urllib3_span2.s
)
asgi_span2 = get_first_span_by_filter(spans, span_filter)
self.assertTrue(asgi_span2)

# Same traceId
traceId = test_span.t
self.assertEqual(traceId, urllib3_span1.t)
self.assertEqual(traceId, asgi_span1.t)
self.assertEqual(traceId, urllib3_span2.t)
self.assertEqual(traceId, asgi_span2.t)

self.assertIn("X-INSTANA-T", result.headers)
self.assertEqual(result.headers["X-INSTANA-T"], asgi_span1.t)

self.assertIn("X-INSTANA-S", result.headers)
self.assertEqual(result.headers["X-INSTANA-S"], asgi_span1.s)

self.assertIn("X-INSTANA-L", result.headers)
self.assertEqual(result.headers["X-INSTANA-L"], "1")

self.assertIn("Server-Timing", result.headers)
server_timing_value = "intid;desc=%s" % asgi_span1.t
self.assertEqual(result.headers["Server-Timing"], server_timing_value)

self.assertIsNone(asgi_span1.ec)
self.assertEqual(asgi_span1.data["http"]["host"], "127.0.0.1")
self.assertEqual(asgi_span1.data["http"]["path"], "/non_async_simple")
self.assertEqual(asgi_span1.data["http"]["path_tpl"], "/non_async_simple")
self.assertEqual(asgi_span1.data["http"]["method"], "GET")
self.assertEqual(asgi_span1.data["http"]["status"], 200)

self.assertIsNone(asgi_span1.data["http"]["error"])
self.assertIsNone(asgi_span1.data["http"]["params"])

def test_non_async_threadpool(self):
with async_tracer.start_active_span("test"):
result = requests.get(testenv["fastapi_server"] + "/non_async_threadpool")

self.assertEqual(result.status_code, 200)

spans = async_tracer.recorder.queued_spans()
self.assertEqual(3, len(spans))

span_filter = (
lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test"
)
test_span = get_first_span_by_filter(spans, span_filter)
self.assertTrue(test_span)

span_filter = lambda span: span.n == "urllib3"
urllib3_span = get_first_span_by_filter(spans, span_filter)
self.assertTrue(urllib3_span)

span_filter = lambda span: span.n == "asgi"
asgi_span = get_first_span_by_filter(spans, span_filter)
self.assertTrue(asgi_span)

# Same traceId
self.assertEqual(test_span.t, urllib3_span.t)
self.assertEqual(urllib3_span.t, asgi_span.t)

# Parent relationships
self.assertEqual(asgi_span.p, urllib3_span.s)
self.assertEqual(urllib3_span.p, test_span.s)

self.assertIn("X-INSTANA-T", result.headers)
self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t)

self.assertIn("X-INSTANA-S", result.headers)
self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s)

self.assertIn("X-INSTANA-L", result.headers)
self.assertEqual(result.headers["X-INSTANA-L"], "1")

self.assertIn("Server-Timing", result.headers)
server_timing_value = "intid;desc=%s" % asgi_span.t
self.assertEqual(result.headers["Server-Timing"], server_timing_value)

self.assertIsNone(asgi_span.ec)
self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1")
self.assertEqual(asgi_span.data["http"]["path"], "/non_async_threadpool")
self.assertEqual(asgi_span.data["http"]["path_tpl"], "/non_async_threadpool")
self.assertEqual(asgi_span.data["http"]["method"], "GET")
self.assertEqual(asgi_span.data["http"]["status"], 200)

self.assertIsNone(asgi_span.data["http"]["error"])
self.assertIsNone(asgi_span.data["http"]["params"])