From b60ece578304df21602d39c7022a7a4dbc6437e0 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 6 Jul 2025 18:07:56 +0000 Subject: feat: Add prometheus monitoring. Fixes #758 --- docs/docs/03-configuration.md | 36 ++++---- packages/api/index.ts | 5 +- packages/api/middlewares/prometheusAuth.ts | 33 +++++++ packages/api/package.json | 1 + packages/api/routes/metrics.ts | 16 ++++ packages/shared/config.ts | 6 ++ packages/trpc/index.ts | 42 +++++++-- packages/trpc/package.json | 1 + packages/trpc/stats.ts | 142 +++++++++++++++++++++++++++++ pnpm-lock.yaml | 104 +++++++++++++++------ 10 files changed, 331 insertions(+), 55 deletions(-) create mode 100644 packages/api/middlewares/prometheusAuth.ts create mode 100644 packages/api/routes/metrics.ts create mode 100644 packages/trpc/stats.ts diff --git a/docs/docs/03-configuration.md b/docs/docs/03-configuration.md index d8981843..c7b533ff 100644 --- a/docs/docs/03-configuration.md +++ b/docs/docs/03-configuration.md @@ -2,30 +2,30 @@ The app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are: -| Name | Required | Default | Description | -| ------------------------- | ------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| DATA_DIR | Yes | Not set | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set. | -| ASSETS_DIR | No | Not set | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`. | -| NEXTAUTH_URL | Yes | Not set | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example. | -| NEXTAUTH_SECRET | Yes | Not set | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. | -| MEILI_ADDR | No | Not set | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`) | -| MEILI_MASTER_KEY | Only in Prod and if search is enabled | Not set | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \| tr -dc 'A-Za-z0-9'` | -| MAX_ASSET_SIZE_MB | No | 50 | Sets the maximum allowed asset size (in MB) to be uploaded | -| DISABLE_NEW_RELEASE_CHECK | No | false | If set to true, latest release check will be disabled in the admin panel. | +| Name | Required | Default | Description | +| ------------------------- | ------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DATA_DIR | Yes | Not set | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set. | +| ASSETS_DIR | No | Not set | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`. | +| NEXTAUTH_URL | Yes | Not set | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example. | +| NEXTAUTH_SECRET | Yes | Not set | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. | +| MEILI_ADDR | No | Not set | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`) | +| MEILI_MASTER_KEY | Only in Prod and if search is enabled | Not set | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \| tr -dc 'A-Za-z0-9'` | +| MAX_ASSET_SIZE_MB | No | 50 | Sets the maximum allowed asset size (in MB) to be uploaded | +| DISABLE_NEW_RELEASE_CHECK | No | false | If set to true, latest release check will be disabled in the admin panel. | +| PROMETHEUS_AUTH_TOKEN | No | Not set | If set, will enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, that endpoint will return 404. | ## Asset Storage Karakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed. -| Name | Required | Default | Description | -| -------------------------------- | ----------------- | --------- | --------------------------------------------------------------------------------------------------------- | -| ASSET_STORE_S3_ENDPOINT | No | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. | +| Name | Required | Default | Description | +| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- | +| ASSET_STORE_S3_ENDPOINT | No | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. | | ASSET_STORE_S3_REGION | No | Not set | The S3 region to use. | -| ASSET_STORE_S3_BUCKET | Yes when using S3 | Not set | The S3 bucket name where assets will be stored. | -| ASSET_STORE_S3_ACCESS_KEY_ID | Yes when using S3 | Not set | The S3 access key ID for authentication. | -| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication. | -| ASSET_STORE_S3_FORCE_PATH_STYLE | No | false | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. | - +| ASSET_STORE_S3_BUCKET | Yes when using S3 | Not set | The S3 bucket name where assets will be stored. | +| ASSET_STORE_S3_ACCESS_KEY_ID | Yes when using S3 | Not set | The S3 access key ID for authentication. | +| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication. | +| ASSET_STORE_S3_FORCE_PATH_STYLE | No | false | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. | :::info When using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket. diff --git a/packages/api/index.ts b/packages/api/index.ts index 2eb22d8f..4103e033 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -10,6 +10,7 @@ import bookmarks from "./routes/bookmarks"; import health from "./routes/health"; import highlights from "./routes/highlights"; import lists from "./routes/lists"; +import metrics, { registerMetrics } from "./routes/metrics"; import publicRoute from "./routes/public"; import rss from "./routes/rss"; import tags from "./routes/tags"; @@ -37,6 +38,7 @@ const app = new Hono<{ }>() .use(logger()) .use(poweredBy()) + .use("*", registerMetrics) .use(async (c, next) => { // Ensure that the ctx is set if (!c.var.ctx) { @@ -49,6 +51,7 @@ const app = new Hono<{ .route("/trpc", trpc) .route("/v1", v1) .route("/assets", assets) - .route("/public", publicRoute); + .route("/public", publicRoute) + .route("/metrics", metrics); export default app; diff --git a/packages/api/middlewares/prometheusAuth.ts b/packages/api/middlewares/prometheusAuth.ts new file mode 100644 index 00000000..bf35608f --- /dev/null +++ b/packages/api/middlewares/prometheusAuth.ts @@ -0,0 +1,33 @@ +import { createMiddleware } from "hono/factory"; +import { HTTPException } from "hono/http-exception"; + +import serverConfig from "@karakeep/shared/config"; + +export const prometheusAuthMiddleware = createMiddleware(async (c, next) => { + const { metricsToken } = serverConfig.prometheus; + + // If no token is configured, deny access (safe default) + if (!metricsToken) { + throw new HTTPException(404, { + message: "Not Found", + }); + } + + const auth = c.req.header("Authorization"); + + if (!auth || !auth.startsWith("Bearer ")) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + const token = auth.slice(7); // Remove "Bearer " prefix + + if (token !== metricsToken) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + await next(); +}); diff --git a/packages/api/package.json b/packages/api/package.json index 54656e64..18d70501 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -13,6 +13,7 @@ "test": "vitest" }, "dependencies": { + "@hono/prometheus": "^1.0.2", "@hono/trpc-server": "^0.4.0", "@hono/zod-validator": "^0.5.0", "@karakeep/db": "workspace:*", diff --git a/packages/api/routes/metrics.ts b/packages/api/routes/metrics.ts new file mode 100644 index 00000000..90eff5b9 --- /dev/null +++ b/packages/api/routes/metrics.ts @@ -0,0 +1,16 @@ +// Import stats to register Prometheus metrics +import "@karakeep/trpc/stats"; + +import { prometheus } from "@hono/prometheus"; +import { Hono } from "hono"; +import { register } from "prom-client"; + +import { prometheusAuthMiddleware } from "../middlewares/prometheusAuth"; + +export const { printMetrics, registerMetrics } = prometheus({ + registry: register, +}); + +const app = new Hono().get("/", prometheusAuthMiddleware, printMetrics); + +export default app; diff --git a/packages/shared/config.ts b/packages/shared/config.ts index 9294e154..c435d012 100644 --- a/packages/shared/config.ts +++ b/packages/shared/config.ts @@ -94,6 +94,9 @@ const allEnv = z.object({ // A flag to detect if the user is running in the old separete containers setup USING_LEGACY_SEPARATE_CONTAINERS: stringBool("false"), + // Prometheus metrics configuration + PROMETHEUS_AUTH_TOKEN: z.string().optional(), + // Asset storage configuration ASSET_STORE_S3_ENDPOINT: z.string().optional(), ASSET_STORE_S3_REGION: z.string().optional(), @@ -222,6 +225,9 @@ const serverConfigSchema = allEnv.transform((val) => { forcePathStyle: val.ASSET_STORE_S3_FORCE_PATH_STYLE, }, }, + prometheus: { + metricsToken: val.PROMETHEUS_AUTH_TOKEN, + }, }; }); diff --git a/packages/trpc/index.ts b/packages/trpc/index.ts index e34e56eb..90f37ae4 100644 --- a/packages/trpc/index.ts +++ b/packages/trpc/index.ts @@ -5,6 +5,12 @@ import { ZodError } from "zod"; import type { db } from "@karakeep/db"; import serverConfig from "@karakeep/shared/config"; +import { + apiErrorsTotalCounter, + apiRequestDurationSummary, + apiRequestsTotalCounter, +} from "./stats"; + interface User { id: string; name?: string | null | undefined; @@ -36,6 +42,11 @@ const t = initTRPC.context().create({ transformer: superjson, errorFormatter(opts) { const { shape, error } = opts; + apiErrorsTotalCounter.inc({ + type: opts.type, + path: opts.path, + code: error.code, + }); return { ...shape, data: { @@ -51,15 +62,30 @@ const t = initTRPC.context().create({ export const createCallerFactory = t.createCallerFactory; // Base router and procedure helpers export const router = t.router; -export const procedure = t.procedure.use(function isDemoMode(opts) { - if (serverConfig.demoMode && opts.type == "mutation") { - throw new TRPCError({ - message: "Mutations are not allowed in demo mode", - code: "FORBIDDEN", +export const procedure = t.procedure + .use(function isDemoMode(opts) { + if (serverConfig.demoMode && opts.type == "mutation") { + throw new TRPCError({ + message: "Mutations are not allowed in demo mode", + code: "FORBIDDEN", + }); + } + return opts.next(); + }) + .use(async (opts) => { + const end = apiRequestDurationSummary.startTimer({ + path: opts.path, + type: opts.type, }); - } - return opts.next(); -}); + const res = await opts.next(); + apiRequestsTotalCounter.inc({ + type: opts.type, + path: opts.path, + is_error: res.ok ? 0 : 1, + }); + end(); + return res; + }); export const publicProcedure = procedure; export const authedProcedure = procedure.use(function isAuthed(opts) { diff --git a/packages/trpc/package.json b/packages/trpc/package.json index b0280d6d..f4a9d122 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -19,6 +19,7 @@ "bcryptjs": "^2.4.3", "deep-equal": "^2.2.3", "drizzle-orm": "^0.38.3", + "prom-client": "^15.1.3", "superjson": "^2.2.1", "tiny-invariant": "^1.3.3", "zod": "^3.24.2" diff --git a/packages/trpc/stats.ts b/packages/trpc/stats.ts new file mode 100644 index 00000000..465bddcd --- /dev/null +++ b/packages/trpc/stats.ts @@ -0,0 +1,142 @@ +import { count, sum } from "drizzle-orm"; +import { Counter, Gauge, register, Summary } from "prom-client"; + +import { db } from "@karakeep/db"; +import { assets, bookmarks, users } from "@karakeep/db/schema"; +import { + AssetPreprocessingQueue, + FeedQueue, + LinkCrawlerQueue, + OpenAIQueue, + RuleEngineQueue, + SearchIndexingQueue, + TidyAssetsQueue, + VideoWorkerQueue, + WebhookQueue, +} from "@karakeep/shared/queues"; + +// Queue metrics +const queuePendingJobsGauge = new Gauge({ + name: "queue_jobs", + help: "Number of jobs in each background queue", + labelNames: ["queue_name", "status"], + async collect() { + const queues = [ + { name: "link_crawler", queue: LinkCrawlerQueue }, + { name: "openai", queue: OpenAIQueue }, + { name: "search_indexing", queue: SearchIndexingQueue }, + { name: "tidy_assets", queue: TidyAssetsQueue }, + { name: "video_worker", queue: VideoWorkerQueue }, + { name: "feed", queue: FeedQueue }, + { name: "asset_preprocessing", queue: AssetPreprocessingQueue }, + { name: "webhook", queue: WebhookQueue }, + { name: "rule_engine", queue: RuleEngineQueue }, + ]; + + const stats = await Promise.all( + queues.map(async ({ name, queue }) => { + try { + return { + ...(await queue.stats()), + name, + }; + } catch (error) { + console.error(`Failed to get stats for queue ${name}:`, error); + return { name, pending: 0, pending_retry: 0, failed: 0, running: 0 }; + } + }), + ); + + stats.forEach(({ name, pending, pending_retry, failed, running }) => { + this.set({ queue_name: name, status: "pending" }, pending); + this.set({ queue_name: name, status: "pending_retry" }, pending_retry); + this.set({ queue_name: name, status: "failed" }, failed); + this.set({ queue_name: name, status: "running" }, running); + }); + }, +}); + +// User metrics +const totalUsersGauge = new Gauge({ + name: "total_users", + help: "Total number of users in the system", + async collect() { + try { + const result = await db.select({ count: count() }).from(users); + this.set(result[0]?.count ?? 0); + } catch (error) { + console.error("Failed to get user count:", error); + this.set(0); + } + }, +}); + +// Asset metrics +const totalAssetSizeGauge = new Gauge({ + name: "total_asset_size_bytes", + help: "Total size of all assets in bytes", + async collect() { + try { + const result = await db + .select({ totalSize: sum(assets.size) }) + .from(assets); + this.set(Number(result[0]?.totalSize ?? 0)); + } catch (error) { + console.error("Failed to get total asset size:", error); + this.set(0); + } + }, +}); + +// Bookmark metrics +const totalBookmarksGauge = new Gauge({ + name: "total_bookmarks", + help: "Total number of bookmarks in the system", + async collect() { + try { + const result = await db.select({ count: count() }).from(bookmarks); + this.set(result[0]?.count ?? 0); + } catch (error) { + console.error("Failed to get bookmark count:", error); + this.set(0); + } + }, +}); + +// Api metrics +const apiRequestsTotalCounter = new Counter({ + name: "trpc_requests_total", + help: "Total number of API requests", + labelNames: ["type", "path", "is_error"], +}); + +const apiErrorsTotalCounter = new Counter({ + name: "trpc_errors_total", + help: "Total number of API requests", + labelNames: ["type", "path", "code"], +}); + +const apiRequestDurationSummary = new Summary({ + name: "trpc_request_duration_seconds", + help: "Duration of tRPC requests in seconds", + labelNames: ["type", "path"], +}); + +// Register all metrics +register.registerMetric(queuePendingJobsGauge); +register.registerMetric(totalUsersGauge); +register.registerMetric(totalAssetSizeGauge); +register.registerMetric(totalBookmarksGauge); +register.registerMetric(apiRequestsTotalCounter); +register.registerMetric(apiErrorsTotalCounter); +register.registerMetric(apiRequestDurationSummary); + +export { + queuePendingJobsGauge, + totalUsersGauge, + totalAssetSizeGauge, + totalBookmarksGauge, + apiRequestsTotalCounter, + apiErrorsTotalCounter, + apiRequestDurationSummary, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cafcae03..d9fc238d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: version: 11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2) '@trpc/next': specifier: 11.0.0 - version: 11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@14.2.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2) + version: 11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@14.2.25(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2) '@trpc/react-query': specifier: 11.0.0 version: 11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2) @@ -604,7 +604,7 @@ importers: version: 1.11.10 drizzle-orm: specifier: ^0.38.3 - version: 0.38.3(@types/better-sqlite3@7.6.13)(@types/react@18.3.12)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@18.3.12)(better-sqlite3@11.3.0)(react@18.3.1) fastest-levenshtein: specifier: ^1.0.16 version: 1.0.16 @@ -622,22 +622,22 @@ importers: version: 0.501.0(react@18.3.1) next: specifier: 14.2.25 - version: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + version: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.11(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-i18next: specifier: ^15.3.1 - version: 15.3.1(i18next@23.16.5)(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.3.1(i18next@23.16.5)(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) next-pwa: specifier: ^5.6.0 - version: 5.6.0(@babel/core@7.26.0)(@types/babel__core@7.20.5)(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(webpack@5.99.9) + version: 5.6.0(@babel/core@7.26.0)(@types/babel__core@7.20.5)(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(webpack@5.99.9) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1) + version: 2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1) prettier: specifier: ^3.4.2 version: 3.4.2 @@ -782,7 +782,7 @@ importers: version: 16.4.5 drizzle-orm: specifier: ^0.38.3 - version: 0.38.3(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) execa: specifier: 9.3.1 version: 9.3.1 @@ -791,7 +791,7 @@ importers: version: 24.1.3 liteque: specifier: ^0.3.2 - version: 0.3.2(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.3.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) metascraper: specifier: ^5.46.18 version: 5.47.1 @@ -929,6 +929,9 @@ importers: packages/api: dependencies: + '@hono/prometheus': + specifier: ^1.0.2 + version: 1.0.2(hono@4.7.11)(prom-client@15.1.3) '@hono/trpc-server': specifier: ^0.4.0 version: 0.4.0(@trpc/server@11.0.0(typescript@5.8.2))(hono@4.7.11) @@ -995,7 +998,7 @@ importers: version: 16.4.5 drizzle-orm: specifier: ^0.38.3 - version: 0.38.3(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) tsx: specifier: ^4.7.1 version: 4.7.1 @@ -1110,7 +1113,7 @@ importers: version: 1.0.20 liteque: specifier: ^0.3.2 - version: 0.3.2(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.3.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) meilisearch: specifier: ^0.37.0 version: 0.37.0(encoding@0.1.13) @@ -1193,7 +1196,10 @@ importers: version: 2.2.3 drizzle-orm: specifier: ^0.38.3 - version: 0.38.3(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + version: 0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + prom-client: + specifier: ^15.1.3 + version: 15.1.3 superjson: specifier: ^2.2.1 version: 2.2.1 @@ -3294,6 +3300,12 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/prometheus@1.0.2': + resolution: {integrity: sha512-7z2nBMaiHEaAFfNWfIV2H5/HRezv9kLH0jDY6ZotQQAr3QR7cIYAd6FGiyTIng4GUAw6ZWeX3C0Y4QS36SLRjg==} + peerDependencies: + hono: '>=3.*' + prom-client: ^15.0.0 + '@hono/trpc-server@0.4.0': resolution: {integrity: sha512-LGlJfCmNIGMwcknZEIYdujVMs9OkNVazhpOhaz3kTWOXvNL660VOHpvvktosCiJrajyBY1RtIJKQ+IKaQvNuSg==} engines: {node: '>=16.0.0'} @@ -3809,6 +3821,10 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oxlint/darwin-arm64@1.2.0': resolution: {integrity: sha512-DsdZPp59sPPmuI6pR6MP1QepWWkpibFhVmRXa7ZOUobxxubUBg12SVCchAI1Iq8jejcAg9/XHXsRFpuny2LawQ==} cpu: [arm64] @@ -6092,6 +6108,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -11711,6 +11730,10 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} @@ -13246,6 +13269,9 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -18381,6 +18407,11 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/prometheus@1.0.2(hono@4.7.11)(prom-client@15.1.3)': + dependencies: + hono: 4.7.11 + prom-client: 15.1.3 + '@hono/trpc-server@0.4.0(@trpc/server@11.0.0(typescript@5.8.2))(hono@4.7.11)': dependencies: '@trpc/server': 11.0.0(typescript@5.8.2) @@ -19030,6 +19061,8 @@ snapshots: dependencies: semver: 7.7.2 + '@opentelemetry/api@1.9.0': {} + '@oxlint/darwin-arm64@1.2.0': optional: true @@ -20712,11 +20745,11 @@ snapshots: '@trpc/server': 11.0.0(typescript@5.8.2) typescript: 5.8.2 - '@trpc/next@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@14.2.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)': + '@trpc/next@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@18.3.1))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@14.2.25(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)': dependencies: '@trpc/client': 11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2) '@trpc/server': 11.0.0(typescript@5.8.2) - next: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) typescript: 5.8.2 @@ -21790,6 +21823,8 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + bintrees@1.0.2: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -23056,22 +23091,25 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.33.0(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): + drizzle-orm@0.33.0(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/react': 19.1.6 better-sqlite3: 11.3.0 react: 18.3.1 - drizzle-orm@0.38.3(@types/better-sqlite3@7.6.13)(@types/react@18.3.12)(better-sqlite3@11.3.0)(react@18.3.1): + drizzle-orm@0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@18.3.12)(better-sqlite3@11.3.0)(react@18.3.1): optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/react': 18.3.12 better-sqlite3: 11.3.0 react: 18.3.1 - drizzle-orm@0.38.3(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): + drizzle-orm@0.38.3(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/react': 19.1.6 better-sqlite3: 11.3.0 @@ -25714,11 +25752,11 @@ snapshots: liquid-json@0.3.1: {} - liteque@0.3.2(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): + liteque@0.3.2(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1): dependencies: async-mutex: 0.4.1 better-sqlite3: 11.3.0 - drizzle-orm: 0.33.0(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) + drizzle-orm: 0.33.0(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/react@19.1.6)(better-sqlite3@11.3.0)(react@18.3.1) zod: 3.24.2 transitivePeerDependencies: - '@aws-sdk/client-rds-data' @@ -27279,13 +27317,13 @@ snapshots: nested-error-stacks@2.0.1: {} - next-auth@4.24.11(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.11(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.27.6 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.11.3 @@ -27294,7 +27332,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) uuid: 8.3.2 - next-i18next@15.3.1(i18next@23.16.5)(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-i18next@15.3.1(i18next@23.16.5)(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.27.6 '@types/hoist-non-react-statics': 3.3.6 @@ -27302,16 +27340,16 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 23.16.5 i18next-fs-backend: 2.6.0 - next: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) react: 18.3.1 react-i18next: 15.1.1(i18next@23.16.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-pwa@5.6.0(@babel/core@7.26.0)(@types/babel__core@7.20.5)(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(webpack@5.99.9): + next-pwa@5.6.0(@babel/core@7.26.0)(@types/babel__core@7.20.5)(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(webpack@5.99.9): dependencies: babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.99.9) clean-webpack-plugin: 4.0.0(webpack@5.99.9) globby: 11.1.0 - next: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) terser-webpack-plugin: 5.3.14(webpack@5.99.9) workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.99.9) workbox-window: 6.6.0 @@ -27331,7 +27369,7 @@ snapshots: next-tick@1.1.0: {} - next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1): + next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1): dependencies: '@next/env': 14.2.25 '@swc/helpers': 0.5.5 @@ -27352,6 +27390,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.25 '@next/swc-win32-ia32-msvc': 14.2.25 '@next/swc-win32-x64-msvc': 14.2.25 + '@opentelemetry/api': 1.9.0 sass: 1.89.1 transitivePeerDependencies: - '@babel/core' @@ -27476,12 +27515,12 @@ snapshots: nullthrows@1.1.1: {} - nuqs@2.4.3(next@14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1): + nuqs@2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1): dependencies: mitt: 3.0.1 react: 18.3.1 optionalDependencies: - next: 14.2.25(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) + next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1) react-router: 6.22.1(react@18.3.1) react-router-dom: 6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -28582,6 +28621,11 @@ snapshots: progress@2.0.3: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.0 + tdigest: 0.1.2 + promise-retry@2.0.1: dependencies: err-code: 2.0.3 @@ -30623,6 +30667,10 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + temp-dir@2.0.0: {} temp@0.8.4: -- cgit v1.2.3-70-g09d2