From 0a783ad566b2c9a6c40b36d5d4dda7a09bf63016 Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Sat, 24 May 2025 18:43:17 +0300 Subject: [PATCH 1/8] feat: Implement Prometheus metrics collection and update hooks for real-time metrics tracking Signed-off-by: matanbaruch --- lib/units/api/controllers/metrics.js | 48 ++++++ lib/units/api/index.js | 12 +- lib/units/api/swagger/api_v1.yaml | 21 +++ lib/util/metrics-collector.js | 162 ++++++++++++++++++++ lib/util/metrics-hooks.js | 111 ++++++++++++++ lib/util/metrics.js | 161 +++++++++++++++++++ package.json | 1 + res/app/group-list/group-list-controller.js | 10 ++ 8 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 lib/units/api/controllers/metrics.js create mode 100644 lib/util/metrics-collector.js create mode 100644 lib/util/metrics-hooks.js create mode 100644 lib/util/metrics.js diff --git a/lib/units/api/controllers/metrics.js b/lib/units/api/controllers/metrics.js new file mode 100644 index 0000000000..50155d25d2 --- /dev/null +++ b/lib/units/api/controllers/metrics.js @@ -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' + }) + }) +} + +module.exports = { + getMetrics +} diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 78357fef92..5f57c4a3c5 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -18,6 +18,7 @@ var zmqutil = require('../../util/zmqutil') var srv = require('../../util/srv') var lifecycle = require('../../util/lifecycle') var wireutil = require('../../wire/util') +var MetricsCollector = require('../../util/metrics-collector') module.exports = function(options) { var log = logger.createLogger('api') @@ -138,8 +139,17 @@ module.exports = function(options) { app.disable('x-powered-by') + // Initialize metrics collection + const metricsCollector = new MetricsCollector({ + interval: 30000 // Collect metrics every 30 seconds + }) + metricsCollector.start() + lifecycle.observe(function() { - [push, sub, pushdev, subdev].forEach(function(sock) { + // Stop metrics collection on shutdown + metricsCollector.stop() + + ;[push, sub, pushdev, subdev].forEach(function(sock) { try { sock.close() } diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 7dc2b8b879..62d3dfad88 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -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 + 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: diff --git a/lib/util/metrics-collector.js b/lib/util/metrics-collector.js new file mode 100644 index 0000000000..30b4597903 --- /dev/null +++ b/lib/util/metrics-collector.js @@ -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) + + 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, + 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 + } + } + catch (error) { + log.error('Error collecting user metrics:', error) + return { + total: 0 + } + } + } + + async collectGroupMetrics() { + try { + // Get group statistics from database + const groups = await dbapi.getGroups() + + 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 + } + + return groupStats + } + catch (error) { + log.error('Error collecting group metrics:', error) + return { + total: 0, + active: 0, + ready: 0, + pending: 0 + } + } + } + + // Method to collect quota metrics for a specific user + async collectUserQuotaMetrics(user) { + try { + // This would depend on how quotas are implemented in STF + // For now, return placeholder data + const quotaTypes = ['devices', 'duration'] + + quotaTypes.forEach(quotaType => { + // Example: Get quota usage from database + const consumed = 0 // Would be actual consumed amount + const allocated = 10 // Would be actual allocated amount + + metrics.updateUserQuota(user, quotaType, consumed, allocated) + }) + } + catch (error) { + log.error('Error collecting user quota metrics:', error) + } + } +} + +module.exports = MetricsCollector diff --git a/lib/util/metrics-hooks.js b/lib/util/metrics-hooks.js new file mode 100644 index 0000000000..1db8daba26 --- /dev/null +++ b/lib/util/metrics-hooks.js @@ -0,0 +1,111 @@ +/** + * Copyright © 2025 STF Metrics Hooks - Licensed under the Apache license 2.0 + * + * Hooks to update metrics in real-time when entities change + */ + +const metrics = require('./metrics') +const logger = require('./logger') +const log = logger.createLogger('metrics-hooks') + +class MetricsHooks { + static onDeviceAdded() { + // Increment total devices counter + metrics.metrics.totalDevices.inc() + log.debug('Device added') + } + + static onDeviceRemoved() { + // Decrement total devices counter + metrics.metrics.totalDevices.dec() + log.debug('Device removed') + } + + static onDeviceStatusChanged(device) { + // Update device status metrics + try { + let status = 'offline' + if (device.present) { + status = device.owner ? 'busy' : 'available' + } + + // This is a simple approximation - in a real implementation + // you'd want to track the previous status and update accordingly + log.debug('Device status changed:', device.serial, 'status:', status) + } + catch (error) { + log.error('Error updating device status metrics:', error) + } + } + + static onUserAdded() { + // Increment total users counter + metrics.metrics.totalUsers.inc() + log.debug('User added') + } + + static onUserRemoved() { + // Decrement total users counter + metrics.metrics.totalUsers.dec() + log.debug('User removed') + } + + static onGroupAdded() { + // Increment total groups counter + metrics.metrics.totalGroups.inc() + log.debug('Group added') + } + + static onGroupRemoved() { + // Decrement total groups counter + metrics.metrics.totalGroups.dec() + log.debug('Group removed') + } + + static onGroupStatusChanged(group, oldStatus, newStatus) { + // Update group status metrics + try { + if (oldStatus !== newStatus) { + // Decrement old status + if (oldStatus === 'active') { + metrics.metrics.activeGroups.dec() + } + else if (oldStatus === 'ready') { + metrics.metrics.readyGroups.dec() + } + else if (oldStatus === 'pending') { + metrics.metrics.pendingGroups.dec() + } + + // Increment new status + if (newStatus === 'active') { + metrics.metrics.activeGroups.inc() + } + else if (newStatus === 'ready') { + metrics.metrics.readyGroups.inc() + } + else if (newStatus === 'pending') { + metrics.metrics.pendingGroups.inc() + } + + log.debug('Group status changed:', group.id, 'from:', oldStatus, 'to:', newStatus) + } + } + catch (error) { + log.error('Error updating group status metrics:', error) + } + } + + static updateUserQuota(userEmail, quotaType, consumed, allocated) { + // Update user quota metrics + try { + metrics.updateUserQuota(userEmail, quotaType, consumed, allocated) + log.debug('User quota updated:', userEmail, quotaType, consumed, '/', allocated) + } + catch (error) { + log.error('Error updating user quota metrics:', error) + } + } +} + +module.exports = MetricsHooks diff --git a/lib/util/metrics.js b/lib/util/metrics.js new file mode 100644 index 0000000000..bc9681173a --- /dev/null +++ b/lib/util/metrics.js @@ -0,0 +1,161 @@ +/** + * Copyright © 2025 STF Metrics Module - Licensed under the Apache license 2.0 + * + * Prometheus metrics collection for STF (Smartphone Test Farm) + */ + +const client = require('prom-client') +const logger = require('./logger') + +const log = logger.createLogger('metrics') + +// Create a Registry which registers the metrics +const register = new client.Registry() + +// Add a default label which is added to all metrics +register.setDefaultLabels({ + app: 'stf' +}) + +// Enable the collection of default metrics +client.collectDefaultMetrics({register}) + +// Define custom metrics +const metrics = { + // Device related metrics + totalDevices: new client.Gauge({ + name: 'stf_devices_total' + , help: 'Total number of devices in the system' + , registers: [register] + }) + + , totalProviders: new client.Gauge({ + name: 'stf_providers_total' + , help: 'Total number of device providers' + , registers: [register] + }) + + , usableDevices: new client.Gauge({ + name: 'stf_devices_usable' + , help: 'Number of devices available for use' + , registers: [register] + }) + + , busyDevices: new client.Gauge({ + name: 'stf_devices_busy' + , help: 'Number of devices currently in use' + , registers: [register] + }) + + // User related metrics + , totalUsers: new client.Gauge({ + name: 'stf_users_total' + , help: 'Total number of users in the system' + , registers: [register] + }) + + // Group related metrics + , totalGroups: new client.Gauge({ + name: 'stf_groups_total' + , help: 'Total number of groups in the system' + , registers: [register] + }) + + , activeGroups: new client.Gauge({ + name: 'stf_groups_active' + , help: 'Number of active groups' + , registers: [register] + }) + + , readyGroups: new client.Gauge({ + name: 'stf_groups_ready' + , help: 'Number of ready groups' + , registers: [register] + }) + + , pendingGroups: new client.Gauge({ + name: 'stf_groups_pending' + , help: 'Number of pending groups' + , registers: [register] + }) + + // Additional operational metrics + , devicesByStatus: new client.Gauge({ + name: 'stf_devices_by_status' + , help: 'Number of devices by status' + , labelNames: ['status'] + , registers: [register] + }) + + , userQuotaUsage: new client.Gauge({ + name: 'stf_user_quota_usage_percent' + , help: 'User quota usage percentage' + , labelNames: ['user', 'quota_type'] + , registers: [register] + }) +} + +// Helper functions to update metrics +function updateDeviceMetrics(deviceData) { + if (typeof deviceData.total === 'number') { + metrics.totalDevices.set(deviceData.total) + } + if (typeof deviceData.usable === 'number') { + metrics.usableDevices.set(deviceData.usable) + } + if (typeof deviceData.busy === 'number') { + metrics.busyDevices.set(deviceData.busy) + } + if (typeof deviceData.providers === 'number') { + metrics.totalProviders.set(deviceData.providers) + } + if (deviceData.byStatus) { + Object.keys(deviceData.byStatus).forEach(status => { + metrics.devicesByStatus.set({status}, deviceData.byStatus[status]) + }) + } + log.debug('Updated device metrics', deviceData) +} + +function updateUserMetrics(userData) { + if (typeof userData.total === 'number') { + metrics.totalUsers.set(userData.total) + } + log.debug('Updated user metrics', userData) +} + +function updateGroupMetrics(groupData) { + if (typeof groupData.total === 'number') { + metrics.totalGroups.set(groupData.total) + } + if (typeof groupData.active === 'number') { + metrics.activeGroups.set(groupData.active) + } + if (typeof groupData.ready === 'number') { + metrics.readyGroups.set(groupData.ready) + } + if (typeof groupData.pending === 'number') { + metrics.pendingGroups.set(groupData.pending) + } + log.debug('Updated group metrics', groupData) +} + +function updateUserQuota(user, quotaType, consumed, allocated) { + if (allocated > 0) { + const percentage = (consumed / allocated) * 100 + metrics.userQuotaUsage.set({user, quota_type: quotaType}, percentage) + } +} + +// Export the register and helper functions +module.exports = { + register + , metrics + , updateDeviceMetrics + , updateUserMetrics + , updateGroupMetrics + , updateUserQuota + + // Export the prom-client for advanced usage + , client +} diff --git a/package.json b/package.json index a0585fd160..430d926ef9 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "openid": "^2.0.1", "passport": "^0.6.0", "passport-oauth2": "^1.1.2", + "prom-client": "^15.1.3", "protobufjs": "^5.0.3", "proxy-addr": "^2.0.7", "pug": "^3.0.3", diff --git a/res/app/group-list/group-list-controller.js b/res/app/group-list/group-list-controller.js index a9fc7b4e6a..754d5f00dd 100644 --- a/res/app/group-list/group-list-controller.js +++ b/res/app/group-list/group-list-controller.js @@ -64,6 +64,16 @@ module.exports = function GroupListCtrl( incrStateStats(oldGroup, -1) } } + + // Send group metrics to backend if available (for metrics collection) + if (typeof window !== 'undefined' && window.stfMetrics) { + window.stfMetrics.updateGroupMetrics({ + total: $scope.groups.length, + active: $scope.activeGroups, + ready: $scope.readyGroups, + pending: $scope.pendingGroups + }) + } } function updateGroupExtraProperties(group) { From 145db2ef353a1c581d81a1b6daacb86cbde0022b Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Sat, 24 May 2025 18:48:46 +0300 Subject: [PATCH 2/8] fix: Update ESLint configuration for better compatibility and improve metrics collector code formatting Signed-off-by: matanbaruch --- .eslintrc | 7 ++--- lib/util/metrics-collector.js | 50 +++++++++++++++++------------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/.eslintrc b/.eslintrc index 1f553887a0..f06f25db9c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,11 @@ { "extends": "eslint:recommended", "env": { - "node": true + "node": true, + "es6": true }, "parserOptions": { - "ecmaVersion": 2015 + "ecmaVersion": 2017 }, "rules": { // Possible errors @@ -153,7 +154,7 @@ "no-path-concat": 2, // `2` is default "no-process-exit": 0, // `2` is default "no-restricted-modules": 0, // no default, optionally set `[2, "fs", "os"]` - "no-sync": 1, // `2` is default + "no-sync": 1 // `2` is default // eslint v2 //"keyword-spacing": 2 diff --git a/lib/util/metrics-collector.js b/lib/util/metrics-collector.js index 30b4597903..4cf067db86 100644 --- a/lib/util/metrics-collector.js +++ b/lib/util/metrics-collector.js @@ -42,13 +42,13 @@ class MetricsCollector { log.debug('Collecting metrics...') const [ - deviceData, - userData, - groupData + deviceData + , userData + , groupData ] = await Promise.all([ - this.collectDeviceMetrics(), - this.collectUserMetrics(), - this.collectGroupMetrics() + this.collectDeviceMetrics() + , this.collectUserMetrics() + , this.collectGroupMetrics() ]) // Update the metrics @@ -69,11 +69,11 @@ class MetricsCollector { 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, - byStatus: {} + 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 + , byStatus: {} } // Count devices by status @@ -87,11 +87,11 @@ class MetricsCollector { catch (error) { log.error('Error collecting device metrics:', error) return { - total: 0, - usable: 0, - busy: 0, - providers: 0, - byStatus: {} + total: 0 + , usable: 0 + , busy: 0 + , providers: 0 + , byStatus: {} } } } @@ -119,10 +119,10 @@ class MetricsCollector { const groups = await dbapi.getGroups() 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 + 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 } return groupStats @@ -130,10 +130,10 @@ class MetricsCollector { catch (error) { log.error('Error collecting group metrics:', error) return { - total: 0, - active: 0, - ready: 0, - pending: 0 + total: 0 + , active: 0 + , ready: 0 + , pending: 0 } } } @@ -147,7 +147,7 @@ class MetricsCollector { quotaTypes.forEach(quotaType => { // Example: Get quota usage from database - const consumed = 0 // Would be actual consumed amount + const consumed = 0 // Would be actual consumed amount const allocated = 10 // Would be actual allocated amount metrics.updateUserQuota(user, quotaType, consumed, allocated) From bd23bc0db66f15a50e351063b764bd6e28bfcb43 Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Sat, 24 May 2025 18:51:24 +0300 Subject: [PATCH 3/8] feat: Expose getDevices function in dbapi module Signed-off-by: matanbaruch --- lib/db/api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/db/api.js b/lib/db/api.js index 9a89df93b0..5eed290b15 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -1661,4 +1661,6 @@ dbapi.loadAccessToken = function(id) { return db.run(r.table('accessTokens').get(id)) } +dbapi.getDevices = getDevices + module.exports = dbapi From c73c31f5326789d5fd9f834b7e062807cc6703d1 Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Mon, 26 May 2025 00:33:13 +0300 Subject: [PATCH 4/8] feat: Add secure device metrics aggregation function and update metrics collector Signed-off-by: matanbaruch --- lib/db/api.js | 53 ++++++++++++++++++++++++++++++++++- lib/util/metrics-collector.js | 20 ++----------- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/lib/db/api.js b/lib/db/api.js index 5eed290b15..e485a6c3d8 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -17,6 +17,7 @@ const _ = require('lodash') const logger = require('../util/logger') const log = logger.createLogger('db:api') +// Internal function for bootstrap/migration - not exported to prevent access control bypass function getDevices() { return db.run(r.table('devices')) .then(function(cursor) { @@ -1661,6 +1662,56 @@ dbapi.loadAccessToken = function(id) { return db.run(r.table('accessTokens').get(id)) } -dbapi.getDevices = getDevices +// Metrics-specific function that provides aggregate device statistics +// without exposing individual device data or bypassing access control +dbapi.getDeviceMetrics = function() { + return Promise.all([ + // Get total device count + db.run(r.table('devices').count()) + // Get device counts by status + , db.run(r.table('devices').groupBy('status').count().ungroup()) + // Get provider count (line split to meet max length) + , db.run(r.table('devices').hasFields('provider') + .map(r.row('provider')('name')).distinct().count()) + ]) + .then(function(results) { + const totalCount = results[0] + const statusCounts = results[1] || [] + const providerCount = results[2] || 0 + + const stats = { + total: totalCount + , usable: 0 + , busy: 0 + , providers: providerCount + , byStatus: {} + } + + statusCounts.forEach(function(item) { + const status = item.group || 'unknown' + const count = item.reduction + stats.byStatus[status] = count + + if (status === 'available' || status === 'busy') { + stats.usable += count + } + if (status === 'busy') { + stats.busy += count + } + }) + + return stats + }) + .catch(function(error) { + log.error('Error getting device metrics:', error) + return { + total: 0 + , usable: 0 + , busy: 0 + , providers: 0 + , byStatus: {} + } + }) +} module.exports = dbapi diff --git a/lib/util/metrics-collector.js b/lib/util/metrics-collector.js index 4cf067db86..4697ba4be3 100644 --- a/lib/util/metrics-collector.js +++ b/lib/util/metrics-collector.js @@ -65,23 +65,9 @@ class MetricsCollector { 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 - , byStatus: {} - } - - // Count devices by status - devices.forEach(device => { - const status = device.status || 'unknown' - deviceStats.byStatus[status] = (deviceStats.byStatus[status] || 0) + 1 - }) - + // 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) { From c44a40c9276aaa31de1401c3d6d31fca27f1074f Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Mon, 26 May 2025 00:37:18 +0300 Subject: [PATCH 5/8] fix: Update device metrics query to use group instead of groupBy for improved performance Signed-off-by: matanbaruch --- lib/db/api.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/db/api.js b/lib/db/api.js index e485a6c3d8..ca6c484e00 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -17,7 +17,6 @@ const _ = require('lodash') const logger = require('../util/logger') const log = logger.createLogger('db:api') -// Internal function for bootstrap/migration - not exported to prevent access control bypass function getDevices() { return db.run(r.table('devices')) .then(function(cursor) { @@ -1669,7 +1668,7 @@ dbapi.getDeviceMetrics = function() { // Get total device count db.run(r.table('devices').count()) // Get device counts by status - , db.run(r.table('devices').groupBy('status').count().ungroup()) + , db.run(r.table('devices').group('status').count().ungroup()) // Get provider count (line split to meet max length) , db.run(r.table('devices').hasFields('provider') .map(r.row('provider')('name')).distinct().count()) From 8e2246941e860a54a09ddb1f2194471354d2ce98 Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Mon, 26 May 2025 00:42:13 +0300 Subject: [PATCH 6/8] fix: Update metrics collection to use $window instead of window for better compatibility Signed-off-by: matanbaruch --- res/app/group-list/group-list-controller.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/res/app/group-list/group-list-controller.js b/res/app/group-list/group-list-controller.js index 754d5f00dd..774105175d 100644 --- a/res/app/group-list/group-list-controller.js +++ b/res/app/group-list/group-list-controller.js @@ -64,10 +64,9 @@ module.exports = function GroupListCtrl( incrStateStats(oldGroup, -1) } } - // Send group metrics to backend if available (for metrics collection) - if (typeof window !== 'undefined' && window.stfMetrics) { - window.stfMetrics.updateGroupMetrics({ + if (typeof $window !== 'undefined' && $window.stfMetrics) { + $window.stfMetrics.updateGroupMetrics({ total: $scope.groups.length, active: $scope.activeGroups, ready: $scope.readyGroups, From ffcb820ff6feabc200cb43355d06edf57ea2841d Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Mon, 26 May 2025 00:44:44 +0300 Subject: [PATCH 7/8] Fix Copilot suggestions in metrics-hooks.js Signed-off-by: matanbaruch --- lib/util/metrics-hooks.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/util/metrics-hooks.js b/lib/util/metrics-hooks.js index 1db8daba26..ce093b84cc 100644 --- a/lib/util/metrics-hooks.js +++ b/lib/util/metrics-hooks.js @@ -24,18 +24,22 @@ class MetricsHooks { static onDeviceStatusChanged(device) { // Update device status metrics try { - let status = 'offline' + const previousStatus = device.previousStatus || 'offline'; + let newStatus = 'offline'; if (device.present) { - status = device.owner ? 'busy' : 'available' + newStatus = device.owner ? 'busy' : 'available'; + } + // Update metrics if the status has changed + if (previousStatus !== newStatus) { + // Decrement the count for the previous status + metrics.metrics.devicesByStatus.dec({ status: previousStatus }); + // Increment the count for the new status + metrics.metrics.devicesByStatus.inc({ status: newStatus }); + log.debug('Device status changed:', device.serial, 'from:', previousStatus, 'to:', newStatus); } - - // This is a simple approximation - in a real implementation - // you'd want to track the previous status and update accordingly - log.debug('Device status changed:', device.serial, 'status:', status) } catch (error) { - log.error('Error updating device status metrics:', error) - } + log.error('Error updating device status metrics:', error); } } static onUserAdded() { From 89225ade66732aec54edd2a4fb5050350b82fa7b Mon Sep 17 00:00:00 2001 From: matanbaruch Date: Mon, 26 May 2025 06:37:04 +0300 Subject: [PATCH 8/8] fix: Add $window dependency to GroupListCtrl for improved compatibility Signed-off-by: matanbaruch --- lib/util/metrics-hooks.js | 28 +++++++++++---------- res/app/group-list/group-list-controller.js | 1 + 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/util/metrics-hooks.js b/lib/util/metrics-hooks.js index ce093b84cc..f4a667fb7e 100644 --- a/lib/util/metrics-hooks.js +++ b/lib/util/metrics-hooks.js @@ -24,22 +24,24 @@ class MetricsHooks { static onDeviceStatusChanged(device) { // Update device status metrics try { - const previousStatus = device.previousStatus || 'offline'; - let newStatus = 'offline'; + const previousStatus = device.previousStatus || 'offline' + let newStatus = 'offline' if (device.present) { - newStatus = device.owner ? 'busy' : 'available'; + newStatus = device.owner ? 'busy' : 'available' } // Update metrics if the status has changed if (previousStatus !== newStatus) { // Decrement the count for the previous status - metrics.metrics.devicesByStatus.dec({ status: previousStatus }); + metrics.metrics.devicesByStatus.dec({status: previousStatus}) // Increment the count for the new status - metrics.metrics.devicesByStatus.inc({ status: newStatus }); - log.debug('Device status changed:', device.serial, 'from:', previousStatus, 'to:', newStatus); + metrics.metrics.devicesByStatus.inc({status: newStatus}) + log.debug('Device status changed:', device.serial, 'from:', + previousStatus, 'to:', newStatus) } } catch (error) { - log.error('Error updating device status metrics:', error); } + log.error('Error updating device status metrics:', error) + } } static onUserAdded() { @@ -74,10 +76,10 @@ class MetricsHooks { if (oldStatus === 'active') { metrics.metrics.activeGroups.dec() } - else if (oldStatus === 'ready') { + else if (oldStatus === 'ready') { metrics.metrics.readyGroups.dec() } - else if (oldStatus === 'pending') { + else if (oldStatus === 'pending') { metrics.metrics.pendingGroups.dec() } @@ -85,17 +87,17 @@ class MetricsHooks { if (newStatus === 'active') { metrics.metrics.activeGroups.inc() } - else if (newStatus === 'ready') { + else if (newStatus === 'ready') { metrics.metrics.readyGroups.inc() } - else if (newStatus === 'pending') { + else if (newStatus === 'pending') { metrics.metrics.pendingGroups.inc() } log.debug('Group status changed:', group.id, 'from:', oldStatus, 'to:', newStatus) } } - catch (error) { + catch (error) { log.error('Error updating group status metrics:', error) } } @@ -106,7 +108,7 @@ class MetricsHooks { metrics.updateUserQuota(userEmail, quotaType, consumed, allocated) log.debug('User quota updated:', userEmail, quotaType, consumed, '/', allocated) } - catch (error) { + catch (error) { log.error('Error updating user quota metrics:', error) } } diff --git a/res/app/group-list/group-list-controller.js b/res/app/group-list/group-list-controller.js index 774105175d..ab16bc5641 100644 --- a/res/app/group-list/group-list-controller.js +++ b/res/app/group-list/group-list-controller.js @@ -7,6 +7,7 @@ const _ = require('lodash') module.exports = function GroupListCtrl( $scope , $filter +, $window , GroupsService , UserService , UsersService