From 730b5c073b900145bdaf3e6d01bc72c057af0513 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Mon, 11 Nov 2024 15:26:05 -0800 Subject: [PATCH 1/4] Add live API integration check script This basic script sends some metrics to Datadog using this library, then queries the API for those same values to make sure they were successfully ingested and will be visible in Datadog's UI. This is mainly meant as a simple test to ensure changes to the underlying Datadog client keep sending things in the correct form. Unit tests *mostly* cover this use case, so the check here is pretty lightweight. We don't check every metric type, for example. But this is still important. Fixes #89. --- .gitignore | 4 ++ package.json | 1 + test/integration_check.js | 114 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 test/integration_check.js diff --git a/.gitignore b/.gitignore index 4f0df76..5841282 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ +.DS_Store + # Logs logs *.log # IDE .idea +.vscode # Runtime data pids @@ -39,6 +42,7 @@ package-lock.json # Users Environment Variables .lock-wscript +.env # Scratch working files scratch.* diff --git a/package.json b/package.json index 71a910c..c651ff9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "scripts": { "prepack": "npm run clean && npm run build-types", "test": "mocha --reporter spec", + "check-integration": "node test/integration_check.js", "check-codestyle": "npx eslint .", "check-text": "test/lint_text.sh", "build-types": "tsc --build", diff --git a/test/integration_check.js b/test/integration_check.js new file mode 100644 index 0000000..beef47d --- /dev/null +++ b/test/integration_check.js @@ -0,0 +1,114 @@ +/** + * A basic test of our complete integration with Datadog. This check sends some + * metrics, then queries to make sure they actually got ingested correctly by + * Datadog and will show up as expected. + */ + +'use strict'; + +const { setTimeout } = require('node:timers/promises'); +const { client, v1 } = require('@datadog/datadog-api-client'); +const datadogMetrics = require('..'); + +function floorTo(value, points) { + const factor = 10 ** points; + return Math.round(value * factor) / factor; +} + +// Make timestamps round seconds for ease of comparison. +const NOW = floorTo(Date.now(), -3); +const MINUTE = 60 * 1000; + +// How long to keep querying for the metric before giving up. +const MAX_WAIT_TIME = 2.5 * MINUTE; +// How long to wait between checks. +const CHECK_INTERVAL_SECONDS = 15; + +const metricName = 'node.datadog.metrics.test.gauge'; +const metricTags = ['test-tag-1']; +const metricPoints = [ + [NOW - 60 * 1000, floorTo(10 * Math.random(), 1)], + [NOW - 30 * 1000, floorTo(10 * Math.random(), 1)], +]; + +async function main() { + await sendMetrics(); + await setTimeout(5000); + const result = await waitForSentMetrics(); + + if (!result) { + process.exitCode = 1; + } +} + +async function sendMetrics() { + console.log(`Sending random points for "${metricName}"`); + + datadogMetrics.init({ + flushIntervalSeconds: 0 + }); + + for (const [timestamp, value] of metricPoints) { + datadogMetrics.gauge(metricName, value, metricTags, timestamp); + await datadogMetrics.flush(); + } +} + +async function queryMetrics() { + const configuration = client.createConfiguration({ + authMethods: { + apiKeyAuth: process.env.DATADOG_API_KEY, + appKeyAuth: process.env.DATADOG_APP_KEY, + }, + }); + configuration.setServerVariables({ site: process.env.DATADOG_API_HOST }); + const metricsApi = new v1.MetricsApi(configuration); + + // NOTE: Query timestamps are seconds, but result points are milliseconds. + const data = await metricsApi.queryMetrics({ + from: Math.floor((NOW - 5 * MINUTE) / 1000), + to: Math.ceil(Date.now() / 1000), + query: `${metricName}{${metricTags[0]}}`, + }); + + return data.series && data.series[0]; +} + +async function waitForSentMetrics() { + const endTime = Date.now() + MAX_WAIT_TIME; + while (Date.now() < endTime) { + console.log('Querying Datadog for sent metrics...'); + const series = await queryMetrics(); + + if (series) { + const found = metricPoints.every(([timestamp, value]) => { + return series.pointlist.some(([remoteTimestamp, remoteValue]) => { + // Datadog may round values differently or place them into + // time intervals based on the metric's configuration. Look + // for timestamp/value combinations that are close enough. + return ( + Math.abs(remoteTimestamp - timestamp) < 10000 && + Math.abs(remoteValue - value) < 0.1 + ); + }); + }); + + if (found) { + console.log(' Found sent metrics!'); + return true; + } else { + console.log(' Found series, but with no matching points.'); + console.log(` Looking for: ${JSON.stringify(metricPoints)}`); + console.log(' Found:', JSON.stringify(series, null, 2)); + } + } + + console.log(` Nothing found, waiting ${CHECK_INTERVAL_SECONDS}s before trying again.`); + await setTimeout(CHECK_INTERVAL_SECONDS * 1000); + } + + console.log('Nothing found.'); + return false; +} + +main().catch(error => console.error(error)); From ebfe508848495b7ecb4d1225951916f5cafc4f85 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Tue, 12 Nov 2024 12:24:34 -0800 Subject: [PATCH 2/4] Make integration check work in older Node.js and without depending on #125. --- test/integration_check.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/integration_check.js b/test/integration_check.js index beef47d..08ac013 100644 --- a/test/integration_check.js +++ b/test/integration_check.js @@ -6,7 +6,6 @@ 'use strict'; -const { setTimeout } = require('node:timers/promises'); const { client, v1 } = require('@datadog/datadog-api-client'); const datadogMetrics = require('..'); @@ -15,6 +14,11 @@ function floorTo(value, points) { return Math.round(value * factor) / factor; } +// Remove when upgrading to Node.js 16; this is built-in (node:times/promises). +function sleep(milliseconds) { + return new Promise(r => setTimeout(r, milliseconds)); +} + // Make timestamps round seconds for ease of comparison. const NOW = floorTo(Date.now(), -3); const MINUTE = 60 * 1000; @@ -33,7 +37,7 @@ const metricPoints = [ async function main() { await sendMetrics(); - await setTimeout(5000); + await sleep(5000); const result = await waitForSentMetrics(); if (!result) { @@ -44,13 +48,13 @@ async function main() { async function sendMetrics() { console.log(`Sending random points for "${metricName}"`); - datadogMetrics.init({ - flushIntervalSeconds: 0 - }); + datadogMetrics.init({ flushIntervalSeconds: 0 }); for (const [timestamp, value] of metricPoints) { datadogMetrics.gauge(metricName, value, metricTags, timestamp); - await datadogMetrics.flush(); + await new Promise((resolve, reject) => { + datadogMetrics.flush(resolve, reject); + }); } } @@ -104,11 +108,13 @@ async function waitForSentMetrics() { } console.log(` Nothing found, waiting ${CHECK_INTERVAL_SECONDS}s before trying again.`); - await setTimeout(CHECK_INTERVAL_SECONDS * 1000); + await sleep(CHECK_INTERVAL_SECONDS * 1000); } console.log('Nothing found.'); return false; } -main().catch(error => console.error(error)); +if (require.main === module) { + main().catch(error => console.error(error)); +} From 24aad08b29da4c1424250087fe2c6d75715aa672 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Tue, 12 Nov 2024 12:35:58 -0800 Subject: [PATCH 3/4] Move check scripts to `test-other` directory It turns out Mocha was seeing these (or at least the ones that are JS) and at least executing the top-level code. We could adjust Mocha's settings, but it's probably better to separate these checks anyway, since they aren't really unit tests. --- package.json | 6 +++--- {test => test-other}/integration_check.js | 0 {test => test-other}/lint_text.sh | 0 {test => test-other}/types_check.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename {test => test-other}/integration_check.js (100%) rename {test => test-other}/lint_text.sh (100%) rename {test => test-other}/types_check.ts (100%) diff --git a/package.json b/package.json index c651ff9..6602d2a 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "scripts": { "prepack": "npm run clean && npm run build-types", "test": "mocha --reporter spec", - "check-integration": "node test/integration_check.js", + "check-integration": "node test-other/integration_check.js", "check-codestyle": "npx eslint .", - "check-text": "test/lint_text.sh", + "check-text": "test-other/lint_text.sh", "build-types": "tsc --build", - "check-types": "tsc --noEmit --strict test/types_check.ts", + "check-types": "tsc --noEmit --strict test-other/types_check.ts", "clean": "tsc --build --clean" }, "keywords": [ diff --git a/test/integration_check.js b/test-other/integration_check.js similarity index 100% rename from test/integration_check.js rename to test-other/integration_check.js diff --git a/test/lint_text.sh b/test-other/lint_text.sh similarity index 100% rename from test/lint_text.sh rename to test-other/lint_text.sh diff --git a/test/types_check.ts b/test-other/types_check.ts similarity index 100% rename from test/types_check.ts rename to test-other/types_check.ts From 57d0203580bf16734fe6102974ccf59e65537d3e Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 13 Nov 2024 09:33:35 -0800 Subject: [PATCH 4/4] Also test distributions Since distributions get send through an entirely different API endpoint, we should include them in the test. --- test-other/integration_check.js | 61 +++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/test-other/integration_check.js b/test-other/integration_check.js index 08ac013..d51a228 100644 --- a/test-other/integration_check.js +++ b/test-other/integration_check.js @@ -28,37 +28,54 @@ const MAX_WAIT_TIME = 2.5 * MINUTE; // How long to wait between checks. const CHECK_INTERVAL_SECONDS = 15; -const metricName = 'node.datadog.metrics.test.gauge'; -const metricTags = ['test-tag-1']; -const metricPoints = [ +const testPoints = [ [NOW - 60 * 1000, floorTo(10 * Math.random(), 1)], [NOW - 30 * 1000, floorTo(10 * Math.random(), 1)], ]; +const testMetrics = [ + { + type: 'gauge', + name: 'node.datadog.metrics.test.gauge', + tags: ['test-tag-1'], + }, + { + type: 'distribution', + name: 'node.datadog.metrics.test.dist', + tags: ['test-tag-2'], + }, +]; + async function main() { - await sendMetrics(); + datadogMetrics.init({ flushIntervalSeconds: 0 }); + + for (const metric of testMetrics) { + await sendMetric(metric); + } + await sleep(5000); - const result = await waitForSentMetrics(); - if (!result) { - process.exitCode = 1; + for (const metric of testMetrics) { + const result = await waitForSentMetric(metric); + + if (!result) { + process.exitCode = 1; + } } } -async function sendMetrics() { - console.log(`Sending random points for "${metricName}"`); - - datadogMetrics.init({ flushIntervalSeconds: 0 }); +async function sendMetric(metric) { + console.log(`Sending random points for ${metric.type} "${metric.name}"`); - for (const [timestamp, value] of metricPoints) { - datadogMetrics.gauge(metricName, value, metricTags, timestamp); + for (const [timestamp, value] of testPoints) { + datadogMetrics[metric.type](metric.name, value, metric.tags, timestamp); await new Promise((resolve, reject) => { datadogMetrics.flush(resolve, reject); }); } } -async function queryMetrics() { +async function queryMetric(metric) { const configuration = client.createConfiguration({ authMethods: { apiKeyAuth: process.env.DATADOG_API_KEY, @@ -72,20 +89,20 @@ async function queryMetrics() { const data = await metricsApi.queryMetrics({ from: Math.floor((NOW - 5 * MINUTE) / 1000), to: Math.ceil(Date.now() / 1000), - query: `${metricName}{${metricTags[0]}}`, + query: `${metric.name}{${metric.tags[0]}}`, }); return data.series && data.series[0]; } -async function waitForSentMetrics() { +async function waitForSentMetric(metric) { const endTime = Date.now() + MAX_WAIT_TIME; while (Date.now() < endTime) { - console.log('Querying Datadog for sent metrics...'); - const series = await queryMetrics(); + console.log(`Querying Datadog for sent points in ${metric.type} "${metric.name}"...`); + const series = await queryMetric(metric); if (series) { - const found = metricPoints.every(([timestamp, value]) => { + const found = testPoints.every(([timestamp, value]) => { return series.pointlist.some(([remoteTimestamp, remoteValue]) => { // Datadog may round values differently or place them into // time intervals based on the metric's configuration. Look @@ -98,11 +115,11 @@ async function waitForSentMetrics() { }); if (found) { - console.log(' Found sent metrics!'); + console.log('✔︎ Found sent points! Test passed.'); return true; } else { console.log(' Found series, but with no matching points.'); - console.log(` Looking for: ${JSON.stringify(metricPoints)}`); + console.log(` Looking for: ${JSON.stringify(testPoints)}`); console.log(' Found:', JSON.stringify(series, null, 2)); } } @@ -111,7 +128,7 @@ async function waitForSentMetrics() { await sleep(CHECK_INTERVAL_SECONDS * 1000); } - console.log('Nothing found.'); + console.log('✘ Nothing found and gave up waiting. Test failed!'); return false; }