Skip to content

Gin (Go)

The agent observes a Gin service via two channels: stdout for logs, and a Prometheus /metrics endpoint for HTTP metrics. No application-side Lumin SDK — the agent reads what your service already exposes.

services:
api:
image: my-gin-api:latest
labels:
lumin.logs: "true"
lumin.service: "api"
lumin.metrics: "true"
lumin.metrics.port: "8080"
lumin.metrics.framework: "gin"

Use prometheus/client_golang plus a Gin middleware. The Lumin team’s lumin-api service uses exactly this approach — the middleware records request count, latency histogram, and error count, and the canonical Lumin schema (http.requests, http.latency_ms, http.errors) is reconstructed by the agent from the underlying Prometheus names when lumin.metrics.framework=gin is set.

Minimal middleware shape:

package main
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
requests = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total"},
[]string{"method", "route", "status_class"},
)
latency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_ms",
Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
},
[]string{"method", "route"},
)
errors = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_errors_total"},
[]string{"method", "route", "status"},
)
)
func init() {
prometheus.MustRegister(requests, latency, errors)
}
func HTTPMetrics() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
route := c.FullPath() // templated route, e.g. /projects/:id
if route == "" {
return // 404, don't pollute cardinality
}
status := c.Writer.Status()
class := strconv.Itoa(status/100) + "xx"
requests.WithLabelValues(c.Request.Method, route, class).Inc()
latency.WithLabelValues(c.Request.Method, route).Observe(float64(time.Since(start).Milliseconds()))
if status >= 500 {
errors.WithLabelValues(c.Request.Method, route, strconv.Itoa(status)).Inc()
}
}
}
func main() {
r := gin.Default()
r.Use(HTTPMetrics())
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
// ...your routes...
r.Run(":8080")
}

Three things to watch:

  • c.FullPath() returns the templated route, e.g. /projects/:id. Always use this — c.Request.URL.Path is the expanded path and will explode your cardinality.
  • Skip non-routes. When a request doesn’t match any registered route, FullPath() is empty. Don’t record metrics for those — they’re 404s and they’d all share a "" route label.
  • Skip the metrics endpoint itself. Either return early in the middleware for /metrics, or don’t Use() the middleware on that route, otherwise your dogfood charts count the agent’s own scrapes as application traffic.

Use any JSON logger writing to stdout — slog with the JSON handler is the boring choice and works perfectly. The agent’s log tailer recognises JSON lines with a level field and stores everything in the attributes JSON column.

package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("server starting", "port", 8080)
}

Lumin reads level out of the object to set the row’s level, and stores the whole object as attributes. The row’s timestamp comes from Docker’s log timestamp rather than a time or ts field, and message keeps the raw JSON line. No field is promoted to a dedicated column — trace_id, span_id, and session_id are all reachable as attr.<key>.

If you use @lumin-monitor/browser, have it send the session ID as X-Lumin-Session on every API request, then read it server-side and include it on every log line for that request:

func WithSession() gin.HandlerFunc {
return func(c *gin.Context) {
sid := c.GetHeader("X-Lumin-Session")
if sid != "" {
c.Set("session_id", sid)
}
c.Next()
}
}
// In your handler:
slog.InfoContext(c.Request.Context(), "fetched project", "session_id", c.GetString("session_id"))

Once that field is on the log row, clicking a 500 in the Logs UI will jump to the matching browser session.