From 816d98d2143e1aeaefaecd1d80603fd519cc9852 Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:47:34 +0000 Subject: [PATCH 1/7] feat: api monitoring --- .env.example | 3 ++ package.json | 4 ++- src/app.ts | 27 ++++++++++++++++++ yarn.lock | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d098d10..ab174c2 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,6 @@ MONGO_URI=mongodb://localhost:27017/your-database-name # Auth Plugin AUTH_DISCOVERY_URL=https://login.microsoftonline.com/c917f3e2-9322-4926-9bb3-daca730413ca/v2.0/.well-known/openid-configuration AUTH_CLIENT_ID=b4bc4b9a-7162-44c5-bb50-fe935dce1f5a + +# Loki Host +# LOKI_HOST=http://localhost:3100 diff --git a/package.json b/package.json index 5d06e4a..6307ab1 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,12 @@ "@sinclair/typebox": "^0.34.41", "fastify": "^5.6.2", "fastify-cli": "7.4.1", + "fastify-metrics": "^12.1.0", "fastify-plugin": "^5.1.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", - "openid-client": "^6.8.1" + "openid-client": "^6.8.1", + "pino-loki": "^3.0.0" }, "devDependencies": { "@commitlint/cli": "^20.1.0", diff --git a/src/app.ts b/src/app.ts index 7ffe4b0..2059075 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,9 +11,13 @@ import { RawRequestDefaultExpression, RawServerDefault, } from "fastify"; +import { createRequire } from "module"; import * as path from "path"; import { fileURLToPath } from "url"; +const require = createRequire(import.meta.url); +const fastifyMetrics = require("fastify-metrics"); + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -21,6 +25,7 @@ export type AppOptions = { // Place your custom options for app below here. // MongoDB URI (Optional) // mongoUri: string; + lokiHost?: string; } & FastifyServerOptions & Partial & AuthPluginOptions; @@ -49,6 +54,7 @@ const options: AppOptions = { // mongoUri: getOption("MONGO_URI")!, authDiscoveryURL: getOption("AUTH_DISCOVERY_URL")!, authClientID: getOption("AUTH_CLIENT_ID")!, + lokiHost: getOption("LOKI_HOST", false), authSkip: (() => { const opt = getOption("AUTH_SKIP", false); if (opt !== undefined) { @@ -59,6 +65,21 @@ const options: AppOptions = { })(), }; +if (options.lokiHost) { + options.logger = { + level: "info", + transport: { + target: "pino-loki", + options: { + batching: true, + interval: 5, + host: options.lokiHost, + labels: { application: "template-service" }, + }, + }, + }; +} + // Support Typebox export type FastifyTypebox = FastifyInstance< RawServerDefault, @@ -83,6 +104,12 @@ const app: FastifyPluginAsync = async ( origin: "*", }); + // Register Metrics + await fastify.register(fastifyMetrics, { + endpoint: "/metrics", + defaultMetrics: { enabled: true }, + }); + // Register Swagger & Swagger UI & Scalar await fastify.register(import("@fastify/swagger"), { openapi: { diff --git a/yarn.lock b/yarn.lock index 250fbf5..32a194d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -686,6 +686,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:^1.4.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add + languageName: node + linkType: hard + "@pinojs/redact@npm:^0.4.0": version: 0.4.0 resolution: "@pinojs/redact@npm:0.4.0" @@ -1342,6 +1349,13 @@ __metadata: languageName: node linkType: hard +"bintrees@npm:1.0.2": + version: 1.0.2 + resolution: "bintrees@npm:1.0.2" + checksum: 10c0/132944b20c93c1a8f97bf8aa25980a76c6eb4291b7f2df2dbcd01cb5b417c287d3ee0847c7260c9f05f3d5a4233aaa03dec95114e97f308abe9cc3f72bed4a44 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.12 resolution: "brace-expansion@npm:1.1.12" @@ -2080,6 +2094,18 @@ __metadata: languageName: node linkType: hard +"fastify-metrics@npm:^12.1.0": + version: 12.1.0 + resolution: "fastify-metrics@npm:12.1.0" + dependencies: + fastify-plugin: "npm:^5.0.0" + prom-client: "npm:^15.1.3" + peerDependencies: + fastify: ">=5" + checksum: 10c0/b42940b8c7cbfcd86182b9a85b53dc903727c73523cf14ff465fca7a64ffa966dcf14e9af944efdecfb1be027f2cb356dbaf5126f25c789eed6cd5b6d4767e24 + languageName: node + linkType: hard + "fastify-plugin@npm:^4.5.1": version: 4.5.1 resolution: "fastify-plugin@npm:4.5.1" @@ -3611,6 +3637,27 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^3.0.0": + version: 3.0.0 + resolution: "pino-abstract-transport@npm:3.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10c0/4486e1b9508110aaf963d07741ac98d660b974dd51d8ad42077d215118e27cda20c64da46c07c926898d52540aab7c6b9c37dc0f5355c203bb1d6a72b5bd8d6c + languageName: node + linkType: hard + +"pino-loki@npm:^3.0.0": + version: 3.0.0 + resolution: "pino-loki@npm:3.0.0" + dependencies: + pino-abstract-transport: "npm:^3.0.0" + pump: "npm:^3.0.3" + bin: + pino-loki: dist/cli.mjs + checksum: 10c0/d7d83b8989366ff73d461f0c39adf1c7000c54a658f282cdb0e035e0fea88ac63933bd0f84dd13fbfcc210581f54b97389ff104f501559ea635c5316a1a6b398 + languageName: node + linkType: hard + "pino-pretty@npm:^13.0.0": version: 13.0.0 resolution: "pino-pretty@npm:13.0.0" @@ -3810,6 +3857,16 @@ __metadata: languageName: node linkType: hard +"prom-client@npm:^15.1.3": + version: 15.1.3 + resolution: "prom-client@npm:15.1.3" + dependencies: + "@opentelemetry/api": "npm:^1.4.0" + tdigest: "npm:^0.1.1" + checksum: 10c0/816525572e5799a2d1d45af78512fb47d073c842dc899c446e94d17cfc343d04282a1627c488c7ca1bcd47f766446d3e49365ab7249f6d9c22c7664a5bce7021 + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -3820,6 +3877,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.3": + version: 3.0.3 + resolution: "pump@npm:3.0.3" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9 + languageName: node + linkType: hard + "punycode@npm:^2.1.0, punycode@npm:^2.3.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -4160,6 +4227,15 @@ __metadata: languageName: node linkType: hard +"tdigest@npm:^0.1.1": + version: 0.1.2 + resolution: "tdigest@npm:0.1.2" + dependencies: + bintrees: "npm:1.0.2" + checksum: 10c0/10187b8144b112fcdfd3a5e4e9068efa42c990b1e30cd0d4f35ee8f58f16d1b41bc587e668fa7a6f6ca31308961cbd06cd5d4a4ae1dc388335902ae04f7d57df + languageName: node + linkType: hard + "template-api@workspace:.": version: 0.0.0-use.local resolution: "template-api@workspace:." @@ -4185,6 +4261,7 @@ __metadata: eslint-plugin-prettier: "npm:^5.5.4" fastify: "npm:^5.6.2" fastify-cli: "npm:7.4.1" + fastify-metrics: "npm:^12.1.0" fastify-plugin: "npm:^5.1.0" fastify-tsconfig: "npm:^3.0.0" globals: "npm:^16.5.0" @@ -4192,6 +4269,7 @@ __metadata: jsonwebtoken: "npm:^9.0.2" jwks-rsa: "npm:^3.2.0" openid-client: "npm:^6.8.1" + pino-loki: "npm:^3.0.0" prettier: "npm:3.7.4" prettier-plugin-jsdoc: "npm:^1.7.0" typescript: "npm:^5.9.3" From 39fb6008a0887efb05616b3d9f9253638518b25a Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:25:57 +0000 Subject: [PATCH 2/7] chore: clean-up imports and usage --- src/app.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app.ts b/src/app.ts index 2059075..7ed71bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,13 +11,10 @@ import { RawRequestDefaultExpression, RawServerDefault, } from "fastify"; -import { createRequire } from "module"; +import fastifyMetrics from "fastify-metrics"; import * as path from "path"; import { fileURLToPath } from "url"; -const require = createRequire(import.meta.url); -const fastifyMetrics = require("fastify-metrics"); - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -74,7 +71,7 @@ if (options.lokiHost) { batching: true, interval: 5, host: options.lokiHost, - labels: { application: "template-service" }, + labels: { application: packageJson.name }, }, }, }; @@ -105,7 +102,7 @@ const app: FastifyPluginAsync = async ( }); // Register Metrics - await fastify.register(fastifyMetrics, { + await fastify.register(fastifyMetrics.default, { endpoint: "/metrics", defaultMetrics: { enabled: true }, }); From 605784622ad54d0a78432627a91560280058d347 Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:50:33 +0000 Subject: [PATCH 3/7] feat: limit /metrics to local networks using plugins --- package.json | 1 + src/app.ts | 9 +----- src/plugins/metrics.ts | 67 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 +++++ 4 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 src/plugins/metrics.ts diff --git a/package.json b/package.json index 6307ab1..bea7fe0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "fastify-cli": "7.4.1", "fastify-metrics": "^12.1.0", "fastify-plugin": "^5.1.0", + "ipaddr.js": "^2.3.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", "openid-client": "^6.8.1", diff --git a/src/app.ts b/src/app.ts index 7ed71bb..1a2fd48 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,6 @@ import { RawRequestDefaultExpression, RawServerDefault, } from "fastify"; -import fastifyMetrics from "fastify-metrics"; import * as path from "path"; import { fileURLToPath } from "url"; @@ -69,7 +68,7 @@ if (options.lokiHost) { target: "pino-loki", options: { batching: true, - interval: 5, + interval: 5, // Every 5 seconds, default. host: options.lokiHost, labels: { application: packageJson.name }, }, @@ -101,12 +100,6 @@ const app: FastifyPluginAsync = async ( origin: "*", }); - // Register Metrics - await fastify.register(fastifyMetrics.default, { - endpoint: "/metrics", - defaultMetrics: { enabled: true }, - }); - // Register Swagger & Swagger UI & Scalar await fastify.register(import("@fastify/swagger"), { openapi: { diff --git a/src/plugins/metrics.ts b/src/plugins/metrics.ts new file mode 100644 index 0000000..6fdb946 --- /dev/null +++ b/src/plugins/metrics.ts @@ -0,0 +1,67 @@ +import fastifyMetrics from "fastify-metrics"; +import fp from "fastify-plugin"; +import ipaddr from "ipaddr.js"; + +export default fp(async (fastify, opts) => { + fastify.addHook("onRequest", async (request, reply) => { + // Only check IP on the /metrics endpoint + // Since we don't register this endpoint, we have to make this check. + if (request.url.split("?")[0] === "/metrics") { + const ip = request.ip; + + let remoteIP; + try { + remoteIP = ipaddr.parse(ip); + } catch { + request.log.warn({ ip }, "Invalid IP address accessing metrics"); + return reply.code(403).send("Forbidden"); + } + + // Handle IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) + if ( + remoteIP.kind() === "ipv6" && + (remoteIP as ipaddr.IPv6).isIPv4MappedAddress() + ) { + remoteIP = (remoteIP as ipaddr.IPv6).toIPv4Address(); + } + + const allowedIPv4 = [ + ipaddr.parseCIDR("127.0.0.0/8"), // localhost + ipaddr.parseCIDR("10.0.0.0/8"), // private A + ipaddr.parseCIDR("172.16.0.0/12"), // private B (includes 172.17.0.0/16 docker) + ]; + + const allowedIPv6 = [ + ipaddr.parseCIDR("::1/128"), // localhost + ipaddr.parseCIDR("fc00::/7"), // unique local + ]; + + let allowed = false; + if (remoteIP.kind() === "ipv4") { + for (const range of allowedIPv4) { + if (remoteIP.match(range)) { + allowed = true; + break; + } + } + } else if (remoteIP.kind() === "ipv6") { + for (const range of allowedIPv6) { + if (remoteIP.match(range)) { + allowed = true; + break; + } + } + } + + if (!allowed) { + request.log.warn({ ip }, "Access to metrics denied"); + return reply.code(403).send("Forbidden"); + } + } + }); + + await fastify.register(fastifyMetrics.default, { + endpoint: "/metrics", + defaultMetrics: { enabled: true }, + }); +}); diff --git a/yarn.lock b/yarn.lock index 32a194d..79f4599 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,6 +2500,13 @@ __metadata: languageName: node linkType: hard +"ipaddr.js@npm:^2.3.0": + version: 2.3.0 + resolution: "ipaddr.js@npm:2.3.0" + checksum: 10c0/084bab99e2f6875d7a62adc3325e1c64b038a12c9521e35fb967b5e263a8b3afb1b8884dd77c276092331f5d63298b767491e10997ef147c62da01b143780bbd + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -4266,6 +4273,7 @@ __metadata: fastify-tsconfig: "npm:^3.0.0" globals: "npm:^16.5.0" husky: "npm:^9.1.7" + ipaddr.js: "npm:^2.3.0" jsonwebtoken: "npm:^9.0.2" jwks-rsa: "npm:^3.2.0" openid-client: "npm:^6.8.1" From de287f160d1172bb1f7366b019ac8740d54586dd Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:22:48 +0000 Subject: [PATCH 4/7] Revert "feat: limit /metrics to local networks using plugins" This reverts commit 605784622ad54d0a78432627a91560280058d347. --- package.json | 1 - src/app.ts | 9 +++++- src/plugins/metrics.ts | 67 ------------------------------------------ yarn.lock | 8 ----- 4 files changed, 8 insertions(+), 77 deletions(-) delete mode 100644 src/plugins/metrics.ts diff --git a/package.json b/package.json index bea7fe0..6307ab1 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "fastify-cli": "7.4.1", "fastify-metrics": "^12.1.0", "fastify-plugin": "^5.1.0", - "ipaddr.js": "^2.3.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", "openid-client": "^6.8.1", diff --git a/src/app.ts b/src/app.ts index 1a2fd48..7ed71bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { RawRequestDefaultExpression, RawServerDefault, } from "fastify"; +import fastifyMetrics from "fastify-metrics"; import * as path from "path"; import { fileURLToPath } from "url"; @@ -68,7 +69,7 @@ if (options.lokiHost) { target: "pino-loki", options: { batching: true, - interval: 5, // Every 5 seconds, default. + interval: 5, host: options.lokiHost, labels: { application: packageJson.name }, }, @@ -100,6 +101,12 @@ const app: FastifyPluginAsync = async ( origin: "*", }); + // Register Metrics + await fastify.register(fastifyMetrics.default, { + endpoint: "/metrics", + defaultMetrics: { enabled: true }, + }); + // Register Swagger & Swagger UI & Scalar await fastify.register(import("@fastify/swagger"), { openapi: { diff --git a/src/plugins/metrics.ts b/src/plugins/metrics.ts deleted file mode 100644 index 6fdb946..0000000 --- a/src/plugins/metrics.ts +++ /dev/null @@ -1,67 +0,0 @@ -import fastifyMetrics from "fastify-metrics"; -import fp from "fastify-plugin"; -import ipaddr from "ipaddr.js"; - -export default fp(async (fastify, opts) => { - fastify.addHook("onRequest", async (request, reply) => { - // Only check IP on the /metrics endpoint - // Since we don't register this endpoint, we have to make this check. - if (request.url.split("?")[0] === "/metrics") { - const ip = request.ip; - - let remoteIP; - try { - remoteIP = ipaddr.parse(ip); - } catch { - request.log.warn({ ip }, "Invalid IP address accessing metrics"); - return reply.code(403).send("Forbidden"); - } - - // Handle IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) - if ( - remoteIP.kind() === "ipv6" && - (remoteIP as ipaddr.IPv6).isIPv4MappedAddress() - ) { - remoteIP = (remoteIP as ipaddr.IPv6).toIPv4Address(); - } - - const allowedIPv4 = [ - ipaddr.parseCIDR("127.0.0.0/8"), // localhost - ipaddr.parseCIDR("10.0.0.0/8"), // private A - ipaddr.parseCIDR("172.16.0.0/12"), // private B (includes 172.17.0.0/16 docker) - ]; - - const allowedIPv6 = [ - ipaddr.parseCIDR("::1/128"), // localhost - ipaddr.parseCIDR("fc00::/7"), // unique local - ]; - - let allowed = false; - if (remoteIP.kind() === "ipv4") { - for (const range of allowedIPv4) { - if (remoteIP.match(range)) { - allowed = true; - break; - } - } - } else if (remoteIP.kind() === "ipv6") { - for (const range of allowedIPv6) { - if (remoteIP.match(range)) { - allowed = true; - break; - } - } - } - - if (!allowed) { - request.log.warn({ ip }, "Access to metrics denied"); - return reply.code(403).send("Forbidden"); - } - } - }); - - await fastify.register(fastifyMetrics.default, { - endpoint: "/metrics", - defaultMetrics: { enabled: true }, - }); -}); diff --git a/yarn.lock b/yarn.lock index 79f4599..32a194d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,13 +2500,6 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.3.0": - version: 2.3.0 - resolution: "ipaddr.js@npm:2.3.0" - checksum: 10c0/084bab99e2f6875d7a62adc3325e1c64b038a12c9521e35fb967b5e263a8b3afb1b8884dd77c276092331f5d63298b767491e10997ef147c62da01b143780bbd - languageName: node - linkType: hard - "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -4273,7 +4266,6 @@ __metadata: fastify-tsconfig: "npm:^3.0.0" globals: "npm:^16.5.0" husky: "npm:^9.1.7" - ipaddr.js: "npm:^2.3.0" jsonwebtoken: "npm:^9.0.2" jwks-rsa: "npm:^3.2.0" openid-client: "npm:^6.8.1" From 886092142bc3636245fa9da00c2d04c9dad4285b Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:55:29 +0000 Subject: [PATCH 5/7] feat: prometheus key to access /metrics --- .env.example | 3 +++ src/app.ts | 26 +++++++++++++++++++++-- test/helper.ts | 6 ++++-- test/routes/metrics.test.ts | 41 +++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 test/routes/metrics.test.ts diff --git a/.env.example b/.env.example index ab174c2..b1b7de8 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,6 @@ AUTH_CLIENT_ID=b4bc4b9a-7162-44c5-bb50-fe935dce1f5a # Loki Host # LOKI_HOST=http://localhost:3100 + +# Prometheus Key +PROMETHEUS_KEY=prometheus diff --git a/src/app.ts b/src/app.ts index 7ed71bb..d515014 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,6 +10,9 @@ import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, + FastifyReply, + FastifyRequest, + RouteOptions, } from "fastify"; import fastifyMetrics from "fastify-metrics"; import * as path from "path"; @@ -23,6 +26,7 @@ export type AppOptions = { // MongoDB URI (Optional) // mongoUri: string; lokiHost?: string; + prometheusKey?: string; } & FastifyServerOptions & Partial & AuthPluginOptions; @@ -52,6 +56,7 @@ const options: AppOptions = { authDiscoveryURL: getOption("AUTH_DISCOVERY_URL")!, authClientID: getOption("AUTH_CLIENT_ID")!, lokiHost: getOption("LOKI_HOST", false), + prometheusKey: getOption("PROMETHEUS_KEY", false), authSkip: (() => { const opt = getOption("AUTH_SKIP", false); if (opt !== undefined) { @@ -69,7 +74,7 @@ if (options.lokiHost) { target: "pino-loki", options: { batching: true, - interval: 5, + interval: 5, // Logs are sent every 5 seconds, default. host: options.lokiHost, labels: { application: packageJson.name }, }, @@ -102,9 +107,26 @@ const app: FastifyPluginAsync = async ( }); // Register Metrics + const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey + ? { + url: "/metrics", + method: "GET", + handler: async () => {}, // Overridden by fastify-metrics + onRequest: async (request: FastifyRequest, reply: FastifyReply) => { + if ( + request.headers.authorization !== `Bearer ${opts.prometheusKey}` + ) { + reply.code(401).send("Unauthorized"); + return reply; + } + }, + } + : "/metrics"; + await fastify.register(fastifyMetrics.default, { - endpoint: "/metrics", + endpoint: metricsEndpoint, defaultMetrics: { enabled: true }, + clearRegisterOnInit: true, }); // Register Swagger & Swagger UI & Scalar diff --git a/test/helper.ts b/test/helper.ts index db4bdde..9edb964 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -25,14 +25,16 @@ async function config(): Promise { } // Automatically build and tear down our instance -async function build(t: TestContext) { +async function build(t: TestContext, options?: Partial) { // you can set all the options supported by the fastify CLI command const argv = [AppPath]; + const appOptions = { ...(await config()), ...options }; + // fastify-plugin ensures that all decorators // are exposed for testing purposes, this is // different from the production setup - const app = await helper.build(argv, await config(), await config()); + const app = await helper.build(argv, appOptions, appOptions); // Tear down our app after we are done t.after(() => void app.close()); diff --git a/test/routes/metrics.test.ts b/test/routes/metrics.test.ts new file mode 100644 index 0000000..bc479f7 --- /dev/null +++ b/test/routes/metrics.test.ts @@ -0,0 +1,41 @@ +import { test } from "node:test"; +import * as assert from "node:assert"; +import { build } from "../helper.js"; + +test("metrics route without key", async (t) => { + const app = await build(t); + + const response = await app.inject({ + url: "/metrics", + }); + + assert.equal(response.statusCode, 200); +}); + +test("metrics route with key", async (t) => { + const app = await build(t, { prometheusKey: "secret" }); + + // Without auth header + const response = await app.inject({ + url: "/metrics", + }); + assert.equal(response.statusCode, 401); + + // With correct auth header + const responseAuth = await app.inject({ + url: "/metrics", + headers: { + authorization: "Bearer secret", + }, + }); + assert.equal(responseAuth.statusCode, 200); + + // With incorrect auth header + const responseBadAuth = await app.inject({ + url: "/metrics", + headers: { + authorization: "Bearer wrong", + }, + }); + assert.equal(responseBadAuth.statusCode, 401); +}); From c1d65cc52ef593f68f02b74576f60a57018c4c0e Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:29:25 +0000 Subject: [PATCH 6/7] chore: fix linting --- test/routes/metrics.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/routes/metrics.test.ts b/test/routes/metrics.test.ts index bc479f7..25224ad 100644 --- a/test/routes/metrics.test.ts +++ b/test/routes/metrics.test.ts @@ -1,6 +1,6 @@ -import { test } from "node:test"; -import * as assert from "node:assert"; import { build } from "../helper.js"; +import * as assert from "node:assert"; +import { test } from "node:test"; test("metrics route without key", async (t) => { const app = await build(t); From 18d4a1b913478ab5fb862bf5a79db7b3b7155898 Mon Sep 17 00:00:00 2001 From: wylited <44154103+wylited@users.noreply.github.com> Date: Sun, 11 Jan 2026 23:44:01 +0800 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app.ts | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/app.ts b/src/app.ts index d515014..b8a3efb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -68,18 +68,40 @@ const options: AppOptions = { }; if (options.lokiHost) { - options.logger = { - level: "info", - transport: { - target: "pino-loki", - options: { - batching: true, - interval: 5, // Logs are sent every 5 seconds, default. - host: options.lokiHost, - labels: { application: packageJson.name }, - }, + const lokiTransport = { + target: "pino-loki", + options: { + batching: true, + interval: 5, // Logs are sent every 5 seconds, default. + host: options.lokiHost, + labels: { application: packageJson.name }, }, }; + + const existingLogger = options.logger; + + if (existingLogger && typeof existingLogger === "object") { + const existingTransport = (existingLogger as any).transport; + + let mergedTransport: any; + if (Array.isArray(existingTransport)) { + mergedTransport = [...existingTransport, lokiTransport]; + } else if (existingTransport) { + mergedTransport = [existingTransport, lokiTransport]; + } else { + mergedTransport = lokiTransport; + } + + options.logger = { + ...(existingLogger as any), + transport: mergedTransport, + } as any; + } else { + options.logger = { + level: "info", + transport: lokiTransport, + }; + } } // Support Typebox @@ -116,8 +138,8 @@ const app: FastifyPluginAsync = async ( if ( request.headers.authorization !== `Bearer ${opts.prometheusKey}` ) { - reply.code(401).send("Unauthorized"); - return reply; + reply.code(401).send({ status: "error", message: "Unauthorized" }); + return; } }, }