Arquitetura SaaS

18 de fevereiro de 2026 · 10 min de leitura

Observabilidade é um daqueles tópicos que não parece urgente — até o momento em que um cliente liga dizendo que algo travou e você não consegue nem confirmar se o problema aconteceu de verdade, quando foi exatamente, e se ainda está happening. Nesse momento, você percebe que logs soltos no console e métricas genéricas de infraestrutura não são observabilidade. São ruído. Em SaaS multi-tenant, o desafio é maior porque uma falha pode afetar um tenant específico, um subconjunto de tenants, ou todos — e sem as correlações certas nos logs, você não consegue distinguir. Este post cobre o que configurar antes do primeiro cliente em produção.

O que observabilidade significa na prática (sem o buzzword)

Observabilidade é a capacidade de responder perguntas sobre o sistema a partir dos dados que ele produz — sem precisar modificar o código para investigar um problema específico.

As três perguntas que você mais vai precisar responder em produção:

  • 'O que aconteceu exatamente às 14h37 para o tenant X?'
  • 'Essa lentidão está afetando todos os tenants ou só alguns?'
  • 'Qual endpoint está causando o aumento de latência desde o último deploy?'

Para responder essas três perguntas, você precisa de três coisas: logs estruturados com contexto de tenant, traces distribuídos correlacionando requests entre serviços, e métricas separadas por tenant. Não necessariamente os sistemas mais sofisticados do mercado — mas os três pilares precisam existir.

Logs estruturados com Serilog: o contexto que não pode faltar

Log estruturado significa que cada entrada de log é um objeto com campos definidos — não uma string de texto livre. A diferença entre os dois:

// Log de texto livre — praticamente inútil para busca em produção
[14:37:22] Erro ao processar pedido do tenant abc123: timeout na conexão com banco

// Log estruturado — filtrável, agregável, correlacionável
{
  "timestamp": "2026-05-28T14:37:22Z",
  "level": "Error",
  "message": "Timeout na conexão com banco",
  "tenantId": "3e4f5a6b-...",
  "requestId": "a1b2c3d4",
  "endpoint": "POST /api/pedidos",
  "duracao_ms": 30012,
  "exception": "SqlException: Connection timeout"
}

Configuração do Serilog no .NET Core para output em JSON com os campos certos:

// Program.cs
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.Seq(serverUrl: configuration["Seq:Url"]!) // ou CloudWatch, Datadog, etc.
    .CreateLogger();

builder.Host.UseSerilog();

O Enrich.FromLogContext() é o que permite injetar campos dinamicamente em todos os logs do request — incluindo tenantId e requestId. O middleware que faz essa injeção:

public class LogContextMiddleware
{
    private readonly RequestDelegate _next;

    public LogContextMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context, ITenantContext tenantContext)
    {
        var requestId = context.TraceIdentifier;

        using (LogContext.PushProperty("requestId", requestId))
        using (LogContext.PushProperty("tenantId", tenantContext.TenantId))
        using (LogContext.PushProperty("tenantPlan", tenantContext.Plan))
        using (LogContext.PushProperty("endpoint", $"{context.Request.Method} {context.Request.Path}"))
        {
            await _next(context);
        }
    }
}

Com esse middleware ativo, qualquer _logger.LogError("...") em qualquer parte da aplicação automaticamente inclui tenantId, requestId e endpoint na entrada de log. O desenvolvedor não precisa lembrar de passar esses campos — eles estão no contexto.

Isolamento de logs: o que um tenant não pode ver

Logs estruturados com tenantId resolvem a correlação — mas criam um risco se você expõe logs para operadores ou clientes: um log mal filtrado pode mostrar dados de outro tenant.

Três regras para manter o isolamento:

Nunca logue dados de negócio que não são seus para logar. Um log de erro pode incluir o tenantId e o ID do recurso que falhou. Não precisa incluir o nome do cliente, o valor da transação, ou qualquer dado sensível do payload. Se o payload inteiro for logado para debug, garanta que isso só aconteça em ambientes não-produção.

