diff --git a/examples/sync-server/index.js b/examples/sync-server/index.js index 10286e479..1e1d31a48 100644 --- a/examples/sync-server/index.js +++ b/examples/sync-server/index.js @@ -5,8 +5,35 @@ import { WebSocketServer } from "ws" import { Repo } from "@automerge/automerge-repo" import { NodeWSServerAdapter } from "@automerge/automerge-repo-network-websocket" import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs" +import { default as Prometheus } from "prom-client" import os from "os" +const registry = new Prometheus.Registry() +Prometheus.collectDefaultMetrics({ register: registry }) + +const buckets = Prometheus.linearBuckets(0, 1000, 60) + +const metrics = { + docLoaded: new Prometheus.Histogram({ + name: "automerge_repo_doc_loaded_duration_millis", + help: "Duration of loading a document", + buckets, + registers: [registry], + }), + receiveSyncMessage: new Prometheus.Histogram({ + name: "automerge_repo_receive_sync_message_duration_millis", + help: "Duration of receiving a sync message", + buckets, + registers: [registry], + }), + numOps: new Prometheus.Histogram({ + name: "automerge_repo_num_ops", + help: "Number of operations in a document", + buckets: Prometheus.exponentialBuckets(1, 2, 20), + registers: [registry], + }), +} + export class Server { /** @type WebSocketServer */ #socket @@ -45,12 +72,25 @@ export class Server { } const serverRepo = new Repo(config) + // Observe metrics for prometheus and also log the events so log aggregators like loki can pick them up + serverRepo.on("doc-metrics", (event) => { + console.log(JSON.stringify(event)) + metrics.numOps.observe(event.numOps) + if (event.type === "doc-loaded") { + metrics.docLoaded.observe(event.durationMillis) + } else if (event.type === "receive-sync-message") { + metrics.receiveSyncMessage.observe(event.durationMillis) + } + }) + app.get("/", (req, res) => { res.send(`👍 @automerge/example-sync-server is running`) }) - app.get("/metrics", (req, res) => { - res.json(serverRepo.metrics()) + // In a real server this endpoint would be authenticated or not event part of the same express app + app.get("/prometheus_metrics", async (req, res) => { + res.set("Content-Type", registry.contentType) + res.end(await registry.metrics()) }) this.#server = app.listen(PORT, () => { diff --git a/examples/sync-server/package.json b/examples/sync-server/package.json index e488c3ab2..b379f6296 100644 --- a/examples/sync-server/package.json +++ b/examples/sync-server/package.json @@ -15,6 +15,7 @@ "@automerge/automerge-repo-network-websocket": "workspace:*", "@automerge/automerge-repo-storage-nodefs": "workspace:*", "express": "^4.18.1", + "prom-client": "^15.1.3", "ws": "^8.7.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89a0d4c22..9c9da7efb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: express: specifier: ^4.18.1 version: 4.19.2 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 ws: specifier: ^8.7.0 version: 8.18.0 @@ -1027,6 +1030,10 @@ packages: '@octokit/types@9.3.2': resolution: {integrity: sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1782,6 +1789,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -4244,6 +4254,10 @@ packages: resolution: {integrity: sha512-69agxLtnI8xBs9gUGqEnK26UfiexpHy+KUpBQWabiytQjnn5wFY8rklAi7GRfABIuPNnQ/ik48+LGLkYYJcy4A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + promise-all-reject-late@1.0.1: resolution: {integrity: sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==} @@ -4924,6 +4938,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + temp-dir@1.0.0: resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} engines: {node: '>=4'} @@ -6283,6 +6300,8 @@ snapshots: dependencies: '@octokit/openapi-types': 18.1.1 + '@opentelemetry/api@1.9.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -7109,6 +7128,8 @@ snapshots: binary-extensions@2.3.0: {} + bintrees@1.0.2: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -9993,6 +10014,11 @@ snapshots: proggy@2.0.0: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.0 + tdigest: 0.1.2 + promise-all-reject-late@1.0.1: {} promise-call-limit@3.0.1: {} @@ -10777,6 +10803,10 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + temp-dir@1.0.0: {} test-exclude@6.0.0: