← Monitorização e Observabilidade: medir e investigar
09 / 12 · 60 MIN

Collector: configuração e transformação

Liga componentes, interpreta a ordem de processamento e verifica transformações com um caso de controlo.

Da definição à pipeline executada

No caso fictício Cedro, a equipa define attributes/sanitize para remover um marcador do span. A validação passa, mas o marcador continua no console. A lista processors da pipeline contém apenas batch: o componente foi definido, mas não foi ligado. Separa estas duas condições ao ler a configuração. Confirma também distribuição e versão, porque os binários do Collector podem incluir componentes diferentes. O nosso exercício fixa otelcol-contrib 0.162.0. O nome de uma instância usa tipo e, opcionalmente, uma designação, como attributes/prepare. A referência tem de corresponder à definição. Uma referência inexistente deve ser corrigida antes de arrancar o fluxo. Validação estática é uma primeira barreira, mas não confirma receção nem o conteúdo exportado. Regista o caminho esperado: emissor, protocolo, endpoint, receiver, processors pela ordem declarada e exporter. Esta sequência ajuda a localizar a primeira fronteira onde a observação diverge do esperado.

A ordem e a ação fazem diferença

O span sintético começa com demo.stage=raw e demo.keep=original. O processor prepare faz upsert de stage para prepared, tenta insert de keep com outro valor e update de uma chave ausente. O resultado conserva keep=original e não cria a chave ausente. No processor seguinte, sanitize copia stage para demo.copy e remove demo.secret. Se prepare executar primeiro, a cópia contém prepared; se sanitize executar primeiro, contém raw, embora stage seja alterado mais tarde. Não existe atualização automática da cópia. Esta distinção evita diagnosticar como falha de exportação um resultado explicado pela sequência. A remoção de uma chave de span não demonstra remoção de atributos de recurso, corpos de logs ou outras pipelines. O teste usa um marcador completamente fictício. Não coloques dados de clientes no debug exporter para repetir o exercício. Define a cobertura pretendida e prepara casos apropriados para cada localização e caminho, com as condições de acesso aplicáveis.

Batch, debug e limites da evidência

O batch processor agrupa itens para envio. send_batch_size é um trigger, não um máximo rígido; send_batch_max_size define o limite superior quando não é zero e tem de ser pelo menos o trigger. O laboratório rejeita a combinação três e dois. Num desenho com memory_limiter e sampling, a documentação recomenda agrupar depois desses tratamentos; esta oficina não executa pressão de memória nem sampling. O debug exporter detailed permite inspecionar a saída local, mas o seu formato não é uma API estável. Por isso, as verificações de texto estão ligadas à versão fixada. Uma resposta OTLP HTTP 200 e um contador de spans aceites demonstram uma fronteira de receção; não demonstram armazenamento remoto, pesquisa no fornecedor ou resultado do pagamento. Se o emissor usa HTTP e o receiver só aceita gRPC naquela porta, começa pela compatibilidade de transporte. Aumentar sampling não corrige esse desacordo.

Laboratório local com controlo

Obtém o binário oficial adequado ao teu sistema e verifica a versão 0.162.0. Define DR_OTELCOL com o caminho do binário, copia o código completo abaixo para run.py e executa com Python 3.13. O código não descarrega software: cria configurações temporárias e inicia três processos, um de cada vez, ligados apenas a 127.0.0.1. Envia um span fictício e consulta métricas internas. O primeiro caso mantém o processor desligado e confirma que o marcador chega; o segundo verifica a transformação; o terceiro inverte a ordem. Duas configurações inválidas são rejeitadas. Os processos são terminados e os ficheiros temporários removidos no fim. Foram executadas 23 verificações no ambiente de autoria. O artefacto descarregado foi comparado com o digest da release oficial; isso não é uma verificação independente da assinatura Sigstore. O laboratório não valida Dynatrace, persistência, saturação, failover, notificações ou uma pipeline de produção.

"""Original loopback-only synthetic OTLP lab for otelcol-contrib 0.162.0.
PT: definir DR_OTELCOL para o binário oficial e executar este código completo.
EN: set DR_OTELCOL to the official binary and run this complete script.
No production credentials, agents, external export, or customer data are used.
"""
import copy
import hashlib
import json
import os
from pathlib import Path
import socket
import subprocess
import tempfile
import time
import urllib.request

BINARY = os.environ.get('DR_OTELCOL')
if not BINARY:
    raise SystemExit('Set DR_OTELCOL to otelcol-contrib 0.162.0')
BINARY = str(Path(BINARY).resolve())
version = subprocess.check_output([BINARY, '--version'], text=True).strip()
if version != 'otelcol-contrib version 0.162.0':
    raise SystemExit('This lab requires otelcol-contrib 0.162.0')
checks = []
def check(name, condition):
    if not condition:
        raise AssertionError(name)
    checks.append(name)

def port():
    with socket.socket() as s:
        s.bind(('127.0.0.1', 0))
        return s.getsockname()[1]

def config(receiver_port, metrics_port, processors):
    return {
        'receivers': {'otlp/lab': {'protocols': {'http': {'endpoint': f'127.0.0.1:{receiver_port}'}}}},
        'processors': {
            'attributes/prepare': {'actions': [
                {'key':'demo.stage','action':'upsert','value':'prepared'},
                {'key':'demo.keep','action':'insert','value':'replacement'},
                {'key':'demo.absent','action':'update','value':'must-not-appear'}]},
            'attributes/sanitize': {'actions': [
                {'key':'demo.copy','action':'upsert','from_attribute':'demo.stage'},
                {'key':'demo.secret','action':'delete'}]},
            'batch': {'timeout':'100ms','send_batch_size':1,'send_batch_max_size':2}},
        'exporters': {'debug/lab': {'verbosity':'detailed'}},
        'service': {
            'telemetry': {'metrics': {'readers': [{'pull': {'exporter': {'prometheus': {
                'host':'127.0.0.1','port':metrics_port}}}}]}},
            'pipelines': {'traces': {'receivers':['otlp/lab'],'processors':processors,'exporters':['debug/lab']}}}}

