packages feed

kiroku-metrics-0.4.0.0: src/Kiroku/Metrics/Types.hs

-- | Immutable, JSON-encodable snapshot of a running Kiroku store's operational
-- metrics.
--
-- A 'MetricsSnapshot' is produced by
-- 'Kiroku.Metrics.Collector.snapshotMetrics' and is the single source of truth
-- every endpoint renders. It carries three parts:
--
-- * 'StoreGauges' — point-in-time gauges read from the live store handle at
--   snapshot time: the gap-free global position (which doubles as the total
--   number of events appended store-wide and the high-water mark), the active
--   subscriber count, and pool connection gauges/counters.
-- * 'LifecycleCounters' — monotonic counters accumulated from the store's
--   'Kiroku.Store.Observability.KirokuEvent' callback stream.
-- * a per-subscription map ('SubscriptionMetrics') keyed by subscription name,
--   carrying each subscription's last-known position, derived lag, database-error
--   count, and last stop reason.
--
-- The 'ToJSON' instances are written by hand (not derived) so the wire shape is
-- stable and documented for downstream consumers (EP-2's JSON/Prometheus
-- renderers and EP-4's user guide).
module Kiroku.Metrics.Types
  ( MetricsSnapshot (..),
    StoreGauges (..),
    LifecycleCounters (..),
    SubscriptionMetrics (..),
  )
where

import Data.Aeson (ToJSON (..), object, (.=))
import Data.Int (Int64)
import Data.Map.Strict (Map)
import Data.Text (Text)

-- | A coherent point-in-time view of the store's metrics.
data MetricsSnapshot = MetricsSnapshot
  { store :: !StoreGauges,
    counters :: !LifecycleCounters,
    -- | Keyed by 'Kiroku.Store.Subscription.Types.SubscriptionName' text.
    subscriptions :: !(Map Text SubscriptionMetrics)
  }
  deriving stock (Eq, Show)

-- | Gauges read from the live store handle at snapshot time.
data StoreGauges = StoreGauges
  { -- | Total events appended store-wide (gap-free) == high-water mark.
    globalPosition :: !Int64,
    activeSubscribers :: !Int,
    poolConnecting :: !Int,
    poolReady :: !Int,
    poolInUse :: !Int,
    poolEstablishedTotal :: !Int64,
    poolTerminatedTotal :: !Int64
  }
  deriving stock (Eq, Show)

-- | Monotonic counters accumulated from the 'KirokuEvent' callback stream.
--
-- The constructor set the store emits is richer than counters here name one-to-one:
-- subscription lifecycle events ('Started', 'CaughtUp', 'Paused', 'Resumed',
-- 'Reconnecting', 'Retrying', 'DeadLettered', 'Stopped' by reason), per-batch
-- delivery, live fetches, per-phase database errors, notifier reconnects,
-- publisher pool and loop errors, and hard deletes.
data LifecycleCounters = LifecycleCounters
  { notifierReconnecting :: !Int64,
    notifierReconnected :: !Int64,
    publisherPoolErrors :: !Int64,
    publisherLoopErrors :: !Int64,
    publisherDecodeFailures :: !Int64,
    subscriptionHandlerStalls :: !Int64,
    subscriptionDbErrorsLoad :: !Int64,
    subscriptionDbErrorsFetch :: !Int64,
    subscriptionDbErrorsSave :: !Int64,
    subscriptionsStarted :: !Int64,
    subscriptionsCaughtUp :: !Int64,
    subscriptionsPaused :: !Int64,
    subscriptionsResumed :: !Int64,
    subscriptionsReconnecting :: !Int64,
    subscriptionsRetrying :: !Int64,
    subscriptionsDeadLettered :: !Int64,
    subscriptionsStoppedHandler :: !Int64,
    subscriptionsStoppedCancelled :: !Int64,
    subscriptionsStoppedOverflow :: !Int64,
    subscriptionsStoppedCrashed :: !Int64,
    subscriptionsStoppedUndecodable :: !Int64,
    -- | Count of live-mode DB fetches ('KirokuEventSubscriptionFetched').
    liveFetches :: !Int64,
    -- | Count of non-empty batches delivered ('KirokuEventSubscriptionDelivered').
    batchesDelivered :: !Int64,
    -- | Sum of batch row counts across all deliveries.
    eventsDelivered :: !Int64,
    hardDeletesIssued :: !Int64
  }
  deriving stock (Eq, Show)

-- | Per-subscription accumulated metrics.
data SubscriptionMetrics = SubscriptionMetrics
  { -- | Most recent position seen at a lifecycle event (lower bound on true position).
    lastKnownPosition :: !Int64,
    -- | @max 0 (globalPosition - lastKnownPosition)@ (upper bound on true lag).
    lag :: !Int64,
    dbErrorCount :: !Int64,
    -- | @"handler" | "cancelled" | "overflow" | "crashed"@.
    lastStopReason :: !(Maybe Text)
  }
  deriving stock (Eq, Show)

instance ToJSON MetricsSnapshot where
  toJSON s =
    object
      [ "store" .= s.store,
        "counters" .= s.counters,
        "subscriptions" .= s.subscriptions
      ]

instance ToJSON StoreGauges where
  toJSON g =
    object
      [ "global_position" .= g.globalPosition,
        "active_subscribers" .= g.activeSubscribers,
        "pool_connecting" .= g.poolConnecting,
        "pool_ready" .= g.poolReady,
        "pool_in_use" .= g.poolInUse,
        "pool_established_total" .= g.poolEstablishedTotal,
        "pool_terminated_total" .= g.poolTerminatedTotal
      ]

instance ToJSON LifecycleCounters where
  toJSON c =
    object
      [ "notifier_reconnecting" .= c.notifierReconnecting,
        "notifier_reconnected" .= c.notifierReconnected,
        "publisher_pool_errors" .= c.publisherPoolErrors,
        "publisher_loop_errors" .= c.publisherLoopErrors,
        "publisher_decode_failures" .= c.publisherDecodeFailures,
        "subscription_handler_stalls" .= c.subscriptionHandlerStalls,
        "subscription_db_errors_load" .= c.subscriptionDbErrorsLoad,
        "subscription_db_errors_fetch" .= c.subscriptionDbErrorsFetch,
        "subscription_db_errors_save" .= c.subscriptionDbErrorsSave,
        "subscriptions_started" .= c.subscriptionsStarted,
        "subscriptions_caught_up" .= c.subscriptionsCaughtUp,
        "subscriptions_paused" .= c.subscriptionsPaused,
        "subscriptions_resumed" .= c.subscriptionsResumed,
        "subscriptions_reconnecting" .= c.subscriptionsReconnecting,
        "subscriptions_retrying" .= c.subscriptionsRetrying,
        "subscriptions_dead_lettered" .= c.subscriptionsDeadLettered,
        "subscriptions_stopped_handler" .= c.subscriptionsStoppedHandler,
        "subscriptions_stopped_cancelled" .= c.subscriptionsStoppedCancelled,
        "subscriptions_stopped_overflow" .= c.subscriptionsStoppedOverflow,
        "subscriptions_stopped_crashed" .= c.subscriptionsStoppedCrashed,
        "subscriptions_stopped_undecodable" .= c.subscriptionsStoppedUndecodable,
        "live_fetches" .= c.liveFetches,
        "batches_delivered" .= c.batchesDelivered,
        "events_delivered" .= c.eventsDelivered,
        "hard_deletes_issued" .= c.hardDeletesIssued
      ]

instance ToJSON SubscriptionMetrics where
  toJSON m =
    object
      [ "last_known_position" .= m.lastKnownPosition,
        "lag" .= m.lag,
        "db_error_count" .= m.dbErrorCount,
        "last_stop_reason" .= m.lastStopReason
      ]