Elasticsearch MigrationClickHouse Workshops

03 Executar a migração

Provisione o ClickHouse Cloud, faça gravação dupla pelo OpenTelemetry, valide a paridade, explore o ClickStack e realize a transição.

Execute este módulo a partir do diretório de artefatos do workshop:

cd "$(git rev-parse --show-toplevel)/workshops/elasticsearch_migration_lab/part3"

Objetivo: execute o plano de migração desenvolvido na Parte 2. Implante um OTel Collector, crie tabelas otimizadas do ClickHouse, configure o enriquecimento por meio de dicionários, valide a paridade com o ambiente Elasticsearch em execução e faça a transição.

Tempo estimado: 120–180 minutos

Pré-requisitos: Partes 1 e 2 concluídas; o ambiente Docker da Parte 1 está em execução (Elasticsearch, Kibana, geradores de logs e APM Server). As credenciais da sua conta do ClickHouse Cloud estão disponíveis.

Arquivos:

part3/
├── exercises/
│   ├── setup-checklist.md           ← Fill in as you work through each step
│   └── sql-exercises.md             ← 6 SQL exercises (attempt before checking solutions)
├── clickhouse/
│   ├── schema.sql                   ← All DDL (run first)
│   ├── dictionaries.sql             ← GeoIP dictionary DDL
│   ├── geoip-sample-data.csv        ← ~400 CIDR rows (no MaxMind account needed)
│   ├── alert-tables.sql             ← Alert pre-computation + summary MVs
│   └── validation-queries.sql       ← Spot-checks and parity queries
├── configs/
│   ├── otel-collector-config.parallel.yaml ← File-based log collector — parallel run (CH + ES dual-write)
│   ├── otel-collector-config.cutover.yaml  ← File-based log collector — cutover (CH only)
│   ├── otelcol-demo-config.parallel.yml    ← OTel Demo collector — parallel run (APM + CH)
│   └── otelcol-demo-config.cutover.yml     ← OTel Demo collector — cutover (CH only)
├── docker/
│   ├── docker-compose.otel-demo.parallel.yml  ← Compose override for parallel-run swap
│   └── docker-compose.otel-demo.cutover.yml   ← Compose override for cutover swap
├── diagrams/
│   ├── step3-architecture.mmd       ← Mermaid source — post-Step-3 architecture
│   ├── step3-architecture.png       ← Rendered PNG embedded in Step 3
│   └── render.sh                    ← Re-render *.mmd → *.png via Docker (mermaid-cli)
├── images/
│   └── *.png                        ← Screenshots referenced by hyperdx-guide.md
└── scripts/
    ├── swap-otelcol-demo-config.sh  ← Swap otelcol-demo config (parallel|cutover)
    ├── validate_migration.sh        ← Automated parity check
    └── validate_enrichment.sh       ← Enrichment column verification

Etapa 1: provisionar o ClickHouse Cloud

  1. Cadastre-se em clickhouse.cloud (avaliação gratuita — a camada Basic é suficiente para este laboratório)
  2. Crie um novo serviço — a camada Basic é suficiente para este laboratório
  3. Anote os dados da conexão: host e senha (o usuário é default e a porta para TLS nativo é 9440)
  4. Teste a conectividade:
clickhouse client \
    --host <your-host>.clickhouse.cloud \
    --port 9440 \
    --user default \
    --password <your-password> \
    --secure \
    --query "SELECT version()"

Observação sobre a arquitetura do ClickHouse Cloud: Diferentemente do Elasticsearch, o ClickHouse Cloud armazena todos os dados em armazenamento de objetos (S3/GCS), com cache local automático. Não existe o conceito de camadas de nós hot/warm/cold — o mecanismo de consultas busca e armazena os dados em cache de modo transparente. Portanto, todo o mecanismo hot→warm→cold do ILM do ES não tem equivalente no ClickHouse Cloud. Você precisa apenas de exclusão baseada em TTL, que será configurada na Etapa 2.

Defina as variáveis de ambiente para o restante do laboratório:

# Copy and fill in once, then source before every session
cp ../common/env.sh.example ../common/env.sh
# edit ../common/env.sh with your CH_HOST and CH_PASSWORD (from Cloud console → Connect → Native protocol)
source ../common/env.sh

Etapa 2: criar tabelas e dicionários de destino

