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":[["",""],...]}]} // 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 }