MÓDULO 05 · CONCEITO 08 DE 14

Observabilidade como aspect

OpenTelemetry, traces, spans, baggage, metrics. Por que instrumentação automática cobre 80% e os 20% que importam exigem aspect manual — e por que cardinalidade alta é a fonte mais cara de sustos em runtime.

Tempo de leitura ~22 min Pré-requisito Conceito 07 (logging estruturado) Próximo Retry, timeout e circuit breaker como policies

Em maio de 2019, dois projetos da Cloud Native Computing Foundation anunciaram fusão. OpenTracing, criado em 2016 por Ben Sigelman (Google, depois LightStep) com base no paper Dapper de 2010, oferecia API neutra para tracing distribuído. OpenCensus, lançado pelo Google em 2018, oferecia API neutra para métricas e tracing juntos. Os dois competiam pelo mesmo espaço, e a fragmentação era custosa. A fusão produziu OpenTelemetry — usualmente abreviado como OTel —, que em 2026 é o padrão de facto para instrumentação em qualquer linguagem moderna.

Observabilidade como disciplina nasceu antes da fusão. O termo veio da teoria de controle (Rudolf Kálmán, 1960), mas foi Charity Majors, Liz Fong-Jones e Cindy Sridharan que o popularizaram em engenharia de software por volta de 2018, argumentando que monitoring tradicional (alertar em métricas pré-definidas) era insuficiente para sistemas complexos. A disciplina exige três pilares: logs (eventos discretos textuais — conceito 07), metrics (valores agregados ao longo do tempo) e traces (cadeias causais de operações em um request distribuído). Os três se reforçam quando correlacionados, e ficam meio-úteis isolados.

Observabilidade é o cross-cutting concern mais transversal de todos. Toda operação de domínio, todo middleware, todo cliente externo, todo job — todos podem ser instrumentados. Tentar adicionar essa instrumentação à mão em cada lugar é, no melhor caso, ruído gigantesco no código de domínio; no pior, esquecimento de pontos críticos. A solução madura é tratar observabilidade como aspect: middleware HTTP gera span automaticamente, cliente HTTP propaga trace context, auto-instrumentação cobre frameworks comuns, e o desenvolvedor adiciona spans manuais apenas onde precisa granularidade extra.

Este conceito articula o vocabulário OTel mínimo, distingue o que vem de auto-instrumentação do que precisa ser explícito, e mostra a forma idiomática em três linguagens. Os tópicos de coleta, backend e visualização (Jaeger, Tempo, Prometheus, Grafana) ficam para o módulo 10 — aqui o foco é instrumentar, não operar.

O vocabulário OTel — span, trace, baggage

Um span é a unidade básica de tracing — uma operação delimitada no tempo, com início, fim, atributos, e pai opcional. Um span pode ser uma request HTTP, uma query SQL, uma chamada de método de domínio, um job. Cada span tem ID único (span_id), e um conjunto de spans que compartilham origem comum forma um trace, identificado por trace_id.

A árvore de spans dentro de um trace mostra a cadeia causal: span "POST /pedidos" tem como filho o span "validate", que tem como filho "load customer from db", "save order to db", "publish event". Em um sistema de microsserviços, o trace continua entre serviços — o cliente HTTP propaga trace_id e span_id via cabeçalho W3C traceparent (RFC do W3C, 2020), e o servidor receptor anexa novos spans à mesma árvore. O resultado é uma visão hierárquica de toda a operação, atravessando processos.

Atributos são pares chave/valor anexados a span. http.method=POST, http.status_code=201, db.system=postgresql, customer.id=.... Atributos com nomes padrão seguem as semantic conventions da OTel — vocabulário compartilhado que permite análises consistentes entre serviços e ferramentas. Atributos custom aparecem ao lado sem cerimônia.

Baggage é dado contextual propagado com o trace, mas distinto dos atributos do span. Útil para fluxo de informação que precisa atravessar serviços sem virar argumento de cada chamada — tenant_id, feature_flag_variant, experiment_arm. Baggage tem cuidado especial: o que está em baggage costuma ser exposto a todos os serviços a jusante, então não deve conter dado sensível.

