Gist: Push logs to Loki with Go
Push logs to Loki HTTP API with Go (only using the standard library).
Last updated on: 2024-12-24
Stream type definitions (and JSON marshaling)
package loki
import (
"bytes"
"encoding/json"
"fmt"
"time"
)
type Streams struct {
Streams []*Stream `json:"streams"`
}
type Stream struct {
Stream map[string]string `json:"stream"` // Labels to attach to logs.
Values []*StreamValue `json:"values"` // Actual logs.
}
type StreamValue struct {
At time.Time
Line string
Data [][2]string
}
func (s *StreamValue) MarshalJSON() ([]byte, error) {
b := &bytes.Buffer{}
lineJSON, err := json.Marshal(s.Line)
if err != nil {
return nil, fmt.Errorf("encode log message line: %w", err)
}
b.WriteString("[\"" + Timestamp(s.At) + "\", " + string(lineJSON) + "")
if len(s.Data) > 0 {
b.WriteString(", {")
for i, field := range s.Data {
if i > 0 {
b.WriteString(",")
}
fieldKey, err := json.Marshal(field[0])
if err != nil {
return nil, fmt.Errorf("encode field key (%d): %w", i, err)
}
fieldValue, err := json.Marshal(field[1])
if err != nil {
return nil, fmt.Errorf("encode field value: %q %w", field[0], err)
}
b.WriteString(string(fieldKey) + ": " + string(fieldValue))
}
b.WriteString("}")
}
b.WriteString("]")
return b.Bytes(), nil
}
Utilities
package loki
package loki
import (
"strconv"
"time"
)
// Unix epoch in nanoseconds.
func Timestamp(t time.Time) string {
return strconv.FormatInt(t.UnixNano(), 10)
}
HTTP logs pusher
package loki
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
type HTTPLogsPusher struct {
url string // Ex: https://example.org/loki/api/v1/push
httpAuthUsername string // Username for HTTP basic auth.
httpAuthPassword string // Password pair for HTTP basic auth.
httpClient *http.Client // HTTP client to use for Loki's HTTP API.
}
func NewHTTPLogsPusher(url, authUsername, authPassword string, httpClient *http.Client) *HTTPLogsPusher {
return &HTTPLogsPusher{
url: url,
httpAuthUsername: authUsername,
httpAuthPassword: authPassword,
httpClient: httpClient,
}
}
func (c *HTTPLogsPusher) Push(ctx context.Context, streams *Streams) error {
// Encode JSON request body.
b, err := json.Marshal(streams)
if err != nil {
return fmt.Errorf("encode streams: %w", err)
}
// Push logs to Loki.
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(b))
if err != nil {
return fmt.Errorf("init HTTP request: %w", err)
}
req.SetBasicAuth(c.httpAuthUsername, c.httpAuthPassword)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("send HTTP request: %w", err)
}
defer resp.Body.Close()
// Ensure we got a 204.
if resp.StatusCode != http.StatusNoContent {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
respBody = nil
}
return fmt.Errorf("unexpected response: %s: %s", resp.Status, respBody)
}
return nil
}