Skip to content

Commit

Permalink
contrib/starlette: fix outcome for 500 responses without exception (e…
Browse files Browse the repository at this point in the history
…lastic#2060)

* contrib/starlette: fix outcome for 500 responses without exception

Set the outcome based on the status code of the http.response.start
ASGI message instead of relying only on the raise of an exception.

* contrib/asgi: fix outcome for 500 responses without exception

Set the outcome based on the status code of the http.response.start
ASGI message instead of relying only on the raise of an exception.
  • Loading branch information
xrmx committed Jun 10, 2024
1 parent 8a4e9c9 commit 5fd6662
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 0 deletions.
1 change: 1 addition & 0 deletions elasticapm/contrib/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def wrapped_send(message) -> None:
await set_context(lambda: middleware.get_data_from_response(message, constants.TRANSACTION), "response")
result = "HTTP {}xx".format(message["status"] // 100)
elasticapm.set_transaction_result(result, override=False)
elasticapm.set_transaction_outcome(http_status_code=message["status"], override=False)
await send(message)

return wrapped_send
Expand Down
1 change: 1 addition & 0 deletions elasticapm/contrib/starlette/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ async def wrapped_send(message) -> None:
)
result = "HTTP {}xx".format(message["status"] // 100)
elasticapm.set_transaction_result(result, override=False)
elasticapm.set_transaction_outcome(http_status_code=message["status"], override=False)
await send(message)

_mocked_receive = None
Expand Down
5 changes: 5 additions & 0 deletions tests/contrib/asgi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ async def boom() -> None:
@app.route("/body")
async def json():
return jsonify({"hello": "world"})


@app.route("/500", methods=["GET"])
async def error():
return "KO", 500
17 changes: 17 additions & 0 deletions tests/contrib/asgi/asgi_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ async def test_transaction_span(instrumented_app, elasticapm_client):
assert span["sync"] == False


@pytest.mark.asyncio
async def test_transaction_span(instrumented_app, elasticapm_client):
async with async_asgi_testclient.TestClient(instrumented_app) as client:
resp = await client.get("/500")
assert resp.status_code == 500
assert resp.text == "KO"

assert len(elasticapm_client.events[constants.TRANSACTION]) == 1
assert len(elasticapm_client.events[constants.SPAN]) == 0
transaction = elasticapm_client.events[constants.TRANSACTION][0]
assert transaction["name"] == "GET unknown route"
assert transaction["result"] == "HTTP 5xx"
assert transaction["outcome"] == "failure"
assert transaction["context"]["request"]["url"]["full"] == "/500"
assert transaction["context"]["response"]["status_code"] == 500


@pytest.mark.asyncio
async def test_transaction_ignore_url(instrumented_app, elasticapm_client):
elasticapm_client.config.update("1", transaction_ignore_urls="/foo*")
Expand Down
25 changes: 25 additions & 0 deletions tests/contrib/asyncio/starlette_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ async def with_slash(request):
async def without_slash(request):
return PlainTextResponse("Hi {}".format(request.path_params["name"]))

@app.route("/500/", methods=["GET"])
async def with_500_status_code(request):
return PlainTextResponse("Oops", status_code=500)

@sub.route("/hi")
async def hi_from_sub(request):
return PlainTextResponse("sub")
Expand Down Expand Up @@ -236,6 +240,27 @@ def test_exception(app, elasticapm_client):
assert error["context"]["request"] == transaction["context"]["request"]


def test_failure_outcome_with_500_status_code(app, elasticapm_client):
client = TestClient(app)

client.get("/500/")

assert len(elasticapm_client.events[constants.TRANSACTION]) == 1
transaction = elasticapm_client.events[constants.TRANSACTION][0]
spans = elasticapm_client.spans_for_transaction(transaction)
assert len(spans) == 0

assert transaction["name"] == "GET /500/"
assert transaction["result"] == "HTTP 5xx"
assert transaction["outcome"] == "failure"
assert transaction["type"] == "request"
request = transaction["context"]["request"]
assert request["method"] == "GET"
assert transaction["context"]["response"]["status_code"] == 500

assert len(elasticapm_client.events[constants.ERROR]) == 0


@pytest.mark.parametrize("header_name", [constants.TRACEPARENT_HEADER_NAME, constants.TRACEPARENT_LEGACY_HEADER_NAME])
def test_traceparent_handling(app, elasticapm_client, header_name):
client = TestClient(app)
Expand Down

0 comments on commit 5fd6662

Please sign in to comment.