Merge branch 'request-id' into 'main'

Add requestId middleware

See merge request soapbox-pub/ditto!704
This commit is contained in:
Alex Gleason 2025-02-28 00:10:49 +00:00
commit d458fc8464
14 changed files with 59 additions and 33 deletions

View file

@ -1,5 +1,5 @@
import { DittoConf } from '@ditto/conf'; import { DittoConf } from '@ditto/conf';
import { DittoDB, DittoPolyPg } from '@ditto/db'; import { DittoPolyPg } from '@ditto/db';
import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware'; import { paginationMiddleware, tokenMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
import { DittoApp, type DittoEnv } from '@ditto/mastoapi/router'; import { DittoApp, type DittoEnv } from '@ditto/mastoapi/router';
import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics'; import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics';
@ -152,21 +152,15 @@ import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts'; import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
export interface AppEnv extends DittoEnv { export interface AppEnv extends DittoEnv {
Variables: { Variables: DittoEnv['Variables'] & {
conf: DittoConf;
/** Uploader for the user to upload files. */ /** Uploader for the user to upload files. */
uploader?: NUploader; uploader?: NUploader;
/** NIP-98 signed event proving the pubkey is owned by the user. */ /** NIP-98 signed event proving the pubkey is owned by the user. */
proof?: NostrEvent; proof?: NostrEvent;
/** Kysely instance for the database. */
db: DittoDB;
/** Base database store. No content filtering. */
relay: NRelay;
/** Normalized pagination params. */ /** Normalized pagination params. */
pagination: { since?: number; until?: number; limit: number }; pagination: { since?: number; until?: number; limit: number };
/** Translation service. */ /** Translation service. */
translator?: DittoTranslator; translator?: DittoTranslator;
signal: AbortSignal;
user?: { user?: {
/** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */
signer: NostrSigner; signer: NostrSigner;

View file

@ -128,7 +128,7 @@ const adminAccountActionSchema = z.object({
}); });
const adminActionController: AppController = async (c) => { const adminActionController: AppController = async (c) => {
const { conf, relay } = c.var; const { conf, relay, requestId } = c.var;
const body = await parseBody(c.req.raw); const body = await parseBody(c.req.raw);
const result = adminAccountActionSchema.safeParse(body); const result = adminAccountActionSchema.safeParse(body);
@ -155,7 +155,7 @@ const adminActionController: AppController = async (c) => {
n.disabled = true; n.disabled = true;
n.suspended = true; n.suspended = true;
relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => { relay.remove!([{ authors: [authorId] }]).catch((e: unknown) => {
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
}); });
} }
if (data.type === 'revoke_name') { if (data.type === 'revoke_name') {
@ -163,7 +163,7 @@ const adminActionController: AppController = async (c) => {
try { try {
await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]); await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]);
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.admin.account.action', type: data.type, requestId, error: errorJson(e) });
return c.json({ error: 'Unexpected runtime error' }, 500); return c.json({ error: 'Unexpected runtime error' }, 500);
} }
} }

View file

@ -103,7 +103,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => {
/** Gets a wallet, if it exists. */ /** Gets a wallet, if it exists. */
route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, async (c) => { route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, async (c) => {
const { conf, relay, user, signal } = c.var; const { conf, relay, user, signal, requestId } = c.var;
const pubkey = await user.signer.getPublicKey(); const pubkey = await user.signer.getPublicKey();
@ -139,7 +139,7 @@ route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, as
return accumulator + current.amount; return accumulator + current.amount;
}, 0); }, 0);
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.cashu.wallet.swap', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.cashu.wallet.swap', requestId, error: errorJson(e) });
} }
} }

View file

@ -21,7 +21,7 @@ const mediaUpdateSchema = z.object({
}); });
const mediaController: AppController = async (c) => { const mediaController: AppController = async (c) => {
const { user, signal } = c.var; const { user, signal, requestId } = c.var;
const pubkey = await user!.signer.getPublicKey(); const pubkey = await user!.signer.getPublicKey();
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw)); const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
@ -35,7 +35,7 @@ const mediaController: AppController = async (c) => {
const media = await uploadFile(c, file, { pubkey, description }, signal); const media = await uploadFile(c, file, { pubkey, description }, signal);
return c.json(renderAttachment(media)); return c.json(renderAttachment(media));
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.api.media', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.api.media', requestId, error: errorJson(e) });
return c.json({ error: 'Failed to upload file.' }, 500); return c.json({ error: 'Failed to upload file.' }, 500);
} }
}; };

View file

@ -65,7 +65,8 @@ const limiter = new TTLCache<string, number>();
const connections = new Set<WebSocket>(); const connections = new Set<WebSocket>();
const streamingController: AppController = async (c) => { const streamingController: AppController = async (c) => {
const { conf, relay, user } = c.var; const { conf, relay, user, requestId } = c.var;
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
const token = c.req.header('sec-websocket-protocol'); const token = c.req.header('sec-websocket-protocol');
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream')); const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
@ -122,7 +123,7 @@ const streamingController: AppController = async (c) => {
} }
} }
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.streaming', msg: 'Error in streaming', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.streaming', msg: 'Error in streaming', requestId, error: errorJson(e) });
} }
} }

