From 2b54523b0a31381a197a09e0983ed56947fb3b66 Mon Sep 17 00:00:00 2001 From: Mike Johanson Date: Thu, 17 Aug 2023 14:48:29 -0700 Subject: [PATCH 1/2] feat: add telemetry --- package-lock.json | 86 ++++++++++++++++++++++++++++ package.json | 1 + src/Index.ts | 4 ++ src/middleware/stats.ts | 44 ++++++++++++++ src/stateMachines/activation.ts | 7 ++- src/stateMachines/unconfiguration.ts | 6 ++ src/utils/stats.ts | 52 +++++++++++++++++ 7 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/middleware/stats.ts create mode 100644 src/utils/stats.ts diff --git a/package-lock.json b/package-lock.json index 79eeac763..bc3b70db9 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.0.3", @@ -2368,6 +2369,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", @@ -4300,6 +4310,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", @@ -4808,6 +4824,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", @@ -6582,6 +6609,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", @@ -8648,6 +8681,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", @@ -10842,6 +10889,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", @@ -12290,6 +12346,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", @@ -12659,6 +12721,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", @@ -13994,6 +14064,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", @@ -15530,6 +15606,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 34b166260..cb793688a 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.0.3", diff --git a/src/Index.ts b/src/Index.ts index 3c4cfdb47..661bd9784 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 statsD from './utils/stats' +import expressStatsdInit from './middleware/stats' const log = new Logger('Index') @@ -47,6 +49,7 @@ 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('rps.startup', 1) 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..85ac3d708 --- /dev/null +++ b/src/middleware/stats.ts @@ -0,0 +1,44 @@ +/********************************************************************* + * 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' + client.increment('status_code.' + statusCode) + + // Response Time + const duration = new Date().getTime() - startTime + 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/stateMachines/activation.ts b/src/stateMachines/activation.ts index f5274e7bb..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 @@ -752,7 +753,7 @@ export class Activation { async getAMTProfile (context: ActivationContext, event: ActivationEvent): Promise { this.db = await this.dbFactory.getDb() const profile = await this.configurator.profileManager.getAmtProfile(devices[context.clientId].ClientData.payload.profile.profileName, context.tenantId) - return await Promise.resolve(profile) + return profile } async getDeviceFromMPS (context: ActivationContext, event: ActivationEvent): Promise { @@ -774,7 +775,7 @@ export class Activation { async getAMTDomainCert (context: ActivationContext, event: ActivationEvent): Promise { const domain = await this.configurator.domainCredentialManager.getProvisioningCert(devices[context.clientId].ClientData.payload.fqdn, context.tenantId) - return await Promise.resolve(domain) + return domain } sendMessageToDevice (context: ActivationContext, event: ActivationEvent): void { @@ -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 4f7f32f4c..521272374 100644 --- a/src/stateMachines/unconfiguration.ts +++ b/src/stateMachines/unconfiguration.ts @@ -19,6 +19,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 @@ -611,6 +612,7 @@ export class Unconfiguration { type: 'final' }, SUCCESS: { + entry: ['Metric Capture'], type: 'final' } } @@ -638,6 +640,9 @@ export class Unconfiguration { }, actions: { + 'Metric Capture': (context, event) => { + statsD.increment('unconfiguration.success', 1) + }, 'Update CIRA Status': (context, event) => { devices[context.clientId].status.CIRAConnection = context.statusMessage }, @@ -648,6 +653,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', 1) }, 'Reset Unauth Count': (context, event) => { devices[context.clientId].unauthCount = 0 }, 'Read WiFi Endpoint Settings Pull Response': this.readWiFiEndpointSettingsPullResponse.bind(this), diff --git a/src/utils/stats.ts b/src/utils/stats.ts new file mode 100644 index 000000000..c7badd881 --- /dev/null +++ b/src/utils/stats.ts @@ -0,0 +1,52 @@ +/********************************************************************* + * Copyright (c) Intel Corporation 2022 + * SPDX-License-Identifier: Apache-2.0 + **********************************************************************/ + +import { StatsD } from 'hot-shots' + +class StatsDClient { + private static instance: StatsDClient + private readonly client: StatsD + + private constructor () { + this.client = new StatsD({ + host: 'telegraf', + port: 8020, + globalTags: { service: 'rps' }, + errorHandler: this.errorHandler + }) + } + + public static getInstance (): StatsDClient { + if (!StatsDClient.instance) { + StatsDClient.instance = new StatsDClient() + } + + return StatsDClient.instance + } + + private errorHandler (error: Error): void { + console.error('Error encountered in StatsD client:', error) + } + + public increment (stat: string, value?: number, sampleRate?: number, tags?: string[] | Record): void { + this.client.increment('rps.' + stat, value, sampleRate, 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 close (callback?: (error?: Error) => void): void { + this.client.close(callback) + } +} + +// Export the singleton instance +const statsD = StatsDClient.getInstance() +export default statsD From ac9b103348947c3841614db524b17dfba52ee41c Mon Sep 17 00:00:00 2001 From: Mike Johanson Date: Mon, 11 Sep 2023 09:19:30 -0700 Subject: [PATCH 2/2] feat: adds basic telemetry --- .rpsrc | 4 +++- src/Index.ts | 6 +++--- src/middleware/stats.ts | 4 +--- src/models/index.ts | 2 ++ src/stateMachines/unconfiguration.ts | 4 ++-- src/test/helper/Config.ts | 4 +++- src/utils/stats.ts | 27 ++++++++++++++++++--------- 7 files changed, 32 insertions(+), 19 deletions(-) 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/src/Index.ts b/src/Index.ts index 661bd9784..1a0ff9b5c 100644 --- a/src/Index.ts +++ b/src/Index.ts @@ -24,8 +24,8 @@ import * as ServiceManager from './serviceManager' import { ConsulService } from './consul' import { type IServiceManager } from './interfaces/IServiceManager' import path = require('path') -import statsD from './utils/stats' import expressStatsdInit from './middleware/stats' +import statsD from './utils/stats' const log = new Logger('Index') @@ -44,7 +44,7 @@ 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()) @@ -70,7 +70,7 @@ export const waitForSecretsManager = async function (secretsManager: ISecretMana } export const startItUp = (): void => { - statsD.increment('rps.startup', 1) + 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 index 85ac3d708..cd4299236 100644 --- a/src/middleware/stats.ts +++ b/src/middleware/stats.ts @@ -16,10 +16,8 @@ export default function expressStatsdInit (): Handler { function sendStats (): void { // Status Code const statusCode: string = res.statusCode.toString() || 'unknown_status' - client.increment('status_code.' + statusCode) - - // Response Time 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() 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/unconfiguration.ts b/src/stateMachines/unconfiguration.ts index 521272374..a09d9f8ca 100644 --- a/src/stateMachines/unconfiguration.ts +++ b/src/stateMachines/unconfiguration.ts @@ -641,7 +641,7 @@ export class Unconfiguration { }, actions: { 'Metric Capture': (context, event) => { - statsD.increment('unconfiguration.success', 1) + statsD.increment('unconfiguration.success') }, 'Update CIRA Status': (context, event) => { devices[context.clientId].status.CIRAConnection = context.statusMessage @@ -653,7 +653,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', 1) + 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 index c7badd881..d20dcbdc9 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -4,18 +4,14 @@ **********************************************************************/ import { StatsD } from 'hot-shots' +import { Environment } from './Environment' class StatsDClient { private static instance: StatsDClient - private readonly client: StatsD + private client: StatsD private constructor () { - this.client = new StatsD({ - host: 'telegraf', - port: 8020, - globalTags: { service: 'rps' }, - errorHandler: this.errorHandler - }) + } public static getInstance (): StatsDClient { @@ -26,12 +22,21 @@ class 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, value?: number, sampleRate?: number, tags?: string[] | Record): void { - this.client.increment('rps.' + stat, value, sampleRate, tags) + public increment (stat: string, tags?: string[] | Record): void { + this.client.increment('rps.' + stat, tags) } public decrement (stat: string, tags?: string[], sampleRate?: number): void { @@ -42,6 +47,10 @@ class StatsDClient { 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) }