ecluse
Safe HaskellNone
LanguageGHC2021

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 Manager settings (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 a 403 is explainable from the trace alone), withMirrorEnqueueSpan (the synchronous serve handing off to the asynchronous mirror), and withMirrorJobSpan (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

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.

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.