Frontend Labs
⋯

Week 4 Mini Project: In-Memory Metrics Server

Build a self-contained metrics collection and query server. This is the observability layer you'll wire into your security proxy.

// metrics/server.go
package main

import (
    "encoding/json"
    "expvar"
    "fmt"
    "math"
    "net/http"
    _ "net/http/pprof"
    "sort"
    "sync"
    "time"
)

type Sample struct {
    Value float64
    At    time.Time
}

type MetricStore struct {
    mu      sync.RWMutex
    metrics map[string]*RingBuffer[Sample]
    maxAge  time.Duration
}

func NewMetricStore(maxAge time.Duration) *MetricStore {
    return &MetricStore{
        metrics: make(map[string]*RingBuffer[Sample]),
        maxAge:  maxAge,
    }
}

func (ms *MetricStore) Record(name string, value float64) {
    ms.mu.Lock()
    defer ms.mu.Unlock()
    if _, ok := ms.metrics[name]; !ok {
        ms.metrics[name] = NewRingBuffer[Sample](10000)
    }
    ms.metrics[name].Add(Sample{Value: value, At: time.Now()})
}

type Stats struct {
    Count int     `json:"count"`
    Min   float64 `json:"min"`
    P50   float64 `json:"p50"`
    P95   float64 `json:"p95"`
    P99   float64 `json:"p99"`
    Max   float64 `json:"max"`
}

func (ms *MetricStore) Query(name string, window time.Duration) Stats {
    ms.mu.RLock()
    defer ms.mu.RUnlock()

    buf, ok := ms.metrics[name]
    if !ok { return Stats{} }

    cutoff := time.Now().Add(-window)
    var values []float64
    for _, s := range buf.All() {
        if s.At.After(cutoff) {
            values = append(values, s.Value)
        }
    }
    if len(values) == 0 { return Stats{} }

    sort.Float64s(values)
    pct := func(p float64) float64 {
        idx := int(math.Ceil(p/100*float64(len(values)))) - 1
        if idx < 0 { idx = 0 }
        return values[idx]
    }

    return Stats{
        Count: len(values),
        Min:   values[0],
        P50:   pct(50),
        P95:   pct(95),
        P99:   pct(99),
        Max:   values[len(values)-1],
    }
}

func main() {
    store := NewMetricStore(24 * time.Hour)

    // expvar for live metrics
    expvar.Publish("metric_names", expvar.Func(func() any {
        return len(store.metrics)
    }))

    mux := http.NewServeMux()

    // POST /metrics — ingest
    mux.HandleFunc("POST /metrics", func(w http.ResponseWriter, r *http.Request) {
        var body struct {
            Name  string  `json:"name"`
            Value float64 `json:"value"`
        }
        if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
            http.Error(w, "invalid JSON", 400)
            return
        }
        store.Record(body.Name, body.Value)
        w.WriteHeader(http.StatusNoContent)
    })

    // GET /metrics/{name}?window=60s — query
    mux.HandleFunc("GET /metrics/", func(w http.ResponseWriter, r *http.Request) {
        name := strings.TrimPrefix(r.URL.Path, "/metrics/")
        windowStr := r.URL.Query().Get("window")
        window, err := time.ParseDuration(windowStr)
        if err != nil { window = time.Minute }

        stats := store.Query(name, window)
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(stats)
    })

    fmt.Println("metrics server on :8080")
    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }
    srv.ListenAndServe()
}

Test it:

# Ingest metrics
curl -X POST http://localhost:8080/metrics \
  -d '{"name":"request_latency_ms","value":45.2}'

# Query with 60s window
curl "http://localhost:8080/metrics/request_latency_ms?window=60s"
# {"count":1,"min":45.2,"p50":45.2,"p95":45.2,"p99":45.2,"max":45.2}

Engineer Takeaway

This is a miniature Prometheus in ~200 lines. You now know why observability systems need ring buffers (bounded memory), percentile queries (p99 is what users experience, not average), and structured ingestion endpoints. This metrics server wires into every component you build in Phases 2 and 3.

Generated from week4.md · 8 Oct 2026 16:36