Monitorando Sites com Go: Criando seu Próprio Health Check CLI

Tabela de Conteúdo

Se você precisa verificar se um endpoint interno, uma API ou um site está no ar, você tem duas opções: configurar um Zabbix/Prometheus da vida (o que pode ser um canhão para matar uma mosca) ou rodar um script rápido.

Hoje nós vamos seguir o caminho de criar um script rapido utilizando Go.

Vamos construir um CLI que recebe uma lista de URLs e nos diz o status e o tempo de resposta de cada uma.

Parte 1: O código básico (sequencial)

A ideia é simples: iterar sobre uma lista de argumentos e fazer um GET.

Vamos usar o pacote net/http que já existe na biblioteca padrão (stdlib). Importante: sempre configure um Timeout. O cliente HTTP padrão do Go não tem timeout, o que pode travar sua CLI se o servidor do outro lado estiver zumbi.

Crie um arquivo healthcheck.go:

package main

import (
    "fmt"
    "net/http"
    "os"
    "time"
)

func main() {
    // Recebemos os argumentos
    urls := os.Args[1:]
    // Validamos se existem argumentos
    if len(urls) == 0 {
        fmt.Println("Uso: ./healthcheck <url1> <url2>")
        os.Exit(1)
    }

    // Criamos um cliente HTTP customizado para ter controle do timeout
    client := http.Client{
        Timeout: 10 * time.Second,
    }

    fmt.Println("Iniciando verificações...")

    // Iteramos sobre os argumentos
    for _, url := range urls {
        check(client, url)
    }
}

// Função que verifica um único URL
func check(client http.Client, url string) {
    inicio := time.Now()

    resp, err := client.Get(url)
    if err != nil {
        fmt.Printf("[ERRO] %s está inacessível: %v\n", url, err)
        return
    }
    // A conexão é fechada
    defer resp.Body.Close()

    // time.Since calcula a diferença automaticamente
    duracao := time.Since(inicio)
    // Exibbimos o status e o tempo de resposta
    fmt.Printf("[%d] %s - %v\n", resp.StatusCode, url, duracao)
}

Rodando

go run healthcheck.go https://google.com https://github.com

Funciona? Sim. É rápido? Depende. Se você tiver 50 sites e cada um demorar 2 segundos, seu script vai levar 100 segundos.

Parte 2: O modo “Turbo” (Concorrência com Goroutines)

Aqui é onde o Go fica interessante. Vamos mudar o código para checar todos os sites ao mesmo tempo.

Em vez de chamar check() direto, vamos usar a palavra-chave go para criar uma Goroutine (uma thread leve gerenciada pelo Go/Runtime). E para saber quando tudo terminou, usaremos um sync.WaitGroup.

package main

import (
    "fmt"
    "net/http"
    "os"
    "sync"
    "time"
)

func main() {
    urls := os.Args[1:]
    var wg sync.WaitGroup // Contador de tarefas

    client := http.Client{Timeout: 10 * time.Second}

    for _, url := range urls {
        wg.Add(1) // Avisamos que tem mais uma tarefa

        // Lançamos a função em background
        go func(u string) {
            defer wg.Done() // Avisa quando terminar
            check(client, u)
        }(url)
    }

    wg.Wait() // Bloqueia até todas as tarefas terminarem
}

func check(client http.Client, url string) {
    inicio := time.Now()
    resp, err := client.Get(url)
    if err != nil {
        fmt.Printf("[ERRO] %s: %s\n", url, err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("[%d] %s - %v\n", resp.StatusCode, url, time.Since(inicio))
}

Agora, se você tiver 50 sites, o tempo total será equivalente ao site mais lento, e não a soma de todos.

Compilando nosso código

Para compilar é simples, basta executarmos o seguinte comando:

go build -o healthcheck healthcheck.go

Agora é só executar:

./healthcheck https://google.com https://github.com https://chcdc.com.br

Aqui temos algumas observações:

  1. http.Client vs DefaultClient: Nunca use http.Get() direto em produção para coisas críticas, pois ele não tem timeout. Se o site travar a conexão, sua thread fica presa para sempre.
  2. Defer: O comando defer resp.Body.Close() garante que a conexão seja finalizada e não vaze memória, mesmo se a função retornar no meio.
  3. Simplicidade: Note que não instalamos nenhuma biblioteca externa (npm install ou pip install). Tudo está na stdlib do Go.

Simples assim! :)