Overview
The Auditing module provides a flexible, high-performance audit logging system that captures security events, HTTP activity, entity changes, and exceptions. It uses a channel-based publisher for non-blocking writes and supports extensible enrichment.
Module Order: 300 (loads after Identity and Multitenancy)
API Base Path: /api/v1/audits
Features
Security Audits Login attempts, token operations, and authorization failures
Activity Logging HTTP requests with timing, status codes, and payload capture
Entity Changes Track all database modifications with before/after snapshots
Exception Tracking Automatic exception capture with stack traces and context
Implementation
The module is defined in AuditingModule.cs (auditing/Modules.Auditing):
namespace FSH . Modules . Auditing ;
public class AuditingModule : IModule
{
public void ConfigureServices ( IHostApplicationBuilder builder )
{
// Configuration
var httpOpts = builder . Configuration . GetSection ( "Auditing" ). Get < AuditHttpOptions >() ?? new AuditHttpOptions ();
builder . Services . AddSingleton ( httpOpts );
// Core services
builder . Services . AddHttpContextAccessor ();
builder . Services . AddScoped < IAuditClient , DefaultAuditClient >();
builder . Services . AddScoped < ISecurityAudit , SecurityAudit >();
builder . Services . AddSingleton < IAuditSerializer , SystemTextJsonAuditSerializer >();
// Scope and enrichers
builder . Services . AddScoped < IAuditScope , HttpAuditScope >();
builder . Services . AddScoped < IAuditMaskingService , JsonMaskingService >();
// Background processing
builder . Services . AddSingleton < ChannelAuditPublisher >();
builder . Services . AddSingleton < IAuditPublisher >( sp => sp . GetRequiredService < ChannelAuditPublisher >());
builder . Services . AddSingleton < IAuditSink , SqlAuditSink >();
builder . Services . AddHostedService < AuditBackgroundWorker >();
// Audit configuration
builder . Services . AddHostedService < AuditingConfigurator >();
// Database
builder . Services . AddHeroDbContext < AuditDbContext >();
builder . Services . AddScoped < IDbInitializer , AuditDbInitializer >();
// Health checks
builder . Services . AddHealthChecks ()
. AddDbContextCheck < AuditDbContext >(
name : "db:auditing" ,
failureStatus : HealthStatus . Unhealthy );
}
public void MapEndpoints ( IEndpointRouteBuilder endpoints )
{
var apiVersionSet = endpoints . NewApiVersionSet ()
. HasApiVersion ( new ApiVersion ( 1 ))
. ReportApiVersions ()
. Build ();
var group = endpoints
. MapGroup ( "api/v{version:apiVersion}/audits" )
. WithTags ( "Audits" )
. WithApiVersionSet ( apiVersionSet );
// Query endpoints
group . MapGetAuditsEndpoint ();
group . MapGetAuditByIdEndpoint ();
group . MapGetAuditsByCorrelationEndpoint ();
group . MapGetAuditsByTraceEndpoint ();
group . MapGetSecurityAuditsEndpoint ();
group . MapGetExceptionAuditsEndpoint ();
group . MapGetAuditSummaryEndpoint ();
}
}
Audit Architecture
The auditing system uses a fluent, channel-based architecture:
Create Audit Event
Use the fluent Audit API to create typed audit events
Enrich Context
Enrichers automatically add user, tenant, trace, and correlation IDs
Channel Publishing
Events are published to an unbounded channel (non-blocking)
Background Processing
AuditBackgroundWorker consumes events and writes to database
Query API
Query endpoints provide filtering, search, and correlation
Audit API
The Audit static class provides a fluent interface for creating audit events (auditing/Core/Audit.cs):
Security Audit
using FSH . Modules . Auditing ;
using FSH . Modules . Auditing . Contracts ;
// Login succeeded
await Audit . ForSecurity ( SecurityAction . LoginSucceeded )
. WithUser ( userId : user . Id , userName : user . Email )
. WithSecurityContext (
subjectId : user . Id ,
clientId : "web-app" ,
authMethod : "Password" ,
claims : new Dictionary < string , object ?>
{
[ "ip" ] = httpContext . Connection . RemoteIpAddress ? . ToString (),
[ "userAgent" ] = httpContext . Request . Headers . UserAgent . ToString ()
})
. WriteAsync ();
// Login failed
await Audit . ForSecurity ( SecurityAction . LoginFailed )
. WithSecurityContext (
subjectId : loginRequest . Email ,
clientId : "web-app" ,
authMethod : "Password" ,
reasonCode : "InvalidCredentials" )
. WithSeverity ( AuditSeverity . Warning )
. WriteAsync ();
Activity Audit
// HTTP activity
await Audit . ForActivity ( ActivityKind . HttpRequest , "GET /api/products" )
. WithActivityResult (
statusCode : 200 ,
durationMs : 145 ,
captured : BodyCapture . Both ,
requestSize : 0 ,
responseSize : 4096 )
. WriteAsync ();
Entity Change Audit
// Entity modification
var changes = new []
{
new PropertyChange ( "Name" , "Old Name" , "New Name" ),
new PropertyChange ( "Price" , "99.99" , "129.99" )
};
await Audit . ForEntityChange (
dbContext : "CatalogDbContext" ,
schema : "catalog" ,
table : "Products" ,
entityName : "Product" ,
key : product . Id . ToString (),
operation : EntityOperation . Update ,
changes : changes )
. WithEntityTransactionId ( transaction . TransactionId . ToString ())
. WriteAsync ();
Exception Audit
try
{
// Some operation
}
catch ( Exception ex )
{
await Audit . ForException ( ex , ExceptionArea . Api , routeOrLocation : "/api/products" )
. WithSeverity ( AuditSeverity . Error )
. WriteAsync ();
throw ;
}
Security Audit Service
The ISecurityAudit service provides convenience methods for common security events (auditing/Core/SecurityAudit.cs):
public interface ISecurityAudit
{
ValueTask LoginSucceededAsync ( string userId , string userName , string clientId , string ip , string userAgent , CancellationToken ct = default );
ValueTask LoginFailedAsync ( string subjectIdOrName , string clientId , string reason , string ip , CancellationToken ct = default );
ValueTask TokenIssuedAsync ( string userId , string userName , string clientId , string tokenFingerprint , DateTime expiresUtc , CancellationToken ct = default );
ValueTask TokenRevokedAsync ( string userId , string clientId , string reason , CancellationToken ct = default );
}
Usage Example
public class TokenService
{
private readonly ISecurityAudit _securityAudit ;
public TokenService ( ISecurityAudit securityAudit )
{
_securityAudit = securityAudit ;
}
public async Task < TokenResponse > GenerateAsync ( LoginRequest request )
{
// Validate credentials...
if ( ! isValid )
{
await _securityAudit . LoginFailedAsync (
subjectIdOrName : request . Email ,
clientId : "web-app" ,
reason : "InvalidCredentials" ,
ip : GetClientIp ());
throw new UnauthorizedException ( "Invalid credentials" );
}
// Generate token...
await _securityAudit . LoginSucceededAsync (
userId : user . Id ,
userName : user . Email ,
clientId : "web-app" ,
ip : GetClientIp (),
userAgent : GetUserAgent ());
return token ;
}
}
Audit Client
The IAuditClient provides a scoped API for writing audits (auditing/Core/DefaultAuditClient.cs):
public interface IAuditClient
{
ValueTask WriteSecurityAsync (
SecurityAction action ,
string ? subjectId = null ,
string ? clientId = null ,
string ? authMethod = null ,
string ? reasonCode = null ,
IReadOnlyDictionary < string , object ?>? claims = null ,
AuditSeverity severity = AuditSeverity . Information ,
string ? source = null ,
CancellationToken ct = default );
ValueTask WriteActivityAsync (
ActivityKind kind ,
string name ,
int ? statusCode = null ,
int durationMs = 0 ,
CancellationToken ct = default );
ValueTask WriteExceptionAsync (
Exception exception ,
ExceptionArea area = ExceptionArea . None ,
string ? routeOrLocation = null ,
CancellationToken ct = default );
}
Audit Endpoints
Query Endpoints
Method Endpoint Description Permission GET/auditsList all audits with pagination Auditing.ViewGET/audits/{id}Get audit by ID Auditing.ViewGET/audits/correlation/{id}Get audits by correlation ID Auditing.ViewGET/audits/trace/{id}Get audits by trace ID Auditing.ViewGET/audits/securityGet security audits only Auditing.ViewGET/audits/exceptionsGet exception audits only Auditing.ViewGET/audits/summaryGet audit summary statistics Auditing.View
Example: List Audits
Endpoint implementation (auditing/Features/v1/GetAudits):
public static class GetAuditsEndpoint
{
public static RouteHandlerBuilder MapGetAuditsEndpoint ( this IEndpointRouteBuilder group )
{
return group . MapGet (
"/" ,
async ([ AsParameters ] GetAuditsQuery query , IMediator mediator , CancellationToken cancellationToken ) =>
await mediator . Send ( query , cancellationToken ))
. WithName ( "GetAudits" )
. WithSummary ( "List and search audit events" )
. WithDescription ( "Retrieve audit events with pagination and filters." )
. RequirePermission ( AuditingPermissionConstants . View );
}
}
Query Parameters
The GetAuditsQuery supports:
public record GetAuditsQuery : IQuery < PagedList < AuditDto >>
{
public int PageNumber { get ; init ; } = 1 ;
public int PageSize { get ; init ; } = 10 ;
public AuditEventType ? EventType { get ; init ; }
public AuditSeverity ? Severity { get ; init ; }
public string ? UserId { get ; init ; }
public string ? TenantId { get ; init ; }
public DateTime ? FromDate { get ; init ; }
public DateTime ? ToDate { get ; init ; }
public string ? SearchTerm { get ; init ; }
}
Example request:
GET /api/v1/audits?
eventType = Security &
severity = Warning &
userId = 123 &
fromDate = 2024-01-01 &
toDate = 2024-01-31 &
pageNumber = 1 &
pageSize = 50
Audit Event Types
public enum AuditEventType
{
EntityChange ,
Security ,
Activity ,
Exception
}
Security Actions
public enum SecurityAction
{
LoginSucceeded ,
LoginFailed ,
TokenIssued ,
TokenRevoked ,
PermissionDenied ,
PolicyFailed
}
Activity Kinds
public enum ActivityKind
{
HttpRequest ,
BackgroundJob ,
Integration ,
Command ,
Query
}
Exception Areas
public enum ExceptionArea
{
None ,
Api ,
Database ,
Integration ,
BackgroundJob
}
Severity Levels
public enum AuditSeverity
{
Information ,
Warning ,
Error ,
Critical
}
Audit Scope
The IAuditScope provides ambient context for the current request:
public interface IAuditScope
{
string ? TenantId { get ; }
string ? UserId { get ; }
string ? UserName { get ; }
string ? TraceId { get ; }
string ? SpanId { get ; }
string ? CorrelationId { get ; }
string ? RequestId { get ; }
string ? Source { get ; }
AuditTag Tags { get ; }
}
The HttpAuditScope implementation automatically populates from HttpContext.
Enrichers
Enrichers add contextual information to audit events:
public interface IAuditEnricher
{
void Enrich ( AuditEnvelope envelope );
}
Common enrichments:
Tenant ID from multitenancy context
User ID and name from claims
Trace and span IDs from distributed tracing
Correlation ID for request grouping
Masking Sensitive Data
The IAuditMaskingService redacts sensitive fields:
public interface IAuditMaskingService
{
string Mask ( string json , IEnumerable < string > fieldsToMask );
}
Example:
var masked = maskingService . Mask (
json : requestBody ,
fieldsToMask : new [] { "password" , "ssn" , "creditCard" });
Background Processing
Audits are processed asynchronously via AuditBackgroundWorker:
public class AuditBackgroundWorker : BackgroundService
{
protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
{
await foreach ( var envelope in _channel . Reader . ReadAllAsync ( stoppingToken ))
{
await _sink . WriteAsync ( envelope , stoppingToken );
}
}
}
Channel Publisher
The ChannelAuditPublisher uses System.Threading.Channels for high-throughput:
public class ChannelAuditPublisher : IAuditPublisher
{
private readonly Channel < IAuditEvent > _channel = Channel . CreateUnbounded < IAuditEvent >();
public async ValueTask PublishAsync ( IAuditEvent auditEvent , CancellationToken ct = default )
{
await _channel . Writer . WriteAsync ( auditEvent , ct );
}
}
Audit Sink
The SqlAuditSink writes to the database:
public interface IAuditSink
{
ValueTask WriteAsync ( IAuditEvent auditEvent , CancellationToken ct = default );
}
Configuration
Configure auditing options in appsettings.json:
{
"Auditing" : {
"CaptureRequestBody" : true ,
"CaptureResponseBody" : false ,
"MaxBodySize" : 4096 ,
"ExcludedPaths" : [
"/health" ,
"/metrics" ,
"/swagger"
],
"SensitiveFields" : [
"password" ,
"token" ,
"secret" ,
"apiKey"
]
}
}
Options Model
public class AuditHttpOptions
{
public bool CaptureRequestBody { get ; set ; } = true ;
public bool CaptureResponseBody { get ; set ; } = false ;
public int MaxBodySize { get ; set ; } = 4096 ;
public List < string > ExcludedPaths { get ; set ; } = new ();
public List < string > SensitiveFields { get ; set ; } = new ();
}
Capturing response bodies can significantly increase storage requirements. Only enable for debugging.
Database Context
public class AuditDbContext : DbContext
{
public DbSet < AuditLog > AuditLogs { get ; set ; }
}
public class AuditLog
{
public Guid Id { get ; set ; }
public DateTime OccurredAtUtc { get ; set ; }
public DateTime ReceivedAtUtc { get ; set ; }
public AuditEventType EventType { get ; set ; }
public AuditSeverity Severity { get ; set ; }
public string ? TenantId { get ; set ; }
public string ? UserId { get ; set ; }
public string ? UserName { get ; set ; }
public string ? TraceId { get ; set ; }
public string ? SpanId { get ; set ; }
public string ? CorrelationId { get ; set ; }
public string ? RequestId { get ; set ; }
public string ? Source { get ; set ; }
public AuditTag Tags { get ; set ; }
public string PayloadJson { get ; set ; } = default ! ;
}
Correlation and Tracing
Audits support distributed tracing and correlation:
By Trace ID
Get all audits for a distributed trace:
GET /api/v1/audits/trace/00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
By Correlation ID
Get all audits for a logical operation:
GET /api/v1/audits/correlation/order-12345
Tags provide additional categorization:
[ Flags ]
public enum AuditTag
{
None = 0 ,
Critical = 1 ,
Compliance = 2 ,
Security = 4 ,
Performance = 8 ,
Debug = 16
}
Usage:
await Audit . ForSecurity ( SecurityAction . LoginFailed )
. WithTags ( AuditTag . Critical | AuditTag . Security )
. WriteAsync ();
Best Practices
Sensitive Data Always mask passwords, tokens, and PII in audit logs
Retention Policy Implement automatic cleanup of old audit records
Performance Channel-based publishing ensures auditing doesn’t block requests
Compliance Use audit logs for GDPR, SOC2, and regulatory compliance
Audit logs are immutable. Never allow deletion or modification except via automated retention policies.
Next Steps
Creating Modules Learn how to build your own custom modules
Identity Module Explore authentication and security features