Um span event é um marcador no tempo dentro de um span — algo que aconteceu mas não vale span próprio. Útil para registrar transições de estado curtas ("cache hit", "validation failed", "retry attempt") sem poluir a árvore.

Os três pilares — quando usar cada um

A confusão recorrente é "log, métrica e trace registram a mesma coisa de jeito diferente". Não. Cada um responde a uma pergunta distinta, e tentar usar um para o trabalho do outro sai caro.

Log responde: "o que aconteceu neste evento específico?". É discreto, textual, com contexto. Bom para diagnóstico de incidente em casos individuais. Mau para resposta agregada ("quantos requests falharam na última hora?") — você consegue, mas é caro.

Métrica responde: "como está se comportando o sistema agregado?". É numérica, pré-agregada, cardinalidade controlada. Boa para alertas, dashboards, SLOs. Má para diagnóstico ponto-a-ponto — por construção, você perdeu o contexto individual.

Trace responde: "como esta request específica atravessou o sistema?". É estruturado, hierárquico, conecta spans entre serviços. Bom para entender latência e cadeia de causa em sistemas distribuídos. Mau para alertas em valores agregados — você não pode dizer "alerte se a latência média passar de X" só com traces.

Sistemas observáveis usam os três conectados: logs carregam trace_id para que você navegue de log a trace; métricas têm exemplars que apontam para um trace específico que produziu aquele valor; traces têm atributos que viram filtros em métricas. OpenTelemetry foi desenhado para esse modelo unificado — daí o nome "Telemetry", em singular.

Auto-instrumentação — o caso 80%

A maior parte da instrumentação que um sistema precisa não precisa ser escrita manualmente. Frameworks comuns — ASP.NET Core, FastAPI, Express, Spring, gRPC, net/http, requests, httpx, aiohttp, EF Core, SQLAlchemy, JDBC, database/sql — têm bibliotecas de auto-instrumentação OTel que criam spans automaticamente para cada operação relevante.

Em .NET, basta adicionar o nuget OpenTelemetry.Instrumentation.AspNetCore e registrar — todo request HTTP vira span automaticamente. Em Python, opentelemetry-instrumentation-fastapi faz o mesmo. Em Go, a comunidade prefere instrumentar manualmente via SDK porque é mais explícito (e Go não tem mecanismo runtime para "monkey-patch" como Python). Em Java, o Java agent de OTel auto-instrumenta dezenas de bibliotecas só com a flag -javaagent:opentelemetry-javaagent.jar — sem mudar uma linha de código.

// .NET 10 — registro de auto-instrumentação OTel
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("catalog-api"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()        // HTTP server spans
        .AddHttpClientInstrumentation()        // HTTP client spans
        .AddSqlClientInstrumentation()         // EF Core / SqlClient spans
        .AddOtlpExporter())                    // exporta para coletor
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()           // GC, threads, working set
        .AddOtlpExporter());

Esse setup mínimo já cobre 80% do que um sistema típico precisa observar. Cada request HTTP entra com span, cada query SQL gera span filho, cada chamada externa via HttpClient também. Se o sistema downstream também tem OTel, o trace continua atravessando processos.

Instrumentação manual — os 20% que importam

Auto-instrumentação cobre o transporte. Não cobre o domínio. O span "POST /pedidos" gerado automaticamente diz "essa request durou 47ms"; o que ela fez, qual operação de domínio executou, quantos itens, qual cliente — só aspect manual conta. A regra de produção é: o span do framework é a raiz, e dentro dele você adiciona spans manuais para operações de domínio significativas.

// Python — span manual em service de domínio
from opentelemetry import trace

tracer = trace.get_tracer(__name__)

class PedidoService:
    async def criar(self, cmd: CriarPedidoCmd) -> Pedido:
        with tracer.start_as_current_span("pedido.criar") as span:
            span.set_attribute("cliente.id", cmd.cliente_id)
            span.set_attribute("pedido.item_count", len(cmd.itens))

            try:
                pedido = await self._repo.salvar(Pedido.from_cmd(cmd))
            except Exception as e:
                span.record_exception(e)
                span.set_status(trace.Status(trace.StatusCode.ERROR))
                raise

            span.set_attribute("pedido.id", str(pedido.id))
            span.add_event("pedido.persisted", {
                "pedido.total": float(pedido.total),
            })
            return pedido

