diff --git a/Dockerfile b/Dockerfile index 54a293a..e60e4bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,12 @@ WORKDIR /src COPY --link package.json ./package.json COPY --link bun.lock ./bun.lock +COPY --link patches ./patches COPY --link prisma ./prisma COPY --link shared ./shared -RUN bun add prisma@6.16.1 -RUN bunx --bun prisma generate +RUN bunx --bun prisma@6.16.1 generate FROM oven/bun:${BUN_VERSION} AS base @@ -25,6 +25,7 @@ WORKDIR /src FROM base AS build COPY --link package.json bun.lock ./ +COPY --link patches ./patches RUN bun install --production COPY --from=prisma /src/shared/generated shared/generated diff --git a/bun.lock b/bun.lock index 183d277..ff63ab5 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "nuxt-app", "dependencies": { + "@artmizu/nuxt-prometheus": "2.6.0", "@nuxt/eslint": "1.9.0", "@nuxtjs/i18n": "^10.0.3", "@nuxtjs/tailwindcss": "^6.14.0", @@ -40,6 +41,9 @@ }, }, }, + "patchedDependencies": { + "@artmizu/nuxt-prometheus@2.6.0": "patches/@artmizu%2Fnuxt-prometheus@2.6.0.patch", + }, "packages": { "@adonisjs/hash": ["@adonisjs/hash@9.1.1", "", { "dependencies": { "@phc/format": "^1.0.0", "@poppinss/utils": "^6.9.3" }, "peerDependencies": { "argon2": "^0.31.2 || ^0.41.0 || ^0.43.0", "bcrypt": "^5.1.1 || ^6.0.0" }, "optionalPeers": ["argon2", "bcrypt"] }, "sha512-ZkRguwjAp4skKvKDdRAfdJ2oqQ0N7p9l3sioyXO1E8o0WcsyDgEpsTQtuVNoIdMiw4sn4gJlmL3nyF4BcK1ZDQ=="], @@ -51,6 +55,8 @@ "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@14.2.0", "", { "dependencies": { "js-yaml": "^4.1.0" }, "peerDependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-NaGMMWwppbByagq+LwQMq6PMXHFWVu6kSwwx+eJfYTJ5zdpOvb9TIk6ZWxEEeXMUvGdVOZq3JalYsjsTZDvtkA=="], + "@artmizu/nuxt-prometheus": ["@artmizu/nuxt-prometheus@2.6.0", "", { "dependencies": { "@mswjs/interceptors": "^0.36.10", "consola": "^3.2.3", "defu": "^6.1.2", "h3": "^1.7.1", "prom-client": "^14.2.0" } }, "sha512-ufS377w+fuhj1cay/2ZwOYdqxhLzyc3F97+SHMMn8TVl/rrtODN2X4VQtMbP967Hz8+qVUyxPMiCMU71zKMFbg=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="], @@ -265,6 +271,8 @@ "@miyaneee/rollup-plugin-json5": ["@miyaneee/rollup-plugin-json5@1.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "json5": "^2.2.3" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.36.10", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@netlify/binary-info": ["@netlify/binary-info@1.0.0", "", {}, "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw=="], @@ -317,6 +325,12 @@ "@nuxtjs/tailwindcss": ["@nuxtjs/tailwindcss@6.14.0", "", { "dependencies": { "@nuxt/kit": "^3.16.0", "autoprefixer": "^10.4.20", "c12": "^3.0.2", "consola": "^3.4.0", "defu": "^6.1.4", "h3": "^1.15.1", "klona": "^2.0.6", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.1.0", "postcss": "^8.5.3", "postcss-nesting": "^13.0.1", "tailwind-config-viewer": "^2.0.4", "tailwindcss": "~3.4.17", "ufo": "^1.5.4", "unctx": "^2.4.1" } }, "sha512-30RyDK++LrUVRgc2A85MktGWIZoRQgeQKjE4CjjD64OXNozyl+4ScHnnYgqVToMM6Ch2ZG2W4wV2J0EN6F0zkQ=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-tza8rCLefHlwksl+uVgdwHtmGKotOYSnJ5PPOdgATgOc+JqQPDbZqf68aTUU8y1VnrOR2bi7iYDFiY6RbkpKQA=="], "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dCSJnY7b4GwCmoWNksou3fNYXlhxibJvktFLvoq5eMmMTLoowrwcZT+WG2cIJV+RF33AREDac4QgZTDKI4LLmw=="], @@ -779,6 +793,8 @@ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bintrees": ["bintrees@1.0.2", "", {}, "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="], + "birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="], "block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="], @@ -1331,6 +1347,8 @@ "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], @@ -1603,6 +1621,8 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + "oxc-minify": ["oxc-minify@0.78.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.78.0", "@oxc-minify/binding-darwin-arm64": "0.78.0", "@oxc-minify/binding-darwin-x64": "0.78.0", "@oxc-minify/binding-freebsd-x64": "0.78.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.78.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.78.0", "@oxc-minify/binding-linux-arm64-gnu": "0.78.0", "@oxc-minify/binding-linux-arm64-musl": "0.78.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.78.0", "@oxc-minify/binding-linux-s390x-gnu": "0.78.0", "@oxc-minify/binding-linux-x64-gnu": "0.78.0", "@oxc-minify/binding-linux-x64-musl": "0.78.0", "@oxc-minify/binding-wasm32-wasi": "0.78.0", "@oxc-minify/binding-win32-arm64-msvc": "0.78.0", "@oxc-minify/binding-win32-x64-msvc": "0.78.0" } }, "sha512-QmoYJBPvzm+uqagkUaCRmyQL5LAzUYnz0r1JtMa6gi2sGyc5elDR8oE0F/1G1NjM3K3Kefwn4sdDFTZeRWc8sA=="], "oxc-parser": ["oxc-parser@0.72.3", "", { "dependencies": { "@oxc-project/types": "^0.72.3" }, "optionalDependencies": { "@oxc-parser/binding-darwin-arm64": "0.72.3", "@oxc-parser/binding-darwin-x64": "0.72.3", "@oxc-parser/binding-freebsd-x64": "0.72.3", "@oxc-parser/binding-linux-arm-gnueabihf": "0.72.3", "@oxc-parser/binding-linux-arm-musleabihf": "0.72.3", "@oxc-parser/binding-linux-arm64-gnu": "0.72.3", "@oxc-parser/binding-linux-arm64-musl": "0.72.3", "@oxc-parser/binding-linux-riscv64-gnu": "0.72.3", "@oxc-parser/binding-linux-s390x-gnu": "0.72.3", "@oxc-parser/binding-linux-x64-gnu": "0.72.3", "@oxc-parser/binding-linux-x64-musl": "0.72.3", "@oxc-parser/binding-wasm32-wasi": "0.72.3", "@oxc-parser/binding-win32-arm64-msvc": "0.72.3", "@oxc-parser/binding-win32-x64-msvc": "0.72.3" } }, "sha512-JYQeJKDcUTTZ/uTdJ+fZBGFjAjkLD1h0p3Tf44ZYXRcoMk+57d81paNPFAAwzrzzqhZmkGvKKXDxwyhJXYZlpg=="], @@ -1797,6 +1817,8 @@ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "prom-client": ["prom-client@14.2.0", "", { "dependencies": { "tdigest": "^0.1.1" } }, "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "protocols": ["protocols@2.0.2", "", {}, "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ=="], @@ -1971,6 +1993,8 @@ "streamx": ["streamx@2.22.1", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA=="], + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2019,6 +2043,8 @@ "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + "tdigest": ["tdigest@0.1.2", "", { "dependencies": { "bintrees": "1.0.2" } }, "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA=="], + "terser": ["terser@5.43.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg=="], "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], diff --git a/nuxt.config.ts b/nuxt.config.ts index fedcf03..89d886b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -14,8 +14,13 @@ export default defineNuxtConfig({ '@vuestic/nuxt', '@pinia/nuxt', '@vueuse/nuxt', - '@nuxt/eslint' + '@nuxt/eslint', + '@artmizu/nuxt-prometheus' ], + prometheus: { + prometheusPath: '/api/metrics', + verbose: false + }, runtimeConfig: { public: { s3Bucket: 'pichunt', diff --git a/package.json b/package.json index 7aaa3e9..86c719f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dev:down": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml down" }, "dependencies": { + "@artmizu/nuxt-prometheus": "2.6.0", "@nuxt/eslint": "1.9.0", "@nuxtjs/i18n": "^10.0.3", "@nuxtjs/tailwindcss": "^6.14.0", @@ -51,5 +52,8 @@ "prettier-plugin-tailwindcss": "^0.6.14", "prisma": "^6.16.1", "vue-tsc": "^3.0.6" + }, + "patchedDependencies": { + "@artmizu/nuxt-prometheus@2.6.0": "patches/@artmizu%2Fnuxt-prometheus@2.6.0.patch" } } diff --git a/patches/@artmizu%2Fnuxt-prometheus@2.6.0.patch b/patches/@artmizu%2Fnuxt-prometheus@2.6.0.patch new file mode 100644 index 0000000..407f1b7 --- /dev/null +++ b/patches/@artmizu%2Fnuxt-prometheus@2.6.0.patch @@ -0,0 +1,21 @@ +diff --git a/dist/runtime/registry.js b/dist/runtime/registry.js +index b06d20fce7900f947ad55f5b1b3f8e25cf9dbe84..b0cf94339671ed3042d6fc3f2c30d2d37723402a 100644 +--- a/dist/runtime/registry.js ++++ b/dist/runtime/registry.js +@@ -1,4 +1,4 @@ +-import { Gauge, Summary, collectDefaultMetrics, register } from "prom-client"; ++import { Gauge, Summary, register } from "prom-client"; + export const metrics = { + /** If `true`, metrics was initialized with `initMetrics` */ + isInitialized: false, +@@ -14,10 +14,6 @@ export const metrics = { + export const initMetrics = (p) => { + if (metrics.isInitialized) + return; +- collectDefaultMetrics({ +- prefix: p.prefix, +- register +- }); + const prefix = p.prefix ?? ""; + metrics.renderTime = new Gauge({ + name: `${prefix}page_render_time`, diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index f2815ed..7fa8e9b 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -1,6 +1,7 @@ import { verify } from 'argon2'; import { useValidatedBody, z } from 'h3-zod'; import prisma from '~~/lib/prisma'; +import { logins } from '~~/server/metrics'; export default defineEventHandler(async (event) => { const { email, password } = await useValidatedBody( @@ -25,10 +26,12 @@ export default defineEventHandler(async (event) => { }); if (!user || user.password.length <= 0) { console.warn(`Failed login attempted: Unknown email: ${email}`); + logins.inc({ success: 'false' }); throw createError({ status: 404, statusText: 'Not found!' }); } if (!(await verify(user.password, password))) { console.warn(`Failed login attempted: Wrong password: ${email}`); + logins.inc({ success: 'false' }); throw createError({ status: 404, statusText: 'Not found!' }); } @@ -41,6 +44,7 @@ export default defineEventHandler(async (event) => { }, loggedInAt: new Date() }); + logins.inc({ success: 'true' }); return { success: true, user: { id: user.id } }; }); diff --git a/server/api/quest/[questId]/submit.post.ts b/server/api/quest/[questId]/submit.post.ts index cebff88..6f8d44c 100644 --- a/server/api/quest/[questId]/submit.post.ts +++ b/server/api/quest/[questId]/submit.post.ts @@ -3,6 +3,11 @@ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import prisma from '~~/lib/prisma'; import { minioClient, s3Bucket, s3Host } from '~~/lib/s3'; +import { + answerChanged, + answerFinalized, + imageUploaded +} from '~~/server/metrics'; export default defineEventHandler(async (event) => { const { questId } = await useValidatedParams( @@ -21,6 +26,7 @@ export default defineEventHandler(async (event) => { }, select: { id: true, + huntId: true, hunt: { select: { teams: { @@ -125,6 +131,9 @@ export default defineEventHandler(async (event) => { undefined; if (data.image) { + imageUploaded.inc({ + hunt: quest.huntId + }); const uuid = randomUUID(); await minioClient.putObject( s3Bucket, @@ -215,5 +224,15 @@ export default defineEventHandler(async (event) => { } } + answerChanged.inc({ + hunt: quest.huntId + }); + + if (data.final) { + answerFinalized.inc({ + hunt: quest.huntId + }); + } + return { ok: true }; }); diff --git a/server/metrics.ts b/server/metrics.ts new file mode 100644 index 0000000..657ef24 --- /dev/null +++ b/server/metrics.ts @@ -0,0 +1,25 @@ +import { Counter } from 'prom-client'; + +export const imageUploaded = new Counter({ + name: 'image_uploaded_total', + help: 'Images uploaded', + labelNames: ['hunt'] +}); + +export const answerFinalized = new Counter({ + name: 'answer_finalized_total', + help: 'Answer got finalized', + labelNames: ['hunt'] +}); + +export const answerChanged = new Counter({ + name: 'answer_changed_total', + help: 'Answer got uploaded or changed', + labelNames: ['hunt'] +}); + +export const logins = new Counter({ + name: 'login_total', + help: 'Logins', + labelNames: ['success'] +});