eventuo11y-prometheus-0.1.0.0: src/Observe/Event/Render/Prometheus.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Description : EventBackend for rendering events as Prometheus metrics
-- Copyright : Copyright 2023 Shea Levy.
-- License : Apache-2.0
-- Maintainer : shea@shealevy.com
module Observe.Event.Render.Prometheus where
import Control.Exception
import Data.Foldable
import Data.IORef
import Data.Map
import Data.Traversable
import Observe.Event.Backend
import System.Metrics.Prometheus.Concurrent.Registry
import qualified System.Metrics.Prometheus.Metric.Counter as PC
import qualified System.Metrics.Prometheus.Metric.Gauge as PG
import qualified System.Metrics.Prometheus.Metric.Histogram as PH
import System.Metrics.Prometheus.MetricId
import Prelude hiding (lookup)
-- | An 'EventBackend' that populates a 'Registry'.
--
-- All metrics are registered before the backend is returned.
prometheusEventBackend :: forall es s. (EventMetrics es) => Registry -> RenderSelectorPrometheus s es -> IO (EventBackend IO PrometheusReference s)
prometheusEventBackend registry render = do
counters <- fmap fromAscList . for [minBound @(Counter es) ..] $ \cId -> do
c <- registerCounter (metricName cId) (metricLabels cId) registry
pure (cId, c)
gauges <- fmap fromAscList . for [minBound @(Gauge es) ..] $ \gId -> do
g <- registerGauge (metricName gId) (metricLabels gId) registry
pure (gId, g)
histograms <- fmap fromAscList . for [minBound @(Histogram es) ..] $ \hId -> do
h <- registerHistogram (metricName hId) (metricLabels hId) (metricBounds hId) registry
pure (hId, h)
let m !@ k = case lookup k m of
Just a -> pure a
Nothing -> throwIO NonExhaustiveMetricEnumeration
modifyCounter (AddCounter v) = PC.add v
modifyCounter IncCounter = PC.inc
modifyGauge (AddGauge v) = PG.add v
modifyGauge (Sub v) = PG.sub v
modifyGauge IncGauge = PG.inc
modifyGauge Dec = PG.dec
modifyGauge (Set v) = PG.set v
modifyHistogram (Observe v) = PH.observe v
performModification (ModifyCounter modC cId) =
counters !@ cId >>= modifyCounter modC
performModification (ModifyGauge modG gId) =
gauges !@ gId >>= modifyGauge modG
performModification (ModifyHistogram modH hId) =
histograms !@ hId >>= modifyHistogram modH
performModifications = traverse_ performModification
pure $
EventBackend
{ newEvent = \(NewEventArgs {..}) -> do
let PrometheusRendered {..} = render newEventSelector
performModifications $ onStart newEventInitialFields Extended
fieldsRef <- newIORef []
pure $
Event
{ reference = PrometheusReference,
addField = \f -> do
performModifications $ onField f
atomicModifyIORef' fieldsRef $ \fields ->
(f : fields, ()),
finalize = \e -> do
fields <- readIORef fieldsRef
performModifications $ onFinalize e (newEventInitialFields ++ (reverse fields))
},
emitImmediateEvent = \(NewEventArgs {..}) -> do
let PrometheusRendered {..} = render newEventSelector
performModifications $ onStart newEventInitialFields Immediate
pure PrometheusReference
}
-- | A specification of a collection of prometheus metrics.
--
-- Note that due to limitations in the underlying prometheus client library, summaries are not yet supported.
class (EventMetric (Counter es), EventMetric (Gauge es), EventHistogram (Histogram es)) => EventMetrics es where
-- | The [counters](https://prometheus.io/docs/concepts/metric_types/#counter)
type Counter es
-- | The [gauges](https://prometheus.io/docs/concepts/metric_types/#gauge)
type Gauge es
-- | The [histograms](https://prometheus.io/docs/concepts/metric_types/#histogram)
type Histogram es
-- | A specification of a single prometheus metric of any type
--
-- Must satisfy @∀ x : a, x \`elem\` [minBound .. maxBound]@
class (Ord a, Enum a, Bounded a) => EventMetric a where
-- | The [name](https://prometheus.io/docs/practices/naming/#metric-names) of the metric
metricName :: a -> Name
-- | The [labels](https://prometheus.io/docs/practices/naming/#labels) of the metric
metricLabels :: a -> Labels
-- | A specification of a prometheus [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram)
class (EventMetric h) => EventHistogram h where
-- | The upper bounds of the histogram buckets.
metricBounds :: h -> [PH.UpperBound]
-- | Render all events selectable by @s@ to prometheus metrics according to 'EventMetrics' @es@
--
-- We may want to add functionality for easily combining 'RenderSelectorPrometheus's and 'EventMetrics'
-- from nested selector types, possibly with additional labels layered on top.
type RenderSelectorPrometheus s es = forall f. s f -> PrometheusRendered f es
-- | How to render a specific 'Event' according to 'EventMetrics' @es@
data PrometheusRendered f es = PrometheusRendered
{ -- | Modify metrics at event start
--
-- Passed the 'newEventInitialFields'.
onStart :: !([f] -> EventDuration -> [MetricModification es]),
-- | Modify metrics when a field is added
--
-- Only called for events added with 'addField'
onField :: !(f -> [MetricModification es]),
-- | Modify metrics when an event finishes.
--
-- Passed all event fields (both initial fields and those added
-- during the event lifetime).
--
-- This is not called if the event is 'Immediate'.
onFinalize :: !(Maybe SomeException -> [f] -> [MetricModification es])
}
-- | DSL for modifying metrics specified in 'EventMetrics' @es@
data MetricModification es
= -- | Modify the specified counter
ModifyCounter !CounterModification !(Counter es)
| -- | Modify the specified gauge
ModifyGauge !GaugeModification !(Gauge es)
| -- | Modify the specified histogram
ModifyHistogram !HistogramModification !(Histogram es)
-- | DSL for modifying a counter metric
data CounterModification
= -- | Add a value to a counter
AddCounter !Int
| -- | Increment a counter
IncCounter
-- | DSL for modifying a gauge metric
data GaugeModification
= -- | Add a value to a gauge
AddGauge !Double
| -- | Subtract a value from a gauge
Sub !Double
| -- | Increment a gauge
IncGauge
| -- | Decrement a gauge
Dec
| -- | Set the value of a gauge
Set !Double
-- | DSL for modifying a histogram metric
data HistogramModification
= -- | Record an observation
Observe !Double
-- | What duration event is this?
data EventDuration
= -- | A immediately finalized event
Immediate
| -- | An event with an extended lifetime
Extended
-- | Reference type for 'prometheusEventBackend'
--
-- Prometheus can't make use of references, so this carries no information.
data PrometheusReference = PrometheusReference
-- | Exception thrown if we encounter an element of an 'EventMetric' that
-- is not in @[minBound .. maxBound]@
data NonExhaustiveMetricEnumeration = NonExhaustiveMetricEnumeration deriving (Show)
instance Exception NonExhaustiveMetricEnumeration