Banco de dados: Todos os objetos da Parte 3 (tabelas, dicionários e views materializadas) ficam em um banco de dados otel dedicado — criado automaticamente por dictionaries.sql e schema.sql por meio de CREATE DATABASE IF NOT EXISTS otel. Isso mantém as tabelas do laboratório isoladas do restante do serviço. Todas as configurações de collector, scripts de validação e fontes do HyperDX desta parte já apontam para otel.

Ordem de execução: Os dicionários GeoIP (Etapa 2a) devem ser criados antes das tabelas de destino (Etapa 2b), pois otel_logs_v2 tem colunas MATERIALIZED que fazem referência a otel.geoip_country e otel.geoip_city. O ClickHouse valida as referências a dicionários durante CREATE TABLE.

2a. Carregar dados GeoIP e criar dicionários

# 1. Create the otel database, geoip_data source table, and empty dictionaries
#    (the table must exist before you can INSERT into it)
clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    < clickhouse/dictionaries.sql

# 2. Load sample data into the source table (note: --database otel)
clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    --database otel \
    --query "INSERT INTO geoip_data FORMAT CSVWithNames" \
    < clickhouse/geoip-sample-data.csv

# 3. Reload dictionaries — they were created with an empty table; force reload now
clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    --query "SYSTEM RELOAD DICTIONARY otel.geoip_country"
clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    --query "SYSTEM RELOAD DICTIONARY otel.geoip_city"

# 4. Verify
clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    --query "SELECT dictGet('otel.geoip_country', 'country', toIPv4('8.8.8.8'))"
# Expected: United States

Ponto de ensino: No Elasticsearch, o processador geoip é uma caixa-preta integrada — basta habilitá-lo. No ClickHouse, você usa um dicionário baseado nos mesmos dados do MaxMind, com controle total sobre a fonte de dados, o intervalo de atualização e o comportamento das consultas. O layout IP_TRIE foi criado especificamente para consultas de intervalos CIDR: dictGet() faz a correspondência do prefixo mais longo na tabela de intervalos de IP em microssegundos.

2b. Criar tabelas e view materializada

clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    < clickhouse/schema.sql

Isso cria nove objetos:

ObjetoTipoFinalidade
otel_logsMecanismo NullDestino de ingestão do OTel Collector; não armazena nada
otel_logs_v2MergeTreeOnde os dados realmente ficam, com colunas de enriquecimento
otel_logs_mvView materializadaEncaminha otel_logs → otel_logs_v2 com transformações
otel_tracesMergeTreeSpans de trace do OTel
otel_metrics_gaugeMergeTreeMétricas gauge do OTel (substitui o APM Server como backend de métricas)
otel_metrics_sumMergeTreeMétricas de soma/contador do OTel
otel_metrics_histogramMergeTreeMétricas de histograma do OTel
otel_metrics_exponentialhistogramMergeTreeMétricas de histograma exponencial do OTel
otel_metrics_summaryMergeTreeMétricas de resumo do OTel

Por que usar o padrão Null → MV → destino?

O mecanismo Null aceita inserções, mas descarta os dados imediatamente. A MV anexada é acionada a cada inserção e grava linhas transformadas em otel_logs_v2. Isso evita armazenar os dados duas vezes: o OTel Collector grava o esquema OTel bruto em otel_logs, e somente as linhas enriquecidas e otimizadas chegam a otel_logs_v2.

As colunas materializadas substituem todos os processadores dos pipelines de ingestão do ES:

Processador do ESEquivalente no ClickHouse
geoipGeoCountry, GeoCity MATERIALIZED por meio de dictGetOrDefault('otel.geoip_country', ...)
user_agentBrowserFamily, OSFamily, IsBot MATERIALIZED por meio de regexpExtract/position
script (derivação da severidade)DerivedSeverity MATERIALIZED por meio de multiIf(StatusCode >= 500, 'critical', ...)
grok/dissect (extração de campos)RequestType, RequestPath, RequestPage, HostName MATERIALIZED a partir de LogAttributes['key']
enriquecimento padrão (event.ingested)IngestTime DEFAULT now()

2c. Criar tabelas de pré-cálculo de alertas e de resumo

clickhouse client \
    --host ${CH_HOST} --port 9440 \
    --user default --password ${CH_PASSWORD} --secure \
    < clickhouse/alert-tables.sql

