Antes do OpenTelemetry, cada vendor de observabilidade (Datadog, New Relic, Dynatrace, Jaeger, Zipkin) tinha seu próprio SDK, seus próprios formatos e sua própria API de instrumentação. O resultado: ao instrumentar seu código com o SDK do Datadog, você criava um acoplamento profundo — migrar para outro vendor exigia reescrever toda a instrumentação. O OpenTelemetry (OTel) é um projeto CNCF nascido em 2019 da fusão do OpenCensus (Google) e OpenTracing (CNCF). Ele define uma API vendor-neutral para instrumentar código (traces, metrics, logs), um SDK que implementa a API com exportadores para qualquer backend, o protocolo OTLP para transporte de telemetria, o OTel Collector como intermediário de processamento e roteamento, e Semantic Conventions — atributos padronizados para HTTP, DB, mensageria, etc. O resultado: você instrumenta uma vez com a API OTel, e pode exportar para Jaeger, Tempo, Datadog, New Relic, Honeycomb, ou qualquer outro backend — sem alterar o código da aplicação.
API vs SDK: A Separação Fundamental
OTel API
A API é o contrato público — o conjunto de interfaces e tipos que bibliotecas e frameworks usam para instrumentar código. A API tem uma implementação no-op por padrão: se nenhum SDK for configurado, todas as chamadas de instrumentação são silenciosas (zero overhead, zero efeito colateral). Bibliotecas e SDKs de terceiros devem depender apenas da API, nunca do SDK. Isso garante que uma biblioteca pode ser instrumentada sem forçar o usuário final a usar um SDK específico.
OTel SDK
O SDK implementa a API e adiciona: processadores de spans, samplers, exportadores, configuração de recursos. O SDK é o que a aplicação final configura — não bibliotecas. O SDK substitui o no-op da API e passa a receber os dados de instrumentação.
// Separação API/SDK na prática — C#
// Biblioteca interna (depende só da API):
using OpenTelemetry.Trace; // API
public class PaymentClient
{
private static readonly ActivitySource _tracer =
new("PaymentClient"); // API — ActivitySource é da API
public async Task<PaymentResult> ChargeAsync(ChargeRequest req)
{
using var activity = _tracer.StartActivity("Charge"); // API
activity?.SetTag("payment.amount", req.Amount); // API
// se nenhum SDK configurado, activity é null — zero overhead
return await _httpClient.PostAsync(...);
}
}
// Aplicação final (configura o SDK):
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("PaymentClient") // registra a ActivitySource no SDK
.AddOtlpExporter()); // SDK — exportador
Auto-Instrumentation vs Manual
A maioria dos frameworks e clientes HTTP têm instrumentação automática via pacotes OTel. Configurar corretamente a auto-instrumentation cobre ~80% do que você precisa sem escrever uma linha de código de observabilidade na lógica de negócio.
# O que a auto-instrumentation captura tipicamente
# HTTP Server (ASP.NET Core, FastAPI, Gin, Echo):
# - span por request: método, path, status code
# - duração total do handler
# - exceções não tratadas como span events
# HTTP Client (HttpClient, httpx, net/http):
# - span por chamada sainte
# - propagação automática do W3C traceparent header
# Database (EF Core, SQLAlchemy, database/sql):
# - span por query
# - db.statement (SQL executado, opcional — desabilite em prod se sensível)
# - db.system, db.name, net.peer.name
# Message queues (MassTransit, aio-pika, sarama):
# - span por publish e consume
# - propagação de contexto via message headers
# gRPC (Grpc.Net.Client, grpcio, google.golang.org/grpc):
# - span por chamada com método, status
No Java, o OTel oferece um Java Agent que injeta instrumentação via bytecode manipulation — zero código no projeto. Em outras linguagens, a auto-instrumentation requer registrar os pacotes de instrumentação na inicialização da aplicação, mas não exige mudanças no código de negócio.
Instrumentação manual é necessária para: spans de negócio (ProcessOrder, CalculatePrice, ValidateRisk), atributos de negócio (order.id, customer.tier, payment.method), span events (pontos de decisão, retries, cache misses), métricas custom (orders_processed_total, revenue_usd) e baggage (propagação cross-cutting de tenant_id, feature_flag).
OTLP — OpenTelemetry Protocol
OTLP é o protocolo de transporte de telemetria do OTel. É o formato que SDKs usam para enviar dados ao OTel Collector (ou diretamente a backends que suportam OTLP). OTLP/gRPC — porta 4317, binário protobuf eficiente. Padrão para server-side. Suporta streaming bidirecional. OTLP/HTTP — porta 4318, protobuf over HTTP/1.1 (ou JSON). Mais fácil de debugar, funciona em ambientes que bloqueiam gRPC.
# Configuração via variáveis de ambiente (funciona em qualquer linguagem OTel)
OTEL_SERVICE_NAME=order-service
OTEL_SERVICE_VERSION=1.2.0
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # gRPC
OTEL_EXPORTER_OTLP_PROTOCOL=grpc # ou http/protobuf
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1 # 10% sampling
OTEL_METRICS_EXPORT_INTERVAL=30000 # 30s
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,cloud.region=us-east-1
# OTLP/HTTP — para frontends e ambientes edge
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTel Collector
O OTel Collector é um processo intermediário entre as aplicações e os backends de observabilidade. Funciona como um pipeline configurável de telemetria. Usar o Collector em vez de exportar diretamente traz: desacoplamento — mudar de backend não requer redeployar aplicações; tail sampling — requer um processo central com buffer de todos os spans; processamento — filtrar, transformar, enriquecer, agregar antes de exportar; fan-out — enviar para múltiplos backends simultaneamente; batching e retry — buffer local para absorver indisponibilidade temporária de backend; redução de custo — filtrar spans de health check, amostrar antes de enviar ao backend pago.
# otel-collector.yaml — configuração completa
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: otel-collector
static_configs:
- targets: ['localhost:8888']
processors:
resource:
attributes:
- key: cloud.provider
value: aws
action: insert
filter:
traces:
span:
- 'attributes["http.target"] == "/health"'
- 'attributes["http.target"] == "/metrics"'
batch:
send_batch_size: 512
timeout: 5s
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 2000 }
- name: sample-rest
type: probabilistic
probabilistic: { sampling_percentage: 10 }
memory_limiter:
limit_mib: 512
spike_limit_mib: 128
check_interval: 5s
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls: { insecure: true }
prometheusremotewrite:
endpoint: http://mimir:9009/api/v1/push
loki:
endpoint: http://loki:3100/loki/api/v1/push
datadog:
api:
key: ${DD_API_KEY}
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter, tail_sampling, batch]
exporters: [otlp/jaeger, datadog]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, resource, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
Agent mode (sidecar/daemonset): um Collector por host ou pod, recebe telemetria local, faz processamento leve, encaminha para um Collector central. Baixa latência, isolamento de falha. Gateway mode (deployment centralizado): pool de Collectors centrais que recebe de todos os agentes. Aqui ficam tail sampling, fan-out, exportação para backends. Escala horizontalmente.
# Kubernetes — DaemonSet como agent + Deployment como gateway
# Agent (DaemonSet): recebe OTLP das aplicações no mesmo nó,
# faz filtros leves, exporta para o gateway
# Gateway (Deployment, 3 réplicas): tail sampling, exportação
# Applications → Agent (4317 local) → Gateway (4317 internal) → Backends
Semantic Conventions
Semantic Conventions são os nomes padronizados de atributos de spans. Seguir as convenções garante que queries e dashboards funcionam across serviços e linguagens sem customização.
# HTTP Server spans
http.request.method: GET/POST/...
url.path: /users/123
http.response.status_code: 200
network.protocol.version: "1.1"
# HTTP Client spans
http.request.method: GET
server.address: api.external.com
server.port: 443
http.response.status_code: 200
# Database spans
db.system: postgresql / mysql / redis / mongodb
db.name: orders_db
db.operation.name: SELECT
db.query.text: "SELECT * FROM orders WHERE id = ?" # cuidado em prod
# Messaging
messaging.system: kafka / rabbitmq / sqs
messaging.destination.name: orders.created
messaging.operation.type: publish / receive / process
messaging.message.id: "msg-123"
# Service resource
service.name: order-service
service.version: 1.2.0
deployment.environment: production
cloud.provider: aws
cloud.region: us-east-1
k8s.pod.name: order-service-7d9f8b-xkp2q
k8s.namespace.name: default
Configuração Completa por Linguagem
// Packages:
// OpenTelemetry.Extensions.Hosting
// OpenTelemetry.Instrumentation.AspNetCore
// OpenTelemetry.Instrumentation.Http
// OpenTelemetry.Instrumentation.EntityFrameworkCore
// OpenTelemetry.Exporter.OpenTelemetryProtocol
// Program.cs
var otelBuilder = builder.Services.AddOpenTelemetry();
otelBuilder.ConfigureResource(resource =>
resource.AddService(
serviceName: builder.Configuration["ServiceName"] ?? "unknown",
serviceVersion: Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown")
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName.ToLower(),
}));
otelBuilder.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation(opts =>
{
opts.RecordException = true;
opts.Filter = ctx =>
!ctx.Request.Path.StartsWithSegments("/health") &&
!ctx.Request.Path.StartsWithSegments("/metrics");
})
.AddHttpClientInstrumentation(opts =>
opts.RecordException = true)
.AddEntityFrameworkCoreInstrumentation(opts =>
opts.SetDbStatementForText = false) // desabilitar SQL em prod
.AddSource("MyApp.*")
.AddOtlpExporter(); // lê OTEL_EXPORTER_OTLP_ENDPOINT da env
});
otelBuilder.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation() // GC, threads, memory
.AddMeter("MyApp.*")
.AddOtlpExporter();
});
// Logs — bridge do ILogger para OTel (não substitui o logger)
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeScopes = true;
logging.AddOtlpExporter();
});
AddOpenTelemetry() usa IHostedService para flush gracioso no shutdown. OTEL_* env vars são lidas automaticamente. SetDbStatementForText = false é a configuração segura para produção.
# Packages:
# opentelemetry-sdk
# opentelemetry-instrumentation-fastapi
# opentelemetry-instrumentation-sqlalchemy
# opentelemetry-instrumentation-httpx
# opentelemetry-exporter-otlp-proto-grpc
# otel_setup.py
import os
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3MultiFormat
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
def setup_otel(app, engine=None):
resource = Resource.create({
SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "unknown"),
SERVICE_VERSION: os.getenv("OTEL_SERVICE_VERSION", "unknown"),
"deployment.environment": os.getenv("ENV", "production"),
})
tp = TracerProvider(resource=resource)
tp.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter()) # lê OTEL_EXPORTER_OTLP_ENDPOINT
)
trace.set_tracer_provider(tp)
reader = PeriodicExportingMetricReader(
OTLPMetricExporter(),
export_interval_millis=30_000
)
mp = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(mp)
# W3C Trace Context + B3 para compatibilidade com sistemas legados
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
B3MultiFormat(),
]))
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="health,metrics",
tracer_provider=tp,
)
HTTPXClientInstrumentor().instrument(tracer_provider=tp)
if engine:
SQLAlchemyInstrumentor().instrument(
engine=engine,
enable_commenter=True, # injeta trace ID nos comentários SQL
)
return tp, mp
enable_commenter no SQLAlchemy injeta o trace ID como comentário SQL — ferramentas de DB monitoring podem correlacionar slow queries com traces. excluded_urls evita que health checks gerem spans.
// Packages:
// go.opentelemetry.io/otel
// go.opentelemetry.io/otel/sdk/trace
// go.opentelemetry.io/otel/sdk/metric
// go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
// go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
// go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
// otel/setup.go
package otel
type ShutdownFunc func(ctx context.Context) error
func Setup(ctx context.Context, serviceName, version string) (ShutdownFunc, error) {
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(version),
attribute.String("deployment.environment", os.Getenv("ENV")),
)
traceExp, err := otlptracegrpc.New(ctx) // lê OTEL_EXPORTER_OTLP_ENDPOINT
if err != nil {
return nil, fmt.Errorf("trace exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithBatcher(traceExp),
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1),
)),
)
metricExp, err := otlpmetricgrpc.New(ctx)
if err != nil {
return nil, fmt.Errorf("metric exporter: %w", err)
}
mp := metric.NewMeterProvider(
metric.WithResource(res),
metric.WithReader(metric.NewPeriodicReader(metricExp,
metric.WithInterval(30*time.Second))),
)
otel.SetTracerProvider(tp)
otel.SetMeterProvider(mp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
shutdown := func(ctx context.Context) error {
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
return nil
}
return shutdown, nil
}
// main.go
shutdown, err := otel.Setup(ctx, "order-service", "1.2.0")
if err != nil {
log.Fatal(err)
}
defer shutdown(context.Background())
// HTTP handler com auto-instrumentation
mux.Handle("/api/orders", otelhttp.NewHandler(
http.HandlerFunc(orderHandler),
"CreateOrder",
otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),
))
O shutdown gracioso é crítico — sem ele, spans não flushed são perdidos no encerramento do processo. Sempre chame tp.Shutdown() no signal handler. No Kubernetes, terminationGracePeriodSeconds deve ser maior que o timeout do shutdown.
Decisões de engenharia
Agent mode (sidecar ou DaemonSet): um Collector por host/pod, recebendo telemetria localmente. Vantagens: baixa latência de exportação, isolamento de falha por host (se o Collector do host A cai, apenas os pods desse host são afetados), e enriquecimento de metadados Kubernetes nativos (o Collector sabe o pod e node onde está rodando). Adequado para Kubernetes com DaemonSet.
Gateway mode (centralized): um pool de Collectors centrais recebendo de todos os serviços. Vantagens: mais fácil de operar e dimensionar, ponto único para configurar tail sampling (que requer que todos os spans de um trace cheguem ao mesmo Collector), e roteamento centralizado para múltiplos backends. Desvantagem: SPOF se não tiver HA. O padrão em produção é Agent → Gateway: os agents locais fazem enriquecimento e pré-processamento, o gateway central faz tail sampling e roteamento.
OTLP gRPC: protocol buffers sobre HTTP/2. Mais eficiente em throughput (multiplexação de streams, compressão nativa, menor overhead por mensagem). Padrão para comunicação entre serviços e Collector em ambiente interno. Porta padrão: 4317. Requer que o cliente suporte HTTP/2 — a maioria dos ambientes modernos suporta, mas proxies legados podem não suportar.
OTLP HTTP: protocol buffers sobre HTTP/1.1 (ou HTTP/2). Mais compatível com ambientes onde gRPC é bloqueado (firewalls corporativos, alguns API gateways). Mais fácil de debugar (curl-able). Porta padrão: 4318. Ligeiramente menos eficiente que gRPC puro. Use OTLP HTTP quando gRPC for bloqueado ou quando você precisa de compatibilidade com ferramentas que não suportam gRPC. Em Kubernetes interno, prefira gRPC.
OTel SDK puro: zero vendor lock-in na instrumentação. Você exporta via OTLP para qualquer backend. Migrar de Jaeger para Datadog não requer alterar código de aplicação — apenas reconfigurar o Collector. É a abordagem recomendada para novos projetos. O custo: algumas features avançadas vendor-specific (profiling contínuo do Datadog, análise de anomalias do Dynatrace) não estão disponíveis via OTel.
Vendor SDK + OTel exporter: use o SDK do Datadog/New Relic mas exporte também via OTLP. Permite aproveitar features específicas do vendor enquanto mantém portabilidade parcial. Alternativa: use OTel na instrumentação e configure o Datadog Agent para receber OTLP — você fica no Datadog mas a instrumentação é vendor-neutral. É a estratégia de migração mais segura quando já há código instrumentado com SDK proprietário.
Logs Bridge API: o OTel define uma bridge que conecta loggers existentes (Serilog, structlog, slog, log4j) ao pipeline OTel. Os logs continuam sendo escritos pelo logger existente, mas o bridge injeta automaticamente trace_id e span_id do span ativo e encaminha para o OTLP pipeline. Zero mudança no código que faz logging. É a abordagem correta para sistemas existentes — não substitua um logger maduro por OTel Logs diretamente.
OTel Logs direto: usar a OTel Logs API diretamente como logger. Adequado para novos serviços onde não há logger estabelecido. Vantagem: um único pipeline para os três sinais, sem bridges. Desvantagem: maturidade menor que loggers especializados (Serilog tem scopes semânticos ricos, structlog tem processadores composáveis). Em 2025, a combinação logger existente + bridge é a abordagem mais pragmática.
Como praticar
-
Configure OTel completo em um serviço com traces, metrics e logs bridge, exportando via OTLP gRPC para o OTel Collector local (Docker Compose). Configure o Collector para exportar traces ao Jaeger, métricas ao Prometheus e logs ao Loki. Verifique que um único request aparece correlacionado nas três ferramentas via Trace ID.
Critério: um request HTTP gera: (1) trace no Jaeger com spans de HTTP + DB; (2) métricas RED no Prometheus com exemplar contendo o trace_id; (3) logs no Loki com trace_id e span_id; navegar do log para o trace e do trace para o log funciona via Grafana. -
Demonstre a separação API/SDK: crie uma biblioteca interna que usa só a API OTel (
ActivitySourceem .NET outrace.Tracerem Go), sem depender do SDK. Escreva dois testes: um sem SDK configurado (chamadas de instrumentação são no-op, zero alocação verificável via benchmark) e um com SDK configurado (spans aparecem no exporter em memória).
Critério: a biblioteca não tem dependência no pacote do SDK em seu arquivo de dependências; benchmark mostra zero alocações sem SDK; com SDK, spans aparecem corretamente no exporter; a biblioteca pode ser usada em outro projeto que usa SDK diferente sem conflito. -
Configure o OTel Collector com tail-based sampling: sempre capturar traces com status Error ou latência acima de 2s, amostrar 10% do restante. Gere 1000 requests (90% normais, 10% com erro simulado). Valide a contagem no Jaeger.
Critério: ~100% dos traces com erro aparecem no Jaeger (mín. 95 de 100); ~10% dos traces normais aparecem (entre 80 e 120 de 900); a configuração do tail sampler no Collector está documentada com explicação de cada política. -
Implemente fan-out no Collector: configure dois exporters de traces — Jaeger (100% dos traces, para debug) e um exporter simulado de produção (apenas erros via filtro por atributo
otel.status_code = ERROR). Valide que Jaeger recebe todos os traces e o exporter de produção recebe apenas os com erro.
Critério: Jaeger mostra todos os traces; exporter de produção mostra apenas traces com erro; a configuração de pipelines no Collector está correta e documentada; nenhuma duplicação de spans no mesmo exporter. -
Configure Semantic Conventions em um endpoint de banco de dados:
db.system,db.name,db.operation.name. Habilitedb.query.textapenas via env varOTEL_DB_QUERY_TEXT=true(desabilitado por padrão por PII/volume). Compare o tamanho do span com e sem o atributo e documente o impacto em bytes.
Critério: semdb.query.text: span tem os 3 atributos obrigatórios e tamanho X bytes; com a env var: span inclui a query SQL e tamanho Y > X; a diferença de volume projetada para 1M spans/dia é calculada; a UI do Jaeger exibe o span de DB com formatação especial paradb.statement.
Perguntas de entrevista
Qual a diferença entre OTel API e OTel SDK? Por que essa separação é fundamental para bibliotecas?
OTel API: o contrato público — interfaces, tipos e funções que código de aplicação e bibliotecas usam para instrumentar. A API tem uma implementação no-op por padrão: sem SDK configurado, todas as chamadas de instrumentação são silenciosas, com zero overhead e zero efeito colateral. A API é estável e garantidamente retrocompatível.
OTel SDK: a implementação da API — gerencia o ciclo de vida de spans (criação, sampling, processamento, exportação), o pipeline de métricas e o log bridge. O SDK é configurado uma vez no ponto de entrada da aplicação (main.go, Program.cs, app.py). É o SDK que decide para onde os dados vão (Jaeger, Prometheus, Datadog) e como são amostrados.
Por que importa para bibliotecas: uma biblioteca que depende apenas da API OTel pode ser instrumentada sem forçar o usuário final a usar um SDK específico. Se a biblioteca dependesse do SDK, qualquer aplicação que a usasse seria forçada a carregar aquele SDK — mesmo que quisesse usar outro. A regra: bibliotecas e frameworks dependem apenas da API; aplicações finais configuram o SDK. Isso é o que permite que o ecossistema de bibliotecas OTel funcione sem conflitos de dependência.
Como funciona o OTel Collector? Por que usá-lo em vez de exportar diretamente do serviço para o backend?
O que é: o OTel Collector é um proxy de telemetria que recebe dados (via OTLP, Jaeger, Zipkin, Prometheus scrape, e outros), processa (filtra, enriquece, faz sampling), e exporta para um ou múltiplos backends. É configurado via YAML com pipelines de receivers → processors → exporters.
Por que usar em vez de exportar diretamente: (1) fan-out — enviar para múltiplos backends sem alterar o serviço (Jaeger + Datadog + S3 ao mesmo tempo); (2) tail sampling — impossível no serviço (que não conhece o trace completo) mas possível no Collector centralizado; (3) enriquecimento — adicionar metadados Kubernetes (namespace, pod, node) sem poluir o código da aplicação; (4) buffering e retry — se o backend estiver indisponível, o Collector bufferiza e tenta novamente; (5) transformação — remover atributos sensíveis (PII), renomear, filtrar spans de baixo valor antes de enviar ao backend pago.
Topologia recomendada: serviço → OTLP → Collector Agent (DaemonSet, enriquece com metadados K8s) → OTLP → Collector Gateway (tail sampling, fan-out, transformação) → backends.
O que é OTLP e por que ele se tornou o protocolo padrão para telemetria?
OTLP (OpenTelemetry Protocol): protocolo de transporte para traces, métricas e logs definido pelo projeto OTel. Usa protocol buffers como formato de serialização, com transporte via gRPC (porta 4317) ou HTTP/JSON (porta 4318). É o único protocolo que transporta os três sinais (traces, metrics, logs) em um único endpoint de forma padronizada.
Por que se tornou padrão: (1) todos os vendors adotaram — Datadog, New Relic, Dynatrace, Grafana Cloud, Honeycomb, AWS X-Ray aceitam OTLP nativamente; (2) elimina N protocolos vendor-specific (Jaeger Thrift, Zipkin JSON, StatsD, etc.) por um único; (3) é extensível sem breaking changes; (4) performance: protobuf é mais compacto e rápido que JSON; (5) suporte a compressão nativa (gzip, zstd) para reduzir custo de rede.
Impacto prático: antes do OTLP, migrar de Jaeger para Datadog exigia reconfigurar o SDK de cada serviço. Com OTLP, você reconfigura apenas o Collector — o código da aplicação não muda. É a separação entre instrumentação (estável, no serviço) e roteamento (mutável, no Collector).
Como você configuraria o shutdown gracioso do SDK OTel e por que ele é crítico?
O problema sem shutdown gracioso: spans criados mas não flushed são perdidos quando o processo termina. Em Go, o TracerProvider.Shutdown(ctx) drena o buffer de spans e força o export antes de retornar. Sem isso, os últimos spans do processo — frequentemente os mais interessantes, como os que ocorrem durante o graceful shutdown do servidor — são silenciosamente descartados.
Implementação correta em Go: registrar o shutdown com defer tp.Shutdown(context.Background()) no main, mas com um contexto com timeout (ex: 5s) para não bloquear indefinidamente se o backend estiver indisponível. Em Kubernetes, o terminationGracePeriodSeconds deve ser maior que o timeout do shutdown do OTel SDK + o timeout de graceful shutdown do servidor HTTP.
Ordem importa: o shutdown do TracerProvider deve acontecer após o servidor HTTP parar de aceitar novas requisições, mas antes do processo terminar. A sequência: (1) receber SIGTERM; (2) parar de aceitar novas conexões; (3) drenar requisições em andamento; (4) fazer shutdown do OTel SDK (flush spans); (5) terminar. Se o OTel SDK for shutado antes de drenar as requisições, os spans das requisições em andamento são perdidos.
Como você evitaria conflitos entre atributos customizados e as Semantic Conventions do OTel?
O problema: a OTel Semantic Conventions define atributos padronizados que crescem a cada versão. Se você criar um atributo customizado http.url com semântica diferente da convenção oficial, pode conflitar quando o SDK atualizar para incluir http.url oficialmente — gerando dois valores para o mesmo nome ou comportamento inesperado no backend.
Regra de namespace: atributos customizados devem usar um namespace de domínio com ponto como prefixo — acme.order.id, acme.customer.tier, acme.payment.method. O prefixo acme. (ou o nome da sua empresa/produto) garante que nunca colide com convenções OTel (http.*, db.*, messaging.*, rpc.*).
Verificar antes de criar: antes de criar qualquer atributo customizado, pesquisar nas Semantic Conventions se já existe um nome padronizado para aquele conceito. db.user existe? messaging.destination.name existe? Usar o nome padrão quando disponível permite que ferramentas como Jaeger e Datadog exibam o atributo com formatação especial e filtros nativos.
Versionamento: documentar os atributos customizados usados em um OTEL_CONVENTIONS.md interno com nome, tipo, valores possíveis, e em quais serviços são usados. Quando as Semantic Conventions adotarem um equivalente, migrar todos os serviços de forma coordenada.
Referências
- docs OpenTelemetry — Documentação oficial — CNCF.
- docs OTel Collector — Configuração e deployment — CNCF.
- docs Semantic Conventions — OpenTelemetry.
- docs OTLP Specification — OpenTelemetry.
- artigo OpenTelemetry: a Brief History — Austin Parker (2023).
- livro Observability Engineering — Charity Majors, Liz Fong-Jones, George Miranda (O'Reilly, 2022).
- docs OTel SDK — Java, Go, Python, .NET — OpenTelemetry.
- docs OTel Collector Contrib — Receivers e Exporters — OpenTelemetry.
- docs OpenTelemetry Demo Application — CNCF.
- artigo Tail-based Sampling with OpenTelemetry Collector — Grafana Labs (2023).
- artigo Migrating from OpenCensus to OpenTelemetry — OpenTelemetry (2022).
- paper Dapper, a Large-Scale Distributed Systems Tracing Infrastructure — Sigelman et al., Google (2010).