@@ -173,6 +173,26 @@ const federation = createFederation<void>({
173173});
174174~~~~
175175
176+ > [ !NOTE]
177+ > The document and context loader metrics
178+ > (` activitypub.document.fetch[.duration] ` and
179+ > ` activitypub.document.cache ` ) are opt-in inside Fedify: they are
180+ > emitted only when ` meterProvider ` is explicitly configured on
181+ > ` createFederation() ` . Omitting it preserves strict reference identity
182+ > for ` Context.documentLoader ` , ` Context.contextLoader ` , and the
183+ > authenticated document loader (` ctx.documentLoader === userLoader ` ),
184+ > so existing test code that asserts identity on a user-supplied
185+ > factory's output continues to work. The other metrics (delivery,
186+ > inbox, outbox, fanout, queue, HTTP server, signature verification,
187+ > signature key fetch, public key lookup, and ` lookupObject ` actor
188+ > classification) follow the standard “fall back to the global
189+ > [ ` MeterProvider ` ] ” behavior described above. Calling
190+ > ` lookupObject() ` directly from ` @fedify/vocab ` (without going through
191+ > a ` Context ` ) still requires an explicit
192+ > ` LookupObjectOptions.meterProvider ` to emit
193+ > ` activitypub.object.lookup ` ; ` Context.lookupObject() ` threads the
194+ > Federation's meter provider through automatically.
195+
176196[ `MeterProvider` ] : https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.MeterProvider.html
177197
178198
@@ -308,6 +328,12 @@ Fedify records the following OpenTelemetry metrics:
308328| ` activitypub.signature.verification_failure ` | Counter | ` {failure} ` | Counts failed signature verification for inbox requests. |
309329| ` activitypub.signature.verification.duration ` | Histogram | ` ms ` | Measures signature verification duration across HTTP, Linked Data, and Object Integrity Proofs. |
310330| ` activitypub.signature.key_fetch.duration ` | Histogram | ` ms ` | Measures public key lookup duration during signature verification. |
331+ | ` activitypub.key.lookup ` | Counter | ` {lookup} ` | Counts public key lookups performed by ` fetchKey() ` / ` fetchKeyDetailed() ` . |
332+ | ` activitypub.key.lookup.duration ` | Histogram | ` ms ` | Measures public key lookup duration, including cache hits and remote fetches. |
333+ | ` activitypub.document.fetch ` | Counter | ` {fetch} ` | Counts remote JSON-LD document loader invocations made by Fedify-wrapped loaders. |
334+ | ` activitypub.document.fetch.duration ` | Histogram | ` ms ` | Measures remote JSON-LD document loader invocation duration. |
335+ | ` activitypub.document.cache ` | Counter | ` {lookup} ` | Counts KV-backed document loader cache lookups, classified as ` hit ` or ` miss ` . |
336+ | ` activitypub.object.lookup ` | Counter | ` {lookup} ` | Counts ` lookupObject() ` calls, classified by whether the resolved value is an Actor. |
311337| ` fedify.http.server.request.count ` | Counter | ` {request} ` | Counts inbound HTTP requests handled by ` Federation.fetch() ` . |
312338| ` fedify.http.server.request.duration ` | Histogram | ` ms ` | Measures inbound HTTP request duration in ` Federation.fetch() ` . |
313339| ` fedify.queue.task.enqueued ` | Counter | ` {task} ` | Counts inbox, outbox, and fanout tasks Fedify enqueued. |
@@ -473,6 +499,113 @@ Fedify records the following OpenTelemetry metrics:
473499 for the stale attempt and one `fetched` for the freshly fetched retry)
474500 alongside the single verification measurement that covers both.
475501
502+ ` activitypub.key.lookup ` and ` activitypub.key.lookup.duration `
503+ : ` activitypub.lookup.kind ` is always ` public_key ` on these metrics; the
504+ enumeration also covers ` actor ` , ` object ` , ` context ` , and ` other ` for
505+ the document-fetch and lookup-object families described below.
506+ ` activitypub.lookup.result ` is always present and is one of:
507+
508+ - `hit`: the key was served from the configured `KeyCache`, either
509+ a valid cached key or a cached negative entry recording a prior
510+ failed fetch.
511+ - `fetched`: the key was not in the cache and was loaded through
512+ the document loader, returning a usable key.
513+ - `not_found`: the remote responded with `404 Not Found` or
514+ `410 Gone`. Recorded together with `http.response.status_code`.
515+ - `invalid`: the remote responded with a payload Fedify could not
516+ parse into a `CryptographicKey` or `Multikey`.
517+ - `network_error`: no HTTP response was received. DNS, connect,
518+ TLS, redirect-loop, or aborted-fetch failures all fall into this
519+ bucket via the shared error classifier.
520+ - `error`: any other unexpected failure (non-2xx HTTP response that
521+ is neither `404` nor `410`, thrown exceptions that are not
522+ recognised as transport failures, etc.).
523+
524+ `activitypub.cache.enabled` is always present and is `true` when the
525+ caller passed a `KeyCache`, `false` otherwise. `activitypub.remote.host`
526+ is the hostname of the key URL. `http.response.status_code` is
527+ present only when an HTTP response was observed. Key IDs, full key
528+ URLs, and actor IDs are deliberately excluded from these metrics;
529+ they remain on the `activitypub.fetch_key` span for trace-level
530+ investigation.
531+
532+ These metrics complement
533+ [`activitypub.signature.key_fetch.duration`](#instrumented-metrics).
534+ The signature-scoped histogram keeps an `activitypub.signature.kind`
535+ dimension and is the right metric to slice signature verification
536+ latency by `http` / `linked_data` / `object_integrity`; the new
537+ `activitypub.key.lookup*` metrics cover *every* key lookup performed
538+ by Fedify (including non-signature uses such as direct `fetchKey()`
539+ calls) and add a bounded HTTP `status_code` and richer
540+ `lookup.result` taxonomy.
541+
542+ ` activitypub.document.fetch ` and ` activitypub.document.fetch.duration `
543+ : ` activitypub.lookup.kind ` is always present and is one of ` object `
544+ (Fedify's generic document loader), ` context ` (the JSON-LD context
545+ loader), or ` other ` (callers that supply a custom kind hint).
546+ Actor documents fetched through the generic loader are still
547+ classified as ` object ` at this layer because the kind is decided at
548+ the loader boundary, * before* the response is parsed; the
549+ [ ` activitypub.object.lookup ` ] ( #instrumented-metrics ) counter
550+ provides the parsed-result actor / object split.
551+
552+ `activitypub.lookup.result` is always present and is one of
553+ `fetched`, `not_found` (with `http.response.status_code`),
554+ `network_error`, or `error`. The shared error classifier only
555+ surfaces these four values at the loader boundary; `invalid` is
556+ reserved for the key lookup metrics, where the parser can decide
557+ that a successful HTTP response still does not contain a usable
558+ key. `activitypub.remote.host` records the hostname of the
559+ fetched URL when the URL parses; otherwise it is omitted.
560+ `activitypub.cache.enabled` is `true` for Fedify's built-in
561+ `kvCache()`-backed document and context loaders and `false` for the
562+ authenticated document loader; for user-supplied factories Fedify
563+ cannot introspect caching behavior, so the attribute is omitted
564+ rather than recorded as a confident `true` or `false`.
565+
566+ Counter and histogram are always emitted together for one wrapped
567+ loader call, so dashboards can compute average duration as
568+ `duration_sum / counter`. Document IDs, JSON-LD context URLs, and
569+ full request URLs are deliberately excluded; the
570+ `activitypub.fetch_document` span keeps the full URL for sampled
571+ traces.
572+
573+ ` activitypub.document.cache `
574+ : ` activitypub.lookup.kind ` is always present (same values as
575+ ` activitypub.document.fetch ` ). ` activitypub.lookup.result ` is
576+ ` hit ` when the KV cache returned a ` RemoteDocument ` and ` miss `
577+ when it did not. Cache lookups that bypass the KV cache entirely
578+ (preloaded JSON-LD contexts and call sites without a matching cache
579+ rule) emit no measurement. ` activitypub.remote.host ` records the
580+ hostname of the looked-up URL when it parses.
581+
582+ ` activitypub.object.lookup `
583+ : ` activitypub.lookup.kind ` is always present and is one of:
584+
585+ - `actor`: `lookupObject()` resolved to an `Actor` subtype
586+ (`Application`, `Group`, `Organization`, `Person`, `Service`).
587+ - `object`: `lookupObject()` resolved to a non-actor
588+ `Object` subtype.
589+ - `other`: `lookupObject()` returned `null` (the document could
590+ not be fetched, the response could not be parsed, or the
591+ cross-origin check rejected the resolved object) **or** the
592+ call threw before resolving an object. The metric is emitted
593+ in a `finally` block, so a thrown error is still counted with
594+ `kind=other`.
595+
596+ `activitypub.remote.host` is the hostname extracted from the
597+ identifier: a parsed `URL`, an `acct:user@host` URI, or a bare
598+ `@user@host` / `user@host` handle. Inputs that do not reduce
599+ cleanly to an authority (paths, query strings, fragments, or
600+ whitespace mixed in with the handle suffix) result in the
601+ attribute being omitted, rather than recording a high-cardinality
602+ value. This counter has no companion histogram: `lookupObject()`
603+ drives `activitypub.document.fetch.duration` through the document
604+ loader, and emitting another duration here would double-count
605+ latency. Use `activitypub.object.lookup` for the parsed-result
606+ classification and `activitypub.document.fetch[.duration]` for
607+ the loader-level rate and latency.
608+
476609` fedify.http.server.request.count ` and ` fedify.http.server.request.duration `
477610: ` http.request.method ` and ` fedify.endpoint ` are always present.
478611 ` http.request.method ` is normalized to one of the standard HTTP methods
@@ -557,6 +690,19 @@ and query strings are deliberately excluded to keep metric cardinality bounded.
557690Activity types use the same qualified URI form as Fedify's trace attributes,
558691for example ` https://www.w3.org/ns/activitystreams#Create ` .
559692
693+ The key lookup, document fetch, document cache, and object lookup metrics
694+ share an ` activitypub.lookup.kind ` and (where applicable)
695+ ` activitypub.lookup.result ` attribute taxonomy. Both are drawn from small
696+ fixed enumerations (` kind ` ∈ ` {public_key, actor, object, context, other} `
697+ and ` result ` ∈
698+ ` {hit, miss, fetched, not_found, invalid, network_error, error} ` ), so an
699+ attacker-controlled remote cannot inflate cardinality by returning arbitrary
700+ status codes, content types, or thrown exceptions. Full URLs, key IDs, actor
701+ IDs, object IDs, JSON-LD context URLs, and fediverse handles are deliberately
702+ excluded; they remain on the corresponding spans (` activitypub.fetch_key ` ,
703+ ` activitypub.fetch_document ` , ` activitypub.lookup_object ` ) for trace-level
704+ investigation.
705+
560706The HTTP server request metrics deliberately exclude high-cardinality fields
561707such as the full URL, raw path, query string, actor identifier, and inbox
562708URL. Use the request span's ` url.full ` attribute when you need the exact URL
0 commit comments