Go-Logs an UptimeEye senden (slog, zap)

log/slog von Go (und zap) liefern schon die richtige Form: ein JSON-Objekt pro Zeile mit time, level, msg und deinen Attributen. In Containern schreibst du nach stdout und lässt den Agent verschicken. Ohne Agent packst du den Handler in einen kleinen bündelnden Writer.

Endpunkt: https://logs.uptimeeye.com/v1/ingest/jsonline

1.Container: slog-JSON nach stdout

main.go
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).With(
    "service", "checkout",
    "env", os.Getenv("ENV"),
)
slog.SetDefault(logger)

slog.Info("payment authorized", "order_id", "4711", "duration_ms", 812)
// {"time":"2026-09-04T11:42:08.877Z","level":"INFO","msg":"payment authorized","service":"checkout","env":"prod","order_id":"4711","duration_ms":812}
Note: Setz bei den Vector-Beispielen _msg_field: msg und _time_field: time in der Query des Sinks.

2.Ohne Agent: ein bündelnder Writer für slog oder zap

uptimeeye.go
package uptimeeye

import (
	"bytes"
	"net/http"
	"os"
	"sync"
	"time"
)

const endpoint = "https://logs.uptimeeye.com/v1/ingest/jsonline?_msg_field=msg&_time_field=time&_stream_fields=service,env"

// Writer buffers JSON lines and posts them every 2s or at 256 KiB.
type Writer struct {
	mu  sync.Mutex
	buf bytes.Buffer
	key string
}

func New() *Writer {
	w := &Writer{key: os.Getenv("UPTIMEEYE_INGEST_KEY")}
	go func() {
		for range time.Tick(2 * time.Second) {
			w.Flush()
		}
	}()
	return w
}

func (w *Writer) Write(p []byte) (int, error) {
	w.mu.Lock()
	w.buf.Write(p)
	full := w.buf.Len() >= 256<<10
	w.mu.Unlock()
	if full {
		w.Flush()
	}
	return len(p), nil
}

func (w *Writer) Flush() {
	w.mu.Lock()
	if w.buf.Len() == 0 {
		w.mu.Unlock()
		return
	}
	body := make([]byte, w.buf.Len())
	copy(body, w.buf.Bytes())
	w.buf.Reset()
	w.mu.Unlock()

	req, _ := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+w.key)
	req.Header.Set("Content-Type", "application/x-ndjson")
	if res, err := http.DefaultClient.Do(req); err == nil {
		res.Body.Close()
	}
}

// usage:
//   w := uptimeeye.New(); defer w.Flush()
//   slog.SetDefault(slog.New(slog.NewJSONHandler(io.MultiWriter(os.Stdout, w), nil)).With("service", "checkout", "env", "prod"))
//   zap: zapcore.NewCore(zapcore.NewJSONEncoder(cfg), zapcore.AddSync(w), zap.InfoLevel)

Felder, die du bekommst

Diese Felder erscheinen im Panel „Felder“ und funktionieren in jedem Filter:

  • service, env
  • level
  • msg als Nachricht
  • jedes slog-Attribut / zap-Feld

Tipps

  • Nimm io.MultiWriter(os.Stdout, w), dann behältst du die lokale Ausgabe und verschickst trotzdem.
  • zap: zapcore.EncoderConfig{TimeKey: "time", LevelKey: "level", MessageKey: "msg", EncodeTime: zapcore.RFC3339NanoTimeEncoder} passt zu den Feldnamen oben.
  • OpenTelemetry für Go: go.opentelemetry.io/contrib/bridges/otelslog verbindet slog mit dem OTLP-Exporter; Endpoint und Header wie auf der OpenTelemetry-Seite.

FAQ

Was passiert mit Zeilen, solange UptimeEye nicht erreichbar ist?

Der Writer oben verwirft sie nach einem fehlgeschlagenen Versuch – in Ordnung für eine Demo ohne Sidecar, nicht für die Produktion. Ein Agent mit Plattenpuffer (Vector, Fluent Bit) versucht es für dich erneut.

Wo bekomme ich den Ingest-Key?

In der App unter API-Keys → Neuer API-Key → Typ „Log-Ingest“. Der Key beginnt mit ue_ingest_ und wird nur einmal angezeigt. Management-Keys (ue_live_) lehnt der Ingest-Endpoint ab.

Wie sehe ich, ob Logs ankommen?

Öffne Logs in der App, wähl den Zeitraum „15 Min.“ und such nach service:=<your service>. Neue Zeilen sind nach etwa einer Sekunde durchsuchbar; Live-Tail zeigt sie mit etwa 6 s Verzögerung.