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;
+}