packages feed

otel-effectful-1.0.0: src/Effectful/OpenTelemetry/Metrics/Gauge.hs

{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

module Effectful.OpenTelemetry.Metrics.Gauge
    ( Gauge
    , new
    , set
    , setIO
    , measurement
    , Payload (..)
    )
where

import Control.Concurrent.STM (TMVar)
import Control.Concurrent.STM qualified as STM
import Data.Aeson.Types (ToJSON (..))
import Data.Maybe (maybeToList)
import Data.Scientific (Scientific)
import Data.Text (Text)
import Effectful
import Effectful.Dispatch.Static (unsafeEff_)
import Effectful.OpenTelemetry.Metrics.Effect (Metrics)
import Effectful.OpenTelemetry.Metrics.Effect qualified as Metrics
import Effectful.OpenTelemetry.Metrics.Instrument (Instrument)
import Effectful.OpenTelemetry.Metrics.Instrument qualified as Instrument
import Effectful.OpenTelemetry.Metrics.Measurement
    ( Measurement
    , Metric (..)
    , NumberDataPoint (..)
    )
import Effectful.OpenTelemetry.Metrics.Measurement qualified as Measurement
import Effectful.OpenTelemetry.Metrics.Metadata (Metadata (..))
import Effectful.OpenTelemetry.Timestamp (Timestamp)
import Effectful.OpenTelemetry.Timestamp qualified as Timestamp
import GHC.Generics (Generic)
import Proto3.Wire.Encode.Class qualified as Proto
import Prelude

-- | A synchronous 'Instrument' which records non-additive values when changes occur.
--
-- Example uses for 'Gauge':
--
-- - subscribe to change events for the background noise level
-- - subscribe to change events for the CPU fan speed
--
-- See <https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge the OpenTelemetry spec>.
data Gauge = Gauge
    { name :: Text
    , startTime :: Timestamp
    , value :: TMVar (Timestamp, Scientific)
    , metadata :: Metadata
    }

-- | Create a new 'Gauge' and register it to be sampled and exported.
new :: (Metrics :> es) => Text -> Metadata -> Eff es Gauge
new name metadata =
    Metrics.register =<< unsafeEff_ do
        startTime <- Timestamp.now
        value <- STM.newEmptyTMVarIO
        pure Gauge{..}

-- | Set a 'Gauge' to the given value.
set :: (Metrics :> es) => Gauge -> Scientific -> Eff es ()
set = (unsafeEff_ .) . setIO

setIO :: Gauge -> Scientific -> IO ()
setIO gauge value = do
    time <- Timestamp.now
    STM.atomically . STM.writeTMVar gauge.value $ (time, value)

-- | A gauge 'Measurement' of a single observed data point.
measurement :: Metadata -> Text -> NumberDataPoint -> Measurement
measurement metadata name dataPoint =
    Measurement.create name metadata Payload{dataPoints = [dataPoint]}

instance Instrument Gauge where
    name = name
    sample Gauge{metadata = metadata@Metadata{..}, ..} = do
        measurements <- maybeToList <$> STM.atomically (STM.tryReadTMVar value)
        pure
            [ (time, measurement metadata name NumberDataPoint{..})
            | (time, value) <- measurements
            ]

newtype Payload = Payload {dataPoints :: [NumberDataPoint]}
    deriving stock (Generic, Show, Eq)
    deriving anyclass (ToJSON)

instance Proto.Encode Payload where
    encode Payload{..} = foldMap (Proto.encodeField 1) dataPoints

instance Metric Payload where
    metricJsonKey _ = "gauge"
    metricProtoFieldNumber _ = 5