Last update: 2024-12-24
Pushing Logs to Loki with Go
Here's how you can push logs to the Loki HTTP API using Go, without any 3rd party dependencies.
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
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
}