Unverified Commit 1d390609 authored by Hong Minhee (洪 民憙)'s avatar Hong Minhee (洪 民憙) Committed by GitHub
Browse files

Merge pull request #483 from dahlia/opentelemetry

Add OpenTelemetry span events and missing instrumentation
parents 4cc6d2c8 0faf26fe
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -8,6 +8,36 @@ Version 1.10.0

To be released.

### @fedify/fedify

 -  Enhanced OpenTelemetry instrumentation with span events for capturing
    detailed activity data.  Span events now record complete activity JSON
    payloads and verification status, enabling richer observability and
    debugging capabilities without relying solely on span attributes
    (which only support primitive values).  [[#323]]

     -  Added `activitypub.activity.received` span event to the
        `activitypub.inbox` span, recording the full activity JSON,
        verification status (activity verified, HTTP signatures verified,
        Linked Data signatures verified), and actor information.
     -  Added `activitypub.activity.sent` span event to the
        `activitypub.send_activity` span, recording the full activity JSON
        and target inbox URL.
     -  Added `activitypub.object.fetched` span event to the
        `activitypub.lookup_object` span, recording the fetched object's
        type and complete JSON-LD representation.

 -  Added OpenTelemetry spans for previously uninstrumented operations:
    [[#323]]

     -  Added `activitypub.fetch_document` span for document loader operations,
        tracking URL fetching, HTTP redirects, and final document URLs.
     -  Added `activitypub.verify_key_ownership` span for cryptographic
        key ownership verification, recording actor ID, key ID, verification
        result, and the verification method used.

[#323]: https://github.com/fedify-dev/fedify/issues/323

### @fedify/nestjs

 -  Allowed Express 5 in the `express` peer dependency range to support NestJS 11.
+33 −3
Original line number Diff line number Diff line
@@ -80,7 +80,9 @@
    "npm:@multiformats/base-x@^4.0.1": "4.0.1",
    "npm:@nestjs/common@^11.0.1": "11.1.6_reflect-metadata@0.2.2_rxjs@7.8.2",
    "npm:@opentelemetry/api@^1.9.0": "1.9.0",
    "npm:@opentelemetry/semantic-conventions@^1.27.0": "1.37.0",
    "npm:@opentelemetry/core@^1.30.1": "1.30.1_@opentelemetry+api@1.9.0",
    "npm:@opentelemetry/sdk-trace-base@^1.30.1": "1.30.1_@opentelemetry+api@1.9.0",
    "npm:@opentelemetry/semantic-conventions@^1.27.0": "1.28.0",
    "npm:@phensley/language-tag@^1.9.0": "1.13.1",
    "npm:@poppanator/http-constants@^1.1.1": "1.1.1",
    "npm:@preact/signals-core@1.5.1": "1.5.1",
@@ -1343,8 +1345,32 @@
    "@opentelemetry/api@1.9.0": {
      "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="
    },
    "@opentelemetry/semantic-conventions@1.37.0": {
      "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA=="
    "@opentelemetry/core@1.30.1_@opentelemetry+api@1.9.0": {
      "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
      "dependencies": [
        "@opentelemetry/api",
        "@opentelemetry/semantic-conventions"
      ]
    },
    "@opentelemetry/resources@1.30.1_@opentelemetry+api@1.9.0": {
      "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
      "dependencies": [
        "@opentelemetry/api",
        "@opentelemetry/core",
        "@opentelemetry/semantic-conventions"
      ]
    },
    "@opentelemetry/sdk-trace-base@1.30.1_@opentelemetry+api@1.9.0": {
      "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
      "dependencies": [
        "@opentelemetry/api",
        "@opentelemetry/core",
        "@opentelemetry/resources",
        "@opentelemetry/semantic-conventions"
      ]
    },
    "@opentelemetry/semantic-conventions@1.28.0": {
      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="
    },
    "@oxc-project/types@0.94.0": {
      "integrity": "sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw=="
@@ -4584,6 +4610,8 @@
          "npm:@cfworker/json-schema@^4.1.1",
          "npm:@multiformats/base-x@^4.0.1",
          "npm:@opentelemetry/api@^1.9.0",
          "npm:@opentelemetry/core@^1.30.1",
          "npm:@opentelemetry/sdk-trace-base@^1.30.1",
          "npm:@opentelemetry/semantic-conventions@^1.27.0",
          "npm:@phensley/language-tag@^1.9.0",
          "npm:asn1js@^3.0.5",
@@ -4604,6 +4632,8 @@
            "npm:@js-temporal/polyfill@~0.5.1",
            "npm:@multiformats/base-x@^4.0.1",
            "npm:@opentelemetry/api@^1.9.0",
            "npm:@opentelemetry/core@^1.30.1",
            "npm:@opentelemetry/sdk-trace-base@^1.30.1",
            "npm:@opentelemetry/semantic-conventions@^1.27.0",
            "npm:@phensley/language-tag@^1.9.0",
            "npm:@types/node@^24.2.1",
+156 −0
Original line number Diff line number Diff line
@@ -174,7 +174,9 @@ spans:
| `activitypub.outbox`                                | Consumer    | Dequeues the ActivityPub activity to send.    |
| `activitypub.outbox`                                | Producer    | Enqueues the ActivityPub activity to send.    |
| `activitypub.parse_object`                          | Internal    | Parses the Activity Streams object.           |
| `activitypub.fetch_document`                        | Client      | Fetches a remote JSON-LD document.            |
| `activitypub.send_activity`                         | Client      | Sends the ActivityPub activity.               |
| `activitypub.verify_key_ownership`                  | Internal    | Verifies actor ownership of a key.            |
| `http_signatures.sign`                              | Internal    | Signs the HTTP request.                       |
| `http_signatures.verify`                            | Internal    | Verifies the HTTP request signature.          |
| `ld_signatures.sign`                                | Internal    | Makes the Linked Data signature.              |
@@ -189,6 +191,47 @@ More operations will be instrumented in the future releases.
[Span kind]: https://opentelemetry.io/docs/specs/otel/trace/api/#spankind


Span events
-----------

In addition to spans, Fedify also records [span events] to capture rich,
structured data about key operations.  Span events allow recording complex data
that wouldn't fit in span attributes (which are limited to primitive values).

The following span events are recorded:

| Event name                         | Recorded on span             | Description                                                                      |
|------------------------------------|------------------------------|----------------------------------------------------------------------------------|
| `activitypub.activity.received`    | `activitypub.inbox`          | Records full activity JSON and verification status when an activity is received. |
| `activitypub.activity.sent`        | `activitypub.send_activity`  | Records full activity JSON and delivery details when an activity is sent.        |
| `activitypub.object.fetched`       | `activitypub.lookup_object`  | Records full object JSON when successfully fetched.                              |

### Event attributes

Each span event includes attributes with detailed information:

**`activitypub.activity.received` event attributes:**

 -  `activitypub.activity.json`: The complete activity JSON
 -  `activitypub.activity.verified`: Whether the activity was verified (`true`/`false`)
 -  `ld_signatures.verified`: Whether Linked Data Signatures were verified (`true`/`false`)
 -  `http_signatures.verified`: Whether HTTP Signatures were verified (`true`/`false`)
 -  `http_signatures.key_id`: The key ID used for HTTP signature verification

**`activitypub.activity.sent` event attributes:**

 -  `activitypub.activity.json`: The complete activity JSON being sent
 -  `activitypub.inbox.url`: The inbox URL where the activity was delivered
 -  `activitypub.activity.id`: The activity ID

**`activitypub.object.fetched` event attributes:**

 -  `activitypub.object.type`: The type URI of the fetched object
 -  `activitypub.object.json`: The complete object JSON

[span events]: https://opentelemetry.io/docs/concepts/signals/traces/#span-events


Semantic [attributes] for ActivityPub
-------------------------------------

@@ -209,6 +252,9 @@ for ActivityPub:
| `activitypub.actor.id`                | string   | The URI of the actor object.                                                             | `"https://example.com/actor/1"`                                      |
| `activitypub.actor.key.cached`        | boolean  | Whether the actor's public keys are cached.                                              | `true`                                                               |
| `activitypub.actor.type`              | string[] | The qualified URI(s) of the actor type(s).                                               | `["https://www.w3.org/ns/activitystreams#Person"]`                   |
| `activitypub.key.id`                  | string   | The URI of the cryptographic key being verified.                                         | `"https://example.com/actor/1#main-key"`                             |
| `activitypub.key_ownership.method`    | string   | The method used to verify key ownership (`owner_id` or `actor_fetch`).                   | `"actor_fetch"`                                                      |
| `activitypub.key_ownership.verified`  | boolean  | Whether the key ownership was successfully verified.                                     | `true`                                                               |
| `activitypub.collection.id`           | string   | The URI of the collection object.                                                        | `"https://example.com/collection/1"`                                 |
| `activitypub.collection.type`         | string[] | The qualified URI(s) of the collection type(s).                                          | `["https://www.w3.org/ns/activitystreams#OrderedCollection"]`        |
| `activitypub.collection.total_items`  | int      | The total number of items in the collection.                                             | `42`                                                                 |
@@ -217,12 +263,16 @@ for ActivityPub:
| `activitypub.object.in_reply_to`      | string[] | The URI(s) of the original object to which the object reply.                             | `["https://example.com/object/1"]`                                   |
| `activitypub.inboxes`                 | int      | The number of inboxes the activity is sent to.                                           | `12`                                                                 |
| `activitypub.shared_inbox`            | boolean  | Whether the activity is sent to the shared inbox.                                        | `true`                                                               |
| `docloader.context_url`               | string   | The URL of the JSON-LD context document (if provided via Link header).                   | `"https://www.w3.org/ns/activitystreams"`                            |
| `docloader.document_url`              | string   | The final URL of the fetched document (after following redirects).                       | `"https://example.com/object/1"`                                     |
| `fedify.actor.identifier`             | string   | The identifier of the actor.                                                             | `"1"`                                                                |
| `fedify.inbox.recipient`              | string   | The identifier of the inbox recipient.                                                   | `"1"`                                                                |
| `fedify.object.type`                  | string   | The URI of the object type.                                                              | `"https://www.w3.org/ns/activitystreams#Note"`                       |
| `fedify.object.values.{parameter}`    | string[] | The argument values of the object dispatcher.                                            | `["1", "2"]`                                                         |
| `fedify.collection.cursor`            | string   | The cursor of the collection.                                                            | `"eyJpZCI6IjEiLCJ0eXBlIjoiT3JkZXJlZENvbGxlY3Rpb24ifQ=="`             |
| `fedify.collection.items`             | number   | The number of items in the collection page.  It can be less than the total items.        | `10`                                                                 |
| `http.redirect.url`                   | string   | The redirect URL when a document fetch results in a redirect.                            | `"https://example.com/new-location"`                                 |
| `http.response.status_code`           | int      | The HTTP response status code.                                                           | `200`                                                                |
| `http_signatures.signature`           | string   | The signature of the HTTP request in hexadecimal.                                        | `"73a74c990beabe6e59cc68f9c6db7811b59cbb22fd12dcffb3565b651540efe9"` |
| `http_signatures.algorithm`           | string   | The algorithm of the HTTP request signature.                                             | `"rsa-sha256"`                                                       |
| `http_signatures.key_id`              | string   | The public key ID of the HTTP request signature.                                         | `"https://example.com/actor/1#main-key"`                             |
@@ -233,8 +283,114 @@ for ActivityPub:
| `object_integrity_proofs.cryptosuite` | string   | The cryptographic suite of the object integrity proof.                                   | `"eddsa-jcs-2022"`                                                   |
| `object_integrity_proofs.key_id`      | string   | The public key ID of the object integrity proof.                                         | `"https://example.com/actor/1#main-key"`                             |
| `object_integrity_proofs.signature`   | string   | The integrity proof of the object in hexadecimal.                                        | `"73a74c990beabe6e59cc68f9c6db7811b59cbb22fd12dcffb3565b651540efe9"` |
| `url.full`                            | string   | The full URL being fetched by the document loader.                                       | `"https://example.com/actor/1"`                                      |
| `webfinger.resource`                  | string   | The queried resource URI.                                                                | `"acct:fedify@hollo.social"`                                         |
| `webfinger.resource.scheme`           | string   | The scheme of the queried resource URI.                                                  | `"acct"`                                                             |

[attributes]: https://opentelemetry.io/docs/specs/otel/common/#attribute
[OpenTelemetry Semantic Conventions]: https://opentelemetry.io/docs/specs/semconv/


Building observability tools with OpenTelemetry
------------------------------------------------

The OpenTelemetry instrumentation in Fedify provides a powerful foundation for
building custom observability tools.  By implementing a custom [SpanExporter],
you can capture and process all the telemetry data generated by Fedify to build
tools like debug dashboards, activity monitors, or analytics systems.

### Example: ActivityPub debug dashboard

Here's an example of how you might implement a custom `SpanExporter` to capture
ActivityPub activities for a debug dashboard:

~~~~ typescript
import type { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { ExportResultCode } from "@opentelemetry/core";

interface ActivityRecord {
  direction: "inbound" | "outbound";
  activity: unknown;
  timestamp: Date;
  verified?: boolean;
}

export class FedifyDebugExporter implements SpanExporter {
  private activities: ActivityRecord[] = [];

  export(spans: ReadableSpan[], resultCallback: (result: { code: ExportResultCode }) => void): void {
    for (const span of spans) {
      // Capture inbound activities
      if (span.name === "activitypub.inbox") {
        const event = span.events.find(
          (e) => e.name === "activitypub.activity.received"
        );
        if (event && event.attributes) {
          this.activities.push({
            direction: "inbound",
            activity: JSON.parse(
              event.attributes["activitypub.activity.json"] as string
            ),
            timestamp: new Date(span.startTime[0] * 1000),
            verified: event.attributes["activitypub.activity.verified"] as boolean,
          });
        }
      }

      // Capture outbound activities
      if (span.name === "activitypub.send_activity") {
        const event = span.events.find(
          (e) => e.name === "activitypub.activity.sent"
        );
        if (event && event.attributes) {
          this.activities.push({
            direction: "outbound",
            activity: JSON.parse(
              event.attributes["activitypub.activity.json"] as string
            ),
            timestamp: new Date(span.startTime[0] * 1000),
          });
        }
      }
    }
    resultCallback({ code: ExportResultCode.SUCCESS });
  }

  async forceFlush(): Promise<void> {
    // Flush any pending data
  }

  async shutdown(): Promise<void> {
    // Clean up resources
  }

  getActivities(): ActivityRecord[] {
    return this.activities;
  }
}
~~~~

### Integrating the custom exporter

To use the custom exporter, add it to your OpenTelemetry SDK configuration:

~~~~ typescript
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { createFederation } from "@fedify/fedify";

const debugExporter = new FedifyDebugExporter();
const tracerProvider = new NodeTracerProvider();
tracerProvider.addSpanProcessor(new SimpleSpanProcessor(debugExporter));

const federation = createFederation({
  kv: /* your KV store */,
  tracerProvider,
});
~~~~

Now the `debugExporter` will receive all telemetry data from Fedify, and you
can use `debugExporter.getActivities()` to access the captured activities for
your debug dashboard or other observability tools.

[SpanExporter]: https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html
+2 −0
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@
    "@cfworker/json-schema": "npm:@cfworker/json-schema@^4.1.1",
    "@multiformats/base-x": "npm:@multiformats/base-x@^4.0.1",
    "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
    "@opentelemetry/core": "npm:@opentelemetry/core@^1.30.1",
    "@opentelemetry/sdk-trace-base": "npm:@opentelemetry/sdk-trace-base@^1.30.1",
    "@opentelemetry/semantic-conventions": "npm:@opentelemetry/semantic-conventions@^1.27.0",
    "@phensley/language-tag": "npm:@phensley/language-tag@^1.9.0",
    "@std/assert": "jsr:@std/assert@^0.226.0",
+2 −0
Original line number Diff line number Diff line
@@ -155,6 +155,8 @@
    "@logtape/logtape": "catalog:",
    "@multiformats/base-x": "^4.0.1",
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/core": "^1.30.1",
    "@opentelemetry/sdk-trace-base": "^1.30.1",
    "@opentelemetry/semantic-conventions": "^1.27.0",
    "@phensley/language-tag": "^1.9.0",
    "asn1js": "^3.0.5",
Loading