Observabilidade em Elixir: Um Guia Prático para Métricas, Logs e Traces
Mix.install([
{:kino, "~> 0.17.0"},
{:jason, "~> 1.4"},
{:req, "~> 0.4"}
])
Growth-App
require Kino.RPC
node = :"growth@172.25.0.2"
Node.set_cookie(node, String.to_atom(System.fetch_env!("LB_GROWTH_RELEASE_COOKIE")))
Kino.RPC.eval_string(node, ~S":ok", file: __ENV__.file)
Cansado de não saber o que sua aplicação Elixir
está fazendo em produção? Nesta palestra, vamos dar um pontapé inicial no mundo da observabilidade de forma prática e sem complicação. Você vai ver como o :telemetry nos ajuda a criar logs melhores, coletar métricas com PromEx
e até começar a brincar com tracing usando OpenTelemetry
.
Quem somos
O que é Observabilidade?
O que acontece com a sua aplicação quando ela vai para produção?
Os três pilares da observabilidade:
- Logs
São os registros de eventos. É o que aconteceu no sistema, em texto. Por exemplo: Usuário 123 fez login
, ou Erro de timeout ao acessar o banco
. Os logs contam a história passo a passo.
- Métricas
São valores numéricos que a gente consegue acompanhar ao longo do tempo. Quantas requisições por segundo, qual a latência média, quanto de CPU a aplicação está usando. Elas ajudam a ver tendências e padrões.
- Traces
Já os traces permitem seguir o caminho de uma requisição. Quando um usuário faz uma chamada, o trace mostra por onde ela passou: gateway → API → banco → outro serviço. Isso ajuda muito a achar gargalos e entender onde está lento.
E por que isso importa? Porque sem observabilidade acontece o clássico: a aplicação caiu e ninguém sabe por quê. A equipe fica no escuro, testando hipóteses às cegas. Com observabilidade, a gente tem ferramentas pra diagnosticar rápido e resolver com confiança.
Qual a importância da observabilidade?
Aplicações modernas são distribuídas, rodam em containers, escalam em vários serviços. Quando dá problema, não é mais tão simples abrir um log no servidor e encontrar a causa.
Benefícios principais:
Reduz tempo de diagnóstico (MTTR)
Com observabilidade, a gente identifica rápido o que deu errado. Sem ela, podemos gastar horas ou dias investigando.
Melhora a confiabilidade do sistema
Quando conseguimos ver métricas de performance e erros em tempo real, detectamos falhas antes mesmo do usuário perceber.
Ajuda a tomar decisões técnicas e de negócio
Métricas não servem só para incidentes. Elas ajudam a ver tendências de uso: se uma rota é muito chamada, se precisamos de mais recursos, ou até se uma funcionalidade não está sendo usada.
Facilita a colaboração entre times
Com logs, métricas e traces visíveis para todos, o time inteiro fala a mesma língua. Infra, backend, frontend conseguem entender o que está acontecendo.
Em resumo: observabilidade não é luxo. É o que garante que a aplicação continue saudável, os usuários satisfeitos e o time produtivo. Sem isso, estamos sempre correndo atrás do prejuízo.
Instrumentando a aplicação com Telemetry
O que é Telemetry?
O Telemetry
é uma biblioteca padrão no ecossistema Elixir para instrumentação de aplicações. Ele permite que diferentes partes do sistema emitam eventos sobre o que está acontecendo internamente, sem acoplamento entre quem emite e quem consome esses eventos. Isso é fundamental para observabilidade, pois nos permite coletar métricas, logs e, futuramente, traces, de forma padronizada e eficiente.
Por que usar Telemetry?
Desacoplamento
O código de negócio não precisa saber quem está coletando ou processando os dados.
Performance
Telemetry é extremamente leve, não impactando a performance da aplicação.
Flexibilidade
Podemos conectar diferentes ferramentas de métricas, logs ou tracing sem alterar o código da aplicação.
Como instrumentar com Telemetry?
No nosso projeto, usamos principalmente o :telemetry.span/3
para medir a duração de operações importantes, como cálculos de métricas de crescimento. O span
emite dois eventos: um no início e outro no fim da operação, permitindo medir tempo e capturar informações contextuais.
:telemetry.span(
[:growth, :calculation],
%{
age_in_months: child.age_in_months,
gender: child.gender,
measure_date: child.measure_date
},
fn ->
weight_result = calculate_result(weight, :weight, child)
height_result = calculate_result(height, :height, child)
bmi_result = calculate_result(bmi, :bmi, child)
head_circumference_result =
calculate_result(head_circumference, :head_circumference, child)
result = %{
weight_result: weight_result,
height_result: height_result,
head_circumference_result: head_circumference_result,
bmi_result: bmi_result
}
measure = %Measure{growth | results: result}
{measure, %{count: 1},
%{
age_in_months: child.age_in_months,
gender: child.gender,
has_bmi_result: bmi_result != "no results",
has_head_circumference_result: head_circumference_result != "no results",
has_height_result: height_result != "no results",
has_weight_result: weight_result != "no results",
measure_date: child.measure_date,
success: true
}}
end
)
Aqui, emitimos um evento chamado [:growth, :calculation]
, passando dados relevantes como idade, gênero e sucesso da operação. Esses eventos podem ser consumidos por qualquer handler Telemetry, como exportadores de métricas ou sistemas de logs.
Boas práticas
- Instrumente pontos críticos: foque em operações que afetam performance ou são essenciais para o negócio.
-
Use nomes de eventos claros e hierárquicos, como
[:growth, :calculation]
. - Sempre envie metadados úteis nos eventos, como identificadores, parâmetros e resultados.
Como ficou nosso módulo Telemetry
defmodule GrowthWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
require Logger
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
# Attach a logger for all our events.
# This is useful for development and debugging.
:ok = attach_events()
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_joined.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# == Growth Metrics ==
# User Journey Events
counter("growth.child.created.count"),
counter("growth.measure.submitted.count"),
# Business Logic Span Events
summary("growth.calculation.stop.duration",
unit: {:native, :millisecond}
),
counter("growth.calculation.stop.count"),
summary("growth.calculation.measure.stop.duration",
# tags: [:data_type, :success],
unit: {:native, :millisecond}
),
summary("growth.reference_data.load.stop.duration",
# tags: [:data_type, :success],
unit: {:native, :millisecond}
),
summary("growth.reference_data.chart.stop.duration",
# tags: [:data_type, :success],
unit: {:native, :millisecond}
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {GrowthWeb, :count_users, []}
]
end
defp attach_events do
case Application.get_env(:growth, :env) do
:dev ->
:telemetry.attach_many("growth-logger", all_events(), &handle_event/4, nil)
_ ->
:ok
end
end
defp all_events do
[
# User Journey
[:growth, :child, :created],
[:growth, :measure, :submitted],
# [:growth, :results, :viewed],
# [:growth, :form, :reset],
# Spans (start, stop, exception)
[:growth, :calculation, :start],
[:growth, :calculation, :stop],
[:growth, :calculation, :measure, :start],
[:growth, :calculation, :measure, :stop],
[:growth, :reference_data, :load, :start],
[:growth, :reference_data, :load, :stop],
[:growth, :reference_data, :chart, :start],
[:growth, :reference_data, :chart, :stop]
]
end
defp handle_event(event, measurements, metadata, _config) do
Logger.info("[Telemetry] #{inspect(event)}
Measurements: #{inspect(measurements)}
Metadata: #{inspect(metadata)}")
end
end
O que é o módulo GrowthWeb.Telemetry?
Ele é o coração da nossa observabilidade. Ele coleta, organiza e expõe métricas e eventos, permitindo que a gente monitore, debugue e evolua a aplicação com confiança. Instrumentar com Telemetry é simples, eficiente e essencial.
Como funciona?
Supervisão e Polling
O módulo inicia um supervisor que inclui um :telemetry_poller
. Esse poller executa medições periódicas, como métricas da VM, a cada 10 segundos.
Podemos adicionar funções customizadas para serem chamadas periodicamente, por exemplo, para contar usuários ativos.
Definição de Métricas
A função metrics/0
retorna uma lista de métricas que queremos coletar.
Incluímos métricas do Phoenix (tempo de requisição, duração de handlers, etc.), métricas de negócio (quantidade de crianças criadas, medições submetidas, duração dos cálculos de crescimento) e métricas da VM (memória, filas de execução).
Exemplo:
summary("growth.calculation.stop.duration", unit: {:native, :millisecond})
counter("growth.child.created.count")
Logs de Telemetry para Desenvolvimento
Em ambiente de desenvolvimento, o módulo se conecta a todos os eventos relevantes usando :telemetry.attach_many/4. O callback handle_event/4 registra no Logger todos os eventos, medições e metadados, facilitando o debug e a validação da instrumentação.
Exemplo de log:
[Telemetry] [:growth, :calculation, :stop]
Measurements: %{duration: 12345}
Metadata: %{age_in_months: 12, gender: "female", success: true}
Extensibilidade
O módulo está pronto para receber novos eventos e métricas. Basta adicionar novas entradas em metrics/0
ou em all_events/0
.
Também é possível adicionar exportadores, como Prometheus, para enviar essas métricas para dashboards.
Por que isso é importante?
Visibilidade: Sabemos o que está acontecendo na aplicação em tempo real. Diagnóstico: Identificamos gargalos, lentidão e erros rapidamente. Evolução: Podemos tomar decisões baseadas em dados, não em achismos.
Boas práticas demonstradas
Separação de métricas de negócio e técnicas: Métricas de domínio (ex: cálculos de crescimento) e de infraestrutura (ex: memória da VM). Logs estruturados e ricos em contexto: Facilitam a análise e a correlação de eventos. Facilidade de extensão: O módulo é facilmente adaptável para novas necessidades de observabilidade.
Instrumentar logs, métricas e traces
Logs
O que são: registros de eventos que aconteceram no sistema, geralmente em texto.
Para que servem: ajudam a entender o que aconteceu e quando aconteceu.
Exemplo:
"Usuário 123 fez login com sucesso"
"Erro: timeout ao conectar no banco"
Analogia: um diário — cada página conta algo que ocorreu.
Métricas
O que são: valores numéricos que mostram como o sistema se comporta ao longo do tempo.
Para que servem: permitem ver tendências, padrões e comparar períodos.
Exemplo:
200 requisições por segundo
95% das requisições respondem em menos de 100 ms
30% de CPU em uso
Analogia: os sinais vitais do paciente — batimento, pressão, temperatura.
Traces
O que são: registros detalhados do caminho que uma requisição percorre dentro do sistema.
Para que servem: mostram onde está lento, onde ocorreu erro e como serviços diferentes interagem.
Exemplo:
Usuário chama /checkout
Passa pelo API Gateway → Serviço de Pagamento → Banco de Dados
Cada etapa tem duração e status registrados
Analogia: um mapa com o trajeto completo que a requisição fez.
Observabilidade na Prática: Logs, Métricas e Traces com Elixir, OpenTelemetry e SigNoz
Adotamos uma arquitetura moderna baseada em OpenTelemetry e SigNoz, uma plataforma open-source para visualização e análise de logs, métricas e traces.
Arquitetura Geral
Nossa stack é composta por:
growth-app: aplicação Elixir/Phoenix instrumentada com Telemetry e OpenTelemetry.
Logspout: coleta logs dos containers Docker.
OpenTelemetry Collector: recebe, processa e exporta logs, métricas e traces.
ClickHouse: banco de dados columnar de alta performance para armazenar todos os dados de observabilidade.
SigNoz: interface web para explorar, criar dashboards, alertas e analisar todos os dados.
Fluxo dos dados:
A aplicação envia logs, métricas e traces para o Otel Collector. O Collector armazena tudo no ClickHouse. O SigNoz consulta o ClickHouse e exibe dashboards e gráficos.
flowchart TD
subgraph Docker Host
subgraph growth-app [growth-app container]
direction LR
app([Application])
end
subgraph Observability Stack
direction LR
logspout[Logspout] -->|http| otel_collector
otel_collector(OpenTelemetry Collector)
signoz[SigNoz UI]
clickhouse[(ClickHouse)]
end
end
app -- OTLP --> otel_collector
otel_collector -- ClickHouse Exporter --> clickhouse
signoz -- queries --> clickhouse
style growth-app fill:#fff,stroke:#333,stroke-width:2px
style Observability Stack fill:#f9f9f9,stroke:#333,stroke-width:2px
Observabilidade na Aplicação Growth
Logs
Como funciona:
Utilizamos o Logger nativo do Elixir para registrar eventos importantes da aplicação. Além disso, implementamos a biblioteca logger_json
para criar logs estruturados, que permite a correlação eficiente de logs com traces e métricas.
Logger JSON ({:logger_json, "~> 7.0.0"})
Por que usar?
O Logger JSON transforma logs não estruturados em formato JSON estruturado, essencial para: Logs Estruturados: Facilita parsing e análise automatizada Correlação de Dados: Permite correlacionar logs com traces e métricas Busca Eficiente: Logs JSON são mais fáceis de indexar e pesquisar Integração com Ferramentas: Compatível com stacks de observabilidade modernas Metadados Ricos: Preserva contexto importante como request_id, user_id, etc.
Como é usado na aplicação?
Configuração Global (config/runtime.exs:23
):
config :logger, :default_handler,
formatter: LoggerJSON.Formatters.Basic.new(metadata: :all)
Pipeline de Processamento:
- Logs da aplicação são automaticamente formatados em JSON
- Incluem metadados como timestamps, níveis, request_id
- Capturados pelo Logspout e enviados ao OpenTelemetry Collector
- O Logspout captura esses logs dos containers e envia para o OTel Collector via syslog
- O Collector armazena os logs na tabela signoz_logs do ClickHouse
Fluxo: Application Logs (JSON) → Logspout → OpenTelemetry Collector → ClickHouse → SigNoz
Na prática:
Todos os logs ficam centralizados e podem ser buscados e filtrados na interface do SigNoz. Isso facilita identificar erros, analisar fluxos e auditar o sistema. Os logs estruturados facilitam o troubleshooting e a correlação com os demais dados de telemetria.
Métricas
Como funciona:
O OpenTelemetry Collector coleta métricas de duas formas:
- Scrape do endpoint /metrics exposto pela aplicação (formato Prometheus)
- Recebimento via OTLP diretamente da aplicação instrumentada
As métricas são armazenadas na tabela signoz_metrics do ClickHouse.
PromEx ({:prom_ex, "~> 1.11.0"})
Por que usar?
O PromEx é uma biblioteca Elixir que facilita a coleta e exposição de métricas no formato Prometheus, proporcionando observabilidade completa da aplicação Phoenix:
- Monitoramento de Performance: Coleta automaticamente métricas de latência, throughput e taxa de erro
- Visibilidade da Aplicação: Monitora recursos do sistema como CPU, memória e processos Erlang/Elixir
- Detecção Proativa de Problemas: Permite identificar gargalos e anomalias antes que afetem os usuários
- Dashboards Automatizados: Integra-se com Grafana para visualizações prontas
Como é usado na aplicação?
Configuração no Application Tree (lib/growth/application.ex:15
):
children = [
Growth.PromEx, # Iniciado como um dos primeiros processos
# ... outros processos
]
Módulo PromEx Customizado (lib/growth/prom_ex.ex
):
Plugins Habilitados:
-
Plugins.Application
: Métricas da aplicação OTP -
Plugins.Beam
: Métricas da VM Erlang (garbage collection, schedulers) -
Plugins.Phoenix
: Métricas HTTP (requests, responses, latência) -
Plugins.PhoenixLiveView
: Métricas específicas do LiveView
Exposição de Métricas (lib/growth_web/endpoint.ex:41
):
plug PromEx.Plug, prom_ex_module: Growth.PromEx
Expõe endpoint /metrics
para scraping do Prometheus
Configurado para ser coletado a cada 5 segundos pelo OpenTelemetry Collector
Na prática:
Monitoramos contadores, histogramas de duração, uso de recursos, etc. No SigNoz, criamos dashboards para acompanhar saúde, performance e uso do sistema. Podemos configurar alertas para anomalias ou quedas de performance
Traces
Como funciona:
A aplicação Elixir envia traces via OTLP
(gRPC ou HTTP) para o OpenTelemetry Collector
. Cada trace representa o caminho de uma requisição ou operação importante, com spans detalhando cada etapa. Os traces são armazenados na tabela signoz_traces
do ClickHouse.
Bibliotecas OpenTelemetry:
OpenTelemetry Core ({:opentelemetry, "~> 1.6.0"} e {:opentelemetry_api, "~> 1.4.0"})
Por que usar?
O OpenTelemetry é o padrão da indústria para observabilidade, fornecendo:
- Padronização: Protocolo universal para telemetria (logs, métricas, traces)
- Vendor Agnostic: Não fica preso a uma ferramenta específica de observabilidade
- Distributed Tracing: Rastreamento de requisições através de múltiplos serviços
- Correlação Automática: Liga automaticamente logs, métricas e traces
- Instrumentação Automática: Coleta dados sem modificar código de negócio
OpenTelemetry Bandit ({:opentelemetry_bandit, "~> 0.3.0"})
Instrumenta automaticamente o servidor HTTP Bandit (usado pelo Phoenix), coletando:
- Métricas HTTP: Latência, status codes, throughput
- Traces de Requisições: Rastreamento completo de cada request
- Contexto de Propagação: Mantém trace context entre serviços
OpenTelemetry Phoenix ({:opentelemetry_phoenix, "~> 2.0.0"})
Instrumenta especificamente o framework Phoenix, capturando:
- Controller Actions: Tempo de execução de cada action
- Pipeline Plugs: Performance de cada plug no pipeline
- LiveView Events: Instrumentação de eventos do LiveView
- Template Rendering: Tempo de renderização de templates
OpenTelemetry Telemetry ({:opentelemetry_telemetry, "~> 1.1.0"})
Faz a ponte entre o sistema de Telemetry do Elixir e o OpenTelemetry:
- Conversão Automática: Transforma eventos Telemetry em spans OpenTelemetry
- Instrumentação Nativa: Aproveita a instrumentação já existente no ecossistema Elixir
- Contexto Distribuído: Propaga trace context através de processos Elixir
OpenTelemetry Exporter ({:opentelemetry_exporter, "~> 1.9.0"})
Responsável por enviar os dados coletados para sistemas externos:
- Protocolo OTLP: Envia traces via protocolo OpenTelemetry padrão
- Batching: Agrupa dados para envio eficiente
- Retry Logic: Reenvio automático em caso de falhas
- Compressão: Reduz bandwidth com compressão gzip
Na prática:
No SigNoz
, visualizamos o fluxo completo de requisições, identificando gargalos e dependências. É possível correlacionar traces com logs e métricas para diagnóstico rápido de problemas.
Fluxo Completo de Observabilidade
Arquitetura:
Application → OpenTelemetry SDK → OTLP Exporter → OTel Collector → ClickHouse → SigNoz
Sinergia das Bibliotecas:
As bibliotecas trabalham em conjunto para fornecer os três pilares da observabilidade:
Métricas (PromEx): “O que está acontecendo?”
Logs (Logger JSON): “Por que está acontecendo?”
Traces (OpenTelemetry): “Onde está acontecendo?”
Benefícios da Stack Completa:
- Observabilidade 360°: Métricas + Logs + Traces correlacionados
- Troubleshooting Eficiente: Correlação automática entre os três pilares
- Performance Insights: Identificação precisa de gargalos
- Alertas Inteligentes: Baseados em dados estruturados e correlacionados
- Vendor Independence: Pode migrar entre ferramentas sem reescrever instrumentação
Conclusão
Com essa arquitetura, temos observabilidade de ponta a ponta: logs
, métricas
e traces
centralizados, acessíveis e correlacionados.
Isso nos permite:
- Detectar problemas rapidamente
- Entender o comportamento do sistema
- Evoluir com segurança e confiança
O uso de ferramentas open-source como OpenTelemetry, ClickHouse e SigNoz torna a solução acessível, flexível e escalável, proporcionando uma base sólida para manter aplicações resilientes e performáticas em produção.
SigNoz
O que é
O SigNoz é uma plataforma open source de observabilidade. Ele junta os três pilares — logs, métricas e traces — em um único lugar, compatível com o padrão OpenTelemetry.
Como funciona
-
Ele coleta dados enviados pela aplicação via OpenTelemetry.
-
Esses dados são armazenados e processados (normalmente com ClickHouse por baixo).
-
A interface web permite:
-
Criar dashboards de métricas
-
Consultar logs estruturados
-
Explorar traces distribuídos
-
Por que usar
Open source e gratuito – sem custo por host/volume de dados como nas soluções cloud.
Controle total – os dados ficam na sua infra.
Integração simples – já entende OpenTelemetry, então funciona com Elixir, Node, Go, etc.
Comparável a players grandes – traz a mesma experiência de ferramentas comerciais
O que aprendemos
- Observabilidade é essencial, não opcional
Ter logs, métricas e traces centralizados nos permite entender o comportamento real da aplicação em produção.
Problemas que seriam difíceis de diagnosticar (como lentidão, erros intermitentes ou picos de uso) ficam evidentes com poucos cliques no dashboard.
- Arquitetura desacoplada facilita evolução
Separar a coleta (aplicação), processamento (otel-collector) e visualização (SigNoz) permite trocar ou evoluir cada parte sem grandes impactos.
O padrão OpenTelemetry nos dá liberdade para integrar com outras ferramentas no futuro, sem lock-in.
- Instrumentação no código é simples e poderosa
Com Telemetry no Elixir, instrumentar pontos críticos do código é fácil e não polui a lógica de negócio.
Emitir eventos, métricas e traces se torna parte natural do desenvolvimento.
- Visualização e análise aceleram o ciclo de feedback
Dashboards e alertas no SigNoz permitem agir rapidamente diante de anomalias.
Conseguimos identificar gargalos, monitorar uso de recursos e entender o fluxo das requisições em tempo real.
- Logs, métricas e traces se complementam
Logs mostram o detalhe do que aconteceu.
Métricas mostram tendências e alertam para mudanças de comportamento.
Traces mostram o caminho completo de cada requisição, facilitando encontrar a causa raiz de problemas.
- Observabilidade é para todos
A stack é open-source, acessível e pode ser usada tanto em ambientes locais quanto em produção.
Não é preciso depender de soluções proprietárias caras para ter visibilidade de ponta a ponta.
- Cultura de melhoria contínua
Com dados em mãos, a equipe pode tomar decisões baseadas em fatos, priorizar melhorias e evoluir o sistema com confiança.
Observabilidade não é só tecnologia, mas parte da cultura de desenvolvimento moderno.
Observando a Growth
public_url = "https://crescer.dubas.dev"
internal_url = "http://172.25.0.2:4000"
node_name = "growth@172.25.0.2"
IO.puts("""
🌐 CONECTANDO À APLICAÇÃO EM PRODUÇÃO:
✅ URL Pública: #{public_url}
🐳 Container: #{node_name}
🔗 URL Interna: #{internal_url}
🚀 Ambiente: Produção (Docker)
📊 Dados: Reais de usuários reais!
Vamos conectar e observar a telemetria ao vivo!
""")
IO.puts("🌐 Testando acesso público...")
case Req.get(public_url) do
{:ok, %{status: 200}} ->
IO.puts("✅ Aplicação pública acessível!")
{:ok, %{status: status}} ->
IO.puts("⚠️ Aplicação respondeu com status #{status}")
{:error, reason} ->
IO.puts("❌ Erro de conexão pública: #{inspect(reason)}")
end
IO.puts("\n🐳 Testando acesso interno ao container...")
case Req.get(internal_url, receive_timeout: 5000) do
{:ok, %{status: 200}} ->
IO.puts("✅ Container acessível diretamente!")
{:ok, %{status: status}} ->
IO.puts("⚠️ Container respondeu com status #{status}")
{:error, reason} ->
IO.puts("ℹ️ Acesso interno não disponível (normal): #{inspect(reason)}")
IO.puts("💡 Usando URL pública para demonstração")
end
defmodule NodeConnection do
def try_connect_to_node do
node_name = :"growth@172.25.0.2"
IO.puts("🔗 TENTANDO CONECTAR AO NODE ERLANG:")
IO.puts(" Node: #{node_name}")
IO.puts(" Node local: #{Node.self()}")
# Verificar se o node está acessível
case Node.ping(node_name) do
:pong ->
IO.puts("✅ Node conectado com sucesso!")
# Obter informações do sistema remoto
remote_info = :rpc.call(node_name, :erlang, :system_info, [:system_version])
IO.puts("📊 Versão do sistema: #{remote_info}")
# Obter estatísticas de memória
memory_info = :rpc.call(node_name, :erlang, :memory, [])
IO.puts("🧠 Uso de memória:")
Enum.each(memory_info, fn {type, bytes} ->
mb = Float.round(bytes / (1024 * 1024), 2)
IO.puts(" #{type}: #{mb}MB")
end)
# Obter número de processos
process_count = :rpc.call(node_name, :erlang, :system_info, [:process_count])
IO.puts("⚙️ Processos ativos: #{process_count}")
{:ok, :connected}
:pang ->
IO.puts("ℹ️ Node não acessível diretamente (normal em produção)")
IO.puts("💡 Isso é esperado por questões de segurança")
IO.puts("🔄 Continuando com métricas HTTP...")
{:error, :not_accessible}
end
end
def get_vm_metrics_simulation do
IO.puts("📊 SIMULAÇÃO DE MÉTRICAS DA VM ERLANG:")
IO.puts("=" |> String.duplicate(50))
IO.puts("🐳 Node: growth@172.25.0.2")
IO.puts("⏰ Timestamp: #{DateTime.utc_now()}")
IO.puts("")
# Simular métricas que estariam disponíveis
simulated_metrics = [
{"Memória Total", "45.6MB"},
{"Processos", "156"},
{"Schedulers", "8"},
{"Uptime", "2d 14h 32m"},
{"GC Count", "1,234"},
{"Reductions", "12,345,678"}
]
Enum.each(simulated_metrics, fn {metric, value} ->
IO.puts("📈 #{metric}: #{value}")
end)
IO.puts("\n💡 Em um ambiente com acesso direto ao node, poderíamos obter:")
IO.puts(" - Estatísticas detalhadas de memória")
IO.puts(" - Informações de processos em tempo real")
IO.puts(" - Métricas de garbage collection")
IO.puts(" - Estado dos schedulers")
IO.puts(" - Traces distribuídos internos")
end
end
# Tentar conectar (provavelmente falhará por segurança)
NodeConnection.try_connect_to_node()
# Mostrar simulação das métricas que teríamos
IO.puts("")
NodeConnection.get_vm_metrics_simulation()
defmodule LiveMetrics do
def fetch_metrics(base_url \\ "https://crescer.dubas.dev") do
metrics_url = "#{base_url}/metrics"
case Req.get(metrics_url) do
{:ok, %{status: 200, body: body}} ->
{:ok, body}
{:ok, %{status: status}} ->
{:error, "HTTP #{status} - Métricas podem não estar expostas publicamente"}
{:error, reason} ->
{:error, "Conexão falhou: #{inspect(reason)}"}
end
end
def parse_metrics(metrics_text) do
metrics_text
|> String.split("\n")
|> Enum.reject(&String.starts_with?(&1, "#"))
|> Enum.reject(&(&1 == ""))
|> Enum.take(20) # Primeiras 20 métricas para não sobrecarregar
end
def display_metrics(metrics_lines) do
IO.puts("📊 MÉTRICAS REAIS DA APLICAÇÃO GROWTH (PRODUÇÃO)")
IO.puts("=" |> String.duplicate(60))
IO.puts("🔗 Fonte: https://crescer.dubas.dev/metrics")
IO.puts("⏰ Timestamp: #{DateTime.utc_now()}")
IO.puts("")
Enum.with_index(metrics_lines, 1)
|> Enum.each(fn {line, index} ->
IO.puts("#{index}. #{line}")
end)
end
end
# Buscar métricas reais
case LiveMetrics.fetch_metrics() do
{:ok, metrics_text} ->
metrics_text
|> LiveMetrics.parse_metrics()
|> LiveMetrics.display_metrics()
{:error, reason} ->
IO.puts("❌ ERRO: #{reason}")
IO.puts("💡 Certifique-se de que a aplicação está rodando em http://localhost:4000")
IO.puts(" Execute: mix phx.server")
end
defmodule TrafficGenerator do
def make_requests(count \\ 5) do
IO.puts("🚀 GERANDO #{count} REQUISIÇÕES...")
results =
1..count
|> Enum.map(fn i ->
IO.puts("📡 Requisição #{i}/#{count}")
start_time = System.monotonic_time(:millisecond)
result = case Req.get("https://crescer.dubas.dev/") do
{:ok, %{status: status}} ->
duration = System.monotonic_time(:millisecond) - start_time
%{status: status, duration: duration, success: true}
{:error, reason} ->
duration = System.monotonic_time(:millisecond) - start_time
%{error: reason, duration: duration, success: false}
end
Process.sleep(500) # Pausa entre requisições
result
end)
# Mostrar resultados
IO.puts("\n📈 RESULTADOS:")
Enum.with_index(results, 1)
|> Enum.each(fn {result, i} ->
if result.success do
IO.puts("✅ #{i}. HTTP #{result.status} - #{result.duration}ms")
else
IO.puts("❌ #{i}. ERRO: #{result.error} - #{result.duration}ms")
end
end)
# Estatísticas
successful = Enum.count(results, & &1.success)
total_duration = Enum.sum(Enum.map(results, & &1.duration))
avg_duration = if count > 0, do: total_duration / count, else: 0
IO.puts("\n📊 ESTATÍSTICAS:")
IO.puts("✅ Sucessos: #{successful}/#{count}")
IO.puts("⏱️ Duração média: #{Float.round(avg_duration, 2)}ms")
IO.puts("📈 Taxa de sucesso: #{Float.round(successful/count*100, 1)}%")
results
end
end
# Gerar tráfego
TrafficGenerator.make_requests(3)
# Capturar métricas antes
IO.puts("📊 CAPTURANDO MÉTRICAS ANTES...")
{:ok, metrics_before} = LiveMetrics.fetch_metrics()
# Gerar mais tráfego
IO.puts("\n🚀 GERANDO TRÁFEGO...")
TrafficGenerator.make_requests(5)
# Aguardar um pouco para as métricas serem atualizadas
Process.sleep(2000)
# Capturar métricas depois
IO.puts("\n📊 CAPTURANDO MÉTRICAS DEPOIS...")
{:ok, metrics_after} = LiveMetrics.fetch_metrics()
defmodule MetricsComparison do
def extract_metric_value(metrics_text, metric_name) do
metrics_text
|> String.split("\n")
|> Enum.find(&String.starts_with?(&1, metric_name))
|> case do
nil -> 0
line ->
line
|> String.split(" ")
|> List.last()
|> String.to_float()
end
end
def compare_metrics(before, following) do
metrics_to_compare = [
"phoenix_http_requests_total",
"phoenix_endpoint_stop_duration_seconds_count",
"vm_memory_bytes_total"
]
IO.puts("🔍 COMPARAÇÃO DE MÉTRICAS:")
IO.puts("=" |> String.duplicate(50))
Enum.each(metrics_to_compare, fn metric ->
before_val = extract_metric_value(before, metric)
after_val = extract_metric_value(following, metric)
diff = after_val - before_val
IO.puts("📈 #{metric}:")
IO.puts(" Antes: #{before_val}")
IO.puts(" Depois: #{after_val}")
IO.puts(" Diferença: +#{diff}")
IO.puts("")
end)
end
end
MetricsComparison.compare_metrics(metrics_before, metrics_after)
defmodule GrowthAppSimulator do
def simulate_child_growth_calculation do
IO.puts("👶 SIMULANDO CÁLCULO DE CRESCIMENTO...")
# Dados de exemplo de uma criança
child_data = %{
name: "Ana",
gender: "female",
birthday: "2021-01-01"
}
measure_data = %{
weight: 14.5,
height: 95.0,
head_circumference: 48.0
}
IO.puts("📋 Dados da criança:")
IO.puts(" Nome: #{child_data.name}")
IO.puts(" Gênero: #{child_data.gender}")
IO.puts(" Nascimento: #{child_data.birthday}")
IO.puts("\n📏 Medidas:")
IO.puts(" Peso: #{measure_data.weight}kg")
IO.puts(" Altura: #{measure_data.height}cm")
IO.puts(" Perímetro cefálico: #{measure_data.head_circumference}cm")
# Fazer requisição para a página principal
IO.puts("\n🌐 Acessando aplicação em produção...")
case Req.get("https://crescer.dubas.dev/") do
{:ok, %{status: 200}} ->
IO.puts("✅ Aplicação acessada com sucesso!")
IO.puts("💡 Em uma aplicação real, aqui faríamos:")
IO.puts(" 1. POST para salvar dados da criança")
IO.puts(" 2. POST para salvar medidas")
IO.puts(" 3. Cálculo dos z-scores e percentis")
IO.puts(" 4. Geração dos gráficos de crescimento")
# Simular múltiplas requisições para gerar telemetria
IO.puts("\n🔄 Simulando fluxo completo...")
Enum.each(1..3, fn step ->
IO.puts(" Passo #{step}: Processando...")
Req.get("http://localhost:4000/")
Process.sleep(1000)
end)
IO.puts("✅ Simulação concluída!")
{:error, reason} ->
IO.puts("❌ Erro ao acessar aplicação: #{inspect(reason)}")
end
end
end
GrowthAppSimulator.simulate_child_growth_calculation()
defmodule LiveLogs do
def simulate_real_logs do
IO.puts("📋 LOGS QUE ESTARIAM SENDO GERADOS AGORA:")
IO.puts("=" |> String.duplicate(60))
logs = [
%{
timestamp: DateTime.utc_now(),
level: "info",
message: "GET / - Sent 200",
metadata: %{
duration: "45.2ms",
request_id: "F8mOKAmVmjsAABEh"
}
},
%{
timestamp: DateTime.utc_now() |> DateTime.add(-2, :second),
level: "info",
message: "LiveView connected",
metadata: %{
socket_id: "phx-F8mOKAmVmjsAABEh",
transport: "websocket"
}
},
%{
timestamp: DateTime.utc_now() |> DateTime.add(-5, :second),
level: "info",
message: "CSV data loaded: weight_for_age.csv",
metadata: %{
records: 1856,
duration: "580ms"
}
}
]
Enum.each(logs, fn log ->
level_icon = case log.level do
"info" -> "ℹ️"
"error" -> "🚨"
"warn" -> "⚠️"
end
IO.puts("#{level_icon} [#{DateTime.to_time(log.timestamp)}] #{log.message}")
if log.metadata do
Enum.each(log.metadata, fn {key, value} ->
IO.puts(" #{key}: #{value}")
end)
end
IO.puts("")
end)
IO.puts("💡 Para ver logs reais, execute no terminal:")
IO.puts(" tail -f _build/dev/lib/growth/ebin/growth.log")
IO.puts(" ou observe o terminal onde mix phx.server está rodando")
end
end
LiveLogs.simulate_real_logs()
defmodule LiveDashboard do
def create_live_dashboard do
IO.puts("📊 DASHBOARD AO VIVO - APLICAÇÃO GROWTH")
IO.puts("=" |> String.duplicate(50))
IO.puts("🕐 Atualizado em: #{DateTime.utc_now()}")
IO.puts("")
# Buscar métricas atuais
case LiveMetrics.fetch_metrics() do
{:ok, metrics} ->
# Extrair métricas específicas
http_requests = extract_counter(metrics, "phoenix_http_requests_total")
memory_usage = extract_gauge(metrics, "vm_memory_bytes_total")
IO.puts("🌐 HTTP:")
IO.puts(" 📈 Total de requisições: #{http_requests}")
IO.puts("\n💻 SISTEMA:")
memory_mb = if memory_usage > 0, do: Float.round(memory_usage / (1024*1024), 1), else: 0
IO.puts(" 🧠 Uso de memória: #{memory_mb}MB")
IO.puts("\n🏥 STATUS:")
status = if http_requests > 0, do: "🟢 ATIVO", else: "🟡 AGUARDANDO TRÁFEGO"
IO.puts(" #{status}")
{:error, reason} ->
IO.puts("❌ Não foi possível buscar métricas: #{reason}")
IO.puts("💡 Certifique-se de que a aplicação está rodando")
end
end
defp extract_counter(metrics, name) do
metrics
|> String.split("\n")
|> Enum.filter(&String.contains?(&1, name))
|> Enum.reject(&String.starts_with?(&1, "#"))
|> Enum.map(fn line ->
line
|> String.split(" ")
|> List.last()
|> parse_number()
end)
|> Enum.sum()
|> trunc()
end
defp extract_gauge(metrics, name) do
metrics
|> String.split("\n")
|> Enum.find(&String.starts_with?(&1, name))
|> case do
nil -> 0
line ->
line
|> String.split(" ")
|> List.last()
|> parse_number()
end
end
defp parse_number(str) do
cond do
String.contains?(str, ".") ->
# É um float
case Float.parse(str) do
{num, _} -> num
:error -> 0
end
true ->
# É um inteiro
case Integer.parse(str) do
{num, _} -> num
:error -> 0
end
end
end
end
LiveDashboard.create_live_dashboard()
IO.puts("📊 DASHBOARD INICIAL:")
LiveDashboard.create_live_dashboard()
IO.puts("\n💡 DICAS PARA DEMONSTRAÇÃO:")
IO.puts("1. 🌐 Acesse https://crescer.dubas.dev em outra aba")
IO.puts("2. 👶 Preencha dados de uma criança")
IO.puts("3. 📏 Adicione medidas e calcule crescimento")
IO.puts("4. 🔄 Execute a célula abaixo para atualizar dashboard")
IO.puts("5. 📊 Observe as métricas mudando em tempo real!")
IO.puts("🔄 ATUALIZANDO DASHBOARD...")
IO.puts("⏰ #{DateTime.utc_now()}")
IO.puts("")
LiveDashboard.create_live_dashboard()
IO.puts("\n🎯 Execute esta célula quantas vezes quiser para ver mudanças!")
IO.puts("💡 Gere tráfego na aplicação e execute novamente para ver diferenças")