diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2250348..8ca9684 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,29 @@ jobs: run: | npm test + test-integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.default_node_version }} + cache: 'npm' + cache-dependency-path: '**/package.json' + + - name: Install dependencies + run: npm install + + - name: Check Integration + env: + DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }} + DATADOG_APP_KEY: ${{ secrets.DATADOG_APP_KEY }} + DATADOG_SITE: ${{ secrets.DATADOG_API_HOST }} + run: | + npm run check-integration + lint: runs-on: ubuntu-latest steps: 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 2838bce..dc035d1 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,11 @@ "scripts": { "prepack": "npm run clean && npm run build-types && npm run check-types", "test": "mocha --reporter spec", + "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-other/integration_check.js b/test-other/integration_check.js new file mode 100644 index 0000000..a9c884a --- /dev/null +++ b/test-other/integration_check.js @@ -0,0 +1,140 @@ +/** + * 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 { client, v1 } = require('@datadog/datadog-api-client'); +const datadogMetrics = require('..'); + +function floorTo(value, points) { + const factor = 10 ** 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; + +// 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 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() { + datadogMetrics.init({ flushIntervalSeconds: 0 }); + + for (const metric of testMetrics) { + await sendMetric(metric); + } + + await sleep(5000); + + for (const metric of testMetrics) { + const result = await waitForSentMetric(metric); + + if (!result) { + process.exitCode = 1; + } + } +} + +async function sendMetric(metric) { + console.log(`Sending random points for ${metric.type} "${metric.name}"`); + + 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 queryMetric(metric) { + const configuration = client.createConfiguration({ + authMethods: { + apiKeyAuth: process.env.DATADOG_API_KEY, + appKeyAuth: process.env.DATADOG_APP_KEY, + }, + }); + configuration.setServerVariables({ site: process.env.DATADOG_SITE }); + 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: `${metric.name}{${metric.tags[0]}}`, + }); + + return data.series && data.series[0]; +} + +async function waitForSentMetric(metric) { + const endTime = Date.now() + MAX_WAIT_TIME; + while (Date.now() < endTime) { + console.log(`Querying Datadog for sent points in ${metric.type} "${metric.name}"...`); + const series = await queryMetric(metric); + + if (series) { + 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 + // 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 points! Test passed.'); + return true; + } else { + console.log(' Found series, but with no matching points.'); + console.log(` Looking for: ${JSON.stringify(testPoints)}`); + console.log(' Found:', JSON.stringify(series, null, 2)); + } + } + + console.log(` Nothing found, waiting ${CHECK_INTERVAL_SECONDS}s before trying again.`); + await sleep(CHECK_INTERVAL_SECONDS * 1000); + } + + console.log('✘ Nothing found and gave up waiting. Test failed!'); + return false; +} + +if (require.main === module) { + main().catch(error => { + process.exitCode = 1; + console.error(error); + }); +} 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