Table of Contents

Diagnostics & observability

Hypermedia failures are easy to miss: a link that doesn't resolve, a response shape that can't carry links, a type nothing configured. Cairn's position is that these should never fail silently — in the default Lax mode a problem costs you a link, not a 500, but every drop is logged and metered so it can be seen.

This page covers the runtime side. The build-time counterpart — analyzers that catch unknown route names and unconfigured types before you run — is route safety.

Logged warnings

All warnings log under the category Cairn.AspNetCore at Warning level, and each fires once per condition per host — a 1,000-item page or a chatty test suite produces one line, not thousands. (The gate is a per-host singleton, so side-by-side hosts such as WebApplicationFactory suites each get their own warnings.)

Warning When it fires
Unresolved link A link or affordance failed to resolve in Lax mode (for example an unknown route name) and was dropped. Once per resource type + relation.
Affordances lost to HAL The negotiated format is HAL, which has no affordance section, and the resource declared affordances.
Value-type resource The endpoint returned a struct. Cairn correlates hypermedia to instances by reference; a value type boxes to a different instance at each stage, so links can't attach. Use a class or record.
IAsyncEnumerable<T> response An async stream can't be enumerated twice, so it can't be linked. Materialize first (ToListAsync()).
Unconfigured type An endpoint opted in with .WithLinks()/[CairnLinks] but the returned type (and no base type) has a registered LinkConfig<T>. Also caught at build time by CAIRN002.
Deferred sequence in an immutable result The response is a deferred sequence (IQueryable, LINQ projection) inside TypedResults.Ok(...), where Cairn cannot buffer it: computing links enumerates it once and serialization enumerates it again (an IQueryable runs its query twice). Fires while the request runs. Materialize (ToList()) before wrapping.
Deferred envelope items with no stable property A pagination envelope exposes deferred items only through a property that recomputes a fresh sequence on each access (or a non-public member), so the buffered items can't be substituted at serialization; the sequence is enumerated twice and item links may be lost. A settable or init-only property works — materialize (ToList()) or expose a stable property otherwise.
Custom JsonConverter on a resource type Hypermedia was computed for a type whose JSON contract is handled by a custom converter, so the property injection can never emit it.
Computed but never emitted Hypermedia was computed for instances that never reached the serializer — typically a deferred sequence whose re-enumeration produced fresh instances. Materialize (ToList()) before wrapping.
Policy-gated links under output caching A resource whose link config references authorization policies was computed on a request subject to OutputCache (which ignores Vary), so one caller's link set could be replayed to others. See Caching.
default template collision Two When()/policy-gated AsDefault() affordances both emitted on the same response and collided on the reserved HAL-FORMS default key; the last one wins on the wire.
Absolute URLs trust the incoming Host Logged once at startup when UrlStyle is Absolute and neither ForwardedHeadersOptions nor PublicBaseUri is configured — links reflect whatever Host the client sent. See Link URL policy.

In Strict mode (o.Mode = LinkResolutionMode.Strict) an unresolved link throws a LinkResolutionException instead of dropping — see getting started.

If you want to react to drops programmatically (fail a canary, page a dashboard), LinkContext.OnUnresolvedLink is the underlying hook: it receives an UnresolvedLink(Type, LinkRelation, LinkTarget) for every Lax-mode drop.

Tracing and metrics

Cairn publishes an ActivitySource and a Meter, both named Cairn.AspNetCore. The names are available as constants so you never hardcode them:

using Cairn.AspNetCore;

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddSource(CairnDiagnostics.ActivitySourceName))
    .WithMetrics(m => m.AddMeter(CairnDiagnostics.MeterName));

Tracing — one Cairn.ComputeHypermedia span per linked top-level value, tagged with cairn.format (the negotiated format name or custom media type) and cairn.resource_type. It measures the compute stage: evaluating conditions and policies and resolving URLs, not serialization.

Metrics — all counters:

Instrument Unit Meaning
cairn.resources.linked {resource} Resources that received hypermedia.
cairn.links.computed {link} Links computed.
cairn.affordances.computed {affordance} Affordances computed.
cairn.links.unresolved {link} Lax-mode drops, tagged cairn.resource_type and cairn.relation. The metric counts every drop even though the log line fires once.
cairn.hypermedia.unemitted {resource_type} Responses whose computed hypermedia never reached the wire, tagged cairn.resource_type.

cairn.links.unresolved and cairn.hypermedia.unemitted are the two to alert on: both mean clients are receiving less hypermedia than the code declares.