@platformatic/metrics 3.33.0 → 3.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -284,12 +284,12 @@ export async function collectThreadMetrics (applicationId, workerId, metricsConf
284
284
  }
285
285
 
286
286
  if (metricsConfig.httpMetrics && !ensureMetricsGroup(registry, 'http')) {
287
+ // Build custom labels configuration
288
+ const { customLabels, getCustomLabels } = buildCustomLabelsConfig(metricsConfig.httpCustomLabels)
289
+
287
290
  collectHttpMetrics(registry, {
288
- customLabels: ['telemetry_id'],
289
- getCustomLabels: req => {
290
- const telemetryId = req.headers?.['x-plt-telemetry-id'] ?? 'unknown'
291
- return { telemetry_id: telemetryId }
292
- },
291
+ customLabels,
292
+ getCustomLabels,
293
293
  histogram: {
294
294
  name: 'http_request_all_duration_seconds',
295
295
  help: 'request duration in seconds summary for all requests',
@@ -313,6 +313,35 @@ export async function collectThreadMetrics (applicationId, workerId, metricsConf
313
313
  }
314
314
  }
315
315
 
316
+ // Build custom labels configuration from metrics config
317
+ // Returns { customLabels: string[], getCustomLabels: (req) => object }
318
+ export function buildCustomLabelsConfig (customLabelsConfig) {
319
+ // Default: use telemetry_id from x-plt-telemetry-id header
320
+ if (!customLabelsConfig || customLabelsConfig.length === 0) {
321
+ return {
322
+ customLabels: ['telemetry_id'],
323
+ getCustomLabels: req => {
324
+ const telemetryId = req.headers?.['x-plt-telemetry-id'] ?? 'unknown'
325
+ return { telemetry_id: telemetryId }
326
+ }
327
+ }
328
+ }
329
+
330
+ // Build custom labels from configuration
331
+ const customLabels = customLabelsConfig.map(label => label.name)
332
+
333
+ const getCustomLabels = req => {
334
+ const labels = {}
335
+ for (const labelConfig of customLabelsConfig) {
336
+ const headerValue = req.headers?.[labelConfig.header.toLowerCase()]
337
+ labels[labelConfig.name] = headerValue ?? labelConfig.default ?? 'unknown'
338
+ }
339
+ return labels
340
+ }
341
+
342
+ return { customLabels, getCustomLabels }
343
+ }
344
+
316
345
  // Original function for backward compatibility (collects all metrics)
317
346
  export async function collectMetrics (applicationId, workerId, metricsConfig = {}, registry = undefined) {
318
347
  if (!registry) {
@@ -341,12 +370,12 @@ export async function collectMetrics (applicationId, workerId, metricsConfig = {
341
370
  }
342
371
 
343
372
  if (metricsConfig.httpMetrics && !ensureMetricsGroup(registry, 'http')) {
373
+ // Build custom labels configuration
374
+ const { customLabels, getCustomLabels } = buildCustomLabelsConfig(metricsConfig.httpCustomLabels)
375
+
344
376
  collectHttpMetrics(registry, {
345
- customLabels: ['telemetry_id'],
346
- getCustomLabels: req => {
347
- const telemetryId = req.headers?.['x-plt-telemetry-id'] ?? 'unknown'
348
- return { telemetry_id: telemetryId }
349
- },
377
+ customLabels,
378
+ getCustomLabels,
350
379
  histogram: {
351
380
  name: 'http_request_all_duration_seconds',
352
381
  help: 'request duration in seconds summary for all requests',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/metrics",
3
- "version": "3.33.0",
3
+ "version": "3.34.0",
4
4
  "description": "Platformatic Capability Metrics",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert'
2
2
  import { test } from 'node:test'
3
- import { client, collectMetrics } from '../index.js'
3
+ import { buildCustomLabelsConfig, client, collectMetrics } from '../index.js'
4
4
 
5
5
  const nextTick = () => new Promise(resolve => process.nextTick(resolve))
6
6
 
@@ -126,3 +126,115 @@ test('httpMetrics summary resets after metric collection', async () => {
126
126
  assert.strictEqual(sum?.value || 0, 0, 'summary sum should be reset to 0')
127
127
  assert.strictEqual(count?.value || 0, 0, 'summary count should be reset to 0')
128
128
  })
129
+
130
+ // Tests for buildCustomLabelsConfig
131
+ test('buildCustomLabelsConfig returns default telemetry_id when no config provided', () => {
132
+ const result = buildCustomLabelsConfig(undefined)
133
+
134
+ assert.deepStrictEqual(result.customLabels, ['telemetry_id'])
135
+ assert.strictEqual(typeof result.getCustomLabels, 'function')
136
+
137
+ // Test default getCustomLabels function
138
+ const labels = result.getCustomLabels({ headers: { 'x-plt-telemetry-id': 'test-id' } })
139
+ assert.deepStrictEqual(labels, { telemetry_id: 'test-id' })
140
+ })
141
+
142
+ test('buildCustomLabelsConfig returns default telemetry_id when empty array provided', () => {
143
+ const result = buildCustomLabelsConfig([])
144
+
145
+ assert.deepStrictEqual(result.customLabels, ['telemetry_id'])
146
+ })
147
+
148
+ test('buildCustomLabelsConfig uses unknown as default when header is missing', () => {
149
+ const result = buildCustomLabelsConfig(undefined)
150
+
151
+ const labels = result.getCustomLabels({ headers: {} })
152
+ assert.deepStrictEqual(labels, { telemetry_id: 'unknown' })
153
+ })
154
+
155
+ test('buildCustomLabelsConfig builds custom labels from configuration', () => {
156
+ const config = [
157
+ { name: 'domain', header: 'x-forwarded-host' },
158
+ { name: 'api_version', header: 'x-api-version' }
159
+ ]
160
+
161
+ const result = buildCustomLabelsConfig(config)
162
+
163
+ assert.deepStrictEqual(result.customLabels, ['domain', 'api_version'])
164
+ assert.strictEqual(typeof result.getCustomLabels, 'function')
165
+ })
166
+
167
+ test('buildCustomLabelsConfig getCustomLabels extracts values from headers', () => {
168
+ const config = [
169
+ { name: 'domain', header: 'x-forwarded-host' },
170
+ { name: 'api_version', header: 'x-api-version' }
171
+ ]
172
+
173
+ const result = buildCustomLabelsConfig(config)
174
+
175
+ const labels = result.getCustomLabels({
176
+ headers: {
177
+ 'x-forwarded-host': 'example.com',
178
+ 'x-api-version': 'v2'
179
+ }
180
+ })
181
+
182
+ assert.deepStrictEqual(labels, { domain: 'example.com', api_version: 'v2' })
183
+ })
184
+
185
+ test('buildCustomLabelsConfig uses custom default value when header is missing', () => {
186
+ const config = [
187
+ { name: 'domain', header: 'x-forwarded-host', default: 'default-domain' },
188
+ { name: 'api_version', header: 'x-api-version' }
189
+ ]
190
+
191
+ const result = buildCustomLabelsConfig(config)
192
+
193
+ const labels = result.getCustomLabels({ headers: {} })
194
+
195
+ assert.deepStrictEqual(labels, { domain: 'default-domain', api_version: 'unknown' })
196
+ })
197
+
198
+ test('buildCustomLabelsConfig handles case-insensitive header names', () => {
199
+ const config = [
200
+ { name: 'domain', header: 'X-Forwarded-Host' }
201
+ ]
202
+
203
+ const result = buildCustomLabelsConfig(config)
204
+
205
+ // HTTP headers are case-insensitive, and Node.js lowercases them
206
+ const labels = result.getCustomLabels({
207
+ headers: {
208
+ 'x-forwarded-host': 'example.com'
209
+ }
210
+ })
211
+
212
+ assert.deepStrictEqual(labels, { domain: 'example.com' })
213
+ })
214
+
215
+ test('httpMetrics with custom labels configuration', async () => {
216
+ const httpCustomLabels = [
217
+ { name: 'domain', header: 'x-forwarded-host', default: 'localhost' }
218
+ ]
219
+
220
+ const result = await collectMetrics('test-service', 1, {
221
+ httpMetrics: true,
222
+ httpCustomLabels
223
+ })
224
+
225
+ const metrics = await result.registry.getMetricsAsJSON()
226
+ const histogram = metrics.find(m => m.name === 'http_request_all_duration_seconds')
227
+
228
+ assert.ok(histogram, 'histogram metric should exist')
229
+
230
+ // Verify custom label is used by observing a value
231
+ const histogramMetric = result.registry.getSingleMetric('http_request_all_duration_seconds')
232
+ histogramMetric.observe({ method: 'GET', domain: 'example.com' }, 0.1)
233
+
234
+ const metricsAfterObserve = await result.registry.getMetricsAsJSON()
235
+ const histogramAfterObserve = metricsAfterObserve.find(m => m.name === 'http_request_all_duration_seconds')
236
+
237
+ // Check that the domain label is present in the recorded values
238
+ const hasCustomLabel = histogramAfterObserve.values.some(v => v.labels?.domain === 'example.com')
239
+ assert.ok(hasCustomLabel, 'custom domain label should be present in histogram values')
240
+ })