Note três detalhes idiomáticos. Primeiro: start_as_current_span faz o span virar "current" — qualquer span criado depois (auto ou manual) vira filho dele. Segundo: atributos vão na operação em si (cliente.id, pedido.id); events ficam para transições no meio da operação. Terceiro: record_exception + set_status(ERROR) é o padrão para marcar span como falho — só lançar a exceção sem isso deixa o span "ok" no UI.

Métricas — os quatro tipos canônicos

OpenTelemetry define quatro tipos principais de instrumento de métrica. A escolha entre eles muda significativamente a qualidade do que se consegue medir.

Counter — valor que só sobe (ou reseta). Bom para "quantos pedidos foram criados", "quantos requests retornaram 5xx". Você nunca decrementa um counter; backend calcula taxa derivada (req/s) automaticamente.

UpDownCounter — valor que sobe e desce. Bom para "quantas conexões abertas no pool agora", "quantos mensagens na fila". Diferente de gauge porque você incrementa e decrementa, não seta valor absoluto.

Histogram — distribuição de valores. Bom para "duração de request", "tamanho de payload", "tempo entre eventos". Backend agrega em buckets e calcula percentis (P50, P95, P99) — esse é o tipo de métrica que sustenta SLO.

Gauge / Observable — valor instantâneo, observado periodicamente. Bom para "uso de CPU", "memória heap", "número de threads". Diferente de UpDownCounter porque você não rastreia mudanças — só consulta o valor atual quando o backend pergunta.

// Go — definir e usar métricas com OTel
import "go.opentelemetry.io/otel/metric"

var (
    pedidosCriados metric.Int64Counter
    pedidoDuration metric.Float64Histogram
    filaDepth      metric.Int64UpDownCounter
)

func initMetrics(meter metric.Meter) error {
    var err error
    pedidosCriados, err = meter.Int64Counter("pedidos.criados",
        metric.WithDescription("Total de pedidos criados"),
        metric.WithUnit("{pedido}"))
    if err != nil { return err }

    pedidoDuration, err = meter.Float64Histogram("pedido.duration",
        metric.WithDescription("Tempo para criar pedido"),
        metric.WithUnit("ms"))
    return err
}

func CriarPedido(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
    started := time.Now()
    p, err := repo.Salvar(ctx, Pedido{...})
    pedidoDuration.Record(ctx, float64(time.Since(started).Milliseconds()),
        metric.WithAttributes(
            attribute.String("status", statusOf(err)),
        ))
    if err == nil {
        pedidosCriados.Add(ctx, 1, metric.WithAttributes(
            attribute.String("tipo", string(p.Tipo)),
        ))
    }
    return p, err
}

Cardinalidade — onde está o tropeço caro

A armadilha mais cara em métricas é cardinalidade alta. Cada combinação única de atributos vira uma série temporal separada no backend. Adicionar atributo customer.id a uma métrica em um sistema com 10 milhões de clientes ativos pode transformar uma série única em 10 milhões de séries — Prometheus, Datadog, qualquer backend que cobra por série temporal vai gerar fatura assustadora.

A regra prática: atributos de métrica devem ter cardinalidade baixa. http.method tem cardinalidade ~5. http.status_code tem ~50. environment tem 3. pedido.tipo tem 4. Esses são bons candidatos. customer.id, request_id, email, url.path com IDs em path — todos esses são alta cardinalidade e não devem virar atributo de métrica.

Onde colocar dado de alta cardinalidade? Em traces, com naturalidade — cada span já é único, e atributos de span não criam séries temporais. Quando você precisa correlacionar métrica agregada com request específica, o mecanismo é exemplar — uma métrica histogram pode ter exemplars que apontam para trace_id de uma request que caiu naquele bucket. Esse é o pilar onde alta cardinalidade é cidadã de primeira classe.

armadilha em produção

