Skip to content

Commit

Permalink
feat(mockotlpserver): add tunnel option for HTTP requests (#608)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-luna authored Feb 27, 2025
1 parent cf99f23 commit 8c10660
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 21 deletions.
6 changes: 6 additions & 0 deletions packages/mockotlpserver/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const OPTIONS = [
type: 'bool',
help: 'Start a web server to inspect traces with some charts.',
},
{
names: ['tunnel', 't'],
type: 'string',
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.`,
},
];

async function main() {
Expand Down Expand Up @@ -165,6 +170,7 @@ async function main() {
services,
grpcHostname: opts.hostname || DEFAULT_HOSTNAME,
httpHostname: opts.hostname || DEFAULT_HOSTNAME,
tunnel: opts.tunnel,
uiHostname: opts.hostname || DEFAULT_HOSTNAME,
});
await otlpServer.start();
Expand Down
53 changes: 32 additions & 21 deletions packages/mockotlpserver/lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {
} = require('./diagch');
const {getProtoRoot} = require('./proto');
const {Service} = require('./service');
const {createHttpTunnel} = require('./tunnel');

const protoRoot = getProtoRoot();

Expand Down Expand Up @@ -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.tunnel]
*/
constructor(opts) {
super();
Expand All @@ -133,10 +135,16 @@ class HttpService extends Service {
}

async start() {
const {log, hostname, port} = this._opts;
const {log, hostname, port, tunnel} = this._opts;
const httpTunnel = tunnel && createHttpTunnel(log, tunnel);

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}"`
Expand All @@ -146,26 +154,29 @@ class HttpService extends Service {
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
Expand Down
3 changes: 3 additions & 0 deletions packages/mockotlpserver/lib/mockotlpserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class MockOtlpServer {
* @param {number} [opts.httpPort] Default 4318. Use 0 to select a free port.
* @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.
Expand All @@ -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._tunnel = opts.tunnel;
this._grpcHostname = opts.grpcHostname ?? DEFAULT_HOSTNAME;
this._grpcPort = opts.grpcPort ?? DEFAULT_GRPC_PORT;
this._uiHostname = opts.uiHostname ?? DEFAULT_HOSTNAME;
Expand Down Expand Up @@ -118,6 +120,7 @@ class MockOtlpServer {
log: this._log,
hostname: this._httpHostname,
port: this._httpPort,
tunnel: this._tunnel,
});
await this._httpService.start();
this.httpUrl = this._httpService.url;
Expand Down
76 changes: 76 additions & 0 deletions packages/mockotlpserver/lib/tunnel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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
* @returns {((req: http.IncomingMessage, res: http.ServerResponse) => void) | undefined}
*/
function createHttpTunnel(log, target) {
/** @type {URL} */
let targetUrl;

try {
targetUrl = new URL(target);
} catch {
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.`
);
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 options = {
host: targetUrl.host,
port,
method: req.method,
headers: {...req.headers, host},
path: req.url,
};
const tunnelReq = httpFlavor.request(options, (tunnelRes) => {
tunnelRes.pipe(res);
});
req.pipe(tunnelReq);
};
}

module.exports = {
createHttpTunnel,
};

0 comments on commit 8c10660

Please sign in to comment.