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.
Labels on your container
Section titled “Labels on your container”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"Emit Prometheus metrics
Section titled “Emit Prometheus metrics”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.Pathis 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’tUse()the middleware on that route, otherwise your dogfood charts count the agent’s own scrapes as application traffic.
Emit structured logs
Section titled “Emit structured logs”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>.
Propagating browser session IDs
Section titled “Propagating browser session IDs”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.