// Errado — loga payload inteiro em produção
_logger.LogDebug("Payload recebido: {Payload}", JsonSerializer.Serialize(request));

// Correto — loga apenas identificadores
_logger.LogDebug("Request recebido. TenantId={TenantId} RecursoId={RecursoId}",
    tenantId, request.RecursoId);

Separe alertas por tenant de alertas de plataforma. Um aumento de erros no tenant X não deve disparar o mesmo alerta que um aumento de erros na plataforma inteira. Com logs estruturados, você consegue criar queries separadas: tenantId = 'X' AND level = 'Error' vs level = 'Error' sem filtro de tenant.

Nunca exponha logs brutos para o cliente. Se você vai oferecer algum tipo de audit log ou histórico de atividade para o tenant, construa uma interface específica que filtra e formata os dados — nunca acesso direto ao sistema de logs.

Distributed tracing: seguindo um request de ponta a ponta

Quando o sistema tem mais de um serviço — API, Worker, serviço de e-mail, banco — um único request do usuário pode gerar operações em vários desses componentes. Sem tracing, você vê o log de erro no Worker mas não consegue correlacionar com o request HTTP que o originou.

O .NET tem suporte nativo a distributed tracing via System.Diagnostics.Activity. Para exportar esses traces para uma ferramenta de visualização, use OpenTelemetry:

// Program.cs
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.EnrichWithHttpRequest = (activity, request) =>
            {
                // Adiciona tenantId como atributo do trace
                var tenantId = request.Headers["x-tenant-id"].FirstOrDefault();
                if (tenantId != null)
                    activity.SetTag("tenant.id", tenantId);
            };
        })
        .AddEntityFrameworkCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(configuration["Otlp:Endpoint"]!);
        })
    );

Com isso, cada request gera um trace com spans para cada operação: a chamada HTTP, as queries no banco, as chamadas para serviços externos. O tenant.id como atributo do trace permite filtrar por tenant no Jaeger, Grafana Tempo, Honeycomb, ou qualquer ferramenta compatível com OpenTelemetry.

No contexto early-stage, não é necessário subir um sistema de tracing completo imediatamente. O que não pode faltar é o requestId sendo propagado entre serviços via header — isso já permite correlacionar logs manualmente enquanto o volume é baixo.

Métricas por tenant: quando o problema é de um cliente específico

Métricas de infraestrutura — CPU, memória, latência média — são úteis para detectar problemas globais. Elas não ajudam a detectar que um tenant específico está com performance degradada enquanto todos os outros estão normais.

Duas métricas por tenant que detectam a maioria dos problemas práticos:

Taxa de erro por tenant — se um tenant começa a ter taxa de erro acima de 1%, você quer saber antes do cliente ligar.

Latência de P95 por tenant — a latência média esconde outliers. P95 (o tempo de resposta abaixo do qual estão 95% das requisições) revela degradação que afeta uma minoria dos requests mas é consistente o suficiente para causar experiência ruim.

Com Prometheus e a biblioteca prometheus-net no .NET:

public class MetricasMiddleware
{
    private static readonly Counter _requestCounter = Metrics.CreateCounter(
        "http_requests_total",
        "Total de requests HTTP",
        new CounterConfiguration
        {
            LabelNames = new[] { "tenant_id", "metodo", "status_code" }
        });

    private static readonly Histogram _requestDuration = Metrics.CreateHistogram(
        "http_request_duration_seconds",
        "Duração dos requests HTTP",
        new HistogramConfiguration
        {
            LabelNames = new[] { "tenant_id", "endpoint" },
            Buckets = new[] { 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0 }
        });

    private readonly RequestDelegate _next;

