Monitoring & Operations
Setting up logging, monitoring, and operational observability for PearDrop applications.
Structured Logging
Use structured logging to enable querying and analysis:
// ❌ BAD - String interpolation (unstructured)
logger.LogInformation($"Processing command {command.GetType().Name} for user {userId}");
// ✅ GOOD - Structured logging
logger.LogInformation(
"Processing command {CommandName} for user {UserId}",
command.GetType().Name,
userId);
// Structured output enables filtering/searching:
// - Find all commands for a specific user
// - Find slow commands by execution time
// - Correlate logs across services
Logging Strategy
Log at appropriate levels:
Debug Level
Detailed diagnostic information (local development):
logger.LogDebug(
"Query {QueryName} executed with {ParameterCount} parameters",
query.GetType().Name,
parameterCount);
Information Level
Normal application flow (production-safe):
logger.LogInformation(
"Command {CommandName} completed successfully for {UserId} in {ElapsedMilliseconds}ms",
command.GetType().Name,
userId,
stopwatch.ElapsedMilliseconds);
logger.LogInformation(
"User {UserId} logged in from {IpAddress}",
userId,
ipAddress);
Warning Level
Unusual but handled situations:
logger.LogWarning(
"Command {CommandName} validation failed with {ErrorCount} errors",
command.GetType().Name,
validationErrors.Count);
logger.LogWarning(
"Failed login attempt from IP {IpAddress} after {AttemptCount} attempts",
ipAddress,
failedAttempts);
Error Level
Recoverable errors:
logger.LogError(
"Command {CommandName} failed to execute: {Error}",
command.GetType().Name,
result.Error?.Message);
logger.LogError(
exception,
"Database save failed for aggregate {AggregateType}",
aggregateType);
Critical Level
Application might crash:
logger.LogCritical(
"Database connection lost to {DatabaseServer}",
databaseServer);
logger.LogCritical(
exception,
"Unexpected error in payment processing");
Command/Query Logging
Log command and query execution:
public sealed class LoggingBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : IBaseRequest
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> logger;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
logger.LogInformation(
"Executing {RequestType}",
typeof(TRequest).Name);
try
{
var response = await next();
stopwatch.Stop();
logger.LogInformation(
"Executed {RequestType} in {ElapsedMilliseconds}ms",
typeof(TRequest).Name,
stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
logger.LogError(
ex,
"Error executing {RequestType} after {ElapsedMilliseconds}ms",
typeof(TRequest).Name,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
// Register in DI
builder.Services.AddMediatR(config =>
{
config.RegisterServicesFromAssembly(typeof(Program).Assembly);
config.AddOpenBehavior(typeof(LoggingBehavior<,>));
});
Performance Monitoring
Track command/query execution time:
public sealed class PerformanceMonitoringBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : IBaseRequest
{
private readonly ILogger<PerformanceMonitoringBehavior<TRequest, TResponse>> logger;
private const int SlowThresholdMs = 500;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > SlowThresholdMs)
{
logger.LogWarning(
"Slow {RequestType} detected: {ElapsedMilliseconds}ms",
typeof(TRequest).Name,
stopwatch.ElapsedMilliseconds);
}
return response;
}
}
Failed Command Tracking
Monitor and alert on failed commands:
public sealed class CommandFailureTrackingBehavior<TRequest, TResponse> :
IPipelineBehavior<TRequest, TResponse>
where TRequest : IBaseRequest
{
private readonly ILogger<CommandFailureTrackingBehavior<TRequest, TResponse>> logger;
private readonly IMetricsService metricsService;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var response = await next();
// Check if response indicates failure
if (response is CommandResult result && !result.IsSuccess)
{
logger.LogWarning(
"Command {CommandName} failed: {ErrorCode} - {ErrorMessage}",
typeof(TRequest).Name,
result.Error?.Code,
result.Error?.Message);
// Track failure metric
this.metricsService.IncrementCounter(
$"command.failures.{typeof(TRequest).Name}");
}
return response;
}
}
Application Insights Integration
Enable detailed monitoring with Application Insights:
// appsettings.json
{
"ApplicationInsights": {
"InstrumentationKey": "YOUR_INSTRUMENTATION_KEY"
}
}
// Program.cs
builder.Services
.AddApplicationInsightsTelemetry(builder.Configuration);
// Add custom telemetry
builder.Services.AddSingleton<ITelemetryInitializer, UserIdTelemetryInitializer>();
// Custom telemetry initializer
public sealed class UserIdTelemetryInitializer : ITelemetryInitializer
{
private readonly IHttpContextAccessor httpContextAccessor;
public void Initialize(ITelemetry telemetry)
{
var userId = this.httpContextAccessor.HttpContext?.User
?.FindFirstValue(ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(userId))
{
telemetry.Context.User.Id = userId;
}
}
}
Health Checks
Monitor application health:
// Program.cs
builder.Services
.AddHealthChecks()
.AddDbContextCheck<OrderDbContext>("database")
.AddCheck<CustomHealthCheck>("custom");
app.MapHealthChecks("/health");
public sealed class DatabaseHealthCheck : IHealthCheck
{
private readonly OrderDbContext dbContext;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var canConnect = await this.dbContext.Database
.CanConnectAsync(cancellationToken);
return canConnect
? HealthCheckResult.Healthy("Database connection successful")
: HealthCheckResult.Unhealthy("Cannot connect to database");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(
"Database health check failed",
ex);
}
}
}
Configuration for Production
Logging Settings
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Information",
"YourApp": "Information"
},
"ApplicationInsights": {
"LogLevel": {
"Default": "Warning"
}
}
}
}
Health Check Settings
{
"HealthChecks": {
"Enabled": true,
"Interval": 60,
"Timeout": 10,
"FailureThreshold": 3
}
}
Best Practices
✅ DO:
- Log entry and exit of important operations
- Use appropriate log levels
- Include context in log messages (userId, commandName, etc.)
- Log exceptions with full stack traces
- Set up performance monitoring for slow operations
- Monitor failed commands and errors
- Use structured logging (not string interpolation)
- Configure different logging levels for environments
- Archive logs for compliance
- Set up alerts for critical errors
❌ DON'T:
- Log sensitive data (passwords, API keys, PII)
- Use Debug level in production (too verbose)
- Log at Error/Critical for expected failures
- Leave debug logging enabled in production
- Log passwords or tokens
- Skip error context (error codes, messages)
- Use string concatenation for logging
- Log every single operation (too much noise)
Monitoring Dashboard
Set up dashboards to track:
KPI Metrics
├── Failed Commands (by type)
├── Slow Operations (> 500ms)
├── Database Connection Failures
├── Authorization Failures
├── Validation Failure Rate
└── Command Success Rate
User Activity
├── Active Users
├── Login Attempts (including failures)
├── User Commands Executed
└── Error Rate by User
System Health
├── Database Health
├── CPU/Memory Usage
├── Request Duration (p50, p95, p99)
└── Error Rates
See Also
- Configuration Best Practices - Configuration management
- Testing PearDrop Applications - Testing strategies
- Security & Authorization - Security monitoring