@@ -79,6 +79,7 @@ def on_end(self, span: ReadableSpan) -> None:
79
79
_tweak_asgi_send_receive_spans (span_dict )
80
80
_tweak_sqlalchemy_connect_spans (span_dict )
81
81
_tweak_http_spans (span_dict )
82
+ _tweak_fastapi_span (span_dict )
82
83
_summarize_db_statement (span_dict )
83
84
_set_error_level_and_status (span_dict )
84
85
_transform_langchain_span (span_dict )
@@ -299,6 +300,34 @@ def _summarize_db_statement(span: ReadableSpanDict):
299
300
span ['attributes' ] = {** attributes , ATTRIBUTES_MESSAGE_KEY : summary }
300
301
301
302
303
+ def _tweak_fastapi_span (span : ReadableSpanDict ):
304
+ scope = span ['instrumentation_scope' ]
305
+
306
+ if not (scope and scope .name == 'opentelemetry.instrumentation.fastapi' ):
307
+ return
308
+
309
+ # Our fastapi instrumentation records some exceptions directly on the request span.
310
+ # These might be handled and not seen again, or they may bubble through and be recorded by the OTel middleware,
311
+ # thus appearing twice on the same span.
312
+ # We dedupe them here, keeping the latter event which has a fuller traceback.
313
+ events = span ['events' ]
314
+ new_events : list [Event ] = []
315
+ # (type, message) keys of exceptions we've seen.
316
+ seen_exceptions : set [tuple [Any , Any ]] = set ()
317
+ # Go in reverse order to give the latter events precedence.
318
+ for event in events [::- 1 ]:
319
+ attrs = event .attributes
320
+ if not (event .name == 'exception' and attrs and 'exception.type' in attrs and 'exception.message' in attrs ):
321
+ new_events .append (event )
322
+ continue
323
+ key = (attrs ['exception.type' ], attrs ['exception.message' ])
324
+ if key in seen_exceptions and attrs .get ('recorded_by_logfire_fastapi' ):
325
+ continue
326
+ seen_exceptions .add (key )
327
+ new_events .append (event )
328
+ span ['events' ] = new_events [::- 1 ]
329
+
330
+
302
331
def _transform_langchain_span (span : ReadableSpanDict ):
303
332
"""Transform spans generated by LangSmith to work better in the Logfire UI.
304
333
0 commit comments