Isso cria:

  • alert_error_rate + alert_error_rate_mv — pré-calculam a taxa de 5xx por minuto no momento da inserção
  • logs_summary_1min + logs_summary_1min_mv — rollup AggregatingMergeTree (substituto das transformações do ES)

Etapa 3: implantar e configurar o OTel Collector

Arquitetura de destino

Ao final desta etapa, você terá passado da linha de base da Parte 1 (Filebeat → ES, otelcol-demo → somente APM) para uma execução paralela, na qual dois OTel Collectors distribuem todos os sinais para ambos os destinos: a stack Elasticsearch existente e o ClickHouse Cloud:

Arquitetura após a Etapa 3

O que muda nesta etapa:

  • 3a: parar o Filebeat (em vermelho, no canto inferior esquerdo do diagrama).
  • 3b–3c: iniciar otelcol-lab (collector baseado em arquivos) — ele acompanha os mesmos arquivos de log que o Filebeat lia e faz gravação dupla no ES (logs-{web_access,application,infrastructure}-lab) e no ClickHouse (otel.otel_logs → MV → otel.otel_logs_v2).
  • 3d: trocar a configuração de otelcol-demo pela versão de execução paralela, para que o tráfego OTLP dos 16 serviços do OTel Demo também seja distribuído aos dois backends (APM Server + ClickHouse).

Fonte do diagrama: diagrams/step3-architecture.mmd. Para renderizar novamente após alterações, execute bash diagrams/render.sh (usa minlag/mermaid-cli por meio do Docker; Node/npm não são necessários).

3a. Parar o Filebeat

O OTel Collector que você está prestes a iniciar acompanha os mesmos arquivos de log que o Filebeat e grava nos mesmos fluxos de dados do ES (logs-web_access-lab, logs-application-lab, logs-infrastructure-lab), além de no ClickHouse. Se o Filebeat e o OTel Collector estiverem em execução, cada linha de log será indexada duas vezes no ES — literalmente o problema de "gravação dupla no mesmo destino".

Pare o Filebeat para que o OTel Collector se torne o único produtor desses fluxos de dados:

docker compose -f ../part1/docker/docker-compose.source.yml stop filebeat
docker compose -f ../part1/docker/docker-compose.source.yml ps filebeat
# Should show: filebeat ... exited

O Elasticsearch, Kibana, os geradores de logs e o APM Server permanecem em execução. Somente o Filebeat para. O OTel Collector assume os dois backends: ele envia cada linha de log ao ClickHouse (o novo backend) e ao Elasticsearch (os fluxos de dados existentes), mantendo as comparações de paridade significativas durante toda a execução paralela. Na transição (Etapa 10a), os exporters do ES são removidos da configuração.

3b. Executar o OTel Collector

Os arquivos de log ficam no volume nomeado do Docker docker_log-data. No macOS (e em qualquer ambiente em que os arquivos de log estejam em volumes do Docker), execute o collector como um contêiner Docker para que ele possa acessar o volume diretamente:

# macOS / Docker volume approach (recommended)
docker run -d \
  --name otelcol-lab \
  --restart unless-stopped \
  --network docker_default \
  -v docker_log-data:/var/log/generators:ro \
  -v "$(pwd)/configs/otel-collector-config.parallel.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  -e CH_HOST="${CH_HOST}" \
  -e CH_PASSWORD="${CH_PASSWORD}" \
  otel/opentelemetry-collector-contrib:0.146.1

Observação sobre dial_timeout: Tanto otel-collector-config.parallel.yaml quanto otel-collector-config.cutover.yaml definem dial_timeout=60s no URI do endpoint do ClickHouse para que o handshake da inicialização a frio tenha tempo suficiente antes da primeira reinicialização. Em um CH auto-hospedado com inicialização imediata, você pode reduzir esse valor novamente para 10s.

Host Linux com acesso direto ao volume: Se você estiver usando Linux e tiver acesso direto aos arquivos de log no host (fora dos volumes do Docker), poderá usar o binário:

wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.146.1/otelcol-contrib_0.146.1_linux_amd64.tar.gz
tar -xzf otelcol-contrib_0.146.1_linux_amd64.tar.gz
CH_HOST=${CH_HOST} CH_PASSWORD=${CH_PASSWORD} ./otelcol-contrib --config configs/otel-collector-config.parallel.yaml

3c. Verificar a inicialização

