Considere o seguinte cenário: o dashboard de monitoramento mostra que a latência P95 da API aumentou de 120ms para 850ms às 15h43. Os logs de erro não mostram nada anormal — as requisições estão completando com sucesso, apenas lentamente. A taxa de erros continua em zero. O time sabe que há um problema, mas não sabe onde.
A API chama um serviço de autenticação, que por sua vez consulta o banco de dados de usuários. Ela também chama um serviço de inventário, que consulta outro banco de dados e um cache Redis. Uma única requisição ao endpoint /checkout pode envolver seis serviços diferentes e doze chamadas de rede.
Com métricas e logs, é possível saber que o problema existe. Com rastreamento distribuído, é possível saber exatamente onde ele está — qual serviço, qual operação, qual linha de código contribuiu com mais tempo para aquela requisição de 850ms.
O rastreamento distribuído captura o caminho completo de uma requisição através de todos os serviços que ela toca, atribuindo a cada segmento um intervalo de tempo chamado span. O conjunto de spans de uma requisição forma um trace — uma árvore que visualiza a causalidade e a temporalidade de toda a operação.
OpenTelemetry: O Padrão da Indústria
Durante anos, cada fornecedor de observabilidade — Datadog, New Relic, Jaeger, Zipkin — tinha seu próprio SDK proprietário. Instrumentar uma aplicação significava depender de um fornecedor específico. Migrar para outro exigia reinstrumentar todo o código.
O OpenTelemetry resolve esse problema definindo um padrão aberto e agnóstico de fornecedor para coleta de telemetria — métricas, logs e traces. Uma aplicação instrumentada com OpenTelemetry pode exportar dados para Jaeger, Zipkin, Datadog, Grafana Tempo ou qualquer outro backend sem alterar o código da aplicação.
A arquitetura do OpenTelemetry tem três componentes principais:
SDK — a biblioteca que roda dentro da aplicação e coleta telemetria. Disponível para Node.js, Python, Java, Go, .NET e outras linguagens.
API — a interface de programação usada para criar spans e registrar atributos. Separada do SDK para que bibliotecas possam ser instrumentadas sem depender de uma implementação específica.
Collector — um processo independente que recebe telemetria de múltiplas aplicações, processa e exporta para um ou mais backends. Atua como um proxy de telemetria.
Instrumentando uma Aplicação Node.js
A instrumentação automática do OpenTelemetry para Node.js captura traces de HTTP, banco de dados, Redis e dezenas de outras bibliotecas sem modificar o código da aplicação. A instrumentação manual complementa com spans específicos do domínio do negócio.
Instalação das dependências:
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
Arquivo de instrumentação — deve ser carregado antes de qualquer outro módulo:
// src/instrumentation.js
'use strict';
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
// Define os atributos do recurso — identificam a aplicação nos traces
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'minha-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION || '0.0.0',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});
// Exportador de traces — envia para o OpenTelemetry Collector
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|| 'http://otel-collector:4318/v1/traces',
});
// Exportador de métricas
const metricExporter = new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
|| 'http://otel-collector:4318/v1/metrics',
});
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 15000,
}),
instrumentations: [
getNodeAutoInstrumentations({
// Instrumenta automaticamente: HTTP, Express, PostgreSQL,
// Redis, MongoDB, gRPC e dezenas de outras bibliotecas
'@opentelemetry/instrumentation-http': {
enabled: true,
// Ignora o endpoint de métricas para não poluir os traces
ignoreIncomingRequestHook: (req) => {
return req.url === '/metrics' || req.url === '/health';
},
},
'@opentelemetry/instrumentation-express': {
enabled: true,
},
'@opentelemetry/instrumentation-pg': {
enabled: true,
// Inclui o texto das queries nos spans — cuidado com dados sensíveis
addSqlCommenterCommentToQueries: true,
},
'@opentelemetry/instrumentation-redis-4': {
enabled: true,
},
'@opentelemetry/instrumentation-fs': {
// Desabilitado — gera volume excessivo de spans
enabled: false,
},
}),
],
});
// Inicia o SDK antes de qualquer código da aplicação
sdk.start();
// Garante encerramento gracioso ao desligar
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OpenTelemetry encerrado'))
.catch((err) => console.error('Erro ao encerrar OpenTelemetry', err))
.finally(() => process.exit(0));
});
Carregando a instrumentação antes da aplicação:
// package.json
{
"scripts": {
"start": "node --require ./src/instrumentation.js src/server.js",
"dev": "nodemon --require ./src/instrumentation.js src/server.js"
}
}
Instrumentação Manual com Spans Customizados
A instrumentação automática captura as camadas de infraestrutura — HTTP, banco de dados, cache. A instrumentação manual adiciona contexto do domínio do negócio — operações específicas, regras de negócio, fluxos complexos:
// src/services/pedido.service.js
const { trace, SpanStatusCode, context } = require('@opentelemetry/api');
const tracer = trace.getTracer('minha-api.pedido-service');
class PedidoService {
async processarPedido(pedidoId, usuarioId) {
// Cria um span para toda a operação de processamento
return tracer.startActiveSpan('pedido.processar', async (span) => {
try {
// Adiciona atributos que ficam visíveis no trace
span.setAttributes({
'pedido.id': pedidoId,
'usuario.id': usuarioId,
'pedido.operacao': 'processamento-completo',
});
// Validação do pedido — span filho
const pedido = await this.validarPedido(pedidoId);
span.setAttributes({
'pedido.valor_total': pedido.valorTotal,
'pedido.qtd_itens': pedido.itens.length,
});
// Verificação de estoque — span filho
await this.verificarEstoque(pedido.itens);
// Processamento de pagamento — span filho
const pagamento = await this.processarPagamento(pedido);
span.setAttributes({
'pagamento.id': pagamento.id,
'pagamento.metodo': pagamento.metodo,
});
// Atualização do banco de dados — capturada automaticamente pelo pg
await this.salvarPedidoProcessado(pedido, pagamento);
// Notificação — span filho
await this.notificarCliente(usuarioId, pedido);
span.setStatus({ code: SpanStatusCode.OK });
return { pedidoId, status: 'processado', pagamentoId: pagamento.id };
} catch (error) {
// Registra o erro no span — aparece destacado no Jaeger/Tempo
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
async validarPedido(pedidoId) {
return tracer.startActiveSpan('pedido.validar', async (span) => {
try {
span.setAttribute('pedido.id', pedidoId);
const pedido = await this.db.query(
'SELECT * FROM pedidos WHERE id = $1',
[pedidoId]
);
if (!pedido) {
throw new Error(`Pedido ${pedidoId} não encontrado`);
}
if (pedido.status !== 'pendente') {
throw new Error(`Pedido ${pedidoId} não está pendente`);
}
span.setAttribute('pedido.status_atual', pedido.status);
return pedido;
} finally {
span.end();
}
});
}
async verificarEstoque(itens) {
return tracer.startActiveSpan('estoque.verificar', async (span) => {
try {
span.setAttribute('estoque.qtd_itens', itens.length);
const resultados = await Promise.all(
itens.map(item => this.redis.get(`estoque:${item.produtoId}`))
);
const itensInsuficientes = itens.filter((item, idx) => {
const estoqueAtual = parseInt(resultados[idx] || '0');
return estoqueAtual < item.quantidade;
});
if (itensInsuficientes.length > 0) {
span.setAttribute(
'estoque.itens_insuficientes',
itensInsuficientes.map(i => i.produtoId).join(',')
);
throw new Error(`Estoque insuficiente para ${itensInsuficientes.length} item(s)`);
}
span.setAttribute('estoque.resultado', 'disponivel');
return true;
} finally {
span.end();
}
});
}
}
module.exports = PedidoService;
Propagação de Contexto entre Serviços
Para que o rastreamento seja realmente distribuído — conectando spans de diferentes serviços em um único trace — o contexto do trace precisa ser propagado nas chamadas entre serviços. O OpenTelemetry faz isso automaticamente via headers HTTP quando a instrumentação automática está ativa, usando o formato W3C TraceContext:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ versão ^^ trace-id (128 bits) ^^ span-id (64 bits) ^^ flags
Para verificar que a propagação está funcionando, pode-se inspecionar os headers de uma requisição de saída:
// Exemplo de chamada entre serviços — a instrumentação automática
// injeta os headers de trace automaticamente
const axios = require('axios');
async function chamarServicoDeInventario(produtoId) {
// A instrumentação HTTP injeta automaticamente:
// traceparent: 00-abc123...-def456...-01
// tracestate: (vazio ou com dados do vendor)
const response = await axios.get(
`http://servico-inventario/api/produtos/${produtoId}`
);
return response.data;
}
Para chamadas que não usam HTTP — mensagens em filas, por exemplo — a propagação precisa ser manual:
// Propagando contexto em mensagens para uma fila
const { context, propagation } = require('@opentelemetry/api');
async function publicarEventoPedidoCriado(pedido) {
return tracer.startActiveSpan('fila.publicar', async (span) => {
try {
// Extrai os headers de propagação do contexto atual
const headers = {};
propagation.inject(context.active(), headers);
// Inclui os headers na mensagem da fila
await filaService.publicar('pedidos.criados', {
payload: pedido,
tracingHeaders: headers, // <-- propagação do trace
});
} finally {
span.end();
}
});
}
// No consumidor da fila — extrai o contexto do trace
async function consumirEventoPedidoCriado(mensagem) {
// Restaura o contexto do trace a partir dos headers da mensagem
const contextoParent = propagation.extract(
context.active(),
mensagem.tracingHeaders
);
return context.with(contextoParent, () => {
return tracer.startActiveSpan('fila.consumir', async (span) => {
try {
span.setAttribute('fila.nome', 'pedidos.criados');
await processarPedidoCriado(mensagem.payload);
} finally {
span.end();
}
});
});
}
Configurando o OpenTelemetry Collector
O Collector recebe telemetria das aplicações, processa e exporta para múltiplos backends simultaneamente — permitindo enviar traces para o Grafana Tempo para visualização e para o Datadog para alertas, sem alterar as aplicações:
# observabilidade/otel-collector/otel-collector.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Recebe métricas no formato Prometheus (pull)
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 10s
static_configs:
- targets: ['localhost:8888']
processors:
# Remove spans de baixo valor para reduzir volume
filter/drop_health_checks:
traces:
span:
- 'attributes["http.route"] == "/health"'
- 'attributes["http.route"] == "/metrics"'
# Adiciona atributos do recurso a todos os spans
resource:
attributes:
- action: insert
key: collector.version
value: "0.91.0"
# Agrupa spans em lotes antes de exportar — melhora a performance
batch:
send_batch_size: 1024
timeout: 5s
send_batch_max_size: 2048
# Limita o uso de memória do collector
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
exporters:
# Exporta traces para o Grafana Tempo
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Exporta métricas para o Prometheus
prometheus:
endpoint: "0.0.0.0:8889"
namespace: otelcol
# Log dos traces no stdout — útil para debugging
logging:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter/drop_health_checks, resource, batch]
exporters: [otlp/tempo, logging]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheus]
telemetry:
logs:
level: warn
Adicionando o Grafana Tempo ao Stack
O Grafana Tempo é o backend de traces do ecossistema Grafana — gratuito, open source e integrado nativamente com o Grafana:
# Adicionando ao docker-compose.observabilidade.yml
tempo:
image: grafana/tempo:2.4.0
container_name: tempo
command: ["-config.file=/etc/tempo.yml"]
volumes:
- ./observabilidade/tempo/tempo.yml:/etc/tempo.yml
- tempo_data:/tmp/tempo
ports:
- "3200:3200" # HTTP
- "4317:4317" # OTLP gRPC
restart: unless-stopped
otel-collector:
image: otel/opentelemetry-collector-contrib:0.91.0
container_name: otel-collector
volumes:
- ./observabilidade/otel-collector/otel-collector.yml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus metrics
depends_on:
- tempo
restart: unless-stopped
volumes:
tempo_data:
# observabilidade/tempo/tempo.yml
stream_over_http_enabled: true
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
max_block_duration: 5m
compactor:
compaction:
block_retention: 168h # 7 dias
storage:
trace:
backend: local
local:
path: /tmp/tempo/blocks
wal:
path: /tmp/tempo/wal
# Gera métricas a partir dos traces — span metrics
metrics_generator:
registry:
external_labels:
source: tempo
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
send_exemplars: true
processors:
- service-graphs
- span-metrics
Correlacionando Logs, Métricas e Traces no Grafana
A observabilidade completa se realiza quando os três pilares estão conectados — é possível ir de um alerta de latência alta para os traces afetados, e de um trace com erro para os logs específicos daquela requisição.
No Grafana, essa correlação é configurada via derived fields no datasource do Loki e exemplars no Prometheus:
# Datasource do Loki com link para traces
- name: Loki
type: loki
url: http://loki:3100
jsonData:
derivedFields:
- matcherRegex: '"traceId":"([a-f0-9]{32})"'
name: TraceID
url: '$${__value.raw}'
datasourceUid: tempo
urlDisplayLabel: 'Ver trace no Tempo'
Para que os logs contenham o trace ID, a aplicação deve injetá-lo automaticamente:
// src/middleware/logging.js — versão com trace correlation
const { trace, context } = require('@opentelemetry/api');
const logger = require('../logger');
function loggingMiddleware(req, res, next) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
// Cria um logger filho com os IDs do trace injetados
req.log = logger.child({
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
requestId: req.headers['x-request-id'],
});
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
req.log.info({
msg: 'Requisição concluída',
method: req.method,
url: req.url,
status: res.statusCode,
duration,
});
});
next();
}
Com essa configuração, cada entrada de log contém o traceId da requisição. No Grafana, ao visualizar um log de erro, um clique no link "Ver trace no Tempo" abre o trace completo daquela requisição — com todos os spans, timings e atributos — conectando os três pilares em um único fluxo de investigação.
O Que Vem a Seguir
O próximo artigo fecha o tema abordando alertas inteligentes e cultura de resposta a incidentes — como estruturar runbooks, como conduzir postmortems sem culpa e como usar os dados de observabilidade para melhorar o sistema de forma contínua.
Referências para Aprofundamento
Documentação oficial
- OpenTelemetry Documentation — opentelemetry.io — Documentação completa do OpenTelemetry, incluindo guias de instrumentação para todas as linguagens suportadas e referência da especificação.
- OpenTelemetry JS — GitHub — Repositório oficial do SDK JavaScript, com exemplos de instrumentação automática e manual para Node.js.
- Grafana Tempo Documentation — grafana.com — Documentação do Grafana Tempo, cobrindo configuração, ingestão de traces e integração com Prometheus e Loki.
Conceitos e arquitetura
- OpenTelemetry Collector — opentelemetry.io — Documentação do Collector, incluindo configuração de receivers, processors e exporters para múltiplos backends.
- W3C TraceContext Specification — w3.org — Especificação oficial do formato de propagação de contexto usado pelo OpenTelemetry entre serviços HTTP.
- OpenTelemetry — Observability Primer: https://opentelemetry.io/docs/concepts/observability-primer/
Exercícios
Exercício 1
No cenário de abertura, a latência P95 saltou de 120 ms para 850 ms, mas a taxa de erros é zero e os logs não mostram nada anormal. Por que métricas e logs são insuficientes aqui, e o que exatamente um trace acrescenta? Defina span e trace.
Ver resposta
✓ Resposta: Porque as métricas mostram o sintoma agregado — a latência subiu — sem indicar onde o tempo foi gasto. E os logs, embora detalhem eventos individuais, estão espalhados por serviços diferentes e sem nada que os conecte: cada um registra que sua parte concluiu com sucesso, o que é verdade. Nenhum dos dois responde "qual dos seis serviços consumiu os 730 ms extras".
O trace acrescenta a decomposição temporal e causal de uma requisição específica: quanto tempo cada serviço levou, quais chamadas foram feitas em sequência e quais em paralelo, e onde está o trecho que domina o total. É a diferença entre saber que a requisição demorou e ver que 700 ms foram numa consulta ao banco do serviço de inventário.
Um span é o intervalo de tempo de uma operação — uma chamada HTTP, uma query, um método de negócio —, com início, fim e atributos. Um trace é o conjunto de spans de uma mesma requisição, organizados como uma árvore que expressa quem chamou quem.
Exercício 2
Qual problema o OpenTelemetry resolve em relação aos SDKs proprietários? Explique a divisão entre API, SDK e Collector, e por que separar API de SDK importa para autores de bibliotecas.
Ver resposta
✓ Resposta: Ele acaba com o aprisionamento ao fornecedor. Antes, instrumentar significava adotar o SDK do Datadog, do New Relic ou do Jaeger, e trocar de ferramenta exigia reinstrumentar toda a aplicação. Com o OpenTelemetry, o código é instrumentado uma vez segundo um padrão aberto e o destino da telemetria vira configuração — Tempo, Jaeger, Datadog ou vários ao mesmo tempo.
- API — a interface usada para criar spans e registrar atributos.
- SDK — a implementação que roda na aplicação, coleta e exporta a telemetria.
- Collector — processo separado que recebe telemetria de várias aplicações, processa e exporta para um ou mais backends; funciona como proxy de telemetria.
A separação entre API e SDK importa porque permite que bibliotecas de terceiros sejam instrumentadas sem impor uma implementação. Uma biblioteca depende apenas da API; se a aplicação que a consome não configurou nenhum SDK, as chamadas viram operações vazias, sem custo e sem erro. Fosse tudo um pacote só, instrumentar uma biblioteca significaria arrastar um coletor inteiro para dentro de todo projeto que a usasse.
Exercício 3
O script de start é node --require ./src/instrumentation.js src/server.js. Por que a instrumentação precisa ser carregada antes do código da aplicação? O que aconteceria com require('./instrumentation') na primeira linha do server.js?
Ver resposta
✓ Resposta: Porque a instrumentação automática funciona por monkey patching: o SDK intercepta o carregamento de módulos como http, express, pg e redis e envolve suas funções com código que cria spans. Para conseguir isso, ele precisa estar ativo antes de esses módulos serem carregados — depois, as referências já foram resolvidas e distribuídas pela aplicação, e o patch não alcança mais o que já está em uso.
Colocar require('./instrumentation') na primeira linha do server.js parece equivalente, mas frequentemente não é. O Node resolve todos os require do topo do arquivo antes de executar qualquer linha, e basta que algum módulo importado logo abaixo já traga o express ou o driver do banco para que eles sejam carregados sem patch. O resultado é o pior caso possível: a aplicação sobe normalmente e alguns spans simplesmente não aparecem — uma falha silenciosa, que se manifesta como lacunas no trace.
O --require elimina essa ambiguidade: o Node carrega o arquivo indicado antes de tocar no ponto de entrada. Pela mesma razão o sdk.start() é chamado no corpo do módulo, e não exportado para ser invocado depois.
Exercício 4
Decomponha o header abaixo. Por que chamadas HTTP entre serviços não precisam de código extra para manter o trace unido, mas mensagens em fila precisam de propagation.inject e propagation.extract?
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Ver resposta
✓ Resposta: O header segue o padrão W3C TraceContext e tem quatro partes: 00 é a versão do formato; 4bf92f35...4736 é o trace-id de 128 bits, que identifica a requisição inteira e é o mesmo em todos os serviços; 00f067aa0ba902b7 é o span-id de 64 bits do span que está fazendo a chamada, e que será o pai do span criado do outro lado; 01 são as flags, indicando aqui que o trace foi amostrado e deve ser registrado.
Chamadas HTTP não precisam de código extra porque a instrumentação automática de HTTP já intercepta as requisições de saída e injeta esse header sozinha, além de lê-lo nas requisições de entrada. Do ponto de vista de quem escreve o axios.get(...), nada muda — e é isso que mantém os serviços conectados no mesmo trace.
Filas quebram esse mecanismo por duas razões. A primeira é que não há headers HTTP a interceptar: a mensagem tem o formato que a aplicação definir. A segunda, mais profunda, é que produtor e consumidor são desacoplados no tempo — a mensagem pode ser consumida segundos ou horas depois, quando o contexto de execução do produtor já não existe. Por isso o contexto precisa ser serializado explicitamente com propagation.inject, viajar como dado dentro da mensagem (tracingHeaders), e ser restaurado no consumidor com propagation.extract seguido de context.with(...), que reativa aquele contexto para que o novo span nasça vinculado ao trace original.
Exercício 5
No pipeline de traces do Collector, os processors aparecem nesta ordem: [memory_limiter, filter/drop_health_checks, resource, batch]. Explique a função de cada um e por que a ordem importa — em especial por que memory_limiter vem primeiro e batch por último.
Ver resposta
✓ Resposta: O memory_limiter protege o próprio Collector, recusando dados quando o uso de memória se aproxima do limite (512 MiB, com folga de pico de 128 MiB). O filter/drop_health_checks descarta spans de /health e /metrics, que são altíssimos em volume e nulos em valor diagnóstico. O resource acrescenta atributos comuns a todos os spans, como collector.version. O batch agrupa spans em lotes de até 1024 antes de exportar.
A ordem é a própria lógica do pipeline. O memory_limiter vem primeiro porque é uma proteção: de nada adianta limitar memória depois de já ter alocado tudo processando dados que serão recusados adiante. Ele precisa ser a primeira barreira, para que a rejeição aconteça antes de qualquer trabalho.
O filter vem logo em seguida por eficiência: descartar cedo significa que os processors seguintes trabalham sobre um volume menor — não faz sentido enriquecer com atributos um span que será jogado fora.
O batch fica por último porque agrupa o resultado final para exportação. Colocá-lo antes do filtro produziria lotes cheios de spans a descartar, desperdiçando a montagem; e antes do resource, os atributos teriam de ser aplicados a lotes já formados. Como regra: proteger, reduzir, enriquecer e só então empacotar.