-
Notifications
You must be signed in to change notification settings - Fork 607
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 3 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 | ||
| } | ||
| } | ||
|
|
||
| const metrics = require('../../../util/metrics') | ||
| const logger = require('../../../util/logger') | ||
| const log = logger.createLogger('api:controllers:metrics') | ||
|
|
||
| /** | ||
| * GET /metrics | ||
| * | ||
| * Returns Prometheus metrics in the expected format | ||
| * @param {Object} req - Express request object | ||
| * @param {Object} res - Express response object | ||
| * @returns {void} | ||
| */ | ||
| function getMetrics(req, res) { | ||
| // Set the content type to plain text as expected by Prometheus | ||
| res.set('Content-Type', metrics.register.contentType) | ||
|
|
||
| // Return the metrics (handle Promise from prom-client v15+) | ||
| metrics.register.metrics() | ||
| .then(metricsData => { | ||
| res.end(metricsData) | ||
| log.debug('Served Prometheus metrics') | ||
| }) | ||
| .catch(error => { | ||
| log.error('Error serving metrics:', error) | ||
| res.status(500).json({ | ||
| success: false | ||
| , description: 'Internal server error while fetching metrics' | ||
| }) | ||
| }) | ||
|
matanbaruch marked this conversation as resolved.
|
||
| } | ||
|
|
||
| module.exports = { | ||
| getMetrics | ||
| } | ||
| 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,162 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const devices = await dbapi.getDevices() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const deviceStats = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total: devices.length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , usable: devices.filter(d => d.status === 'available' || d.status === 'busy').length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , busy: devices.filter(d => d.status === 'busy').length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , providers: new Set(devices.map(d => d.provider && d.provider.name)).size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
matanbaruch marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| , byStatus: {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Count devices by status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| devices.forEach(device => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = device.status || 'unknown' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deviceStats.byStatus[status] = (deviceStats.byStatus[status] || 0) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.
This controller mutates Node’s built-in util module by polyfilling util.isError, but nothing in this repo uses util.isError. Global monkey-patches like this are hard to reason about and can have unexpected side effects; please remove it unless a concrete dependency requires it.