1.Containers: slog JSON to 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: With the Vector examples set
_msg_field: msg and _time_field: time in the sink query.2.No agent: a batching writer for slog or 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)Fields you get
These show up in the fields panel and can be used in every filter:
service,envlevelmsgas the message- every slog attribute / zap field
Tips
- Use
io.MultiWriter(os.Stdout, w)so you keep local output while shipping. - zap:
zapcore.EncoderConfig{TimeKey: "time", LevelKey: "level", MessageKey: "msg", EncodeTime: zapcore.RFC3339NanoTimeEncoder}matches the field names used above. - OpenTelemetry Go:
go.opentelemetry.io/contrib/bridges/otelslogbridges slog to the OTLP exporter; endpoint and headers as on the OpenTelemetry page.
FAQ
- What happens to lines while UptimeEye is unreachable?
- The writer above drops them after one failed attempt — acceptable for a sidecar-less demo, not for production. An agent with a disk buffer (Vector, Fluent Bit) retries for you.
- Where do I get the ingest key?
- In the app under API Keys → New API Key → type “Log ingest”. The key starts with
ue_ingest_and is shown once. Management keys (ue_live_) are refused by the ingest endpoint. - How do I check that logs arrive?
- Open Logs in the app, pick the 15m range and search for
service:=<your service>. New lines are searchable within about a second; Live tail shows them with a ~6 s delay.
Related
HTTP API / curlThe UptimeEye Logs ingest API: send JSON lines with curl or your own code, choose stream fields, compress with gzip and read the response codes..KubernetesCollect stdout/stderr of every pod with a Vector DaemonSet and send it to UptimeEye Logs with namespace, container and node attached.OpenTelemetryPoint an OpenTelemetry Collector or any OTel SDK at UptimeEye Logs over OTLP/HTTP.Searching logsNow that lines arrive: filters, fields, time ranges and live tail.