From 6c562f71eff9c7c35943f46045a864de358019d4 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 13 Feb 2025 18:03:30 +0100 Subject: [PATCH 01/11] feat(mockotlpserver): add http proxy option --- packages/mockotlpserver/lib/cli.js | 6 ++ packages/mockotlpserver/lib/http.js | 9 ++- packages/mockotlpserver/lib/mockotlpserver.js | 3 + packages/mockotlpserver/lib/proxy.js | 78 +++++++++++++++++++ packages/opentelemetry-node/test/testutils.js | 1 + 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/mockotlpserver/lib/proxy.js diff --git a/packages/mockotlpserver/lib/cli.js b/packages/mockotlpserver/lib/cli.js index 2a604c7e..7eb0180e 100755 --- a/packages/mockotlpserver/lib/cli.js +++ b/packages/mockotlpserver/lib/cli.js @@ -118,6 +118,11 @@ const OPTIONS = [ type: 'bool', help: 'Start a web server to inspect traces with some charts.', }, + { + names: ['httpProxy'], + type: 'string', + help: `Set this option to a URL to proxy all HTTP request to another server. This won't stop the processing of OTLP data.`, + }, ]; async function main() { @@ -165,6 +170,7 @@ async function main() { services, grpcHostname: opts.hostname || DEFAULT_HOSTNAME, httpHostname: opts.hostname || DEFAULT_HOSTNAME, + httpProxy: opts.httpProxy, uiHostname: opts.hostname || DEFAULT_HOSTNAME, }); await otlpServer.start(); diff --git a/packages/mockotlpserver/lib/http.js b/packages/mockotlpserver/lib/http.js index 862f6b43..c634a3b7 100644 --- a/packages/mockotlpserver/lib/http.js +++ b/packages/mockotlpserver/lib/http.js @@ -27,6 +27,7 @@ const { } = require('./diagch'); const {getProtoRoot} = require('./proto'); const {Service} = require('./service'); +const {proxyHttp} = require('./proxy'); const protoRoot = getProtoRoot(); @@ -125,6 +126,7 @@ class HttpService extends Service { * @param {import('./luggite').Logger} opts.log * @param {string} opts.hostname * @param {number} opts.port + * @param {string} [opts.proxy] */ constructor(opts) { super(); @@ -133,7 +135,7 @@ class HttpService extends Service { } async start() { - const {log, hostname, port} = this._opts; + const {log, hostname, port, proxy} = this._opts; this._server = http.createServer((req, res) => { const contentType = req.headers['content-type']; if (!parsersMap[contentType]) { @@ -143,6 +145,11 @@ class HttpService extends Service { ); } + // Do a proxy request if configured. This won't stop its processing + if (proxy) { + proxyHttp(log, proxy, req); + } + const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { diff --git a/packages/mockotlpserver/lib/mockotlpserver.js b/packages/mockotlpserver/lib/mockotlpserver.js index 8a69e325..c2a276ff 100644 --- a/packages/mockotlpserver/lib/mockotlpserver.js +++ b/packages/mockotlpserver/lib/mockotlpserver.js @@ -69,6 +69,7 @@ class MockOtlpServer { * and 'ui'. If not provided, then defaults to starting all services. * @param {string} [opts.httpHostname] Default 'localhost'. * @param {number} [opts.httpPort] Default 4318. Use 0 to select a free port. + * @param {string} [opts.httpProxy] Default undefined. * @param {string} [opts.grpcHostname] Default 'localhost'. * @param {number} [opts.grpcPort] Default 4317. Use 0 to select a free port. * @param {string} [opts.uiHostname] Default 'localhost'. @@ -86,6 +87,7 @@ class MockOtlpServer { this._services = opts.services ?? ['http', 'grpc', 'ui']; this._httpHostname = opts.httpHostname ?? DEFAULT_HOSTNAME; this._httpPort = opts.httpPort ?? DEFAULT_HTTP_PORT; + this._httpProxy = opts.httpProxy; this._grpcHostname = opts.grpcHostname ?? DEFAULT_HOSTNAME; this._grpcPort = opts.grpcPort ?? DEFAULT_GRPC_PORT; this._uiHostname = opts.uiHostname ?? DEFAULT_HOSTNAME; @@ -118,6 +120,7 @@ class MockOtlpServer { log: this._log, hostname: this._httpHostname, port: this._httpPort, + proxy: this._httpProxy, }); await this._httpService.start(); this.httpUrl = this._httpService.url; diff --git a/packages/mockotlpserver/lib/proxy.js b/packages/mockotlpserver/lib/proxy.js new file mode 100644 index 00000000..2eedaec2 --- /dev/null +++ b/packages/mockotlpserver/lib/proxy.js @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 http = require('http'); +const https = require('https'); + +/** + * @param {import('./luggite').Logger} log + * @param {string} target + * @param {http.IncomingMessage} req + */ +function proxyHttp(log, target, req) { + /** @type {URL} */ + let targetUrl; + + try { + targetUrl = new URL(target); + } catch { + log.warn(`Cannot proxy request to target "${target}". Invalid URL.`); + return; + } + + const {protocol, host} = targetUrl; + if (protocol !== 'http:' && protocol !== 'https:') { + log.warn( + `Invalid protocol for proxy requests to "${target}". Valid protocols are: http, https.` + ); + return; + } + + const httpFlavor = protocol === 'http:' ? http : https; + const proxyUrl = new URL(req.url, `${protocol}//${host}/`); + const headers = {...req.headers}; + // XXX: missing how to pass this. discuss in meeting + headers['authorization'] = 'Bearer XXXXX'; + delete headers.host; + const options = { + host, + method: req.method, + headers, + path: proxyUrl.pathname, + search: proxyUrl.search, + }; + log.info(options, 'proxy request options'); + const proxyReq = httpFlavor.request(options, (res) => { + log.info('proxy response callback received'); + const chunks = []; + res.on('data', (chunk) => { + log.info('chunk of proxy response'); + chunks.push(chunk); + }); + res.on('end', () => { + log.info('proxy response ended'); + log.info(Buffer.concat(chunks).toString('utf-8')); + }); + }); + req.pipe(proxyReq); +} + +module.exports = { + proxyHttp, +}; diff --git a/packages/opentelemetry-node/test/testutils.js b/packages/opentelemetry-node/test/testutils.js index ff7b374a..9ef25b65 100644 --- a/packages/opentelemetry-node/test/testutils.js +++ b/packages/opentelemetry-node/test/testutils.js @@ -596,6 +596,7 @@ function runTestFixtures(suite, testFixtures) { services: ['http'], httpHostname: '127.0.0.1', // avoid default 'localhost' because possible IPv6 httpPort: 0, + // TODO: add proxy here (with auth headers) onTrace: collector.onTrace.bind(collector), onMetrics: collector.onMetrics.bind(collector), onLogs: collector.onLogs.bind(collector), From 2fb610dc6873f93a55e6755255fb8f35a1ba70c2 Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 19 Feb 2025 12:57:04 +0100 Subject: [PATCH 02/11] chore: tunnel connect requests --- packages/mockotlpserver/lib/cli.js | 6 +- packages/mockotlpserver/lib/http.js | 61 ++++++++++--------- packages/mockotlpserver/lib/mockotlpserver.js | 6 +- .../lib/{proxy.js => tunnel.js} | 51 +++++++--------- 4 files changed, 60 insertions(+), 64 deletions(-) rename packages/mockotlpserver/lib/{proxy.js => tunnel.js} (58%) diff --git a/packages/mockotlpserver/lib/cli.js b/packages/mockotlpserver/lib/cli.js index 7eb0180e..5901455e 100755 --- a/packages/mockotlpserver/lib/cli.js +++ b/packages/mockotlpserver/lib/cli.js @@ -119,9 +119,9 @@ const OPTIONS = [ help: 'Start a web server to inspect traces with some charts.', }, { - names: ['httpProxy'], + names: ['tunnel', 't'], type: 'string', - help: `Set this option to a URL to proxy all HTTP request to another server. This won't stop the processing of OTLP data.`, + help: `Set this option to a URL to send all HTTP requests to another server. This won't stop the processing of OTLP data.`, }, ]; @@ -170,7 +170,7 @@ async function main() { services, grpcHostname: opts.hostname || DEFAULT_HOSTNAME, httpHostname: opts.hostname || DEFAULT_HOSTNAME, - httpProxy: opts.httpProxy, + tunnel: opts.tunnel, uiHostname: opts.hostname || DEFAULT_HOSTNAME, }); await otlpServer.start(); diff --git a/packages/mockotlpserver/lib/http.js b/packages/mockotlpserver/lib/http.js index c634a3b7..d7560105 100644 --- a/packages/mockotlpserver/lib/http.js +++ b/packages/mockotlpserver/lib/http.js @@ -27,7 +27,7 @@ const { } = require('./diagch'); const {getProtoRoot} = require('./proto'); const {Service} = require('./service'); -const {proxyHttp} = require('./proxy'); +const {createHttpTunnel} = require('./tunnel'); const protoRoot = getProtoRoot(); @@ -126,7 +126,7 @@ class HttpService extends Service { * @param {import('./luggite').Logger} opts.log * @param {string} opts.hostname * @param {number} opts.port - * @param {string} [opts.proxy] + * @param {string} [opts.tunnel] */ constructor(opts) { super(); @@ -135,44 +135,49 @@ class HttpService extends Service { } async start() { - const {log, hostname, port, proxy} = this._opts; + const {log, hostname, port, tunnel} = this._opts; + const httpTunnel = tunnel && createHttpTunnel(log, tunnel); + + console.log('tunnel', httpTunnel) this._server = http.createServer((req, res) => { const contentType = req.headers['content-type']; - if (!parsersMap[contentType]) { + + // Tunnel requests if defined or validate otherwise + if (httpTunnel) { + httpTunnel(req, res); + } else if (!parsersMap[contentType]) { return badRequest( res, `unexpected request Content-Type: "${contentType}"` ); } - // Do a proxy request if configured. This won't stop its processing - if (proxy) { - proxyHttp(log, proxy, req); - } - const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { - // TODO: send back the proper error - if (chunks.length === 0) { - return badRequest(res); - } - - // TODO: in future response may add some header to communicate back - // some information about - // - the collector - // - the config - // - something else - // PS: maybe collector could be able to tell the sdk/distro to stop sending - // because of: high load, sample rate changed, et al?? - let resBody = null; - if (contentType === 'application/json') { - resBody = JSON.stringify({ - ok: 1, - }); + // Provide a response if there is no tunnel + if (!httpTunnel) { + // TODO: send back the proper error + if (chunks.length === 0) { + return badRequest(res); + } + + // TODO: in future response may add some header to communicate back + // some information about + // - the collector + // - the config + // - something else + // PS: maybe collector could be able to tell the sdk/distro to stop sending + // because of: high load, sample rate changed, et al?? + let resBody = null; + if (contentType === 'application/json') { + resBody = JSON.stringify({ + ok: 1, + }); + } + res.writeHead(200); + res.end(resBody); } - res.writeHead(200); - res.end(resBody); // We publish into diagnostics channel after returning a response to the client to avoid not returning // a response if one of the handlers throws (the hanlders run synchronously in the diff --git a/packages/mockotlpserver/lib/mockotlpserver.js b/packages/mockotlpserver/lib/mockotlpserver.js index c2a276ff..73bc892c 100644 --- a/packages/mockotlpserver/lib/mockotlpserver.js +++ b/packages/mockotlpserver/lib/mockotlpserver.js @@ -69,9 +69,9 @@ class MockOtlpServer { * and 'ui'. If not provided, then defaults to starting all services. * @param {string} [opts.httpHostname] Default 'localhost'. * @param {number} [opts.httpPort] Default 4318. Use 0 to select a free port. - * @param {string} [opts.httpProxy] Default undefined. * @param {string} [opts.grpcHostname] Default 'localhost'. * @param {number} [opts.grpcPort] Default 4317. Use 0 to select a free port. + * @param {string} [opts.tunnel] Default undefined. * @param {string} [opts.uiHostname] Default 'localhost'. * @param {number} [opts.uiPort] Default 8080. Use 0 to select a free port. * @param {Function} [opts.onTrace] Called for each received trace service request. @@ -87,7 +87,7 @@ class MockOtlpServer { this._services = opts.services ?? ['http', 'grpc', 'ui']; this._httpHostname = opts.httpHostname ?? DEFAULT_HOSTNAME; this._httpPort = opts.httpPort ?? DEFAULT_HTTP_PORT; - this._httpProxy = opts.httpProxy; + this._tunnel = opts.tunnel; this._grpcHostname = opts.grpcHostname ?? DEFAULT_HOSTNAME; this._grpcPort = opts.grpcPort ?? DEFAULT_GRPC_PORT; this._uiHostname = opts.uiHostname ?? DEFAULT_HOSTNAME; @@ -120,7 +120,7 @@ class MockOtlpServer { log: this._log, hostname: this._httpHostname, port: this._httpPort, - proxy: this._httpProxy, + tunnel: this._tunnel, }); await this._httpService.start(); this.httpUrl = this._httpService.url; diff --git a/packages/mockotlpserver/lib/proxy.js b/packages/mockotlpserver/lib/tunnel.js similarity index 58% rename from packages/mockotlpserver/lib/proxy.js rename to packages/mockotlpserver/lib/tunnel.js index 2eedaec2..019d3b8a 100644 --- a/packages/mockotlpserver/lib/proxy.js +++ b/packages/mockotlpserver/lib/tunnel.js @@ -23,9 +23,9 @@ const https = require('https'); /** * @param {import('./luggite').Logger} log * @param {string} target - * @param {http.IncomingMessage} req + * @returns {((req: http.IncomingMessage, res: http.ServerResponse) => void) | undefined} */ -function proxyHttp(log, target, req) { +function createHttpTunnel(log, target) { /** @type {URL} */ let targetUrl; @@ -43,36 +43,27 @@ function proxyHttp(log, target, req) { ); return; } - - const httpFlavor = protocol === 'http:' ? http : https; - const proxyUrl = new URL(req.url, `${protocol}//${host}/`); - const headers = {...req.headers}; - // XXX: missing how to pass this. discuss in meeting - headers['authorization'] = 'Bearer XXXXX'; - delete headers.host; - const options = { - host, - method: req.method, - headers, - path: proxyUrl.pathname, - search: proxyUrl.search, - }; - log.info(options, 'proxy request options'); - const proxyReq = httpFlavor.request(options, (res) => { - log.info('proxy response callback received'); - const chunks = []; - res.on('data', (chunk) => { - log.info('chunk of proxy response'); - chunks.push(chunk); - }); - res.on('end', () => { - log.info('proxy response ended'); - log.info(Buffer.concat(chunks).toString('utf-8')); + + return function httpTunnel(req, res) { + const httpFlavor = protocol === 'http:' ? http : https; + const proxyUrl = new URL(req.url, `${protocol}//${host}/`); + const headers = {...req.headers}; + delete headers.host; + const options = { + host, + method: req.method, + headers, + path: proxyUrl.pathname, + search: proxyUrl.search, + }; + const tunnelReq = httpFlavor.request(options, (tunnelRes) => { + tunnelRes.pipe(res); }); - }); - req.pipe(proxyReq); + req.pipe(tunnelReq); + } } + module.exports = { - proxyHttp, + createHttpTunnel, }; From ecf41c33d220fef7d34b6ad5c334a921503a611b Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 19 Feb 2025 13:00:54 +0100 Subject: [PATCH 03/11] chore: update log messages --- packages/mockotlpserver/lib/tunnel.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/mockotlpserver/lib/tunnel.js b/packages/mockotlpserver/lib/tunnel.js index 019d3b8a..eb7028b7 100644 --- a/packages/mockotlpserver/lib/tunnel.js +++ b/packages/mockotlpserver/lib/tunnel.js @@ -32,15 +32,13 @@ function createHttpTunnel(log, target) { try { targetUrl = new URL(target); } catch { - log.warn(`Cannot proxy request to target "${target}". Invalid URL.`); + log.warn(`Cannot create a tunnel to target "${target}". The given URL is invalid.`); return; } const {protocol, host} = targetUrl; if (protocol !== 'http:' && protocol !== 'https:') { - log.warn( - `Invalid protocol for proxy requests to "${target}". Valid protocols are: http, https.` - ); + log.warn(`Cannot create a tunnel to target "${target}". Protocol must be one of: http, https.`); return; } From f8ca3134bf9873c264b1d026c83794bd6be89272 Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 19 Feb 2025 18:30:27 +0100 Subject: [PATCH 04/11] chore: add use fixtures script --- packages/mockotlpserver/lib/http.js | 1 - .../scripts/use-fixtures.js | 118 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/opentelemetry-node/scripts/use-fixtures.js diff --git a/packages/mockotlpserver/lib/http.js b/packages/mockotlpserver/lib/http.js index d7560105..e553e69b 100644 --- a/packages/mockotlpserver/lib/http.js +++ b/packages/mockotlpserver/lib/http.js @@ -138,7 +138,6 @@ class HttpService extends Service { const {log, hostname, port, tunnel} = this._opts; const httpTunnel = tunnel && createHttpTunnel(log, tunnel); - console.log('tunnel', httpTunnel) this._server = http.createServer((req, res) => { const contentType = req.headers['content-type']; diff --git a/packages/opentelemetry-node/scripts/use-fixtures.js b/packages/opentelemetry-node/scripts/use-fixtures.js new file mode 100644 index 00000000..86ad7bd4 --- /dev/null +++ b/packages/opentelemetry-node/scripts/use-fixtures.js @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 {execSync, execFile} = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const {URL} = require('url'); + +const dotenv = require('dotenv'); +// const semver = require('semver'); +const { MockOtlpServer } = require('@elastic/mockotlpserver'); + +const edotPath = path.join(__dirname, '..'); +const envPath = path.join(edotPath, 'test', 'test-services.env'); +const fixtPath = path.join(edotPath, 'test', 'fixtures'); +const version = require(path.join(edotPath, 'package.json')).version; + +/** + * @param {string} name + * @returns {boolean} + */ +function validFixture(name) { + const exclude = ['-aws-', '-fs', '-fastify', 'host-metrics', 'elastic-openai']; + return name.endsWith('.js') && !exclude.some((e) => name.includes(e)); +} + +async function main() { + if (typeof process.env.OTEL_EXPORTER_OTLP_ENDPOINT !== 'string') { + console.log(`"OTEL_EXPORTER_OTLP_ENDPOINT" not set. Skipping`); + return; + } + + // ??? + // if (semver.lt(process.version, '18.0.0')) { + // console.log(`Process version "${process.version}" not supported. Skipping`); + // return; + // } + Object.keys(process.env).filter((k) => k.startsWith('OTEL_')).forEach((n) => { + console.log(`env: ${n}=${process.env[n]}`); + }) + + // Start the Mock server + const otlpServer = new MockOtlpServer({ + logLevel: 'warn', + services: ['http'], + httpHostname: '127.0.0.1', // avoid default 'localhost' because possible IPv6 + httpPort: 0, + tunnel: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, // set tunnel to the real endpoint + onTrace: console.log, + onMetrics: () => null, + onLogs: () => null, + }); + await otlpServer.start(); + console.log(`MockOtlpServer listening at ${otlpServer.httpUrl.href}`) + + const testEnv = dotenv.parse(Buffer.from(fs.readFileSync(envPath))) + + const fixtures = fs.readdirSync(fixtPath).filter(validFixture); + for (const fixt of fixtures) { + const serviceName = fixt.replace('.js', '-service'); + const fixtFile = path.join(fixtPath, fixt); + + console.log(`running fixture ${fixt}`); + await new Promise((resolve, reject) => { + execFile( + process.execPath, + [fixtFile], + { + killSignal: 'SIGINT', + env: Object.assign( + {}, + process.env, + testEnv, + { + OTEL_EXPORTER_OTLP_ENDPOINT: + otlpServer.httpUrl.href, + OTEL_EXPORTER_OTLP_PROTOCOL: 'http/json', + OTEL_RESOURCE_ATTRIBUTES:`service.name=${serviceName},service.version=${version},deployment.environment=test`, + NODE_OPTIONS: '--require=@elastic/opentelemetry-node', + }, + ), + }, + async function done(err, stdout, stderr) { + if (err) { + console.log(`fixture ${fixt} errored`); + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + return reject(err); + } + console.log(`fixture ${fixt} okay`); + // console.log(`stdout: ${stdout}`); + resolve(); + } + ) + }); + } + + console.log('ran all text fixtures'); + await otlpServer.close(); +} + +main(); From 3251862ab6e59c203dd7c95922c743beef081bc3 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 20 Feb 2025 11:18:28 +0100 Subject: [PATCH 05/11] chore: add warning when using a possibly wrong protocol --- packages/mockotlpserver/lib/tunnel.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/mockotlpserver/lib/tunnel.js b/packages/mockotlpserver/lib/tunnel.js index eb7028b7..e13185e6 100644 --- a/packages/mockotlpserver/lib/tunnel.js +++ b/packages/mockotlpserver/lib/tunnel.js @@ -19,7 +19,6 @@ const http = require('http'); const https = require('https'); - /** * @param {import('./luggite').Logger} log * @param {string} target @@ -41,18 +40,23 @@ function createHttpTunnel(log, target) { log.warn(`Cannot create a tunnel to target "${target}". Protocol must be one of: http, https.`); return; } - + + const port = targetUrl.port || (protocol === 'http:' ? 80 : 443); return function httpTunnel(req, res) { + // APM server does not support 'http/json' protocol + // ref: https://www.elastic.co/guide/en/observability/current/apm-api-otlp.html + const contentType = req.headers['content-type']; + if (contentType !== 'application/x-protobuf') { + log.warn(`Content type "${contentType}" may not be accepted by the target server (${target})`); + } + const httpFlavor = protocol === 'http:' ? http : https; - const proxyUrl = new URL(req.url, `${protocol}//${host}/`); - const headers = {...req.headers}; - delete headers.host; const options = { - host, + host: targetUrl.host, + port, method: req.method, - headers, - path: proxyUrl.pathname, - search: proxyUrl.search, + headers: {...req.headers, host}, + path: req.url, }; const tunnelReq = httpFlavor.request(options, (tunnelRes) => { tunnelRes.pipe(res); @@ -61,7 +65,6 @@ function createHttpTunnel(log, target) { } } - module.exports = { createHttpTunnel, }; From 89a92c7bb7ca7eb5a69c746b9be865672f7b403c Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 20 Feb 2025 12:46:20 +0100 Subject: [PATCH 06/11] chore: start and stop services around using test fixtures --- .../scripts/use-fixtures.js | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/opentelemetry-node/scripts/use-fixtures.js b/packages/opentelemetry-node/scripts/use-fixtures.js index 86ad7bd4..b956603f 100644 --- a/packages/opentelemetry-node/scripts/use-fixtures.js +++ b/packages/opentelemetry-node/scripts/use-fixtures.js @@ -20,16 +20,15 @@ const {execSync, execFile} = require('child_process'); const fs = require('fs'); const path = require('path'); -const {URL} = require('url'); const dotenv = require('dotenv'); // const semver = require('semver'); const { MockOtlpServer } = require('@elastic/mockotlpserver'); const edotPath = path.join(__dirname, '..'); -const envPath = path.join(edotPath, 'test', 'test-services.env'); -const fixtPath = path.join(edotPath, 'test', 'fixtures'); -const version = require(path.join(edotPath, 'package.json')).version; +const testEnvPath = path.join(edotPath, 'test', 'test-services.env'); +const fixturesPath = path.join(edotPath, 'test', 'fixtures'); +const edotVersion = require(path.join(edotPath, 'package.json')).version; /** * @param {string} name @@ -41,6 +40,7 @@ function validFixture(name) { } async function main() { + // TODO: more validations? if (typeof process.env.OTEL_EXPORTER_OTLP_ENDPOINT !== 'string') { console.log(`"OTEL_EXPORTER_OTLP_ENDPOINT" not set. Skipping`); return; @@ -55,6 +55,10 @@ async function main() { console.log(`env: ${n}=${process.env[n]}`); }) + console.log('starting services'); + execSync('docker compose -f ./test/docker-compose.yaml up -d --wait', {cwd: edotPath}); + console.log('services started'); + // Start the Mock server const otlpServer = new MockOtlpServer({ logLevel: 'warn', @@ -69,14 +73,19 @@ async function main() { await otlpServer.start(); console.log(`MockOtlpServer listening at ${otlpServer.httpUrl.href}`) - const testEnv = dotenv.parse(Buffer.from(fs.readFileSync(envPath))) + const servicesEnv = dotenv.parse(Buffer.from(fs.readFileSync(testEnvPath))) - const fixtures = fs.readdirSync(fixtPath).filter(validFixture); - for (const fixt of fixtures) { - const serviceName = fixt.replace('.js', '-service'); - const fixtFile = path.join(fixtPath, fixt); + const fixtures = fs.readdirSync(fixturesPath).filter(validFixture); + for (const fixture of fixtures) { + const serviceName = fixture.replace('.js', '-service'); + const fixtFile = path.join(fixturesPath, fixture); + const attribs = [ + `service.name=${serviceName}`, + `service.version=${edotVersion}`, + 'deployment.environment=test', + ]; - console.log(`running fixture ${fixt}`); + console.log(`running fixture ${fixture}`); await new Promise((resolve, reject) => { execFile( process.execPath, @@ -86,24 +95,24 @@ async function main() { env: Object.assign( {}, process.env, - testEnv, + servicesEnv, { OTEL_EXPORTER_OTLP_ENDPOINT: - otlpServer.httpUrl.href, - OTEL_EXPORTER_OTLP_PROTOCOL: 'http/json', - OTEL_RESOURCE_ATTRIBUTES:`service.name=${serviceName},service.version=${version},deployment.environment=test`, + otlpServer.httpUrl.href, // trick EDOT to send to mocotlpserver + OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf', // JSON not accepted by APM server + OTEL_RESOURCE_ATTRIBUTES: attribs.join(','), NODE_OPTIONS: '--require=@elastic/opentelemetry-node', }, ), }, async function done(err, stdout, stderr) { if (err) { - console.log(`fixture ${fixt} errored`); + console.log(`fixture ${fixture} errored`); console.log(`stdout: ${stdout}`); console.log(`stderr: ${stderr}`); return reject(err); } - console.log(`fixture ${fixt} okay`); + console.log(`fixture ${fixture} okay`); // console.log(`stdout: ${stdout}`); resolve(); } @@ -113,6 +122,10 @@ async function main() { console.log('ran all text fixtures'); await otlpServer.close(); + + console.log('stopping services') + execSync('docker compose -f ./test/docker-compose.yaml down', {cwd: edotPath}); + console.log('services stopped') } main(); From 0e6cd7061b03ccb0723c9f7556ee75620f77f260 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 21 Feb 2025 15:48:00 +0100 Subject: [PATCH 07/11] chore: remove fixtures script --- .../scripts/use-fixtures.js | 131 ------------------ 1 file changed, 131 deletions(-) delete mode 100644 packages/opentelemetry-node/scripts/use-fixtures.js diff --git a/packages/opentelemetry-node/scripts/use-fixtures.js b/packages/opentelemetry-node/scripts/use-fixtures.js deleted file mode 100644 index b956603f..00000000 --- a/packages/opentelemetry-node/scripts/use-fixtures.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 {execSync, execFile} = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const dotenv = require('dotenv'); -// const semver = require('semver'); -const { MockOtlpServer } = require('@elastic/mockotlpserver'); - -const edotPath = path.join(__dirname, '..'); -const testEnvPath = path.join(edotPath, 'test', 'test-services.env'); -const fixturesPath = path.join(edotPath, 'test', 'fixtures'); -const edotVersion = require(path.join(edotPath, 'package.json')).version; - -/** - * @param {string} name - * @returns {boolean} - */ -function validFixture(name) { - const exclude = ['-aws-', '-fs', '-fastify', 'host-metrics', 'elastic-openai']; - return name.endsWith('.js') && !exclude.some((e) => name.includes(e)); -} - -async function main() { - // TODO: more validations? - if (typeof process.env.OTEL_EXPORTER_OTLP_ENDPOINT !== 'string') { - console.log(`"OTEL_EXPORTER_OTLP_ENDPOINT" not set. Skipping`); - return; - } - - // ??? - // if (semver.lt(process.version, '18.0.0')) { - // console.log(`Process version "${process.version}" not supported. Skipping`); - // return; - // } - Object.keys(process.env).filter((k) => k.startsWith('OTEL_')).forEach((n) => { - console.log(`env: ${n}=${process.env[n]}`); - }) - - console.log('starting services'); - execSync('docker compose -f ./test/docker-compose.yaml up -d --wait', {cwd: edotPath}); - console.log('services started'); - - // Start the Mock server - const otlpServer = new MockOtlpServer({ - logLevel: 'warn', - services: ['http'], - httpHostname: '127.0.0.1', // avoid default 'localhost' because possible IPv6 - httpPort: 0, - tunnel: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, // set tunnel to the real endpoint - onTrace: console.log, - onMetrics: () => null, - onLogs: () => null, - }); - await otlpServer.start(); - console.log(`MockOtlpServer listening at ${otlpServer.httpUrl.href}`) - - const servicesEnv = dotenv.parse(Buffer.from(fs.readFileSync(testEnvPath))) - - const fixtures = fs.readdirSync(fixturesPath).filter(validFixture); - for (const fixture of fixtures) { - const serviceName = fixture.replace('.js', '-service'); - const fixtFile = path.join(fixturesPath, fixture); - const attribs = [ - `service.name=${serviceName}`, - `service.version=${edotVersion}`, - 'deployment.environment=test', - ]; - - console.log(`running fixture ${fixture}`); - await new Promise((resolve, reject) => { - execFile( - process.execPath, - [fixtFile], - { - killSignal: 'SIGINT', - env: Object.assign( - {}, - process.env, - servicesEnv, - { - OTEL_EXPORTER_OTLP_ENDPOINT: - otlpServer.httpUrl.href, // trick EDOT to send to mocotlpserver - OTEL_EXPORTER_OTLP_PROTOCOL: 'http/protobuf', // JSON not accepted by APM server - OTEL_RESOURCE_ATTRIBUTES: attribs.join(','), - NODE_OPTIONS: '--require=@elastic/opentelemetry-node', - }, - ), - }, - async function done(err, stdout, stderr) { - if (err) { - console.log(`fixture ${fixture} errored`); - console.log(`stdout: ${stdout}`); - console.log(`stderr: ${stderr}`); - return reject(err); - } - console.log(`fixture ${fixture} okay`); - // console.log(`stdout: ${stdout}`); - resolve(); - } - ) - }); - } - - console.log('ran all text fixtures'); - await otlpServer.close(); - - console.log('stopping services') - execSync('docker compose -f ./test/docker-compose.yaml down', {cwd: edotPath}); - console.log('services stopped') -} - -main(); From 7f7c85b57d7a8ad191c097f8e63416758dcea7c0 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 21 Feb 2025 15:50:06 +0100 Subject: [PATCH 08/11] chore: remove TODO comment --- packages/opentelemetry-node/test/testutils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opentelemetry-node/test/testutils.js b/packages/opentelemetry-node/test/testutils.js index 9ef25b65..ff7b374a 100644 --- a/packages/opentelemetry-node/test/testutils.js +++ b/packages/opentelemetry-node/test/testutils.js @@ -596,7 +596,6 @@ function runTestFixtures(suite, testFixtures) { services: ['http'], httpHostname: '127.0.0.1', // avoid default 'localhost' because possible IPv6 httpPort: 0, - // TODO: add proxy here (with auth headers) onTrace: collector.onTrace.bind(collector), onMetrics: collector.onMetrics.bind(collector), onLogs: collector.onLogs.bind(collector), From b29d9139a131a3950c5673d72ac6efbea15f9c88 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 24 Feb 2025 14:57:47 +0100 Subject: [PATCH 09/11] chore: update option docs --- packages/mockotlpserver/lib/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mockotlpserver/lib/cli.js b/packages/mockotlpserver/lib/cli.js index 5901455e..9d4ffffa 100755 --- a/packages/mockotlpserver/lib/cli.js +++ b/packages/mockotlpserver/lib/cli.js @@ -121,7 +121,7 @@ const OPTIONS = [ { names: ['tunnel', 't'], type: 'string', - help: `Set this option to a URL to send all HTTP requests to another server. This won't stop the processing of OTLP data.`, + help: `Set this option to a URL to send all requests to another server (only HTTP supported for now). This won't stop the processing of OTLP data.`, }, ]; From 7a2a8c7a639047677a24407dcc825c576cf8a24f Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 24 Feb 2025 15:25:32 +0100 Subject: [PATCH 10/11] chore: fix lint issue --- packages/mockotlpserver/lib/tunnel.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/mockotlpserver/lib/tunnel.js b/packages/mockotlpserver/lib/tunnel.js index e13185e6..4f03e734 100644 --- a/packages/mockotlpserver/lib/tunnel.js +++ b/packages/mockotlpserver/lib/tunnel.js @@ -31,13 +31,17 @@ function createHttpTunnel(log, target) { try { targetUrl = new URL(target); } catch { - log.warn(`Cannot create a tunnel to target "${target}". The given URL is invalid.`); + log.warn( + `Cannot create a tunnel to target "${target}". The given URL is invalid.` + ); return; } const {protocol, host} = targetUrl; if (protocol !== 'http:' && protocol !== 'https:') { - log.warn(`Cannot create a tunnel to target "${target}". Protocol must be one of: http, https.`); + log.warn( + `Cannot create a tunnel to target "${target}". Protocol must be one of: http, https.` + ); return; } @@ -47,7 +51,9 @@ function createHttpTunnel(log, target) { // ref: https://www.elastic.co/guide/en/observability/current/apm-api-otlp.html const contentType = req.headers['content-type']; if (contentType !== 'application/x-protobuf') { - log.warn(`Content type "${contentType}" may not be accepted by the target server (${target})`); + log.warn( + `Content type "${contentType}" may not be accepted by the target server (${target})` + ); } const httpFlavor = protocol === 'http:' ? http : https; @@ -62,7 +68,7 @@ function createHttpTunnel(log, target) { tunnelRes.pipe(res); }); req.pipe(tunnelReq); - } + }; } module.exports = { From e33bd420c89e6c862e00f2c7471202c834181f6f Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 26 Feb 2025 22:55:36 +0100 Subject: [PATCH 11/11] Update packages/mockotlpserver/lib/cli.js Co-authored-by: Trent Mick --- packages/mockotlpserver/lib/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mockotlpserver/lib/cli.js b/packages/mockotlpserver/lib/cli.js index 9d4ffffa..d633d5bb 100755 --- a/packages/mockotlpserver/lib/cli.js +++ b/packages/mockotlpserver/lib/cli.js @@ -121,7 +121,7 @@ const OPTIONS = [ { names: ['tunnel', 't'], type: 'string', - help: `Set this option to a URL to send all requests to another server (only HTTP supported for now). This won't stop the processing of OTLP data.`, + help: `Tunnel all incoming requests to the given server. Only supported for the HTTP OTLP server (port 4318). Received OTLP data will still be printed per the '-o' option.`, }, ];