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:09:31 +00:00
commit fb83577737
14 changed files with 59 additions and 33 deletions

View file

@ -1,5 +1,5 @@
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 { DittoApp, type DittoEnv } from '@ditto/mastoapi/router';
import { relayPoolRelaysSizeGauge, relayPoolSubscriptionsSizeGauge } from '@ditto/metrics';
@ -152,21 +152,15 @@ import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
export interface AppEnv extends DittoEnv {
Variables: {
conf: DittoConf;
Variables: DittoEnv['Variables'] & {
/** Uploader for the user to upload files. */
uploader?: NUploader;
/** NIP-98 signed event proving the pubkey is owned by the user. */
proof?: NostrEvent;
/** Kysely instance for the database. */
db: DittoDB;
/** Base database store. No content filtering. */
relay: NRelay;
/** Normalized pagination params. */
pagination: { since?: number; until?: number; limit: number };
/** Translation service. */
translator?: DittoTranslator;
signal: AbortSignal;
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: NostrSigner;

View file

@ -128,7 +128,7 @@ const adminAccountActionSchema = z.object({
});
const adminActionController: AppController = async (c) => {
const { conf, relay } = c.var;
const { conf, relay, requestId } = c.var;
const body = await parseBody(c.req.raw);
const result = adminAccountActionSchema.safeParse(body);
@ -155,7 +155,7 @@ const adminActionController: AppController = async (c) => {
n.disabled = true;
n.suspended = true;
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') {
@ -163,7 +163,7 @@ const adminActionController: AppController = async (c) => {
try {
await relay.remove!([{ kinds: [30360], authors: [await conf.signer.getPublicKey()], '#p': [authorId] }]);
} 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);
}
}

View file

@ -103,7 +103,7 @@ route.put('/wallet', userMiddleware({ enc: 'nip44' }), async (c) => {
/** Gets a wallet, if it exists. */
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();
@ -139,7 +139,7 @@ route.get('/wallet', userMiddleware({ enc: 'nip44' }), swapNutzapsMiddleware, as
return accumulator + current.amount;
}, 0);
} 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 { user, signal } = c.var;
const { user, signal, requestId } = c.var;
const pubkey = await user!.signer.getPublicKey();
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);
return c.json(renderAttachment(media));
} 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);
}
};

View file

@ -65,7 +65,8 @@ const limiter = new TTLCache<string, number>();
const connections = new Set<WebSocket>();
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 token = c.req.header('sec-websocket-protocol');
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
@ -122,7 +123,7 @@ const streamingController: AppController = async (c) => {
}
}
} 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 { relay, user, signal } = c.var;
const { relay, user, signal, requestId } = c.var;
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')) {
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);
}
};

View file

@ -4,7 +4,10 @@ import { logi } from '@soapbox/logi';
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 { 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);
}
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);
};

View file

@ -14,6 +14,8 @@ import { renderAccount } from '@/views/mastodon/accounts.ts';
const META_PLACEHOLDER = '<!--server-generated-meta-->' as const;
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');
try {
@ -26,7 +28,7 @@ export const frontendController: AppMiddleware = async (c) => {
const meta = renderMetadata(c.req.url, entities);
return c.html(content.replace(META_PLACEHOLDER, meta));
} 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);
}
}

View file

@ -11,6 +11,7 @@ import {
NostrClientMsg,
NostrClientREQ,
NostrRelayMsg,
NRelay,
NSchema as n,
} from '@nostrify/nostrify';
@ -40,8 +41,17 @@ const limiters = {
/** Connections for metrics purposes. */
const connections = new Set<WebSocket>();
interface ConnectStreamOpts {
conf: DittoConf;
relay: NRelay;
requestId: string;
}
/** 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>();
if (ip) {
@ -74,7 +84,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
const msg = result.data;
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 });
handleMsg(result.data);
@ -165,7 +175,7 @@ function connectStream(conf: DittoConf, relay: DittoPgStore, socket: WebSocket,
send(['OK', event.id, false, e.message]);
} else {
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 { conf, relay } = c.var;
const { conf } = c.var;
const upgrade = c.req.header('upgrade');
// 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);
connectStream(conf, relay as DittoPgStore, socket, ip);
connectStream(socket, ip, c.var);
return response;
};

View file

@ -1,11 +1,13 @@
import { MiddlewareHandler } from '@hono/hono';
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 { 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();
@ -15,5 +17,5 @@ export const logiMiddleware: MiddlewareHandler = async (c, next) => {
const duration = (end.getTime() - start.getTime()) / 1000;
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.
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);
this.use((c, next) => {
@ -15,6 +15,7 @@ export class DittoApp extends Hono<DittoEnv> {
c.set('conf', opts.conf);
c.set('relay', opts.relay);
c.set('signal', c.req.raw.signal);
c.set('requestId', c.req.header('X-Request-Id') ?? crypto.randomUUID());
return next();
});
}

View file

@ -16,5 +16,7 @@ export interface DittoEnv extends Env {
db: DittoDB;
/** Abort signal for the request. */
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';
// 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.relay) this.throwMissingVar('relay');
if (!vars.signal) this.throwMissingVar('signal');
if (!vars.requestId) this.throwMissingVar('requestId');
return {
...vars,
@ -32,6 +33,7 @@ export class DittoRoute extends Hono<DittoEnv> {
conf: vars.conf,
relay: vars.relay,
signal: vars.signal,
requestId: vars.requestId,
};
}