Skip to content

Commit d3183df

Browse files
Copilotsteunix
andauthored
Add KMS counters per single KMS with description labels only (#305)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: steunix <[email protected]> Co-authored-by: Stefano Rivoir <[email protected]>
1 parent 1bdab29 commit d3183df

File tree

9 files changed

+141
-13
lines changed

9 files changed

+141
-13
lines changed

lib/const.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,5 @@ export const METRICS_ITEMS_UPDATED = 'items_updated_total'
111111
export const METRICS_ITEMS_DELETED = 'items_deleted_total'
112112
export const METRICS_KMS_ENCRYPTIONS = 'kms_encryptions_total'
113113
export const METRICS_KMS_DECRYPTIONS = 'kms_decryptions_total'
114+
export const METRICS_KMS_ENCRYPTIONS_PER_KMS = 'kms_encryptions_per_kms_total'
115+
export const METRICS_KMS_DECRYPTIONS_PER_KMS = 'kms_decryptions_per_kms_total'

lib/kms/kms-google.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ export class KMSGoogleCloud {
2121
#config = {}
2222
kmsclient = null
2323
#keyname = ''
24+
#description = ''
2425

2526
/**
2627
* Constructor
2728
* @param {String} id KMS id
2829
* @param {String} config Configuration string
30+
* @param {String} description KMS description
2931
*/
30-
constructor (id, config) {
32+
constructor (id, config, description) {
3133
this.#kmsid = id
3234
this.#config = JSON.parse(config)
35+
this.#description = description
3336
}
3437

3538
/**
@@ -144,4 +147,12 @@ export class KMSGoogleCloud {
144147

145148
return decrypted
146149
}
150+
151+
/**
152+
* Get KMS description
153+
* @returns {String} KMS description
154+
*/
155+
getDescription () {
156+
return this.#description
157+
}
147158
}

lib/kms/kms-localfile.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@ export class KMSLocalFile {
2020
#masterKeyPath = ''
2121
#masterKey = ''
2222
#type = Const.KMS_TYPE_LOCALFILE
23+
#description = ''
2324

2425
/**
2526
* Constructor
2627
* @param {String} id KMS id
2728
* @param {String} config Configuration string
29+
* @param {String} description KMS description
2830
*/
29-
constructor (id, config) {
31+
constructor (id, config, description) {
3032
this.#kmsid = id
3133
this.#masterKeyPath = JSON.parse(config).master_key_path
34+
this.#description = description
3235
}
3336

3437
/**
@@ -148,4 +151,12 @@ export class KMSLocalFile {
148151

149152
return decrypted
150153
}
154+
155+
/**
156+
* Get KMS description
157+
* @returns {String} KMS description
158+
*/
159+
getDescription () {
160+
return this.#description
161+
}
151162
}

lib/kms/kms-nodek.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,18 @@ export class KMSNoDEK {
1616
#kmsid = ''
1717
#masterKey = ''
1818
#type = Const.KMS_TYPE_NODEK
19+
#description = ''
1920

2021
/**
2122
* Constructor
2223
* @param {String} id KMS id
2324
* @param {String} config Configuration string
25+
* @param {String} description KMS description
2426
*/
25-
constructor (id, config) {
27+
constructor (id, config, description) {
2628
this.#kmsid = id
2729
this.#masterKey = Config.get().master_key
30+
this.#description = description
2831
}
2932

3033
/**
@@ -104,4 +107,12 @@ export class KMSNoDEK {
104107
encrypted: null
105108
}
106109
}
110+
111+
/**
112+
* Get KMS description
113+
* @returns {String} KMS description
114+
*/
115+
getDescription () {
116+
return this.#description
117+
}
107118
}

lib/kms/kms.mjs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,30 @@ async function getKMS (kmsid) {
3131
if (kmsid === 'none') {
3232
reckms = {
3333
type: Const.KMS_TYPE_NODEK,
34-
config: null
34+
config: null,
35+
description: 'No DEK'
3536
}
3637
} else {
3738
reckms = await DB.kms.findUnique({
3839
where: { id: kmsid },
3940
select: {
4041
type: true,
41-
config: true
42+
config: true,
43+
description: true
4244
}
4345
})
4446
}
4547

4648
switch (reckms.type) {
4749
case Const.KMS_TYPE_LOCALFILE:
48-
Wallet[kmsid] = new KMSLocalFile(kmsid, reckms.config)
50+
Wallet[kmsid] = new KMSLocalFile(kmsid, reckms.config, reckms.description)
4951
break
5052
case Const.KMS_TYPE_GOOGLECLOUD:
51-
Wallet[kmsid] = new KMSGoogleCloud(kmsid, reckms.config)
53+
Wallet[kmsid] = new KMSGoogleCloud(kmsid, reckms.config, reckms.description)
5254
break
5355
default:
5456
// Defaults to no DEK
55-
Wallet[kmsid] = new KMSNoDEK(null, null)
57+
Wallet[kmsid] = new KMSNoDEK(null, null, reckms.description)
5658
}
5759
await Wallet[kmsid].init()
5860
kms = Wallet[kmsid]
@@ -82,7 +84,9 @@ export async function encrypt (data, algorithm) {
8284
// Call KMS to encrypt
8385
const ret = await kms.encrypt(data, algorithm)
8486

87+
// Increment both global and per-KMS counters
8588
Metrics.counterInc(Const.METRICS_KMS_ENCRYPTIONS)
89+
Metrics.counterInc(Const.METRICS_KMS_ENCRYPTIONS_PER_KMS, kms.getDescription() || 'Unknown')
8690
return ret
8791
}
8892

@@ -102,6 +106,8 @@ export async function decrypt (kmsid, dek, data, iv, authTag, algorithm) {
102106
// Call KMS to decrypt
103107
const ret = await kms.decrypt(dek, data, iv, authTag, algorithm)
104108

109+
// Increment both global and per-KMS counters
105110
Metrics.counterInc(Const.METRICS_KMS_DECRYPTIONS)
111+
Metrics.counterInc(Const.METRICS_KMS_DECRYPTIONS_PER_KMS, kms.getDescription() || 'Unknown')
106112
return ret
107113
}

lib/metrics.mjs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,64 @@ export async function output () {
3636
* Create a counter
3737
* @param {string} name Counter name
3838
* @param {string} help Counter help
39-
* @param {string} label Counter label
39+
* @param {string} label Counter label (for single label use)
40+
* @param {string[]} labelNames Array of label names for Prometheus labels
4041
*/
41-
export function createCounter (name, help, label) {
42+
export function createCounter (name, help, label, labelNames) {
4243
if (!enabled) {
4344
return false
4445
}
45-
counters[`${name}/${label || ''}`] = new PromClient.Counter({ name, help })
46+
const key = `${name}/${label || ''}`
47+
if (labelNames && labelNames.length > 0) {
48+
counters[key] = new PromClient.Counter({ name, help, labelNames })
49+
} else if (label && label.length > 0) {
50+
// Support for single label
51+
counters[key] = new PromClient.Counter({ name, help, labelNames: [label] })
52+
} else {
53+
counters[key] = new PromClient.Counter({ name, help })
54+
}
4655
}
4756

4857
/**
4958
* Increments counter
5059
* @param {string} name Counter name
51-
* @param {string} label Counter label
60+
* @param {string} label Counter label value (for single label counters)
5261
*/
5362
export function counterInc (name, label) {
5463
if (!enabled) {
5564
return false
5665
}
57-
counters[`${name}/${label || ''}`].inc()
66+
67+
// First, try to find a counter with no label (old style)
68+
let key = `${name}/`
69+
let counter = counters[key]
70+
71+
if (!counter) {
72+
// Look for counters with single labels
73+
for (const counterKey in counters) {
74+
if (counterKey.startsWith(`${name}/`) && counterKey !== `${name}/`) {
75+
counter = counters[counterKey]
76+
key = counterKey
77+
break
78+
}
79+
}
80+
}
81+
82+
if (!counter) {
83+
return false
84+
}
85+
86+
if (label && label.length > 0) {
87+
// Support for single label value
88+
const labelName = key.split('/')[1] // Extract label name from key
89+
if (labelName) {
90+
const labelObject = {}
91+
labelObject[labelName] = label
92+
counter.inc(labelObject)
93+
} else {
94+
counter.inc()
95+
}
96+
} else {
97+
counter.inc()
98+
}
5899
}

passweaver-api.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ if (cfg?.enable_metrics) {
131131
Metrics.createCounter(Const.METRICS_ITEMS_READ, 'Read items')
132132
Metrics.createCounter(Const.METRICS_KMS_ENCRYPTIONS, 'Encryptions')
133133
Metrics.createCounter(Const.METRICS_KMS_DECRYPTIONS, 'Decryptions')
134+
Metrics.createCounter(Const.METRICS_KMS_ENCRYPTIONS_PER_KMS, 'Encryptions per KMS', 'kms_description')
135+
Metrics.createCounter(Const.METRICS_KMS_DECRYPTIONS_PER_KMS, 'Decryptions per KMS', 'kms_description')
134136
}
135137

136138
// HTTP(S) server start

test/kms-metrics.spec.cjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* global describe, it, assert */
2+
require('./common.cjs')
3+
4+
// Test for the new per-KMS metrics functionality
5+
describe('KMS Per-KMS Metrics', function () {
6+
it('Should create labeled counters for per-KMS metrics', async () => {
7+
// This test verifies that the metrics module can create and use labeled counters
8+
// for per-KMS tracking without requiring the full application setup
9+
10+
const { init, createCounter, counterInc, output } = await import('../lib/metrics.mjs')
11+
const { METRICS_KMS_ENCRYPTIONS_PER_KMS, METRICS_KMS_DECRYPTIONS_PER_KMS } = await import('../lib/const.mjs')
12+
13+
// Initialize metrics
14+
init()
15+
16+
// Create the per-KMS counters with single label
17+
createCounter(METRICS_KMS_ENCRYPTIONS_PER_KMS, 'Encryptions per KMS', 'kms_description')
18+
createCounter(METRICS_KMS_DECRYPTIONS_PER_KMS, 'Decryptions per KMS', 'kms_description')
19+
20+
// Increment counters with different KMS descriptions
21+
counterInc(METRICS_KMS_ENCRYPTIONS_PER_KMS, 'Test Local File KMS')
22+
counterInc(METRICS_KMS_DECRYPTIONS_PER_KMS, 'Test Google Cloud KMS')
23+
24+
// Get metrics output
25+
const metricsOutput = await output()
26+
27+
// Verify the metrics exist and have the correct labels
28+
assert.match(metricsOutput, /kms_encryptions_per_kms_total/, 'Per-KMS encryption metric should exist')
29+
assert.match(metricsOutput, /kms_decryptions_per_kms_total/, 'Per-KMS decryption metric should exist')
30+
assert.match(metricsOutput, /kms_description="Test Local File KMS"/, 'Should have kms_description label for local file')
31+
assert.match(metricsOutput, /kms_description="Test Google Cloud KMS"/, 'Should have kms_description label for google cloud')
32+
})
33+
})

test/metrics.spec.cjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,15 @@ describe('Metrics', function () {
1111
assert.strictEqual(res1.status, 200)
1212
assert.match(res1.text, /.+/)
1313
})
14+
15+
it('Check per-KMS metrics are included', async () => {
16+
const res1 = await agent
17+
.get(`${global.host}/api/v1/metrics`)
18+
.catch(v => v)
19+
20+
assert.strictEqual(res1.status, 200)
21+
// Check that the new per-KMS metrics are included in the output
22+
assert.match(res1.text, /kms_encryptions_per_kms_total/)
23+
assert.match(res1.text, /kms_decryptions_per_kms_total/)
24+
})
1425
})

0 commit comments

Comments
 (0)