From 1bb80deca93a05dd84475265f5204eab2f952051 Mon Sep 17 00:00:00 2001 From: steunix Date: Mon, 25 Aug 2025 23:50:05 +0200 Subject: [PATCH] Add metrics for each API login Fixes #367 --- api/v1/controllers/login.mjs | 9 ++++++++- docs/index.md | 5 +++-- lib/const.mjs | 1 + lib/metrics.mjs | 3 +++ passweaver-api.mjs | 1 + test/metrics.spec.cjs | 34 ++++++++++++++++++++++++++++++++-- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/api/v1/controllers/login.mjs b/api/v1/controllers/login.mjs index 0821922..328ba21 100644 --- a/api/v1/controllers/login.mjs +++ b/api/v1/controllers/login.mjs @@ -45,6 +45,7 @@ export async function login (req, res, next) { // If an API key is provided, validate it and get the user let isapikey = false + let apikeydescription = '' if (data.apikey && data.secret) { if (!await ApiKey.exists(data.apikey)) { await Events.add(data.username, Const.EV_ACTION_LOGIN_APIKEY_NOTFOUND, Const.EV_ENTITY_APIKEY, data.apikey) @@ -60,8 +61,10 @@ export async function login (req, res, next) { // Search the API key and check if it is active const apik = await DB.apikeys.findUnique({ where: { id: data.apikey }, - select: { userid: true, active: true, ipwhitelist: true, timewhitelist: true } + select: { userid: true, active: true, ipwhitelist: true, timewhitelist: true, description: true } }) + apikeydescription = apik.description + if (!apik.active) { await Events.add(data.username, Const.EV_ACTION_LOGIN_APIKEY_NOTVALID, Const.EV_ENTITY_APIKEY, data.apikey) res.status(R.UNAUTHORIZED).send(R.ko('API key not valid')) @@ -208,7 +211,11 @@ export async function login (req, res, next) { // Creates JWT token const token = await Auth.createToken(user.id, false) + // API key metrics Metrics.counterInc(isapikey ? Const.METRICS_LOGIN_APIKEYS : Const.METRICS_LOGIN_USERS) + if (isapikey) { + Metrics.counterInc(Const.METRICS_LOGIN_APIKEYS_PER_KEY, apikeydescription) + } // Create user tree cache await Folder.userTree(user.id) diff --git a/docs/index.md b/docs/index.md index 033dc17..50f9546 100644 --- a/docs/index.md +++ b/docs/index.md @@ -305,13 +305,14 @@ PassWeaver API logs every HTTP call in a 'combined log format' (the file is name ## Metrics -If enabled in configuration, PassWeaver API export various metrics (along default NodeJS ones) at the `/api/v1/metrics' endpoint: +If enabled in configuration, PassWeaver API export various metrics (along with default NodeJS environment ones) at the `/api/v1/metrics' endpoint: - Users logins count (`login_users_total`) - API keys logins count (`login_apikeys_total`) + - API keys logins per single key (`login_apikeys_per_key_total`) - Item create, update, delete and read count (`items_created_total`, `items_updated_total`, `items_deleted_total`, `items_read_total`) - One time tokens count (`onetimetokens_created_total`, `onetimetokens_read_total`) - KMS encryptions and decryptions count (`kms_encryptions_total`, `kms_decryptions_total`) - - KMS encryptions and descriptions for each KMS + - KMS encryptions and descriptions for each KMS (`kms_encryptions_per_kms_total`, `kms_decryptions_per_kms_total`) ## Cache diff --git a/lib/const.mjs b/lib/const.mjs index 4605def..a957024 100644 --- a/lib/const.mjs +++ b/lib/const.mjs @@ -122,3 +122,4 @@ export const METRICS_KMS_ENCRYPTIONS = 'kms_encryptions_total' export const METRICS_KMS_DECRYPTIONS = 'kms_decryptions_total' export const METRICS_KMS_ENCRYPTIONS_PER_KMS = 'kms_encryptions_per_kms_total' export const METRICS_KMS_DECRYPTIONS_PER_KMS = 'kms_decryptions_per_kms_total' +export const METRICS_LOGIN_APIKEYS_PER_KEY = 'login_apikeys_per_key_total' diff --git a/lib/metrics.mjs b/lib/metrics.mjs index 42eef41..cc10721 100644 --- a/lib/metrics.mjs +++ b/lib/metrics.mjs @@ -16,6 +16,9 @@ let enabled = false * Initialize module */ export function init () { + if (enabled) { + return + } PromClient.collectDefaultMetrics() enabled = true } diff --git a/passweaver-api.mjs b/passweaver-api.mjs index ef507da..2243ef5 100644 --- a/passweaver-api.mjs +++ b/passweaver-api.mjs @@ -135,6 +135,7 @@ if (cfg?.enable_metrics) { Metrics.createCounter(Const.METRICS_KMS_DECRYPTIONS, 'Decryptions') Metrics.createCounter(Const.METRICS_KMS_ENCRYPTIONS_PER_KMS, 'Encryptions per KMS', 'kms_description') Metrics.createCounter(Const.METRICS_KMS_DECRYPTIONS_PER_KMS, 'Decryptions per KMS', 'kms_description') + Metrics.createCounter(Const.METRICS_LOGIN_APIKEYS_PER_KEY, 'Login per API key', 'apikey_description') } // HTTP(S) server start diff --git a/test/metrics.spec.cjs b/test/metrics.spec.cjs index 74ee1cc..2641a0f 100644 --- a/test/metrics.spec.cjs +++ b/test/metrics.spec.cjs @@ -12,7 +12,7 @@ describe('Metrics', function () { assert.match(res1.text, /.+/) }) - it('Check per-KMS metrics are included', async () => { + it('Check global metrics', async () => { const res1 = await agent .get(`${global.host}/api/v1/metrics`) .catch(v => v) @@ -32,9 +32,10 @@ describe('Metrics', function () { assert.match(res1.text, /kms_decryptions_total/) assert.match(res1.text, /kms_encryptions_per_kms_total/) assert.match(res1.text, /kms_decryptions_per_kms_total/) + assert.match(res1.text, /login_apikeys_per_key_total/) }) - it('Should create labeled counters for per-KMS metrics', async () => { + it('Check per-KMS metrics', async () => { // This test verifies that the metrics module can create and use labeled counters // for per-KMS tracking without requiring the full application setup @@ -61,4 +62,33 @@ describe('Metrics', function () { assert.match(metricsOutput, /kms_description="Test Local File KMS"/, 'Should have kms_description label for local file') assert.match(metricsOutput, /kms_description="Test Google Cloud KMS"/, 'Should have kms_description label for google cloud') }) + + it('Check per-API metrics', async () => { + // This test verifies that the metrics module can create and use labeled counters + // for per-API key tracking without requiring the full application setup + + const { init, createCounter, counterInc, output } = await import('../lib/metrics.mjs') + const { METRICS_LOGIN_APIKEYS_PER_KEY } = await import('../lib/const.mjs') + + // Initialize metrics + init() + + // Create the per-API key counter with single label + createCounter(METRICS_LOGIN_APIKEYS_PER_KEY, 'Login per API key', 'apikey_description') + + // Increment counters with different API key descriptions + counterInc(METRICS_LOGIN_APIKEYS_PER_KEY, 'Test API Key 1') + counterInc(METRICS_LOGIN_APIKEYS_PER_KEY, 'Test API Key 2') + counterInc(METRICS_LOGIN_APIKEYS_PER_KEY, 'Test API Key 1') // Increment again to test counting + + // Get metrics output + const metricsOutput = await output() + + // Verify the metrics exist and have the correct labels and counts + assert.match(metricsOutput, /login_apikeys_per_key_total/, 'Per-API key login metric should exist') + assert.match(metricsOutput, /apikey_description="Test API Key 1"/, 'Should have apikey_description label for Test API Key 1') + assert.match(metricsOutput, /apikey_description="Test API Key 2"/, 'Should have apikey_description label for Test API Key 2') + assert.match(metricsOutput, /login_apikeys_per_key_total{apikey_description="Test API Key 1"} 2/, 'Test API Key 1 should have count of 2') + assert.match(metricsOutput, /login_apikeys_per_key_total{apikey_description="Test API Key 2"} 1/, 'Test API Key 2 should have count of 1') + }) })