Observabilidade e Monitoramento para .NET Senior
📋 Conceitos Fundamentais
Os Três Pilares da Observabilidade
- Logs: Registros estruturados de eventos
- Metrics: Medições quantitativas de performance
- Traces: Rastreamento de requisições através do sistema
Por que Observabilidade é Crítica?
- Detecção Proativa: Identificar problemas antes que afetem usuários
- Debugging Rápido: Localizar a causa raiz de problemas
- Performance: Otimizar pontos de gargalo
- Business Intelligence: Entender comportamento do usuário
📝 Logging Estruturado
1. Configuração do Serilog
csharp
public class Program
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(new JsonFormatter())
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
{
AutoRegisterTemplate = true,
IndexFormat = $"logs-{DateTime.UtcNow:yyyy-MM}"
})
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.CreateLogger();
try
{
Log.Information("Starting web application");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
}2. Logging Estruturado em Controllers
csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly ILogger<OrdersController> _logger;
private readonly IOrderService _orderService;
public OrdersController(ILogger<OrdersController> logger, IOrderService orderService)
{
_logger = logger;
_orderService = orderService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["CustomerId"] = request.CustomerId,
["OrderId"] = Guid.NewGuid().ToString()
});
_logger.LogInformation("Creating new order for customer {CustomerId}", request.CustomerId);
try
{
var order = await _orderService.CreateOrderAsync(request);
_logger.LogInformation("Order {OrderId} created successfully with total {TotalAmount}",
order.Id, order.TotalAmount);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation failed for order creation: {ValidationErrors}",
string.Join(", ", ex.Errors));
return BadRequest(ex.Errors);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create order for customer {CustomerId}", request.CustomerId);
throw;
}
}
}3. Middleware de Logging
csharp
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
var requestBody = await GetRequestBodyAsync(context.Request);
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["RequestId"] = context.TraceIdentifier,
["UserAgent"] = context.Request.Headers["User-Agent"].ToString(),
["IPAddress"] = context.Connection.RemoteIpAddress?.ToString()
});
_logger.LogInformation("HTTP {Method} {Path} started",
context.Request.Method, context.Request.Path);
try
{
await _next(context);
stopwatch.Stop();
_logger.LogInformation("HTTP {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds, context.Response.StatusCode);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "HTTP {Method} {Path} failed after {ElapsedMs}ms",
context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds);
throw;
}
}
private async Task<string> GetRequestBodyAsync(HttpRequest request)
{
request.EnableBuffering();
using var reader = new StreamReader(request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
request.Body.Position = 0;
return body;
}
}📊 Métricas e Monitoramento
1. Prometheus Metrics
csharp
public class MetricsService
{
private readonly Counter _orderCreatedCounter;
private readonly Histogram _orderProcessingDuration;
private readonly Gauge _activeOrdersGauge;
public MetricsService()
{
_orderCreatedCounter = Metrics.CreateCounter("orders_created_total", "Total orders created");
_orderProcessingDuration = Metrics.CreateHistogram("order_processing_duration_seconds",
"Order processing duration in seconds");
_activeOrdersGauge = Metrics.CreateGauge("active_orders", "Number of active orders");
}
public void IncrementOrderCreated(string customerType)
{
_orderCreatedCounter.WithLabels(customerType).Inc();
}
public IDisposable MeasureOrderProcessing()
{
return _orderProcessingDuration.NewTimer();
}
public void SetActiveOrders(int count)
{
_activeOrdersGauge.Set(count);
}
}2. Health Checks
csharp
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database")
.AddCheck<RedisHealthCheck>("redis")
.AddCheck<ExternalApiHealthCheck>("external-api")
.AddPrometheusGatewayPublisher();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.UseHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.UseHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
}
}
public class DatabaseHealthCheck : IHealthCheck
{
private readonly IDbConnection _connection;
public DatabaseHealthCheck(IDbConnection connection)
{
_connection = connection;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
await _connection.QueryAsync<int>("SELECT 1");
return HealthCheckResult.Healthy("Database is responding");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Database is not responding", ex);
}
}
}🔍 Distributed Tracing
1. Configuração do Jaeger
csharp
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenTelemetryTracing(builder =>
{
builder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddJaegerExporter(opts =>
{
opts.AgentHost = "localhost";
opts.AgentPort = 6831;
});
});
}
}2. Custom Spans
csharp
public class OrderService
{
private readonly Tracer _tracer;
public OrderService(Tracer tracer)
{
_tracer = tracer;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
using var span = _tracer.StartActiveSpan("create-order");
span.SetAttribute("customer.id", request.CustomerId);
span.SetAttribute("order.items.count", request.Items.Count);
try
{
using var validationSpan = _tracer.StartActiveSpan("validate-order");
await ValidateOrderAsync(request);
validationSpan.SetStatus(Status.Ok);
using var persistenceSpan = _tracer.StartActiveSpan("persist-order");
var order = await PersistOrderAsync(request);
persistenceSpan.SetStatus(Status.Ok);
using var notificationSpan = _tracer.StartActiveSpan("send-notifications");
await SendOrderNotificationsAsync(order);
notificationSpan.SetStatus(Status.Ok);
span.SetStatus(Status.Ok);
return order;
}
catch (Exception ex)
{
span.SetStatus(Status.Error, ex.Message);
span.RecordException(ex);
throw;
}
}
}3. Correlation IDs
csharp
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Response.Headers["X-Correlation-ID"] = correlationId;
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
});
await _next(context);
}
}📈 APM (Application Performance Monitoring)
1. Application Insights
csharp
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = Configuration["ApplicationInsights:ConnectionString"];
options.EnableAdaptiveSampling = false;
options.EnableQuickPulseMetricStream = true;
});
services.AddApplicationInsightsTelemetryProcessor<CustomTelemetryProcessor>();
}
}
public class CustomTelemetryProcessor : ITelemetryProcessor
{
private readonly ITelemetryProcessor _next;
public CustomTelemetryProcessor(ITelemetryProcessor next)
{
_next = next;
}
public void Process(ITelemetry item)
{
if (item is RequestTelemetry request)
{
request.Context.User.Id = GetUserIdFromContext();
request.Context.Session.Id = GetSessionId();
}
_next.Process(item);
}
}2. Custom Metrics
csharp
public class BusinessMetricsService
{
private readonly TelemetryClient _telemetryClient;
public BusinessMetricsService(TelemetryClient telemetryClient)
{
_telemetryClient = telemetryClient;
}
public void TrackOrderCreated(string customerType, decimal orderValue)
{
_telemetryClient.TrackEvent("OrderCreated", new Dictionary<string, string>
{
["CustomerType"] = customerType
}, new Dictionary<string, double>
{
["OrderValue"] = (double)orderValue
});
}
public void TrackUserRegistration(string source)
{
_telemetryClient.TrackEvent("UserRegistered", new Dictionary<string, string>
{
["Source"] = source
});
}
}🚨 Alerting e Notificações
1. Alert Rules (Prometheus)
yaml
groups:
- name: application_alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors per second"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
description: "95th percentile response time is {{ $value }} seconds"
- alert: DatabaseConnectionIssues
expr: up{job="database"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Database is down"
description: "Database connection is not responding"2. Alert Manager Configuration
yaml
global:
slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'slack-notifications'
receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#alerts'
title: '{{ template "slack.title" . }}'
text: '{{ template "slack.text" . }}'
send_resolved: true🔧 Ferramentas e Integrações
1. ELK Stack (Elasticsearch, Logstash, Kibana)
csharp
public class ElasticsearchLogger
{
private readonly ElasticClient _client;
public async Task LogAsync(LogEntry entry)
{
var document = new
{
timestamp = DateTime.UtcNow,
level = entry.Level,
message = entry.Message,
exception = entry.Exception?.ToString(),
properties = entry.Properties,
correlationId = entry.CorrelationId
};
await _client.IndexDocumentAsync(document);
}
}2. Grafana Dashboards
json
{
"dashboard": {
"title": "Application Metrics",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{path}}"
}
]
},
{
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m])",
"legendFormat": "Errors"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
}
]
}
}🧪 Testes de Observabilidade
1. Testes de Health Checks
csharp
[Test]
public async Task HealthCheck_WhenDatabaseIsHealthy_ReturnsHealthy()
{
var healthCheck = new DatabaseHealthCheck(_mockConnection.Object);
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
Assert.That(result.Status, Is.EqualTo(HealthStatus.Healthy));
}
[Test]
public async Task HealthCheck_WhenDatabaseIsUnhealthy_ReturnsUnhealthy()
{
_mockConnection.Setup(c => c.QueryAsync<int>(It.IsAny<string>(), null, null, null, null))
.ThrowsAsync(new Exception("Connection failed"));
var healthCheck = new DatabaseHealthCheck(_mockConnection.Object);
var result = await healthCheck.CheckHealthAsync(new HealthCheckContext());
Assert.That(result.Status, Is.EqualTo(HealthStatus.Unhealthy));
}2. Testes de Métricas
csharp
[Test]
public void OrderCreated_ShouldIncrementCounter()
{
var metrics = new MetricsService();
var initialValue = GetCounterValue("orders_created_total");
metrics.IncrementOrderCreated("premium");
var finalValue = GetCounterValue("orders_created_total");
Assert.That(finalValue, Is.GreaterThan(initialValue));
}📋 Checklist de Implementação
✅ Logging
- [ ] Configurar logging estruturado (Serilog/Log4Net)
- [ ] Implementar correlation IDs
- [ ] Configurar diferentes níveis de log por ambiente
- [ ] Implementar log rotation e retention policies
✅ Métricas
- [ ] Configurar Prometheus ou similar
- [ ] Implementar métricas de negócio
- [ ] Configurar dashboards no Grafana
- [ ] Implementar health checks
✅ Tracing
- [ ] Configurar Jaeger ou Zipkin
- [ ] Implementar custom spans
- [ ] Configurar sampling strategies
- [ ] Implementar correlation entre serviços
✅ Alerting
- [ ] Configurar alertas para métricas críticas
- [ ] Implementar notificações (Slack, Email)
- [ ] Configurar escalação de alertas
- [ ] Implementar runbooks para incidentes
✅ Performance
- [ ] Monitorar response times
- [ ] Configurar alertas de performance
- [ ] Implementar profiling automático
- [ ] Monitorar uso de recursos
🎯 Melhores Práticas
1. Logging
- Use logging estruturado sempre
- Inclua correlation IDs em todos os logs
- Configure níveis apropriados por ambiente
- Implemente log aggregation centralizada
2. Métricas
- Foque em métricas de negócio
- Use histogramas para latência
- Configure alertas baseados em SLOs
- Mantenha dashboards simples e focados
3. Tracing
- Use sampling para controlar overhead
- Implemente spans customizados para operações importantes
- Correlacione traces entre serviços
- Configure retention policies apropriadas
4. Alerting
- Configure alertas baseados em sintomas, não causas
- Use runbooks para responder a alertas
- Implemente escalação automática
- Revise e ajuste alertas regularmente