docker logs otelcol-lab --tail=10

Uma saída íntegra se parece com:

info  Everything is ready. Begin running and processing data.
info  Started watching file  path=/var/log/generators/web-access-api-gateway.log

Principais decisões de configuração em configs/otel-collector-config.parallel.yaml (collector de logs baseado em arquivos — variante de execução paralela):

  • Fan-out de gravação dupla: todos os pipelines listam [clickhouse, elasticsearch/<stream>], portanto cada linha de log é enviada aos dois backends durante a execução paralela. A Etapa 10a remove os exporters do ES na transição.
  • Um exporter do ES por fluxo de dados (elasticsearch/web → logs-web_access-lab, elasticsearch/app → logs-application-lab, elasticsearch/infra → logs-infrastructure-lab), para que cada pipeline grave no mesmo fluxo de dados em que o Filebeat gravava antes.
  • mapping.mode: ecs nos exporters do ES — converte registros no formato OTel (Body, Attributes, ...) em documentos no formato ECS compatíveis com os enviados pelo Filebeat, para que os modelos de componentes do fluxo de dados os aceitem.
  • create_schema: false no exporter do CH — já criamos nossas tabelas otimizadas; o exporter não deve criar seu esquema padrão.
  • logs_table_name: otel_logs — grava na tabela Null, que aciona a MV para otel_logs_v2.
  • compress=lz4 no endpoint do CH — compressão LZ4 durante a transmissão; o ClickHouse Cloud descompacta os dados ao recebê-los.

Observação: O collector do laboratório implantado acima trata apenas logs baseados em arquivos (web/aplicação/infraestrutura). Os 16 serviços do OTel Demo emitem OTLP para um segundo collector (otelcol-demo), provisionado na Parte 1, que atualmente encaminha tudo ao APM Server. A Etapa 3d abaixo muda para uma configuração de gravação dupla sem editar o arquivo de linha de base da Parte 1.

3d. Trocar otelcol-demo pela configuração de execução paralela (gravação dupla em APM + ClickHouse)

O contêiner otelcol-demo iniciado na Parte 1 encaminha traces/métricas/logs OTLP somente ao APM Server. Para executar em paralelo, mude para uma configuração da Parte 3 que distribui os dados ao APM e ao ClickHouse:

source ../common/env.sh   # CH_HOST, CH_PASSWORD must be set
bash scripts/swap-otelcol-demo-config.sh parallel

Isso recria o contêiner usando docker compose -f ... -f docker/docker-compose.otel-demo.parallel.yml up -d --force-recreate --no-deps otelcol-demo. O arquivo de configuração da Parte 1 não é modificado — a substituição monta uma configuração da Parte 3 em outro caminho do contêiner e substitui command: para carregá-la.

Verifique se ele iniciou sem erros:

docker logs "$(docker ps -qf name=otelcol-demo)" --tail=10
# Expected: "Everything is ready. Begin running and processing data."
# No "Failed to start component" or "connection refused" errors.

Por que trocar em vez de editar o arquivo da Parte 1 diretamente? Editar part1/docker/configs/otelcol-demo-config.yml alteraria a linha de base; assim, um futuro cleanup.sh && start from Part 1 levaria a uma configuração que já exige CH_HOST/CH_PASSWORD. O padrão de troca mantém a Parte 1 autocontida e reproduzível.


Etapa 4: validar a execução paralela

Aguarde pelo menos 5 minutos para os dados se acumularem e execute o script de validação automatizada:

bash scripts/validate_migration.sh

O script verifica:

  • Contagens de linhas no ClickHouse e no Elasticsearch (dentro de 5% de tolerância para dados recentes)
  • Cobertura do enriquecimento GeoCountry ≥ 20% (dados de exemplo; o conjunto completo do MaxMind fornece >90%)
  • Status dos dicionários (ambos LOADED)
  • As MVs de alertas e resumos estão sendo preenchidas
  • As tabelas de métricas (otel_metrics_sum) estão recebendo dados do collector do OTel Demo
  • O TTL está configurado

Para uma análise detalhada da qualidade do enriquecimento:

bash scripts/validate_enrichment.sh

Verificação manual de paridade — 10 principais caminhos de requisição:

Execute isto no Elasticsearch:

