Skip to content

Observabilidade e Monitoramento para .NET Senior

📋 Conceitos Fundamentais

Os Três Pilares da Observabilidade

  1. Logs: Registros estruturados de eventos
  2. Metrics: Medições quantitativas de performance
  3. 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