diff --git a/packages/store-indexer/bin/postgres-decoded-indexer.ts b/packages/store-indexer/bin/postgres-decoded-indexer.ts index 01c35523da..0b34d72008 100644 --- a/packages/store-indexer/bin/postgres-decoded-indexer.ts +++ b/packages/store-indexer/bin/postgres-decoded-indexer.ts @@ -10,6 +10,9 @@ import postgres from "postgres"; import { createStorageAdapter } from "@latticexyz/store-sync/postgres-decoded"; import { createStoreSync } from "@latticexyz/store-sync"; import { indexerEnvSchema, parseEnv } from "./parseEnv"; +import { sentry } from "../src/koa-middleware/sentry"; +import { healthcheck } from "../src/koa-middleware/healthcheck"; +import { helloWorld } from "../src/koa-middleware/helloWorld"; const env = parseEnv( z.intersection( @@ -18,6 +21,7 @@ const env = parseEnv( DATABASE_URL: z.string(), HEALTHCHECK_HOST: z.string().optional(), HEALTHCHECK_PORT: z.coerce.number().optional(), + SENTRY_DSN: z.string().optional(), }) ) ); @@ -88,33 +92,21 @@ combineLatest([latestBlockNumber$, storedBlockLogs$]) if (env.HEALTHCHECK_HOST != null || env.HEALTHCHECK_PORT != null) { const { default: Koa } = await import("koa"); const { default: cors } = await import("@koa/cors"); - const { default: Router } = await import("@koa/router"); const server = new Koa(); - server.use(cors()); - - const router = new Router(); - - router.get("/", (ctx) => { - ctx.body = "emit HelloWorld();"; - }); - // k8s healthchecks - router.get("/healthz", (ctx) => { - ctx.status = 200; - }); - router.get("/readyz", (ctx) => { - if (isCaughtUp) { - ctx.status = 200; - ctx.body = "ready"; - } else { - ctx.status = 424; - ctx.body = "backfilling"; - } - }); + if (env.SENTRY_DSN) { + server.use(sentry(env.SENTRY_DSN)); + } - server.use(router.routes()); - server.use(router.allowedMethods()); + server.use(cors()); + server.use(cors()); + server.use( + healthcheck({ + isReady: () => isCaughtUp, + }) + ); + server.use(helloWorld()); server.listen({ host: env.HEALTHCHECK_HOST, port: env.HEALTHCHECK_PORT }); console.log( diff --git a/packages/store-indexer/bin/postgres-frontend.ts b/packages/store-indexer/bin/postgres-frontend.ts index 6fec89b174..037a53cddf 100644 --- a/packages/store-indexer/bin/postgres-frontend.ts +++ b/packages/store-indexer/bin/postgres-frontend.ts @@ -3,7 +3,6 @@ import "dotenv/config"; import { z } from "zod"; import Koa from "koa"; import cors from "@koa/cors"; -import Router from "@koa/router"; import { createKoaMiddleware } from "trpc-koa-adapter"; import { createAppRouter } from "@latticexyz/store-sync/trpc-indexer"; import { drizzle } from "drizzle-orm/postgres-js"; @@ -11,13 +10,16 @@ import postgres from "postgres"; import { frontendEnvSchema, parseEnv } from "./parseEnv"; import { createQueryAdapter } from "../src/postgres/deprecated/createQueryAdapter"; import { apiRoutes } from "../src/postgres/apiRoutes"; -import { registerSentryMiddlewares } from "../src/sentry"; +import { sentry } from "../src/koa-middleware/sentry"; +import { healthcheck } from "../src/koa-middleware/healthcheck"; +import { helloWorld } from "../src/koa-middleware/helloWorld"; const env = parseEnv( z.intersection( frontendEnvSchema, z.object({ DATABASE_URL: z.string(), + SENTRY_DSN: z.string().optional(), }) ) ); @@ -26,30 +28,15 @@ const database = postgres(env.DATABASE_URL, { prepare: false }); const server = new Koa(); -if (process.env.SENTRY_DSN) { - registerSentryMiddlewares(server); +if (env.SENTRY_DSN) { + server.use(sentry(env.SENTRY_DSN)); } server.use(cors()); +server.use(healthcheck()); +server.use(helloWorld()); server.use(apiRoutes(database)); -const router = new Router(); - -router.get("/", (ctx) => { - ctx.body = "emit HelloWorld();"; -}); - -// k8s healthchecks -router.get("/healthz", (ctx) => { - ctx.status = 200; -}); -router.get("/readyz", (ctx) => { - ctx.status = 200; -}); - -server.use(router.routes()); -server.use(router.allowedMethods()); - server.use( createKoaMiddleware({ prefix: "/trpc", diff --git a/packages/store-indexer/bin/postgres-indexer.ts b/packages/store-indexer/bin/postgres-indexer.ts index e62d3813c3..15cce32a1a 100644 --- a/packages/store-indexer/bin/postgres-indexer.ts +++ b/packages/store-indexer/bin/postgres-indexer.ts @@ -93,33 +93,18 @@ combineLatest([latestBlockNumber$, storedBlockLogs$]) if (env.HEALTHCHECK_HOST != null || env.HEALTHCHECK_PORT != null) { const { default: Koa } = await import("koa"); const { default: cors } = await import("@koa/cors"); - const { default: Router } = await import("@koa/router"); + const { healthcheck } = await import("../src/koa-middleware/healthcheck"); + const { helloWorld } = await import("../src/koa-middleware/helloWorld"); const server = new Koa(); - server.use(cors()); - - const router = new Router(); - - router.get("/", (ctx) => { - ctx.body = "emit HelloWorld();"; - }); - - // k8s healthchecks - router.get("/healthz", (ctx) => { - ctx.status = 200; - }); - router.get("/readyz", (ctx) => { - if (isCaughtUp) { - ctx.status = 200; - ctx.body = "ready"; - } else { - ctx.status = 424; - ctx.body = "backfilling"; - } - }); - server.use(router.routes()); - server.use(router.allowedMethods()); + server.use(cors()); + server.use( + healthcheck({ + isReady: () => isCaughtUp, + }) + ); + server.use(helloWorld()); server.listen({ host: env.HEALTHCHECK_HOST, port: env.HEALTHCHECK_PORT }); console.log( diff --git a/packages/store-indexer/bin/sqlite-indexer.ts b/packages/store-indexer/bin/sqlite-indexer.ts index 3e04f7d9ea..bfea5260bb 100644 --- a/packages/store-indexer/bin/sqlite-indexer.ts +++ b/packages/store-indexer/bin/sqlite-indexer.ts @@ -8,7 +8,6 @@ import Database from "better-sqlite3"; import { createPublicClient, fallback, webSocket, http, Transport } from "viem"; import Koa from "koa"; import cors from "@koa/cors"; -import Router from "@koa/router"; import { createKoaMiddleware } from "trpc-koa-adapter"; import { createAppRouter } from "@latticexyz/store-sync/trpc-indexer"; import { chainState, schemaVersion, syncToSqlite } from "@latticexyz/store-sync/sqlite"; @@ -16,8 +15,10 @@ import { createQueryAdapter } from "../src/sqlite/createQueryAdapter"; import { isDefined } from "@latticexyz/common/utils"; import { combineLatest, filter, first } from "rxjs"; import { frontendEnvSchema, indexerEnvSchema, parseEnv } from "./parseEnv"; +import { healthcheck } from "../src/koa-middleware/healthcheck"; +import { helloWorld } from "../src/koa-middleware/helloWorld"; import { apiRoutes } from "../src/sqlite/apiRoutes"; -import { registerSentryMiddlewares } from "../src/sentry"; +import { sentry } from "../src/koa-middleware/sentry"; const env = parseEnv( z.intersection( @@ -93,35 +94,19 @@ combineLatest([latestBlockNumber$, storedBlockLogs$]) }); const server = new Koa(); -server.use(cors()); -server.use(apiRoutes(database)); if (env.SENTRY_DSN) { - registerSentryMiddlewares(server); + server.use(sentry(env.SENTRY_DSN)); } -const router = new Router(); - -router.get("/", (ctx) => { - ctx.body = "emit HelloWorld();"; -}); - -// k8s healthchecks -router.get("/healthz", (ctx) => { - ctx.status = 200; -}); -router.get("/readyz", (ctx) => { - if (isCaughtUp) { - ctx.status = 200; - ctx.body = "ready"; - } else { - ctx.status = 424; - ctx.body = "backfilling"; - } -}); - -server.use(router.routes()); -server.use(router.allowedMethods()); +server.use(cors()); +server.use( + healthcheck({ + isReady: () => isCaughtUp, + }) +); +server.use(helloWorld()); +server.use(apiRoutes(database)); server.use( createKoaMiddleware({ diff --git a/packages/store-indexer/package.json b/packages/store-indexer/package.json index 112a5739b6..fa70a07b35 100644 --- a/packages/store-indexer/package.json +++ b/packages/store-indexer/package.json @@ -68,7 +68,6 @@ "devDependencies": { "@types/accepts": "^1.3.7", "@types/better-sqlite3": "^7.6.4", - "@types/cors": "^2.8.13", "@types/debug": "^4.1.7", "@types/koa": "^2.13.12", "@types/koa-compose": "^3.2.8", diff --git a/packages/store-indexer/src/compress.ts b/packages/store-indexer/src/koa-middleware/compress.ts similarity index 100% rename from packages/store-indexer/src/compress.ts rename to packages/store-indexer/src/koa-middleware/compress.ts diff --git a/packages/store-indexer/src/koa-middleware/healthcheck.ts b/packages/store-indexer/src/koa-middleware/healthcheck.ts new file mode 100644 index 0000000000..885b5f6b5d --- /dev/null +++ b/packages/store-indexer/src/koa-middleware/healthcheck.ts @@ -0,0 +1,37 @@ +import { Middleware } from "koa"; + +type HealthcheckOptions = { + isHealthy?: () => boolean; + isReady?: () => boolean; +}; + +/** + * Middleware to add Kubernetes healthcheck endpoints + */ +export function healthcheck({ isHealthy, isReady }: HealthcheckOptions = {}): Middleware { + return async function healthcheckMiddleware(ctx, next): Promise { + if (ctx.path === "/healthz") { + if (isHealthy == null || isHealthy()) { + ctx.status = 200; + ctx.body = "healthy"; + } else { + ctx.status = 503; + ctx.body = "not healthy"; + } + return; + } + + if (ctx.path === "/readyz") { + if (isReady == null || isReady()) { + ctx.status = 200; + ctx.body = "ready"; + } else { + ctx.status = 503; + ctx.body = "not ready"; + } + return; + } + + await next(); + }; +} diff --git a/packages/store-indexer/src/koa-middleware/helloWorld.ts b/packages/store-indexer/src/koa-middleware/helloWorld.ts new file mode 100644 index 0000000000..10a081aee6 --- /dev/null +++ b/packages/store-indexer/src/koa-middleware/helloWorld.ts @@ -0,0 +1,12 @@ +import { Middleware } from "koa"; + +export function helloWorld(): Middleware { + return async function helloWorldMiddleware(ctx, next): Promise { + if (ctx.path === "/") { + ctx.status = 200; + ctx.body = "emit HelloWorld();"; + return; + } + await next(); + }; +} diff --git a/packages/store-indexer/src/koa-middleware/sentry.ts b/packages/store-indexer/src/koa-middleware/sentry.ts new file mode 100644 index 0000000000..ea59ea7492 --- /dev/null +++ b/packages/store-indexer/src/koa-middleware/sentry.ts @@ -0,0 +1,101 @@ +import * as Sentry from "@sentry/node"; +import { ProfilingIntegration } from "@sentry/profiling-node"; +import { stripUrlQueryAndFragment } from "@sentry/utils"; +import debug from "debug"; +import Koa from "koa"; +import compose from "koa-compose"; + +export function errorHandler(): Koa.Middleware { + return async function errorHandlerMiddleware(ctx, next) { + try { + await next(); + } catch (err) { + Sentry.withScope((scope) => { + scope.addEventProcessor((event) => { + return Sentry.addRequestDataToEvent(event, ctx.request); + }); + Sentry.captureException(err); + }); + throw err; + } + }; +} + +export function requestHandler(): Koa.Middleware { + return async function requestHandlerMiddleware(ctx, next) { + await Sentry.runWithAsyncContext(async () => { + const hub = Sentry.getCurrentHub(); + hub.configureScope((scope) => + scope.addEventProcessor((event) => + Sentry.addRequestDataToEvent(event, ctx.request, { + include: { + user: false, + }, + }) + ) + ); + await next(); + }); + }; +} + +export function tracing(): Koa.Middleware { + // creates a Sentry transaction per request + return async function tracingMiddleware(ctx, next) { + const reqMethod = (ctx.method || "").toUpperCase(); + const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url); + + // Connect to trace of upstream app + let traceparentData; + if (ctx.request.get("sentry-trace")) { + traceparentData = Sentry.extractTraceparentData(ctx.request.get("sentry-trace")); + } + + const transaction = Sentry.startTransaction({ + name: `${reqMethod} ${reqUrl}`, + op: "http.server", + ...traceparentData, + }); + + ctx.__sentry_transaction = transaction; + + // We put the transaction on the scope so users can attach children to it + Sentry.getCurrentHub().configureScope((scope) => { + scope.setSpan(transaction); + }); + + ctx.res.on("finish", () => { + // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction closes + setImmediate(() => { + // If you're using koa router, set the matched route as transaction name + if (ctx._matchedRoute) { + const mountPath = ctx.mountPath || ""; + transaction.setName(`${reqMethod} ${mountPath}${ctx._matchedRoute}`); + } + + transaction.setHttpStatus(ctx.status); + transaction.finish(); + }); + }); + + await next(); + }; +} + +export function sentry(dsn: string): Koa.Middleware { + debug("Initializing Sentry"); + Sentry.init({ + dsn, + integrations: [ + // Automatically instrument Node.js libraries and frameworks + ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + new ProfilingIntegration(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, + // Set sampling rate for profiling - this is relative to tracesSampleRate + profilesSampleRate: 1.0, + }); + + return compose([errorHandler(), requestHandler(), tracing()]); +} diff --git a/packages/store-indexer/src/postgres/apiRoutes.ts b/packages/store-indexer/src/postgres/apiRoutes.ts index 8f71014cbc..383700fc5f 100644 --- a/packages/store-indexer/src/postgres/apiRoutes.ts +++ b/packages/store-indexer/src/postgres/apiRoutes.ts @@ -8,7 +8,7 @@ import { queryLogs } from "./queryLogs"; import { recordToLog } from "./recordToLog"; import { debug, error } from "../debug"; import { createBenchmark } from "@latticexyz/common"; -import { compress } from "../compress"; +import { compress } from "../koa-middleware/compress"; export function apiRoutes(database: Sql): Middleware { const router = new Router(); diff --git a/packages/store-indexer/src/postgres/queryLogs.ts b/packages/store-indexer/src/postgres/queryLogs.ts index eee4ba8096..b887a7a026 100644 --- a/packages/store-indexer/src/postgres/queryLogs.ts +++ b/packages/store-indexer/src/postgres/queryLogs.ts @@ -39,7 +39,6 @@ export function queryLogs(sql: Sql, opts: z.infer): PendingQuery hex columns via custom types: https://github.com/porsager/postgres#custom-types - // TODO: sort by logIndex (https://github.com/latticexyz/mud/issues/1979) return sql` WITH config AS ( diff --git a/packages/store-indexer/src/sentry.ts b/packages/store-indexer/src/sentry.ts deleted file mode 100644 index 09c477b12c..0000000000 --- a/packages/store-indexer/src/sentry.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as Sentry from "@sentry/node"; -import { ProfilingIntegration } from "@sentry/profiling-node"; -import { stripUrlQueryAndFragment } from "@sentry/utils"; -import { debug } from "./debug"; -import Koa from "koa"; - -Sentry.init({ - dsn: process.env.SENTRY_DSN, - integrations: [ - // Automatically instrument Node.js libraries and frameworks - ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), - new ProfilingIntegration(), - ], - // Performance Monitoring - tracesSampleRate: 1.0, - // Set sampling rate for profiling - this is relative to tracesSampleRate - profilesSampleRate: 1.0, -}); - -const requestHandler: Koa.Middleware = (ctx, next) => { - return new Promise((resolve, reject) => { - Sentry.runWithAsyncContext(async () => { - const hub = Sentry.getCurrentHub(); - hub.configureScope((scope) => - scope.addEventProcessor((event) => - Sentry.addRequestDataToEvent(event, ctx.request, { - include: { - user: false, - }, - }) - ) - ); - - try { - await next(); - } catch (err) { - reject(err); - } - resolve(); - }); - }); -}; - -// This tracing middleware creates a transaction per request -const tracingMiddleWare: Koa.Middleware = async (ctx, next) => { - const reqMethod = (ctx.method || "").toUpperCase(); - const reqUrl = ctx.url && stripUrlQueryAndFragment(ctx.url); - - // Connect to trace of upstream app - let traceparentData; - if (ctx.request.get("sentry-trace")) { - traceparentData = Sentry.extractTraceparentData(ctx.request.get("sentry-trace")); - } - - const transaction = Sentry.startTransaction({ - name: `${reqMethod} ${reqUrl}`, - op: "http.server", - ...traceparentData, - }); - - ctx.__sentry_transaction = transaction; - - // We put the transaction on the scope so users can attach children to it - Sentry.getCurrentHub().configureScope((scope) => { - scope.setSpan(transaction); - }); - - ctx.res.on("finish", () => { - // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction closes - setImmediate(() => { - // If you're using koa router, set the matched route as transaction name - if (ctx._matchedRoute) { - const mountPath = ctx.mountPath || ""; - transaction.setName(`${reqMethod} ${mountPath}${ctx._matchedRoute}`); - } - - transaction.setHttpStatus(ctx.status); - transaction.finish(); - }); - }); - - await next(); -}; - -const errorHandler: Koa.Middleware = async (ctx, next) => { - try { - await next(); - } catch (err) { - Sentry.withScope((scope) => { - scope.addEventProcessor((event) => { - return Sentry.addRequestDataToEvent(event, ctx.request); - }); - Sentry.captureException(err); - }); - throw err; - } -}; - -export const registerSentryMiddlewares = (server: Koa): void => { - debug("Registering Sentry middlewares"); - - server.use(errorHandler); - server.use(requestHandler); - server.use(tracingMiddleWare); -}; diff --git a/packages/store-indexer/src/sqlite/apiRoutes.ts b/packages/store-indexer/src/sqlite/apiRoutes.ts index c83e87ef0e..2ad7962a11 100644 --- a/packages/store-indexer/src/sqlite/apiRoutes.ts +++ b/packages/store-indexer/src/sqlite/apiRoutes.ts @@ -5,7 +5,7 @@ import { input } from "@latticexyz/store-sync/indexer-client"; import { storeTables, tablesWithRecordsToLogs } from "@latticexyz/store-sync"; import { debug } from "../debug"; import { createBenchmark } from "@latticexyz/common"; -import { compress } from "../compress"; +import { compress } from "../koa-middleware/compress"; import { getTablesWithRecords } from "./getTablesWithRecords"; import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d641c39ae..f799c72a2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -909,9 +909,6 @@ importers: '@types/better-sqlite3': specifier: ^7.6.4 version: 7.6.4 - '@types/cors': - specifier: ^2.8.13 - version: 2.8.13 '@types/debug': specifier: ^4.1.7 version: 4.1.7 @@ -3447,12 +3444,6 @@ packages: '@types/node': 18.15.11 dev: true - /@types/cors@2.8.13: - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} - dependencies: - '@types/node': 18.15.11 - dev: true - /@types/debug@4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: