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

Oversampling Mitigation #541

Merged
merged 12 commits into from
Nov 10, 2022
2 changes: 1 addition & 1 deletion packages/core/lib/env/aws_lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports.init = function init() {

var facadeSegment = function facadeSegment() {
var segment = new Segment('facade');
var whitelistFcn = ['addNewSubsegment', 'addSubsegment', 'removeSubsegment', 'toString'];
var whitelistFcn = ['addNewSubsegment', 'addSubsegment', 'removeSubsegment', 'toString', 'addSubsegmentWithoutSampling', 'addNewSubsegmentWithoutSampling'];
var silentFcn = ['incrementCounter', 'decrementCounter', 'isClosed', 'close', 'format', 'flush'];
var xAmznTraceId = process.env._X_AMZN_TRACE_ID;

Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/env/sqs_message_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class SqsMessageHelper {
constructor() {
}

isSampled(message) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this static? Would need to update docs as well

const {attributes} = message; // extract attributes from message
if (!('AWSTraceHeader' in attributes)) {
return false;
}
return attributes['AWSTraceHeader'].includes('Sampled=1');
}
}

export default SqsMessageHelper;
3 changes: 2 additions & 1 deletion packages/core/lib/patchers/aws_p.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ function captureAWSRequest(req) {

var buildListener = function(req) {
req.httpRequest.headers['X-Amzn-Trace-Id'] = 'Root=' + traceId + ';Parent=' + subsegment.id +
';Sampled=' + (subsegment.segment.notTraced ? '0' : '1');
';Sampled=' + (subsegment.isSampled ? '1' : '0');
Copy link
Contributor

@atshaw43 atshaw43 Oct 17, 2022

Choose a reason for hiding this comment

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

Should we stick with the variable name notTraced since customers are expecting that on segment?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1


};

var completeListener = function(res) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/lib/patchers/http_p.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ function enableCapture(module, downstreamXRayEnabled, subsegmentCallback) {
if (!options.headers) {
options.headers = {};
}

options.headers['X-Amzn-Trace-Id'] = 'Root=' + root.trace_id + ';Parent=' + subsegment.id +
';Sampled=' + (!root.notTraced ? '1' : '0');
';Sampled=' + (subsegment.isSampled ? '1' : '0');

const errorCapturer = function errorCapturer(e) {
if (subsegmentCallback) {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/lib/segments/attributes/subsegment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ declare class Subsegment {
parent: SegmentLike;
segment: Segment;
namespace?: string;
isSampled: boolean;

constructor(name: string);

addNewSubsegment(name: string): Subsegment;

addSubsegment(subsegment: Subsegment): void;

addNewSubsegmentWithoutSampling(name: String): Subsegment;

addSubsegmentWithoutSampling(subsegment: Subsegment): void;

removeSubsegment(subsegment: Subsegment): void;

addAttribute(name: string, data: any): void;
Expand Down
26 changes: 22 additions & 4 deletions packages/core/lib/segments/attributes/subsegment.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Subsegment.prototype.init = function init(name) {
this.start_time = SegmentUtils.getCurrentTime();
this.in_progress = true;
this.counter = 0;
this.isSampled = true;
};

/**
Expand All @@ -39,9 +40,23 @@ Subsegment.prototype.init = function init(name) {
Subsegment.prototype.addNewSubsegment = function addNewSubsegment(name) {
var subsegment = new Subsegment(name);
this.addSubsegment(subsegment);
subsegment.isSampled = subsegment.segment? !subsegment.segment.notTraced : true;
return subsegment;
};

Subsegment.prototype.addSubsegmentWithoutSampling = function addSubsegmentWithoutSampling(subsegment){
subsegment.isSampled = false;
this.addSubsegment(subsegment);
};

Subsegment.prototype.addNewSubsegmentWithoutSampling = function addNewSubsegmentWithoutSampling(name){
var subsegment = new Subsegment(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: use const

subsegment.isSampled = false;
this.addSubsegment(subsegment);

return subsegment;
};

/**
* Adds a subsegment to the array of subsegments.
* @param {Subsegment} subsegment - The subsegment to append.
Expand All @@ -60,11 +75,14 @@ Subsegment.prototype.addSubsegment = function(subsegment) {
subsegment.segment = this.segment;
subsegment.parent = this;

if (subsegment.end_time === undefined) {
if (subsegment.end_time === undefined && subsegment.isSampled) {
this.incrementCounter(subsegment.counter);
}

this.subsegments.push(subsegment);
// don't push to subsegment array if subsegment is not sampled
if(subsegment.isSampled){
this.subsegments.push(subsegment);
}

};

/**
Expand Down Expand Up @@ -362,7 +380,7 @@ Subsegment.prototype.streamSubsegments = function streamSubsegments() {
var open = [];

this.subsegments.forEach(function(child) {
if (!child.streamSubsegments()) {
if (!child.streamSubsegments()) {
open.push(child);
}
});
Expand Down
4 changes: 4 additions & 0 deletions packages/core/lib/segments/segment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ declare class Segment {

addSubsegment(subsegment: Subsegment): void;

addSubsegmentWithoutSampling(subsegment: Subsegment): void;

addNewSubsegmentWithoutSampling(name: String): Subsegment

removeSubsegment(subsegment: Subsegment): void;

addError(err: Error | string, remote?: boolean): void;
Expand Down
23 changes: 21 additions & 2 deletions packages/core/lib/segments/segment.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ Segment.prototype.addNewSubsegment = function addNewSubsegment(name) {
return subsegment;
};

Segment.prototype.addSubsegmentWithoutSampling = function addSubsegmentWithoutSampling(subsegment){
subsegment.isSampled = false;
this.addSubsegment(subsegment);

};

Segment.prototype.addNewSubsegmentWithoutSampling = function addNewSubsegmentWithoutSampling(name){
var subsegment = new Subsegment(name);
subsegment.isSampled = false;
this.addSubsegment(subsegment);

return subsegment;
};

/**
* Adds a subsegment to the array of subsegments.
* @param {Subsegment} subsegment - The subsegment to append.
Expand All @@ -233,13 +247,18 @@ Segment.prototype.addSubsegment = function addSubsegment(subsegment) {

subsegment.segment = this;
subsegment.parent = this;
this.subsegments.push(subsegment);

if (!subsegment.end_time) {
if(subsegment.isSampled){
this.subsegments.push(subsegment);
}

if (!subsegment.end_time && subsegment.isSampled) {
this.incrementCounter(subsegment.counter);
}
};



/**
* Removes the subsegment from the subsegments array, used in subsegment streaming.
*/
Expand Down
85 changes: 85 additions & 0 deletions packages/core/test/unit/env/sqs_message_helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
var assert = require('chai').assert;
var chai = require('chai');
var sinonChai = require('sinon-chai');

import SqsMessageHelper from '../../../lib/env/sqs_message_helper';

chai.should();
chai.use(sinonChai);

describe('#SqsMessageHelper', function (){

// sample records from https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html
const sampleSqsMessageEvent = {
"Records": [
{
"messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
"receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...",
"body": "Test message.",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1545082649183",
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
"ApproximateFirstReceiveTimestamp": "1545082649185",
"AWSTraceHeader":"Root=1-632BB806-bd862e3fe1be46a994272793;Sampled=1"
},
"messageAttributes": {},
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
"awsRegion": "us-east-2"
},
{
"messageId": "2e1424d4-f796-459a-8184-9c92662be6da",
"receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...",
"body": "Test message.",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1545082650636",
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
"ApproximateFirstReceiveTimestamp": "1545082650649",
"AWSTraceHeader":"Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0"
},
"messageAttributes": {},
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
"awsRegion": "us-east-2"
},
{
"messageId": "2e1424d4-f796-459a-8184-9c92662be6da",
"receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...",
"body": "Test message.",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1545082650636",
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
"ApproximateFirstReceiveTimestamp": "1545082650649",
"AWSTraceHeader":"Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8"
},
"messageAttributes": {},
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
"awsRegion": "us-east-2"
}
]
}

describe('SqsMessageHelper isSampled', function(){
let sqsMessageHelper = new SqsMessageHelper();

it('should return true when AWSTraceHeader has Sampled=1', function(){
assert.equal(sqsMessageHelper.isSampled(sampleSqsMessageEvent.Records[0]), true)
});

it('should return false when AWSTraceHeader has Sampled=0', function(){
assert.equal(sqsMessageHelper.isSampled(sampleSqsMessageEvent.Records[1]), false)
});

it('should return false when AWSTraceHeader has no Sampled flag', function(){
assert.equal(sqsMessageHelper.isSampled(sampleSqsMessageEvent.Records[2]), false)
});

})
});
27 changes: 27 additions & 0 deletions packages/core/test/unit/segments/attributes/subsegment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,33 @@ describe('Subsegment', function() {
});
});

describe('#addSubsegmentWithoutSampling', function (){
it('should have isSampled flag set to false', function(){
var subsegment = new Subsegment('test');
var child = new Subsegment('child')
subsegment.addSubsegmentWithoutSampling(child);

assert.equal(subsegment.isSampled, true);
assert.equal(child.isSampled, false);
})

it('should have isSampled flag set to false for new subsegment', function(){
var subsegment = new Subsegment('test');
var child = subsegment.addNewSubsegmentWithoutSampling('child');

assert.equal(subsegment.isSampled, true);
assert.equal(child.isSampled, false);
})

it('should not contain the child subsegment if not sampled', function(){
var subsegment = new Subsegment('test');
var child = new Subsegment('child')
subsegment.addSubsegmentWithoutSampling(child);

assert.notEqual(subsegment.subsegments[0], child);
})
});

describe('#addError', function() {
var err, exceptionStub, sandbox, subsegment;

Expand Down
86 changes: 86 additions & 0 deletions packages/core/test/unit/segments/segment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var SegmentUtils = require('../../../lib/segments/segment_utils');
var Segment = require('../../../lib/segments/segment');
var Subsegment = require('../../../lib/segments/attributes/subsegment');
var logger = require('../../../lib/logger');
var Lambda = require('../../../lib/env/aws_lambda');
var Context = require('../../../lib/context_utils');

chai.should();
chai.use(sinonChai);
Expand Down Expand Up @@ -262,6 +264,90 @@ describe('Segment', function() {
});
});

describe('#addSubsegmentWithoutSampling', function (){
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice set of tests!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you!

let sandbox, setSegmentStub;

beforeEach(function() {
sandbox = sinon.createSandbox();
setSegmentStub = sandbox.stub(Context, 'setSegment');
});

afterEach(function() {
sandbox.restore();
});

it('should have isSampled flag set to false for subsegment of Lambda facade segment', function(){
process.env._X_AMZN_TRACE_ID;
Lambda.init();

setSegmentStub.should.have.been.calledOnce;

let facade = setSegmentStub.args[0][0];
let unsampledSegment = facade.addNewSubsegmentWithoutSampling('unsampled-subsegment');
assert.equal(unsampledSegment.isSampled, false);
})

it('should have isSampled flag set to true for subsegment of Lambda facade segment', function(){
process.env._X_AMZN_TRACE_ID;
Lambda.init();

setSegmentStub.should.have.been.calledOnce;

let facade = setSegmentStub.args[0][0];
let sampledSubsegment = facade.addNewSubsegment('sampled-subsegment');
assert.equal(sampledSubsegment.isSampled, true);
})

it('should have isSampled flag set to false', function(){
var segment = new Segment('parent');
var child = new Subsegment('child');
segment.addSubsegmentWithoutSampling(child);

assert.equal(child.isSampled, false);
})

it('should have isSampled flag set to false for new subsegment', function(){
var segment = new Segment('parent');
var child = segment.addNewSubsegmentWithoutSampling('child');

assert.equal(child.isSampled, false);
})

it('should not contain the child subsegment if not sampled', function(){
var segment = new Segment('parent');
var child = new Subsegment('child');
segment.addSubsegmentWithoutSampling(child);

assert.notEqual(segment.subsegments[0], child);
})


it('should not sample subsegment or subsegment of subsegment', function(){
var segment = new Segment('parent');
var child = new Subsegment('child');
var child2 = new Subsegment('child-2');
segment.addSubsegmentWithoutSampling(child);
child.addSubsegmentWithoutSampling(child2)

assert.equal(child.isSampled, false);
assert.equal(child2.isSampled, false);
})

it('should not sample subsegment or subsegment of subsegment - mix', function(){
var segment = new Segment('parent');
Copy link
Contributor

@atshaw43 atshaw43 Oct 17, 2022

Choose a reason for hiding this comment

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

Us needing to create the segment here signals that we re not running through the lambda specific code.

When the code thinks it is in lambda it will create a facade segment for us. The facade segment's trace data is filled out by what is in this variable. You can see an example in aws_lambda.test.js

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

I believe you need to call "init" here with that variable set.
packages/core/lib/env/aws_lambda.js

Let me know if you need more context here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing that out! I've incorporated that change into the updated unit tests

Copy link
Contributor Author

@carolabadeer carolabadeer Oct 19, 2022

Choose a reason for hiding this comment

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

To answer the question above, yes, this was tested in Lambda using the sample sample JSON data used in the Java oversampling mitigation PR.

For trace header with sampled=1:
image (3)

For trace header with sampled=0:
image (4)

var child = new Subsegment('child');
var child2 = new Subsegment('child-2');
var child3 = new Subsegment('child-3');
segment.addSubsegmentWithoutSampling(child);
child.addSubsegment(child2)
child.addSubsegmentWithoutSampling(child3)

assert.equal(child.isSampled, false);
assert.equal(child2.isSampled, true);
assert.equal(child3.isSampled, false);
})
});

Copy link
Contributor

Choose a reason for hiding this comment

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

After we close the segment, is it possible to check if the emitter sent out the segment or not?
And can we examine the trace header of the subsegment that is sent downstream to see what the sample flag is set to?

describe('#addError', function() {
var err, exceptionStub, sandbox, segment;

Expand Down