Skip to content

Commit 80a02b4

Browse files
committed
feat: Enforce 20% sampling on successful invocations in prod environment
1 parent f8e2870 commit 80a02b4

File tree

4 files changed

+60
-11
lines changed

4 files changed

+60
-11
lines changed

node/packages/aws-lambda-sdk/instrument/index.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ const serverlessSdk = require('./lib/sdk');
1818
const objHasOwnProperty = Object.prototype.hasOwnProperty;
1919
const unresolvedPromise = new Promise(() => {});
2020

21+
const coreTraceSpanNames = new Set([
22+
'aws.lambda',
23+
'aws.lambda.initialization',
24+
'aws.lambda.invocation',
25+
]);
26+
const alertEventNames = new Set(['telemetry.error.generated.v1', 'telemetry.warning.generated.v1']);
27+
2128
const capturedEvents = [];
2229
serverlessSdk._eventEmitter.on('captured-event', (capturedEvent) =>
2330
capturedEvents.push(capturedEvent)
@@ -91,25 +98,38 @@ const reportResponse = async (response, context, endTime) => {
9198
await sendTelemetry('request-response', payloadBuffer);
9299
};
93100

94-
const reportTrace = () => {
101+
const reportTrace = ({ isErrorOutcome }) => {
102+
// Sample out 80% of traces in production mode if no warning or error event was reported
103+
const isSampledOut =
104+
(!isErrorOutcome &&
105+
!serverlessSdk._isDebugMode &&
106+
!serverlessSdk._isDevMode &&
107+
!capturedEvents.some(({ name }) => alertEventNames.has(name)) &&
108+
Math.random() > 0.2) ||
109+
undefined;
95110
const payload = (serverlessSdk._lastTrace = {
111+
isSampledOut,
96112
slsTags: {
97113
orgId: serverlessSdk.orgId,
98114
service: process.env.AWS_LAMBDA_FUNCTION_NAME,
99115
sdk: { name: pkgJson.name, version: pkgJson.version },
100116
},
101-
spans: Array.from(awsLambdaSpan.spans).map((span) => {
117+
spans: Array.from(awsLambdaSpan.spans, (span) => {
118+
if (isSampledOut && !coreTraceSpanNames.has(span.name)) return null;
102119
const spanPayload = span.toProtobufObject();
103120
delete spanPayload.input;
104121
delete spanPayload.output;
105122
return spanPayload;
106-
}),
107-
events: capturedEvents
108-
.filter(filterCapturedEvent)
109-
.map((capturedEvent) => capturedEvent.toProtobufObject()),
110-
customTags: objHasOwnProperty.call(serverlessSdk, '_customTags')
111-
? JSON.stringify(serverlessSdk._customTags)
112-
: undefined,
123+
}).filter(Boolean),
124+
events: isSampledOut
125+
? []
126+
: capturedEvents
127+
.filter(filterCapturedEvent)
128+
.map((capturedEvent) => capturedEvent.toProtobufObject()),
129+
customTags:
130+
!isSampledOut && objHasOwnProperty.call(serverlessSdk, '_customTags')
131+
? JSON.stringify(serverlessSdk._customTags)
132+
: undefined,
113133
});
114134
const payloadBuffer = (serverlessSdk._lastTraceBuffer =
115135
traceProto.TracePayload.encode(payload).finish());
@@ -172,7 +192,7 @@ const closeTrace = async (outcome, outcomeResult) => {
172192
awsLambdaSpan.close({ endTime });
173193
// Root span comes with "aws.lambda.*" tags, which require unconditionally requestId
174194
// which we don't have if handler crashed at initialization
175-
if (invocationContextAccessor.value) reportTrace();
195+
if (invocationContextAccessor.value) reportTrace({ isErrorOutcome });
176196
flushSpans();
177197
clearRootSpan();
178198

node/packages/aws-lambda-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"author": "Serverless, Inc.",
66
"dependencies": {
77
"@serverless/sdk": "^0.5.2",
8-
"@serverless/sdk-schema": "^0.15.1",
8+
"@serverless/sdk-schema": "^0.15.2",
99
"d": "^1.0.1",
1010
"ext": "^1.7.0",
1111
"long": "^5.2.1",

node/packages/aws-lambda-sdk/test/integration/index.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,20 @@ describe('integration', function () {
552552
['v14', { configuration: { Runtime: 'nodejs14.x' } }],
553553
['v16', { configuration: { Runtime: 'nodejs16.x' } }],
554554
['v18', { configuration: { Runtime: 'nodejs18.x' } }],
555+
[
556+
'sampled',
557+
{
558+
configuration: {
559+
Environment: {
560+
Variables: {
561+
SLS_ORG_ID: process.env.SLS_ORG_ID,
562+
SLS_CRASH_ON_SDK_ERROR: '1',
563+
AWS_LAMBDA_EXEC_WRAPPER: '/opt/sls-sdk-node/exec-wrapper.sh',
564+
},
565+
},
566+
},
567+
},
568+
],
555569
[
556570
'sqs',
557571
{

node/packages/aws-lambda-sdk/test/unit/internal-extension/index.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,32 @@ describe('internal-extension/index.test.js', () => {
140140
process.env.SLS_ORG_ID = 'dummy';
141141
process.env.SLS_UNIT_TEST_RUN = '1';
142142
process.env.SLS_CRASH_ON_SDK_ERROR = '1';
143+
process.env.SLS_SDK_DEBUG = '1';
143144
});
144145
afterEach(() => {
145146
delete process.env._HANDLER;
146147
delete process.env.AWS_LAMBDA_FUNCTION_NAME;
147148
});
148149

150+
describe('sampling', () => {
151+
before(() => {
152+
delete process.env.SLS_SDK_DEBUG;
153+
});
154+
after(() => {
155+
process.env.SLS_SDK_DEBUG = '1';
156+
});
157+
it('should produce complete basic trace when sampled', async () => {
158+
// There's 80% chance that trace will be sampled
159+
return handleInvocation('callback');
160+
});
161+
});
162+
149163
it('should handle "ESM callback"', async () => handleInvocation('esm-callback/index'));
150164
it('should handle "ESM thenable"', async () => handleInvocation('esm-thenable/index'));
151165
it('should handle "ESM nested module"', async () =>
152166
handleInvocation('esm-nested/nested/within/index'));
153167
it('should handle "callback"', async () => handleInvocation('callback'));
168+
154169
it('should handle "thenable"', async () => handleInvocation('thenable'));
155170
it('should handle "esbuild from ESM callback', async () =>
156171
handleInvocation('esbuild-from-esm-callback'));

0 commit comments

Comments
 (0)