diff --git a/packages/ditto/app.ts b/packages/ditto/app.ts index 5f3a12e4..44b0b9b4 100644 --- a/packages/ditto/app.ts +++ b/packages/ditto/app.ts @@ -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; diff --git a/packages/ditto/controllers/api/admin.ts b/packages/ditto/controllers/api/admin.ts index f3611035..124e0f88 100644 --- a/packages/ditto/controllers/api/admin.ts +++ b/packages/ditto/controllers/api/admin.ts @@ -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); } } diff --git a/packages/ditto/controllers/api/cashu.ts b/packages/ditto/controllers/api/cashu.ts index 4546dda3..1989a569 100644 --- a/packages/ditto/controllers/api/cashu.ts +++ b/packages/ditto/controllers/api/cashu.ts @@ -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) }); } } diff --git a/packages/ditto/controllers/api/media.ts b/packages/ditto/controllers/api/media.ts index c6c6b062..65660e99 100644 --- a/packages/ditto/controllers/api/media.ts +++ b/packages/ditto/controllers/api/media.ts @@ -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); } }; diff --git a/packages/ditto/controllers/api/streaming.ts b/packages/ditto/controllers/api/streaming.ts index e6924641..4eebe6b3 100644 --- a/packages/ditto/controllers/api/streaming.ts +++ b/packages/ditto/controllers/api/streaming.ts @@ -65,7 +65,8 @@ const limiter = new TTLCache(); const connections = new Set(); 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) }); } } diff --git a/packages/ditto/controllers/api/translate.ts b/packages/ditto/controllers/api/translate.ts index 65a95a26..ac62ddbe 100644 --- a/packages/ditto/controllers/api/translate.ts +++ b/packages/ditto/controllers/api/translate.ts @@ -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); } }; diff --git a/packages/ditto/controllers/error.ts b/packages/ditto/controllers/error.ts index a00a530b..42c2088f 100644 --- a/packages/ditto/controllers/error.ts +++ b/packages/ditto/controllers/error.ts @@ -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 = (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); }; diff --git a/packages/ditto/controllers/frontend.ts b/packages/ditto/controllers/frontend.ts index c0a6848f..bd2b4de3 100644 --- a/packages/ditto/controllers/frontend.ts +++ b/packages/ditto/controllers/frontend.ts @@ -14,6 +14,8 @@ import { renderAccount } from '@/views/mastodon/accounts.ts'; const META_PLACEHOLDER = '' 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); } } diff --git a/packages/ditto/controllers/nostr/relay.ts b/packages/ditto/controllers/nostr/relay.ts index f6641549..dea341e3 100644 --- a/packages/ditto/controllers/nostr/relay.ts +++ b/packages/ditto/controllers/nostr/relay.ts @@ -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(); +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(); 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; }; diff --git a/packages/ditto/middleware/logiMiddleware.ts b/packages/ditto/middleware/logiMiddleware.ts index be17e3bb..7db2fa87 100644 --- a/packages/ditto/middleware/logiMiddleware.ts +++ b/packages/ditto/middleware/logiMiddleware.ts @@ -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 }); }; diff --git a/packages/mastoapi/router/DittoApp.ts b/packages/mastoapi/router/DittoApp.ts index 2d3c0107..f16ca61e 100644 --- a/packages/mastoapi/router/DittoApp.ts +++ b/packages/mastoapi/router/DittoApp.ts @@ -7,7 +7,7 @@ export class DittoApp extends Hono { // @ts-ignore Require a DittoRoute for type safety. declare route: (path: string, app: Hono) => Hono; - constructor(opts: Omit & HonoOptions) { + constructor(opts: Omit & HonoOptions) { super(opts); this.use((c, next) => { @@ -15,6 +15,7 @@ export class DittoApp extends Hono { 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(); }); } diff --git a/packages/mastoapi/router/DittoEnv.ts b/packages/mastoapi/router/DittoEnv.ts index 7f399e62..35fb0118 100644 --- a/packages/mastoapi/router/DittoEnv.ts +++ b/packages/mastoapi/router/DittoEnv.ts @@ -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; }; } diff --git a/packages/mastoapi/router/DittoMiddleware.ts b/packages/mastoapi/router/DittoMiddleware.ts index 1483ca90..91afd533 100644 --- a/packages/mastoapi/router/DittoMiddleware.ts +++ b/packages/mastoapi/router/DittoMiddleware.ts @@ -2,4 +2,4 @@ import type { MiddlewareHandler } from '@hono/hono'; import type { DittoEnv } from './DittoEnv.ts'; // deno-lint-ignore ban-types -export type DittoMiddleware = MiddlewareHandler; +export type DittoMiddleware = MiddlewareHandler; diff --git a/packages/mastoapi/router/DittoRoute.ts b/packages/mastoapi/router/DittoRoute.ts index 369fb858..53d2109b 100644 --- a/packages/mastoapi/router/DittoRoute.ts +++ b/packages/mastoapi/router/DittoRoute.ts @@ -25,6 +25,7 @@ export class DittoRoute extends Hono { 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 { conf: vars.conf, relay: vars.relay, signal: vars.signal, + requestId: vars.requestId, }; }