ecluse
Safe HaskellNone
LanguageGHC2021

Ecluse.Telemetry.Instruments

Description

The runtime metric instruments and the typed emit helpers the hot path records through — the IO layer over the pure ecluse.* catalogue (Ecluse.Telemetry.Metrics).

Ecluse.Telemetry.Metrics defines what the catalogue is (the names and the closed set of bounded labels); this module turns that catalogue into live OpenTelemetry instruments and exposes one typed record* per signal. Each helper takes only the bounded label values its metric carries — never a free identifier — so the bounded-label discipline is enforced at the call site by the type, and the attribute set an instrument ever sees is drawn from a small fixed product of the label domains.

Gating: inert when telemetry is off

newMetrics builds the instruments from the Telemetry handle's meter provider when telemetry is enabled, and from the SDK's no-op meter provider when it is not. A no-op instrument discards every measurement, so the record* helpers are called unconditionally on the hot path and are genuinely inert when telemetry is off — no per-call branch, no provider fabricated at the edge. The Metrics handle is therefore total: every signal has a real instrument whichever posture the proxy is in. The no-op recording function ignores its arguments, so the metricAttributes a call passes is never forced — no attribute set is materialised when telemetry is off, only a thunk that is discarded.

The catalogue and the cardinality rule are described in docs/architecture/observability.md.

Synopsis

The instrument handle

data Metrics Source #

The live metric instruments, one per ecluse.* signal, created against a single meter. Opaque: built with newMetrics and recorded through the record* helpers. Held in the composition root (Ecluse.Env) so every layer records through the same instruments.

http.server.request.duration is not here: the WAI instrumentation emits it from the server-span meter (Ecluse.Telemetry.Tracing), so duplicating it would double the series. Advisory-sync and breaker instruments the catalogue names are present; their wiring is layered on as the subsystems that own them are built.

newMetrics :: Telemetry -> IO Metrics Source #

Build the metric instruments from a Telemetry handle. When telemetry is enabled the instruments are created on the handle's meter provider; when it is disabled they are created on the SDK's no-op meter provider, so every recorded measurement is discarded and the record* helpers are inert.

Instruments are created once here (at composition) rather than per measurement, so the hot path only records.

Timing

timedSeconds :: MonadIO m => m a -> m (a, Double) Source #

Run an action and return its result alongside the wall-clock seconds it took, measured on the monotonic clock so a system-clock step never yields a negative or absurd duration. The seconds are what the latency histograms record.

Serve decision

recordServeDecision :: MonadIO m => Metrics -> Decision -> m () Source #

Record one serve decision (ecluse.serve.decision): admit, deny, or unavailable.

Rule gate

recordRuleDenial :: MonadIO m => Metrics -> Maybe Text -> ReasonClass -> m () Source #

Record one rule denial (ecluse.rule.denials) by reason class and, for a policy denial, the deciding rule. A non-policy refusal (a missing-integrity or upstream cause) carries the reason class alone — no rule attributed it, so none is labelled.

recordRuleEvalDuration :: MonadIO m => Metrics -> Tier -> Double -> m () Source #

Record a rule-evaluation latency sample (ecluse.rule.eval.duration) by tier.

recordRuleEffectfulFailure :: MonadIO m => Metrics -> Cause -> m () Source #

Record one effectful-rule failure (ecluse.rule.effectful.failures) by cause.

recordBreakerState :: MonadIO m => Metrics -> BreakerSource -> BreakerState -> m () Source #

Record the current circuit-breaker state (ecluse.rule.breaker.state) for a source as the gauge's bounded ordinal (0 closed, 1 half-open, 2 open).

Upstream fetch (data plane)

recordUpstreamFetch :: MonadIO m => Metrics -> Upstream -> StatusClass -> Double -> m () Source #

Record an upstream metadata-fetch latency sample (ecluse.upstream.fetch.duration) by which upstream was fetched and the response's status class.

recordUpstreamFetchError :: MonadIO m => Metrics -> Upstream -> Cause -> m () Source #

Record one upstream metadata-fetch error (ecluse.upstream.fetch.errors) by which upstream and the bounded cause.

Metadata cache

recordCacheRequest :: MonadIO m => Metrics -> CacheResult -> m () Source #

Record one metadata-cache lookup (ecluse.metadata_cache.requests) as a hit or miss.

recordCacheEntries :: MonadIO m => Metrics -> Int -> m () Source #

Record the metadata cache's current occupancy (ecluse.metadata_cache.entries).

Mirror

recordMirrorEnqueued :: MonadIO m => Metrics -> m () Source #

Record one mirror job enqueued (ecluse.mirror.enqueued).

recordMirrorEnqueueFailure :: MonadIO m => Metrics -> m () Source #

Record one mirror enqueue failure (ecluse.mirror.enqueue.failures).

recordMirrorJobProcessed :: MonadIO m => Metrics -> MirrorResult -> m () Source #

Record one processed mirror job (ecluse.mirror.jobs.processed) by its result.

recordMirrorPublishDuration :: MonadIO m => Metrics -> Double -> m () Source #

Record a mirror publish latency sample (ecluse.mirror.publish.duration).

Credentials

recordCredentialRefresh :: MonadIO m => Metrics -> Provider -> CredentialResult -> m () Source #

Record one credential refresh (ecluse.credential.refresh) by result and provider.

recordCredentialTokenTtl :: MonadIO m => Metrics -> Provider -> Int -> m () Source #

Record an outbound token's remaining lifetime in whole seconds (ecluse.credential.token.ttl.seconds) by provider, so a stuck refresh alarms as the gauge decays towards zero.