-
Notifications
You must be signed in to change notification settings - Fork 0
feat: API monitoring #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bb6beea
02a39cd
fde5785
9380014
4d47d15
2933a14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| global: | ||
| scrape_interval: 15s # By default, scrape targets every 15 seconds. | ||
|
|
||
| # Attach these labels to any time series or alerts when communicating with | ||
| # external systems (federation, remote storage, Alertmanager). | ||
| external_labels: | ||
| monitor: 'codelab-monitor' | ||
|
|
||
| # Scrape configurations used for local development: | ||
| # - `prometheus`: Prometheus' own internal metrics endpoint. | ||
| # - `template-api`: this project's API (expects metrics at `http://localhost:3000/metrics`). | ||
| scrape_configs: | ||
| # Prometheus server metrics (job=prometheus) | ||
| - job_name: 'prometheus' | ||
|
|
||
| # Short interval for local testing. | ||
| scrape_interval: 5s | ||
|
|
||
| static_configs: | ||
| - targets: ['localhost:9090'] | ||
|
|
||
| # The template API exposes application metrics at `/metrics`. | ||
| # Make sure the API is running locally and exposes Prometheus metrics (e.g. using `prom-client` or middleware). | ||
| - job_name: 'template-api' | ||
| # Keep a short interval for fast feedback during development. | ||
| scrape_interval: 5s | ||
| # Prometheus will request the `/metrics` path by default, but we show it here explicitly for clarity. | ||
| metrics_path: /metrics | ||
| static_configs: | ||
| - targets: ['localhost:3000'] | ||
|
|
||
| # Optional: relabeling examples for common local/dev setups. Uncomment and adapt as needed. | ||
| # - Add a static label (useful for environment-specific queries) | ||
| # relabel_configs: | ||
| # - target_label: env | ||
| # replacement: dev | ||
| # | ||
| # - Set `instance` to the hostname (strip port) | ||
| # - source_labels: [__address__] | ||
| # regex: '([^:]+):.*' | ||
| # target_label: instance | ||
| # replacement: '$1' | ||
| # | ||
| # - Drop a specific target (useful to temporarily disable scraping) | ||
| # - source_labels: [__address__] | ||
| # regex: '127\\.0\\.0\\.1:3000' | ||
| # action: drop | ||
| # | ||
| # - Preserve original address in another label before rewriting (advanced example) | ||
| # - source_labels: [__address__] | ||
| # target_label: __param_target | ||
| # replacement: '$1' | ||
| # | ||
| # Notes: | ||
| # - `relabel_configs` run during target discovery and can add/modify/drop labels. | ||
| # - Keep examples commented so the default local setup continues to work. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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"; | ||||||
|
|
||||||
|
|
@@ -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; | ||||||
|
|
@@ -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
|
||||||
| } | ||||||
polyipseity marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| // Support Typebox | ||||||
| export type FastifyTypebox = FastifyInstance< | ||||||
| RawServerDefault, | ||||||
|
|
@@ -93,6 +139,29 @@ const app: FastifyPluginAsync<AppOptions> = async ( | |||||
| origin: "*", | ||||||
| }); | ||||||
|
|
||||||
| // Register Metrics | ||||||
| const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey | ||||||
|
||||||
| const metricsEndpoint: RouteOptions | string | null = opts.prometheusKey | |
| const metricsEndpoint: RouteOptions | string = opts.prometheusKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since opts.prometheusKey, is nullable, so is the route.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that the entire statement is always non-null:
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";If opts.prometheusKey is null then it goes to the falsy branch and evaluates to "/metrics". Maybe it's not what you intended?
wylited marked this conversation as resolved.
Show resolved
Hide resolved
| 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); | ||
| }); | ||
wylited marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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); | ||
wylited marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // With incorrect auth header | ||
| const responseBadAuth = await app.inject({ | ||
| url: "/metrics", | ||
| headers: { | ||
| authorization: "Bearer wrong", | ||
| }, | ||
| }); | ||
| assert.equal(responseBadAuth.statusCode, 401); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.