UpWatch
Monitoramento de disponibilidade que cabe num binário e mostra latência
junto com o estado — porque quase todo incidente começa com o serviço
ficando lento, não caindo.
jbnado.github.io/upwatch — o
mesmo conteúdo em página, para quem chega de fora.
Um arquivo, um contêiner, um volume. A interface vem embarcada no
executável: não há nginx ao lado nem pasta de arquivos estáticos que
possa ficar defasada em relação ao servidor que os acompanha.
docker run -d --name upwatch -p 8080:8080 -v upwatch:/data \
ghcr.io/jbnado/upwatch:latest
Abra http://localhost:8080 e crie a conta de administração. É a única
conta criada sem autenticação; depois dela, o cadastro fecha.
O que ele faz
Verifica por HTTP, TCP, ICMP, DNS, TLS e sinal do próprio serviço
(push, para tarefas agendadas e processos sem porta exposta). O intervalo
vai de cinco segundos em diante, por monitor.
Guarda meses sem guardar meses de dados crus. As batidas duram uma
semana; depois viram agregado horário e diário, com percentis exatos
calculados sempre a partir do dado cru — nunca percentil de percentil.
Uma instalação com cinco alvos verificando a cada minuto ocupou 2,6 MB
depois de trinta dias.
Confirma antes de alarmar. Uma queda só vira incidente depois de N
falhas seguidas, configurável por monitor. Oscilação de rede não acorda
ninguém.
Sabe distinguir a queda do alvo da queda da própria rede. Quando as
verificações começam a falhar, uma sonda independente confere se a rede
local ainda responde; se não responder, os resultados viram "sem
medição" em vez de "fora do ar". Essa sonda só ganha esse poder depois
de provar que funciona — uma sonda bloqueada por firewall silenciaria
todos os alertas para sempre.
Avisa por webhook, Discord e Slack, com um botão de teste em cada
canal. Descobrir que o alerta não chega durante a queda é descobrir
tarde. O webhook genérico aceita cabeçalhos próprios e deixa você
escolher a forma do corpo — veja Webhook.
Agrupa por etiqueta. Homolog e produção lado a lado sem virar uma
lista única de quarenta linhas: etiquete os alvos e o painel oferece
agrupar por qualquer uma delas. As etiquetas são normalizadas na entrada
— "Produção", "produção " e "PRODUÇÃO" são o mesmo grupo, e não três.
Tem dois papéis de acesso. Administrador cadastra e altera;
observador só lê — serve para plantão, gerência e time vizinho
acompanharem sem poder mexer. A barreira está no servidor, não em botão
escondido: um observador com token recebe 403 em qualquer escrita.
Publica uma página de estado no formato que Anthropic, Cloudflare e
Google consolidaram: veredito no topo, componentes agrupados, noventa
barras de histórico, incidentes anteriores com linha do tempo. Ela nunca
revela o endereço do alvo nem a causa detectada pela sonda — o que sai é
só o que você escreveu.
Fala com quem já tem Prometheus em /metrics, sem credencial, para
o alerta morar onde já mora o resto.
Instalação
Docker Compose
curl -O https://raw.githubusercontent.com/Jbnado/upwatch/main/compose.yaml
docker compose up -d
Com PostgreSQL, quando a disponibilidade do próprio monitorador importa e
você quer mais de uma instância:
curl -O https://raw.githubusercontent.com/Jbnado/upwatch/main/compose.postgres.yaml
POSTGRES_PASSWORD=$(openssl rand -base64 24) docker compose -f compose.postgres.yaml up -d
Binário
Baixe o executável da página de releases. Ele é estático e não depende de
nada instalado:
UPWATCH_DB_DSN=./upwatch.db ./upwatch
Configuração
Tudo por variável de ambiente ou arquivo YAML. A variável ganha do
arquivo, e o arquivo ganha do padrão — assim uma imagem sobe sem
configuração nenhuma e uma instalação séria versiona o YAML.
| Variável |
Padrão |
O que faz |
UPWATCH_LISTEN |
:8080 |
Endereço de escuta |
UPWATCH_DB_DRIVER |
sqlite |
sqlite ou postgres |
UPWATCH_DB_DSN |
/data/upwatch.db |
Caminho do arquivo ou string de conexão |
UPWATCH_RETENTION_RAW |
168h |
Quanto tempo as batidas cruas duram |
UPWATCH_RETENTION_HOURLY |
2160h |
Retenção do agregado horário |
UPWATCH_RETENTION_DAILY |
17520h |
Retenção do agregado diário |
UPWATCH_ROLLUP_INTERVAL |
5m |
De quanto em quanto a agregação roda |
UPWATCH_WORKERS |
50 |
Teto de verificações simultâneas |
UPWATCH_SESSION_TTL |
168h |
Validade da sessão da interface |
UPWATCH_SECURE_COOKIES |
false |
Marca o cookie como Secure; ligue ao servir por HTTPS |
UPWATCH_PUBLIC_URL |
vazio |
Endereço externo, para o feed da página pública usar URLs absolutas |
Durações aceitam sufixo de dias: 90d é o mesmo que 2160h.
UPWATCH_SECURE_COOKIES fica desligado por padrão de propósito. Muita
instalação caseira serve em HTTP na rede local, e um cookie Secure ali
simplesmente não é enviado — o login pareceria quebrado sem explicação.
API
A API é de primeira classe, não um acessório da interface: a tela consome
exatamente os mesmos endpoints que qualquer script. A especificação
OpenAPI é servida pela própria instalação, em /api/v1/openapi.yaml, e
um teste garante que ela não diverge das rotas de verdade — nos dois
sentidos.
# Token de acesso: Ajustes → Tokens de acesso
curl -H "Authorization: Bearer upw_..." http://localhost:8080/api/v1/monitors
Webhook
O canal webhook entrega um POST com Content-Type: application/json.
Sem configuração extra, o corpo é este envelope:
{
"text": "api-de-producao está fora do ar: connection refused",
"monitor": "api-de-producao",
"monitor_id": 7,
"target": "https://api.exemplo.com",
"status": "down",
"previous_status": "up",
"message": "connection refused",
"at": "2026-07-30T18:42:11Z",
"duration_seconds": 0
}
status e previous_status são up, down, degraded ou unknown.
duration_seconds é quanto durou o estado anterior: na queda vem 0, na
volta vem o tempo que o alvo ficou fora. message é a causa observada e
pode vir vazia.
Um destino que já existe espera os campos com os nomes dele, e nem sempre
dá para mudar quem recebe. Em body_template você declara a forma, e os
marcadores são substituídos:
{
"url": "https://automacao.exemplo/alertas",
"headers": { "X-Chave": "…" },
"body_template": {
"event": "$status",
"service": { "name": "$monitor", "id": "$monitor_id" },
"outage_seconds": "$duration_seconds",
"summary": "[$status] $monitor"
}
}
| Marcador |
Entrega |
$monitor |
Nome do alvo |
$monitor_id |
Identificador, como número |
$target |
Endereço verificado |
$status |
up, down, degraded ou unknown |
$previous_status |
O estado anterior |
$message |
Causa observada, quando houver |
$at |
Instante da mudança |
$duration_seconds |
Duração do estado anterior, como número |
$text |
A frase pronta do aviso |
Também vale ${nome}, para quando o marcador encosta em texto que
continuaria o identificador, e $$ escreve um cifrão literal.
Sozinho, o marcador entrega o valor com o tipo dele — "$duration_seconds"
chega como 300, não "300". Dentro de um texto, compõe a frase. A
diferença importa para quem valida esquema do outro lado.
A substituição acontece sobre a estrutura JSON já decodificada, e o
resultado é serializado de volta — nunca por concatenação de texto. É o
que garante que uma aspa no nome do monitor ou uma quebra de linha na
causa não produzam um corpo que o destino recusa, perdendo o aviso da
queda por causa da própria queda.
Marcador desconhecido é recusado no cadastro do canal, não na entrega:
descobrir o erro de digitação durante o incidente é descobrir tarde
demais. Discord e Slack recusam body_template pelo mesmo motivo — o
corpo deles é ditado pelo destino.
Os campos avançados também estão na interface, em Ajustes → Canais de
aviso, atrás de "cabeçalhos e formato do corpo".
Página pública de estado
Em Ajustes → Páginas de estado. Marque uma como padrão e ela responde em
/status; com várias, cada uma responde no próprio /status/<slug>.
Duas coisas que ela faz de propósito e que valem saber antes de publicar:
As barras são automáticas, o relato não. A causa que a sonda detecta é
literal e interna — dial tcp 10.0.3.7:5432: connect: connection refused
— e entregaria endereço, porta e tecnologia de um serviço que ninguém de
fora deveria enxergar. Por isso ela nunca é publicada. Uma instalação
recém-subida mostra as barras e "nenhum incidente relatado"; o texto que
aparece em "incidentes anteriores" é o que você escrever.
Cada componente tem um rótulo público. O monitor pode se chamar
api-prod-us-east-1 na operação e aparecer como "API" para quem lê, sem
obrigar você a renomeá-lo nem a entregar sua convenção de nomes.
Há também um feed Atom em /api/v1/public/<slug>/feed.atom, para
acompanhar sem cadastrar e-mail e para ligar num canal de chat sem que o
UpWatch precise saber falar com aquele canal.
Prometheus
scrape_configs:
- job_name: upwatch
static_configs:
- targets: ['upwatch:8080']
Em upwatch_monitor_status, 1 é no ar, 0 fora do ar, 2 degradado e -1
sem medição. "Sem medição" não é zero de propósito: confundir os dois
faria o alerta disparar para todo monitor recém-criado.
O endereço do alvo nunca vira rótulo. Além de descrever a topologia
interna, endereço em rótulo é cardinalidade alta — é assim que se derruba
um Prometheus.
Desenvolvimento
make test # suíte completa, com detector de corrida
make test-fast # pula a simulação de retenção de 30 dias
make lint # vet, gofmt e tsc
make build # interface + binário estático
A interface é embarcada por go:embed, então compilá-la é pré-requisito
de compilar o servidor — o make build cuida da ordem.
Para mexer no desenho sem recompilar o binário a cada mudança, suba os
dois lados:
go run ./cmd/upwatch # num terminal
cd web && pnpm dev # noutro; encaminha /api para o Go
Sobre os testes
O projeto é escrito em TDD, e a suíte é o principal artefato de desenho.
Duas partes merecem destaque:
A suíte de conformidade do armazenamento roda a mesma bateria contra
SQLite e PostgreSQL — 124 casos idênticos nos dois, zero pulados. É o que
impede "banco plugável" de virar fachada. Para rodar contra PostgreSQL:
UPWATCH_TEST_POSTGRES_DSN='postgres://...?sslmode=disable' go test ./internal/store/sqlstore/
Os testes de invasão da página pública atacam a única superfície sem
credencial: travessia de caminho em nove formas, injeção de SQL no slug,
enumeração de páginas, texto hostil, cabeçalho Host forjado. Um deles
encontrou um defeito real durante o desenvolvimento.
Como contribuir
Leia CONTRIBUTING.md antes de abrir um pull request —
sobretudo a parte de testes, que é onde este projeto tem opinião.
Falha de segurança não vai em issue pública: veja
SECURITY.md.
Licença
AGPL-3.0. Você pode usar, modificar e distribuir; se oferecer o UpWatch
modificado como serviço para terceiros, precisa disponibilizar o código
das suas modificações.