diff --git a/docs/core/logger.md b/docs/core/logger.md index fb25804135..a36660baa3 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -46,12 +46,12 @@ The library requires two settings. You can set them as environment variables, or These settings will be used across all logs emitted: -| Setting | Description | Environment variable | Default Value | Allowed Values | Example Value | Constructor parameter | -|-------------------------|------------------------------------------------------------------------------------------------------------------|---------------------------------|---------------------|--------------------------------|--------------------|-----------------------| -| **Service name** | Sets the name of service of which the Lambda function is part of, that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline`| `serviceName` | -| **Logging level** | Sets how verbose Logger should be | `LOG_LEVEL` | `info` |`DEBUG`, `INFO`, `WARN`, `ERROR`| `ERROR` | `logLevel` | -| **Log incoming event** | Whether to log or not the incoming event when using the decorator or middleware. | `POWERTOOLS_LOGGER_LOG_EVENT` | `false` | `true`, `false` | `false` | `logEvent` | -| **Debug log sampling** | Probability that a Lambda invocation will print all the log items regardless of the log level setting. | `POWERTOOLS_LOGGER_SAMPLE_RATE` | `0` | `0.0` to `1` | `0.5` | `sampleRateValue` | +| Setting | Description | Environment variable | Default Value | Allowed Values | Example Value | Constructor parameter | +|-------------------------|------------------------------------------------------------------------------------------------------------------|---------------------------------|---------------------|-------------------------------------------|--------------------|-----------------------| +| **Service name** | Sets the name of service of which the Lambda function is part of, that will be present across all log statements | `POWERTOOLS_SERVICE_NAME` | `service_undefined` | Any string | `serverlessAirline`| `serviceName` | +| **Logging level** | Sets how verbose Logger should be, from the most verbose to the least verbose (no logs) | `LOG_LEVEL` | `info` |`DEBUG`, `INFO`, `WARN`, `ERROR`, `SILENT` | `ERROR` | `logLevel` | +| **Log incoming event** | Whether to log or not the incoming event when using the decorator or middleware | `POWERTOOLS_LOGGER_LOG_EVENT` | `false` | `true`, `false` | `false` | `logEvent` | +| **Debug log sampling** | Probability that a Lambda invocation will print all the log items regardless of the log level setting | `POWERTOOLS_LOGGER_SAMPLE_RATE` | `0` | `0.0` to `1` | `0.5` | `sampleRateValue` | #### Example using AWS Serverless Application Model (SAM) @@ -81,7 +81,7 @@ Your Logger will include the following keys to your structured logging (default | Key | Example | Note | |-----------------------------|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **level**: `string` | `INFO` | Logging level set for the Lambda function"s invocation | +| **level**: `string` | `INFO` | Logging level set for the Lambda function's invocation | | **message**: `string` | `Query performed to DynamoDB` | A descriptive, human-readable representation of this log item | | **sampling_rate**: `float` | `0.1` | When enabled, it prints all the logs of a percentage of invocations, e.g. 10% | | **service**: `string` | `serverlessAirline` | A unique name identifier of the service this Lambda function belongs to, by default `service_undefined` | @@ -555,6 +555,17 @@ For example, by setting the "sample rate" to `0.5`, roughly 50% of your lambda i } ``` +### Silencing logs + +The `SILENT` log level provides a simple and efficient way to suppress all log messages without the need to modify your code. When you set this log level, all log messages, regardless of their severity, will be silenced. + +This feature is useful when you want to have your code instrumented to produce logs, but due to some requirement or business decision, you prefer to not emit them. + +By setting the log level to `SILENT`, which can be done either through the `logLevel` constructor option or by using the `LOG_LEVEL` environment variable, you can easily suppress all logs as needed. + +!!! note + Use the `SILENT` log level with care, as it can make it more challenging to monitor and debug your application. Therefore, we advise using this log level judiciously. + ### Custom Log formatter (Bring Your Own Formatter) You can customize the structure (keys and values) of your log items by passing a custom log formatter, an object that implements the `LogFormatter` abstract class. diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 663ce2bd37..13f80db3ed 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -130,11 +130,13 @@ class Logger extends Utility implements ClassThatLogs { private logLevel?: Uppercase; + // Log levels are in ascending order from the most verbose to the least verbose (no logs) private readonly logLevelThresholds: LogLevelThresholds = { DEBUG: 8, INFO: 12, WARN: 16, ERROR: 20, + SILENT: 24, }; private logsSampled: boolean = false; diff --git a/packages/logger/src/types/Log.ts b/packages/logger/src/types/Log.ts index 020b9cf200..15042910e5 100644 --- a/packages/logger/src/types/Log.ts +++ b/packages/logger/src/types/Log.ts @@ -2,8 +2,9 @@ type LogLevelDebug = 'DEBUG'; type LogLevelInfo = 'INFO'; type LogLevelWarn = 'WARN'; type LogLevelError = 'ERROR'; +type LogLevelSilent = 'SILENT'; -type LogLevel = LogLevelDebug | Lowercase | LogLevelInfo | Lowercase | LogLevelWarn | Lowercase | LogLevelError | Lowercase; +type LogLevel = LogLevelDebug | Lowercase | LogLevelInfo | Lowercase | LogLevelWarn | Lowercase | LogLevelError | Lowercase | LogLevelSilent | Lowercase; type LogLevelThresholds = { [key in Uppercase]: number; diff --git a/packages/logger/tests/unit/Logger.test.ts b/packages/logger/tests/unit/Logger.test.ts index eedb348c81..026e2ccb20 100644 --- a/packages/logger/tests/unit/Logger.test.ts +++ b/packages/logger/tests/unit/Logger.test.ts @@ -10,7 +10,7 @@ import { import { createLogger, Logger } from '../../src'; import { EnvironmentVariablesService } from '../../src/config'; import { PowertoolLogFormatter } from '../../src/formatter'; -import { ClassThatLogs, LogJsonIndent, ConstructorOptions } from '../../src/types'; +import { ClassThatLogs, LogJsonIndent, ConstructorOptions, LogLevelThresholds } from '../../src/types'; import { Context } from 'aws-lambda'; import { Console } from 'console'; @@ -22,6 +22,13 @@ describe('Class: Logger', () => { const ENVIRONMENT_VARIABLES = process.env; const context = dummyContext.helloworldContext; const event = dummyEvent.Custom.CustomEvent; + const logLevelThresholds: LogLevelThresholds = { + DEBUG: 8, + INFO: 12, + WARN: 16, + ERROR: 20, + SILENT: 24, + }; beforeEach(() => { dateSpy.mockClear(); @@ -60,9 +67,7 @@ describe('Class: Logger', () => { const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); // Act - if (logger[methodOfLogger]) { - logger[methodOfLogger]('foo'); - } + logger[methodOfLogger]('foo'); // Assess expect(consoleSpy).toBeCalledTimes(debugPrints ? 1 : 0); @@ -87,9 +92,7 @@ describe('Class: Logger', () => { const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); // Act - if (logger[methodOfLogger]) { - logger[methodOfLogger]('foo'); - } + logger[methodOfLogger]('foo'); // Assess expect(consoleSpy).toBeCalledTimes(infoPrints ? 1 : 0); @@ -114,9 +117,7 @@ describe('Class: Logger', () => { const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); // Act - if (logger[methodOfLogger]) { - logger[methodOfLogger]('foo'); - } + logger[methodOfLogger]('foo'); // Assess expect(consoleSpy).toBeCalledTimes(warnPrints ? 1 : 0); @@ -141,9 +142,7 @@ describe('Class: Logger', () => { const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); // Act - if (logger[methodOfLogger]) { - logger[methodOfLogger]('foo'); - } + logger[methodOfLogger]('foo'); // Assess expect(consoleSpy).toBeCalledTimes(errorPrints ? 1 : 0); @@ -159,6 +158,41 @@ describe('Class: Logger', () => { }); + test('when the Logger\'s log level is SILENT, it DOES NOT print to stdout', () => { + + // Prepare + const logger: Logger = createLogger({ + logLevel: 'SILENT', + }); + const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); + + // Act + logger[methodOfLogger]('foo'); + + // Assess + expect(consoleSpy).toBeCalledTimes(0); + }); + + test('when the Logger\'s log level is set through LOG_LEVEL env variable, it DOES print to stdout', () => { + + // Prepare + process.env.LOG_LEVEL = methodOfLogger.toUpperCase(); + const logger = new Logger(); + const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); + + // Act + logger[methodOfLogger]('foo'); + + // Assess + expect(consoleSpy).toBeCalledTimes(1); + expect(consoleSpy).toHaveBeenNthCalledWith(1, JSON.stringify({ + level: methodOfLogger.toUpperCase(), + message: 'foo', + service: 'hello-world', + timestamp: '2016-06-20T12:08:10.000Z', + xray_trace_id: '1-5759e988-bd862e3fe1be46a994272793', + })); + }); }); describe('Feature: sample rate', () => { @@ -169,7 +203,7 @@ describe('Class: Logger', () => { // Prepare const logger: Logger = createLogger({ - logLevel: 'ERROR', + logLevel: 'SILENT', sampleRateValue: 0, }); const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); @@ -180,14 +214,14 @@ describe('Class: Logger', () => { } // Assess - expect(consoleSpy).toBeCalledTimes(method === 'error' ? 1 : 0); + expect(consoleSpy).toBeCalledTimes(0); }); test('when the Logger\'s log level is higher and the current Lambda invocation IS sampled for logging, it DOES print to stdout', () => { // Prepare const logger: Logger = createLogger({ - logLevel: 'ERROR', + logLevel: 'SILENT', sampleRateValue: 1, }); const consoleSpy = jest.spyOn(logger['console'], methodOfLogger).mockImplementation(); @@ -630,10 +664,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1396,10 +1427,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1422,10 +1450,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: true, persistentLogAttributes: {}, @@ -1448,10 +1473,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: true, persistentLogAttributes: {}, @@ -1512,10 +1534,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1538,10 +1557,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: { @@ -1566,10 +1582,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: true, persistentLogAttributes: {}, @@ -1592,10 +1605,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'ERROR', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1641,10 +1651,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1667,10 +1674,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: { @@ -1700,10 +1704,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: { @@ -1746,10 +1747,7 @@ describe('Class: Logger', () => { logFormatter: expect.any(PowertoolLogFormatter), logLevel: 'DEBUG', logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -1980,5 +1978,4 @@ describe('Class: Logger', () => { }); }); - }); diff --git a/packages/logger/tests/unit/helpers.test.ts b/packages/logger/tests/unit/helpers.test.ts index 31842614fb..7da1226802 100644 --- a/packages/logger/tests/unit/helpers.test.ts +++ b/packages/logger/tests/unit/helpers.test.ts @@ -6,12 +6,19 @@ import { Console } from 'console'; import { ConfigServiceInterface, EnvironmentVariablesService } from '../../src/config'; import { LogFormatter, PowertoolLogFormatter } from '../../src/formatter'; -import { ConstructorOptions } from '../../src/types'; +import { ConstructorOptions, LogLevelThresholds } from '../../src/types'; import { createLogger, Logger } from './../../src'; describe('Helper: createLogger function', () => { const ENVIRONMENT_VARIABLES = process.env; + const logLevelThresholds: LogLevelThresholds = { + DEBUG: 8, + INFO: 12, + WARN: 16, + ERROR: 20, + SILENT: 24, + }; beforeEach(() => { jest.resetModules(); @@ -83,10 +90,7 @@ describe('Helper: createLogger function', () => { logLevel: 'WARN', console: expect.any(Console), logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: true, persistentLogAttributes: { @@ -125,10 +129,7 @@ describe('Helper: createLogger function', () => { logLevel: 'INFO', console: expect.any(Console), logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {}, @@ -279,10 +280,7 @@ describe('Helper: createLogger function', () => { logLevel: 'INFO', console: expect.any(Console), logLevelThresholds: { - DEBUG: 8, - ERROR: 20, - INFO: 12, - WARN: 16, + ...logLevelThresholds }, logsSampled: false, persistentLogAttributes: {},