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

feat(logger): add CRITICAL log level #1399

Merged
merged 1 commit into from
Apr 7, 2023
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
24 changes: 19 additions & 5 deletions packages/logger/src/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ class Logger extends Utility implements ClassThatLogs {
INFO: 12,
WARN: 16,
ERROR: 20,
SILENT: 24,
CRITICAL: 24,
SILENT: 28,
};

private logsSampled: boolean = false;
Expand Down Expand Up @@ -212,17 +213,27 @@ class Logger extends Utility implements ClassThatLogs {
};
const parentsPowertoolsLogData = this.getPowertoolLogData();
const childLogger = new Logger(merge(parentsOptions, parentsPowertoolsLogData, options));

const parentsPersistentLogAttributes = this.getPersistentLogAttributes();
childLogger.addPersistentLogAttributes(parentsPersistentLogAttributes);

if (parentsPowertoolsLogData.lambdaContext) {
childLogger.addContext(parentsPowertoolsLogData.lambdaContext as Context);
}

return childLogger;
}

/**
* It prints a log item with level CRITICAL.
*
* @param {LogItemMessage} input
* @param {Error | LogAttributes | string} extraInput
*/
public critical(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
this.processLogItem('CRITICAL', input, extraInput);
}

/**
* It prints a log item with level DEBUG.
*
Expand Down Expand Up @@ -645,7 +656,10 @@ class Logger extends Utility implements ClassThatLogs {
private printLog(logLevel: LogLevel, log: LogItem): void {
log.prepareForPrint();

const consoleMethod = logLevel.toLowerCase() as keyof ClassThatLogs;
const consoleMethod =
logLevel === 'CRITICAL' ?
'error' :
logLevel.toLowerCase() as keyof Omit<ClassThatLogs, 'critical'>;

this.console[consoleMethod](JSON.stringify(log.getAttributes(), this.getReplacer(), this.logIndentation));
}
Expand Down
15 changes: 14 additions & 1 deletion packages/logger/src/types/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@ type LogLevelInfo = 'INFO';
type LogLevelWarn = 'WARN';
type LogLevelError = 'ERROR';
type LogLevelSilent = 'SILENT';
type LogLevelCritical = 'CRITICAL';

type LogLevel = LogLevelDebug | Lowercase<LogLevelDebug> | LogLevelInfo | Lowercase<LogLevelInfo> | LogLevelWarn | Lowercase<LogLevelWarn> | LogLevelError | Lowercase<LogLevelError> | LogLevelSilent | Lowercase<LogLevelSilent>;
type LogLevel =
LogLevelDebug |
Lowercase<LogLevelDebug> |
LogLevelInfo |
Lowercase<LogLevelInfo> |
LogLevelWarn |
Lowercase<LogLevelWarn> |
LogLevelError |
Lowercase<LogLevelError> |
LogLevelSilent |
Lowercase<LogLevelSilent> |
LogLevelCritical |
Lowercase<LogLevelCritical>;

type LogLevelThresholds = {
[key in Uppercase<LogLevel>]: number;
Expand Down
9 changes: 7 additions & 2 deletions packages/logger/src/types/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { AsyncHandler, LambdaInterface, SyncHandler } from '@aws-lambda-powertoo
import { Handler } from 'aws-lambda';
import { ConfigServiceInterface } from '../config';
import { LogFormatterInterface } from '../formatter';
import { Environment, LogAttributes, LogAttributesWithMessage, LogLevel } from './Log';
import {
Environment,
LogAttributes,
LogAttributesWithMessage,
LogLevel,
} from './Log';

type ClassThatLogs = {
[key in 'debug' | 'error' | 'info' | 'warn']: (input: LogItemMessage, ...extraInput: LogItemExtraInput) => void;
[key in Exclude<Lowercase<LogLevel>, 'silent'>]: (input: LogItemMessage, ...extraInput: LogItemExtraInput) => void;
};

type HandlerOptions = {
Expand Down
62 changes: 29 additions & 33 deletions packages/logger/tests/unit/Logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { Console } from 'console';

const mockDate = new Date(1466424490000);
const dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const getConsoleMethod = (method: string): keyof Omit<ClassThatLogs, 'critical'> =>
method === 'critical' ?
'error' :
method.toLowerCase() as keyof Omit<ClassThatLogs, 'critical'>;

describe('Class: Logger', () => {

Expand All @@ -27,7 +31,8 @@ describe('Class: Logger', () => {
INFO: 12,
WARN: 16,
ERROR: 20,
SILENT: 24,
CRITICAL: 24,
SILENT: 28,
};

beforeEach(() => {
Expand All @@ -40,6 +45,7 @@ describe('Class: Logger', () => {
[ 'info', 'DOES', true, 'DOES', true, 'DOES NOT', false, 'DOES NOT', false ],
[ 'warn', 'DOES', true, 'DOES', true, 'DOES', true, 'DOES NOT', false ],
[ 'error', 'DOES', true, 'DOES', true, 'DOES', true, 'DOES', true ],
[ 'critical', 'DOES', true, 'DOES', true, 'DOES', true, 'DOES', true ],
])(
'Method: %p',
(
Expand All @@ -54,17 +60,17 @@ describe('Class: Logger', () => {
errorPrints,
) => {

describe('Feature: log level', () => {
const methodOfLogger = method as keyof ClassThatLogs;

const methodOfLogger = method as keyof ClassThatLogs;
describe('Feature: log level', () => {

test('when the Logger\'s log level is DEBUG, it ' + debugAction + ' print to stdout', () => {

// Prepare
const logger: Logger = createLogger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(method)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -89,7 +95,7 @@ describe('Class: Logger', () => {
const logger: Logger = createLogger({
logLevel: 'INFO',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -114,7 +120,7 @@ describe('Class: Logger', () => {
const logger: Logger = createLogger({
logLevel: 'WARN',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -139,7 +145,7 @@ describe('Class: Logger', () => {
const logger: Logger = createLogger({
logLevel: 'ERROR',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -164,7 +170,7 @@ describe('Class: Logger', () => {
const logger: Logger = createLogger({
logLevel: 'SILENT',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -178,7 +184,7 @@ describe('Class: Logger', () => {
// Prepare
process.env.LOG_LEVEL = methodOfLogger.toUpperCase();
const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
logger[methodOfLogger]('foo');
Expand All @@ -197,16 +203,14 @@ describe('Class: Logger', () => {

describe('Feature: sample rate', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when the Logger\'s log level is higher and the current Lambda invocation IS NOT sampled for logging, it DOES NOT print to stdout', () => {

// Prepare
const logger: Logger = createLogger({
logLevel: 'SILENT',
sampleRateValue: 0,
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -224,7 +228,7 @@ describe('Class: Logger', () => {
logLevel: 'SILENT',
sampleRateValue: 1,
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -247,13 +251,11 @@ describe('Class: Logger', () => {

describe('Feature: inject context', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when the Lambda context is not captured and a string is passed as log message, it should print a valid ' + method.toUpperCase() + ' log', () => {

// Prepare
const logger: Logger = createLogger();
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -279,7 +281,7 @@ describe('Class: Logger', () => {
logLevel: 'DEBUG',
});
logger.addContext(context);
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand Down Expand Up @@ -307,15 +309,13 @@ describe('Class: Logger', () => {

describe('Feature: ephemeral log attributes', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when added, they should appear in that log item only', () => {

// Prepare
const logger: Logger = createLogger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

interface NestedObject { bool: boolean; str: string; num: number; err: Error }
interface ArbitraryObject<TNested> { value: 'CUSTOM' | 'USER_DEFINED'; nested: TNested }
Expand Down Expand Up @@ -444,8 +444,6 @@ describe('Class: Logger', () => {

describe('Feature: persistent log attributes', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when persistent log attributes are added to the Logger instance, they should appear in all logs printed by the instance', () => {

// Prepare
Expand All @@ -456,7 +454,7 @@ describe('Class: Logger', () => {
aws_region: 'eu-west-1',
},
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -481,15 +479,13 @@ describe('Class: Logger', () => {

describe('Feature: X-Ray Trace ID injection', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when the `_X_AMZN_TRACE_ID` environment variable is set it parses it correctly and adds the Trace ID to the log', () => {

// Prepare
const logger: Logger = createLogger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -515,7 +511,7 @@ describe('Class: Logger', () => {
const logger: Logger = createLogger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();

// Act
if (logger[methodOfLogger]) {
Expand All @@ -537,15 +533,13 @@ describe('Class: Logger', () => {

describe('Feature: handle safely unexpected errors', () => {

const methodOfLogger = method as keyof ClassThatLogs;

test('when a logged item references itself, the logger ignores the keys that cause a circular reference', () => {

// Prepare
const logger: Logger = createLogger({
logLevel: 'DEBUG',
});
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();
const circularObject = {
foo: 'bar',
self: {},
Expand Down Expand Up @@ -581,7 +575,7 @@ describe('Class: Logger', () => {

// Prepare
const logger = new Logger();
jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();
const message = `This is an ${methodOfLogger} log with BigInt value`;
const logItem = { value: BigInt(42) };
const errorMessage = 'Do not know how to serialize a BigInt';
Expand All @@ -595,7 +589,7 @@ describe('Class: Logger', () => {

// Prepare
const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();
const message = `This is an ${methodOfLogger} log with BigInt value`;
const logItem = { value: BigInt(42) };

Expand All @@ -619,7 +613,7 @@ describe('Class: Logger', () => {

// Prepare
const logger = new Logger();
const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation();
const consoleSpy = jest.spyOn(logger['console'], getConsoleMethod(methodOfLogger)).mockImplementation();
const message = `This is an ${methodOfLogger} log with empty, null, and undefined values`;
const logItem = { value: 42, emptyValue: '', undefinedValue: undefined, nullValue: null };

Expand Down Expand Up @@ -1050,6 +1044,7 @@ describe('Class: Logger', () => {
biz: 'baz'
}
});
jest.spyOn(logger['console'], 'debug').mockImplementation();
class LambdaFunction implements LambdaInterface {

@logger.injectLambdaContext({ clearState: true })
Expand Down Expand Up @@ -1091,6 +1086,7 @@ describe('Class: Logger', () => {
biz: 'baz'
}
});
jest.spyOn(logger['console'], 'debug').mockImplementation();
class LambdaFunction implements LambdaInterface {

@logger.injectLambdaContext({ clearState: true })
Expand Down
3 changes: 2 additions & 1 deletion packages/logger/tests/unit/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe('Helper: createLogger function', () => {
INFO: 12,
WARN: 16,
ERROR: 20,
SILENT: 24,
CRITICAL: 24,
SILENT: 28,
};

beforeEach(() => {
Expand Down