Introdução
O Tracing é o framework de observabilidade padrão do ecossistema Rust. Diferente de sistemas de logging tradicionais que registram mensagens de texto isoladas, o Tracing trabalha com spans (intervalos de tempo) e events (pontos no tempo), permitindo rastrear o fluxo de execução de forma estruturada e hierárquica.
Isso é especialmente poderoso em aplicações assíncronas, onde uma requisição pode saltar entre tasks e threads. O Tracing mantém o contexto da execução mesmo nesses cenários, permitindo que você correlacione logs, métricas e traces de forma coerente.
O ecossistema Tracing é composto por várias crates complementares:
- tracing: a API de instrumentação (spans, events, macros)
- tracing-subscriber: implementações de subscribers (formatadores, filtros)
- tracing-appender: escrita em arquivos com rotação
- tracing-opentelemetry: exportação para OpenTelemetry
Por que usar o Tracing?
- Estruturado: dados são campos tipados, não apenas texto
- Hierárquico: spans aninhados mostram o fluxo de execução
- Async-aware: funciona corretamente com async/await
- Performático: overhead mínimo quando desabilitado
- Extensível: subscribers customizáveis para qualquer destino
- Ecossistema rico: JSON, OpenTelemetry, Jaeger, Datadog, etc.
- Padrão da indústria: usado por Tokio, Axum, Hyper, Tonic e praticamente todo o ecossistema
Instalação
Adicione o Tracing ao seu Cargo.toml:
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
Para uma configuração mais completa:
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [
"env-filter", # Filtragem via RUST_LOG
"json", # Output em JSON
"fmt", # Formatação de texto
"time", # Timestamps
] }
tracing-appender = "0.2" # Escrita em arquivos
tracing-opentelemetry = "0.27" # OpenTelemetry
opentelemetry = "0.27"
opentelemetry-otlp = "0.27"
Uso Básico
Configuração mínima
use tracing::{info, warn, error, debug, trace};
fn main() {
// Inicializar o subscriber padrão
tracing_subscriber::fmt::init();
info!("Aplicação iniciada");
debug!("Modo debug ativo");
warn!("Este é um aviso");
error!("Este é um erro");
// Eventos com campos estruturados
info!(
usuario = "maria",
acao = "login",
"Usuário fez login"
);
// Campos numéricos
info!(
latencia_ms = 42,
status = 200,
metodo = "GET",
caminho = "/api/usuarios",
"Requisição processada"
);
}
// Output:
// 2026-02-27T10:30:00.000Z INFO app: Aplicação iniciada
// 2026-02-27T10:30:00.001Z INFO app: Usuário fez login usuario="maria" acao="login"
Spans: rastreando intervalos de tempo
use tracing::{info, info_span, warn};
fn main() {
tracing_subscriber::fmt::init();
// Criar um span manualmente
let span = info_span!("processamento", tarefa_id = 42);
let _guard = span.enter();
info!("Iniciando processamento");
processar_dados();
info!("Processamento concluído");
}
fn processar_dados() {
// Span aninhado
let span = info_span!("validacao", etapa = "dados");
let _guard = span.enter();
info!("Validando dados de entrada");
// Outro nível de aninhamento
let span = info_span!("transformacao");
let _guard = span.enter();
info!("Transformando dados");
warn!("Campo opcional ausente, usando valor padrão");
}
// Output mostra a hierarquia:
// INFO processamento{tarefa_id=42}: Iniciando processamento
// INFO processamento{tarefa_id=42}:validacao{etapa="dados"}: Validando dados de entrada
// INFO processamento{tarefa_id=42}:validacao{etapa="dados"}:transformacao: Transformando dados
// WARN processamento{tarefa_id=42}:validacao{etapa="dados"}:transformacao: Campo opcional ausente
A macro #[instrument]
A forma mais ergonômica de instrumentar funções:
use tracing::{info, instrument, warn};
#[instrument]
fn processar_pedido(pedido_id: u64, cliente: &str) {
info!("Processando pedido");
validar_estoque(pedido_id);
calcular_frete(pedido_id, "SP");
info!("Pedido processado com sucesso");
}
#[instrument(skip(pedido_id))] // Não incluir pedido_id nos campos do span
fn validar_estoque(pedido_id: u64) {
info!("Verificando estoque");
}
#[instrument(fields(regiao = %destino))] // Campo customizado
fn calcular_frete(pedido_id: u64, destino: &str) {
info!(valor = 15.90, "Frete calculado");
}
fn main() {
tracing_subscriber::fmt::init();
processar_pedido(12345, "Maria");
}
// Output:
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}: Processando pedido
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}:validar_estoque: Verificando estoque
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}:calcular_frete{regiao="SP"}: Frete calculado valor=15.9
Instrument com funções async
use tracing::{info, instrument};
#[instrument]
async fn buscar_usuario(id: u64) -> Result<String, String> {
info!("Buscando no banco de dados");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(format!("Usuário {}", id))
}
#[instrument]
async fn processar_requisicao(usuario_id: u64) {
info!("Iniciando processamento");
match buscar_usuario(usuario_id).await {
Ok(usuario) => info!(usuario = %usuario, "Usuário encontrado"),
Err(e) => tracing::error!(erro = %e, "Falha ao buscar usuário"),
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
processar_requisicao(42).await;
}
Recursos Avançados
Configuração detalhada do subscriber
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
fn configurar_tracing_desenvolvimento() {
tracing_subscriber::fmt()
// Formato legível para desenvolvimento
.pretty()
// Mostrar nível do evento
.with_level(true)
// Mostrar nome do target (módulo)
.with_target(true)
// Mostrar timestamps
.with_timer(fmt::time::ChronoLocal::new("%H:%M:%S%.3f".to_string()))
// Mostrar número da linha
.with_line_number(true)
// Mostrar nome do arquivo
.with_file(true)
// Filtro padrão
.with_env_filter(EnvFilter::new("debug"))
.init();
}
fn configurar_tracing_producao() {
tracing_subscriber::fmt()
// JSON para produção (fácil de parsear por ferramentas)
.json()
// Timestamps ISO 8601
.with_timer(fmt::time::ChronoUtc::rfc_3339())
// Incluir spans atuais
.with_current_span(true)
// Incluir hierarquia de spans
.with_span_list(true)
// Filtro via variável RUST_LOG
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
}
fn main() {
let ambiente = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());
if ambiente == "producao" {
configurar_tracing_producao();
} else {
configurar_tracing_desenvolvimento();
}
tracing::info!("Aplicação iniciada no ambiente: {}", ambiente);
}
Filtragem com EnvFilter
use tracing_subscriber::EnvFilter;
fn exemplos_filtros() {
// Filtro simples por nível
let _ = EnvFilter::new("info");
// Filtro por módulo
let _ = EnvFilter::new("minha_app=debug,hyper=warn,tower=info");
// Filtro complexo
let _ = EnvFilter::new(
"info,minha_app=trace,minha_app::db=debug,sqlx=warn,hyper=error"
);
// Filtro com spans específicos
let _ = EnvFilter::new("info,[requisicao]=debug");
// Via variável de ambiente RUST_LOG
let _ = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
}
// Uso na linha de comando:
// RUST_LOG=debug cargo run
// RUST_LOG=minha_app=trace,sqlx=warn cargo run
// RUST_LOG="info,[requisicao{id=42}]=trace" cargo run
JSON logging para produção
use tracing::{info, instrument};
use tracing_subscriber::fmt;
fn configurar_json_logging() {
tracing_subscriber::fmt()
.json()
// Flatten campos de eventos e spans
.flatten_event(true)
// Incluir span hierárquico
.with_current_span(true)
.with_span_list(true)
// Timestamp
.with_timer(fmt::time::ChronoUtc::rfc_3339())
.init();
}
#[instrument(fields(request_id = %uuid::Uuid::new_v4()))]
async fn handler_exemplo(metodo: &str, caminho: &str) {
info!(
metodo = metodo,
caminho = caminho,
"Requisição recebida"
);
// Simular processamento
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
info!(
status = 200,
latencia_ms = 10,
"Resposta enviada"
);
}
#[tokio::main]
async fn main() {
configurar_json_logging();
handler_exemplo("GET", "/api/usuarios").await;
}
// Output JSON (uma linha por evento):
// {"timestamp":"2026-02-27T10:30:00.000Z","level":"INFO","message":"Requisição recebida","metodo":"GET","caminho":"/api/usuarios","request_id":"a1b2c3d4-...","target":"app"}
// {"timestamp":"2026-02-27T10:30:00.010Z","level":"INFO","message":"Resposta enviada","status":200,"latencia_ms":10,"request_id":"a1b2c3d4-...","target":"app"}
Logging em arquivo com rotação
use tracing_appender::rolling;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
fn configurar_log_arquivo() {
// Rotação diária de arquivos de log
let file_appender = rolling::daily("logs", "app.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// Combinar stdout e arquivo
let stdout_layer = fmt::layer()
.pretty()
.with_filter(EnvFilter::new("info"));
let file_layer = fmt::layer()
.json()
.with_writer(non_blocking)
.with_filter(EnvFilter::new("debug"));
tracing_subscriber::registry()
.with(stdout_layer)
.with(file_layer)
.init();
// IMPORTANTE: _guard deve ser mantido vivo durante toda a execução
// Se _guard for dropado, os logs pendentes podem ser perdidos
}
fn configurar_log_arquivo_com_guard() -> tracing_appender::non_blocking::WorkerGuard {
let file_appender = rolling::daily("logs", "app.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::fmt()
.json()
.with_writer(non_blocking)
.with_env_filter(EnvFilter::new("info"))
.init();
guard // Retornar o guard para mantê-lo vivo
}
fn main() {
let _guard = configurar_log_arquivo_com_guard();
tracing::info!("Aplicação iniciada");
// ... resto da aplicação
// O guard é dropado aqui, garantindo que todos os logs são escritos
}
Múltiplos subscribers (layers)
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer};
fn configurar_multiplos_layers() {
// Layer para console (texto formatado)
let console_layer = fmt::layer()
.pretty()
.with_filter(EnvFilter::new("info"));
// Layer para arquivo (JSON)
let file_appender = tracing_appender::rolling::daily("logs", "app.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
let file_layer = fmt::layer()
.json()
.with_writer(non_blocking)
.with_filter(EnvFilter::new("debug"));
// Layer para erros (arquivo separado)
let error_appender = tracing_appender::rolling::daily("logs", "errors.log");
let (error_non_blocking, _error_guard) = tracing_appender::non_blocking(error_appender);
let error_layer = fmt::layer()
.json()
.with_writer(error_non_blocking)
.with_filter(EnvFilter::new("error"));
// Combinar todos os layers
tracing_subscriber::registry()
.with(console_layer)
.with(file_layer)
.with(error_layer)
.init();
}
Integração com OpenTelemetry
use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use tracing_subscriber::{prelude::*, EnvFilter, fmt};
async fn configurar_opentelemetry() -> Result<(), Box<dyn std::error::Error>> {
// Configurar exporter OTLP
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint("http://localhost:4317")
.build()?;
let provider = opentelemetry_sdk::trace::TracerProvider::builder()
.with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
.with_resource(opentelemetry_sdk::Resource::new(vec![
opentelemetry::KeyValue::new("service.name", "minha-app"),
opentelemetry::KeyValue::new("service.version", "1.0.0"),
]))
.build();
let tracer = provider.tracer("minha-app");
// Layer do OpenTelemetry
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
// Layer do console
let fmt_layer = fmt::layer()
.pretty()
.with_filter(EnvFilter::new("info"));
// Combinar
tracing_subscriber::registry()
.with(otel_layer)
.with(fmt_layer)
.init();
Ok(())
}
use tracing::{info, instrument};
#[instrument]
async fn processar_pedido(pedido_id: u64) {
info!("Processando pedido");
validar_pagamento(pedido_id).await;
enviar_notificacao(pedido_id).await;
info!("Pedido concluído");
}
#[instrument]
async fn validar_pagamento(pedido_id: u64) {
info!("Validando pagamento");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
info!("Pagamento validado");
}
#[instrument]
async fn enviar_notificacao(pedido_id: u64) {
info!("Enviando notificação");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
info!("Notificação enviada");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
configurar_opentelemetry().await?;
processar_pedido(12345).await;
// Dar tempo para o exporter enviar os dados
opentelemetry::global::shutdown_tracer_provider();
Ok(())
}
Campos dinâmicos em spans
use tracing::{info, info_span, Span};
fn exemplo_campos_dinamicos() {
let span = info_span!("requisicao", request_id = tracing::field::Empty, user_id = tracing::field::Empty);
let _guard = span.enter();
// Preencher campos depois de criar o span
Span::current().record("request_id", "req-abc-123");
info!("Requisição recebida");
// Após autenticação, adicionar user_id
Span::current().record("user_id", 42);
info!("Usuário autenticado");
}
Boas Práticas
1. Use campos estruturados, não interpolação de strings
use tracing::info;
fn exemplo_boas_praticas() {
// RUIM: dados interpolados na string
let usuario_id = 42;
let acao = "login";
info!("Usuário {} realizou {}", usuario_id, acao);
// BOM: campos estruturados (parseáveis por ferramentas)
info!(
usuario_id = usuario_id,
acao = acao,
"Ação do usuário"
);
}
2. Use #[instrument] em todas as funções importantes
use tracing::instrument;
#[instrument(skip(senha))] // Nunca logar senhas!
async fn autenticar(email: &str, senha: &str) -> Result<u64, String> {
// ...
Ok(42)
}
#[instrument(err)] // Logar automaticamente se retornar Err
async fn buscar_dados(id: u64) -> Result<String, std::io::Error> {
// ...
Ok("dados".to_string())
}
#[instrument(ret)] // Logar o valor de retorno
fn calcular(a: i32, b: i32) -> i32 {
a + b
}
#[instrument(
name = "processar_pagamento", // Nome customizado do span
skip(cartao), // Pular dados sensíveis
fields(
valor_brl = %valor, // Campo customizado
moeda = "BRL", // Campo estático
)
)]
async fn processar(pedido_id: u64, valor: f64, cartao: &str) -> Result<(), String> {
// ...
Ok(())
}
3. Configure filtros por ambiente
use tracing_subscriber::EnvFilter;
fn filtro_para_ambiente(ambiente: &str) -> EnvFilter {
match ambiente {
"dev" => EnvFilter::new("debug,hyper=info,h2=info"),
"staging" => EnvFilter::new("info,minha_app=debug"),
"producao" => EnvFilter::new("warn,minha_app=info"),
_ => EnvFilter::new("info"),
}
}
4. Nunca logue dados sensíveis
use tracing::{info, instrument};
// BOM: skip de campos sensíveis
#[instrument(skip(token, senha))]
async fn criar_conta(email: &str, senha: &str, token: &str) -> Result<u64, String> {
info!(email = email, "Criando conta");
Ok(1)
}
// BOM: mascarar dados parcialmente
fn mascarar_email(email: &str) -> String {
let partes: Vec<&str> = email.split('@').collect();
if partes.len() == 2 {
let usuario = partes[0];
let dominio = partes[1];
let visivel = if usuario.len() > 2 { 2 } else { 1 };
format!("{}***@{}", &usuario[..visivel], dominio)
} else {
"***".to_string()
}
}
5. Adicione contexto com spans
use tracing::{info, info_span, Instrument};
async fn processar_requisicao(request_id: &str, usuario_id: u64) {
// Span de nível de requisição
let span = info_span!(
"requisicao",
request_id = request_id,
usuario_id = usuario_id,
);
async {
info!("Processando requisição");
// Sub-operações herdam o contexto
buscar_dados().await;
salvar_resultado().await;
info!("Requisição concluída");
}
.instrument(span) // Associar o span à future
.await;
}
async fn buscar_dados() {
let span = info_span!("buscar_dados");
async {
info!("Consultando banco");
}
.instrument(span)
.await;
}
async fn salvar_resultado() {
let span = info_span!("salvar_resultado");
async {
info!("Salvando no banco");
}
.instrument(span)
.await;
}
Exemplos Práticos
Serviço web observável com Axum
use axum::{
extract::{Path, State},
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::Serialize;
use std::time::{Duration, Instant};
use tracing::{error, info, info_span, instrument, warn, Instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use uuid::Uuid;
// === Configuração de tracing ===
fn configurar_tracing() {
let ambiente = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());
if ambiente == "producao" {
// JSON para produção
tracing_subscriber::fmt()
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(true)
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")),
)
.init();
} else {
// Texto formatado para desenvolvimento
tracing_subscriber::fmt()
.pretty()
.with_env_filter(EnvFilter::new(
"debug,hyper=info,h2=info,tower=info",
))
.init();
}
}
// === Middleware de tracing ===
async fn middleware_tracing(request: Request<axum::body::Body>, next: Next) -> Response {
let request_id = Uuid::new_v4().to_string();
let metodo = request.method().clone();
let uri = request.uri().clone();
let versao = request.version();
let span = info_span!(
"requisicao",
request_id = %request_id,
metodo = %metodo,
uri = %uri,
versao = ?versao,
status = tracing::field::Empty,
latencia_ms = tracing::field::Empty,
);
let inicio = Instant::now();
let response = next.run(request).instrument(span.clone()).await;
let latencia = inicio.elapsed();
let status = response.status().as_u16();
span.record("status", status);
span.record("latencia_ms", latencia.as_millis() as u64);
let _guard = span.enter();
if status >= 500 {
error!(
status = status,
latencia_ms = latencia.as_millis() as u64,
"Erro interno do servidor"
);
} else if status >= 400 {
warn!(
status = status,
latencia_ms = latencia.as_millis() as u64,
"Erro do cliente"
);
} else {
info!(
status = status,
latencia_ms = latencia.as_millis() as u64,
"Requisição concluída"
);
}
response
}
// === Handlers ===
#[derive(Serialize)]
struct Usuario {
id: u64,
nome: String,
email: String,
}
#[instrument(skip(state))]
async fn listar_usuarios(State(state): State<AppState>) -> Json<Vec<Usuario>> {
info!("Listando usuários");
let usuarios = buscar_usuarios_do_banco(&state).await;
info!(total = usuarios.len(), "Usuários carregados");
Json(usuarios)
}
#[instrument(skip(state))]
async fn obter_usuario(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<Json<Usuario>, StatusCode> {
info!("Buscando usuário");
match buscar_usuario_por_id(&state, id).await {
Some(usuario) => {
info!(nome = %usuario.nome, "Usuário encontrado");
Ok(Json(usuario))
}
None => {
warn!("Usuário não encontrado");
Err(StatusCode::NOT_FOUND)
}
}
}
#[instrument]
async fn saude() -> impl IntoResponse {
info!("Health check");
Json(serde_json::json!({"status": "ok"}))
}
// === Funções de negócio instrumentadas ===
#[instrument(skip(state))]
async fn buscar_usuarios_do_banco(state: &AppState) -> Vec<Usuario> {
info!("Consultando banco de dados");
// Simular latência do banco
tokio::time::sleep(Duration::from_millis(20)).await;
vec![
Usuario {
id: 1,
nome: "Maria Silva".to_string(),
email: "maria@email.com".to_string(),
},
Usuario {
id: 2,
nome: "João Santos".to_string(),
email: "joao@email.com".to_string(),
},
]
}
#[instrument(skip(state))]
async fn buscar_usuario_por_id(state: &AppState, id: u64) -> Option<Usuario> {
info!("Consultando banco de dados por ID");
tokio::time::sleep(Duration::from_millis(10)).await;
if id <= 2 {
Some(Usuario {
id,
nome: format!("Usuário {}", id),
email: format!("usuario{}@email.com", id),
})
} else {
None
}
}
// === Estado e App ===
#[derive(Clone)]
struct AppState {
db_url: String,
}
fn criar_app() -> Router {
let state = AppState {
db_url: "postgres://localhost/app".to_string(),
};
Router::new()
.route("/saude", get(saude))
.route("/usuarios", get(listar_usuarios))
.route("/usuarios/{id}", get(obter_usuario))
.layer(middleware::from_fn(middleware_tracing))
.with_state(state)
}
#[tokio::main]
async fn main() {
configurar_tracing();
info!(
versao = env!("CARGO_PKG_VERSION"),
"Iniciando servidor"
);
let app = criar_app();
let addr = "0.0.0.0:3000";
info!(endereco = addr, "Servidor pronto");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Comparação com Alternativas
| Característica | Tracing | log + env_logger | slog | log4rs |
|---|---|---|---|---|
| Abordagem | Spans + events | Events apenas | Structured logging | Logging framework |
| Estruturado | Sim (nativo) | Não | Sim | Parcial |
| Spans | Sim | Não | Não | Não |
| Async-aware | Sim | Não | Não | Não |
| Hierárquico | Sim (spans) | Não | Parcial | Não |
| OpenTelemetry | Nativo | Via bridge | Via bridge | Não |
| JSON output | Sim | Não | Sim | Sim |
| Performance | Excelente | Boa | Boa | Boa |
| Ecossistema | Tokio, Axum, etc. | Amplo | Moderado | Limitado |
- Tracing vs log: log é mais simples e adequado para libs que não precisam de spans. Tracing é superior para aplicações, especialmente async. O Tracing implementa o facade do log, então libs usando log funcionam com subscribers do Tracing.
- Tracing vs slog: slog foi pioneiro em structured logging no Rust, mas Tracing se tornou o padrão com melhor integração com o ecossistema async.
- Quando usar log: em bibliotecas que querem dependência mínima e não precisam de spans.
- Quando usar Tracing: em aplicações, especialmente as que usam async/await.
Conclusão
O Tracing transformou a observabilidade em Rust de simples “imprimir mensagens” para um sistema sofisticado de rastreamento estruturado. A combinação de spans hierárquicos, eventos estruturados e integração nativa com o ecossistema async torna-o indispensável para qualquer aplicação de produção.
O investimento em instrumentar seu código com Tracing paga dividendos enormes quando você precisa diagnosticar problemas em produção. A capacidade de correlacionar eventos dentro de spans, filtrar por contexto e exportar para sistemas como Jaeger, Datadog ou Grafana Tempo faz toda a diferença na operação de serviços em escala.
Próximos passos
- Configure tracing-opentelemetry para exportar traces para Jaeger ou Grafana Tempo
- Use tower-http TraceLayer para instrumentar automaticamente requisições Axum
- Explore tracing-appender para rotação de arquivos de log
- Combine com Config para configurar níveis de log por ambiente
- Aprenda sobre métricas com opentelemetry-metrics para complementar os traces