View file

@ -17,7 +17,7 @@ const translateSchema = z.object({
}); });
const translateController: AppController = async (c) => { const translateController: AppController = async (c) => {
const { relay, user, signal } = c.var; const { relay, user, signal, requestId } = c.var;
const result = translateSchema.safeParse(await parseBody(c.req.raw)); const result = translateSchema.safeParse(await parseBody(c.req.raw));
@ -143,7 +143,7 @@ const translateController: AppController = async (c) => {
if (e instanceof Error && e.message.includes('not supported')) { if (e instanceof Error && e.message.includes('not supported')) {
return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422); return c.json({ error: `Translation of source language '${event.language}' not supported` }, 422);
} }
logi({ level: 'error', ns: 'ditto.translate', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.translate', requestId, error: errorJson(e) });
return c.json({ error: 'Service Unavailable' }, 503); return c.json({ error: 'Service Unavailable' }, 503);
} }
}; };

View file

@ -4,7 +4,10 @@ import { logi } from '@soapbox/logi';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
export const errorHandler: ErrorHandler = (err, c) => { import type { DittoEnv } from '@ditto/mastoapi/router';
export const errorHandler: ErrorHandler<DittoEnv> = (err, c) => {
const { requestId } = c.var;
const { method } = c.req; const { method } = c.req;
const { pathname } = new URL(c.req.url); const { pathname } = new URL(c.req.url);
@ -22,7 +25,15 @@ export const errorHandler: ErrorHandler = (err, c) => {
return c.json({ error: 'The server was unable to respond in a timely manner' }, 500); return c.json({ error: 'The server was unable to respond in a timely manner' }, 500);
} }
logi({ level: 'error', ns: 'ditto.http', msg: 'Unhandled error', method, pathname, error: errorJson(err) }); logi({
level: 'error',
ns: 'ditto.http',
msg: 'Unhandled error',
method,
pathname,
requestId,
error: errorJson(err),
});
return c.json({ error: 'Something went wrong' }, 500); return c.json({ error: 'Something went wrong' }, 500);
}; };

View file

@ -14,6 +14,8 @@ import { renderAccount } from '@/views/mastodon/accounts.ts';
const META_PLACEHOLDER = '<!--server-generated-meta-->' as const; const META_PLACEHOLDER = '<!--server-generated-meta-->' as const;
export const frontendController: AppMiddleware = async (c) => { export const frontendController: AppMiddleware = async (c) => {
const { requestId } = c.var;
c.header('Cache-Control', 'max-age=86400, s-maxage=30, public, stale-if-error=604800'); c.header('Cache-Control', 'max-age=86400, s-maxage=30, public, stale-if-error=604800');
try { try {
@ -26,7 +28,7 @@ export const frontendController: AppMiddleware = async (c) => {
const meta = renderMetadata(c.req.url, entities); const meta = renderMetadata(c.req.url, entities);
return c.html(content.replace(META_PLACEHOLDER, meta)); return c.html(content.replace(META_PLACEHOLDER, meta));
} catch (e) { } catch (e) {
logi({ level: 'error', ns: 'ditto.frontend', msg: 'Error building meta tags', error: errorJson(e) }); logi({ level: 'error', ns: 'ditto.frontend', msg: 'Error building meta tags', requestId, error: errorJson(e) });
return c.html(content); return c.html(content);
} }
} }

View file

@ -11,6 +11,7 @@ import {
NostrClientMsg, NostrClientMsg,
NostrClientREQ, NostrClientREQ,
NostrRelayMsg, NostrRelayMsg,
NRelay,
NSchema as n, NSchema as n,
} from '@nostrify/nostrify'; } from '@nostrify/nostrify';
@ -40,8 +41,17 @@ const limiters = {
/** Connections for metrics purposes. */ /** Connections for metrics purposes. */
const connections = new Set<WebSocket>(); const connections = new Set<WebSocket>();
interface ConnectStreamOpts {
conf: DittoConf;
relay: NRelay;
requestId: string;
}
/** Set up the Websocket connection. */ /** Set up the Websocket connection. */
function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket, ip: string | undefined) { function connectStream(socket: WebSocket, ip: string | undefined, opts: ConnectStreamOpts): void {
const { conf, requestId } = opts;
const relay = opts.relay as DittoPgStore;
const controllers = new Map<string, AbortController>(); const controllers = new Map<string, AbortController>();
if (ip) { if (ip) {
@ -74,7 +84,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
const msg = result.data; const msg = result.data;
const verb = msg[0]; const verb = msg[0];
logi({ level: 'trace', ns: 'ditto.relay.msg', verb, msg: msg as JsonValue, ip }); logi({ level: 'trace', ns: 'ditto.relay.msg', verb, msg: msg as JsonValue, ip, requestId });
relayMessagesCounter.inc({ verb }); relayMessagesCounter.inc({ verb });
handleMsg(result.data); handleMsg(result.data);
@ -165,7 +175,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
send(['OK', event.id, false, e.message]); send(['OK', event.id, false, e.message]);
} else { } else {
send(['OK', event.id, false, 'error: something went wrong']); send(['OK', event.id, false, 'error: something went wrong']);
logi({ level: 'error', ns: 'ditto.relay', msg: 'Error in relay', error: errorJson(e), ip }); logi({ level: 'error', ns: 'ditto.relay', msg: 'Error in relay', error: errorJson(e), ip, requestId });
} }
} }
} }
@ -195,7 +205,8 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
} }
const relayController: AppController = (c, next) => { const relayController: AppController = (c, next) => {
const { conf, relay } = c.var; const { conf } = c.var;
const upgrade = c.req.header('upgrade'); const upgrade = c.req.header('upgrade');
// NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md // NIP-11: https://github.com/nostr-protocol/nips/blob/master/11.md
@ -214,7 +225,7 @@ const relayController: AppController = (c, next) => {
} }
const { socket, response } = Deno.upgradeWebSocket(c.req.raw); const { socket, response } = Deno.upgradeWebSocket(c.req.raw);
connectStream(conf, relay as DittoPgStore, socket, ip); connectStream(socket, ip, c.var);
return response; return response;
}; };

