Julien Sellier

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
}