    public MetricasMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context, ITenantContext tenantContext)
    {
        var sw = Stopwatch.StartNew();
        await _next(context);
        sw.Stop();

        var tenantId = tenantContext.TenantId.ToString();
        var endpoint = context.Request.Path.Value ?? "unknown";
        var statusCode = context.Response.StatusCode.ToString();

        _requestCounter.WithLabels(tenantId, context.Request.Method, statusCode).Inc();
        _requestDuration.WithLabels(tenantId, endpoint).Observe(sw.Elapsed.TotalSeconds);
    }
}

Um ponto de atenção: com muitos tenants, o número de séries temporais cresce proporcionalmente. Para plataformas com centenas de tenants, considere agrupar tenants por plano em vez de por ID individual — ou use uma ferramenta como Grafana com agregação dinâmica.

SLO por tenant: o que prometer e como medir

SLO (Service Level Objective) é o nível de serviço que você se compromete a entregar. Em SaaS B2B, isso normalmente aparece no contrato como SLA — mas antes de colocar no contrato, você precisa conseguir medir.

Um SLO simples e mensurável para começar:

  • Disponibilidade: 99,5% de uptime no mês (equivale a menos de 3h36min de downtime/mês)
  • Latência: P95 de responses abaixo de 1 segundo para endpoints críticos
  • Erro: taxa de erro abaixo de 0,5% dos requests

O importante é que esses números venham de dados reais — não de expectativa. Se você ainda não tem dados de produção, comece com SLOs conservadores e ajuste para cima conforme a estabilidade do sistema for comprovada.

Para calcular disponibilidade por tenant a partir dos logs:

# Query no Seq, Grafana Loki ou CloudWatch Insights
disponibilidade = (requests_com_sucesso / total_requests) * 100

# Filtrado por tenant
SELECT
  tenantId,
  COUNT(*) FILTER (WHERE statusCode < 500) * 100.0 / COUNT(*) AS disponibilidade_pct
FROM logs
WHERE timestamp > NOW() - INTERVAL '30 days'
GROUP BY tenantId
HAVING disponibilidade_pct < 99.5;

Rodar essa query semanalmente — ou ter um dashboard que exibe continuamente — é o mínimo para saber se você está cumprindo os SLOs antes que um cliente perceba que não está.

O stack mínimo que funciona sem custo proibitivo no early-stage

A tentação ao montar observabilidade é escolher o stack mais completo possível logo de início. Datadog, New Relic, Honeycomb — são ótimas ferramentas, mas com custo que não faz sentido quando você tem dez tenants.

Um stack que cobre os fundamentos com custo controlado:

Logs: Seq (self-hosted no Docker, gratuito para desenvolvimento e times pequenos) ou CloudWatch Logs (pago por ingestão, mas integrado nativamente se você já está na AWS). Ambos suportam queries estruturadas e alertas.

Traces: Jaeger self-hosted para começar. Quando o volume crescer e o custo de operação superar o custo de uma ferramenta gerenciada, migra para Grafana Tempo ou Honeycomb.

Métricas: Prometheus + Grafana. O Grafana Cloud tem um tier gratuito generoso para times pequenos. Para quem já está na AWS, CloudWatch Metrics com dashboards customizados também funciona bem.

Alertas: qualquer uma das opções acima suporta alertas por e-mail ou Slack. Defina alertas para pelo menos três situações: taxa de erro acima de 1% nos últimos 5 minutos, latência P95 acima de 2s, e DLQ com mensagens acumulando.

O que não pode faltar, independente de qual stack você escolher: tenantId em todos os logs, requestId propagado entre serviços, e pelo menos um dashboard que mostre saúde por tenant — não só saúde global da plataforma.


Se você quer montar a camada de observabilidade do seu SaaS antes que os problemas em produção apareçam sem você conseguir depurar — fala com a Logik Digital.

Conversar com a Logik Digital

Tags: observabilidade saas logs correlacionados tenant, Arquitetura SaaS, TOFU