Skip to content

Commit

Permalink
fix: size and time limitations on large invocations (#490)
Browse files Browse the repository at this point in the history
* fix: rewrite the max latency reached log
* fix: better debug log for splitting spans into zip bulks
* fix: log when spans are skipped due to large total spans size
* feat: env var for max spans size stored
* test: getStoredSpansMaxSize using value from env var if present
  • Loading branch information
sagivoululumigo authored Jul 14, 2024
1 parent 16ecb28 commit b6d3153
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 20 deletions.
8 changes: 7 additions & 1 deletion src/globals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as globals from './globals';
import {
DEFAULT_MAX_SIZE_FOR_REQUEST,
DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR,
DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
MAX_TRACER_ADDED_DURATION_ALLOWED,
} from './globals';
import { getMaxRequestSize } from './utils';
Expand Down Expand Up @@ -64,7 +65,7 @@ describe('globals', () => {
expect(spans).toEqual([span1]);
});

test('SpansContainer - cleanning the request size limiter', () => {
test('SpansContainer - cleaning the request size limiter', () => {
const span1 = { a: 'b', c: 'd', id: '1' };
for (let i = 0; i < getMaxRequestSize(); i++) {
globals.SpansContainer.addSpan(span1);
Expand Down Expand Up @@ -238,6 +239,7 @@ describe('globals', () => {
const isStepFunction = false;
const maxSizeForRequest = 1234;
const maxSizeForRequestOnError = 12345;
const maxSizeForStoredSpansInMemory = 1234567;
globals.TracerGlobals.setTracerInputs({
token,
debug,
Expand All @@ -246,6 +248,7 @@ describe('globals', () => {
isStepFunction,
maxSizeForRequest,
maxSizeForRequestOnError,
maxSizeForStoredSpansInMemory,
});
expect(globals.TracerGlobals.getTracerInputs()).toEqual({
token,
Expand All @@ -256,6 +259,7 @@ describe('globals', () => {
lambdaTimeout: MAX_TRACER_ADDED_DURATION_ALLOWED,
maxSizeForRequest,
maxSizeForRequestOnError,
maxSizeForStoredSpansInMemory,
});
globals.TracerGlobals.clearTracerInputs();
expect(globals.TracerGlobals.getTracerInputs()).toEqual({
Expand All @@ -267,6 +271,7 @@ describe('globals', () => {
isStepFunction: false,
maxSizeForRequest: DEFAULT_MAX_SIZE_FOR_REQUEST,
maxSizeForRequestOnError: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR,
maxSizeForStoredSpansInMemory: DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
});
});

Expand Down Expand Up @@ -310,6 +315,7 @@ describe('globals', () => {
lambdaTimeout: MAX_TRACER_ADDED_DURATION_ALLOWED,
maxSizeForRequest: DEFAULT_MAX_SIZE_FOR_REQUEST,
maxSizeForRequestOnError: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR,
maxSizeForStoredSpansInMemory: DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
});
});

Expand Down
41 changes: 32 additions & 9 deletions src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ import { BasicSpan } from './types/spans/basicSpan';
import {
getAutoTagKeys,
getJSONBase64Size,
getMaxRequestSize,
getMaxRequestSizeOnError,
getMaxSizeForStoredSpansInMemory,
isLambdaTraced,
spanHasErrors,
} from './utils';
import { GlobalDurationTimer } from './utils/globalDurationTimer';
import { isString } from '@lumigo/node-core/lib/common';
import { runOneTimeWrapper } from './utils/functionUtils';

const MAX_TAGS = 50;
const MAX_TAG_KEY_LEN = 50;
const MAX_TAG_VALUE_LEN = 70;
const ADD_TAG_ERROR_MSG_PREFIX = 'Skipping addExecutionTag: Unable to add tag';
export const DEFAULT_MAX_SIZE_FOR_REQUEST = 1024 * 500;
export const DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR = 1024 * 990;
export const DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY =
DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR * 10;
export const MAX_TRACER_ADDED_DURATION_ALLOWED = 750;
export const MIN_TRACER_ADDED_DURATION_ALLOWED = 200;

const warnSpansSizeOnce = runOneTimeWrapper((threshold: number, currentSize: number) => {
logger.info(
`Lumigo tracer is no longer collecting data on the invocation - maximum size of total spans collected (${threshold} bytes allowed, current size is ${currentSize} bytes)`
);
}, {});

export class SpansContainer {
private static spans: { [id: string]: BasicSpan } = {};
private static currentSpansSize: number = 0;
Expand All @@ -33,12 +41,19 @@ export class SpansContainer {
this.totalSpans += 1;
}
// Memory optimization, take up to 10x maxSize because of smart span selection logic
if (spanHasErrors(span) || getMaxRequestSizeOnError() * 10 > this.currentSpansSize) {
const maxSpansSize = getMaxSizeForStoredSpansInMemory();
if (spanHasErrors(span) || this.currentSpansSize <= maxSpansSize) {
this.spans[span.id] = span;
this.currentSpansSize += getJSONBase64Size(span);
logger.debug('Span created', span);
return true;
}

logger.debug('Span was not added due to size limitations', {
currentSpansSize: this.currentSpansSize,
maxSpansSize,
});
warnSpansSizeOnce(maxSpansSize, this.currentSpansSize);
return false;
}

Expand Down Expand Up @@ -213,6 +228,7 @@ export const TracerGlobals = (() => {
isStepFunction: false,
maxSizeForRequest: DEFAULT_MAX_SIZE_FOR_REQUEST,
maxSizeForRequestOnError: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR,
maxSizeForStoredSpansInMemory: DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
lambdaTimeout: MAX_TRACER_ADDED_DURATION_ALLOWED,
};

Expand Down Expand Up @@ -241,7 +257,15 @@ export const TracerGlobals = (() => {
maxSizeForRequest = null,
maxSizeForRequestOnError = null,
lambdaTimeout = MAX_TRACER_ADDED_DURATION_ALLOWED,
}: TracerOptions) =>
maxSizeForStoredSpansInMemory = DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
}: TracerOptions) => {
const parsedMaxSizeForRequestOnError =
maxSizeForRequestOnError ||
(process.env['LUMIGO_MAX_SIZE_FOR_REQUEST_ON_ERROR']
? parseInt(process.env.LUMIGO_MAX_SIZE_FOR_REQUEST_ON_ERROR)
: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR);
const parsedMaxSizeForStoredSpansInMemory =
maxSizeForStoredSpansInMemory || parsedMaxSizeForRequestOnError * 10;
Object.assign(tracerInputs, {
token: token || process.env.LUMIGO_TRACER_TOKEN,
debug: debug,
Expand All @@ -259,12 +283,10 @@ export const TracerGlobals = (() => {
(process.env['LUMIGO_MAX_SIZE_FOR_REQUEST']
? parseInt(process.env.LUMIGO_MAX_SIZE_FOR_REQUEST)
: DEFAULT_MAX_SIZE_FOR_REQUEST),
maxSizeForRequestOnError:
maxSizeForRequestOnError ||
(process.env['LUMIGO_MAX_SIZE_FOR_REQUEST_ON_ERROR']
? parseInt(process.env.LUMIGO_MAX_SIZE_FOR_REQUEST_ON_ERROR)
: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR),
maxSizeForRequestOnError: parsedMaxSizeForRequestOnError,
maxSizeForStoredSpansInMemory: parsedMaxSizeForStoredSpansInMemory,
});
};

const getTracerInputs = () => tracerInputs;

Expand All @@ -277,6 +299,7 @@ export const TracerGlobals = (() => {
isStepFunction: false,
maxSizeForRequest: DEFAULT_MAX_SIZE_FOR_REQUEST,
maxSizeForRequestOnError: DEFAULT_MAX_SIZE_FOR_REQUEST_ON_ERROR,
maxSizeForStoredSpansInMemory: DEFAULT_MAX_SIZE_FOR_SPANS_STORED_IN_MEMORY,
});

return {
Expand Down
2 changes: 1 addition & 1 deletion src/reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ describe('reporter', () => {
expect(JSON.parse(logs[1].obj).stack).toBeTruthy();
});

test('sending a huge payload, bigger than the regular limit, zipping and geeting is back completely', async () => {
test('sending a huge payload, bigger than the regular limit, zipping and getting is back completely', async () => {
const token = 'DEADBEEF';
TracerGlobals.setTracerInputs({ token });
const span = {
Expand Down
2 changes: 1 addition & 1 deletion src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const forgeAndScrubRequestBody = (
) {
if (shouldTryZip) {
logger.debug(
`Spans are too big, size [${size}], bigger than: [${maxRequestSize}], trying to split and zip`
`Spans are too big, size [${size}], [${spans.length}] spans, bigger than: [${maxRequestSize}], trying to split and zip`
);
const zippedSpansBulks = splitAndZipSpans(spans);
const areAllSpansSmallEnough = zippedSpansBulks.every(
Expand Down
1 change: 1 addition & 0 deletions src/tracer/tracer-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface TracerOptions {
maxSizeForRequestOnError?: number | null;
lambdaTimeout?: number;
verbose?: boolean;
maxSizeForStoredSpansInMemory?: number;
}
2 changes: 1 addition & 1 deletion src/tracer/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const sendEndTraceSpans = async (functionSpan, handlerReturnValue) => {
logLeakedSpans(spans);

const { transactionId } = endFunctionSpan;
logger.debug('Tracer ended', { transactionId });
logger.debug('Tracer ended', { transactionId, totalSpans: spans.length });
clearGlobals();
};

Expand Down
12 changes: 12 additions & 0 deletions src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
shouldScrubDomain,
getTraceId,
safeExecuteAsync,
getMaxSizeForStoredSpansInMemory,
LUMIGO_STORED_SPANS_MAX_SIZE_BYTES_ENV_VAR,
LUMIGO_SUPPORT_LARGE_INVOCATIONS,
} from './utils';

describe('utils', () => {
Expand Down Expand Up @@ -311,6 +314,15 @@ describe('utils', () => {
expect(utils.getEventEntitySize()).toBe(DEFAULT_LUMIGO_MAX_ENTRY_SIZE);
});

test('getStoredSpansMaxSize using env var value', () => {
const valueBefore = getMaxSizeForStoredSpansInMemory();
process.env[LUMIGO_STORED_SPANS_MAX_SIZE_BYTES_ENV_VAR] = `${valueBefore + 100}`;
expect(getMaxSizeForStoredSpansInMemory()).toBe(valueBefore);

process.env[LUMIGO_SUPPORT_LARGE_INVOCATIONS] = 'true';
expect(getMaxSizeForStoredSpansInMemory()).toBe(valueBefore + 100);
});

test('getConnectionTimeout => simple flow', () => {
process.env.LUMIGO_CONNECTION_TIMEOUT = '600';
expect(utils.getConnectionTimeout()).toBe(600);
Expand Down
30 changes: 27 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const LUMIGO_SECRET_MASKING_ALL_MAGIC = 'all';
export const LUMIGO_SECRET_MASKING_EXACT_PATH = 'LUMIGO_SECRET_MASKING_EXACT_PATH';
export const LUMIGO_WHITELIST_KEYS_REGEXES = 'LUMIGO_WHITELIST_KEYS_REGEXES';
export const LUMIGO_SUPPORT_LARGE_INVOCATIONS = 'LUMIGO_SUPPORT_LARGE_INVOCATIONS';
export const LUMIGO_STORED_SPANS_MAX_SIZE_BYTES_ENV_VAR = 'LUMIGO_STORED_SPANS_MAX_SIZE_BYTES';
export const OMITTING_KEYS_REGEXES = [
'.*pass.*',
'.*key.*',
Expand Down Expand Up @@ -225,7 +226,6 @@ const STORE_LOGS_FLAG = 'LUMIGO_STORE_LOGS';
const TIMEOUT_BUFFER_FLAG = 'LUMIGO_TIMEOUT_BUFFER';
const TIMEOUT_MIN_DURATION = 'LUMIGO_TIMEOUT_MIN_DURATION';
const TIMEOUT_BUFFER_FLAG_MS = 'LUMIGO_TIMEOUT_BUFFER_MS';
const TRACER_TIMEOUT_FLAG = 'LUMIGO_TRACER_TIMEOUT';
const AGENT_KEEPALIVE = 'LUMIGO_AGENT_KEEPALIVE_MS';
const REUSE_CONNECTION = 'LUMIGO_REUSE_HTTP_CONNECTION';
const KEEP_HEADERS = 'LUMIGO_KEEP_HTTP_HEADERS';
Expand All @@ -240,6 +240,7 @@ const LUMIGO_STACK_PATTERNS = [
new RegExp('/node_modules/@lumigo/tracer/', 'i'),
];

export const TRACER_TIMEOUT_FLAG = 'LUMIGO_TRACER_TIMEOUT';
export const SWITCH_OFF_FLAG = 'LUMIGO_SWITCH_OFF';

const validateEnvVar = (envVar: string, value: string = 'TRUE'): boolean =>
Expand Down Expand Up @@ -342,8 +343,6 @@ export const isLambdaWrapped = (): boolean => validateEnvVar(WRAPPED_FLAG);

export const shouldPropagateW3C = (): boolean => !validateEnvVar(LUMIGO_PROPAGATE_W3C, 'FALSE');

export const shouldTryZip = (): boolean => validateEnvVar(LUMIGO_SUPPORT_LARGE_INVOCATIONS);

export const setLambdaWrapped = (): void => {
process.env[WRAPPED_FLAG] = 'TRUE';
};
Expand Down Expand Up @@ -674,6 +673,31 @@ export const filterObjectKeys = (
return Object.assign(cur, { [key]: obj[key] });
}, {});

export const shouldTryZip = () => validateEnvVar(LUMIGO_SUPPORT_LARGE_INVOCATIONS);

const getLumigoStoredSpansMaxSizeBytesOverrideValue = () => {
const value = process.env[LUMIGO_STORED_SPANS_MAX_SIZE_BYTES_ENV_VAR];
if (value && shouldTryZip()) {
const parsedValue = parseInt(value);
if (!isNaN(parsedValue)) {
return parsedValue;
}
}
return undefined;
};

/**
* The maximum size of all spans stored in memory before sending them to lumigo.
* This limit is in place to prevent storing too many spans in memory and causing OOM errors.
* Note: when the invocation ends and the spans are processed before sending to Lumigo, more processing and truncating
* might take place
* @returns number maximum size in bytes
*/
export const getMaxSizeForStoredSpansInMemory = (): number => {
const overrideValue = getLumigoStoredSpansMaxSizeBytesOverrideValue();
return overrideValue || TracerGlobals.getTracerInputs().maxSizeForStoredSpansInMemory;
};

export const isLambdaTraced = () => isAwsEnvironment() && !isSwitchedOff();
export const getRequestBodyMaskingRegex = (): string | undefined =>
process.env[LUMIGO_SECRET_MASKING_REGEX_HTTP_REQUEST_BODIES];
Expand Down
12 changes: 8 additions & 4 deletions src/utils/globalDurationTimer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import * as logger from '../logger';
import { getTracerMaxDurationTimeout } from '../utils';
import { getTracerMaxDurationTimeout, TRACER_TIMEOUT_FLAG } from '../utils';
import { runOneTimeWrapper } from './functionUtils';

const warnTimeoutOnce = runOneTimeWrapper(() => {
logger.warn('Lumigo tracer is no longer collecting data on the invocation.');
const warnTimeoutOnce = runOneTimeWrapper((threshold: number, currentDuration: number) => {
logger.info(
`Stopped collecting data for this invocation because it reached the maximum ` +
`of ${threshold} ms added (added ${currentDuration} ms so far) to the duration of the Lambda. ` +
`This limit can be modified by setting the ${TRACER_TIMEOUT_FLAG} environment variable`
);
}, {});

export const GlobalDurationTimer = (() => {
Expand Down Expand Up @@ -32,7 +36,7 @@ export const GlobalDurationTimer = (() => {
appendTime();
threshold = threshold || getTracerMaxDurationTimeout();
if (currentDuration >= threshold) {
warnTimeoutOnce();
warnTimeoutOnce(threshold, currentDuration);
return true;
}
return false;
Expand Down

0 comments on commit b6d3153

Please sign in to comment.