View file

@ -1,11 +1,13 @@
import { MiddlewareHandler } from '@hono/hono';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
export const logiMiddleware: MiddlewareHandler = async (c, next) => { import type { DittoMiddleware } from '@ditto/mastoapi/router';
export const logiMiddleware: DittoMiddleware = async (c, next) => {
const { requestId } = c.var;
const { method } = c.req; const { method } = c.req;
const { pathname } = new URL(c.req.url); const { pathname } = new URL(c.req.url);
logi({ level: 'info', ns: 'ditto.http.request', method, pathname }); logi({ level: 'info', ns: 'ditto.http.request', method, pathname, requestId });
const start = new Date(); const start = new Date();
@ -15,5 +17,5 @@ export const logiMiddleware: MiddlewareHandler = async (c, next) => {
const duration = (end.getTime() - start.getTime()) / 1000; const duration = (end.getTime() - start.getTime()) / 1000;
const level = c.res.status >= 500 ? 'error' : 'info'; const level = c.res.status >= 500 ? 'error' : 'info';
logi({ level, ns: 'ditto.http.response', method, pathname, status: c.res.status, duration }); logi({ level, ns: 'ditto.http.response', method, pathname, status: c.res.status, duration, requestId });
}; };

View file

@ -7,7 +7,7 @@ export class DittoApp extends Hono<DittoEnv> {
// @ts-ignore Require a DittoRoute for type safety. // @ts-ignore Require a DittoRoute for type safety.
declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>; declare route: (path: string, app: Hono<DittoEnv>) => Hono<DittoEnv>;
constructor(opts: Omit<DittoEnv['Variables'], 'signal'> & HonoOptions<DittoEnv>) { constructor(opts: Omit<DittoEnv['Variables'], 'signal' | 'requestId'> & HonoOptions<DittoEnv>) {
super(opts); super(opts);
this.use((c, next) => { this.use((c, next) => {
@ -15,6 +15,7 @@ export class DittoApp extends Hono<DittoEnv> {
c.set('conf', opts.conf); c.set('conf', opts.conf);
c.set('relay', opts.relay); c.set('relay', opts.relay);
c.set('signal', c.req.raw.signal); c.set('signal', c.req.raw.signal);
c.set('requestId', c.req.header('X-Request-Id') ?? crypto.randomUUID());
return next(); return next();
}); });
} }

View file

@ -16,5 +16,7 @@ export interface DittoEnv extends Env {
db: DittoDB; db: DittoDB;
/** Abort signal for the request. */ /** Abort signal for the request. */
signal: AbortSignal; signal: AbortSignal;
/** Unique ID for the request. */
requestId: string;
}; };
} }

View file

@ -2,4 +2,4 @@ import type { MiddlewareHandler } from '@hono/hono';
import type { DittoEnv } from './DittoEnv.ts'; import type { DittoEnv } from './DittoEnv.ts';
// deno-lint-ignore ban-types // deno-lint-ignore ban-types
export type DittoMiddleware<T extends {}> = MiddlewareHandler<DittoEnv & { Variables: T }>; export type DittoMiddleware<T extends {} = {}> = MiddlewareHandler<DittoEnv & { Variables: T }>;

View file

@ -25,6 +25,7 @@ export class DittoRoute extends Hono<DittoEnv> {
if (!vars.conf) this.throwMissingVar('conf'); if (!vars.conf) this.throwMissingVar('conf');
if (!vars.relay) this.throwMissingVar('relay'); if (!vars.relay) this.throwMissingVar('relay');
if (!vars.signal) this.throwMissingVar('signal'); if (!vars.signal) this.throwMissingVar('signal');
if (!vars.requestId) this.throwMissingVar('requestId');
return { return {
...vars, ...vars,
@ -32,6 +33,7 @@ export class DittoRoute extends Hono<DittoEnv> {
conf: vars.conf, conf: vars.conf,
relay: vars.relay, relay: vars.relay,
signal: vars.signal, signal: vars.signal,
requestId: vars.requestId,
}; };
} }