mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
113 lines
2.6 KiB
Go
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
|
|
}
|