def payload():
    attrs = [('demo.secret','SYNTHETIC_ONLY'),('demo.stage','raw'),('demo.keep','original')]
    return {'resourceSpans':[{'resource':{'attributes':[{'key':'service.name','value':{'stringValue':'dr-fictional-lab'}}]},
        'scopeSpans':[{'scope':{'name':'dr-authored-lab'},'spans':[{
            'traceId':'11111111111111111111111111111111','spanId':'2222222222222222',
            'name':'fictional-position-query','kind':2,
            'startTimeUnixNano':'1791030000000000000','endTimeUnixNano':'1791030000001000000',
            'attributes':[{'key':k,'value':{'stringValue':v}} for k,v in attrs]}]}]}]}

def validate(tmp, cfg, name, expected):
    file = tmp / (name+'.json')
    file.write_text(json.dumps(cfg))
    result = subprocess.run([BINARY,'validate','--config',str(file)],capture_output=True,text=True,timeout=15)
    check(name+' validation', (result.returncode == 0) == expected)
    return file

def run_case(tmp, name, processors):
    rp, mp = port(), port()
    while rp == mp:
        mp = port()
    cfg = config(rp,mp,processors)
    file = validate(tmp,cfg,name,True)
    log = tmp / (name+'.log')
    with log.open('w') as output:
        process = subprocess.Popen([BINARY,'--config',str(file)],stdout=output,stderr=output)
        try:
            for _ in range(100):
                if process.poll() is not None:
                    raise RuntimeError('Collector exited: '+log.read_text())
                if 'Everything is ready' in log.read_text():
                    break
                time.sleep(.05)
            else:
                raise RuntimeError('Collector startup timeout')
            request = urllib.request.Request(f'http://127.0.0.1:{rp}/v1/traces',
                data=json.dumps(payload()).encode(),headers={'Content-Type':'application/json'})
            with urllib.request.urlopen(request,timeout=5) as response:
                check(name+' OTLP accepted', response.status == 200)
                body=json.loads(response.read())
                check(name+' no partial rejection', not body.get('partialSuccess',{}).get('rejectedSpans',0))
            for _ in range(100):
                text = log.read_text()
                if 'fictional-position-query' in text:
                    break
                time.sleep(.05)
            else:
                raise RuntimeError('Debug export timeout')
            with urllib.request.urlopen(f'http://127.0.0.1:{mp}/metrics',timeout=5) as response:
                metrics=response.read().decode()
            check(name+' internal receiver signal', 'otelcol_receiver_accepted_spans' in metrics)
            return text
        finally:
            if process.poll() is None:
                process.terminate()
                try:
                    process.wait(timeout=10)
                except subprocess.TimeoutExpired:
                    process.kill();process.wait(timeout=5)

with tempfile.TemporaryDirectory(prefix='dr-otel-pipeline-') as directory:
    tmp=Path(directory)
    raw=run_case(tmp,'defined-but-unwired',['batch'])
    check('unwired processor does not delete', 'demo.secret: Str(SYNTHETIC_ONLY)' in raw)
    clean=run_case(tmp,'ordered',['attributes/prepare','attributes/sanitize','batch'])
    check('wired deletion', 'demo.secret' not in clean and 'SYNTHETIC_ONLY' not in clean)
    check('ordered copy', 'demo.copy: Str(prepared)' in clean)
    check('insert preserves existing', 'demo.keep: Str(original)' in clean)
    check('update does not insert', 'demo.absent' not in clean)
    reverse=run_case(tmp,'reversed',['attributes/sanitize','attributes/prepare','batch'])
    check('processor order changes copied value', 'demo.copy: Str(raw)' in reverse)
    check('reversed still deletes selected attribute', 'demo.secret' not in reverse)
    check('resource attribute retained', 'service.name: Str(dr-fictional-lab)' in clean)
    invalid=config(port(),port(),['attributes/missing'])
    validate(tmp,invalid,'missing-processor-reference',False)
    invalid=config(port(),port(),['batch'])
    invalid['processors']['batch'].update(send_batch_size=3,send_batch_max_size=2)
    validate(tmp,invalid,'batch-trigger-exceeds-max',False)
    check('payload was not mutated', payload()['resourceSpans'][0]['scopeSpans'][0]['spans'][0]['attributes'][0]['value']['stringValue']=='SYNTHETIC_ONLY')
print(json.dumps(dict(version=version,checks_passed=len(checks),checks=checks,
    binary_sha256=hashlib.sha256(Path(BINARY).read_bytes()).hexdigest(),
    scope='Three real local Collector runs and two rejected configurations using only synthetic OTLP traces. No vendor backend, production monitoring, persistence, saturation or failover test.'),indent=2))
NA PRÁTICA

prepare → sanitize produz demo.copy=prepared; sanitize → prepare produz demo.copy=raw. A diferença resulta da ordem, não de um exporter defeituoso.

Armadilhas comuns

Componente definido como ativo; insert como upsert; ordem ignorada; trigger como máximo; debug como prova de armazenamento remoto.

Tópicos relacionados: OpenTelemetry Collector · Tratamento de atributos · Diagnóstico por fronteiras

Leva esta ideia contigo

Valida a configuração e demonstra o comportamento observado com uma versão fixada, um controlo e limites explícitos.

Criar conta

Referência: Collector configuration · Observability 2026-09; selected OpenTelemetry, Prometheus and Dynatrace Classic concepts