Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds proxy argument for routing HttpLogger through a proxy #206

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/zipkin-transport-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
"license": "Apache-2.0",
"repository": "https://github.com/openzipkin/zipkin-js",
"dependencies": {
"http-proxy-agent": "^2.1.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the risk of having these dependencies?

"https-proxy-agent": "^2.2.1",
"node-fetch": "^1.5.3"
},
"devDependencies": {
"babel-cli": "^6.23.0",
"body-parser": "^1.15.2",
"express": "^4.13.4",
"http-proxy-middleware": "^0.18.0",
"mocha": "^3.2.0",
"zipkin": "^0.12.0"
}
Expand Down
27 changes: 24 additions & 3 deletions packages/zipkin-transport-http/src/HttpLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@
const globalFetch =
(typeof window !== 'undefined' && window.fetch) ||
(typeof global !== 'undefined' && global.fetch);

// eslint-disable-next-line global-require
const fetch = globalFetch || require.call(null, 'node-fetch');

const {
jsonEncoder: {JSON_V1}
} = require('zipkin');
const HttpsProxyAgent = require('https-proxy-agent');
const HttpProxyAgent = require('http-proxy-agent');

const EventEmitter = require('events').EventEmitter;

class HttpLogger extends EventEmitter {
constructor({endpoint, headers = {}, httpInterval = 1000, jsonEncoder = JSON_V1, timeout = 0}) {
constructor({
endpoint,
headers = {},
httpInterval = 1000,
jsonEncoder = JSON_V1,
timeout = 0,
proxy = null
}) {
super(); // must be before any reference to *this*
this.endpoint = endpoint;
this.queue = [];
Expand All @@ -30,6 +38,9 @@ class HttpLogger extends EventEmitter {
// @see https://github.com/bitinn/node-fetch#fetch-options
this.timeout = timeout;

// prioritise passed in proxy, followed by secure proxies and insecure proxies if they exist
this.proxy = proxy;

const timer = setInterval(() => {
this.processQueue();
}, httpInterval);
Expand All @@ -50,6 +61,16 @@ class HttpLogger extends EventEmitter {
this.queue.push(this.jsonEncoder.encode(span));
}

createProxyAgentIfAvailable() {
if (typeof this.proxy !== 'string') {
return undefined;
} else if (this.proxy.indexOf('https') === 0) {
return new HttpsProxyAgent(this.proxy);
} else {
return new HttpProxyAgent(this.proxy);
}
}

processQueue() {
const self = this;
if (self.queue.length > 0) {
Expand All @@ -59,11 +80,11 @@ class HttpLogger extends EventEmitter {
body: postBody,
headers: self.headers,
timeout: self.timeout,
agent: this.createProxyAgentIfAvailable(),
}).then((response) => {
if (response.status !== 202) {
const err = 'Unexpected response while sending Zipkin data, status:' +
`${response.status}, body: ${postBody}`;

if (self.errorListenerSet) this.emit('error', new Error(err));
else console.error(err);
}
Expand Down
147 changes: 147 additions & 0 deletions packages/zipkin-transport-http/test/integrationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
} = require('zipkin');
const HttpLogger = require('../src/HttpLogger');
const express = require('express');
const proxy = require('http-proxy-middleware');
const bodyParser = require('body-parser');

describe('HTTP transport - integration test', () => {
Expand Down Expand Up @@ -113,4 +114,150 @@ describe('HTTP transport - integration test', () => {
});
});
});

context('using a proxy', () => {
let actualServer;
let proxyServer;

beforeEach(() => {
proxyServer = express();
actualServer = express();
actualServer.use(bodyParser.json());
actualServer.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
res.status(403).json('not from proxy');
});
});

it('should not work without a proxy argument passed in', function(done) {
actualServer.post('/api/v1/spans/proxy', (req, res) => {
res.status(202).json({});
this.actualServer.close(() => {
this.proxyServer.close(
done.bind(null, new Error('request succeeded even though it should not'))
);
});
});
this.actualServer = actualServer.listen(0, () => {
const serverPort = this.actualServer.address().port;
proxyServer.use(proxy({
changeOrigin: true,
logLevel: 'silent',
target: `http://localhost:${serverPort}`,
}));
this.proxyServer = proxyServer.listen(0, () => {
const httpLogger = new HttpLogger({
endpoint: `http://localhost:${serverPort}/api/v1/spans`,
});
httpLogger.on('error', () => done());

const ctxImpl = new ExplicitContext();
const recorder = new BatchRecorder({logger: httpLogger});
const tracer = new Tracer({recorder, ctxImpl});

ctxImpl.scoped(() => {
tracer.recordAnnotation(new Annotation.ServerRecv());
tracer.recordServiceName('my-proxied-service-wo-proxy');
tracer.recordRpc('GET');
tracer.recordBinary('http.url', 'http://example-no-proxy-fail.com');
tracer.recordBinary('http.response_code', '200');
tracer.recordAnnotation(new Annotation.ServerSend());
});
});
});
});

it('should send trace data via HTTP using JSON_V1', function(done) {
actualServer.post('/api/v1/spans/proxy', (req, res) => {
res.status(202).json({});
expect(req.headers['content-type']).to.equal('application/json');
const traceData = req.body;
expect(traceData.length).to.equal(1);
expect(traceData[0].name).to.equal('get');
expect(traceData[0].binaryAnnotations.length).to.equal(2);
expect(traceData[0].annotations.length).to.equal(2);
this.actualServer.close(() => {
this.proxyServer.close(done);
});
});
this.actualServer = actualServer.listen(0, () => {
const serverPort = this.actualServer.address().port;
proxyServer.use(proxy({
logLevel: 'silent',
target: `http://localhost:${serverPort}`,
pathRewrite: {
'/api/v1/spans': '/api/v1/spans/proxy'
}
}));
this.proxyServer = proxyServer.listen(0, () => {
const proxyPort = this.proxyServer.address().port;
const httpLogger = new HttpLogger({
endpoint: `http://localhost:${serverPort}/api/v1/spans`,
proxy: `http://localhost:${proxyPort}`,
});

const ctxImpl = new ExplicitContext();
const recorder = new BatchRecorder({logger: httpLogger});
const tracer = new Tracer({recorder, ctxImpl});

ctxImpl.scoped(() => {
tracer.recordAnnotation(new Annotation.ServerRecv());
tracer.recordServiceName('my-service-via-proxy-v1');
tracer.recordRpc('GET');
tracer.recordBinary('http.url', 'http://example-proxy-json-v1.com');
tracer.recordBinary('http.response_code', '200');
tracer.recordAnnotation(new Annotation.ServerSend());
});
});
});
});

it('should send trace data via HTTP using JSON_V2', function(done) {
actualServer.post('/api/v1/spans/proxy', (req, res) => {
res.status(202).json({});
expect(req.headers['content-type']).to.equal('application/json');
expect(req.headers.authorization).to.equal('Token');
const traceData = req.body;
expect(traceData.length).to.equal(1);
expect(traceData[0].name).to.equal('get');
expect(traceData[0].kind).to.equal('SERVER');
expect(traceData[0].tags['http.url']).to.equal('http://example-proxy-json-v2.com');
expect(traceData[0].tags['http.response_code']).to.equal('200');
this.actualServer.close(() => {
this.proxyServer.close(done);
});
});
this.actualServer = actualServer.listen(0, () => {
const serverPort = this.actualServer.address().port;
proxyServer.use(proxy({
logLevel: 'silent',
target: `http://localhost:${serverPort}`,
pathRewrite: {
'/api/v1/spans': '/api/v1/spans/proxy'
}
}));
this.proxyServer = proxyServer.listen(0, () => {
const proxyPort = this.proxyServer.address().port;
const httpLogger = new HttpLogger({
endpoint: `http://localhost:${serverPort}/api/v1/spans`,
proxy: `http://localhost:${proxyPort}`,
headers: {Authorization: 'Token'},
jsonEncoder: JSON_V2
});

const ctxImpl = new ExplicitContext();
const recorder = new BatchRecorder({logger: httpLogger});
const tracer = new Tracer({recorder, ctxImpl});

ctxImpl.scoped(() => {
tracer.recordAnnotation(new Annotation.ServerRecv());
tracer.recordServiceName('my-service-via-proxy-v2');
tracer.recordRpc('GET');
tracer.recordBinary('http.url', 'http://example-proxy-json-v2.com');
tracer.recordBinary('http.response_code', '200');
tracer.recordAnnotation(new Annotation.ServerSend());
});
});
});
});
});
});