curl -s "http://localhost:9200/logs-web_access-lab/_search" \
    -H "Content-Type: application/json" \
    -d '{"size":0,"aggs":{"top_paths":{"terms":{"field":"request_path.keyword","size":10}}}}' \
    | jq '.aggregations.top_paths.buckets'

Execute isto no ClickHouse:

SELECT RequestPage AS path, count() AS c
FROM otel_logs_v2
WHERE RequestType != ''
GROUP BY path
ORDER BY c DESC
LIMIT 10;

Os principais caminhos e suas posições relativas devem corresponder.


Etapa 5: explorar seus dados no HyperDX (interface do ClickStack)

O HyperDX é a interface de observabilidade integrada do ClickStack — incluída em todos os serviços do ClickHouse Cloud. Nesta etapa, você iniciará o HyperDX, conectará a interface ao banco de dados otel e confirmará que logs/traces/métricas podem ser consultados pela interface.

→ Siga o guia passo a passo com capturas de tela: hyperdx-guide.md

O guia aborda:

A. Iniciar o ClickStackAbrir o HyperDX na barra lateral do console do Cloud
B. Criar três fontes de dadosConectar o HyperDX a otel.otel_traces, otel.otel_logs_v2 e às cinco tabelas otel.otel_metrics_*
C. Pesquisar logs em tempo realConfirmar que os dados estão fluindo e usar facetas e pesquisa de texto completo
D. Criar um gráfico com o AI AssistantTraduzir linguagem natural ("Contagem de erros por serviço nas últimas 2 horas") em um gráfico funcional

Ponto de ensino: O Service Map, a pesquisa de texto completo e o AI Assistant do HyperDX são executados diretamente nas tabelas MergeTree otel.* — sem índice separado, rollups ou resharding. Os mesmos casos de uso operacionais abordados nos dashboards do Kibana da Parte 1 estão disponíveis aqui, mas as consultas subjacentes também podem ser escritas ad hoc como SQL (consulte os exercícios de SQL da Etapa 9) — algo que o Kibana nunca ofereceu.


Etapa 6: verificar o ciclo de vida dos dados (TTL)

O TTL já está configurado no DDL schema.sql. Verifique se ele está presente:

SELECT name, extractAll(create_table_query, 'TTL[^\\n]+') AS ttl_clauses
FROM system.tables
WHERE database = 'otel'
  AND name IN ('otel_logs_v2', 'otel_traces',
               'otel_metrics_gauge', 'otel_metrics_sum', 'otel_metrics_histogram',
               'otel_metrics_exponentialhistogram', 'otel_metrics_summary');

Verifique os tamanhos e as idades das partições:

SELECT
    partition,
    sum(rows)                              AS total_rows,
    formatReadableSize(sum(bytes_on_disk)) AS disk_size,
    min(min_time)                          AS oldest_data,
    max(max_time)                          AS newest_data
FROM system.parts
WHERE database = 'otel' AND table = 'otel_logs_v2' AND active
GROUP BY partition
ORDER BY partition;

Ponto de ensino: por que o ILM se torna uma linha de DDL

Na Parte 1, você configurou uma política de ILM de 3 fases: rollover em 5 GB/1 dia (hot), shrink + forcemerge em 2 dias (warm) e exclusão em 30 dias. Isso exigiu funções de nós hot/warm, conhecimento da alocação de shards e JSON da política de ILM.

No ClickHouse Cloud, todo esse mecanismo se resume a uma cláusula em CREATE TABLE:

TTL TimestampDate + INTERVAL 30 DAY DELETE
SETTINGS ttl_only_drop_parts = 1
  • Rollover: desnecessário. O ClickHouse usa uma única tabela com partições baseadas em data.
  • Fase warm (shrink + forcemerge): desnecessária. O mecanismo MergeTree mescla as partes automaticamente.
  • Camadas hot/warm/cold: desnecessárias. O ClickHouse Cloud armazena todos os dados em armazenamento de objetos com cache automático. Não há funções de nós.
  • Fase de exclusão: reproduzida exatamente por TTL ... DELETE. ttl_only_drop_parts = 1 descarta partições inteiras (o limite da partição é um dia) — muito mais eficiente do que excluir linha por linha.

Etapa 7: tabela de resumo de agregação (substitui as transformações do ES)

A tabela logs_summary_1min (criada na Etapa 2c) é uma AggregatingMergeTree que armazena estados parciais de agregação. Consulte-a com combinadores -Merge:

