Skip to content

Commit 15a3316

Browse files
authored
Merge pull request #769 from dahlia/otel/sig-metrics
OpenTelemetry: metrics for signature verification duration
2 parents 3ba8446 + 85e906a commit 15a3316

11 files changed

Lines changed: 1639 additions & 104 deletions

File tree

CHANGES.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ To be released.
4141
attributes, and `TraceActivityRecord.activityJson` is present only when the
4242
span event includes full activity JSON. [[#316], [#619], [#755]]
4343

44+
- Added two OpenTelemetry histograms for signature verification:
45+
`activitypub.signature.verification.duration` measures end-to-end
46+
verification time for HTTP Signatures, Linked Data Signatures, and
47+
Object Integrity Proofs (including local key lookup and remote key
48+
fetches), and `activitypub.signature.key_fetch.duration` measures
49+
public key lookup duration separately so operators can isolate
50+
non-fetch verification work. Both instruments carry
51+
`activitypub.signature.kind` (`http`, `linked_data`, or
52+
`object_integrity`) and bounded result attributes; the verification
53+
histogram additionally carries spec-bounded
54+
`http_signatures.algorithm`, `ld_signatures.type`, or
55+
`object_integrity_proofs.cryptosuite` when known, plus
56+
`http_signatures.failure_reason` on rejected HTTP rows.
57+
[[#316], [#737], [#769]]
58+
4459
- Added OpenTelemetry HTTP server metrics for inbound requests handled by
4560
`Federation.fetch()`: `fedify.http.server.request.count` (Counter) and
4661
`fedify.http.server.request.duration` (Histogram). Both instruments carry
@@ -80,13 +95,15 @@ To be released.
8095
[#619]: https://github.com/fedify-dev/fedify/issues/619
8196
[#735]: https://github.com/fedify-dev/fedify/issues/735
8297
[#736]: https://github.com/fedify-dev/fedify/issues/736
98+
[#737]: https://github.com/fedify-dev/fedify/issues/737
8399
[#740]: https://github.com/fedify-dev/fedify/issues/740
84100
[#748]: https://github.com/fedify-dev/fedify/pull/748
85101
[#752]: https://github.com/fedify-dev/fedify/issues/752
86102
[#753]: https://github.com/fedify-dev/fedify/pull/753
87103
[#755]: https://github.com/fedify-dev/fedify/pull/755
88104
[#757]: https://github.com/fedify-dev/fedify/pull/757
89105
[#759]: https://github.com/fedify-dev/fedify/pull/759
106+
[#769]: https://github.com/fedify-dev/fedify/pull/769
90107

91108
### @fedify/fixture
92109

docs/manual/opentelemetry.md

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -296,21 +296,23 @@ Instrumented metrics
296296

297297
Fedify records the following OpenTelemetry metrics:
298298

299-
| Metric name | Instrument | Unit | Description |
300-
| -------------------------------------------- | ------------- | ----------- | --------------------------------------------------------------- |
301-
| `activitypub.delivery.sent` | Counter | `{attempt}` | Counts outgoing ActivityPub delivery attempts. |
302-
| `activitypub.delivery.permanent_failure` | Counter | `{failure}` | Counts outgoing deliveries abandoned as permanent failures. |
303-
| `activitypub.delivery.duration` | Histogram | `ms` | Measures outgoing ActivityPub delivery attempt duration. |
304-
| `activitypub.inbox.processing_duration` | Histogram | `ms` | Measures inbox listener processing duration. |
305-
| `activitypub.signature.verification_failure` | Counter | `{failure}` | Counts failed signature verification for inbox requests. |
306-
| `fedify.http.server.request.count` | Counter | `{request}` | Counts inbound HTTP requests handled by `Federation.fetch()`. |
307-
| `fedify.http.server.request.duration` | Histogram | `ms` | Measures inbound HTTP request duration in `Federation.fetch()`. |
308-
| `fedify.queue.task.enqueued` | Counter | `{task}` | Counts inbox, outbox, and fanout tasks Fedify enqueued. |
309-
| `fedify.queue.task.started` | Counter | `{task}` | Counts queue tasks Fedify began processing as a worker. |
310-
| `fedify.queue.task.completed` | Counter | `{task}` | Counts queue tasks Fedify finished processing without throwing. |
311-
| `fedify.queue.task.failed` | Counter | `{task}` | Counts queue tasks Fedify abandoned because processing threw. |
312-
| `fedify.queue.task.duration` | Histogram | `ms` | Measures queue task processing duration in Fedify workers. |
313-
| `fedify.queue.task.in_flight` | UpDownCounter | `{task}` | Tracks queue tasks currently in flight in this Fedify process. |
299+
| Metric name | Instrument | Unit | Description |
300+
| --------------------------------------------- | ------------- | ----------- | ----------------------------------------------------------------------------------------------- |
301+
| `activitypub.delivery.sent` | Counter | `{attempt}` | Counts outgoing ActivityPub delivery attempts. |
302+
| `activitypub.delivery.permanent_failure` | Counter | `{failure}` | Counts outgoing deliveries abandoned as permanent failures. |
303+
| `activitypub.delivery.duration` | Histogram | `ms` | Measures outgoing ActivityPub delivery attempt duration. |
304+
| `activitypub.inbox.processing_duration` | Histogram | `ms` | Measures inbox listener processing duration. |
305+
| `activitypub.signature.verification_failure` | Counter | `{failure}` | Counts failed signature verification for inbox requests. |
306+
| `activitypub.signature.verification.duration` | Histogram | `ms` | Measures signature verification duration across HTTP, Linked Data, and Object Integrity Proofs. |
307+
| `activitypub.signature.key_fetch.duration` | Histogram | `ms` | Measures public key lookup duration during signature verification. |
308+
| `fedify.http.server.request.count` | Counter | `{request}` | Counts inbound HTTP requests handled by `Federation.fetch()`. |
309+
| `fedify.http.server.request.duration` | Histogram | `ms` | Measures inbound HTTP request duration in `Federation.fetch()`. |
310+
| `fedify.queue.task.enqueued` | Counter | `{task}` | Counts inbox, outbox, and fanout tasks Fedify enqueued. |
311+
| `fedify.queue.task.started` | Counter | `{task}` | Counts queue tasks Fedify began processing as a worker. |
312+
| `fedify.queue.task.completed` | Counter | `{task}` | Counts queue tasks Fedify finished processing without throwing. |
313+
| `fedify.queue.task.failed` | Counter | `{task}` | Counts queue tasks Fedify abandoned because processing threw. |
314+
| `fedify.queue.task.duration` | Histogram | `ms` | Measures queue task processing duration in Fedify workers. |
315+
| `fedify.queue.task.in_flight` | UpDownCounter | `{task}` | Tracks queue tasks currently in flight in this Fedify process. |
314316

315317
### Metric attributes
316318

@@ -332,6 +334,81 @@ Fedify records the following OpenTelemetry metrics:
332334
: `activitypub.verification.failure_reason`, plus
333335
`activitypub.remote.host` when the failed signature includes a key ID.
334336

337+
`activitypub.signature.verification.duration`
338+
: `activitypub.signature.kind` is always present and is one of `http`,
339+
`linked_data`, or `object_integrity`. `activitypub.signature.result` is
340+
always present and is one of:
341+
342+
- `verified`: the signature was checked and accepted.
343+
- `rejected`: the signature was checked and refused (bad signature,
344+
key fetch failure, owner mismatch, etc.).
345+
- `missing`: no signature was present. Only `http` and `linked_data`
346+
produce this value; `object_integrity` does not, because the caller
347+
decides whether to invoke proof verification at all.
348+
- `error`: verification threw an unexpected error.
349+
350+
The duration covers the full verification path Fedify performs,
351+
*including* local key lookup and remote key fetches; the separate
352+
`activitypub.signature.key_fetch.duration` histogram lets operators
353+
subtract key lookup latency from the total to isolate the rest of the
354+
verification work (canonicalization, hashing, attribution and owner
355+
checks, cryptographic verification, etc.). Direct calls to
356+
`verifyRequest()` / `verifyRequestDetailed()`, `verifyJsonLd()`, and
357+
`verifyProof()` each emit exactly one measurement, even when the
358+
implementation retries internally after a cache mismatch. Wrappers
359+
such as `verifyObject()` emit one measurement per inner `verifyProof()`
360+
call (and none when the object has no proofs); higher-level inbox
361+
handling can perform several verification attempts in series.
362+
363+
Kind-specific optional attributes are recorded only when the value
364+
matches a small, spec-bounded set, to keep cardinality safe even when
365+
attacker-supplied JSON-LD or signature headers reach the verifier:
366+
367+
- `http_signatures.algorithm` (HTTP only) is recorded only when the
368+
parsed algorithm value is one of `rsa-sha1`, `rsa-sha256`,
369+
`rsa-sha512`, `ecdsa-sha256`, `ecdsa-sha384`, `ecdsa-sha512`,
370+
`ed25519`, or `hs2019` (draft-cavage) or one of the keys of the
371+
RFC 9421 algorithm map (`rsa-v1_5-sha256`, `rsa-v1_5-sha512`,
372+
`rsa-pss-sha512`, `ecdsa-p256-sha256`, `ecdsa-p384-sha384`,
373+
`ed25519`).
374+
- `http_signatures.failure_reason` (HTTP only, on `rejected` rows)
375+
is one of `invalidSignature` or `keyFetchError`. HTTP requests
376+
with no signature header are reported as
377+
`activitypub.signature.result=missing` and do not carry a
378+
`http_signatures.failure_reason`.
379+
- `ld_signatures.type` (Linked Data only) is recorded only for the
380+
spec-supported `RsaSignature2017` type.
381+
- `object_integrity_proofs.cryptosuite` (Object Integrity Proofs
382+
only) is recorded only for the spec-supported `eddsa-jcs-2022`
383+
cryptosuite.
384+
385+
Key IDs, actor IDs, request URLs, and object IDs are deliberately
386+
excluded from this histogram. They remain on the corresponding spans
387+
(`http_signatures.verify`, `ld_signatures.verify`,
388+
`object_integrity_proofs.verify`) for trace-level investigation.
389+
390+
`activitypub.signature.key_fetch.duration`
391+
: `activitypub.signature.kind` is always present (same values as above).
392+
`activitypub.signature.key_fetch.result` is always present and is one
393+
of:
394+
395+
- `hit`: the public key was served by the configured `KeyCache`
396+
(which may itself be backed by a remote store such as Redis or a
397+
database; the measurement reflects whatever round trip that
398+
backend incurs).
399+
- `fetched`: the key was not in the cache and was loaded through
400+
the document loader, returning a usable key. This typically
401+
corresponds to a network fetch, but a custom document loader
402+
that serves from a local store will also fall in this bucket.
403+
- `error`: no usable key came back (HTTP failure, invalid response
404+
body, cached negative entry, thrown exception, etc.).
405+
406+
Unlike `activitypub.signature.verification.duration`, this histogram
407+
is recorded *per fetch attempt*: a verification that retries after a
408+
cache mismatch emits two key fetch measurements (typically one `hit`
409+
for the stale attempt and one `fetched` for the freshly fetched retry)
410+
alongside the single verification measurement that covers both.
411+
335412
`fedify.http.server.request.count` and `fedify.http.server.request.duration`
336413
: `http.request.method` and `fedify.endpoint` are always present.
337414
`http.request.method` is normalized to one of the standard HTTP methods

packages/fedify/src/federation/handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ async function handleInboxInternal<TContextData>(
842842
signatureTimeWindow,
843843
skipSignatureVerification,
844844
inboxChallengePolicy,
845+
meterProvider,
845846
tracerProvider,
846847
} = parameters;
847848
const logger = getLogger(["fedify", "federation", "inbox"]);
@@ -913,6 +914,7 @@ async function handleInboxInternal<TContextData>(
913914
contextLoader: ctx.contextLoader,
914915
documentLoader: ctx.documentLoader,
915916
keyCache,
917+
meterProvider,
916918
tracerProvider,
917919
});
918920
} catch (error) {
@@ -942,6 +944,7 @@ async function handleInboxInternal<TContextData>(
942944
contextLoader: ctx.contextLoader,
943945
documentLoader: ctx.documentLoader,
944946
keyCache,
947+
meterProvider,
945948
tracerProvider,
946949
});
947950
} catch (error) {
@@ -991,6 +994,7 @@ async function handleInboxInternal<TContextData>(
991994
documentLoader: ctx.documentLoader,
992995
timeWindow: signatureTimeWindow,
993996
keyCache,
997+
meterProvider,
994998
tracerProvider,
995999
});
9961000
if (verification.verified === false) {

0 commit comments

Comments
 (0)