Skip to content
Merged
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
9 changes: 8 additions & 1 deletion api/v1/controllers/login.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'))
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/const.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
3 changes: 3 additions & 0 deletions lib/metrics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ let enabled = false
* Initialize module
*/
export function init () {
if (enabled) {
return
}
PromClient.collectDefaultMetrics()
enabled = true
}
Expand Down
1 change: 1 addition & 0 deletions passweaver-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 32 additions & 2 deletions test/metrics.spec.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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')
})
})