81 lines
2.3 KiB
Go
81 lines
2.3 KiB
Go
package infra
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// PushLokiStream envia lineas de log a un servidor Grafana Loki via su push API.
|
|
// Construye el cuerpo JSON con la forma {"streams":[{"stream":{labels},"values":[["<ts_ns>","<line>"],...]}]}
|
|
// y lo manda por POST a endpoint (ej "https://logs-xxxx.organic-machine.com/loki/api/v1/push").
|
|
//
|
|
// Reglas:
|
|
// - timestampsNs y lines deben tener la misma longitud; si no, retorna error antes de hacer la peticion.
|
|
// - Si len(lines)==0 es un no-op: no hace ninguna peticion y retorna nil.
|
|
// - labels va tal cual en el campo "stream".
|
|
// - Si user != "", usa Basic Auth con user/pass.
|
|
// - Content-Type: application/json. TLS verificado. Timeout 10s.
|
|
// - Exito = status 2xx (Loki devuelve 204). Si no-2xx, error con el codigo + primeros 200 bytes del cuerpo.
|
|
func PushLokiStream(endpoint string, user string, pass string, labels map[string]string, timestampsNs []int64, lines []string) error {
|
|
if len(timestampsNs) != len(lines) {
|
|
return fmt.Errorf("push_loki_stream: timestampsNs (%d) y lines (%d) tienen longitudes distintas", len(timestampsNs), len(lines))
|
|
}
|
|
|
|
// No-op cuando no hay lineas.
|
|
if len(lines) == 0 {
|
|
return nil
|
|
}
|
|
|
|
values := make([][2]string, len(lines))
|
|
for i := range lines {
|
|
values[i] = [2]string{strconv.FormatInt(timestampsNs[i], 10), lines[i]}
|
|
}
|
|
|
|
stream := labels
|
|
if stream == nil {
|
|
stream = map[string]string{}
|
|
}
|
|
|
|
body := map[string]any{
|
|
"streams": []map[string]any{
|
|
{
|
|
"stream": stream,
|
|
"values": values,
|
|
},
|
|
},
|
|
}
|
|
|
|
payload, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("push_loki_stream: marshal body: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return fmt.Errorf("push_loki_stream: build request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if user != "" {
|
|
req.SetBasicAuth(user, pass)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("push_loki_stream: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
|
|
return fmt.Errorf("push_loki_stream: HTTP %d: %s", resp.StatusCode, string(snippet))
|
|
}
|
|
|
|
return nil
|
|
}
|