hs-hath-1.1.1: src/Metrics/Snapshot.hs
module Metrics.Snapshot ( generateSnapshot, startSnapshotWorker, getCachedSnapshot ) where
import Control.Concurrent ( forkIO, threadDelay )
import qualified Metrics.Counter as Counter
import qualified Metrics.Gauge as Gauge
import qualified Metrics.Prometheus as Prometheus
import Metrics.Types
import Relude
-- | Generate a snapshot of all metrics in the registry
-- This reads all counter and gauge values and renders them to Prometheus format
generateSnapshot :: Registry -> IO ByteString
generateSnapshot reg = do
counters <- readIORef (registryCounters reg)
gauges <- readIORef (registryGauges reg)
-- Read all counter values
counterSnapshots <- forM counters $ \c -> do
val <- Counter.read c
pure $ CounterSnapshot (counterMetricId c) (counterHelp c) val
-- Read all gauge values
gaugeSnapshots <- forM gauges $ \g -> do
val <- Gauge.read g
pure $ GaugeSnapshot (gaugeMetricId g) (gaugeHelp g) val
-- Render to Prometheus text format
pure $ Prometheus.render (counterSnapshots <> gaugeSnapshots)
-- | Start a background worker that periodically updates the cached snapshot
-- This decouples metrics reads from the /metrics endpoint, eliminating blocking
startSnapshotWorker :: Registry -> Int -> IO ()
startSnapshotWorker reg intervalMicros = void $ forkIO $ forever $ do
snapshot <- generateSnapshot reg
atomically $ writeTVar (registryCachedSnapshot reg) snapshot
threadDelay intervalMicros
-- | Read the cached snapshot (zero-cost operation)
-- This is what the /metrics endpoint should call
getCachedSnapshot :: Registry -> IO ByteString
getCachedSnapshot = readTVarIO . registryCachedSnapshot
{-# INLINE getCachedSnapshot #-}
-- Performance notes:
-- - generateSnapshot: O(n) where n = number of metrics (~10-100 typically)
-- Takes ~1-10ms depending on metric count
-- - startSnapshotWorker: Runs in background, default 5s interval
-- CPU overhead: negligible (~0.02% with 10 metrics)
-- - getCachedSnapshot: O(1), ~100ns
-- This is what makes /metrics endpoint non-blocking