SELECT
    minute,
    ServiceName,
    SeverityText,
    countMerge(count)                           AS total_events,
    avgMerge(avg_run_time)                      AS avg_run_time_ms,
    quantileMerge(0.99)(p99_run_time)           AS p99_run_time_ms,
    uniqMerge(uniq_remote_addr)                 AS unique_ips
FROM logs_summary_1min
WHERE minute >= now() - INTERVAL 1 HOUR
GROUP BY minute, ServiceName, SeverityText
ORDER BY minute DESC;

Ponto de ensino: padrão de combinadores State/Merge

A tabela de resumo armazena estados parciais de agregação, não valores finais. countState() armazena uma contagem parcial serializada; avgState() armazena a soma + a contagem necessárias para calcular uma média. Ao consultar com countMerge(), o ClickHouse combina os estados parciais e calcula o valor final.

Esta é a principal diferença em relação às transformações do Elasticsearch: as transformações do ES reagregam os dados brutos periodicamente. O AggregatingMergeTree do ClickHouse acumula novos dados de forma incremental, sem reler registros históricos — fundamentalmente mais eficiente em escala.


Etapa 8: migrar regras de alerta

Você configurou duas regras de alerta do Kibana na Parte 1. Migre-as para uma das opções:

O HyperDX tem uma visualização integrada de Alerts (na barra lateral esquerda, entre Chart Explorer e Client Sessions). Os alertas são associados a pesquisas salvas ou blocos de gráficos: você define uma consulta, um limite, uma janela de avaliação e um canal de notificação. Como as fontes de dados configuradas na Etapa 5 já apontam para o banco de dados otel, nenhuma integração adicional é necessária.

Para conhecer o fluxo exato da interface, siga a documentação oficial: Alertas do ClickStack — clickhouse.com/docs. Use a tabela abaixo para os dois alertas exigidos neste laboratório; a documentação orienta o fluxo Save Search → Create Alert → Configure threshold → Notify de cada um.

#Nome do alertaFonte do HyperDXCritérios de pesquisa (cole na barra Search antes de salvar)Condição do alertaJanelaRegra do Kibana substituída
1web-5xx-errorslogRequestType:* AND StatusCode:>=500count() > 0 (absoluto) — dispara para qualquer 5xx na janela. Como alternativa, para um limite de taxa, crie um gráfico cujo eixo Y seja countIf(StatusCode >= 500) / count() e dispare o alerta quando value > 0.05.5 minutos, avaliado a cada 1 minuto"Taxa de 5xx > 5% em 5 minutos"
2heartbeat-<service>logServiceName:"<service-name>" (uma pesquisa salva por serviço relevante, por exemplo, payment-service, order-service)count() == 0 — dispara quando a pesquisa salva retorna zero linhas na janela3 minutos, avaliado a cada 1 minuto"O serviço ficou silencioso por mais de 3 minutos"

Atenção ao alerta nº 2: Os alertas de pesquisas salvas do HyperDX avaliam uma consulta; portanto, detectar o silêncio por serviço exige uma pesquisa salva + um alerta para cada serviço. Para mais de cerca de 5 serviços, a Opção B abaixo (padrão SQL NOT IN em uma única MV) é mais adequada.

Opção B: tabela de alertas pré-calculada

A tabela alert_error_rate e a alert_error_rate_mv (criadas na Etapa 2c) pré-calculam a taxa de 5xx no momento da inserção. Um poller externo verifica a pequena tabela pré-agregada em vez de examinar milhões de linhas brutas:

-- Poll this every 1 minute (via cron or any scheduler)
SELECT minute, error_rate
FROM alert_error_rate
WHERE minute >= now() - INTERVAL 5 MINUTE
  AND error_rate > 0.05
ORDER BY minute DESC;

Se esta consulta retornar alguma linha, o alerta será disparado.

Ponto de ensino: Os alertas do Elasticsearch reagregam os dados brutos em todos os intervalos de verificação. A abordagem baseada em MV transfere a agregação dispendiosa para o momento da inserção — a consulta de polling do alerta examina uma pequena tabela com cerca de 1 linha/minuto, não milhões de linhas de logs brutos.


Etapa 9: exercícios de SQL — o que não era possível no Elasticsearch

Abra exercises/sql-exercises.md e conclua os 6 exercícios. Eles demonstram recursos do SQL do ClickHouse que não têm equivalente no Elasticsearch.

