{- | The bridge from the telemetry-agnostic reporters the pre-telemetry providers carry
to the live @ecluse.*@ instruments -- and the deferral that lets a provider built before
the meter exists record once it does.

The circuit breaker ("Ecluse.Core.Breaker") and the refreshing credential provider
("Ecluse.Core.Credential.Refresh") are constructed at boot __before__ the telemetry substrate
(the meter provider) exists, so they cannot be handed a 'Metrics' at construction. Each
instead carries a small, telemetry-agnostic reporter callback. This module supplies those
callbacks, backed by a 'DeferredMetrics' cell: __inert__ (recording nothing) until the
composition root has built the instruments and called 'installMetrics', __live__
thereafter. That mirrors the no-op-meter discipline of "Ecluse.Telemetry.Instruments":
once installed, an inert handle (built on the SDK's no-op meter when telemetry is off)
still discards every measurement, so the providers record unconditionally either way.

The catalogue and the cardinality rule are described in
@docs\/architecture\/observability.md@.
-}
module Ecluse.Telemetry.Reporters (
    -- * Deferred metric handle
    DeferredMetrics,
    newDeferredMetrics,
    installMetrics,

    -- * Reporters over the deferred handle
    deferredBreakerReporter,
    deferredRefreshReporter,
    deferredMirrorEnqueueFailure,

    -- * Breaker-state projection
    breakerStateOf,
) where

import Ecluse.Core.Breaker (Breaker (..), BreakerReporter (..))
import Ecluse.Core.Credential.Refresh (RefreshReporter (..))
import Ecluse.Core.Telemetry.Metrics (
    BreakerSource,
    BreakerState,
    CredentialResult (RefreshFailed, Refreshed),
    Provider,
 )
import Ecluse.Core.Telemetry.Metrics qualified as Metric
import Ecluse.Telemetry.Instruments (
    Metrics,
    recordBreakerState,
    recordCredentialRefresh,
    recordCredentialTokenTtl,
    recordMirrorEnqueueFailure,
 )

{- | A 'Metrics' handle that may not exist yet: empty until the telemetry substrate has
built the instruments, then live. The pre-telemetry boot phase builds providers that
record through reporters closed over this, so they can be wired before the meter exists
and become live once it does. A record through it while empty is a no-op.
-}
newtype DeferredMetrics = DeferredMetrics (IORef (Maybe Metrics))

-- | A fresh, empty 'DeferredMetrics': every reporter over it is inert until 'installMetrics'.
newDeferredMetrics :: IO DeferredMetrics
newDeferredMetrics :: IO DeferredMetrics
newDeferredMetrics = IORef (Maybe Metrics) -> DeferredMetrics
DeferredMetrics (IORef (Maybe Metrics) -> DeferredMetrics)
-> IO (IORef (Maybe Metrics)) -> IO DeferredMetrics
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Metrics -> IO (IORef (Maybe Metrics))
forall (m :: * -> *) a. MonadIO m => a -> m (IORef a)
newIORef Maybe Metrics
forall a. Maybe a
Nothing

{- | Install the live instruments, making every reporter over this handle record through
them from now on. Called once by the composition root after 'newMetrics' has built the
instruments (which are themselves inert when telemetry is off).
-}
installMetrics :: DeferredMetrics -> Metrics -> IO ()
installMetrics :: DeferredMetrics -> Metrics -> IO ()
installMetrics (DeferredMetrics IORef (Maybe Metrics)
ref) = IORef (Maybe Metrics) -> Maybe Metrics -> IO ()
forall (m :: * -> *) a. MonadIO m => IORef a -> a -> m ()
writeIORef IORef (Maybe Metrics)
ref (Maybe Metrics -> IO ())
-> (Metrics -> Maybe Metrics) -> Metrics -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Metrics -> Maybe Metrics
forall a. a -> Maybe a
Just

-- Run an action with the live instruments if installed; a no-op while still empty.
withDeferredMetrics :: DeferredMetrics -> (Metrics -> IO ()) -> IO ()
withDeferredMetrics :: DeferredMetrics -> (Metrics -> IO ()) -> IO ()
withDeferredMetrics (DeferredMetrics IORef (Maybe Metrics)
ref) Metrics -> IO ()
record = IORef (Maybe Metrics) -> IO (Maybe Metrics)
forall (m :: * -> *) a. MonadIO m => IORef a -> m a
readIORef IORef (Maybe Metrics)
ref IO (Maybe Metrics) -> (Maybe Metrics -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= IO () -> (Metrics -> IO ()) -> Maybe Metrics -> IO ()
forall b a. b -> (a -> b) -> Maybe a -> b
maybe IO ()
forall (f :: * -> *). Applicative f => f ()
pass Metrics -> IO ()
record

{- | A 'BreakerReporter' that records a breaker's state to @ecluse.rule.breaker.state@
under the given source, through the deferred handle (inert until it is installed).
-}
deferredBreakerReporter :: DeferredMetrics -> BreakerSource -> BreakerReporter
deferredBreakerReporter :: DeferredMetrics -> BreakerSource -> BreakerReporter
deferredBreakerReporter DeferredMetrics
deferred BreakerSource
source =
    (Breaker -> IO ()) -> BreakerReporter
BreakerReporter ((Breaker -> IO ()) -> BreakerReporter)
-> (Breaker -> IO ()) -> BreakerReporter
forall a b. (a -> b) -> a -> b
$ \Breaker
breaker ->
        DeferredMetrics -> (Metrics -> IO ()) -> IO ()
withDeferredMetrics DeferredMetrics
deferred ((Metrics -> IO ()) -> IO ()) -> (Metrics -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Metrics
metrics ->
            Metrics -> BreakerSource -> BreakerState -> IO ()
forall (m :: * -> *).
MonadIO m =>
Metrics -> BreakerSource -> BreakerState -> m ()
recordBreakerState Metrics
metrics BreakerSource
source (Breaker -> BreakerState
breakerStateOf Breaker
breaker)

{- | A 'RefreshReporter' that records each refresh outcome to @ecluse.credential.refresh@
(by result) and the reported remaining lifetime to @ecluse.credential.token.ttl.seconds@,
both under the given provider, through the deferred handle (inert until it is installed).
-}
deferredRefreshReporter :: DeferredMetrics -> Provider -> RefreshReporter
deferredRefreshReporter :: DeferredMetrics -> Provider -> RefreshReporter
deferredRefreshReporter DeferredMetrics
deferred Provider
provider =
    RefreshReporter
        { onRefreshSucceeded :: Maybe Int -> IO ()
onRefreshSucceeded = CredentialResult -> Maybe Int -> IO ()
report CredentialResult
Refreshed
        , onRefreshFailed :: Maybe Int -> IO ()
onRefreshFailed = CredentialResult -> Maybe Int -> IO ()
report CredentialResult
RefreshFailed
        }
  where
    report :: CredentialResult -> Maybe Int -> IO ()
    report :: CredentialResult -> Maybe Int -> IO ()
report CredentialResult
result Maybe Int
mTtlSeconds =
        DeferredMetrics -> (Metrics -> IO ()) -> IO ()
withDeferredMetrics DeferredMetrics
deferred ((Metrics -> IO ()) -> IO ()) -> (Metrics -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Metrics
metrics -> do
            Metrics -> Provider -> CredentialResult -> IO ()
forall (m :: * -> *).
MonadIO m =>
Metrics -> Provider -> CredentialResult -> m ()
recordCredentialRefresh Metrics
metrics Provider
provider CredentialResult
result
            Maybe Int -> (Int -> IO ()) -> IO ()
forall (f :: * -> *) a.
Applicative f =>
Maybe a -> (a -> f ()) -> f ()
whenJust Maybe Int
mTtlSeconds (Metrics -> Provider -> Int -> IO ()
forall (m :: * -> *).
MonadIO m =>
Metrics -> Provider -> Int -> m ()
recordCredentialTokenTtl Metrics
metrics Provider
provider)

{- | An action recording one mirror enqueue failure to @ecluse.mirror.enqueue.failures@,
through the deferred handle (inert until it is installed). The composition root hangs
it on the enqueue buffer's drop and delivery-failure callbacks
('Ecluse.Core.Queue.newEnqueueBuffer'), which are wired before the instruments exist
but cannot fire until the server is serving -- well after 'installMetrics'.
-}
deferredMirrorEnqueueFailure :: DeferredMetrics -> IO ()
deferredMirrorEnqueueFailure :: DeferredMetrics -> IO ()
deferredMirrorEnqueueFailure DeferredMetrics
deferred =
    DeferredMetrics -> (Metrics -> IO ()) -> IO ()
withDeferredMetrics DeferredMetrics
deferred Metrics -> IO ()
forall (m :: * -> *). MonadIO m => Metrics -> m ()
recordMirrorEnqueueFailure

{- | Project the breaker's runtime state ("Ecluse.Core.Breaker") onto the bounded gauge value
the catalogue records ("Ecluse.Core.Telemetry.Metrics"). The consecutive-failure tally a
'Closed' breaker carries is not observable, so it collapses to the single closed value.
-}
breakerStateOf :: Breaker -> BreakerState
breakerStateOf :: Breaker -> BreakerState
breakerStateOf = \case
    Closed{} -> BreakerState
Metric.Closed
    Breaker
HalfOpen -> BreakerState
Metric.HalfOpen
    Open{} -> BreakerState
Metric.Open