Audit Logging
PhotoFlow implements comprehensive audit logging for performance monitoring, activity tracking, and debugging.
Overview
The audit log system provides:
Performance monitoring for all operations
Activity tracking with detailed context
Async processing with internal queuing
Error tracking for debugging
AuditLog Entity
public class AuditLogEntity : BaseEntity<Guid>
{
public string ServiceName { get; private set; }
public string Action { get; private set; }
public string EntityType { get; private set; }
public string EntityId { get; private set; }
public string UserId { get; private set; }
public string OrganizationId { get; private set; }
public string ProviderId { get; private set; }
public string RequestId { get; private set; }
public string CorrelationId { get; private set; }
public string Operation { get; private set; }
public string Status { get; private set; }
public string ErrorMessage { get; private set; }
public string RequestData { get; private set; } // JSON
public string ResponseData { get; private set; } // JSON
public long ExecutionTimeMs { get; private set; }
public string IpAddress { get; private set; }
public string UserAgent { get; private set; }
public string AdditionalData { get; private set; } // JSON
public DateTime? StartedAt { get; private set; }
public DateTime? CompletedAt { get; private set; }
}
Decorator Pattern
PhotoFlow uses the Decorator Pattern to add audit logging without modifying core services:
public class AuditedFolderService : IFolderService
{
private readonly IFolderService _folderService;
private readonly IAuditLogService _auditLogService;
private readonly IAuthContext _authContext;
private readonly ILogger<AuditedFolderService> _logger;
public async Task<bool> SyncByIdAsync(
CloudProviderType provider,
Guid folderId,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
Guid auditLogId = Guid.Empty;
try
{
// Create audit log entry at start
var auditLog = await _auditLogService.CreateAuditLogAsync(
serviceName: "Storage",
action: "SyncFolder",
entityType: "Folder",
entityId: folderId.ToString(),
userId: _authContext.UserId,
organizationId: _authContext.OrganizationId,
requestData: JsonSerializer.Serialize(new { provider, folderId })
);
auditLogId = auditLog.Id;
// Execute actual operation
var result = await _folderService.SyncByIdAsync(
provider, folderId, cancellationToken);
stopwatch.Stop();
// Mark as completed
await _auditLogService.MarkCompletedAsync(
auditLogId,
JsonSerializer.Serialize(new { success = result }),
stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
await _auditLogService.MarkFailedAsync(
auditLogId,
ex.Message,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
Async Queue Processing
Audit logs are processed asynchronously to minimize performance impact:
public class InternalAuditLogQueueService : IAuditLogService, IDisposable
{
private readonly ConcurrentQueue<AuditLogOperation> _queue;
private readonly SemaphoreSlim _semaphore;
private readonly int _batchSize = 50;
private readonly TimeSpan _processingInterval = TimeSpan.FromMilliseconds(100);
public Task<AuditLogEntity> CreateAuditLogAsync(...)
{
// Enqueue and return immediately
var operation = new AuditLogOperation
{
Type = AuditLogOperationType.Create,
Entity = AuditLogEntity.Create(...)
};
_queue.Enqueue(operation);
return Task.FromResult(operation.Entity);
}
private async Task ProcessQueueAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var batch = new List<AuditLogOperation>();
// Collect batch
while (batch.Count < _batchSize &&
_queue.TryDequeue(out var op))
{
batch.Add(op);
}
if (batch.Count > 0)
{
await ProcessBatchAsync(batch);
}
await Task.Delay(_processingInterval, ct);
}
}
}
Configuration
Service Registration
// Program.cs
services.AddScoped<IFolderService, FolderService>();
services.Decorate<IFolderService, AuditedFolderService>();
services.AddSingleton<IAuditLogService, InternalAuditLogQueueService>();
Queue Settings
{
"AuditLog": {
"BatchSize": 50,
"ProcessingIntervalMs": 100,
"MaxConcurrency": 4,
"RetentionDays": 90
}
}
Average Execution Time by Operation
SELECT
"Operation",
AVG("ExecutionTimeMs") as AvgTime,
MAX("ExecutionTimeMs") as MaxTime,
MIN("ExecutionTimeMs") as MinTime,
COUNT(*) as Count
FROM "AuditLogs"
WHERE "Status" = 'Completed'
GROUP BY "Operation"
ORDER BY AvgTime DESC;
Slow Operations (> 5 seconds)
SELECT
"ServiceName",
"Operation",
"EntityType",
"EntityId",
"ExecutionTimeMs",
"CompletedAt"
FROM "AuditLogs"
WHERE "ExecutionTimeMs" > 5000
ORDER BY "ExecutionTimeMs" DESC
LIMIT 100;
Failed Operations
SELECT
"ServiceName",
"Operation",
"ErrorMessage",
"RequestData",
"CompletedAt"
FROM "AuditLogs"
WHERE "Status" = 'Failed'
ORDER BY "CompletedAt" DESC
LIMIT 100;
User Activity
SELECT
"UserId",
COUNT(*) as ActivityCount,
AVG("ExecutionTimeMs") as AvgExecutionTime
FROM "AuditLogs"
WHERE "CreatedAt" > NOW() - INTERVAL '24 hours'
GROUP BY "UserId"
ORDER BY ActivityCount DESC;
Organization Usage
SELECT
"OrganizationId",
"Operation",
COUNT(*) as UsageCount
FROM "AuditLogs"
WHERE "CreatedAt" > NOW() - INTERVAL '7 days'
GROUP BY "OrganizationId", "Operation"
ORDER BY UsageCount DESC;
Correlation Tracking
All operations include correlation IDs for cross-service tracing:
public class CorrelationMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var correlationId = context.Request.Headers["X-Correlation-ID"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers["X-Correlation-ID"] = correlationId;
await next(context);
}
}
Usage in Audit Logs
var auditLog = await _auditLogService.CreateAuditLogAsync(
serviceName: "Photoflow",
action: "CreateAlbum",
correlationId: _httpContext.Items["CorrelationId"]?.ToString()
);
Log Levels
Status | Description |
|---|
Started
| Operation began |
Completed
| Operation succeeded |
Failed
| Operation failed with error |
Cancelled
| Operation was cancelled |
Retention Policy
Audit logs are retained based on configuration:
public class AuditLogCleanupJob : IHostedService
{
private readonly int _retentionDays = 90;
public async Task ExecuteAsync(CancellationToken ct)
{
await _repository.DeleteOlderThanAsync(
DateTime.UtcNow.AddDays(-_retentionDays), ct);
}
}
Dashboard Integration
Audit logs power the admin dashboard metrics:
Operation counts per service/time period
Average response times by endpoint
Error rates and patterns
User activity heatmaps
Organization usage statistics
Database Overview
Storage Service
API Overview
Last modified: 20 December 2025