Julien's dev blog

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
}
pkg/loki/streams.go

Utilities

package loki

package loki

import (
	"strconv"
	"time"
)

// Unix epoch in nanoseconds.
func Timestamp(t time.Time) string {
	return strconv.FormatInt(t.UnixNano(), 10)
}
pkg/loki/util.go

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
}
pkg/loki/http_client.go