-
Notifications
You must be signed in to change notification settings - Fork 601
feat: Prometheus metrics #860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0a783ad
145db2e
bd23bc0
c73c31f
c44a40c
8e22469
ffcb820
89225ad
83ef628
767c5a1
b61df13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||||||||||||
| /** | ||||||||||||||||
| * Copyright © 2025 STF Metrics Controller - Licensed under the Apache license 2.0 | ||||||||||||||||
| * | ||||||||||||||||
| * Prometheus metrics endpoint controller | ||||||||||||||||
| */ | ||||||||||||||||
|
|
||||||||||||||||
| // Fix for Node.js versions where util.isError was removed | ||||||||||||||||
| const util = require('util') | ||||||||||||||||
| if (!util.isError) { | ||||||||||||||||
| util.isError = function(e) { | ||||||||||||||||
| return e && typeof e === 'object' && e instanceof Error | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+7
to
+14
|
||||||||||||||||
| // Fix for Node.js versions where util.isError was removed | |
| const util = require('util') | |
| if (!util.isError) { | |
| util.isError = function(e) { | |
| return e && typeof e === 'object' && e instanceof Error | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,28 @@ tags: | |
| description: Groups Operations | ||
| - name: admin | ||
| description: Privileged Operations | ||
| - name: metrics | ||
| description: Prometheus Metrics Operations | ||
| paths: | ||
| /metrics: | ||
| x-swagger-router-controller: metrics | ||
| get: | ||
| summary: Get Prometheus metrics | ||
| description: Returns metrics in Prometheus format for monitoring STF system health and usage | ||
|
Comment on lines
+40
to
+44
|
||
| operationId: getMetrics | ||
| tags: | ||
| - metrics | ||
| responses: | ||
| "200": | ||
| description: Prometheus metrics | ||
| schema: | ||
| type: string | ||
| default: | ||
| description: > | ||
| Unexpected Error: | ||
| * 500: Internal Server Error | ||
| schema: | ||
| $ref: "#/definitions/UnexpectedErrorResponse" | ||
| /groups: | ||
| x-swagger-router-controller: groups | ||
| get: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Copyright © 2025 STF Metrics Collector - Licensed under the Apache license 2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Service for collecting STF metrics from database and external sources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const logger = require('./logger') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dbapi = require('../db/api') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const metrics = require('./metrics') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const log = logger.createLogger('metrics-collector') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MetricsCollector { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(options = {}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.interval = options.interval || 30000 // 30 seconds default | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.timer = null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.isRunning = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.isRunning) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info('Starting metrics collection with interval:', this.interval + 'ms') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.isRunning = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.collectMetrics() // Collect immediately | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.timer = setInterval(() => this.collectMetrics(), this.interval) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stop() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.isRunning) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info('Stopping metrics collection') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.isRunning = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.timer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearInterval(this.timer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.timer = null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async collectMetrics() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug('Collecting metrics...') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deviceData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , userData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , groupData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.collectDeviceMetrics() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , this.collectUserMetrics() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , this.collectGroupMetrics() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Update the metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metrics.updateDeviceMetrics(deviceData) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metrics.updateUserMetrics(userData) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metrics.updateGroupMetrics(groupData) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+57
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug('Metrics collection completed') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error('Error during metrics collection:', error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async collectDeviceMetrics() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get device statistics from database using secure aggregation function | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This avoids access control bypass by not exposing individual device data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const deviceStats = await dbapi.getDeviceMetrics() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return deviceStats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error('Error collecting device metrics:', error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total: 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , usable: 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , busy: 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , providers: 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , byStatus: {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async collectUserMetrics() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get user statistics from database | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const users = await dbapi.getUsers() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total: users.length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+87
to
+91
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get user statistics from database | |
| const users = await dbapi.getUsers() | |
| return { | |
| total: users.length | |
| // Get user statistics from database, preferring a DB-side count if available | |
| let total | |
| if (typeof dbapi.getUserCount === 'function') { | |
| // Use optimized aggregation function when provided by dbapi | |
| total = await dbapi.getUserCount() | |
| } | |
| else { | |
| // Fallback to fetching users and counting in memory | |
| const users = await dbapi.getUsers() | |
| total = Array.isArray(users) ? users.length : 0 | |
| } | |
| return { | |
| total: total |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collectGroupMetrics() counts “active” groups via g.state === 'active', but groups use an isActive boolean and state values like 'ready'/'pending'/'waiting'. This will report 0 active groups and mislead dashboards. Count active via g.isActive (and decide how to interpret state vs isActive for the other buckets).
| , active: groups.filter(g => g.state === 'active').length | |
| , active: groups.filter(g => g.isActive).length |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
collectGroupMetrics() similarly loads the entire groups table via dbapi.getGroups() just to compute counts. Consider doing DB-side aggregations (count, group-by state/isActive) to avoid scanning and transferring all group rows every interval.
| const groupStats = { | |
| total: groups.length | |
| , active: groups.filter(g => g.state === 'active').length | |
| , ready: groups.filter(g => g.state === 'ready').length | |
| , pending: groups.filter(g => g.state === 'pending').length | |
| } | |
| const groupStats = { | |
| total: groups.length | |
| , active: 0 | |
| , ready: 0 | |
| , pending: 0 | |
| } | |
| for (const g of groups) { | |
| if (!g || typeof g.state !== 'string') { | |
| continue | |
| } | |
| switch (g.state) { | |
| case 'active': | |
| groupStats.active++ | |
| break | |
| case 'ready': | |
| groupStats.ready++ | |
| break | |
| case 'pending': | |
| groupStats.pending++ | |
| break | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getDeviceMetrics() computes usable/busy by comparing the DB 'status' field to strings 'available'/'busy'. In STF, devices.status is an enum value (ONLINE/OFFLINE/UNAUTHORIZED/…) from wireutil.toDeviceStatus(), so these comparisons will never match and usable/busy will always be 0. Rework the aggregation to use the actual schema (e.g., busy based on owner != null / usable based on present+ready+owner==null, and map enum values to readable label names).