Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 69 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
FastifyReply,
FastifyRequest,
RouteOptions,
} from "fastify";
import fastifyMetrics from "fastify-metrics";
import * as path from "path";
import { fileURLToPath } from "url";

Expand All @@ -21,6 +25,8 @@ export type AppOptions = {
// Place your custom options for app below here.
// MongoDB URI (Optional)
// mongoUri: string;
lokiHost?: string;
prometheusKey?: string;
} & FastifyServerOptions &
Partial<AutoloadPluginOptions> &
AuthPluginOptions;
Expand Down Expand Up @@ -66,9 +72,49 @@ const options: AppOptions = {
// mongoUri: getOption("MONGO_URI")!,
authDiscoveryURL: getOption("AUTH_DISCOVERY_URL")!,
authClientID: getOption("AUTH_CLIENT_ID")!,
lokiHost: getOption("LOKI_HOST", false),
prometheusKey: getOption("PROMETHEUS_KEY", false),
authSkip: getBooleanOption("AUTH_SKIP", false),
};

if (options.lokiHost) {
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 loggerOptions = existingLogger as { transport?: unknown };
const existingTransport = loggerOptions.transport;

let mergedTransport: unknown;
if (Array.isArray(existingTransport)) {
mergedTransport = [...existingTransport, lokiTransport];
} else if (existingTransport) {
mergedTransport = [existingTransport, lokiTransport];
} else {
mergedTransport = lokiTransport;
}

options.logger = {
...(existingLogger as object),
transport: mergedTransport,
} as Exclude<FastifyServerOptions["logger"], boolean | undefined>;
} else {
options.logger = {
level: "info",
transport: lokiTransport,
};
}
Comment on lines +93 to +115
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Loki transport configuration modifies the options object at module level before the app is created. If the logger option is a boolean (true/false) rather than an object, the code in the else block (lines 111-114) will override it, potentially causing unexpected behavior. The condition on line 93 should also check if existingLogger is not a boolean value to properly handle all logger configuration types.

Copilot uses AI. Check for mistakes.
}
Comment on lines +80 to +116
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Loki transport configuration logic (lines 80-116) lacks test coverage. Since the repository uses comprehensive automated testing for TypeScript code, consider adding tests to verify the logger configuration is correctly merged when lokiHost is provided, especially for edge cases like when logger is a boolean, an object with existing transports (single or array), or undefined.

Copilot uses AI. Check for mistakes.

// Support Typebox
export type FastifyTypebox = FastifyInstance<
RawServerDefault,
Expand All @@ -93,6 +139,29 @@ const app: FastifyPluginAsync<AppOptions> = async (
origin: "*",
});

// 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({ status: "error", message: "Unauthorized" });
return;
}
},
}
: "/metrics";

await fastify.register(fastifyMetrics.default, {
endpoint: metricsEndpoint,
defaultMetrics: { enabled: true },
clearRegisterOnInit: true,
});

// Register Swagger & Swagger UI & Scalar
await fastify.register(import("@fastify/swagger"), {
openapi: {
Expand Down
6 changes: 4 additions & 2 deletions test/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ async function config(): Promise<AppOptions> {
}

// Automatically build and tear down our instance
async function build(t: TestContext) {
async function build(t: TestContext, options?: Partial<AppOptions>) {
// 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());
Expand Down
41 changes: 41 additions & 0 deletions test/routes/metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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);

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);
});
78 changes: 78 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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:."
Expand All @@ -4185,13 +4261,15 @@ __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"
husky: "npm:^9.1.7"
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"
Expand Down
Loading