ExercícioConceitoLimitação do ES
1JOIN entre sinais (logs + traces)Não há JOIN na DSL do ES
2Função de janela LAG() (detecção de anomalias)Não há funções de janela no ES
3GROUP BY ilimitado (inventário completo de endpoints)A agregação terms exige size; há o limite max_buckets
4sequenceMatch() (detecção do fluxo de requisições)Não há correspondência de sequências ordenadas no ES
5Combinadores -If (várias métricas em uma consulta)Cada métrica condicional = uma agregação aninhada separada
6Investigação da causa raiz com CTENão há subconsultas nem CTEs na DSL do ES

Tente resolver cada exercício antes de consultar solutions/sql-exercises-solution.md.


Etapa 10: desativar o Elasticsearch (transição final)

Prossiga somente depois que validate_migration.sh passar sem erros.

10a. Trocar o collector baseado em arquivos pela configuração de transição (somente ClickHouse)

O laboratório inclui o arquivo dedicado configs/otel-collector-config.cutover.yaml, que remove os três exporters do ES e deixa o ClickHouse como único destino. Recrie o contêiner otelcol-lab com esse arquivo montado no lugar da versão de execução paralela — não é necessário editar nada diretamente:

docker stop otelcol-lab && docker rm otelcol-lab

source ../common/env.sh
docker run -d \
  --name otelcol-lab \
  --restart unless-stopped \
  --network docker_default \
  -v docker_log-data:/var/log/generators:ro \
  -v "$(pwd)/configs/otel-collector-config.cutover.yaml:/etc/otelcol-contrib/config.yaml:ro" \
  -e CH_HOST="${CH_HOST}" \
  -e CH_PASSWORD="${CH_PASSWORD}" \
  otel/opentelemetry-collector-contrib:0.146.1

Verifique se o novo contêiner iniciou sem erros e tem somente o exporter do ClickHouse carregado:

docker logs otelcol-lab --tail=20 | grep -iE "ready|exporter|fail"
# Expected: "Everything is ready. Begin running and processing data."
# No `elasticsearch/web`, `elasticsearch/app`, `elasticsearch/infra` references.

10b. Validação final

bash scripts/validate_migration.sh

Todas as verificações do ClickHouse ainda devem passar. As verificações de contagem do ES falharão (como esperado — o ES não recebe mais dados); portanto, confirme se as contagens do ClickHouse continuam crescendo.

10c. Verificar a paridade do enriquecimento

bash scripts/validate_enrichment.sh

Confirme uma cobertura de GeoCountry ≥ 20% (dados de exemplo) e BrowserFamily preenchido para todos os logs web.

10d. Parar a stack do Elasticsearch

docker compose -f ../part1/docker/docker-compose.source.yml stop elasticsearch kibana elastic-apm-server filebeat

Os dados do ES expirarão naturalmente por meio da política de ILM; se não for necessário retê-los, você poderá remover os volumes imediatamente.

10e. Trocar otelcol-demo pela configuração de transição (somente ClickHouse)

Depois que a Etapa 10d para elastic-apm-server, o exporter de APM do collector do OTel Demo começa a registrar erros de fila cheia e aplica contrapressão a todo o pipeline — inclusive ao exporter do ClickHouse distribuído ao lado dele. Mude para uma configuração de transição sem exporter de APM:

source ../common/env.sh
bash scripts/swap-otelcol-demo-config.sh cutover

Isso monta configs/otelcol-demo-config.cutover.yml (somente ClickHouse) e recria o contêiner. O arquivo de linha de base da Parte 1 permanece intacto, portanto um novo cleanup.sh && start from Part 1 sempre resulta em um estado limpo e exclusivo de APM.

Verifique se as métricas estão fluindo para o ClickHouse:

SELECT table, count() AS rows
FROM system.parts
WHERE database = 'otel' AND table LIKE 'otel_metrics%' AND active
GROUP BY table ORDER BY table;

Esperado: otel_metrics_gauge, otel_metrics_sum e otel_metrics_histogram, todos exibindo linhas.

Parabéns — migração concluída.


Checklist de conclusão da migração

