From 48b1413957782460c2621c7a840af4c916c30988 Mon Sep 17 00:00:00 2001 From: Bernd Storath <32197462+kaaax0815@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:27:22 +0100 Subject: [PATCH] Feat: Prometheus (#1655) * check metrics password * rewrite prometheus and json metric endpoints * move metrics to general metrics is not per interface * change metrics settings in admin panel * add i18n keys --- src/app/components/ClientCard/Avatar.vue | 8 +- src/app/components/form/DateField.vue | 2 +- src/app/components/form/NullTextField.vue | 26 ++++++ src/app/pages/admin.vue | 1 - src/app/pages/admin/index.vue | 26 +++++- src/app/pages/admin/metrics.vue | 3 - src/app/pages/me.vue | 23 ++--- src/app/pages/setup/1.vue | 10 +- src/i18n/locales/en.json | 15 ++- src/server/api/admin/general.get.ts | 6 +- .../database/migrations/0000_short_skin.sql | 11 +-- .../migrations/0001_classy_the_stranger.sql | 4 +- .../migrations/meta/0000_snapshot.json | 77 +++++----------- .../migrations/meta/0001_snapshot.json | 79 +++++----------- .../database/migrations/meta/_journal.json | 4 +- .../database/repositories/general/schema.ts | 7 ++ .../database/repositories/general/service.ts | 91 +++++++++++++++---- .../database/repositories/general/types.ts | 9 ++ .../database/repositories/interface/schema.ts | 6 +- .../database/repositories/metrics/schema.ts | 21 ----- .../database/repositories/metrics/service.ts | 31 ------- .../database/repositories/metrics/types.ts | 4 - src/server/database/schema.ts | 1 - src/server/database/sqlite.ts | 3 - src/server/routes/metrics/index.get.ts | 14 --- src/server/routes/metrics/json.get.ts | 44 ++++++--- src/server/routes/metrics/prometheus.get.ts | 67 ++++++++++++++ src/server/utils/handler.ts | 60 ++++++++++++ src/server/utils/metrics.ts | 74 --------------- src/shared/utils/time.ts | 10 ++ 30 files changed, 391 insertions(+), 346 deletions(-) create mode 100644 src/app/components/form/NullTextField.vue delete mode 100644 src/app/pages/admin/metrics.vue delete mode 100644 src/server/database/repositories/metrics/schema.ts delete mode 100644 src/server/database/repositories/metrics/service.ts delete mode 100644 src/server/database/repositories/metrics/types.ts delete mode 100644 src/server/routes/metrics/index.get.ts create mode 100644 src/server/routes/metrics/prometheus.get.ts delete mode 100644 src/server/utils/metrics.ts create mode 100644 src/shared/utils/time.ts diff --git a/src/app/components/ClientCard/Avatar.vue b/src/app/components/ClientCard/Avatar.vue index 2f3cfd06..319fd7dc 100644 --- a/src/app/components/ClientCard/Avatar.vue +++ b/src/app/components/ClientCard/Avatar.vue @@ -6,9 +6,11 @@
defineProps<{ id: string; label: string }>(); -const [data] = defineModel({ +const data = defineModel({ set(value) { const temp = value?.trim() ?? null; if (temp === '') { diff --git a/src/app/components/form/NullTextField.vue b/src/app/components/form/NullTextField.vue new file mode 100644 index 00000000..6d904531 --- /dev/null +++ b/src/app/components/form/NullTextField.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/app/pages/admin.vue b/src/app/pages/admin.vue index ba2f4169..aeb01b3d 100644 --- a/src/app/pages/admin.vue +++ b/src/app/pages/admin.vue @@ -45,7 +45,6 @@ const menuItems = [ { id: 'config', name: 'Config' }, { id: 'interface', name: 'Interface' }, { id: 'hooks', name: 'Hooks' }, - { id: 'metrics', name: 'Metrics' }, ]; const activeMenuItem = computed(() => { diff --git a/src/app/pages/admin/index.vue b/src/app/pages/admin/index.vue index a9979e2c..aa7f18e9 100644 --- a/src/app/pages/admin/index.vue +++ b/src/app/pages/admin/index.vue @@ -5,13 +5,31 @@ - Actions - - + {{ $t('general.metrics') }} + + + + + + {{ $t('form.actions') }} + + diff --git a/src/app/pages/admin/metrics.vue b/src/app/pages/admin/metrics.vue deleted file mode 100644 index 056d8979..00000000 --- a/src/app/pages/admin/metrics.vue +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/app/pages/me.vue b/src/app/pages/me.vue index bbe2989e..b1de45a1 100644 --- a/src/app/pages/me.vue +++ b/src/app/pages/me.vue @@ -9,7 +9,11 @@ {{ $t('me.sectionGeneral') }} - + @@ -49,20 +53,7 @@ authStore.update(); const toast = useToast(); const name = ref(authStore.userData?.name); - -const rawEmail = ref(authStore.userData?.email); -const email = computed({ - get: () => rawEmail.value ?? undefined, - set: (value) => { - const temp = value?.trim() ?? null; - if (temp === '') { - rawEmail.value = null; - return; - } - rawEmail.value = temp; - return; - }, -}); +const email = ref(authStore.userData?.email); async function submit() { try { @@ -70,7 +61,7 @@ async function submit() { method: 'post', body: { name: name.value, - email: rawEmail.value, + email: email.value, }, }); toast.showToast({ diff --git a/src/app/pages/setup/1.vue b/src/app/pages/setup/1.vue index decfb2d9..4b7cedfd 100644 --- a/src/app/pages/setup/1.vue +++ b/src/app/pages/setup/1.vue @@ -6,7 +6,9 @@
-
Continue
+
+ Continue +
@@ -17,10 +19,4 @@ definePageMeta({ const setupStore = useSetupStore(); setupStore.setStep(1); - -const router = useRouter(); - -async function nextStep() { - router.push('/setup/2'); -} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3d40afe5..29cfc7fa 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -107,7 +107,6 @@ }, "name": "Name", "username": "Username", - "password": "Password", "signIn": "Sign In", "logout": "Logout", "updateAvailable": "There is an update available!", @@ -151,5 +150,17 @@ "error": { "clear": "Clear", "login": "Log in error" - } + }, + "general": { + "sessionTimeout": "Session Timeout", + "metrics": "Metrics", + "prometheus": "Prometheus", + "json": "JSON" + }, + "form": { + "actions": "Actions", + "save": "Save", + "revert": "Revert" + }, + "password": "Password" } diff --git a/src/server/api/admin/general.get.ts b/src/server/api/admin/general.get.ts index d27f39f9..965cebe2 100644 --- a/src/server/api/admin/general.get.ts +++ b/src/server/api/admin/general.get.ts @@ -1,6 +1,4 @@ export default definePermissionEventHandler(actions.ADMIN, async () => { - const sessionConfig = await Database.general.getSessionConfig(); - return { - sessionTimeout: sessionConfig.sessionTimeout, - }; + const generalConfig = await Database.general.getConfig(); + return generalConfig; }); diff --git a/src/server/database/migrations/0000_short_skin.sql b/src/server/database/migrations/0000_short_skin.sql index 64d44bf0..921c4b5c 100644 --- a/src/server/database/migrations/0000_short_skin.sql +++ b/src/server/database/migrations/0000_short_skin.sql @@ -24,6 +24,9 @@ CREATE TABLE `general_table` ( `setupStep` integer NOT NULL, `session_password` text NOT NULL, `session_timeout` integer NOT NULL, + `metricsPrometheus` integer NOT NULL, + `metricsJson` integer NOT NULL, + `metricsPassword` text, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL ); @@ -54,14 +57,6 @@ CREATE TABLE `interfaces_table` ( ); --> statement-breakpoint CREATE UNIQUE INDEX `interfaces_table_port_unique` ON `interfaces_table` (`port`);--> statement-breakpoint -CREATE TABLE `prometheus_table` ( - `id` text PRIMARY KEY NOT NULL, - `password` text NOT NULL, - `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, - `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, - FOREIGN KEY (`id`) REFERENCES `interfaces_table`(`name`) ON UPDATE cascade ON DELETE cascade -); ---> statement-breakpoint CREATE TABLE `one_time_links_table` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `one_time_link` text NOT NULL, diff --git a/src/server/database/migrations/0001_classy_the_stranger.sql b/src/server/database/migrations/0001_classy_the_stranger.sql index c1dfd3fe..f305f0e7 100644 --- a/src/server/database/migrations/0001_classy_the_stranger.sql +++ b/src/server/database/migrations/0001_classy_the_stranger.sql @@ -1,6 +1,6 @@ PRAGMA journal_mode=WAL;--> statement-breakpoint -INSERT INTO `general_table` (`setupStep`, `session_password`, `session_timeout`) -VALUES (1, hex(randomblob(256)), 3600); +INSERT INTO `general_table` (`setupStep`, `session_password`, `session_timeout`, `metricsPrometheus`, `metricsJson`) +VALUES (1, hex(randomblob(256)), 3600, 0, 0); --> statement-breakpoint INSERT INTO `interfaces_table` (`name`, `device`, `port`, `private_key`, `public_key`, `ipv4_cidr`, `ipv6_cidr`, `mtu`, `enabled`) VALUES ('wg0', 'eth0', 51820, '---default---', '---default---', '10.8.0.0/24', 'fdcc:ad94:bacf:61a4::cafe:0/112', 1420, 1); diff --git a/src/server/database/migrations/meta/0000_snapshot.json b/src/server/database/migrations/meta/0000_snapshot.json index abc6ade7..4d28e70c 100644 --- a/src/server/database/migrations/meta/0000_snapshot.json +++ b/src/server/database/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "25907c5f-be21-4ae6-88c4-1a72b2f335e7", + "id": "2c4694af-5916-430f-96d3-55aac2653e7e", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "clients_table": { @@ -175,6 +175,27 @@ "notNull": true, "autoincrement": false }, + "metricsPrometheus": { + "name": "metricsPrometheus", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metricsJson": { + "name": "metricsJson", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metricsPassword": { + "name": "metricsPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "text", @@ -370,60 +391,6 @@ "uniqueConstraints": {}, "checkConstraints": {} }, - "prometheus_table": { - "name": "prometheus_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": { - "prometheus_table_id_interfaces_table_name_fk": { - "name": "prometheus_table_id_interfaces_table_name_fk", - "tableFrom": "prometheus_table", - "tableTo": "interfaces_table", - "columnsFrom": [ - "id" - ], - "columnsTo": [ - "name" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, "one_time_links_table": { "name": "one_time_links_table", "columns": { diff --git a/src/server/database/migrations/meta/0001_snapshot.json b/src/server/database/migrations/meta/0001_snapshot.json index d0d7078b..07cb02f8 100644 --- a/src/server/database/migrations/meta/0001_snapshot.json +++ b/src/server/database/migrations/meta/0001_snapshot.json @@ -1,6 +1,6 @@ { - "id": "60af732f-adc0-405d-96cc-2f818585f593", - "prevId": "25907c5f-be21-4ae6-88c4-1a72b2f335e7", + "id": "91d39ed5-2c45-4af6-ba39-4cd72ba71f6a", + "prevId": "2c4694af-5916-430f-96d3-55aac2653e7e", "version": "6", "dialect": "sqlite", "tables": { @@ -175,6 +175,27 @@ "notNull": true, "autoincrement": false }, + "metricsPrometheus": { + "name": "metricsPrometheus", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metricsJson": { + "name": "metricsJson", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metricsPassword": { + "name": "metricsPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "created_at": { "name": "created_at", "type": "text", @@ -370,60 +391,6 @@ "uniqueConstraints": {}, "checkConstraints": {} }, - "prometheus_table": { - "name": "prometheus_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(CURRENT_TIMESTAMP)" - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(CURRENT_TIMESTAMP)" - } - }, - "indexes": {}, - "foreignKeys": { - "prometheus_table_id_interfaces_table_name_fk": { - "name": "prometheus_table_id_interfaces_table_name_fk", - "tableFrom": "prometheus_table", - "columnsFrom": [ - "id" - ], - "tableTo": "interfaces_table", - "columnsTo": [ - "name" - ], - "onUpdate": "cascade", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, "one_time_links_table": { "name": "one_time_links_table", "columns": { diff --git a/src/server/database/migrations/meta/_journal.json b/src/server/database/migrations/meta/_journal.json index 0e72d1fe..4029303b 100644 --- a/src/server/database/migrations/meta/_journal.json +++ b/src/server/database/migrations/meta/_journal.json @@ -5,14 +5,14 @@ { "idx": 0, "version": "6", - "when": 1737122352401, + "when": 1739191645161, "tag": "0000_short_skin", "breakpoints": true }, { "idx": 1, "version": "6", - "when": 1737122356601, + "when": 1739191678456, "tag": "0001_classy_the_stranger", "breakpoints": true } diff --git a/src/server/database/repositories/general/schema.ts b/src/server/database/repositories/general/schema.ts index a2baec6c..3945573d 100644 --- a/src/server/database/repositories/general/schema.ts +++ b/src/server/database/repositories/general/schema.ts @@ -3,9 +3,16 @@ import { sqliteTable, text, int } from 'drizzle-orm/sqlite-core'; export const general = sqliteTable('general_table', { id: int().primaryKey({ autoIncrement: false }).default(1), + setupStep: int().notNull(), + sessionPassword: text('session_password').notNull(), sessionTimeout: int('session_timeout').notNull(), + + metricsPrometheus: int({ mode: 'boolean' }).notNull(), + metricsJson: int({ mode: 'boolean' }).notNull(), + metricsPassword: text(), + createdAt: text('created_at') .notNull() .default(sql`(CURRENT_TIMESTAMP)`), diff --git a/src/server/database/repositories/general/service.ts b/src/server/database/repositories/general/service.ts index af48262d..449dbfab 100644 --- a/src/server/database/repositories/general/service.ts +++ b/src/server/database/repositories/general/service.ts @@ -5,45 +5,68 @@ import type { GeneralUpdateType } from './types'; function createPreparedStatement(db: DBType) { return { - find: db.query.general.findFirst().prepare(), - updateSetupStep: db - .update(general) - .set({ - setupStep: sql.placeholder('setupStep') as never as number, + getSetupStep: db.query.general + .findFirst({ + columns: { + setupStep: true, + }, + }) + .prepare(), + getSessionConfig: db.query.general + .findFirst({ + columns: { + sessionPassword: true, + sessionTimeout: true, + }, }) .prepare(), - update: db + getMetricsConfig: db.query.general + .findFirst({ + columns: { + metricsPrometheus: true, + metricsJson: true, + metricsPassword: true, + }, + }) + .prepare(), + getConfig: db.query.general + .findFirst({ + columns: { + sessionTimeout: true, + metricsPrometheus: true, + metricsJson: true, + metricsPassword: true, + }, + }) + .prepare(), + updateSetupStep: db .update(general) .set({ - sessionTimeout: sql.placeholder('sessionTimeout') as never as number, + setupStep: sql.placeholder('setupStep') as never as number, }) .prepare(), }; } export class GeneralService { + #db: DBType; #statements: ReturnType; constructor(db: DBType) { + this.#db = db; this.#statements = createPreparedStatement(db); } /** * @throws */ - private async get() { - const result = await this.#statements.find.execute(); + async getSetupStep() { + const result = await this.#statements.getSetupStep.execute(); + if (!result) { throw new Error('General Config not found'); } - return result; - } - /** - * @throws - */ - async getSetupStep() { - const result = await this.get(); return { step: result.setupStep, done: result.setupStep === 0 }; } @@ -55,14 +78,46 @@ export class GeneralService { * @throws */ async getSessionConfig() { - const result = await this.get(); + const result = await this.#statements.getSessionConfig.execute(); + + if (!result) { + throw new Error('General Config not found'); + } + return { sessionPassword: result.sessionPassword, sessionTimeout: result.sessionTimeout, }; } + /** + * @throws + */ + async getMetricsConfig() { + const result = await this.#statements.getMetricsConfig.execute(); + + if (!result) { + throw new Error('General Config not found'); + } + + return { + prometheus: result.metricsPrometheus, + json: result.metricsJson, + password: result.metricsPassword, + }; + } + update(data: GeneralUpdateType) { - return this.#statements.update.execute(data); + return this.#db.update(general).set(data).execute(); + } + + async getConfig() { + const result = await this.#statements.getConfig.execute(); + + if (!result) { + throw new Error('General Config not found'); + } + + return result; } } diff --git a/src/server/database/repositories/general/types.ts b/src/server/database/repositories/general/types.ts index 05944528..74d8dfc2 100644 --- a/src/server/database/repositories/general/types.ts +++ b/src/server/database/repositories/general/types.ts @@ -5,9 +5,18 @@ import z from 'zod'; export type GeneralType = InferSelectModel; const sessionTimeout = z.number({ message: 'zod.general.sessionTimeout' }); +const metricsEnabled = z.boolean({ message: 'zod.general.metricsEnabled' }); +const metricsPassword = z + .string({ message: 'zod.general.metricsPassword' }) + .min(1, { message: 'zod.general.metricsPasswordMin' }) + // TODO: validate argon2 regex? + .nullable(); export const GeneralUpdateSchema = z.object({ sessionTimeout: sessionTimeout, + metricsPrometheus: metricsEnabled, + metricsJson: metricsEnabled, + metricsPassword: metricsPassword, }); export type GeneralUpdateType = z.infer; diff --git a/src/server/database/repositories/interface/schema.ts b/src/server/database/repositories/interface/schema.ts index 41dd898d..aa7a5b5e 100644 --- a/src/server/database/repositories/interface/schema.ts +++ b/src/server/database/repositories/interface/schema.ts @@ -1,7 +1,7 @@ import { sql, relations } from 'drizzle-orm'; import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { userConfig, hooks, prometheus } from '../../schema'; +import { userConfig, hooks } from '../../schema'; // maybe support multiple interfaces in the future export const wgInterface = sqliteTable('interfaces_table', { @@ -28,10 +28,6 @@ export const wgInterfaceRelations = relations(wgInterface, ({ one }) => ({ fields: [wgInterface.name], references: [hooks.id], }), - prometheus: one(prometheus, { - fields: [wgInterface.name], - references: [prometheus.id], - }), userConfig: one(userConfig, { fields: [wgInterface.name], references: [userConfig.id], diff --git a/src/server/database/repositories/metrics/schema.ts b/src/server/database/repositories/metrics/schema.ts deleted file mode 100644 index 894bc0c3..00000000 --- a/src/server/database/repositories/metrics/schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { sql } from 'drizzle-orm'; -import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; - -import { wgInterface } from '../../schema'; - -export const prometheus = sqliteTable('prometheus_table', { - id: text() - .primaryKey() - .references(() => wgInterface.name, { - onDelete: 'cascade', - onUpdate: 'cascade', - }), - password: text().notNull(), - createdAt: text('created_at') - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - updatedAt: text('updated_at') - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`) - .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), -}); diff --git a/src/server/database/repositories/metrics/service.ts b/src/server/database/repositories/metrics/service.ts deleted file mode 100644 index 38685749..00000000 --- a/src/server/database/repositories/metrics/service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { DBType } from '#db/sqlite'; -import { eq, sql } from 'drizzle-orm'; -import { prometheus } from './schema'; - -function createPreparedStatement(db: DBType) { - return { - get: db.query.prometheus - .findFirst({ where: eq(prometheus.id, sql.placeholder('interface')) }) - .prepare(), - }; -} - -export class PrometheusService { - #statements: ReturnType; - - constructor(db: DBType) { - this.#statements = createPreparedStatement(db); - } - - get(infName: string) { - return this.#statements.get.execute({ interface: infName }); - } -} - -export class MetricsService { - prometheus: PrometheusService; - - constructor(db: DBType) { - this.prometheus = new PrometheusService(db); - } -} diff --git a/src/server/database/repositories/metrics/types.ts b/src/server/database/repositories/metrics/types.ts deleted file mode 100644 index 45c4bcad..00000000 --- a/src/server/database/repositories/metrics/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { InferSelectModel } from 'drizzle-orm'; -import type { prometheus } from './schema'; - -export type PrometheusType = InferSelectModel; diff --git a/src/server/database/schema.ts b/src/server/database/schema.ts index 73edce69..ba56016f 100644 --- a/src/server/database/schema.ts +++ b/src/server/database/schema.ts @@ -3,7 +3,6 @@ export * from './repositories/client/schema'; export * from './repositories/general/schema'; export * from './repositories/hooks/schema'; export * from './repositories/interface/schema'; -export * from './repositories/metrics/schema'; export * from './repositories/oneTimeLink/schema'; export * from './repositories/user/schema'; export * from './repositories/userConfig/schema'; diff --git a/src/server/database/sqlite.ts b/src/server/database/sqlite.ts index 738e1192..222fea3b 100644 --- a/src/server/database/sqlite.ts +++ b/src/server/database/sqlite.ts @@ -11,7 +11,6 @@ import { UserConfigService } from './repositories/userConfig/service'; import { InterfaceService } from './repositories/interface/service'; import { HooksService } from './repositories/hooks/service'; import { OneTimeLinkService } from './repositories/oneTimeLink/service'; -import { MetricsService } from './repositories/metrics/service'; const DB_DEBUG = debug('Database'); @@ -31,7 +30,6 @@ class DBService { interfaces: InterfaceService; hooks: HooksService; oneTimeLinks: OneTimeLinkService; - metrics: MetricsService; constructor(db: DBType) { this.clients = new ClientService(db); @@ -41,7 +39,6 @@ class DBService { this.interfaces = new InterfaceService(db); this.hooks = new HooksService(db); this.oneTimeLinks = new OneTimeLinkService(db); - this.metrics = new MetricsService(db); } } diff --git a/src/server/routes/metrics/index.get.ts b/src/server/routes/metrics/index.get.ts deleted file mode 100644 index 427e84ec..00000000 --- a/src/server/routes/metrics/index.get.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default defineEventHandler(async (event) => { - // TODO: check password - - const prometheus = await Database.metrics.prometheus.get('wg0'); - if (!prometheus) { - throw createError({ - statusCode: 400, - message: 'Prometheus metrics are not enabled', - }); - } - - setHeader(event, 'Content-Type', 'text/plain'); - return getPrometheusResponse(); -}); diff --git a/src/server/routes/metrics/json.get.ts b/src/server/routes/metrics/json.get.ts index d9211241..ff757e8c 100644 --- a/src/server/routes/metrics/json.get.ts +++ b/src/server/routes/metrics/json.get.ts @@ -1,13 +1,35 @@ -export default defineEventHandler(async () => { - // TODO: check password - - const prometheus = await Database.metrics.prometheus.get('wg0'); - if (!prometheus) { - throw createError({ - statusCode: 400, - message: 'Prometheus metrics are not enabled', - }); - } - +export default defineMetricsHandler('json', async () => { return getMetricsJSON(); }); + +async function getMetricsJSON() { + const clients = await WireGuard.getClients(); + let wireguardPeerCount = 0; + let wireguardEnabledPeersCount = 0; + let wireguardConnectedPeersCount = 0; + for (const client of clients) { + wireguardPeerCount++; + if (client.enabled === true) { + wireguardEnabledPeersCount++; + } + if (isPeerConnected(client)) { + wireguardConnectedPeersCount++; + } + } + return { + wireguard_configured_peers: wireguardPeerCount, + wireguard_enabled_peers: wireguardEnabledPeersCount, + wireguard_connected_peers: wireguardConnectedPeersCount, + clients: clients.map((client) => ({ + name: client.name, + enabled: client.enabled, + ipv4Address: client.ipv4Address, + ipv6Address: client.ipv6Address, + publicKey: client.publicKey, + endpoint: client.endpoint, + latestHandshakeAt: client.latestHandshakeAt, + transferRx: client.transferRx, + transferTx: client.transferTx, + })), + }; +} diff --git a/src/server/routes/metrics/prometheus.get.ts b/src/server/routes/metrics/prometheus.get.ts new file mode 100644 index 00000000..762e6663 --- /dev/null +++ b/src/server/routes/metrics/prometheus.get.ts @@ -0,0 +1,67 @@ +export default defineMetricsHandler('prometheus', async ({ event }) => { + setHeader(event, 'Content-Type', 'text/plain'); + return getPrometheusResponse(); +}); + +async function getPrometheusResponse() { + const clients = await WireGuard.getClients(); + let wireguardPeerCount = 0; + let wireguardEnabledPeersCount = 0; + let wireguardConnectedPeersCount = 0; + const wireguardSentBytes = []; + const wireguardReceivedBytes = []; + const wireguardLatestHandshakeSeconds = []; + for (const client of clients) { + wireguardPeerCount++; + if (client.enabled === true) { + wireguardEnabledPeersCount++; + } + + if (isPeerConnected(client)) { + wireguardConnectedPeersCount++; + } + + const id = `interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"`; + + wireguardSentBytes.push( + `wireguard_sent_bytes{${id}} ${client.transferTx ?? 0}` + ); + wireguardReceivedBytes.push( + `wireguard_received_bytes{${id}} ${client.transferRx ?? 0}` + ); + // TODO: if latestHandshakeAt is null this would result in client showing as online? + wireguardLatestHandshakeSeconds.push( + `wireguard_latest_handshake_seconds{${id}} ${client.latestHandshakeAt ? (Date.now() - client.latestHandshakeAt.getTime()) / 1000 : 0}` + ); + } + + const returnText = [ + '# HELP wg-easy and wireguard metrics', + '', + '# HELP wireguard_configured_peers', + '# TYPE wireguard_configured_peers gauge', + `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}`, + '', + '# HELP wireguard_enabled_peers', + '# TYPE wireguard_enabled_peers gauge', + `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}`, + '', + '# HELP wireguard_connected_peers', + '# TYPE wireguard_connected_peers gauge', + `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}`, + '', + '# HELP wireguard_sent_bytes Bytes sent to the peer', + '# TYPE wireguard_sent_bytes counter', + `${wireguardSentBytes.join('\n')}`, + '', + '# HELP wireguard_received_bytes Bytes received from the peer', + '# TYPE wireguard_received_bytes counter', + `${wireguardReceivedBytes.join('\n')}`, + '', + '# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake', + '# TYPE wireguard_latest_handshake_seconds gauge', + `${wireguardLatestHandshakeSeconds.join('\n')}`, + ]; + + return returnText.join('\n'); +} diff --git a/src/server/utils/handler.ts b/src/server/utils/handler.ts index 754bb8d3..bddf2a45 100644 --- a/src/server/utils/handler.ts +++ b/src/server/utils/handler.ts @@ -57,3 +57,63 @@ export const defineSetupEventHandler = < return await handler({ event, setup }); }); }; + +type Metrics = 'prometheus' | 'json'; + +type MetricsHandler< + TReq extends EventHandlerRequest, + TRes extends EventHandlerResponse, +> = { (params: { event: H3Event }): TRes }; + +/** + * check if the metrics are enabled and the token is correct + */ +export const defineMetricsHandler = < + TReq extends EventHandlerRequest, + TRes extends EventHandlerResponse, +>( + type: Metrics, + handler: MetricsHandler +) => { + return defineEventHandler(async (event) => { + const auth = getHeader(event, 'Authorization'); + + if (!auth) { + throw createError({ + statusCode: 401, + statusMessage: 'Unauthorized', + }); + } + + const [method, value] = auth.split(' '); + + if (method !== 'Bearer' || !value) { + throw createError({ + statusCode: 401, + statusMessage: 'Bearer Auth required', + }); + } + + const metricsConfig = await Database.general.getMetricsConfig(); + + if (metricsConfig[type] !== true) { + throw createError({ + statusCode: 400, + statusMessage: 'Metrics not enabled', + }); + } + + if (metricsConfig.password) { + const tokenValid = await isPasswordValid(value, metricsConfig.password); + + if (!tokenValid) { + throw createError({ + statusCode: 401, + statusMessage: 'Incorrect token', + }); + } + } + + return await handler({ event }); + }); +}; diff --git a/src/server/utils/metrics.ts b/src/server/utils/metrics.ts deleted file mode 100644 index 9c007c66..00000000 --- a/src/server/utils/metrics.ts +++ /dev/null @@ -1,74 +0,0 @@ -// TODO: rewrite - -export async function getPrometheusResponse() { - const clients = await WireGuard.getClients(); - let wireguardPeerCount = 0; - let wireguardEnabledPeersCount = 0; - let wireguardConnectedPeersCount = 0; - let wireguardSentBytes = ''; - let wireguardReceivedBytes = ''; - let wireguardLatestHandshakeSeconds = ''; - for (const client of clients) { - wireguardPeerCount++; - if (client.enabled === true) { - wireguardEnabledPeersCount++; - } - if (client.endpoint !== null) { - wireguardConnectedPeersCount++; - } - wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferTx)}\n`; - wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${Number(client.transferRx)}\n`; - wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",ipv4Address="${client.ipv4Address}",ipv6Address="${client.ipv6Address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`; - } - - let returnText = '# HELP wg-easy and wireguard metrics\n'; - - returnText += '\n# HELP wireguard_configured_peers\n'; - returnText += '# TYPE wireguard_configured_peers gauge\n'; - returnText += `wireguard_configured_peers{interface="wg0"} ${wireguardPeerCount}\n`; - - returnText += '\n# HELP wireguard_enabled_peers\n'; - returnText += '# TYPE wireguard_enabled_peers gauge\n'; - returnText += `wireguard_enabled_peers{interface="wg0"} ${wireguardEnabledPeersCount}\n`; - - returnText += '\n# HELP wireguard_connected_peers\n'; - returnText += '# TYPE wireguard_connected_peers gauge\n'; - returnText += `wireguard_connected_peers{interface="wg0"} ${wireguardConnectedPeersCount}\n`; - - returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n'; - returnText += '# TYPE wireguard_sent_bytes counter\n'; - returnText += `${wireguardSentBytes}`; - - returnText += - '\n# HELP wireguard_received_bytes Bytes received from the peer\n'; - returnText += '# TYPE wireguard_received_bytes counter\n'; - returnText += `${wireguardReceivedBytes}`; - - returnText += - '\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n'; - returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n'; - returnText += `${wireguardLatestHandshakeSeconds}`; - - return returnText; -} - -export async function getMetricsJSON() { - const clients = await WireGuard.getClients(); - let wireguardPeerCount = 0; - let wireguardEnabledPeersCount = 0; - let wireguardConnectedPeersCount = 0; - for (const client of clients) { - wireguardPeerCount++; - if (client.enabled === true) { - wireguardEnabledPeersCount++; - } - if (client.endpoint !== null) { - wireguardConnectedPeersCount++; - } - } - return { - wireguard_configured_peers: wireguardPeerCount, - wireguard_enabled_peers: wireguardEnabledPeersCount, - wireguard_connected_peers: wireguardConnectedPeersCount, - }; -} diff --git a/src/shared/utils/time.ts b/src/shared/utils/time.ts new file mode 100644 index 00000000..3d4085e4 --- /dev/null +++ b/src/shared/utils/time.ts @@ -0,0 +1,10 @@ +export function isPeerConnected(client: { latestHandshakeAt: Date | null }) { + if (!client.latestHandshakeAt) { + return false; + } + + const lastHandshakeMs = Date.now() - client.latestHandshakeAt.getTime(); + + // connected if last handshake was less than 10 minutes ago + return lastHandshakeMs < 1000 * 60 * 10; +}