diff --git a/.rpsrc b/.rpsrc index 898b06002..562f0ca60 100644 --- a/.rpsrc +++ b/.rpsrc @@ -17,5 +17,7 @@ "consul_enabled": false, "consul_host": "localhost", "consul_port": "8500", - "consul_key_prefix": "RPS" + "consul_key_prefix": "RPS", + "telegraf_host":"localhost", + "telegraf_port": 8020 } diff --git a/package-lock.json b/package-lock.json index 31fecd9fd..0e78af275 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "express-validator": "^7.0.1", "express-ws": "^5.0.2", "got": "^11.8.6", + "hot-shots": "^10.0.0", "http-z": "^6.1.2", "minimist": "^1.2.8", "mqtt": "^5.1.2", @@ -2384,6 +2385,15 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -4315,6 +4325,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4823,6 +4839,17 @@ "node": ">=10" } }, + "node_modules/hot-shots": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-10.0.0.tgz", + "integrity": "sha512-uy/uGpuJk7yuyiKRfZMBNkF1GAOX5O2ifO9rDCaX9jw8fu6eW9QeWC7WRPDI+O98frW1HQgV3+xwjWsZPECIzQ==", + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "unix-dgram": "2.x" + } + }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -6599,6 +6626,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8670,6 +8703,20 @@ "node": ">= 4.0.0" } }, + "node_modules/unix-dgram": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.6.tgz", + "integrity": "sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.16.0" + }, + "engines": { + "node": ">=0.10.48" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10894,6 +10941,15 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -12341,6 +12397,12 @@ "flat-cache": "^3.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -12710,6 +12772,14 @@ } } }, + "hot-shots": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-10.0.0.tgz", + "integrity": "sha512-uy/uGpuJk7yuyiKRfZMBNkF1GAOX5O2ifO9rDCaX9jw8fu6eW9QeWC7WRPDI+O98frW1HQgV3+xwjWsZPECIzQ==", + "requires": { + "unix-dgram": "2.x" + } + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -14047,6 +14117,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -15588,6 +15664,16 @@ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true }, + "unix-dgram": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.6.tgz", + "integrity": "sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.16.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 3bd67dfcf..20db304ca 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "express-validator": "^7.0.1", "express-ws": "^5.0.2", "got": "^11.8.6", + "hot-shots": "^10.0.0", "http-z": "^6.1.2", "minimist": "^1.2.8", "mqtt": "^5.1.2", diff --git a/src/Index.ts b/src/Index.ts index 3c4cfdb47..1a0ff9b5c 100644 --- a/src/Index.ts +++ b/src/Index.ts @@ -24,6 +24,8 @@ import * as ServiceManager from './serviceManager' import { ConsulService } from './consul' import { type IServiceManager } from './interfaces/IServiceManager' import path = require('path') +import expressStatsdInit from './middleware/stats' +import statsD from './utils/stats' const log = new Logger('Index') @@ -42,11 +44,12 @@ config.delay_tls_put_data_sync = 5000 log.silly(`config: ${JSON.stringify(config, null, 2)}`) Environment.Config = config - +statsD.Initialize() const app = express() app.use(cors()) app.use(express.urlencoded()) app.use(express.json()) +app.use(expressStatsdInit()) export const waitForDB = async function (db: IDB): Promise { await backOff(async () => await db.query('SELECT 1'), { @@ -67,6 +70,7 @@ export const waitForSecretsManager = async function (secretsManager: ISecretMana } export const startItUp = (): void => { + statsD.increment('startup') const configurator = new Configurator() log.silly(`WebSocket Cert Info ${JSON.stringify(Environment.Config)}`) const serverForEnterpriseAssistant: WSEnterpriseAssistantListener = new WSEnterpriseAssistantListener(new Logger('WSEnterpriseAssistantListener')) diff --git a/src/middleware/stats.ts b/src/middleware/stats.ts new file mode 100644 index 000000000..cd4299236 --- /dev/null +++ b/src/middleware/stats.ts @@ -0,0 +1,42 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2022 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { type Handler } from 'express' +import statsD from '../utils/stats' + +export default function expressStatsdInit (): Handler { + const client = statsD + + return function expressStatsd (req, res, next) { + const startTime = new Date().getTime() + + // Function called on response finish that sends stats to statsd + function sendStats (): void { + // Status Code + const statusCode: string = res.statusCode.toString() || 'unknown_status' + const duration = new Date().getTime() - startTime + client.increment('request', { statusCode, method: req.method, url: req.baseUrl, duration: duration.toString() }) + client.timing('response_time', duration) + + cleanup() + } + + // Function to clean up the listeners we've added + function cleanup (): void { + res.removeListener('finish', sendStats) + res.removeListener('error', cleanup) + res.removeListener('close', cleanup) + } + + // Add response listeners + res.once('finish', sendStats) + res.once('error', cleanup) + res.once('close', cleanup) + + if (next) { + next() + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 383df0459..4427425ce 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -104,6 +104,8 @@ export interface RPSConfig { disable_cira_domain_name?: string jwt_token_header: string jwt_tenant_property: string + telegraf_host: string + telegraf_port: number } export enum AMTRedirectionServiceEnabledStates { DISABLED = 32768, diff --git a/src/stateMachines/activation.ts b/src/stateMachines/activation.ts index 3182b8260..9a5b07006 100644 --- a/src/stateMachines/activation.ts +++ b/src/stateMachines/activation.ts @@ -30,6 +30,7 @@ import ClientResponseMsg from '../utils/ClientResponseMsg' import { Unconfiguration } from './unconfiguration' import { type DeviceCredentials } from '../interfaces/ISecretManagerService' import { NetworkConfiguration } from './networkConfiguration' +import statsD from '../utils/stats' export interface ActivationContext { profile: AMTConfiguration @@ -783,9 +784,11 @@ export class Activation { let method = null if (status === 'success') { method = 'success' + statsD.increment('activation.success') } else if (status === 'error') { clientObj.status.Status = context.errorMessage !== '' ? context.errorMessage : 'Failed' method = 'failed' + statsD.increment('activation.failure') } const responseMessage = ClientResponseMsg.get(clientId, null, status, method, JSON.stringify(clientObj.status)) this.logger.info(JSON.stringify(responseMessage, null, '\t')) diff --git a/src/stateMachines/unconfiguration.ts b/src/stateMachines/unconfiguration.ts index 3c9213d4d..4dcadcaf8 100644 --- a/src/stateMachines/unconfiguration.ts +++ b/src/stateMachines/unconfiguration.ts @@ -20,6 +20,7 @@ import { type AMTConfiguration } from '../models' import { Error } from './error' import { invokeWsmanCall } from './common' import { Environment } from '../utils/Environment' +import statsD from '../utils/stats' export interface UnconfigContext { clientId: string @@ -607,6 +608,7 @@ export class Unconfiguration { type: 'final' }, SUCCESS: { + entry: ['Metric Capture'], type: 'final' } } @@ -634,6 +636,9 @@ export class Unconfiguration { shouldRetry: (context, event) => context.retryCount < 3 && event.data instanceof UNEXPECTED_PARSE_ERROR }, actions: { + 'Metric Capture': (context, event) => { + statsD.increment('unconfiguration.success') + }, 'Update CIRA Status': (context, event) => { devices[context.clientId].status.CIRAConnection = context.statusMessage }, @@ -644,6 +649,7 @@ export class Unconfiguration { devices[context.clientId].status.TLSConfiguration = '' devices[context.clientId].status.CIRAConnection = '' devices[context.clientId].status.Status = context.statusMessage + statsD.increment('unconfiguration.failure') }, 'Reset Unauth Count': (context, event) => { devices[context.clientId].unauthCount = 0 }, 'Read WiFi Endpoint Settings Pull Response': this.readWiFiEndpointSettingsPullResponse.bind(this), diff --git a/src/test/helper/Config.ts b/src/test/helper/Config.ts index a33331142..66111bd8b 100644 --- a/src/test/helper/Config.ts +++ b/src/test/helper/Config.ts @@ -39,5 +39,7 @@ export const config: RPSConfig = { db_provider: 'postgres', connection_string: 'postgresql://postgresadmin:admin123@localhost:5432/rpsdb', jwt_tenant_property: '', - jwt_token_header: '' + jwt_token_header: '', + telegraf_host: 'localhost', + telegraf_port: 8020 } diff --git a/src/utils/stats.ts b/src/utils/stats.ts new file mode 100644 index 000000000..d20dcbdc9 --- /dev/null +++ b/src/utils/stats.ts @@ -0,0 +1,61 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2022 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { StatsD } from 'hot-shots' +import { Environment } from './Environment' + +class StatsDClient { + private static instance: StatsDClient + private client: StatsD + + private constructor () { + + } + + public static getInstance (): StatsDClient { + if (!StatsDClient.instance) { + StatsDClient.instance = new StatsDClient() + } + + return StatsDClient.instance + } + + public Initialize (): void { + this.client = new StatsD({ + host: Environment.Config.telegraf_host, + port: Environment.Config.telegraf_port, + globalTags: { service: 'rps' }, + errorHandler: this.errorHandler + }) + } + + private errorHandler (error: Error): void { + console.error('Error encountered in StatsD client:', error) + } + + public increment (stat: string, tags?: string[] | Record): void { + this.client.increment('rps.' + stat, tags) + } + + public decrement (stat: string, tags?: string[], sampleRate?: number): void { + this.client.decrement('rps.' + stat, 1, sampleRate, tags) + } + + public timing (stat: string, value: number): void { + this.client.timing('rps.' + stat, value) + } + + public event (title: string, text?: string, options?: any, tags?: string[]): void { + this.client.event(title, text, options, tags) + } + + public close (callback?: (error?: Error) => void): void { + this.client.close(callback) + } +} + +// Export the singleton instance +const statsD = StatsDClient.getInstance() +export default statsD