Interceptor Tutorials¶
This page covers ArchiPy's remaining interceptors (gRPC exception handling and rate limiting) and how OpenTelemetry is wired through AppUtils rather than custom metric/trace interceptors.
OpenTelemetry (replaces metric/trace interceptors)¶
FastAPI metric middleware and gRPC metric/trace interceptors were removed in 5.0.0. Traces and
metrics come from OpenTelemetry contrib instrumentors configured via BaseConfig.OTEL.
| Stack | Wiring |
|---|---|
| FastAPI | AppUtils.create_fastapi_app → FastAPIUtils.setup_otel (FastAPIInstrumentor) |
| gRPC | AppUtils.create_grpc_app / create_async_grpc_app → contrib server interceptor at index 0 |
| Client gRPC | OtelUtils.grpc_client_interceptors() / async_grpc_client_interceptors() |
import logging
from archipy.configs.base_config import BaseConfig
from archipy.helpers.utils.app_utils import AppUtils
logger = logging.getLogger(__name__)
config = BaseConfig.global_config()
app = AppUtils.create_fastapi_app(config)
grpc_server = AppUtils.create_grpc_app(config)
logger.info("Apps created with OTel auto-instrumentation")
Install archipy[otel-fastapi] and/or archipy[otel-grpc] as needed. Full config and decorator
reference: Observability.
gRPC Interceptors¶
Exception Interceptor¶
GrpcServerExceptionInterceptor / AsyncGrpcServerExceptionInterceptor convert domain errors
to gRPC status codes and call BaseUtils.capture_exception (records on the current OTel span).
AppUtils.create_grpc_app / create_async_grpc_app always register the matching interceptor.
import logging
from concurrent import futures
import grpc
from archipy.helpers.interceptors.grpc.exception.server_interceptor import (
GrpcServerExceptionInterceptor,
)
from archipy.models.errors import InternalError
logger = logging.getLogger(__name__)
def create_grpc_server(max_workers: int = 10) -> grpc.Server:
"""Create a gRPC server with the exception interceptor.
Args:
max_workers: Maximum worker threads for the server.
Returns:
Configured gRPC server instance.
Raises:
InternalError: If server creation fails.
"""
try:
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=max_workers),
interceptors=[GrpcServerExceptionInterceptor()],
)
logger.info("gRPC server created with exception interceptor")
return server
except OSError as e:
raise InternalError(additional_data={"detail": "Failed to create gRPC server"}) from e
Rate Limiting Interceptor¶
Per-RPC gRPC rate limits use two pieces:
grpc_rate_limit_decorator— declares limits on servicer methods (see Decorator Tutorials — gRPC Rate Limit)GrpcServerRateLimitInterceptor/AsyncGrpcServerRateLimitInterceptor— enforces decorated limits via RedisINCREX
Install the required extras:
Note: Redis 8.8+ is required. The interceptor depends on the
INCREXcommand.
Enable automatic registration:
With IS_ENABLED, AppUtils.create_grpc_app / create_async_grpc_app append the matching
interceptor. Per-RPC limits still come only from grpc_rate_limit_decorator.
Sync:
from archipy.configs.base_config import BaseConfig
from archipy.helpers.decorators import grpc_rate_limit_decorator
from archipy.helpers.utils.app_utils import AppUtils
class MySyncServiceServicer(pb2_grpc.MyServiceServicer):
@grpc_rate_limit_decorator(calls_count=100, minutes=1)
def Cheap(self, request, context):
return pb2.CheapResponse()
@grpc_rate_limit_decorator(calls_count=10, seconds=1)
@grpc_rate_limit_decorator(calls_count=1000, days=1)
def Expensive(self, request, context):
return pb2.ExpensiveResponse()
config = BaseConfig.global_config()
server = AppUtils.create_grpc_app(config)
Async:
from archipy.configs.base_config import BaseConfig
from archipy.helpers.decorators import grpc_rate_limit_decorator
from archipy.helpers.utils.app_utils import AppUtils
class MyAsyncServiceServicer(pb2_grpc.MyServiceServicer):
@grpc_rate_limit_decorator(calls_count=100, minutes=1)
async def Cheap(self, request, context):
return pb2.CheapResponse()
@grpc_rate_limit_decorator(calls_count=10, seconds=1)
@grpc_rate_limit_decorator(calls_count=1000, days=1)
async def Expensive(self, request, context):
return pb2.ExpensiveResponse()
config = BaseConfig.global_config()
server = AppUtils.create_async_grpc_app(config)
Manual registration (when IS_ENABLED is false) via customized_interceptors is still supported;
pass GrpcServerRateLimitInterceptor or AsyncGrpcServerRateLimitInterceptor explicitly.
Configuration¶
GRPC_RATE_LIMIT__IS_ENABLED=true
GRPC_RATE_LIMIT__FAIL_CLOSED=true
GRPC_RATE_LIMIT__IDENTITY_FROM_ACCESS_TOKEN=true
Client identification¶
By default the interceptor buckets authenticated callers by verified JWT access token sub from
gRPC invocation metadata (authorization: Bearer ...). Missing or invalid tokens fall back to
context.peer() (for example ipv4:1.2.3.4:5678).
Warning:
context.peer()reflects the immediate TCP peer. Behind an L7 proxy that reconnects upstream, the peer is often the proxy address. Passidentifier_fnto the interceptor when you need a custom server-resolved identity.
Undecorated servicer methods are never rate-limited. Stacked decorators add multiple windows; all windows must pass before the RPC handler runs.
FastAPI Rate Limiting¶
ArchiPy no longer ships a FastAPI rate-limit handler. Use the official
fastapi-redis-sdk:
from fastapi import Depends, FastAPI
from redis_fastapi import FastAPIRedis, rate_limit
app = FastAPI()
FastAPIRedis(app).lifespan().rate_limiting()
@app.get(
"/search",
dependencies=[
Depends(rate_limit("10/second", scope="search:burst")),
Depends(rate_limit("100/minute", scope="search:sustained")),
],
)
async def search() -> dict[str, str]:
return {"results": []}
The gRPC rate-limit interceptor is unaffected.
See Also¶
- Observability — OpenTelemetry config, AppUtils wiring, decorators
- API Reference - Interceptors — interceptor API docs
- Redis Adapter - INCREX — Redis
INCREXwindow counter - Helper Tutorials — overview of all helper tutorials