"Cardinality explosion" — fatura de Datadog/Prometheus triplicando do dia para a noite. A causa quase sempre é a mesma: alguém adicionou um atributo de alta cardinalidade a uma métrica importante. customer.id em http.server.duration, ou request.id em orders.created. Diagnóstico: o backend mostra "active series" subindo em ordem de magnitude. Correção: remover o atributo da métrica e — se a informação é necessária — colocar em trace ou em log estruturado, não em métrica. Lição: revisão de código de qualquer adição de atributo a métrica precisa perguntar "qual é a cardinalidade desse valor em produção?".

Propagação de contexto entre serviços

Em sistemas distribuídos, o trace só tem valor se atravessa os processos. A spec do W3C "Trace Context" (REC, fevereiro de 2020) define o cabeçalho HTTP traceparent que carrega trace_id e span_id entre serviços, e o cabeçalho tracestate para vendor-specific data. OpenTelemetry SDK em qualquer linguagem respeita esses cabeçalhos automaticamente — o cliente HTTP injeta na ida, o servidor extrai na chegada.

Para sistemas que usam fila (Kafka, RabbitMQ, SQS), a propagação acontece via headers/atributos da mensagem. OTel tem instrumentações para os principais brokers. O detalhe importante: produtores de mensagem precisam injetar contexto explicitamente; consumidores precisam extrair antes de processar. Sem isso, o trace para na fila — você tem visão até o produtor, depois "pula" para um trace novo no consumidor, sem conexão.

gRPC propaga trace context nativamente via metadata. Bibliotecas como Apollo Federation (GraphQL) também. A regra geral: se o transporte tem mecanismo de header, OTel sabe propagar; se não tem (eventos custom em PubSub proprietário), você precisa configurar manualmente.

O mesmo span manual, três SDKs

Para fixar a equivalência, considere o cenário canônico: adicionar um span manual a uma operação de domínio com atributos específicos e marcação de erro.

C# — System.Diagnostics.ActivitySource
// .NET usa ActivitySource (do System.Diagnostics) que OTel reconhece
private static readonly ActivitySource ActivitySource = new("Catalog.Pedidos");

public async Task<Pedido> Criar(CriarPedidoCmd cmd, CancellationToken ct)
{
    using var activity = ActivitySource.StartActivity("pedido.criar");
    activity?.SetTag("cliente.id", cmd.ClienteId);
    activity?.SetTag("pedido.item_count", cmd.Itens.Count);

    try
    {
        var p = await _repo.SalvarAsync(new Pedido(cmd), ct);
        activity?.SetTag("pedido.id", p.Id);
        activity?.AddEvent(new ActivityEvent("pedido.persisted",
            tags: new ActivityTagsCollection { ["pedido.total"] = p.Total }));
        return p;
    }
    catch (Exception ex)
    {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        throw;
    }
}

.NET tem ActivitySource nativo na stdlib desde 5.0; OTel SDK Activities sem precisar de API paralela. Resultado: instrumentação que sobrevive mesmo se OTel sair (Activities continuam funcionando para diagnóstico).

Python — opentelemetry-api
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer(__name__)

class PedidoService:
    async def criar(self, cmd: CriarPedidoCmd) -> Pedido:
        with tracer.start_as_current_span("pedido.criar") as span:
            span.set_attribute("cliente.id", cmd.cliente_id)
            span.set_attribute("pedido.item_count", len(cmd.itens))
            try:
                p = await self._repo.salvar(Pedido.from_cmd(cmd))
            except Exception as e:
                span.record_exception(e)
                span.set_status(Status(StatusCode.ERROR, str(e)))
                raise
            span.set_attribute("pedido.id", str(p.id))
            span.add_event("pedido.persisted", {"pedido.total": float(p.total)})
            return p

Python OTel usa context manager (with) para escopo de span. start_as_current_span torna o span "current" — qualquer span criado depois vira filho.

Go — go.opentelemetry.io/otel
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
)

var tracer = otel.Tracer("catalog/pedidos")

