| name | observability |
| description | Guia para observabilidade — logging estruturado, métricas, tracing distribuído, health checks e instrumentação. Use quando a tarefa envolver logging, métricas, OpenTelemetry, tracing, health checks ou monitoramento. |
Observability — Logging, Métricas e Tracing
Quando Usar Esta Skill
Use quando a tarefa envolver:
- Logging estruturado, métricas, tracing distribuído
- OpenTelemetry, Serilog, health checks
- Instrumentação, monitoramento, diagnóstico
- Palavras-chave: "logging", "métricas", "tracing", "observabilidade", "OpenTelemetry", "health check", "monitorar"
1. Os Três Pilares da Observabilidade
1. Logs
Eventos discretos com contexto (timestamp, nível, mensagem, dados estruturados)
2. Métricas
Agregações numéricas ao longo do tempo (counters, gauges, histograms)
3. Traces
Rastreamento de requisições através de sistemas distribuídos
2. Logging Estruturado
ILogger (Built-in .NET)
public class CreateUserUseCase(
IUserRepository repository,
ILogger<CreateUserUseCase> logger) : ICreateUserUseCase
{
public async Task<CreateUserResponseModel> ExecuteAsync(CreateUserInput input, CancellationToken ct = default)
{
logger.LogInformation("Creating user with email {Email}", input.Email);
try
{
if (await repository.ExistsAsync(input.Email, ct))
{
logger.LogWarning("Attempt to create user with existing email {Email}", input.Email);
throw new InvalidOperationException("Email já está em uso.");
}
var user = await repository.CreateAsync(input, ct);
logger.LogInformation("User created successfully with ID {UserId}", user.Id);
return CreateUserPresenter.Present(user);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create user with email {Email}", input.Email);
throw;
}
}
}
LoggerMessage Source Generators (.NET 6+)
Alta performance — evita boxing e alocações:
public static partial class Log
{
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Creating user with email {Email}")]
public static partial void CreatingUser(ILogger logger, string email);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Information,
Message = "User created successfully with ID {UserId}")]
public static partial void UserCreated(ILogger logger, Guid userId);
[LoggerMessage(
EventId = 1003,
Level = LogLevel.Warning,
Message = "Attempt to create user with existing email {Email}")]
public static partial void EmailAlreadyExists(ILogger logger, string email);
[LoggerMessage(
EventId = 1004,
Level = LogLevel.Error,
Message = "Failed to create user with email {Email}")]
public static partial void UserCreationFailed(ILogger logger, string email, Exception exception);
}
public class CreateUserUseCase(IUserRepository repository, ILogger<CreateUserUseCase> logger)
{
public async Task<CreateUserResponseModel> ExecuteAsync(CreateUserInput input, CancellationToken ct = default)
{
Log.CreatingUser(logger, input.Email);
try
{
if (await repository.ExistsAsync(input.Email, ct))
{
Log.EmailAlreadyExists(logger, input.Email);
throw new InvalidOperationException("Email já está em uso.");
}
var user = await repository.CreateAsync(input, ct);
Log.UserCreated(logger, user.Id);
return CreateUserPresenter.Present(user);
}
catch (Exception ex)
{
Log.UserCreationFailed(logger, input.Email, ex);
throw;
}
}
}
Serilog (Opcional — Logging Avançado)
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Process
dotnet add package Serilog.Enrichers.Thread
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithThreadId()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.WriteTo.File(
path: "logs/log-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
.CreateLogger();
try
{
Log.Information("Starting application");
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Logging com Contexto
using Microsoft.Extensions.Logging;
using (logger.BeginScope(new Dictionary<string, object>
{
["UserId"] = userId,
["TenantId"] = tenantId,
["RequestId"] = requestId
}))
{
logger.LogInformation("Processing order");
}
using (LogContext.PushProperty("UserId", userId))
using (LogContext.PushProperty("TenantId", tenantId))
{
logger.LogInformation("Processing order");
}
3. Métricas — System.Diagnostics.Metrics (.NET 8+)
Definir Métricas
using System.Diagnostics.Metrics;
public class OrderMetrics
{
private readonly Counter<int> _ordersProcessed;
private readonly Histogram<double> _orderProcessingTime;
private readonly ObservableGauge<int> _activeOrders;
private int _activeOrderCount;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("Orders");
_ordersProcessed = meter.CreateCounter<int>(
name: "orders.processed",
unit: "orders",
description: "Total number of orders processed");
_orderProcessingTime = meter.CreateHistogram<double>(
name: "orders.processing_time",
unit: "ms",
description: "Order processing time in milliseconds");
_activeOrders = meter.CreateObservableGauge<int>(
name: "orders.active",
observeValue: () => _activeOrderCount,
unit: "orders",
description: "Current number of active orders");
}
public void RecordOrderProcessed() => _ordersProcessed.Add(1);
public void RecordProcessingTime(double milliseconds) => _orderProcessingTime.Record(milliseconds);
public void IncrementActiveOrders() => Interlocked.Increment(ref _activeOrderCount);
public void DecrementActiveOrders() => Interlocked.Decrement(ref _activeOrderCount);
}
Uso em UseCases
public class CreateOrderUseCase(
IOrderRepository repository,
OrderMetrics metrics,
ILogger<CreateOrderUseCase> logger)
{
public async Task<CreateOrderResponseModel> ExecuteAsync(CreateOrderInput input, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
metrics.IncrementActiveOrders();
try
{
logger.LogInformation("Creating order for user {UserId}", input.UserId);
var order = await repository.CreateAsync(input, ct);
metrics.RecordOrderProcessed();
logger.LogInformation("Order created successfully with ID {OrderId}", order.Id);
return CreateOrderPresenter.Present(order);
}
finally
{
sw.Stop();
metrics.RecordProcessingTime(sw.Elapsed.TotalMilliseconds);
metrics.DecrementActiveOrders();
}
}
}
Registro no DI
builder.Services.AddSingleton<OrderMetrics>();
Exportar Métricas (Prometheus)
dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore
using OpenTelemetry.Metrics;
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddMeter("Orders")
.AddMeter("Users")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddPrometheusExporter());
var app = builder.Build();
app.MapPrometheusScrapingEndpoint();
app.Run();
4. Tracing Distribuído — OpenTelemetry (.NET 7+)
Instalação
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient
Configuração
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: "MyApp",
serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(options =>
{
options.SetDbStatementForText = true;
options.RecordException = true;
})
.AddSource("MyApp.*")
.AddConsoleExporter()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
}));
Instrumentação Customizada
using System.Diagnostics;
public class OrderService
{
private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
public async Task<Order> ProcessOrderAsync(Guid orderId)
{
using var activity = ActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
try
{
await ValidateOrderAsync(orderId);
await CalculateTotalAsync(orderId);
await SaveOrderAsync(orderId);
activity?.SetTag("order.status", "completed");
return new Order { Id = orderId };
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
private async Task ValidateOrderAsync(Guid orderId)
{
using var activity = ActivitySource.StartActivity("ValidateOrder");
activity?.SetTag("order.id", orderId);
await Task.Delay(10);
}
private async Task CalculateTotalAsync(Guid orderId)
{
using var activity = ActivitySource.StartActivity("CalculateTotal");
activity?.SetTag("order.id", orderId);
await Task.Delay(20);
}
private async Task SaveOrderAsync(Guid orderId)
{
using var activity = ActivitySource.StartActivity("SaveOrder");
activity?.SetTag("order.id", orderId);
await Task.Delay(30);
}
}
Propagação de Contexto
var activity = Activity.Current;
var traceId = activity?.TraceId.ToString();
var spanId = activity?.SpanId.ToString();
logger.LogInformation("Processing order {OrderId} [TraceId: {TraceId}, SpanId: {SpanId}]",
orderId, traceId, spanId);
5. Health Checks
Configuração Básica
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddDbContextCheck<AppDbContext>()
.AddUrlGroup(new Uri("https://api.external.com/health"), name: "external-api")
.AddCheck<RedisHealthCheck>("redis")
.AddCheck<QueueHealthCheck>("message-queue");
var app = builder.Build();
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds
}),
totalDuration = report.TotalDuration.TotalMilliseconds
});
await context.Response.WriteAsync(result);
}
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
Health Check Customizado
using Microsoft.Extensions.Diagnostics.HealthChecks;
public class DatabaseHealthCheck(AppDbContext context) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
await context.Database.CanConnectAsync(cancellationToken);
return HealthCheckResult.Healthy("Database is reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Database is unreachable", ex);
}
}
}
public class RedisHealthCheck(IConnectionMultiplexer redis) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var db = redis.GetDatabase();
await db.PingAsync();
return HealthCheckResult.Healthy("Redis is reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Redis is unreachable", ex);
}
}
}
builder.Services.AddHealthChecks()
.AddCheck<DatabaseHealthCheck>("database", tags: ["ready"])
.AddCheck<RedisHealthCheck>("redis", tags: ["ready"]);
6. Correlação de Logs, Métricas e Traces
Middleware de Correlação
public class CorrelationMiddleware(RequestDelegate next, ILogger<CorrelationMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers.Append("X-Correlation-ID", correlationId);
Activity.Current?.SetTag("correlation.id", correlationId);
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
await next(context);
}
}
}
app.UseMiddleware<CorrelationMiddleware>();
7. Configuração para Diferentes Ambientes
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"OpenTelemetry": {
"Tracing": {
"Exporter": "Console"
},
"Metrics": {
"Exporter": "Console"
}
}
}
appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"OpenTelemetry": {
"Tracing": {
"Exporter": "OTLP",
"Endpoint": "http://collector:4317"
},
"Metrics": {
"Exporter": "Prometheus"
}
}
}
8. Boas Práticas
Logging
- ✅ Sempre usar logging estruturado (placeholders
{Property}, não string interpolation)
- ✅ Sempre incluir contexto relevante (IDs, tenant, user)
- ✅ Sempre usar níveis apropriados (Trace, Debug, Information, Warning, Error, Critical)
- ✅ Sempre logar exceções com
logger.LogError(ex, "message")
- ✅ Usar LoggerMessage Source Generators para performance crítica
- ❌ Evitar logar dados sensíveis (senhas, tokens, PII)
- ❌ Evitar logging excessivo (impacta performance e custo)
Métricas
- ✅ Sempre usar unidades consistentes (ms, bytes, requests)
- ✅ Sempre incluir tags relevantes (endpoint, status, tenant)
- ✅ Usar Counter para contadores crescentes (total de requests)
- ✅ Usar Histogram para distribuições (latências, tamanhos)
- ✅ Usar Gauge para valores instantâneos (conexões ativas, memória)
- ❌ Evitar cardinalidade alta em tags (não usar IDs únicos)
Tracing
- ✅ Sempre propagar contexto de trace entre serviços
- ✅ Sempre adicionar tags relevantes aos spans
- ✅ Sempre gravar exceções em spans (
activity.RecordException(ex))
- ✅ Criar spans customizados para operações importantes
- ❌ Evitar spans excessivamente granulares (impacta performance)
Health Checks
- ✅ Sempre implementar
/health/live (liveness) e /health/ready (readiness)
- ✅ Sempre verificar dependências críticas (DB, cache, APIs externas)
- ✅ Usar timeouts curtos para health checks (1-2 segundos)
- ❌ Evitar lógica complexa em health checks
Resumo
Esta skill cobre:
- ✅ Logging estruturado (ILogger, LoggerMessage Source Generators, Serilog)
- ✅ Métricas (System.Diagnostics.Metrics, Prometheus)
- ✅ Tracing distribuído (OpenTelemetry, Activity, custom spans)
- ✅ Health checks (liveness, readiness, custom checks)
- ✅ Correlação de logs, métricas e traces
- ✅ Boas práticas de observabilidade
Sempre que trabalhar com logging, métricas, tracing, OpenTelemetry ou health checks, use esta skill como referência.