@@ -78,9 +78,10 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
7878 logging .exception ("Failed to initialize Sentry or start transaction" )
7979
8080 # Initialize and track with Elastic APM if enabled
81+ elastic_client = None
8182 if config .ELASTIC_APM .IS_ENABLED :
8283 try :
83- import elasticapm
84+ import elasticapm # type: ignore[import-not-found]
8485
8586 # Initialize Elastic APM client with config
8687 elastic_client = elasticapm .get_client ()
@@ -95,22 +96,133 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
9596 try :
9697 # Execute the function
9798 result = func (* args , ** kwargs )
99+
100+ # Mark transaction as successful
101+ if sentry_transaction :
102+ sentry_transaction .set_status ("ok" )
103+ if elastic_client :
104+ elastic_client .end_transaction (name = transaction_name , result = "success" )
105+
106+ return result
98107 except Exception :
99108 # Mark transaction as failed and capture the exception
100109 if sentry_transaction :
101110 sentry_transaction .set_status ("internal_error" )
102111 if elastic_client :
103- elastic_client .end_transaction (name = transaction_name , result = "error" ) # type: ignore[no-untyped-call]
104-
105- # Re-raise the exception
112+ elastic_client .end_transaction (name = transaction_name , result = "error" )
106113 raise
107- else :
114+ finally :
115+ # Clean up Sentry transaction
116+ if sentry_transaction :
117+ try :
118+ sentry_transaction .__exit__ (None , None , None )
119+ except Exception :
120+ logging .exception ("Error closing Sentry transaction" )
121+
122+ return cast (F , wrapper )
123+
124+ return decorator
125+
126+
127+ def async_capture_transaction [F : Callable [..., Any ]](
128+ name : str | None = None ,
129+ * ,
130+ op : str = "function" ,
131+ description : str | None = None ,
132+ ) -> Callable [[F ], F ]:
133+ """Decorator to capture a transaction for the decorated async function.
134+
135+ This decorator creates a transaction span around the execution of the decorated async function.
136+ It integrates with both Sentry and Elastic APM based on the application configuration.
137+
138+ Args:
139+ name: Name of the transaction. If None, uses the function name.
140+ op: Operation type/category for the transaction. Defaults to "function".
141+ description: Optional description of the transaction.
142+
143+ Returns:
144+ The decorated async function with transaction tracing capabilities.
145+
146+ Example:
147+ ```python
148+ @async_capture_transaction(name="user_processing", op="business_logic")
149+ async def process_user_data(user_id: int) -> dict[str, Any]:
150+ # Your async business logic here
151+ return {"user_id": user_id, "status": "processed"}
152+
153+ # Transaction will be automatically captured when function is called
154+ result = await process_user_data(123)
155+ ```
156+ """
157+
158+ def decorator (func : F ) -> F :
159+ transaction_name = name or func .__name__
160+
161+ @functools .wraps (func )
162+ async def wrapper (* args : Any , ** kwargs : Any ) -> Any :
163+ config : Any = BaseConfig .global_config ()
164+
165+ # Initialize and track with Sentry if enabled
166+ sentry_transaction = None
167+ if config .SENTRY .IS_ENABLED :
168+ try :
169+ import sentry_sdk
170+
171+ # Initialize Sentry if not already done
172+ if not sentry_sdk .Hub .current .client :
173+ sentry_sdk .init (
174+ dsn = config .SENTRY .DSN ,
175+ debug = config .SENTRY .DEBUG ,
176+ release = config .SENTRY .RELEASE ,
177+ sample_rate = config .SENTRY .SAMPLE_RATE ,
178+ traces_sample_rate = config .SENTRY .TRACES_SAMPLE_RATE ,
179+ environment = getattr (config , "ENVIRONMENT" , None ),
180+ )
181+ sentry_transaction = sentry_sdk .start_transaction (
182+ name = transaction_name ,
183+ op = op ,
184+ description = description or transaction_name ,
185+ )
186+ sentry_transaction .__enter__ ()
187+ except ImportError :
188+ logging .debug ("sentry_sdk is not installed, skipping Sentry transaction capture." )
189+ except Exception :
190+ logging .exception ("Failed to initialize Sentry or start transaction" )
191+
192+ # Initialize and track with Elastic APM if enabled
193+ elastic_client = None
194+ if config .ELASTIC_APM .IS_ENABLED :
195+ try :
196+ import elasticapm
197+
198+ # Initialize Elastic APM client with config
199+ elastic_client = elasticapm .get_client ()
200+ if not elastic_client :
201+ elastic_client = elasticapm .Client (config .ELASTIC_APM .model_dump ())
202+ elastic_client .begin_transaction (transaction_type = "function" )
203+ except ImportError :
204+ logging .debug ("elasticapm is not installed, skipping Elastic APM transaction capture." )
205+ except Exception :
206+ logging .exception ("Failed to initialize Elastic APM or start transaction" )
207+
208+ try :
209+ # Execute the async function
210+ result = await func (* args , ** kwargs )
211+
108212 # Mark transaction as successful
109213 if sentry_transaction :
110214 sentry_transaction .set_status ("ok" )
111215 if elastic_client :
112- elastic_client .end_transaction (name = transaction_name , result = "success" ) # type: ignore[no-untyped-call]
216+ elastic_client .end_transaction (name = transaction_name , result = "success" )
217+
113218 return result
219+ except Exception :
220+ # Mark transaction as failed and capture the exception
221+ if sentry_transaction :
222+ sentry_transaction .set_status ("internal_error" )
223+ if elastic_client :
224+ elastic_client .end_transaction (name = transaction_name , result = "error" )
225+ raise
114226 finally :
115227 # Clean up Sentry transaction
116228 if sentry_transaction :
@@ -190,6 +302,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
190302 sentry_span .__enter__ ()
191303 except ImportError :
192304 logging .debug ("sentry_sdk is not installed, skipping Sentry span capture." )
305+ except Exception :
306+ logging .exception ("Failed to start Sentry span" )
193307
194308 # Track with Elastic APM if enabled
195309 elastic_client = None
@@ -198,46 +312,171 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
198312 try :
199313 import elasticapm
200314
201- elastic_client = elasticapm .get_client () # type: ignore[attr-defined]
315+ elastic_client = elasticapm .get_client ()
202316 if elastic_client :
203- elastic_span = elastic_client .begin_span ( # type: ignore[attr-defined]
317+ elastic_span = elastic_client .begin_span (
204318 name = span_name ,
205319 span_type = op ,
206320 )
207321 except ImportError :
208322 logging .debug ("elasticapm is not installed, skipping Elastic APM span capture." )
323+ except Exception :
324+ logging .exception ("Failed to start Elastic APM span" )
209325
210326 try :
211327 # Execute the function
212328 result = func (* args , ** kwargs )
329+
330+ # Mark span as successful
331+ if sentry_span :
332+ sentry_span .set_status ("ok" )
333+
334+ return result
213335 except Exception as e :
214336 # Mark span as failed and capture the exception
215337 if sentry_span :
216338 sentry_span .set_status ("internal_error" )
217-
218- # Add exception context to spans
219- if sentry_span :
220339 sentry_span .set_tag ("error" , True )
221340 sentry_span .set_data ("exception" , str (e ))
222341
223342 if elastic_span and elastic_client :
224- elastic_client .capture_exception () # type: ignore[no-untyped-call]
343+ elastic_client .capture_exception ()
225344
226- # Re-raise the exception
227345 raise
228- else :
229- # Mark span as successful
346+ finally :
347+ # Clean up spans
348+ if elastic_span and elastic_client :
349+ try :
350+ elastic_client .end_span ()
351+ except Exception :
352+ logging .exception ("Error closing Elastic APM span" )
353+
354+ if sentry_span :
355+ try :
356+ sentry_span .__exit__ (None , None , None )
357+ except Exception :
358+ logging .exception ("Error closing Sentry span" )
359+
360+ return cast (F , wrapper )
361+
362+ return decorator
363+
364+
365+ def async_capture_span [F : Callable [..., Any ]](
366+ name : str | None = None ,
367+ * ,
368+ op : str = "function" ,
369+ description : str | None = None ,
370+ ) -> Callable [[F ], F ]:
371+ """Decorator to capture a span for the decorated async function.
372+
373+ This decorator creates a span around the execution of the decorated async function.
374+ Spans are child operations within a transaction and help provide detailed
375+ performance insights. Works with both Sentry and Elastic APM.
376+
377+ Args:
378+ name: Name of the span. If None, uses the function name.
379+ op: Operation type/category for the span. Defaults to "function".
380+ description: Optional description of the span.
381+
382+ Returns:
383+ The decorated async function with span tracing capabilities.
384+
385+ Example:
386+ ```python
387+ @async_capture_transaction(name="user_processing")
388+ async def process_user_data(user_id: int) -> dict[str, Any]:
389+ user = await get_user(user_id)
390+ processed_data = await transform_data(user)
391+ await save_result(processed_data)
392+ return processed_data
393+
394+ @async_capture_span(name="database_query", op="db")
395+ async def get_user(user_id: int) -> dict[str, Any]:
396+ # Async database query logic here
397+ return {"id": user_id, "name": "John"}
398+
399+ @async_capture_span(name="data_transformation", op="processing")
400+ async def transform_data(user: dict[str, Any]) -> dict[str, Any]:
401+ # Async data transformation logic
402+ return {"processed": True, **user}
403+
404+ @async_capture_span(name="save_operation", op="db")
405+ async def save_result(data: dict[str, Any]) -> None:
406+ # Async save logic here
407+ pass
408+ ```
409+ """
410+
411+ def decorator (func : F ) -> F :
412+ span_name = name or func .__name__
413+
414+ @functools .wraps (func )
415+ async def wrapper (* args : Any , ** kwargs : Any ) -> Any :
416+ config : Any = BaseConfig .global_config ()
417+
418+ # Track with Sentry if enabled
419+ sentry_span = None
420+ if config .SENTRY .IS_ENABLED :
421+ try :
422+ import sentry_sdk
423+
424+ sentry_span = sentry_sdk .start_span (
425+ op = op ,
426+ description = span_name ,
427+ )
428+ sentry_span .__enter__ ()
429+ except ImportError :
430+ logging .debug ("sentry_sdk is not installed, skipping Sentry span capture." )
431+ except Exception :
432+ logging .exception ("Failed to start Sentry span" )
433+
434+ # Track with Elastic APM if enabled
435+ elastic_client = None
436+ elastic_span_context = None
437+ if config .ELASTIC_APM .IS_ENABLED :
438+ try :
439+ import elasticapm
440+
441+ elastic_client = elasticapm .get_client ()
442+ if elastic_client :
443+ # Use async context manager for proper async handling
444+ elastic_span_context = elasticapm .async_capture_span (span_name , span_type = op )
445+ await elastic_span_context .__aenter__ ()
446+ except ImportError :
447+ logging .debug ("elasticapm is not installed, skipping Elastic APM span capture." )
448+ except Exception :
449+ logging .exception ("Failed to start Elastic APM span" )
450+
451+ try :
452+ # Execute the async function
453+ result = await func (* args , ** kwargs )
454+
455+ # Mark Sentry span as successful
230456 if sentry_span :
231457 sentry_span .set_status ("ok" )
458+
232459 return result
460+ except Exception as e :
461+ # Mark span as failed
462+ if sentry_span :
463+ sentry_span .set_status ("internal_error" )
464+ sentry_span .set_tag ("error" , True )
465+ sentry_span .set_data ("exception" , str (e ))
466+
467+ if elastic_client :
468+ elastic_client .capture_exception ()
469+
470+ raise
233471 finally :
234- # Clean up spans
235- if elastic_span and elastic_client :
472+ # Clean up Elastic APM span
473+ if elastic_span_context :
236474 try :
237- elastic_client . end_span () # type: ignore[attr-defined]
475+ await elastic_span_context . __aexit__ ( None , None , None )
238476 except Exception :
239477 logging .exception ("Error closing Elastic APM span" )
240478
479+ # Clean up Sentry span
241480 if sentry_span :
242481 try :
243482 sentry_span .__exit__ (None , None , None )
0 commit comments