Optimizing FastAPI Audit Logging: Balancing Performance and Compliance
A journey from blocking middleware to high-performance audit logging while maintaining security requirements.
Challenge
Lack of audit logging
Solution
Async logging with Azure Application Insights
Impact
Full audit compliance with minimal overhead
Introduction
When building enterprise APIs, we often face competing requirements: comprehensive audit logging for security and compliance, while maintaining high-performance responses for end-users. This was exactly the challenge I faced when implementing a FastAPI-based data exchange API that needed to log detailed audit information to Azure Application Insights.
In this post, I'll walk you through my journey from a basic blocking implementation to a high-performance solution that maintains full audit capabilities without compromising user experience.
Setting Up Basic Audit Logging
Initial Requirements
The audit logging system needed to capture:
- Request details (method, path, query parameters)
- User information from Azure AD tokens
- Request body content for POST/PUT operations
- Response status and timing
- Geographic location and client IP
First Implementation
Our initial approach used FastAPI's middleware system with a basic logging setup:
class AuditLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Basic logging implementation
start_time = time()
response = await call_next(request)
duration = time() - start_time
logging.info(f"Request: {request.method} {request.url.path}")
return response
While this worked, it lacked structured logging and proper integration with Azure Application Insights.
Integrating with Azure Application Insights
The next step was creating a custom handler to properly format our logs for Azure Application Insights:
class ApplicationInsightsHandler(AzureLogHandler):
def __init__(self, connection_string):
super().__init__(connection_string=connection_string)
def emit(self, record):
if hasattr(record, "custom_dimensions"):
self.add_telemetry_processor(
self._add_custom_dimensions(record.custom_dimensions)
)
super().emit(record)
This allowed us to send structured data with custom dimensions, making our logs more queryable and valuable for analysis.
Addressing Performance Bottlenecks
Our initial implementation had a significant issue: it was blocking the request while gathering all the audit information. With complex token decoding and body parsing, some requests were taking over 200ms just for logging!
The solution came in three parts:
- Parallel processing of the audit information
- Asynchronous logging
- Careful request body handling
The Optimized Solution
Our final implementation achieved both goals: comprehensive audit logging and minimal impact on response times. Key improvements included:
- Immediate request forwarding to reduce latency
- Asynchronous token processing
- Efficient body handling
- Structured logging with custom dimensions
The results were impressive: audit logging overhead dropped from 200+ms to less than 1ms per request.
Key Takeaways
This journey taught several valuable lessons:
- Always measure performance impact of middleware
- Use async operations whenever possible
- Structure your logs for easy querying
- Consider the end-user experience in every decision