From 31a5533fd71bd1245962bedfe487ca751addca7a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 21 Jun 2024 22:38:25 -0500 Subject: [PATCH] Add Prometheus metrics --- deno.json | 1 + installation/ditto.conf | 6 +++ src/controllers/metrics.ts | 14 +++++++ src/controllers/nostr/relay.ts | 5 +++ src/firehose.ts | 4 +- src/metrics.ts | 58 +++++++++++++++++++++++++++++ src/middleware/metricsMiddleware.ts | 10 +++++ src/pipeline.ts | 2 + src/storages/EventsDB.ts | 3 ++ src/workers/fetch.ts | 4 +- 10 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/controllers/metrics.ts create mode 100644 src/metrics.ts create mode 100644 src/middleware/metricsMiddleware.ts diff --git a/deno.json b/deno.json index bc5d1e29..240b6c3a 100644 --- a/deno.json +++ b/deno.json @@ -59,6 +59,7 @@ "nostr-relaypool": "npm:nostr-relaypool2@0.6.34", "nostr-tools": "npm:nostr-tools@2.5.1", "nostr-wasm": "npm:nostr-wasm@^0.1.0", + "prom-client": "npm:prom-client@^15.1.2", "question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts", "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", diff --git a/installation/ditto.conf b/installation/ditto.conf index d74a8865..256498f4 100644 --- a/installation/ditto.conf +++ b/installation/ditto.conf @@ -50,6 +50,12 @@ server { root /opt/ditto/public; } + location /metrics { + allow 127.0.0.1; + deny all; + proxy_pass http://ditto; + } + location ~ ^/(instance|sw\.js$|sw\.js\.map$) { root /opt/ditto/public; try_files $uri =404; diff --git a/src/controllers/metrics.ts b/src/controllers/metrics.ts new file mode 100644 index 00000000..419931da --- /dev/null +++ b/src/controllers/metrics.ts @@ -0,0 +1,14 @@ +import { register } from 'prom-client'; + +import { AppController } from '@/app.ts'; + +/** Prometheus/OpenMetrics controller. */ +export const metricsController: AppController = async (c) => { + const metrics = await register.metrics(); + + const headers: HeadersInit = { + 'Content-Type': register.contentType, + }; + + return c.text(metrics, 200, headers); +}; diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 5d08e02c..c19395dd 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -10,6 +10,7 @@ import { import { AppController } from '@/app.ts'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; +import { relayEventCounter, relayMessageCounter } from '@/metrics.ts'; import * as pipeline from '@/pipeline.ts'; import { RelayError } from '@/RelayError.ts'; import { Storages } from '@/storages.ts'; @@ -22,6 +23,7 @@ function connectStream(socket: WebSocket) { const controllers = new Map(); socket.onmessage = (e) => { + relayMessageCounter.inc(); const result = n.json().pipe(n.clientMsg()).safeParse(e.data); if (result.success) { handleMsg(result.data); @@ -40,15 +42,18 @@ function connectStream(socket: WebSocket) { function handleMsg(msg: NostrClientMsg) { switch (msg[0]) { case 'REQ': + relayEventCounter.inc(); handleReq(msg); return; case 'EVENT': + relayEventCounter.inc({ kind: msg[1].kind.toString() }); handleEvent(msg); return; case 'CLOSE': handleClose(msg); return; case 'COUNT': + relayEventCounter.inc(); handleCount(msg); return; } diff --git a/src/firehose.ts b/src/firehose.ts index 2c776fe4..3e6c8fc0 100644 --- a/src/firehose.ts +++ b/src/firehose.ts @@ -1,5 +1,6 @@ import { Stickynotes } from '@soapbox/stickynotes'; +import { firehoseEventCounter } from '@/metrics.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; @@ -12,13 +13,14 @@ const console = new Stickynotes('ditto:firehose'); * side-effects based on them, such as trending hashtag tracking * and storing events for notifications and the home feed. */ -export async function startFirehose() { +export async function startFirehose(): Promise { const store = await Storages.client(); for await (const msg of store.req([{ kinds: [0, 1, 3, 5, 6, 7, 9735, 10002], limit: 0, since: nostrNow() }])) { if (msg[0] === 'EVENT') { const event = msg[2]; console.debug(`NostrEvent<${event.kind}> ${event.id}`); + firehoseEventCounter.inc({ kind: event.kind }); pipeline .handleEvent(event, AbortSignal.timeout(5000)) diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 00000000..efe1a895 --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,58 @@ +import { Counter } from 'prom-client'; + +export const httpRequestCounter = new Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'path'], +}); + +export const fetchCounter = new Counter({ + name: 'fetch_total', + help: 'Total number of fetch requests', + labelNames: ['method', 'path'], +}); + +export const firehoseEventCounter = new Counter({ + name: 'firehose_events_total', + help: 'Total number of Nostr events processed by the firehose', + labelNames: ['kind'], +}); + +export const pipelineEventCounter = new Counter({ + name: 'pipeline_events_total', + help: 'Total number of Nostr events processed by the pipeline', + labelNames: ['kind'], +}); + +export const relayReqCounter = new Counter({ + name: 'relay_reqs_total', + help: 'Total number of REQ messages processed by the relay', +}); + +export const relayEventCounter = new Counter({ + name: 'relay_events_total', + help: 'Total number of EVENT messages processed by the relay', + labelNames: ['kind'], +}); + +export const relayCountCounter = new Counter({ + name: 'relay_counts_total', + help: 'Total number of COUNT messages processed by the relay', +}); + +export const relayMessageCounter = new Counter({ + name: 'relay_messages_total', + help: 'Total number of Nostr messages processed by the relay', +}); + +export const dbQueryCounter = new Counter({ + name: 'db_query_total', + help: 'Total number of database queries', + labelNames: ['kind'], +}); + +export const dbEventCounter = new Counter({ + name: 'db_events_total', + help: 'Total number of database inserts', + labelNames: ['kind'], +}); diff --git a/src/middleware/metricsMiddleware.ts b/src/middleware/metricsMiddleware.ts new file mode 100644 index 00000000..1e88ff3f --- /dev/null +++ b/src/middleware/metricsMiddleware.ts @@ -0,0 +1,10 @@ +import { MiddlewareHandler } from '@hono/hono'; + +import { httpRequestCounter } from '@/metrics.ts'; + +export const metricsMiddleware: MiddlewareHandler = async (c, next) => { + const { method, path } = c.req; + httpRequestCounter.inc({ method, path }); + + await next(); +}; diff --git a/src/pipeline.ts b/src/pipeline.ts index 40ba9c12..695a027e 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -7,6 +7,7 @@ import { Conf } from '@/config.ts'; import { DittoDB } from '@/db/DittoDB.ts'; import { deleteAttachedMedia } from '@/db/unattached-media.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { pipelineEventCounter } from '@/metrics.ts'; import { RelayError } from '@/RelayError.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; @@ -36,6 +37,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise ${event.id}`); + pipelineEventCounter.inc({ kind: event.kind }); if (event.kind !== 24133) { await policyFilter(event); diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index 085a4270..bd350173 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -7,6 +7,7 @@ import { nip27 } from 'nostr-tools'; import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; +import { dbEventCounter, dbQueryCounter } from '@/metrics.ts'; import { RelayError } from '@/RelayError.ts'; import { purifyEvent } from '@/storages/hydrate.ts'; import { isNostrId, isURL } from '@/utils.ts'; @@ -53,6 +54,7 @@ class EventsDB implements NStore { async event(event: NostrEvent, _opts?: { signal?: AbortSignal }): Promise { event = purifyEvent(event); this.console.debug('EVENT', JSON.stringify(event)); + dbEventCounter.inc({ kind: event.kind }); if (await this.isDeletedAdmin(event)) { throw new RelayError('blocked', 'event deleted by admin'); @@ -137,6 +139,7 @@ class EventsDB implements NStore { /** Get events for filters from the database. */ async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise { filters = await this.expandFilters(filters); + dbQueryCounter.inc(); for (const filter of filters) { if (filter.since && filter.since >= 2_147_483_647) { diff --git a/src/workers/fetch.ts b/src/workers/fetch.ts index f0bece58..ad0c834e 100644 --- a/src/workers/fetch.ts +++ b/src/workers/fetch.ts @@ -1,8 +1,9 @@ import * as Comlink from 'comlink'; +import { FetchWorker } from './fetch.worker.ts'; import './handlers/abortsignal.ts'; -import type { FetchWorker } from './fetch.worker.ts'; +import { fetchCounter } from '@/metrics.ts'; const worker = new Worker(new URL('./fetch.worker.ts', import.meta.url), { type: 'module' }); const client = Comlink.wrap(worker); @@ -24,6 +25,7 @@ const fetchWorker: typeof fetch = async (...args) => { await ready; const [url, init] = serializeFetchArgs(args); const { body, signal, ...rest } = init; + fetchCounter.inc({ method: init.method, path: new URL(url).pathname }); const result = await client.fetch(url, { ...rest, body: await prepareBodyForWorker(body) }, signal); return new Response(...result); };