Skip to content

Commit 553b59b

Browse files
authored
Merge pull request #771 from dahlia/otel/key-remote-doc-fetches
2 parents 6cc0266 + 389552e commit 553b59b

14 files changed

Lines changed: 1858 additions & 55 deletions

File tree

CHANGES.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,50 @@ To be released.
113113
and other high-cardinality identifiers are deliberately excluded
114114
from the fanout histogram. [[#316], [#742], [#770]]
115115

116+
- Added OpenTelemetry metrics for public key lookups, remote JSON-LD
117+
document fetches, and `lookupObject()` calls so operators can
118+
observe how often Fedify hits the cache, how long remote fetches
119+
take, and how `lookupObject()` resolutions split between actors,
120+
non-actor objects, and unresolved lookups:
121+
122+
- `activitypub.key.lookup` (counter) and
123+
`activitypub.key.lookup.duration` (histogram) cover every
124+
public key lookup performed by `fetchKey()` /
125+
`fetchKeyDetailed()`, including signature verification paths.
126+
- `activitypub.document.fetch` (counter) and
127+
`activitypub.document.fetch.duration` (histogram) cover every
128+
Fedify-wrapped document or context loader invocation, including
129+
the authenticated loader.
130+
- `activitypub.document.cache` (counter) records `hit` or `miss`
131+
for each `kvCache()`-backed cache lookup.
132+
- `activitypub.object.lookup` (counter) records the
133+
parsed-result classification of every `lookupObject()` call as
134+
`actor`, `object`, or `other`.
135+
136+
Instruments share an `activitypub.lookup.kind` and (where
137+
applicable) `activitypub.lookup.result` attribute drawn from small,
138+
spec-bounded enumerations. `activitypub.remote.host` records the
139+
URL hostname only; `http.response.status_code` is recorded when an
140+
HTTP response was observed; `activitypub.cache.enabled` is
141+
recorded on the key and document fetch metrics whenever Fedify can
142+
confidently report the cache layer's presence. Key IDs, actor
143+
IDs, object IDs, JSON-LD context URLs, full URLs, and fediverse
144+
handles are deliberately excluded so attacker-controlled remotes
145+
cannot inflate metric cardinality. The existing
146+
`activitypub.signature.key_fetch.duration` histogram (introduced in
147+
Fedify 2.3 for signature-scoped key-fetch latency, sliced by
148+
`activitypub.signature.kind`) remains in place; the new
149+
`activitypub.key.lookup.duration` is the general-purpose
150+
histogram that covers non-signature key fetches as well and adds
151+
`http.response.status_code` and a richer
152+
`activitypub.lookup.result` taxonomy. [[#316], [#738], [#771]]
153+
116154
[#316]: https://github.com/fedify-dev/fedify/issues/316
117155
[#619]: https://github.com/fedify-dev/fedify/issues/619
118156
[#735]: https://github.com/fedify-dev/fedify/issues/735
119157
[#736]: https://github.com/fedify-dev/fedify/issues/736
120158
[#737]: https://github.com/fedify-dev/fedify/issues/737
159+
[#738]: https://github.com/fedify-dev/fedify/issues/738
121160
[#740]: https://github.com/fedify-dev/fedify/issues/740
122161
[#742]: https://github.com/fedify-dev/fedify/issues/742
123162
[#748]: https://github.com/fedify-dev/fedify/pull/748
@@ -128,6 +167,7 @@ To be released.
128167
[#759]: https://github.com/fedify-dev/fedify/pull/759
129168
[#769]: https://github.com/fedify-dev/fedify/pull/769
130169
[#770]: https://github.com/fedify-dev/fedify/pull/770
170+
[#771]: https://github.com/fedify-dev/fedify/pull/771
131171

132172
### @fedify/fixture
133173

docs/manual/opentelemetry.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
557690
Activity types use the same qualified URI form as Fedify's trace attributes,
558691
for 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+
560706
The HTTP server request metrics deliberately exclude high-cardinality fields
561707
such as the full URL, raw path, query string, actor identifier, and inbox
562708
URL. Use the request span's `url.full` attribute when you need the exact URL

0 commit comments

Comments
 (0)