Files
at-container-registry/pkg/logging/victoria.go
2026-01-08 22:52:32 -06:00

113 lines
2.6 KiB
Go

package logging
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// VictoriaShipper ships logs to Victoria Logs using the native JSON lines endpoint.
type VictoriaShipper struct {
url string
client *http.Client
service string
username string
password string
}
// NewVictoriaShipper creates a new Victoria Logs shipper.
func NewVictoriaShipper(cfg ShipperConfig) (*VictoriaShipper, error) {
if cfg.URL == "" {
return nil, fmt.Errorf("victoria logs URL is required")
}
return &VictoriaShipper{
url: cfg.URL,
service: cfg.Service,
client: &http.Client{
Timeout: 30 * time.Second,
},
username: cfg.Username,
password: cfg.Password,
}, nil
}
// Ship sends a batch of log entries to Victoria Logs.
func (v *VictoriaShipper) Ship(ctx context.Context, entries []LogEntry) error {
if len(entries) == 0 {
return nil
}
var buf bytes.Buffer
for _, entry := range entries {
doc := map[string]any{
// Victoria Logs special fields
"_time": entry.Time.UTC().Format(time.RFC3339Nano),
"_msg": entry.Message,
// Standard fields
"level": entry.Level.String(),
"source": entry.Source,
}
// Add service if configured
if v.service != "" {
doc["service"] = v.service
}
// Add all custom attributes
for k, val := range entry.Attrs {
// Don't overwrite special fields
if k != "_time" && k != "_msg" && k != "level" && k != "source" && k != "service" {
doc[k] = val
}
}
if err := json.NewEncoder(&buf).Encode(doc); err != nil {
return fmt.Errorf("failed to encode log entry: %w", err)
}
}
// Use the JSON lines endpoint with stream fields for efficient querying
url := v.url + "/insert/jsonline?_stream_fields=service,level"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/stream+json")
// Add basic auth if configured
if v.username != "" && v.password != "" {
req.SetBasicAuth(v.username, v.password)
}
resp, err := v.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send logs: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return fmt.Errorf("victoria logs error %d: %s", resp.StatusCode, string(body))
}
// Drain and close body to allow connection reuse
_, _ = io.Copy(io.Discard, resp.Body)
return nil
}
// Close releases any resources held by the shipper.
func (v *VictoriaShipper) Close() error {
v.client.CloseIdleConnections()
return nil
}