Skip to main content

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