Skip to content

Commit 75798cb

Browse files
authored
Merge pull request #651 from majanjua-amzn/master
[Lambda] Replace Facade with No-Op if trace header is missing data
2 parents 73e1fca + 5731baa commit 75798cb

File tree

10 files changed

+224
-24
lines changed

10 files changed

+224
-24
lines changed

packages/core/lib/context_utils.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ var contextUtils = {
9999

100100
if (!segment) {
101101
contextUtils.contextMissingStrategy.contextMissing('Failed to get the current sub/segment from the context.');
102-
} else if (segment instanceof Segment && process.env.LAMBDA_TASK_ROOT && segment.facade == true) {
103-
segment.resolveLambdaTraceData();
102+
} else if (segment instanceof Segment && process.env.LAMBDA_TASK_ROOT) {
103+
if (segment.facade == true || segment.noOp == true) {
104+
segment.resolveLambdaTraceData();
105+
}
104106
}
105107

106108
return segment;

packages/core/lib/env/aws_lambda.js

+78-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ module.exports.init = function init() {
3636

3737
var namespace = contextUtils.getNamespace();
3838
namespace.enter(namespace.createContext());
39-
contextUtils.setSegment(facadeSegment());
39+
40+
if (LambdaUtils.validTraceData(process.env._X_AMZN_TRACE_ID)) {
41+
contextUtils.setSegment(facadeSegment());
42+
} else {
43+
contextUtils.setSegment(noOpSegment());
44+
}
4045
};
4146

4247
var facadeSegment = function facadeSegment() {
@@ -109,3 +114,75 @@ var facadeSegment = function facadeSegment() {
109114

110115
return segment;
111116
};
117+
118+
var noOpSegment = function noOpSegment() {
119+
var segment = new Segment('no-op');
120+
var whitelistFcn = [];
121+
var silentFcn = ['addNewSubsegment', 'addSubsegment', 'removeSubsegment', 'toString', 'addSubsegmentWithoutSampling', 'addNewSubsegmentWithoutSampling', 'incrementCounter', 'decrementCounter', 'isClosed', 'close', 'format', 'flush'];
122+
var xAmznTraceId = process.env._X_AMZN_TRACE_ID;
123+
124+
for (var key in segment) {
125+
if (typeof segment[key] === 'function' && whitelistFcn.indexOf(key) === -1) {
126+
if (silentFcn.indexOf(key) === -1) {
127+
segment[key] = (function() {
128+
var func = key;
129+
return function noOp() {
130+
logger.getLogger().warn('Function "' + func + '" cannot be called on an AWS Lambda segment. Please use a subsegment to record data.');
131+
return;
132+
};
133+
})();
134+
} else {
135+
segment[key] = function noOp() {
136+
return;
137+
};
138+
}
139+
}
140+
}
141+
142+
segment.trace_id = TraceID.Invalid().toString();
143+
segment.isClosed = function() {
144+
return true;
145+
};
146+
segment.in_progress = false;
147+
segment.counter = 1;
148+
segment.notTraced = true;
149+
segment.noOp = true;
150+
151+
segment.reset = function reset() {
152+
this.trace_id = TraceID.Invalid().toString();
153+
this.id = '00000000';
154+
delete this.subsegments;
155+
this.notTraced = true;
156+
};
157+
158+
segment.resolveLambdaTraceData = function resolveLambdaTraceData() {
159+
var xAmznLambda = process.env._X_AMZN_TRACE_ID;
160+
161+
if (xAmznLambda) {
162+
163+
// This check resets the trace data whenever a new trace header is read to not leak data between invocations
164+
if (xAmznLambda != xAmznTraceIdPrev) {
165+
this.reset();
166+
167+
if (LambdaUtils.populateTraceData(segment, xAmznLambda)) {
168+
xAmznTraceIdPrev = xAmznLambda;
169+
}
170+
}
171+
} else {
172+
this.reset();
173+
contextUtils.contextMissingStrategy.contextMissing('Missing AWS Lambda trace data for X-Ray. ' +
174+
'Ensure Active Tracing is enabled and no subsegments are created outside the function handler.');
175+
}
176+
};
177+
178+
// Since we're in a no-op segment, do not check if the trace data is valid; simply propagate the information
179+
if (LambdaUtils.populateTraceData(segment, xAmznTraceId)) {
180+
xAmznTraceIdPrev = xAmznTraceId;
181+
}
182+
183+
return segment;
184+
};
185+
186+
// For testing
187+
export const exportedFacadeSegment = { facadeSegment };
188+
export const exportedNoOpSegment = { noOpSegment };

packages/core/lib/patchers/aws3_p.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,10 @@ const getXRayMiddleware = (config: RegionResolvedConfig, manualSegment?: Segment
125125
const parent = (segment instanceof Subsegment ? segment.segment : segment);
126126
const data = parent.segment ? parent.segment.additionalTraceData : parent.additionalTraceData;
127127

128-
let traceHeader = stringify(
129-
{
130-
Root: parent.trace_id,
131-
Parent: subsegment.id,
132-
Sampled: subsegment.notTraced ? '0' : '1',
133-
},
134-
';',
135-
);
128+
let traceHeader = 'Root=' + parent.trace_id;
129+
if (!(parent && parent.noOp)) {
130+
traceHeader += ';Parent=' + subsegment.id + ';Sampled=' + (subsegment.notTraced ? '0' : '1');
131+
}
136132

137133
if (data != null) {
138134
for (const [key, value] of Object.entries(data)) {

packages/core/lib/patchers/aws_p.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,11 @@ function captureAWSRequest(req) {
8888
const data = parent.segment ? parent.segment.additionalTraceData : parent.additionalTraceData;
8989

9090
var buildListener = function(req) {
91-
let traceHeader = 'Root=' + traceId + ';Parent=' + subsegment.id +
92-
';Sampled=' + (subsegment.notTraced ? '0' : '1');
91+
let traceHeader = 'Root=' + traceId;
92+
// Only append parent and sample if not in Lambda PassThrough mode
93+
if (!(parent && parent.noOp)) {
94+
traceHeader += ';Parent=' + subsegment.id + ';Sampled=' + (subsegment.notTraced ? '0' : '1');
95+
}
9396
if (data != null) {
9497
for (const [key, value] of Object.entries(data)) {
9598
traceHeader += ';' + key +'=' + value;
@@ -99,6 +102,9 @@ function captureAWSRequest(req) {
99102
};
100103

101104
var completeListener = function(res) {
105+
if (subsegment == null) {
106+
return;
107+
}
102108
subsegment.addAttribute('namespace', 'aws');
103109
subsegment.addAttribute('aws', new Aws(res, subsegment.name));
104110

packages/core/lib/patchers/http_p.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,18 @@ function enableCapture(module, downstreamXRayEnabled, subsegmentCallback) {
123123
subsegment = parent.addNewSubsegment(hostname);
124124
}
125125

126-
const root = parent.segment ? parent.segment : parent;
127126
subsegment.namespace = 'remote';
128127

129128
if (!options.headers) {
130129
options.headers = {};
131130
}
132131

133-
options.headers['X-Amzn-Trace-Id'] = 'Root=' + root.trace_id + ';Parent=' + subsegment.id +
134-
';Sampled=' + (subsegment.notTraced ? '0' : '1');
132+
let traceHeader = 'Root=' + (parent.segment ? parent.segment : parent).trace_id;
133+
if (!(parent && parent.noOp)) {
134+
traceHeader += ';Parent=' + subsegment.id + ';Sampled=' + (subsegment.notTraced ? '0' : '1');
135+
}
136+
137+
options.headers['X-Amzn-Trace-Id'] = traceHeader;
135138

136139
const errorCapturer = function errorCapturer(e) {
137140
if (subsegmentCallback) {

packages/core/lib/utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ var utils = {
168168
if (!traceData) {
169169
traceData = {};
170170
logger.getLogger().error('_X_AMZN_TRACE_ID is empty or has an invalid format');
171+
} else if (segment.noOp == true && traceData.root) {
172+
valid = true;
171173
} else if (!traceData.root || !traceData.parent || !traceData.sampled) {
172174
logger.getLogger().error('_X_AMZN_TRACE_ID is missing required information');
173175
} else {

packages/core/test/unit/env/aws_lambda.test.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe('AWSLambda', function() {
8181
assert.equal(facade.trace_id, TraceID.Invalid().toString());
8282
});
8383

84-
describe('the facade segment', function() {
84+
describe('the facade/no-op segment', function() {
8585
afterEach(function() {
8686
populateStub.returns(true);
8787
delete process.env._X_AMZN_TRACE_ID;
@@ -95,17 +95,25 @@ describe('AWSLambda', function() {
9595
validateStub.should.have.been.calledWith(process.env._X_AMZN_TRACE_ID);
9696
});
9797

98-
it('should call populateTraceData if validTraceData returns true', function() {
98+
it('should call populateTraceData on Facade if validTraceData returns true', function() {
9999
Lambda.init();
100100

101+
var segment = setSegmentStub.args[0][0];
102+
assert.equal(segment.name, 'facade');
103+
assert.isTrue(segment.facade);
104+
101105
populateStub.should.have.been.calledOnce;
102106
});
103107

104-
it('should not call populateTraceData if validTraceData returns false', function() {
108+
it('should call populateTraceData on No-Op if validTraceData returns false', function() {
105109
validateStub.returns(false);
106110
Lambda.init();
107111

108-
populateStub.should.have.not.been.called;
112+
var segment = setSegmentStub.args[0][0];
113+
assert.equal(segment.name, 'no-op');
114+
assert.isTrue(segment.noOp);
115+
116+
populateStub.should.have.been.calledOnce;
109117
});
110118
});
111119
});

packages/core/test/unit/patchers/aws_p.test.js

+91
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ var Utils = require('../../../lib/utils');
1313

1414
var logger = require('../../../lib/logger').getLogger();
1515

16+
import { exportedFacadeSegment, exportedNoOpSegment } from '../../../lib/env/aws_lambda';
17+
const { facadeSegment } = exportedFacadeSegment;
18+
const { noOpSegment } = exportedNoOpSegment;
19+
1620
chai.should();
1721
chai.use(sinonChai);
1822

@@ -348,4 +352,91 @@ describe('AWS patcher', function() {
348352
});
349353

350354
});
355+
356+
357+
describe('#captureAWSRequest-Lambda-PassThrough', function() {
358+
var awsClient, awsRequest, MyEmitter, sandbox, segment, stubResolve, tempHeader;
359+
360+
before(function() {
361+
MyEmitter = function() {
362+
EventEmitter.call(this);
363+
};
364+
365+
awsClient = {
366+
customizeRequests: function customizeRequests(captureAWSRequest) {
367+
this.call = captureAWSRequest;
368+
},
369+
throttledError: function throttledError() {}
370+
};
371+
awsClient = awsPatcher.captureAWSClient(awsClient);
372+
373+
util.inherits(MyEmitter, EventEmitter);
374+
});
375+
376+
beforeEach(function() {
377+
sandbox = sinon.createSandbox();
378+
379+
awsRequest = {
380+
httpRequest: {
381+
method: 'GET',
382+
url: '/',
383+
connection: {
384+
remoteAddress: 'localhost'
385+
},
386+
headers: {}
387+
},
388+
response: {}
389+
};
390+
391+
awsRequest.on = function(event, fcn) {
392+
if (event === 'complete') {
393+
this.emitter.on(event, fcn.bind(this, this.response));
394+
} else {
395+
this.emitter.on(event, fcn.bind(this, this));
396+
}
397+
return this;
398+
};
399+
400+
awsRequest.emitter = new MyEmitter();
401+
402+
tempHeader = process.env._X_AMZN_TRACE_ID;
403+
process.env._X_AMZN_TRACE_ID = 'Root=' + traceId + ';Foo=bar';
404+
405+
segment = noOpSegment();
406+
407+
stubResolve = sandbox.stub(contextUtils, 'resolveSegment').returns(segment);
408+
});
409+
410+
afterEach(function() {
411+
process.env._X_AMZN_TRACE_ID = tempHeader;
412+
sandbox.restore();
413+
});
414+
415+
it('should log an info statement and exit if parent is not found on the context or on the call params', function(done) {
416+
stubResolve.returns();
417+
var logStub = sandbox.stub(logger, 'info');
418+
419+
awsClient.call(awsRequest);
420+
421+
setTimeout(function() {
422+
logStub.should.have.been.calledOnce;
423+
done();
424+
}, 50);
425+
});
426+
427+
it('should inject the tracing headers', function(done) {
428+
sandbox.stub(contextUtils, 'isAutomaticMode').returns(true);
429+
430+
awsClient.call(awsRequest);
431+
432+
awsRequest.emitter.emit('build');
433+
434+
setTimeout(function() {
435+
var expected = new RegExp('^Root=' + traceId + ';Foo=bar$');
436+
assert.match(awsRequest.httpRequest.headers['X-Amzn-Trace-Id'], expected);
437+
done();
438+
}, 50);
439+
});
440+
441+
});
351442
});

sdk_contrib/fetch/lib/fetch_p.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,12 @@ const enableCapture = function enableCapture(baseFetchFunction, requestClass, do
104104

105105
subsegment.namespace = 'remote';
106106

107-
request.headers.set('X-Amzn-Trace-Id',
108-
'Root=' + (parent.segment ? parent.segment : parent).trace_id +
109-
';Parent=' + subsegment.id +
110-
';Sampled=' + (subsegment.notTraced ? '0' : '1'));
107+
let traceHeader = 'Root=' + (parent.segment ? parent.segment : parent).trace_id;
108+
if (!(parent && parent.noOp)) {
109+
traceHeader += ';Parent=' + subsegment.id + ';Sampled=' + (subsegment.notTraced ? '0' : '1');
110+
}
111+
112+
request.headers.set('X-Amzn-Trace-Id', traceHeader);
111113

112114
// Set up fetch call and capture any thrown errors
113115
const capturedFetch = async () => {

sdk_contrib/fetch/test/unit/fetch_p.test.js

+13
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,19 @@ describe('Unit tests', function () {
273273
'Root=12345;Parent=999;Sampled=1');
274274
});
275275

276+
it('adds X-Amzn-Trace-Id header with only root if noOp', async function () {
277+
const activeFetch = captureFetch(true);
278+
stubParentSegment.noOp = true;
279+
stubParentSegment.trace_id = '12345';
280+
stubSubsegment.notTraced = false;
281+
stubSubsegment.id = '999';
282+
const request = new FetchRequest('https://www.foo.com/test');
283+
const requestHeadersSet = sandbox.stub(request.headers, 'set');
284+
await activeFetch(request);
285+
requestHeadersSet.should.have.been.calledOnceWith('X-Amzn-Trace-Id',
286+
'Root=12345');
287+
});
288+
276289
it('calls subsegmentCallback on successful response', async function () {
277290
const spyCallback = sandbox.spy();
278291
const activeFetch = captureFetch(true, spyCallback);

0 commit comments

Comments
 (0)