func CriarPedido(ctx context.Context, cmd CriarPedidoCmd) (*Pedido, error) {
    ctx, span := tracer.Start(ctx, "pedido.criar")
    defer span.End()

    span.SetAttributes(
        attribute.String("cliente.id", cmd.ClienteID),
        attribute.Int("pedido.item_count", len(cmd.Itens)),
    )

    p, err := repo.Salvar(ctx, Pedido{ClienteID: cmd.ClienteID, Itens: cmd.Itens})
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, err
    }

    span.SetAttributes(attribute.String("pedido.id", p.ID))
    span.AddEvent("pedido.persisted",
        trace.WithAttributes(attribute.Float64("pedido.total", p.Total)))
    return p, nil
}

Go é o mais explícito: span é retornado, defer span.End() garante encerramento. Context entra e sai modificado — o trace_id flui pelo context.

Sampling — não tudo precisa virar trace

Em sistemas de tráfego alto, traçar 100% das requests é proibitivo — backend de tracing fica caro, performance da aplicação degrada (mesmo sendo overhead pequeno por trace, milhões de traces somam). A solução é sampling: decidir quais traces gravar e quais descartar.

Head-based sampling — decisão tomada no início do trace (no primeiro serviço). Se "não trace", todo o trace é descartado, em todos os serviços a jusante. Simples de implementar, propaga via flag em traceparent, mas toma decisão sem saber o resultado da request — se a request falhou, talvez você quisesse o trace.

Tail-based sampling — decisão tomada no fim do trace, geralmente no coletor (OTel Collector tem processor de tail sampling). Mais inteligente — decide com base em latência, status, atributos —, mas exige buffer de todo trace no coletor por alguns segundos antes de decidir. Mais caro de operar.

Probabilistic sampling — fixar um percentual (1%, 10%, 100%) e amostrar uniformemente. É o default em produção: 1–10% costuma ser suficiente para diagnóstico agregado. Soma com regra "100% se erro" via tail sampling em sistemas críticos.

Anti-padrões frequentes

Span explosion. Criar span para cada iteração de loop de mil elementos: você ganha mil spans filhos, todos quase idênticos, ruído enorme. Use span event ou métrica counter no lugar.

Atributo de alta cardinalidade em métrica. Já mencionado, mas vale repetir — é a fonte mais comum de surpresa de fatura em backend de telemetria. Alta cardinalidade vai em trace, não em métrica.

Trace que para na fila. Produtor publica mensagem sem injetar contexto, consumidor não extrai. Fix: sempre injetar contexto em mensagens, sempre extrair antes de processar. Bibliotecas modernas de OTel já fazem isso para Kafka/RabbitMQ/SQS.

Esquecer span.End() em Go. Sem defer span.End(), o span fica "aberto" e nunca é enviado para o coletor — você perde a operação no UI. Em Python/.NET o context manager / using cuida; em Go é manual.

Marcar erro só pela exception, sem set_status. Em Python e Go, record_exception/RecordError sozinho não marca o span como erro — tem que chamar set_status(ERROR). Em .NET, SetStatus também é necessário. Sem isso, o UI mostra o span "ok" mesmo tendo gravado a exceção.

heurística do sênior

Antes de adicionar instrumentação manual, pergunte: "essa operação merece span próprio porque tem latência variável e causa relevante para o negócio?". Operações curtas e deterministas raramente merecem (ruído). Operações que chamam externos, transacionam, ou consultam dados quase sempre merecem. Para tudo que está entre, span event ou métrica costuma ser melhor escolha. Sênior não instrumenta tudo — instrumenta o que vai ser olhado em incidente.

Por que importa para a sua carreira

Em 2026, instrumentação OTel não é mais "bom ter" — é expectativa baseline em qualquer sistema sério. Sêniores que entregam sistemas com instrumentação madura (auto-instrumentação configurada, spans manuais nas operações de domínio, atributos com cardinalidade controlada, propagação cross-service funcionando) entregam autonomia operacional ao time. Em entrevista de design, "como você instrumentaria esse sistema?" é pergunta direta para vagas backend pleno-sênior, e a resposta forte cita os três pilares, distingue auto de manual, menciona cardinalidade como restrição, e fala de propagação W3C. Em revisão de código, perguntar "qual a cardinalidade desse atributo?" antes de aprovar adição a métrica é serviço prestado ao time inteiro — uma fatura economizada vale meses de salário.

