Como implementar span attributes customizados no OpenTelemetry

1. Entendendo Span Attributes no OpenTelemetry

No modelo de dados do OpenTelemetry, spans representam unidades de trabalho em um sistema distribuído. Cada span contém metadados essenciais como nome, ID, trace ID, timestamps e, crucialmente, span attributes. Attributes são pares chave-valor que enriquecem o contexto semântico de uma operação.

Attributes diferem de events (que marcam momentos específicos com timestamp) e links (que conectam spans entre traces diferentes). Enquanto events documentam ocorrências pontuais, attributes descrevem propriedades estáveis do span durante toda sua vida útil.

2. Preparando o Ambiente e SDKs Necessários

Para este artigo, usaremos Python, mas os conceitos se aplicam a Java, Go, Node.js e outras linguagens suportadas. A configuração básica envolve:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp

Exemplo de configuração mínima do SDK:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

3. Adicionando Attributes no Momento da Criação do Span

A forma mais direta de incluir attributes é no construtor do span:

with tracer.start_as_current_span(
    "processar_pedido",
    attributes={
        "pedido.id": "ORD-2024-001",
        "pedido.valor": 299.90,
        "pedido.origem": "web"
    }
) as span:
    processar_pedido()

Boas práticas de nomenclatura:
- Use lowercase e underscores
- Adote namespaces com ponto (ex: meuapp.transacao.id)
- Prefira nomes descritivos e consistentes

4. Inserindo Attributes Dinamicamente Durante a Execução

Attributes podem ser adicionados ou modificados após a criação do span usando set_attribute():

with tracer.start_as_current_span("autenticar_usuario") as span:
    usuario_id = obter_usuario_do_token()
    span.set_attribute("auth.usuario.id", usuario_id)

    perfil = carregar_perfil(usuario_id)
    span.set_attribute("auth.usuario.role", perfil.role)
    span.set_attribute("auth.usuario.ultimo_login", perfil.ultimo_login.isoformat())

    tempo_parcial = time.time()
    validar_credenciais(usuario_id)
    span.set_attribute("auth.tempo_validacao_ms", (time.time() - tempo_parcial) * 1000)

Cuidados com concorrência: Em spans ativos em múltiplas threads, use locks ou garanta acesso serializado aos attributes.

5. Attributes Semânticos Padrão vs. Customizados

O OpenTelemetry mantém um catálogo de attributes semânticos (semconv) para operações comuns como HTTP, banco de dados e mensageria. Exemplos:

# Attributes semânticos padrão para HTTP
span.set_attribute("http.method", "POST")
span.set_attribute("http.url", "/api/pedidos")
span.set_attribute("http.status_code", 201)

Quando criar attributes customizados:
- Domínios específicos do negócio (e-commerce, fintech, saúde)
- Métricas proprietárias de performance
- Contexto organizacional (time, projeto, versão)

Exemplo para e-commerce:

with tracer.start_as_current_span("calcular_frete") as span:
    span.set_attribute("frete.cep_origem", "01310-100")
    span.set_attribute("frete.cep_destino", "20040-020")
    span.set_attribute("frete.peso_kg", 2.5)
    span.set_attribute("frete.modalidade", "sedex")
    span.set_attribute("frete.prazo_dias", 3)
    span.set_attribute("frete.valor_calculado", 45.90)

6. Boas Práticas para Nomenclatura e Cardinalidade

Regras de nomenclatura:
- Apenas letras minúsculas, dígitos e underscores
- Namespaces separados por ponto (máximo 3 níveis)
- Evitar caracteres especiais e espaços

Cardinalidade: Attributes com alta cardinalidade (valores únicos por requisição como UUIDs, timestamps precisos ou tokens JWT) podem inflar o armazenamento e degradar performance de consultas.

# ❌ Evitar: alta cardinalidade
span.set_attribute("request.uuid", "a1b2c3d4-e5f6-7890-1234-567890abcdef")
span.set_attribute("request.timestamp_ns", str(time.time_ns()))

# ✅ Preferir: cardinalidade controlada
span.set_attribute("request.tipo", "checkout")
span.set_attribute("request.regiao", "sul")
span.set_attribute("request.canal", "mobile_app")

Limite recomendado: Mantenha até 50 attributes por span. Acima disso, considere usar events ou criar spans filhos.

7. Visualizando e Filtrando Attributes no Backend de Observabilidade

Attributes customizados aparecem diretamente nas interfaces de ferramentas como Jaeger, SigNoz e Grafana Tempo.

Exemplo no SigNoz (consulta SQL-like):

SELECT * FROM spans 
WHERE attributes['frete.modalidade'] = 'sedex' 
AND attributes['frete.valor_calculado'] > 30.0

Exemplo no Grafana Tempo com TraceQL:

{ span.frete.modalidade = "sedex" && span.frete.prazo_dias <= 2 }

Criação de métricas derivadas: No SigNoz, você pode criar métricas baseadas em attributes:

# Métrica: tempo médio de cálculo de frete por modalidade
rate(span.duration_seconds[5m]) by (frete.modalidade)

Alertas baseados em attributes:

# Alertar quando frete exceder 100 reais
sum(rate(span.count[1m])) by (frete.valor_calculado) > 100

8. Testando e Depurando a Implementação de Attributes

Use exportadores console para validação local:

from opentelemetry.sdk.trace.export import ConsoleSpanExporter

console_exporter = ConsoleSpanExporter()
provider.add_span_processor(BatchSpanProcessor(console_exporter))

Saída JSON esperada:

{
    "name": "processar_pagamento",
    "context": {
        "trace_id": "0xabc123...",
        "span_id": "0xdef456..."
    },
    "attributes": {
        "pagamento.parcelas": 6,
        "pagamento.bandeira": "visa",
        "pagamento.valor_parcela": 49.98,
        "pagamento.aprovado": true
    },
    "status": {
        "code": "OK"
    }
}

Erros comuns a evitar:

# ❌ Tipos inválidos (objetos não serializáveis)
span.set_attribute("usuario.dados", {"nome": "João"})  # Objeto dict não aceito

# ✅ Converter para string ou tipo primitivo
span.set_attribute("usuario.nome", "João")
span.set_attribute("usuario.dados_json", json.dumps({"nome": "João"}))

# ❌ Chaves duplicadas (sobrescrita silenciosa)
span.set_attribute("transacao.status", "pendente")
span.set_attribute("transacao.status", "confirmado")  # Sobrescreve o anterior

# ✅ Decidir o valor final antes de setar
status_final = "confirmado" if aprovado else "recusado"
span.set_attribute("transacao.status", status_final)

Conclusão

Implementar span attributes customizados no OpenTelemetry transforma dados brutos de tracing em informações acionáveis para diagnóstico e otimização de sistemas. A chave está em equilibrar riqueza semântica com boas práticas de cardinalidade e nomenclatura. Attributes bem estruturados permitem desde debugging rápido até criação de dashboards e alertas inteligentes baseados em contexto de negócio.

Referências