ComponenteStatus
otel_logs_v2 recebendo dados[ ]
otel_traces recebendo dados[ ]
otel_metrics_sum recebendo dados[ ]
Dicionário GeoIP LOADED, enriquecendo linhas[ ]
Fontes do HyperDX (Traces/log/otel_metrics) configuradas e Search retornando dados em tempo real[ ]
validate_migration.sh aprovado[ ]
TTL configurado em todas as tabelas[ ]
logs_summary_1min AggMergeTree acumulando dados[ ]
Pelo menos uma regra de alerta ativa (alertas do HyperDX ou baseada em MV)[ ]
Todos os 6 exercícios de SQL concluídos[ ]
Exporter do ES removido da configuração do OTel[ ]
Contêineres do ES parados[ ]

Solução de problemas

O OTel Collector é encerrado imediatamente:

  • Verifique se CH_HOST está definido e acessível: nc -zv ${CH_HOST} 9440
  • Verifique create_schema: false — se o exporter tentar criar seu esquema padrão, ele poderá entrar em conflito com o nosso
  • Verifique se a configuração do collector tem database: otel e se o banco de dados otel realmente existe: SHOW DATABASES
  • Verifique se a tabela otel_logs existe no banco de dados otel: SHOW TABLES FROM otel LIKE 'otel_logs'

O OTel Collector é encerrado com schema detection: ... i/o timeout (inicialização a frio do CH Cloud):

  • Isso significa que a sondagem de detecção de esquema realizada na inicialização do ClickHouse Cloud demorou mais do que dial_timeout. É mais comum em serviços CH da camada Basic que ficaram ociosos.
  • Reinicie o contêiner — agora o CH está aquecido: docker start otelcol-lab && sleep 10 && docker logs otelcol-lab --tail=15
  • O docker run da Etapa 3b inclui --restart unless-stopped, portanto a recuperação é automática. Se você iniciou sem essa opção, recrie o contêiner.
  • Pré-aqueça o CH antes da próxima inicialização: clickhouse client --host "$CH_HOST" --port 9440 --user default --password "$CH_PASSWORD" --secure --query "SELECT 1"

GeoCountry está vazio em todas as linhas:

  • Verifique se o dicionário foi carregado: SELECT status FROM system.dictionaries WHERE database = 'otel' AND name = 'geoip_country'
  • Se estiver NOT_LOADED ou FAILED, verifique se geoip_data contém linhas: SELECT count() FROM otel.geoip_data
  • Execute SYSTEM RELOAD DICTIONARY otel.geoip_country e SYSTEM RELOAD DICTIONARY otel.geoip_city após carregar o CSV — os dicionários são criados antes do carregamento do CSV, portanto começam vazios e precisam de uma recarga explícita (a Etapa 2b faz isso por você)

As contagens de linhas no CH são muito menores do que no ES:

  • Verifique se o contêiner do OTel Collector está em execução: docker ps | grep otelcol
  • Consulte os logs do collector em busca de erros: docker logs otelcol-lab --tail=30
  • Verifique as métricas do Collector: curl http://localhost:8888/metrics | grep otelcol_exporter
  • Procure erros de inserção nos logs do Collector (busque failed to send)

logs_summary_1min está vazia:

  • A MV é acionada por inserções em otel.otel_logs (a tabela Null), não em otel.otel_logs_v2
  • Verifique se os dados estão fluindo por otel.otel_logs: em um novo terminal, execute clickhouse client --database otel --query "SELECT count() FROM otel_logs_v2" a cada 30 segundos e confirme se a contagem aumenta

A Search do HyperDX não mostra dados:

  • Confirme se cada fonte de dados do HyperDX tem Database = otel e aponta para a tabela correta (otel_logs_v2 para logs, otel_traces para traces e as cinco tabelas otel_metrics_* para OTEL Metrics)
  • Para a fonte de logs, Timestamp Column deve ser TimestampTime (não Timestamp) — consulte hyperdx-guide.md para entender o motivo
  • Verifique o seletor de intervalo de tempo — tente "Last 24 hours"
  • Verifique se otel_logs_v2 contém linhas: clickhouse client --database otel --query "SELECT count() FROM otel_logs_v2"

Próximo: Parte 4: validação de conhecimento →

Nesta página

Acompanhar seu progresso?

Opcional. Enviaremos um link por e-mail para confirmar seu endereço; o progresso será registrado depois que você o abrir.

Use seu e-mail corporativo, não um endereço pessoal.

O acompanhamento do progresso também exige a aceitação dos Termos de Serviço atuais nas Configurações de privacidade.

PT