diff --git a/features/step_definitions/cf_mock_server.ts b/features/step_definitions/cf_mock_server.ts new file mode 100644 index 0000000..7bb955c --- /dev/null +++ b/features/step_definitions/cf_mock_server.ts @@ -0,0 +1,39 @@ +import http from "http"; +import {AddressInfo} from "net"; + +export class CloudflareMockServer { + server: http.Server | undefined; + + start() { + let self = this; + this.server = http.createServer((req, res) => { + var body = ""; + req.on('readable', function() { + let part = req.read(); + if (part !== undefined && part !== null) { + body += part; + } + }); + req.on('end', function() { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('OK'); + }); + }); + this.server.listen(() => { + console.log('opened server on', self.server?.address()); + }); + } + + url() { + const { port } = this.server?.address() as AddressInfo; + return `http://localhost:${port}/`; + } + + async dispose() { + if (this.server != undefined) { + this.server.close(); + this.server = undefined; + } + } +} diff --git a/features/step_definitions/mf.ts b/features/step_definitions/mf.ts new file mode 100644 index 0000000..0d010e9 --- /dev/null +++ b/features/step_definitions/mf.ts @@ -0,0 +1,76 @@ +import {Log, LogLevel, Miniflare} from "miniflare"; +import { MockAgent } from "undici"; + +export class MiniflareDriver { + mockAgent = new MockAgent(); + mf: Miniflare | undefined; + + start(options?: {metricsUrl?: string, cloudflareApiUrl?: string}): Miniflare { + this.mockAgent + .get("https://cloudflare.com") + .intercept({ path: "/" }) + .reply(200, "cloudflare!"); + + this.mockAgent + .get("https://jsonplaceholder.typicode.com") + .intercept({ path: "/todos/1" }) + .reply( + 200, + { + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false, + }, + { + headers: { + "content-type": "application/json", + }, + } + ); + + let metricsUrl = ""; + let cloudflareApiUrl = ""; + if (options !== undefined) { + if (options.metricsUrl !== undefined) { + metricsUrl = options.metricsUrl; + } + if (options.cloudflareApiUrl !== undefined) { + cloudflareApiUrl = options.cloudflareApiUrl; + } + } + + this.mf = new Miniflare({ + log: new Log(LogLevel.DEBUG), // Enable debug messages + cachePersist: false, + d1Persist: false, + kvPersist: false, + r2Persist: false, + workers: [{ + scriptPath: "./build/worker/shim.mjs", + compatibilityDate: "2022-04-05", + cache: true, + modules: true, + modulesRules: [ + { type: "CompiledWasm", include: ["**/*.wasm"], fallthrough: true }, + ], + fetchMock: this.mockAgent, + }]}); + return this.mf; + } + + dispose() { + if (this.mf === undefined) { + return; + } + let promise = this.mf.dispose(); + this.mf = undefined; + return promise; + } + + async trigger() { + this.start({}); + await this.mf?.dispatchFetch("http://localhost:8787/cdn-cgi/mf/scheduled"); + this.dispose(); + } +} diff --git a/features/step_definitions/o11y_steps.ts b/features/step_definitions/o11y_steps.ts index 4394f8c..a7859ad 100644 --- a/features/step_definitions/o11y_steps.ts +++ b/features/step_definitions/o11y_steps.ts @@ -1,27 +1,33 @@ -import {Given, When, Then} from '@cucumber/cucumber'; +import {After, Given, When, Then} from '@cucumber/cucumber'; +import {cloudflareMockServer, mf, mfConfig, otelServer} from "./state"; +import {expect} from "chai"; Given('Worker is configured to point to mock Cloudflare API', function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; + cloudflareMockServer.start(); + mfConfig.cloudflareApiUrl = cloudflareMockServer.url(); }); Given('Worker is configured to send metrics to a mock OpenTelemetry collector', function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; + otelServer.start(); + mfConfig.metricsUrl = otelServer.metricsUrl(); }); -When('Worker is triggered', function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; +When('Worker is triggered', async function () { + await mf.trigger(); }); Then('Worker metrics are published', function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; + let metrics = otelServer.getMetrics(); + expect(metrics).to.have.length.gte(1); }); - -Then('Meter name should include {string}', function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; +Then('Meter name should include {string}', function (metricName: string) { + let metricNames = otelServer.getMetricNames(); + expect(metricNames).to.contain(metricName); }); + +After(async function () { + await mf.dispose(); + await cloudflareMockServer.dispose(); + await otelServer.dispose(); +}) diff --git a/features/step_definitions/otel_server.ts b/features/step_definitions/otel_server.ts new file mode 100644 index 0000000..d42ad92 --- /dev/null +++ b/features/step_definitions/otel_server.ts @@ -0,0 +1,73 @@ +import http from 'http'; +import {IExportMetricsServiceRequest} from "@opentelemetry/otlp-transformer"; +import {AddressInfo} from "net"; + +export class OpenTelemetryServer { + server: http.Server | undefined; + metrics: IExportMetricsServiceRequest[] = []; + metricNames: Map = new Map(); + + private reset() { + this.metrics = []; + this.indexMetrics(); + } + + start() { + let self = this; + self.reset(); + this.server = http.createServer((req, res) => { + var body = ""; + req.on('readable', function() { + let part = req.read(); + if (part !== undefined && part !== null) { + body += part; + } + }); + req.on('end', function() { + const metrics = JSON.parse(body) as IExportMetricsServiceRequest; + self.metrics.push(metrics); + self.indexMetrics(); + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('OK'); + }); + }); + this.server.listen(() => { + console.log('opened server on', self.server?.address()); + }); + } + + indexMetrics() { + let self = this; + this.metricNames.clear(); + for (let metrics of this.metrics) { + for (let resourceMetrics of metrics.resourceMetrics) { + for (let scopeMetrics of resourceMetrics.scopeMetrics) { + for (let metric of scopeMetrics.metrics) { + self.metricNames.set(metric.name, 1); + } + } + } + } + } + + metricsUrl() { + const { port } = this.server?.address() as AddressInfo; + return `http://localhost:${port}/v1/metrics`; + } + + async dispose() { + if (this.server != undefined) { + this.server.close(); + this.server = undefined; + } + } + + getMetrics() { + return this.metrics; + } + + getMetricNames() { + return this.metricNames.keys(); + } +} diff --git a/features/step_definitions/state.ts b/features/step_definitions/state.ts new file mode 100644 index 0000000..803f975 --- /dev/null +++ b/features/step_definitions/state.ts @@ -0,0 +1,19 @@ +import {MiniflareDriver} from "./mf"; +import {OpenTelemetryServer} from "./otel_server"; +import {CloudflareMockServer} from "./cf_mock_server"; + +const mf = new MiniflareDriver(); +const otelServer = new OpenTelemetryServer(); +const cloudflareMockServer = new CloudflareMockServer(); + +type MfConfig = { + metricsUrl: string|undefined; + cloudflareApiUrl: string|undefined +}; + +const mfConfig: MfConfig = { + metricsUrl: undefined, + cloudflareApiUrl: undefined, +} + +export { mf, mfConfig, otelServer, cloudflareMockServer }; diff --git a/package-lock.json b/package-lock.json index c6ac386..223b243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@cloudflare/workers-types": "^4.20240405.0", "@cucumber/cucumber": "^10.4.0", "@cucumber/pretty-formatter": "^1.0.1", + "@opentelemetry/otlp-transformer": "^0.49.1", "@types/chai": "^4.3.14", "@types/ws": "^8.5.10", "chai": "^4.3.0", @@ -800,6 +801,138 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz", + "integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==", + "dev": true, + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.22.0.tgz", + "integrity": "sha512-0VoAlT6x+Xzik1v9goJ3pZ2ppi6+xd3aUfg4brfrLkDBHRIVjMP0eBHrKrhB+NKcDyMAg8fAbGL3Npg/F6AwWA==", + "dev": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "1.22.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.9.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.49.1.tgz", + "integrity": "sha512-Z+koA4wp9L9e3jkFacyXTGphSWTbOKjwwXMpb0CxNb0kjTHGUxhYRN8GnkLFsFo5NbZPjP07hwAqeEG/uCratQ==", + "dev": true, + "dependencies": { + "@opentelemetry/api-logs": "0.49.1", + "@opentelemetry/core": "1.22.0", + "@opentelemetry/resources": "1.22.0", + "@opentelemetry/sdk-logs": "0.49.1", + "@opentelemetry/sdk-metrics": "1.22.0", + "@opentelemetry/sdk-trace-base": "1.22.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.9.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.22.0.tgz", + "integrity": "sha512-+vNeIFPH2hfcNL0AJk/ykJXoUCtR1YaDUZM+p3wZNU4Hq98gzq+7b43xbkXjadD9VhWIUQqEwXyY64q6msPj6A==", + "dev": true, + "dependencies": { + "@opentelemetry/core": "1.22.0", + "@opentelemetry/semantic-conventions": "1.22.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.9.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.49.1.tgz", + "integrity": "sha512-gCzYWsJE0h+3cuh3/cK+9UwlVFyHvj3PReIOCDOmdeXOp90ZjKRoDOJBc3mvk1LL6wyl1RWIivR8Rg9OToyesw==", + "dev": true, + "dependencies": { + "@opentelemetry/core": "1.22.0", + "@opentelemetry/resources": "1.22.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.9.0", + "@opentelemetry/api-logs": ">=0.39.1" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.22.0.tgz", + "integrity": "sha512-k6iIx6H3TZ+BVMr2z8M16ri2OxWaljg5h8ihGJxi/KQWcjign6FEaEzuigXt5bK9wVEhqAcWLCfarSftaNWkkg==", + "dev": true, + "dependencies": { + "@opentelemetry/core": "1.22.0", + "@opentelemetry/resources": "1.22.0", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.9.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.22.0.tgz", + "integrity": "sha512-pfTuSIpCKONC6vkTpv6VmACxD+P1woZf4q0K46nSUvXFvOFqjBYKFaAMkKD3M1mlKUUh0Oajwj35qNjMl80m1Q==", + "dev": true, + "dependencies": { + "@opentelemetry/core": "1.22.0", + "@opentelemetry/resources": "1.22.0", + "@opentelemetry/semantic-conventions": "1.22.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.9.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.22.0.tgz", + "integrity": "sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 6954aa7..f9dccde 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@cloudflare/workers-types": "^4.20240405.0", "@cucumber/cucumber": "^10.4.0", "@cucumber/pretty-formatter": "^1.0.1", + "@opentelemetry/otlp-transformer": "^0.49.1", "@types/chai": "^4.3.14", "@types/ws": "^8.5.10", "chai": "^4.3.0",