diff --git a/app/apollo/index.js b/app/apollo/index.js index 7278d3900..5888c95b0 100644 --- a/app/apollo/index.js +++ b/app/apollo/index.js @@ -36,6 +36,7 @@ const { models, connectDb } = require('./models'); const promClient = require('prom-client'); const createMetricsPlugin = require('apollo-metrics'); const apolloMetricsPlugin = createMetricsPlugin(promClient.register); +const { customMetricsClient } = require('../customMetricsClient'); // Add custom metrics plugin const apolloMaintenancePlugin = require('./maintenance/maintenanceModePlugin.js'); const { GraphqlPubSub } = require('./subscription'); @@ -140,7 +141,47 @@ const createApolloServer = (schema) => { initLogger.info(customPlugins, 'Apollo server custom plugin are loaded.'); const server = new ApolloServer({ introspection: true, // set to true as long as user has valid token - plugins: customPlugins, + plugins: [ + customPlugins, + { + // Populate API metrics as they occur + requestDidStart(context) { + // Capture the start time when the request starts + const startTime = Date.now(); + + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + + let encounteredError = false; + return { + didResolveOperation() { + // Parse API operation name + const match = context.request.query.match(/\{\s*(\w+)/); + const operationName = match ? match[1] : 'Query name not found'; + // Record API operation duration metrics + const durationInSeconds = (Date.now() - startTime) / 1000; + console.log('potato'); + customMetricsClient.apiCallHistogram(operationName).observe(durationInSeconds); + console.log('potato1'); + }, + didEncounterErrors() { + encounteredError = true; + }, + willSendResponse() { + // Parse API operation name + const match = context.request.query.match(/\{\s*(\w+)/); + const operationName = match ? match[1] : 'Query name not found'; + // Record API operation success and failure gauge metrics + if (encounteredError) { + customMetricsClient.apiCallCounter(operationName).inc({ status: 'failure' }); + } else { + customMetricsClient.apiCallCounter(operationName).inc({ status: 'success' }); + } + } + }; + }, + } + ], schema, allowBatchedHttpRequests: (process.env.GRAPHQL_DISABLE_BATCHING ? false : true), formatError: error => { diff --git a/app/customMetricsClient.js b/app/customMetricsClient.js new file mode 100644 index 000000000..95f83f6a7 --- /dev/null +++ b/app/customMetricsClient.js @@ -0,0 +1,57 @@ +/** + * Copyright 2023 IBM Corp. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { Counter, Histogram } = require('prom-client'); +// Maintain a map for previously created counters and histograms +const counters = {}; +const histograms = {}; + +const apiCallsCount = new Counter({ + name: 'api_calls_total', + help: 'Total number of API calls' +}); + +const customMetricsClient = { + apiCallsCount: apiCallsCount, + + // Count success and failure of each API operation and record as unique metric + apiCallCounter(operationName) { + if (!counters[operationName]) { + counters[operationName] = new Counter({ + name: `${operationName}_counter_result_total`, + help: `Total number of ${operationName} operation calls, labeled by success or failure`, + labelNames: ['status'] + }); + } + return counters[operationName]; + }, + + // Track duration of each API operation and record as unique metric + apiCallHistogram(operationName) { + if (!histograms[operationName]) { + histograms[operationName] = new Histogram({ + name: `${operationName}_duration_seconds`, + help: `Duration of ${operationName} operations in seconds`, + buckets: [0.1, 0.5, 1, 2, 5] + }); + } + return histograms[operationName]; + } +}; + +module.exports = { + customMetricsClient +}; diff --git a/app/index.js b/app/index.js index 3de8c8d8c..9f2764cdd 100644 --- a/app/index.js +++ b/app/index.js @@ -41,7 +41,7 @@ const apollo = require('./apollo'); const promClient = require('prom-client'); const collectDefaultMetrics = promClient.collectDefaultMetrics; -collectDefaultMetrics({ timeout: 5000 }); //Collect all default metrics +collectDefaultMetrics({ timeout: 5000 }); //Collect all default metrics const connections = new promClient.Gauge({ name: 'razee_server_connections_count', help: 'Razee server request count' }); const i18next = require('i18next'); const i18nextMiddleware = require('i18next-http-middleware'); diff --git a/app/routes/v1/channels.js b/app/routes/v1/channels.js index 94c5811b9..1b3aaca55 100644 --- a/app/routes/v1/channels.js +++ b/app/routes/v1/channels.js @@ -22,6 +22,7 @@ const MongoClientClass = require('../../mongo/mongoClient.js'); const MongoClient = new MongoClientClass(mongoConf); const getOrg = require('../../utils/orgs.js').getOrg; const { getDecryptedContent } = require('../../apollo/utils/versionUtils'); +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin router.use(asyncHandler(async (req, res, next) => { req.db = await MongoClient.getClient(); @@ -33,6 +34,11 @@ router.use(asyncHandler(async (req, res, next) => { // --url http://localhost:3333/api/v1/channels/:channelName/:versionId \ // --header 'razee-org-key: orgApiKey-api-key-goes-here' \ const getChannelVersion = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + var orgId = req.org._id; var channelName = req.params.channelName + ''; var versionId = req.params.versionId + ''; @@ -60,6 +66,11 @@ const getChannelVersion = async (req, res) => { org = await Orgs.findOne({ _id: orgId }); deployable = await Channels.findOne({ org_id: orgId, name: channelName }); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); + res.status(404).send({ status: 'error', message: `channel "${channelName}" not found for this org` }); return; } @@ -67,6 +78,11 @@ const getChannelVersion = async (req, res) => { var deployableVersion = await DeployableVersions.findOne({ org_id: orgId, channel_id: deployable.uuid, uuid: versionId }); if (!deployableVersion) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); + res.status(404).send({ status: 'error', message: `versionId "${versionId}" not found` }); return; } @@ -74,8 +90,19 @@ const getChannelVersion = async (req, res) => { try { const data = await getDecryptedContent({ logger: req.log, req_id: req.id, me: null }, org, deployableVersion); res.set('Content-Type', deployableVersion.type); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'success' }); + res.status(200).send(data.content); } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); + req.log.error(error); return res.status(500).json({ status: 'error', message: error.message }); } diff --git a/app/routes/v1/systemSubscriptions.js b/app/routes/v1/systemSubscriptions.js index 4ff632694..3c846d7fd 100644 --- a/app/routes/v1/systemSubscriptions.js +++ b/app/routes/v1/systemSubscriptions.js @@ -21,11 +21,17 @@ const { getOrg, bestOrgKey } = require('../../utils/orgs'); const axios = require('axios'); const yaml = require('js-yaml'); const { getRddArgs } = require('../../utils/rdd'); +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin /* Serves a System Subscription that regenerates the `razee-identity` secret with the 'best' OrgKey value. */ const getPrimaryOrgKeySubscription = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const razeeIdentitySecretYaml = `apiVersion: v1 kind: Secret metadata: @@ -39,6 +45,11 @@ data: type: Opaque `; + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getPrimaryOrgKeySubscription').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getPrimaryOrgKeySubscription').inc({ status: 'success' }); + res.status( 200 ).send( razeeIdentitySecretYaml ); }; @@ -46,6 +57,11 @@ type: Opaque Serves a System Subscription that returns a CronJob that updates the operators: Cluster Subscription, Remote Resource and Watch-Keeper */ const getOperatorsSubscription = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + // Get the image and command for the update cronjob from the current values returned from the razeedeploy-job api const protocol = req.protocol || 'http'; let host = req.header('host') || 'localhost:3333'; @@ -110,6 +126,11 @@ metadata: namespace: razeedeploy `; + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getOperatorsSubscription').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getOperatorsSubscription').inc({ status: 'success' }); + res.status( 200 ).send( razeeupdateYaml ); }; diff --git a/app/routes/v2/clusters.js b/app/routes/v2/clusters.js index 3514b226d..de6294303 100644 --- a/app/routes/v2/clusters.js +++ b/app/routes/v2/clusters.js @@ -40,8 +40,14 @@ const { GraphqlPubSub } = require('../../apollo/subscription'); const pubSub = GraphqlPubSub.getInstance(); const conf = require('../../conf.js').conf; const storageFactory = require('./../../storage/storageFactory'); +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin const addUpdateCluster = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + try { const Clusters = req.db.collection('clusters'); const Stats = req.db.collection('resourceStats'); @@ -51,28 +57,56 @@ const addUpdateCluster = async (req, res, next) => { if (!cluster) { // new cluster flow requires a cluster to be registered first. if (process.env.CLUSTER_REGISTRATION_REQUIRED) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addUpdateCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addUpdateCluster').inc({ status: 'failure' }); + res.status(404).send({error: 'Not found, the api requires you to register the cluster first.'}); return; } const total = await Clusters.count({org_id: req.org._id}); if (total >= CLUSTER_LIMITS.MAX_TOTAL ) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addUpdateCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addUpdateCluster').inc({ status: 'failure' }); + res.status(400).send({error: 'Too many clusters are registered under this organization.'}); return; } await Clusters.insertOne({ org_id: req.org._id, cluster_id: req.params.cluster_id, reg_state, registration: {}, metadata, created: new Date(), updated: new Date() }); runAddClusterWebhook(req, req.org._id, req.params.cluster_id, metadata.name); // dont await. just put it in the bg Stats.updateOne({ org_id: req.org._id }, { $inc: { clusterCount: 1 } }, { upsert: true }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addUpdateCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addUpdateCluster').inc({ status: 'success' }); + res.status(200).send('Welcome to Razee'); } else { if (cluster.dirty) { await Clusters.updateOne({ org_id: req.org._id, cluster_id: req.params.cluster_id }, { $set: { metadata, reg_state, updated: new Date(), dirty: false } }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addUpdateCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addUpdateCluster').inc({ status: 'failure' }); + res.status(205).send('Please resync'); } else { await Clusters.updateOne({ org_id: req.org._id, cluster_id: req.params.cluster_id }, { $set: { metadata, reg_state, updated: new Date() } }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addUpdateCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addUpdateCluster').inc({ status: 'success' }); + res.status(200).send('Thanks for the update'); } } @@ -84,6 +118,11 @@ const addUpdateCluster = async (req, res, next) => { }; const getAddClusterWebhookHeaders = async()=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + // loads the headers specified in the 'razeedash-add-cluster-webhook-headers-secret' secret // returns the key-value pairs of the secret as a js obj const filesDir = '/var/run/secrets/razeeio/razeedash-api/add-cluster-webhook-headers'; @@ -96,10 +135,21 @@ const getAddClusterWebhookHeaders = async()=>{ const val = fs.readFileSync(`${filesDir}/${name}`, 'utf8'); headers[encodeURIComponent(name)] = val; }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getAddClusterWebhookHeaders').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getAddClusterWebhookHeaders').inc({ status: 'success' }); + return headers; }; const runAddClusterWebhook = async(req, orgId, clusterId, clusterName)=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + var postData = { org_id: orgId, cluster_id: clusterId, @@ -107,6 +157,10 @@ const runAddClusterWebhook = async(req, orgId, clusterId, clusterName)=>{ }; var url = process.env.ADD_CLUSTER_WEBHOOK_URL; if(!url){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('runAddClusterWebhook').observe(durationInSeconds); + customMetricsClient.apiCallCounter('runAddClusterWebhook').inc({ status: 'failure' }); return; } req.log.info({ url, postData }, 'posting add cluster webhook'); @@ -116,14 +170,30 @@ const runAddClusterWebhook = async(req, orgId, clusterId, clusterName)=>{ data: postData, headers, }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('runAddClusterWebhook').observe(durationInSeconds); + customMetricsClient.apiCallCounter('runAddClusterWebhook').inc({ status: 'success' }); + req.log.info({ url, postData, statusCode: result.status }, 'posted add cluster webhook'); }catch(err){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('runAddClusterWebhook').observe(durationInSeconds); + customMetricsClient.apiCallCounter('runAddClusterWebhook').inc({ status: 'failure' }); + req.log.error({ url, postData, err }, 'add cluster webhook failed'); } }; function pushToS3Sync(key, searchableDataHash, dataStr, data_location, logger) { - //if its a new or changed resource, write the data out to an S3 object + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + + // if its a new or changed resource, write the data out to an S3 object const result = {}; const bucket = conf.storage.getResourceBucket(data_location); const hash = crypto.createHash('sha256'); @@ -131,16 +201,35 @@ function pushToS3Sync(key, searchableDataHash, dataStr, data_location, logger) { const handler = storageFactory(logger).newResourceHandler(`${keyHash}/${searchableDataHash}`, bucket, data_location); result.promise = handler.setData(dataStr); result.encodedData = handler.serialize(); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('pushToS3Sync').observe(durationInSeconds); + customMetricsClient.apiCallCounter('pushToS3Sync').inc({ status: 'success' }); + return result; } const deleteOrgClusterResourceSelfLinks = async(req, orgId, clusterId, selfLinks)=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const Resources = req.db.collection('resources'); selfLinks = _.filter(selfLinks); // in such a case that a null is passed to us. if you do $in:[null], it returns all items missing the attr, which is not what we want if(selfLinks.length < 1){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrgClusterResourceSelfLinks').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrgClusterResourceSelfLinks').inc({ status: 'failure' }); return; } if(!orgId || !clusterId){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrgClusterResourceSelfLinks').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrgClusterResourceSelfLinks').inc({ status: 'failure' }); throw `missing orgId or clusterId: ${JSON.stringify({ orgId, clusterId })}`; } var search = { @@ -150,10 +239,19 @@ const deleteOrgClusterResourceSelfLinks = async(req, orgId, clusterId, selfLinks $in: selfLinks, } }; + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrgClusterResourceSelfLinks').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrgClusterResourceSelfLinks').inc({ status: 'success' }); await Resources.deleteMany(search); }; const syncClusterResources = async(req, res)=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const orgId = req.org._id; const clusterId = req.params.cluster_id; const Resources = req.db.collection('resources'); @@ -179,15 +277,26 @@ const syncClusterResources = async(req, res)=>{ Stats.updateOne({ org_id: orgId }, { $inc: { deploymentCount: -1 * objsToDelete.length } }); } - + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('syncClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('syncClusterResources').inc({ status: 'success' }); res.status(200).send('Thanks'); }; const updateClusterResources = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); try { var clusterId = req.params.cluster_id; const body = req.body; if (!body) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateClusterResources').inc({ status: 'failure' }); res.status(400).send('Missing resource body'); return; } @@ -350,6 +459,10 @@ const updateClusterResources = async (req, res, next) => { // if obj not in db, then adds it const total = await Resources.count({org_id: req.org._id, deleted: false}); if (total >= RESOURCE_LIMITS.MAX_TOTAL ) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateClusterResources').inc({ status: 'failure' }); res.status(400).send({error: 'Too many resources are registered under this organization.'}); return; } @@ -442,20 +555,38 @@ const updateClusterResources = async (req, res, next) => { }); })); if( unsupportedResourceEvents.length > 0 ) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateClusterResources').inc({ status: 'failure' }); + // This could occur if agent sends `x is forbidden` objects instead of expected polled/modified/added/deleted events. It is useful info, but not a server side error. req.log.info( `Unsupported events received: ${JSON.stringify( unsupportedResourceEvents )}` ); res.status(400).send('invalid payload'); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateClusterResources').inc({ status: 'success' }); res.status(200).send('Thanks'); } } catch (err) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateClusterResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateClusterResources').inc({ status: 'failure' }); req.log.error(err.message); next(err); } }; const addResourceYamlHistObj = async(req, orgId, clusterId, resourceSelfLink, yamlStr)=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + var ResourceYamlHist = req.db.collection('resourceYamlHist'); var id = uuid(); var obj = { @@ -467,12 +598,27 @@ const addResourceYamlHistObj = async(req, orgId, clusterId, resourceSelfLink, ya updated: new Date(), }; await ResourceYamlHist.insertOne(obj); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addResocurceYamlHistObj').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addResocurceYamlHistObj').inc({ status: 'success' }); return id; }; const addClusterMessages = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const body = req.body; if (!body) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addClusterMessages').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addClusterMessages').inc({ status: 'failure' }); + res.status(400).send('Missing message body'); return; } @@ -507,36 +653,75 @@ const addClusterMessages = async (req, res, next) => { const Messages = req.db.collection('messages'); await Messages.updateOne(key, { $set: data, $setOnInsert: insertData }, { upsert: true }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addClusterMessages').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addClusterMessages').inc({ status: 'success' }); req.log.debug({ messagedata: data }, `${messageType} message data posted`); res.status(200).send(`${messageType} message received`); } catch (err) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('addClusterMessages').observe(durationInSeconds); + customMetricsClient.apiCallCounter('addClusterMessages').inc({ status: 'failure' }); req.log.error(err.message); next(err); } }; const getClusters = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); try { const Clusters = req.db.collection('clusters'); const orgId = req.org._id + ''; const clusters = await Clusters.find({ 'org_id': orgId }).toArray(); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getClusters').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getClusters').inc({ status: 'success' }); return res.status(200).send({clusters}); } catch (err) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getClusters').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getClusters').inc({ status: 'failure' }); req.log.error(err.message); next(err); } }; const clusterDetails = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const cluster = req.cluster; // req.cluster was set in `getCluster` if(cluster) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('clusterDetails').observe(durationInSeconds); + customMetricsClient.apiCallCounter('clusterDetails').inc({ status: 'success' }); return res.status(200).send({cluster}); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('clusterDetails').observe(durationInSeconds); + customMetricsClient.apiCallCounter('clusterDetails').inc({ status: 'failure' }); return res.status(404).send('cluster was not found'); } }; const deleteCluster = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); try { if(!req.org._id || !req.params.cluster_id){ throw 'missing orgId or clusterId'; @@ -544,9 +729,18 @@ const deleteCluster = async (req, res, next) => { const Clusters = req.db.collection('clusters'); const cluster_id = req.params.cluster_id; await Clusters.deleteOne({ org_id: req.org._id, cluster_id: cluster_id }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteCluster').inc({ status: 'success' }); req.log.info(`cluster ${cluster_id} deleted`); next(); } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteCluster').inc({ status: 'failure' }); req.log.error(error.message); return res.status(500).json({ status: 'error', message: error.message }); } diff --git a/app/routes/v2/orgs.js b/app/routes/v2/orgs.js index 66ce8a031..1726ec878 100644 --- a/app/routes/v2/orgs.js +++ b/app/routes/v2/orgs.js @@ -20,11 +20,21 @@ const asyncHandler = require('express-async-handler'); const _ = require('lodash'); const verifyAdminOrgKey = require('../../utils/orgs.js').verifyAdminOrgKey; const { v4: uuid } = require('uuid'); +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin const createOrg = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const orgName = (req.body && req.body.name) ? req.body.name.trim() : null; if(!orgName) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('createOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('createOrg').inc({ status: 'failure' }); req.log.warn(`An org name was not specified on route ${req.url}`); return res.status(400).send( 'An org name is required' ); } @@ -33,6 +43,10 @@ const createOrg = async(req, res) => { const Orgs = req.db.collection('orgs'); const foundOrg = await Orgs.findOne({'name': orgName}); if(foundOrg){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('createOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('createOrg').inc({ status: 'failure' }); req.log.warn( 'The org name already exists' ); return res.status(400).send( 'This org already exists' ); } @@ -47,18 +61,34 @@ const createOrg = async(req, res) => { }); if(insertedOrg.result.ok) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('createOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('createOrg').inc({ status: 'success' }); return res.status(200).send( insertedOrg.ops[0] ); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('createOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('createOrg').inc({ status: 'failure' }); req.log.error(insertedOrg.result, `Could not create ${orgName} into the Orgs collection`); return res.status(500).send( 'Could not create the org' ); } } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('createOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('createOrg').inc({ status: 'failure' }); req.log.error(error); return res.status(500).send( 'Error creating the org' ); } }; const getOrgs = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); try { const Orgs = req.db.collection('orgs'); @@ -74,18 +104,36 @@ const getOrgs = async(req, res) => { } const foundOrgs = await Orgs.find(orgsQuery).toArray(); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'success' }); return res.status(200).send( foundOrgs ); } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); req.log.error(error); return res.status(500).send( 'Error searching for orgs' ); } }; const updateOrg = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const existingOrgId = req.params.id; const updates = req.body; if (!updates || _.isEmpty(updates)) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateOrg').inc({ status: 'failure' }); req.log.error('no message body was provided'); return res.status(400).send('Missing message body'); } @@ -94,6 +142,10 @@ const updateOrg = async(req, res) => { const Orgs = req.db.collection('orgs'); const foundOrg = await Orgs.findOne({'_id': existingOrgId}); if(!foundOrg){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateOrg').inc({ status: 'failure' }); req.log.warn( 'The org was not found' ); return res.status(400).send( 'This org was not found' ); } @@ -101,29 +153,58 @@ const updateOrg = async(req, res) => { updates.updated = new Date(); const updatedOrg = await Orgs.updateOne({ _id: foundOrg._id }, { $set: updates } ); if(updatedOrg.result.ok) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateOrg').inc({ status: 'success' }); return res.status(200).send( 'success' ); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateOrg').inc({ status: 'failure' }); req.log.error(updatedOrg); return res.status(500).send( 'Could not update the org' ); } } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('updateOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('updateOrg').inc({ status: 'failure' }); req.log.error(error); return res.status(500).send( 'Error updating the org' ); } }; const deleteOrg = async(req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const existingOrgId = req.params.id; try { const Orgs = req.db.collection('orgs'); const removedOrg = await Orgs.deleteOne({ '_id': existingOrgId } ); if(removedOrg.deletedCount) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrg').inc({ status: 'success' }); return res.status(200).send( 'success' ); } else { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrg').inc({ status: 'failure' }); req.log.error(removedOrg); return res.status(404).send( 'The org could not be deleted' ); } } catch (error) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('deleteOrg').observe(durationInSeconds); + customMetricsClient.apiCallCounter('deleteOrg').inc({ status: 'failure' }); req.log.error(error); return res.status(500).send( 'Error deleting the org' ); } diff --git a/app/routes/v2/resources.js b/app/routes/v2/resources.js index 319153f37..0e512a17b 100644 --- a/app/routes/v2/resources.js +++ b/app/routes/v2/resources.js @@ -19,9 +19,13 @@ const router = express.Router(); const asyncHandler = require('express-async-handler'); const verifyAdminOrgKey = require('../../utils/orgs.js').verifyAdminOrgKey; const _ = require('lodash'); - +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin const getResources = async (req, res, next) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); try { const Resources = req.db.collection('resources'); const orgId = req.org._id + ''; @@ -50,11 +54,20 @@ const getResources = async (req, res, next) => { }; const resources = await Resources.find(query, options).toArray(); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getResources').inc({ status: 'success' }); return res.status(200).send({ resources, limit, skip, }); } catch (err) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getResources').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getResources').inc({ status: 'failure' }); req.log.error(err.message); next(err); } diff --git a/app/routes/v3/gql.js b/app/routes/v3/gql.js index 62b3eb7e2..b765d227e 100644 --- a/app/routes/v3/gql.js +++ b/app/routes/v3/gql.js @@ -20,6 +20,7 @@ const router = express.Router(); const asyncHandler = require('express-async-handler'); const mainServer = require('../../'); const log = require('../../log').createLogger('razeedash-api/app/routes/v1/gql'); +const { customMetricsClient } = require('../../customMetricsClient'); // Add custom metrics plugin const methodTypes = [ 'findOne', 'findMany', 'create', 'update', @@ -27,6 +28,11 @@ const methodTypes = [ // Send request to Graphql, but return a REST style response / code const sendReqToGraphql = async({ req, res, query, variables, operationName, methodType, createdIdentifier })=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const methodName = 'sendReqToGraphql'; log.debug( `${methodName} entry, operationName: ${operationName}` ); @@ -61,13 +67,25 @@ const sendReqToGraphql = async({ req, res, query, variables, operationName, meth // If GET of a single item... if( restReqType == 'GET' ) { if(methodType == 'findOne' && !resVal){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('sendReqToGraphql').observe(durationInSeconds); + customMetricsClient.apiCallCounter('sendReqToGraphql').inc({ status: 'failure' }); return this.status(404).oldSend(''); } + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('sendReqToGraphql').observe(durationInSeconds); + customMetricsClient.apiCallCounter('sendReqToGraphql').inc({ status: 'success' }); // One/Multiple expected, one/multiple found, return 200 (OK) return this.status(200).oldSend( JSON.stringify(resVal) ); } // ElseIf PUT... else if( restReqType == 'PUT' ) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('sendReqToGraphql').observe(durationInSeconds); + customMetricsClient.apiCallCounter('sendReqToGraphql').inc({ status: 'success' }); // Modification may or may not have been necessary, return 200 (OK) return this.status(200).oldSend( '' ); // Ideally should return the updated object(s) here, but graphql doesn't return that } @@ -75,12 +93,21 @@ const sendReqToGraphql = async({ req, res, query, variables, operationName, meth else if( restReqType == 'POST' ) { // One expected, one created, return 201 (CREATED) with `Location` header this.setHeader( 'Location', `${restReqPath}/${resVal[createdIdentifier||'uuid']}` ); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('sendReqToGraphql').observe(durationInSeconds); + customMetricsClient.apiCallCounter('sendReqToGraphql').inc({ status: 'success' }); return this.status(201).oldSend( JSON.stringify(resVal) ); } // Else (unexpected request type) throw new Error( `request type '${restReqType}' is unexpected` ); // Should never occur } catch( e ) { + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('sendReqToGraphql').observe(durationInSeconds); + customMetricsClient.apiCallCounter('sendReqToGraphql').inc({ status: 'failure' }); log.debug( `${methodName} error: ${e.message}` ); return this.status(400).oldSend( e.message ); } @@ -99,16 +126,33 @@ const sendReqToGraphql = async({ req, res, query, variables, operationName, meth }; const getOrgId = (req, res, next)=>{ + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); + const orgId = req.get('org-id') || req.body.orgId || req.query.orgId; if(!orgId){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getOrgId').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getOrgId').inc({ status: 'failure' }); res.status(400).send( 'Please pass an orgId in an "org-id" header, an "orgId" post body param, or an orgId query string attribute' ); return; } + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getOrgId').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getOrgId').inc({ status: 'success' }); req.orgId = orgId; next(); }; const postChannels = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['channels'] // #swagger.summary = 'Adds a channel' const { orgId } = req; @@ -122,6 +166,10 @@ const postChannels = async (req, res) => { `; const name = req.body.name; if(!name){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postChannels').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postChannels').inc({ status: 'failure' }); res.status(400).send( 'needs { name }' ); return; } @@ -131,10 +179,19 @@ const postChannels = async (req, res) => { }; const methodType = 'create'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType, createdIdentifier: 'uuid' }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postChannels').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postChannels').inc({ status: 'success' }); }; router.post('/channels', getOrgId, asyncHandler(postChannels)); const getChannels = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['channels'] // #swagger.summary = 'Gets all channels' const { orgId } = req; @@ -161,10 +218,18 @@ const getChannels = async (req, res) => { }; const methodType = 'findMany'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannels').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannels').inc({ status: 'success' }); }; router.get('/channels', getOrgId, asyncHandler(getChannels)); const getChannel = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['channels'] // #swagger.summary = 'Gets a specified channel' const { orgId } = req; @@ -187,6 +252,10 @@ const getChannel = async (req, res) => { `; const uuid = req.params.uuid; if(!uuid){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannel').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannel').inc({ status: 'failure' }); res.status(400).send( 'needs { uuid }' ); return; } @@ -196,10 +265,19 @@ const getChannel = async (req, res) => { }; const methodType = 'findOne'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannel').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannel').inc({ status: 'success' }); }; router.get('/channels/:uuid', getOrgId, asyncHandler(getChannel)); const postChannelVersion = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['channels'] // #swagger.summary = 'Adds a new channel version' const { orgId } = req; @@ -217,6 +295,10 @@ const postChannelVersion = async (req, res) => { const type = req.body.type; const content = req.body.content; if(!name || !channelUuid || !type || !content){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); res.status(400).send( 'needs { channelUuid, name, type, content }' ); return; } @@ -229,10 +311,18 @@ const postChannelVersion = async (req, res) => { }; const methodType = 'create'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType, createdIdentifier: 'versionUuid' }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'success' }); }; router.post('/channels/:channelUuid/versions', getOrgId, asyncHandler(postChannelVersion)); const getChannelVersion = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['channels'] // #swagger.summary = 'Gets a specified channel version' const { orgId } = req; @@ -250,6 +340,10 @@ const getChannelVersion = async (req, res) => { const channelUuid = req.params.channelUuid; const versionUuid = req.params.versionUuid; if(!channelUuid || !versionUuid){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'failure' }); res.status(400).send( 'needs { channelUuid, versionUuid }' ); return; } @@ -260,10 +354,18 @@ const getChannelVersion = async (req, res) => { }; const methodType = 'findOne'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getChannelVersion').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getChannelVersion').inc({ status: 'success' }); }; router.get('/channels/:channelUuid/versions/:versionUuid', getOrgId, asyncHandler(getChannelVersion)); const getClusters = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['clusters'] // #swagger.summary = 'Gets all clusters' const { orgId } = req; @@ -285,10 +387,18 @@ const getClusters = async (req, res) => { }; const methodType = 'findMany'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getClusters').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getClusters').inc({ status: 'success' }); }; router.get('/clusters', getOrgId, asyncHandler(getClusters)); const getCluster = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['clusters'] // #swagger.summary = 'Gets a specified cluster' const { orgId } = req; @@ -306,6 +416,10 @@ const getCluster = async (req, res) => { `; const clusterId = req.params.clusterId; if(!clusterId){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getCluster').inc({ status: 'failure' }); res.status(400).send( 'needs { clusterId }' ); return; } @@ -315,10 +429,18 @@ const getCluster = async (req, res) => { }; const methodType = 'findOne'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getCluster').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getCluster').inc({ status: 'success' }); }; router.get('/clusters/:clusterId', getOrgId, asyncHandler(getCluster)); const postGroups = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['groups'] // #swagger.summary = 'Adds a group' const { orgId } = req; @@ -332,6 +454,10 @@ const postGroups = async (req, res) => { `; const name = req.body.name; if(!name){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postGroups').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postGroups').inc({ status: 'failure' }); res.status(400).send( 'needs { name }' ); return; } @@ -352,11 +478,19 @@ const postGroups = async (req, res) => { const methodType = 'create'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType, createdIdentifier: 'uuid' }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postGroups').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postGroups').inc({ status: 'success' }); }; router.post('/groups', getOrgId, asyncHandler(postGroups)); // PUT to a group only supports setting clusters (can't change name etc) const putGroup = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['groups'] // #swagger.summary = 'Sets the clusters for a specified group' const { orgId } = req; @@ -371,6 +505,10 @@ const putGroup = async (req, res) => { const uuid = req.params.uuid; const clusters = req.body.clusters; if(!uuid || !clusters){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('putGroup').observe(durationInSeconds); + customMetricsClient.apiCallCounter('putGroup').inc({ status: 'failure' }); res.status(400).send( 'needs { uuid, clusters }' ); return; } @@ -381,10 +519,18 @@ const putGroup = async (req, res) => { }; const methodType = 'update'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('putGroup').observe(durationInSeconds); + customMetricsClient.apiCallCounter('putGroup').inc({ status: 'success' }); }; router.put('/groups/:uuid', getOrgId, asyncHandler(putGroup)); const getGroups = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['groups'] // #swagger.summary = 'Gets all groups' const { orgId } = req; @@ -404,10 +550,18 @@ const getGroups = async (req, res) => { }; const methodType = 'findMany'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getGroups').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getGroups').inc({ status: 'success' }); }; router.get('/groups', getOrgId, asyncHandler(getGroups)); const getGroup = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['groups'] // #swagger.summary = 'Gets a specified group' const { orgId } = req; @@ -424,6 +578,10 @@ const getGroup = async (req, res) => { `; const uuid = req.params.uuid; if(!uuid){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getGroup').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getGroup').inc({ status: 'failure' }); res.status(400).send( 'needs { uuid }' ); return; } @@ -433,10 +591,18 @@ const getGroup = async (req, res) => { }; const methodType = 'findOne'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getGroup').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getGroup').inc({ status: 'success' }); }; router.get('/groups/:uuid', getOrgId, asyncHandler(getGroup)); const postSubscriptions = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['subscriptions'] // #swagger.summary = 'Adds a subscription' const { orgId } = req; @@ -454,6 +620,10 @@ const postSubscriptions = async (req, res) => { const channelUuid = req.body.channelUuid; const versionUuid = req.body.versionUuid; if(!name || !groups || clusterId || !channelUuid || !versionUuid){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postSubscriptions').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postSubscriptions').inc({ status: 'failure' }); res.status(400).send( 'needs { name, groups, channelUuid, versionUuid }' ); return; } @@ -467,10 +637,18 @@ const postSubscriptions = async (req, res) => { }; const methodType = 'create'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType, createdIdentifier: 'uuid' }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('postSubscriptions').observe(durationInSeconds); + customMetricsClient.apiCallCounter('postSubscriptions').inc({ status: 'success' }); }; router.post('/subscriptions', getOrgId, asyncHandler(postSubscriptions)); const getSubscriptions = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['subscriptions'] // #swagger.summary = 'Gets all subscriptions' const { orgId } = req; @@ -495,10 +673,18 @@ const getSubscriptions = async (req, res) => { }; const methodType = 'findMany'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getSubscriptions').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getSubscriptions').inc({ status: 'success' }); }; router.get('/subscriptions', getOrgId, asyncHandler(getSubscriptions)); const getSubscription = async (req, res) => { + // Capture the start time when the request starts + const startTime = Date.now(); + // Increment API counter metric + customMetricsClient.apiCallsCount.inc(); // #swagger.tags = ['subscriptions'] // #swagger.summary = 'Gets a specified subscription' const { orgId } = req; @@ -520,6 +706,10 @@ const getSubscription = async (req, res) => { `; const uuid = req.params.uuid; if(!uuid){ + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getSubscription').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getSubscription').inc({ status: 'failure' }); res.status(400).send( 'needs { uuid }' ); return; } @@ -529,6 +719,10 @@ const getSubscription = async (req, res) => { }; const methodType = 'findOne'; await sendReqToGraphql({ req, res, query, variables, operationName, methodType }); + // Observe the duration for the histogram + const durationInSeconds = (Date.now() - startTime) / 1000; + customMetricsClient.apiCallHistogram('getSubscription').observe(durationInSeconds); + customMetricsClient.apiCallCounter('getSubscription').inc({ status: 'success' }); }; router.get('/subscriptions/:uuid', getOrgId, asyncHandler(getSubscription)); diff --git a/package-lock.json b/package-lock.json index 74e7b7b11..1abed0e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -967,7 +967,7 @@ "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/generator": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-module-transforms": "^7.23.0", "@babel/helpers": "^7.23.2", @@ -1041,12 +1041,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", + "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.3", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -1142,9 +1142,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1240,9 +1240,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1296,13 +1296,13 @@ "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/generator": "^7.23.3", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", + "@babel/parser": "^7.23.3", + "@babel/types": "^7.23.3", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1356,9 +1356,9 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", + "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -2178,7 +2178,7 @@ "dependencies": { "@smithy/types": "^2.4.0", "@smithy/util-buffer-from": "^2.0.0", - "@smithy/util-utf8": "^2.0.0", + "@smithy/util-utf8": "^2.0.1", "tslib": "^2.5.0" }, "engines": { @@ -2405,7 +2405,7 @@ "@smithy/util-hex-encoding": "^2.0.0", "@smithy/util-middleware": "^2.0.5", "@smithy/util-uri-escape": "^2.0.0", - "@smithy/util-utf8": "^2.0.0", + "@smithy/util-utf8": "^2.0.1", "tslib": "^2.5.0" }, "engines": { @@ -2451,9 +2451,9 @@ } }, "node_modules/@smithy/util-base64": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.0.tgz", - "integrity": "sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.1.tgz", + "integrity": "sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==", "optional": true, "dependencies": { "@smithy/util-buffer-from": "^2.0.0", @@ -2608,7 +2608,7 @@ "@smithy/util-base64": "^2.0.0", "@smithy/util-buffer-from": "^2.0.0", "@smithy/util-hex-encoding": "^2.0.0", - "@smithy/util-utf8": "^2.0.0", + "@smithy/util-utf8": "^2.0.1", "tslib": "^2.5.0" }, "engines": { @@ -2628,9 +2628,9 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.0.tgz", - "integrity": "sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.1.tgz", + "integrity": "sha512-JvpZMUTMIil6rstH3tyBjCE7tuTmCprcmCXHW4o8a5mSthhQ8fEj5lDWOonTigtB2CqiBQ/SWlpoctuzVO7J0Q==", "optional": true, "dependencies": { "@smithy/util-buffer-from": "^2.0.0", @@ -5482,17 +5482,20 @@ } }, "node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", "dev": true, "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb"