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

fix: size and time limitations on large invocations #490

Merged
merged 14 commits into from
Jul 14, 2024
Merged
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
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,
sagivoululumigo marked this conversation as resolved.
Show resolved Hide resolved
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)
sagivoululumigo marked this conversation as resolved.
Show resolved Hide resolved
: 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