I am using Sentry and sentry_sdk to monitor errors and traces in my Python application. I want to configure the error and trace rates for different routes in my FastAPI API. To do this, I want to write two decorators called sentry_error_rate
and sentry_trace_rate
that will allow me to set the sample rates for errors and traces, respectively.
The sentry_error_rate
decorator should take a single argument errors_sample_rate
(a float between 0 and 1) and apply it to a specific route.
The sentry_trace_rate
decorator should take a single argument traces_sample_rate
(also a float between 0 and 1) and apply it to a specific route.
def sentry_trace_rate(traces_sample_rate: float = 0.0) -> callable:
""" Decorator to set the traces_sample_rate for a specific route.
This is useful for routes that are called very frequently, but we
want to sample them to reduce the amount of data we send to Sentry.
Args:
traces_sample_rate (float): The sample rate to use for this route.
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Do something here ?
return await func(*args, **kwargs)
return wrapper
return decorator
def sentry_error_rate(errors_sample_rate: float = 0.0) -> callable:
""" Decorator to set the errors_sample_rate for a specific route.
This is useful for routes that are called very frequently, but we
want to sample them to reduce the amount of data we send to Sentry.
Args:
errors_sample_rate (float): The sample rate to use for this route.
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Do something here ?
return await func(*args, **kwargs)
return wrapper
return decorator
Does someone have an idea if this is possible and how it could be done ?
I finally managed to do it using a registry mechanism. Each route with a decorator are registred in a dictionary with their trace/error rate. I then used a trace_sampler/before_send function as indicated here:
Here's my sentry_wrapper.py:
import asyncio
import random
from functools import wraps
from typing import Callable, Union
from fastapi import APIRouter
_route_traces_entrypoints = {}
_route_errors_entrypoints = {}
_fn_traces_entrypoints = {}
_fn_errors_entrypoints = {}
_fn_to_route_entrypoints = {}
def sentry_trace_rate(trace_sample_rate: float = 0.0) -> Callable:
"""Decorator to set the sentry trace rate for a specific endpoint.
This is useful for endpoints that are called very frequently,
and we don't want to report all traces.
Args:
trace_sample_rate (float): The rate to sample traces. 0.0 to disable traces.
"""
def decorator(fn: Callable) -> Callable:
# Assert there is not twice function with the same nam
if fn.__name__ in _fn_traces_entrypoints:
raise ValueError(f"Two function have the same name: {fn.__name__} | {fn.__file__}")
# Add fn entrypoint
_fn_traces_entrypoints[fn.__name__] = trace_sample_rate
# Check for coroutines and return the right wrapper
if asyncio.iscoroutinefunction(fn):
@wraps(fn)
async def wrapper(*args, **kwargs) -> Callable:
return await fn(*args, **kwargs)
return wrapper
else:
@wraps(fn)
def wrapper(*args, **kwargs) -> Callable:
return fn(*args, **kwargs)
return wrapper
return decorator
def sentry_error_rate(error_sample_rate: float = 0.0) -> Callable:
"""Decorator to set the sentry error rate for a specific endpoint.
This is useful for endpoints that are called very frequently,
and we don't want to report all errors.
Args:
error_sample_rate (float): The rate to sample errors. 0.0 to disable errors.
"""
def decorator(fn: Callable) -> Callable:
# Assert there is not twice function with the same nam
if fn.__name__ in _fn_errors_entrypoints:
raise ValueError(f"Two function have the same name: {fn.__name__} | {fn.__file__}")
# Add fn entrypoint
_fn_errors_entrypoints[fn.__name__] = error_sample_rate
# Check for coroutines and return the right wrapper
if asyncio.iscoroutinefunction(fn):
@wraps(fn)
async def wrapper(*args, **kwargs) -> Callable:
return await fn(*args, **kwargs)
return wrapper
else:
@wraps(fn)
def wrapper(*args, **kwargs) -> Callable:
return fn(*args, **kwargs)
return wrapper
return decorator
def register_traces_disabler(router: APIRouter) -> None:
"""Register all the entrypoints for the traces disabler
Args:
router (APIRouter): The router to register
"""
for route in router.routes:
if route.name in _fn_traces_entrypoints:
_route_traces_entrypoints[route.path] = _fn_traces_entrypoints[route.name]
def register_errors_disabler(router: APIRouter) -> None:
"""Register all the entrypoints for the errors disabler
Args:
router (APIRouter): The router to register
"""
for route in router.routes:
if route.name in _fn_errors_entrypoints:
_route_errors_entrypoints[route.path] = _fn_errors_entrypoints[route.name]
class TracesSampler:
"""Class to sample traces for sentry
Args:
default_traces_sample_rate (float, optional): The default sample rate for traces.
Defaults to 1.0.
"""
def __init__(self, default_traces_sample_rate: float = 1.0) -> None:
self.default_traces_sample_rate = default_traces_sample_rate
def __call__(self, sampling_context) -> float:
return _route_traces_entrypoints.get(sampling_context["asgi_scope"]["path"], self.default_traces_sample_rate)
class BeforeSend:
"""Class to sample event before sending them to sentry
Args:
default_errors_sample_rate (float, optional): The default sample rate for errors.
Defaults to 1.0.
"""
def __init__(self, default_errors_sample_rate: float = 1.0) -> None:
self.default_errors_sample_rate = default_errors_sample_rate
def __call__(self, event: dict, hint: dict) -> Union[dict, None]:
# Get the sample rate for this route, or use the default if it's not defined
sample_rate = _route_errors_entrypoints.get(event["transaction"], self.default_errors_sample_rate)
# Generate a random number between 0 and 1, and discard the event if it's greater than the sample rate
if random.random() > sample_rate:
return None
# Return the event if it should be captured
return event
I have then ro register some routes:
@router.get("/route")
@sentry_wrapper.sentry_trace_rate(trace_sample_rate=0.5) # limit traces to 50%
@sentry_wrapper.sentry_error_rate(error_sample_rate=0.25) # limit error to 25%
def route_fn():
pass
And don't forget to register each route at the end of the file:
from app.services.sentry_wrapper import register_errors_disabler, register_traces_disabler
register_traces_disabler(router)
register_errors_disabler(router)