-- SPDX-FileCopyrightText: 2026 Alexandra de Wit -- -- SPDX-License-Identifier: MIT {- | Backend-neutral recording ports for the serve path, mirror worker, Dredger and advisory tasks. Each port accepts the bounded labels defined in "Ecluse.Core.Telemetry.Metrics". The application supplies telemetry implementations. Polled gauges remain outside these ports. -} module Ecluse.Core.Telemetry.Record ( -- * The serve-path recording port MetricsPort (..), -- * The worker recording port WorkerMetricsPort (..), -- * The mirror sweep recording port DredgerMetricsPort (..), -- * The advisory sync recording port AdvisorySyncMetricsPort (..), -- * The advisory compile recording port AdvisoryCompileMetricsPort (..), -- * Timing timedSeconds, ) where import GHC.Clock (getMonotonicTime) import Ecluse.Core.Ecosystem (Ecosystem) import Ecluse.Core.Telemetry.Metrics ( AdvisoryCompileResult, AdvisoryDropCause, AdvisorySyncResult, CacheResult, CacheStore, Cause, Decision, MirrorResult, ReasonClass, RelayAnomaly, RequestFaultCause, StatusClass, SweepResult, SweepTarget, Tier, Upstream, ) {- | The metric-recording port over a telemetry backend, one field per @ecluse.*@ signal. The closed label types in "Ecluse.Core.Telemetry.Metrics" bound each signal's cardinality. -} data MetricsPort = MetricsPort { MetricsPort -> Decision -> IO () mpServeDecision :: Decision -> IO () -- ^ Record one serve decision (@ecluse.serve.decision@): admit, deny, or unavailable. , MetricsPort -> Int -> IO () mpServeAdmissionInFlight :: Int -> IO () -- ^ Record a change (+1 or -1) to in-flight metadata parses (@ecluse.serve.admission.in_flight@). , MetricsPort -> IO () mpServeAdmissionQueued :: IO () -- ^ Record one admission that waited for a slot (@ecluse.serve.admission.queued@). , MetricsPort -> IO () mpMemoryAdmissionQueued :: IO () -- ^ Record one request that waited for its memory entry step (@ecluse.serve.admission.memory.queued@). , MetricsPort -> IO () mpMemoryAdmissionShed :: IO () -- ^ Record one request shed at the memory door (@ecluse.serve.admission.memory.shed@). , MetricsPort -> IO () mpMemoryAdmissionPause :: IO () -- ^ Record one started request pausing for memory (@ecluse.serve.admission.memory.pauses@). , MetricsPort -> IO () mpMemoryAdmissionOverdraw :: IO () -- ^ Record one step taken past the memory budget (@ecluse.serve.admission.memory.overdraws@). , MetricsPort -> Int -> IO () mpPublishBodyInFlightBytes :: Int -> IO () {- ^ Record a change in the bytes held for buffered publish bodies, the reserved weight signed (@ecluse.publish.body.in_flight_bytes@). -} , MetricsPort -> IO () mpPublishBodyShed :: IO () -- ^ Record one publish shed at the body-byte budget (@ecluse.publish.body.shed@). , MetricsPort -> IO () mpMergeDivergence :: IO () {- ^ Record one cross-upstream integrity divergence in the packument merge, once per contradicting version (@ecluse.registry.merge.divergence@). -} , MetricsPort -> Maybe Text -> ReasonClass -> IO () mpRuleDenial :: Maybe Text -> ReasonClass -> IO () {- ^ Record one rule denial (@ecluse.rule.denials@) by reason class and, for a policy denial, the deciding rule. A non-policy refusal carries no rule. -} , MetricsPort -> Tier -> Double -> IO () mpRuleEvalDuration :: Tier -> Double -> IO () -- ^ Record a rule-evaluation latency sample (@ecluse.rule.eval.duration@) by tier. , MetricsPort -> Cause -> IO () mpRuleEffectfulFailure :: Cause -> IO () -- ^ Record one effectful-rule failure (@ecluse.rule.effectful.failures@) by cause. , MetricsPort -> Upstream -> StatusClass -> Double -> IO () mpUpstreamFetch :: Upstream -> StatusClass -> Double -> IO () -- ^ Record an upstream metadata-fetch latency sample (@ecluse.upstream.fetch.duration@). , MetricsPort -> Upstream -> Cause -> IO () mpUpstreamFetchError :: Upstream -> Cause -> IO () -- ^ Record one upstream metadata-fetch error (@ecluse.upstream.fetch.errors@). , MetricsPort -> CacheResult -> IO () mpCacheRequest :: CacheResult -> IO () -- ^ Full-store requests (@ecluse.metadata_cache.requests@). , MetricsPort -> CacheResult -> IO () mpVersionCacheRequest :: CacheResult -> IO () -- ^ Selected-version requests (@ecluse.metadata_cache.version.requests@). , MetricsPort -> CacheResult -> IO () mpAssembledCacheRequest :: CacheResult -> IO () -- ^ Assembled-response requests (@ecluse.metadata_cache.assembled.requests@). , MetricsPort -> CacheStore -> IO () mpCacheRefused :: CacheStore -> IO () -- ^ Capacity refusals and external backend failures (@ecluse.metadata_cache.refused@). , MetricsPort -> Int -> IO () mpCacheEntries :: Int -> IO () -- ^ Record the metadata cache's current occupancy (@ecluse.metadata_cache.entries@). , MetricsPort -> Int -> IO () mpCacheResidentBytes :: Int -> IO () -- ^ The full-packument cache's resident bytes (@ecluse.metadata_cache.resident_bytes@). , MetricsPort -> Int -> IO () mpVersionCacheResidentBytes :: Int -> IO () -- ^ The single-version cache's bytes (@ecluse.metadata_cache.version.resident_bytes@). , MetricsPort -> Int -> IO () mpAssembledCacheResidentBytes :: Int -> IO () -- ^ The assembled store's bytes (@ecluse.metadata_cache.assembled.resident_bytes@). , MetricsPort -> RelayAnomaly -> IO () mpPublicRelayAnomaly :: RelayAnomaly -> IO () {- ^ Record one public artifact relay that did not carry the admitted artifact (@ecluse.serve.relay.anomalies@). Steady state is zero. -} , MetricsPort -> RequestFaultCause -> IO () mpRequestPerimeterFault :: RequestFaultCause -> IO () -- ^ Record one pre-commit handler escape the perimeter answered (@ecluse.serve.perimeter.faults@). , MetricsPort -> IO () mpMirrorEnqueued :: IO () {- ^ Record one mirror job accepted for enqueue (@ecluse.mirror.enqueued@): the serve path's hand-off to the buffer, not the backend write. -} , MetricsPort -> IO () mpMirrorEnqueueFailure :: IO () {- ^ Record one mirror enqueue failure (@ecluse.mirror.enqueue.failures@): a refused hand-off or a failed backend delivery. -} } {- | The mirror worker's metric-recording port: the worker analogue of 'MetricsPort'. The two consumers share no field. @Ecluse.Runtime.Telemetry.Instruments@ supplies the OTel implementation. -} data WorkerMetricsPort = WorkerMetricsPort { WorkerMetricsPort -> MirrorResult -> IO () wmpMirrorJobProcessed :: MirrorResult -> IO () -- ^ Record one processed mirror job by its terminal result (@ecluse.mirror.jobs.processed@). , WorkerMetricsPort -> Double -> IO () wmpMirrorPublishDuration :: Double -> IO () -- ^ Record one mirror publish-latency sample (@ecluse.mirror.publish.duration@). } {- | The mirror sweep's metric-recording port, recorded by "Ecluse.Core.Registry.Sweep". The package and version a disposition concerns ride the sweep's own audit line, never a label. -} newtype DredgerMetricsPort = DredgerMetricsPort { DredgerMetricsPort -> SweepTarget -> SweepResult -> IO () dmpSweptVersion :: SweepTarget -> SweepResult -> IO () {- ^ Record one disposition of one examined version (@ecluse.dredger.versions@). A version counts once as examined and once under what the sweep did with it. -} } {- | The advisory sync task's metric-recording port, recorded by the sync loop in @Ecluse.Runtime.Cve.Sync@. Ecosystem and result are the only labels, so the series stays bounded. -} data AdvisorySyncMetricsPort = AdvisorySyncMetricsPort { AdvisorySyncMetricsPort -> Ecosystem -> AdvisorySyncResult -> IO () asmpSyncAttempt :: Ecosystem -> AdvisorySyncResult -> IO () -- ^ Record one advisory sync attempt (@ecluse.advisory.sync.attempts@) by ecosystem and result. , AdvisorySyncMetricsPort -> Ecosystem -> AdvisorySyncResult -> Double -> IO () asmpSyncDuration :: Ecosystem -> AdvisorySyncResult -> Double -> IO () -- ^ Record one sync attempt's latency in seconds (@ecluse.advisory.sync.duration@). } {- | The Pilot compile's metric-recording port. One port is bound to one ecosystem, so no field carries the ecosystem the compile holds as free text. -} data AdvisoryCompileMetricsPort = AdvisoryCompileMetricsPort { AdvisoryCompileMetricsPort -> Int -> IO () acmpCompileAccepted :: Int -> IO () -- ^ The entries one compile pass accepted (@ecluse.advisory.compile.accepted@). , AdvisoryCompileMetricsPort -> AdvisoryDropCause -> Int -> IO () acmpCompileDropped :: AdvisoryDropCause -> Int -> IO () {- ^ The entries one pass dropped for a cause (@ecluse.advisory.compile.dropped@). A pass with no drops records zero, so the series exists before the first drop. -} , AdvisoryCompileMetricsPort -> AdvisoryCompileResult -> IO () acmpCompileRun :: AdvisoryCompileResult -> IO () -- ^ Record how one compile pass concluded (@ecluse.advisory.compile.runs@). } {- | Run an action and return its result with the elapsed seconds. It measures on the monotonic clock, so a system-clock step never yields a negative or absurd duration. -} timedSeconds :: (MonadIO m) => m a -> m (a, Double) timedSeconds :: forall (m :: * -> *) a. MonadIO m => m a -> m (a, Double) timedSeconds m a action = do start <- IO Double -> m Double forall a. IO a -> m a forall (m :: * -> *) a. MonadIO m => IO a -> m a liftIO IO Double getMonotonicTime result <- action end <- liftIO getMonotonicTime pure (result, end - start)