| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Telemetry.Tracing
Description
The request-lifecycle tracing layer on top of the OpenTelemetry substrate (Ecluse.Telemetry): the WAI server span, the http-client child spans on the data plane, and the hand-added domain spans that carry the decisions an operator cares about -- all inert when telemetry is off.
The substrate decides whether telemetry is wired; this module decides what is
traced. Every entry point takes the Telemetry handle and, when it is
TelemetryDisabled, adds nothing and emits nothing: the
middleware is id, the manager settings are returned untouched, and a domain-span
bracket runs its body against no span. When telemetry is enabled, the handle's
provider is the process-global provider the substrate installed (when enabled,
"Ecluse.Telemetry.withTelemetry" calls initializeGlobalTracerProvider, which also
installs the global text-map propagator), so the WAI and http-client instrumentation -- which read
the process globals -- and the hand-added spans, which read the handle, all hang off
one coherent tracer and join into one trace.
What is traced
- Server span -- one per request, from the WAI instrumentation, as the outermost
middleware so it spans the whole request (
telemetryWaiMiddleware). - Client spans -- one per upstream fetch, from instrumenting the data-plane
Managersettings (instrumentDataPlaneManagerSettings), which also injects W3C trace context into the outbound request so a downstream service continues the trace. - Domain spans --
withRuleEvalSpan(the per-version verdict, so a403is explainable from the trace alone),withMirrorEnqueueSpan(the synchronous serve handing off to the asynchronous mirror), andwithMirrorJobSpan(the worker's fetch → verify → publish). The enqueue span captures its own W3C trace context onto the mirror job, and the worker-job span re-establishes it as an OpenTelemetry __span link__ to that producer span, so the asynchronous mirror hand-off is navigable in a trace rather than only correlated by package/version. A swallowed best-effort enqueue failure is recorded on the enqueue span's status, so the trace explains why the mirror did not happen.
Secret discipline
The data-plane instrumentation uses dataPlaneInstrumentationConfig, which records
no request or response headers, so a forwarded client token or an Authorization
header is never captured on a client span; the WAI instrumentation likewise never
records Authorization. High-cardinality identifiers (package, version, the full
denial message) belong on these spans and are recorded here; secrets never are. The
attribute mapping and the scrub are covered by Ecluse.Telemetry.TracingSpec.
Synopsis
- telemetryWaiMiddleware :: Telemetry -> IO Middleware
- instrumentDataPlaneManagerSettings :: Telemetry -> ManagerSettings -> IO ManagerSettings
- dataPlaneInstrumentationConfig :: HttpClientInstrumentationConfig
- withRuleEvalSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> m (a, ServeDecision) -> m a
- withMirrorEnqueueSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> Text -> (a -> Maybe Text) -> (Maybe RemoteSpanContext -> m a) -> m a
- withPackumentGateSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a
- withMetadataFetchSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a
- withMetadataDecodeSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a
- withMirrorJobSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> Maybe RemoteSpanContext -> (a -> JobSpanOutcome) -> m a -> m a
- data JobSpanOutcome = JobSpanOutcome {}
- withDomainSpan :: MonadUnliftIO m => Telemetry -> SpanKind -> [NewLink] -> Text -> (Maybe Span -> m a) -> m a
- tracingPortOf :: Telemetry -> TracingPort
- workerTracingPortOf :: Telemetry -> WorkerTracingPort
- ruleVerdictFields :: ServeDecision -> [(Text, Text)]
WAI server span
telemetryWaiMiddleware :: Telemetry -> IO Middleware Source #
The WAI server-span middleware for the request stack: one server span per
request, built over the handle's tracer and meter providers. When telemetry is
disabled it is id -- the stack is unchanged and no span is opened -- so it is
additive and inert exactly as the substrate's off posture requires.
It belongs outermost in the stack so the span covers the whole request, including the other middlewares (see Ecluse.Server).
http-client data-plane instrumentation
instrumentDataPlaneManagerSettings :: Telemetry -> ManagerSettings -> IO ManagerSettings Source #
Instrument a data-plane ManagerSettings so every upstream fetch through the
resulting manager opens a client span and carries W3C trace-context headers, or
return the settings untouched when telemetry is disabled.
The gate is the handle, not a per-request check: when telemetry is enabled the substrate has installed the process-global providers the http-client instrumentation reads, so the spans hang off the same tracer as everything else; when disabled the settings are returned verbatim and the data plane runs exactly as it would without this layer.
The configuration is dataPlaneInstrumentationConfig, which records no headers, so a
forwarded client token never reaches a span.
dataPlaneInstrumentationConfig :: HttpClientInstrumentationConfig Source #
The http-client instrumentation configuration the data plane uses: the default,
which records no request or response headers. This is the secret-scrub guarantee
at the configuration boundary -- an Authorization header is never lifted onto a span
-- so it is named rather than inlined, and the scrub test pins the very same value.
Domain spans
withRuleEvalSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> m (a, ServeDecision) -> m a Source #
Run a rule-evaluation domain span around an action that yields its result and the verdict to record. The span carries the package and version and, from the verdict, the decision and -- on a denial -- the deciding rule, the reason class, and the human-readable message, so a refusal is explainable from the trace alone.
Inert when telemetry is disabled: the action runs against no span and its result is returned unchanged.
withMirrorEnqueueSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> Text -> (a -> Maybe Text) -> (Maybe RemoteSpanContext -> m a) -> m a Source #
Run a mirror-enqueue domain span around the serve-time hand-off to the
asynchronous mirror, carrying the package, version, and the artifact's authoritative
URL. A Producer span, since it produces the work the worker later consumes.
The body is handed this span's own W3C trace context (RemoteSpanContext) -- or
Nothing when telemetry is disabled -- to stamp onto the mirror job, so the worker's
per-job span can link back to this producer span across the asynchronous hop. The
project function maps the body's result onto an optional failure detail: a Just
sets the span status to Error, so a swallowed best-effort enqueue failure is still
explained by the trace.
Inert when telemetry is disabled: the body runs against no span and is handed no trace context.
withPackumentGateSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a Source #
Run a packument-gate domain span around the rules and filter application for a public packument.
withMetadataFetchSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a Source #
withMetadataDecodeSpan :: MonadUnliftIO m => Telemetry -> PackageName -> m a -> m a Source #
withMirrorJobSpan :: MonadUnliftIO m => Telemetry -> PackageName -> Version -> Maybe RemoteSpanContext -> (a -> JobSpanOutcome) -> m a -> m a Source #
Run a mirror-worker-job domain span around the worker's fetch → verify →
publish, carrying the package and version and, once the job finishes, its outcome.
A Consumer span (it consumes the enqueued work); the outcome projection names the
bounded outcome label and, for a non-success, the detail that sets the span status to
Error.
The carried trace context (RemoteSpanContext, captured by withMirrorEnqueueSpan
and threaded through the job) re-establishes the cross-async relationship as a span
link to the enqueueing producer span, so a trace navigates from the request to the
mirror it triggered and back. A Nothing context (or one that does not parse) simply
yields no link. Inert when telemetry is disabled.
data JobSpanOutcome #
Constructors
| JobSpanOutcome | |
Fields
| |
Instances
| Show JobSpanOutcome | |
Defined in Ecluse.Core.Telemetry.Span Methods showsPrec :: Int -> JobSpanOutcome -> ShowS # show :: JobSpanOutcome -> String # showList :: [JobSpanOutcome] -> ShowS # | |
| Eq JobSpanOutcome | |
Defined in Ecluse.Core.Telemetry.Span Methods (==) :: JobSpanOutcome -> JobSpanOutcome -> Bool # (/=) :: JobSpanOutcome -> JobSpanOutcome -> Bool # | |
withDomainSpan :: MonadUnliftIO m => Telemetry -> SpanKind -> [NewLink] -> Text -> (Maybe Span -> m a) -> m a Source #
The core tracing ports
tracingPortOf :: Telemetry -> TracingPort Source #
Project the OpenTelemetry-backed domain spans onto the core TracingPort the
serve path (Ecluse.Core.Server.Pipeline) brackets through: the per-version rule
verdict and the serve-time mirror-enqueue hand-off. Each field is the matching
with*Span bracket closed over the Telemetry handle, so the port is exactly this
module's tracing behind the core interface -- inert when telemetry is off. The worker's
mirror-job span is projected separately by workerTracingPortOf onto a
WorkerTracingPort, so this port carries only the two serve-path spans.
workerTracingPortOf :: Telemetry -> WorkerTracingPort Source #
Project the OpenTelemetry-backed mirror-job span onto the core WorkerTracingPort
the worker loop (Ecluse.Core.Worker) brackets through. The single field is
withMirrorJobSpan closed over the Telemetry handle, so the port is exactly this
module's tracing behind the core interface -- inert when telemetry is off.
Verdict attribute mapping
ruleVerdictFields :: ServeDecision -> [(Text, Text)] Source #
Map a serve verdict to the rule-evaluation span's attribute fields. Pure and total.
An Admit records only the decision; a Reject records the decision, the bounded
reason class, the human-readable message, and -- for a policy denial -- the deciding
RuleName. None of these fields can carry a secret: the rule name and reason class
are a closed vocabulary and the message is the rendered decision, never a credential.