Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set log level to warning instead of error for 4xx HTTPExceptions from FastAPI/Starlette #858

Merged
merged 2 commits into from
Feb 12, 2025
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
14 changes: 13 additions & 1 deletion logfire/_internal/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,11 +356,14 @@ def record_exception(
escaped: bool = False,
) -> None:
"""Similar to the OTEL SDK Span.record_exception method, with our own additions."""
if is_starlette_http_exception_400(exception):
span.set_attributes(log_level_attributes('warn'))

# From https://opentelemetry.io/docs/specs/semconv/attributes-registry/exception/
# `escaped=True` means that the exception is escaping the scope of the span.
# This means we know that the exception hasn't been handled,
# so we can set the OTEL status and the log level to error.
if escaped:
elif escaped:
set_exception_status(span, exception)
span.set_attributes(log_level_attributes('error'))

Expand Down Expand Up @@ -392,3 +395,12 @@ def set_exception_status(span: trace_api.Span, exception: BaseException):
description=f'{exception.__class__.__name__}: {exception}',
)
)


def is_starlette_http_exception_400(exception: BaseException) -> bool:
if 'starlette.exceptions' not in sys.modules: # pragma: no cover
return False

from starlette.exceptions import HTTPException

return isinstance(exception, HTTPException) and 400 <= exception.status_code < 500
16 changes: 15 additions & 1 deletion tests/otel_integrations/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
from dirty_equals import IsJson
from fastapi import BackgroundTasks, FastAPI, Response, WebSocket
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.params import Header
from fastapi.security import SecurityScopes
from fastapi.staticfiles import StaticFiles
Expand All @@ -23,6 +23,7 @@
import logfire._internal
import logfire._internal.integrations
import logfire._internal.integrations.fastapi
from logfire._internal.constants import LEVEL_NUMBERS
from logfire._internal.main import set_user_attributes_on_raw_span
from logfire.testing import TestExporter

Expand Down Expand Up @@ -74,6 +75,10 @@ async def echo_body(request: Request):
return await request.body()


async def bad_request_error():
raise HTTPException(400)


async def websocket_endpoint(websocket: WebSocket, name: str):
logfire.info('websocket_endpoint: {name}', name=name)
await websocket.accept()
Expand All @@ -97,6 +102,7 @@ def app():
app.get('/other', name='other_route_name', operation_id='other_route_operation_id')(other_route)
app.get('/exception')(exception)
app.get('/validation_error')(validation_error)
app.get('/bad_request_error')(bad_request_error)
app.get('/with_path_param/{param}')(with_path_param)
app.get('/secret/{path_param}', name='secret')(get_secret)
app.websocket('/ws/{name}')(websocket_endpoint)
Expand Down Expand Up @@ -192,6 +198,14 @@ def test_404(client: TestClient, exporter: TestExporter) -> None:
)


def test_400(client: TestClient, exporter: TestExporter) -> None:
response = client.get('/bad_request_error')
assert response.status_code == 400

[span] = [span for span in exporter.exported_spans if span.events]
assert span.attributes and span.attributes['logfire.level_num'] == LEVEL_NUMBERS['warn']


def test_path_param(client: TestClient, exporter: TestExporter) -> None:
response = client.get('/with_path_param/param_val')
assert response.status_code == 200
Expand Down