packages feed

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

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

module Effectful.OpenTelemetry.Metrics.Counter
    ( Counter
    , new
    , add
    , addIO
    )
where

import Control.Concurrent.STM (TVar)
import Control.Concurrent.STM qualified as STM
import Data.Bifunctor (bimap)
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 (NumberDataPoint (..))
import Effectful.OpenTelemetry.Metrics.Metadata (Metadata (..))
import Effectful.OpenTelemetry.Metrics.Sum (Monotonicity (..))
import Effectful.OpenTelemetry.Metrics.Sum qualified as Sum
import Effectful.OpenTelemetry.Timestamp (Timestamp)
import Effectful.OpenTelemetry.Timestamp qualified as Timestamp
import Prelude

-- | A synchronous 'Instrument' which supports non-negative increments.
--
-- Example uses for 'Counter':
--
-- - count the number of bytes received
-- - count the number of requests completed
-- - count the number of accounts created
-- - count the number of checkpoints run
-- - count the number of HTTP 5xx errors
--
-- See <https://opentelemetry.io/docs/specs/otel/metrics/api/#counter the OpenTelemetry spec>.
data Counter = Counter
    { name :: Text
    , startTime :: Timestamp
    , value :: TVar (Timestamp, Scientific)
    , metadata :: Metadata
    }

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

-- | Increment a 'Counter'.
-- WARNING: This function is partial because it throws on negative increment.
add :: (Metrics :> es) => Counter -> Scientific -> Eff es ()
add = (unsafeEff_ .) . addIO

addIO :: Counter -> Scientific -> IO ()
addIO Counter{..} inc
    | inc < 0 =
        error $ "Counter.add: negative increment (" <> show inc <> ") added to counter " <> show name
    | otherwise = do
        time <- Timestamp.now
        STM.atomically . STM.modifyTVar' value $ bimap (const time) (+ inc)

instance Instrument Counter where
    name = name
    sample Counter{..} = do
        (time, value) <- STM.readTVarIO value
        pure . pure $
            ( time
            , Sum.measurement Monotonic metadata name NumberDataPoint{attributes = metadata.attributes, ..}
            )