Como praticar

  1. Stack OTel local end-to-end. Suba localmente (docker-compose) Jaeger + Prometheus + Grafana + OTel Collector. Em uma das três linguagens, monte uma API com 2-3 endpoints e instrumente: auto-instrumentação para HTTP/SQL, spans manuais para operações de domínio, métricas counter e histogram para criação de pedido. Execute requests e navegue Jaeger → trace → exemplar → métrica → de volta a trace. Esse exercício faz você ver os três pilares conectados de fato.
  2. Trace que atravessa fila. Adicione ao sistema acima um produtor e um consumidor de fila (Redis pub/sub, RabbitMQ, ou Kafka local). Garanta que o trace iniciado em request HTTP atravesse a fila para o consumidor — verificando no Jaeger que aparece como árvore única, não dois traces independentes. Documente o que precisou configurar em cada lado para a propagação funcionar.
  3. Cardinality audit. Pegue um sistema seu instrumentado (ou um aberto que você usa) e liste todas as métricas com seus atributos. Para cada atributo, estime a cardinalidade em produção. Identifique pelo menos um atributo com cardinalidade explosiva e proponha a correção — geralmente removê-lo da métrica e movê-lo para spans. Esse é o tipo de revisão que evita surpresa de fatura.

Referências para aprofundar

  1. paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Benjamin Sigelman et al., Google (2010). research.google/pubs/pub36356 — O paper fundador. Sigelman descreve a infraestrutura interna do Google que inspirou OpenTracing, OpenCensus e (depois) OpenTelemetry.
  2. docs OpenTelemetry — The Documentation. opentelemetry.io/docs — Documentação canônica e atualizada constantemente. Estrutura é "Concepts → Languages → Instrumentation". Vale começar pelos Concepts.
  3. docs OpenTelemetry Semantic Conventions. opentelemetry.io/docs/specs/semconv — As convenções de nomenclatura de atributos. Indispensável para escrever instrumentação que conversa com qualquer backend.
  4. docs W3C Trace Context (Recommendation, 2020). w3.org/TR/trace-context — O padrão para propagação. Define traceparent e tracestate com sintaxe e semântica precisas.
  5. livro Observability Engineering — Charity Majors, Liz Fong-Jones, George Miranda (O'Reilly, 2022). A bíblia da disciplina. Capítulos 6-9 cobrem distributed tracing, eventos com contexto, e SLOs com observabilidade. Maturidade rara em livro técnico.
  6. livro Mastering Distributed Tracing — Yuri Shkuro (Packt, 2019). Shkuro é criador de Jaeger e veio do time do paper Dapper na Uber. Cobre tracing com profundidade técnica que outros livros evitam.
  7. livro OpenTelemetry — Distributed Tracing in Practice — Reese Lee, Rob Ferguson (O'Reilly, 2024). O livro mais atualizado especificamente sobre OTel, cobrindo .NET, Python, Go, Java. Inclui setup de coletor e backends.
  8. livro Site Reliability Engineering — Betsy Beyer et al., Google (O'Reilly, 2016). Capítulos 5 e 6 cobrem monitoring e SLOs, tema que conecta diretamente com observabilidade. Gratuito online em sre.google/books.
  9. artigo Logs and Metrics and Graphs, Oh My — Cindy Sridharan (blog Medium, 2017). medium.com/@copyconstruct — Texto seminal sobre os três pilares e seus papéis distintos. Sridharan articulou a tese antes de virar consenso.
  10. artigo So You Want to Build an Observability Tool — Charity Majors (blog Honeycomb). honeycomb.io/blog/so-you-want-to-build-an-observability-tool — Charity escreve sobre cardinalidade alta como característica fundamental de observabilidade — muda a forma como você pensa em métricas vs traces.
  11. artigo Cardinality Explosion: A Real-World Problem — Tom Wilkie (Grafana Labs blog, 2019+). grafana.com/blog — Wilkie é cofundador da Grafana Labs. O texto é receituário prático sobre como evitar e diagnosticar explosões.
  12. vídeo OpenTelemetry: The Vision and the Reality — Ted Young, Morgan McLean (KubeCon, 2023). YouTube. Os mantenedores principais explicam o estado e as próximas etapas do projeto. Útil para entender direção em 2026.