-
Notifications
You must be signed in to change notification settings - Fork 153
/
Copy pathLogger.ts
1444 lines (1333 loc) · 44.5 KB
/
Logger.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { Console } from 'node:console';
import { randomInt } from 'node:crypto';
import { Utility, isNullOrUndefined } from '@aws-lambda-powertools/commons';
import type {
AsyncHandler,
HandlerMethodDecorator,
SyncHandler,
} from '@aws-lambda-powertools/commons/types';
import type { Context, Handler } from 'aws-lambda';
import merge from 'lodash.merge';
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
import {
LogJsonIndent,
LogLevelThreshold,
ReservedKeys,
UncaughtErrorLogMessage,
} from './constants.js';
import type { LogFormatter } from './formatter/LogFormatter.js';
import type { LogItem } from './formatter/LogItem.js';
import { PowertoolsLogFormatter } from './formatter/PowertoolsLogFormatter.js';
import { CircularMap } from './logBuffer.js';
import type { ConfigServiceInterface } from './types/ConfigServiceInterface.js';
import type {
ConstructorOptions,
CustomJsonReplacerFn,
InjectLambdaContextOptions,
LogAttributes,
LogFunction,
LogItemExtraInput,
LogItemMessage,
LogLevel,
LoggerInterface,
} from './types/Logger.js';
import type {
LogKeys,
PowertoolsLogData,
UnformattedAttributes,
} from './types/logKeys.js';
/**
* The Logger utility provides an opinionated logger with output structured as JSON for AWS Lambda.
*
* **Key features**
* Capturing key fields from the Lambda context, cold starts, and structure logging output as JSON.
* Logging Lambda invocation events when instructed (disabled by default).
* Switch log level to `DEBUG` for a percentage of invocations (sampling).
* Buffering logs for a specific request or invocation, and flushing them automatically on error or manually as needed.
* Appending additional keys to structured logs at any point in time.
* Providing a custom log formatter (Bring Your Own Formatter) to output logs in a structure compatible with your organization’s Logging RFC.
*
* After initializing the Logger class, you can use the methods to log messages at different levels.
*
* @example
* ```typescript
* import { Logger } from '@aws-lambda-powertools/logger';
*
* const logger = new Logger({ serviceName: 'serverlessAirline' });
*
* export const handler = async (event, context) => {
* logger.info('This is an INFO log');
* logger.warn('This is a WARNING log');
* };
* ```
*
* To enrich the log items with information from the Lambda context, you can use the {@link Logger.addContext | `addContext()`} method.
*
* @example
* ```typescript
* import { Logger } from '@aws-lambda-powertools/logger';
*
* const logger = new Logger({ serviceName: 'serverlessAirline' });
*
* export const handler = async (event, context) => {
* logger.addContext(context);
*
* logger.info('This is an INFO log with some context');
* };
* ```
*
* You can also add additional attributes to all log items using the {@link Logger.appendKeys | `appendKeys()`} method.
*
* @example
* ```typescript
* export const handler = async (event, context) => {
* logger.appendKeys({ key1: 'value1' });
*
* logger.info('This is an INFO log with additional keys');
*
* logger.removeKeys(['key1']);
* };
*```
*
* If you write your functions as classes and use TypeScript, you can use the {@link Logger.injectLambdaContext | `injectLambdaContext()`} class method decorator
* to automatically add context to your logs and clear the state after the invocation.
*
* If instead you use Middy.js middlewares, you use the {@link "middleware/middy".injectLambdaContext | `injectLambdaContext()`} middleware.
*
* @see https://docs.powertools.aws.dev/lambda/typescript/latest/core/logger/
*/
class Logger extends Utility implements LoggerInterface {
/**
* Console instance used to print logs.
*
* In AWS Lambda, we create a new instance of the Console class so that we can have
* full control over the output of the logs. In testing environments, we use the
* default console instance.
*
* This property is initialized in the constructor in `setOptions()`.
*/
private console!: Console;
/**
* Custom config service instance used to configure the logger.
*/
private customConfigService?: ConfigServiceInterface;
/**
* Environment variables service instance used to fetch environment variables.
*/
private envVarsService = new EnvironmentVariablesService();
/**
* Whether to print the Lambda invocation event in the logs.
*/
private logEvent = false;
/**
* Formatter used to format the log items.
* @default new PowertoolsLogFormatter()
*/
private logFormatter: ConstructorOptions['logFormatter'];
/**
* JSON indentation used to format the logs.
*/
private logIndentation: number = LogJsonIndent.COMPACT;
/**
* Log level used internally by the current instance of Logger.
*/
private logLevel: number = LogLevelThreshold.INFO;
/**
* Persistent log attributes that will be logged in all log items.
*/
private persistentLogAttributes: LogAttributes = {};
/**
* Standard attributes managed by Powertools that will be logged in all log items.
*/
private powertoolsLogData: PowertoolsLogData = <PowertoolsLogData>{
sampleRateValue: 0,
};
/**
* Temporary log attributes that can be appended with `appendKeys()` method.
*/
private temporaryLogAttributes: LogKeys = {};
/**
* Buffer used to store logs until the logger is initialized.
*
* Sometimes we need to log warnings before the logger is fully initialized, however we can't log them
* immediately because the logger is not ready yet. This buffer stores those logs until the logger is ready.
*/
readonly #initBuffer: [
number,
Parameters<Logger['createAndPopulateLogItem']>,
][] = [];
/**
* Flag used to determine if the logger is initialized.
*/
#isInitialized = false;
/**
* Map used to hold the list of keys and their type.
*
* Because keys of different types can be overwritten, we keep a list of keys that were added and their last
* type. We then use this map at log preparation time to pick the last one.
*/
#keys: Map<string, 'temp' | 'persistent'> = new Map();
/**
* This is the initial log leval as set during the initialization of the logger.
*
* We keep this value to be able to reset the log level to the initial value when the sample rate is refreshed.
*/
#initialLogLevel: number = LogLevelThreshold.INFO;
/**
* Replacer function used to serialize the log items.
*/
#jsonReplacerFn?: CustomJsonReplacerFn;
/**
* Buffer configuration options.
*/
readonly #bufferConfig: {
/**
* Whether the buffer should is enabled
*/
enabled: boolean;
/**
* Whether the buffer should be flushed when an error is logged
*/
flushOnErrorLog: boolean;
/**
* Max size of the buffer. Additions to the buffer beyond this size will
* cause older logs to be evicted from the buffer
*/
maxBytes: number;
/**
* Log level threshold for the buffer
* Logs with a level lower than this threshold will be buffered
* Default is DEBUG
* Can be specified as a number (LogLevelThreshold value) or a string (log level name)
*/
bufferAtVerbosity: number;
} = {
enabled: false,
flushOnErrorLog: true,
maxBytes: 20480,
bufferAtVerbosity: LogLevelThreshold.DEBUG,
};
/**
* Contains buffered logs, grouped by `_X_AMZN_TRACE_ID`, each group with a max size of `maxBufferBytesSize`
*/
#buffer?: CircularMap<string>;
/**
* The debug sampling rate configuration.
*/
readonly #debugLogSampling = {
/**
* The sampling rate value used to determine if the log level should be set to DEBUG.
*/
sampleRateValue: 0,
/**
* The number of times the debug sampling rate has been refreshed.
*
* We use this to determine if we should refresh it again.
*/
refreshedTimes: 0,
};
/**
* Log level used by the current instance of Logger.
*
* Returns the log level as a number. The higher the number, the less verbose the logs.
* To get the log level name, use the {@link getLevelName()} method.
*/
public get level(): number {
return this.logLevel;
}
public constructor(options: ConstructorOptions = {}) {
super();
const { customConfigService, ...rest } = options;
this.setCustomConfigService(customConfigService);
// all logs are buffered until the logger is initialized
this.setOptions(rest);
this.#isInitialized = true;
for (const [level, log] of this.#initBuffer) {
// we call the method directly and create the log item just in time
this.printLog(level, this.createAndPopulateLogItem(...log));
}
this.#initBuffer = [];
}
/**
* Add the current Lambda function's invocation context data to the powertoolLogData property of the instance.
* This context data will be part of all printed log items.
*
* @param context - The Lambda function's invocation context.
*/
public addContext(context: Context): void {
this.addToPowertoolsLogData({
lambdaContext: {
invokedFunctionArn: context.invokedFunctionArn,
coldStart: this.getColdStart(),
awsRequestId: context.awsRequestId,
memoryLimitInMB: context.memoryLimitInMB,
functionName: context.functionName,
functionVersion: context.functionVersion,
},
});
}
/**
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys() `appendPersistentKeys()`} instead.
*/
public addPersistentLogAttributes(attributes: LogKeys): void {
this.appendPersistentKeys(attributes);
}
/**
* Add the given temporary attributes (key-value pairs) to all log items generated by this Logger instance.
*
* If the key already exists in the attributes, it will be overwritten. If the key is one of `level`, `message`, `sampling_rate`,
* `service`, or `timestamp` we will log a warning and drop the value.
*
* @param attributes - The attributes to add to all log items.
*/
public appendKeys(attributes: LogKeys): void {
this.#appendKeys(attributes, 'temp');
}
/**
* Add the given persistent attributes (key-value pairs) to all log items generated by this Logger instance.
*
* If the key already exists in the attributes, it will be overwritten. If the key is one of `level`, `message`, `sampling_rate`,
* `service`, or `timestamp` we will log a warning and drop the value.
*
* @param attributes - The attributes to add to all log items.
*/
public appendPersistentKeys(attributes: LogKeys): void {
this.#appendKeys(attributes, 'persistent');
}
/**
* Create a separate Logger instance, identical to the current one.
* It's possible to overwrite the new instance options by passing them.
*
* @param options - The options to initialize the child logger with.
*/
public createChild(options: ConstructorOptions = {}): Logger {
const childLogger = this.createLogger(
// Merge parent logger options with options passed to createChild,
// the latter having precedence.
merge(
{},
{
logLevel: this.getLevelName(),
serviceName: this.powertoolsLogData.serviceName,
sampleRateValue: this.#debugLogSampling.sampleRateValue,
logFormatter: this.getLogFormatter(),
customConfigService: this.getCustomConfigService(),
environment: this.powertoolsLogData.environment,
persistentLogAttributes: this.persistentLogAttributes,
jsonReplacerFn: this.#jsonReplacerFn,
...(this.#bufferConfig.enabled && {
logBufferOptions: {
maxBytes: this.#bufferConfig.maxBytes,
bufferAtVerbosity: this.getLogLevelNameFromNumber(
this.#bufferConfig.bufferAtVerbosity
),
flushOnErrorLog: this.#bufferConfig.flushOnErrorLog,
},
}),
},
options
)
);
if (this.powertoolsLogData.lambdaContext)
childLogger.addContext(
this.powertoolsLogData.lambdaContext as unknown as Context
);
if (this.temporaryLogAttributes) {
childLogger.appendKeys(this.temporaryLogAttributes);
}
return childLogger;
}
/**
* Print a log item with level CRITICAL.
*
* @param input - The log message.
* @param extraInput - The extra input to log.
*/
public critical(
input: LogItemMessage,
...extraInput: LogItemExtraInput
): void {
this.processLogItem(LogLevelThreshold.CRITICAL, input, extraInput);
}
/**
* Print a log item with level DEBUG.
*
* @param input
* @param extraInput - The extra input to log.
*/
public debug(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
this.processLogItem(LogLevelThreshold.DEBUG, input, extraInput);
}
/**
* Print a log item with level ERROR.
*
* @param input - The log message.
* @param extraInput - The extra input to log.
*/
public error(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
if (this.#bufferConfig.enabled && this.#bufferConfig.flushOnErrorLog) {
this.flushBuffer();
}
this.processLogItem(LogLevelThreshold.ERROR, input, extraInput);
}
/**
* Get the log level name of the current instance of Logger.
*
* Returns the log level name, i.e. `INFO`, `DEBUG`, etc.
* To get the log level as a number, use the {@link Logger.level} property.
*/
public getLevelName(): Uppercase<LogLevel> {
return this.getLogLevelNameFromNumber(this.logLevel);
}
/**
* Return a boolean value. True means that the Lambda invocation events
* are printed in the logs.
*/
public getLogEvent(): boolean {
return this.logEvent;
}
/**
* Return the persistent log attributes, which are the attributes
* that will be logged in all log items.
*/
public getPersistentLogAttributes(): LogAttributes {
return this.persistentLogAttributes;
}
/**
* Print a log item with level INFO.
*
* @param input - The log message.
* @param extraInput - The extra input to log.
*/
public info(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
this.processLogItem(LogLevelThreshold.INFO, input, extraInput);
}
/**
* Class method decorator that adds the current Lambda function context as extra
* information in all log items.
*
* This decorator is useful when you want to enrich your logs with information
* from the function context, such as the function name, version, and request ID, and more.
*
* @example
* ```typescript
* import { Logger } from '@aws-lambda-powertools/logger';
* import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
*
* const logger = new Logger({ serviceName: 'serverlessAirline' });
*
* class Lambda implements LambdaInterface {
* // Decorate your handler class method
* @logger.injectLambdaContext()
* public async handler(_event: unknown, _context: unknown): Promise<void> {
* logger.info('This is an INFO log with some context');
* }
* }
*
* const handlerClass = new Lambda();
* export const handler = handlerClass.handler.bind(handlerClass);
* ```
*
* The decorator can also be used to log the Lambda invocation event; this can be configured both via
* the `logEvent` parameter and the `POWERTOOLS_LOGGER_LOG_EVENT` environment variable. When both
* are set, the `logEvent` parameter takes precedence.
*
* Additionally, the decorator can be used to reset the temporary keys added with the `appendKeys()` method
* after the invocation, or to flush the buffer when an uncaught error is thrown in the handler.
*
* @see https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators
*
* @param options.logEvent - When `true` the logger will log the event.
* @param options.resetKeys - When `true` the logger will clear temporary keys added with {@link Logger.appendKeys() `appendKeys()`} method.
* @param options.flushBufferOnUncaughtError - When `true` the logger will flush the buffer when an uncaught error is thrown in the handler.
*/
public injectLambdaContext(
options?: InjectLambdaContextOptions
): HandlerMethodDecorator {
return (_target, _propertyKey, descriptor) => {
const originalMethod = descriptor.value as
| SyncHandler<Handler>
| AsyncHandler<Handler>;
const loggerRef = this;
// Use a function() {} instead of an () => {} arrow function so that we can
// access `myClass` as `this` in a decorated `myClass.myMethod()`.
descriptor.value = async function (
this: Handler,
event,
context,
callback
) {
loggerRef.refreshSampleRateCalculation();
loggerRef.addContext(context);
loggerRef.logEventIfEnabled(event, options?.logEvent);
try {
return await originalMethod.apply(this, [event, context, callback]);
} catch (error) {
if (options?.flushBufferOnUncaughtError) {
loggerRef.flushBuffer();
loggerRef.error({
message: UncaughtErrorLogMessage,
error,
});
}
throw error;
/* v8 ignore next */
} finally {
if (options?.clearState || options?.resetKeys) loggerRef.resetKeys();
loggerRef.clearBuffer();
}
};
};
}
/**
* @deprecated This method is deprecated and will be removed in the future major versions. Use {@link resetKeys()} instead.
*/
/* v8 ignore start */ public static injectLambdaContextAfterOrOnError(
logger: Logger,
_persistentAttributes: LogAttributes,
options?: InjectLambdaContextOptions
): void {
if (options && (options.clearState || options?.resetKeys)) {
logger.resetKeys();
}
} /* v8 ignore stop */
/**
* @deprecated - This method is deprecated and will be removed in the next major version.
*/
/* v8 ignore start */ public static injectLambdaContextBefore(
logger: Logger,
event: unknown,
context: Context,
options?: InjectLambdaContextOptions
): void {
logger.addContext(context);
let shouldLogEvent = undefined;
if (options && Object.hasOwn(options, 'logEvent')) {
shouldLogEvent = options.logEvent;
}
logger.logEventIfEnabled(event, shouldLogEvent);
} /* v8 ignore stop */
/**
* Log the AWS Lambda event payload for the current invocation if the environment variable `POWERTOOLS_LOGGER_LOG_EVENT` is set to `true`.
*
* @example
* ```ts
* process.env.POWERTOOLS_LOGGER_LOG_EVENT = 'true';
*
* import { Logger } from '@aws-lambda-powertools/logger';
*
* const logger = new Logger();
*
* export const handler = async (event) => {
* logger.logEventIfEnabled(event);
* // ... your handler code
* }
* ```
*
* @param event - The AWS Lambda event payload.
* @param overwriteValue - Overwrite the environment variable value.
*/
public logEventIfEnabled(event: unknown, overwriteValue?: boolean): void {
if (!this.shouldLogEvent(overwriteValue)) return;
this.info('Lambda invocation event', { event });
}
/**
* This method allows recalculating the initial sampling decision for changing
* the log level to DEBUG based on a sample rate value used during initialization,
* potentially yielding a different outcome.
*
* This only works for warm starts, because we don't to avoid double sampling.
*/
public refreshSampleRateCalculation(): void {
if (this.#debugLogSampling.refreshedTimes === 0) {
this.#debugLogSampling.refreshedTimes++;
return;
}
if (
this.#shouldEnableDebugSampling() &&
this.logLevel > LogLevelThreshold.TRACE
) {
this.setLogLevel('DEBUG');
this.debug('Setting log level to DEBUG due to sampling rate');
} else {
this.setLogLevel(this.getLogLevelNameFromNumber(this.#initialLogLevel));
}
}
/**
* Remove temporary attributes based on provided keys to all log items generated by this Logger instance.
*
* @param keys - The keys to remove.
*/
public removeKeys(keys: string[]): void {
for (const key of keys) {
this.temporaryLogAttributes[key] = undefined;
if (this.persistentLogAttributes[key]) {
this.#keys.set(key, 'persistent');
} else {
this.#keys.delete(key);
}
}
}
/**
* Remove the given keys from the persistent keys.
*
* @example
* ```typescript
* import { Logger } from '@aws-lambda-powertools/logger';
*
* const logger = new Logger({
* persistentKeys: {
* environment: 'prod',
* },
* });
*
* logger.removePersistentKeys(['environment']);
* ```
*
* @param keys - The keys to remove from the persistent attributes.
*/
public removePersistentKeys(keys: string[]): void {
for (const key of keys) {
this.persistentLogAttributes[key] = undefined;
if (this.temporaryLogAttributes[key]) {
this.#keys.set(key, 'temp');
} else {
this.#keys.delete(key);
}
}
}
/**
* @deprecated This method is deprecated and will be removed in the future major versions. Use {@link removePersistentKeys()} instead.
*/
public removePersistentLogAttributes(keys: string[]): void {
this.removePersistentKeys(keys);
}
/**
* Remove all temporary log attributes added with {@link appendKeys() `appendKeys()`} method.
*/
public resetKeys(): void {
for (const key of Object.keys(this.temporaryLogAttributes)) {
if (this.persistentLogAttributes[key]) {
this.#keys.set(key, 'persistent');
} else {
this.#keys.delete(key);
}
}
this.temporaryLogAttributes = {};
}
/**
* Set the log level for this Logger instance.
*
* If the log level is set using AWS Lambda Advanced Logging Controls, it sets it
* instead of the given log level to avoid data loss.
*
* @param logLevel The log level to set, i.e. `error`, `warn`, `info`, `debug`, etc.
*/
public setLogLevel(logLevel: LogLevel): void {
if (this.awsLogLevelShortCircuit(logLevel)) return;
if (this.isValidLogLevel(logLevel)) {
this.logLevel = LogLevelThreshold[logLevel];
} else {
throw new Error(`Invalid log level: ${logLevel}`);
}
}
/**
* @deprecated This method is deprecated and will be removed in the future major versions, please use {@link appendPersistentKeys() `appendPersistentKeys()`} instead.
*/
public setPersistentLogAttributes(attributes: LogKeys): void {
this.persistentLogAttributes = attributes;
}
/**
* Check whether the current Lambda invocation event should be printed in the logs or not.
*
* @param overwriteValue - Overwrite the environment variable value.
*/
public shouldLogEvent(overwriteValue?: boolean): boolean {
if (typeof overwriteValue === 'boolean') {
return overwriteValue;
}
return this.getLogEvent();
}
/**
* Print a log item with level TRACE.
*
* @param input - The log message.
* @param extraInput - The extra input to log.
*/
public trace(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
this.processLogItem(LogLevelThreshold.TRACE, input, extraInput);
}
/**
* Print a log item with level WARN.
*
* @param input - The log message.
* @param extraInput - The extra input to log.
*/
public warn(input: LogItemMessage, ...extraInput: LogItemExtraInput): void {
this.processLogItem(LogLevelThreshold.WARN, input, extraInput);
}
/**
* Factory method for instantiating logger instances. Used by `createChild` method.
* Important for customization and subclassing. It allows subclasses, like `MyOwnLogger`,
* to override its behavior while keeping the main business logic in `createChild` intact.
*
* @example
* ```typescript
* // MyOwnLogger subclass
* class MyOwnLogger extends Logger {
* protected createLogger(options?: ConstructorOptions): MyOwnLogger {
* return new MyOwnLogger(options);
* }
* // No need to re-implement business logic from `createChild` and keep track on changes
* public createChild(options?: ConstructorOptions): MyOwnLogger {
* return super.createChild(options) as MyOwnLogger;
* }
* }
* ```
*
* @param options - Logger configuration options.
*/
protected createLogger(options?: ConstructorOptions): Logger {
return new Logger(options);
}
/**
* A custom JSON replacer function that is used to serialize the log items.
*
* By default, we already extend the default serialization behavior to handle `BigInt` and `Error` objects, as well as remove circular references.
* When a custom JSON replacer function is passed to the Logger constructor, it will be called **before** our custom rules for each key-value pair in the object being stringified.
*
* This allows you to customize the serialization while still benefiting from the default behavior.
*
* @see {@link ConstructorOptions.jsonReplacerFn}
*/
protected getJsonReplacer(): (key: string, value: unknown) => void {
const references = new WeakSet();
return (key, value) => {
let replacedValue = value;
if (this.#jsonReplacerFn)
replacedValue = this.#jsonReplacerFn?.(key, replacedValue);
if (replacedValue instanceof Error) {
replacedValue = this.getLogFormatter().formatError(replacedValue);
}
if (typeof replacedValue === 'bigint') {
return replacedValue.toString();
}
if (typeof replacedValue === 'object' && replacedValue !== null) {
if (references.has(replacedValue)) {
return;
}
references.add(replacedValue);
}
return replacedValue;
};
}
/**
* Store information that is printed in all log items.
*
* @param attributes - The attributes to add to all log items.
*/
private addToPowertoolsLogData(attributes: Partial<PowertoolsLogData>): void {
merge(this.powertoolsLogData, attributes);
}
/**
* Shared logic for adding keys to the logger instance.
*
* @param attributes - The attributes to add to the log item.
* @param type - The type of the attributes to add.
*/
#appendKeys(attributes: LogKeys, type: 'temp' | 'persistent'): void {
for (const attributeKey of Object.keys(attributes)) {
if (this.#checkReservedKeyAndWarn(attributeKey) === false) {
this.#keys.set(attributeKey, type);
}
}
if (type === 'temp') {
merge(this.temporaryLogAttributes, attributes);
} else {
merge(this.persistentLogAttributes, attributes);
}
}
private awsLogLevelShortCircuit(selectedLogLevel?: string): boolean {
const awsLogLevel = this.getEnvVarsService().getAwsLogLevel();
if (this.isValidLogLevel(awsLogLevel)) {
this.logLevel = LogLevelThreshold[awsLogLevel];
if (
this.isValidLogLevel(selectedLogLevel) &&
this.logLevel > LogLevelThreshold[selectedLogLevel]
) {
this.warn(
`Current log level (${selectedLogLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level (${awsLogLevel}). This can lead to data loss, consider adjusting them.`
);
}
return true;
}
return false;
}
/**
* Create a log item and populate it with the given log level, input, and extra input.
*/
protected createAndPopulateLogItem(
logLevel: number,
input: LogItemMessage,
extraInput: LogItemExtraInput
): LogItem {
const unformattedBaseAttributes = {
logLevel: this.getLogLevelNameFromNumber(logLevel),
timestamp: new Date(),
xRayTraceId: this.envVarsService.getXrayTraceId(),
...this.getPowertoolsLogData(),
message: '',
};
const additionalAttributes = this.#createAdditionalAttributes();
this.#processMainInput(
input,
unformattedBaseAttributes,
additionalAttributes
);
this.#processExtraInput(extraInput, additionalAttributes);
return this.getLogFormatter().formatAttributes(
unformattedBaseAttributes,
additionalAttributes
);
}
/**
* Create additional attributes from persistent and temporary keys
*/
#createAdditionalAttributes(): LogAttributes {
const attributes: LogAttributes = {};
for (const [key, type] of this.#keys) {
if (!this.#checkReservedKeyAndWarn(key)) {
attributes[key] =
type === 'persistent'
? this.persistentLogAttributes[key]
: this.temporaryLogAttributes[key];
}
}
return attributes;
}
/**
* Process the main input message and add it to the attributes
*/
#processMainInput(
input: LogItemMessage,
baseAttributes: UnformattedAttributes,
additionalAttributes: LogAttributes
): void {
if (typeof input === 'string') {
baseAttributes.message = input;
return;
}
const { message, ...rest } = input;
baseAttributes.message = message;
for (const [key, value] of Object.entries(rest)) {
if (!this.#checkReservedKeyAndWarn(key)) {
additionalAttributes[key] = value;
}
}
}
/**
* Process extra input items and add them to additional attributes
*/
#processExtraInput(
extraInput: LogItemExtraInput,
additionalAttributes: LogAttributes
): void {
for (const item of extraInput) {
if (isNullOrUndefined(item)) {
continue;
}
if (item instanceof Error) {
additionalAttributes.error = item;
} else if (typeof item === 'string') {
additionalAttributes.extra = item;
} else {
this.#processExtraObject(item, additionalAttributes);
}
}
}
/**
* Process an extra input object and add its properties to additional attributes
*/
#processExtraObject(
item: Record<string, unknown>,
additionalAttributes: LogAttributes
): void {
for (const [key, value] of Object.entries(item)) {
if (!this.#checkReservedKeyAndWarn(key)) {
additionalAttributes[key] = value;
}
}
}
/**
* Make a new debug log sampling decision based on the sample rate value.
*/
#shouldEnableDebugSampling() {
return (
this.#debugLogSampling.sampleRateValue &&
randomInt(0, 100) / 100 <= this.#debugLogSampling.sampleRateValue
);
}
/**
* Check if a given key is reserved and warn the user if it is.
*
* @param key - The key to check
*/
#checkReservedKeyAndWarn(key: string): boolean {
if (ReservedKeys.includes(key)) {
this.warn(`The key "${key}" is a reserved key and will be dropped.`);
return true;
}
return false;
}
/**
* Get the custom config service, an abstraction used to fetch environment variables.
*/
private getCustomConfigService(): ConfigServiceInterface | undefined {
return this.customConfigService;
}
/**
* Get the instance of a service that fetches environment variables.
*/
private getEnvVarsService(): EnvironmentVariablesService {
return this.envVarsService as EnvironmentVariablesService;
}
/**
* Get the instance of a service that formats the structure of a
* log item's keys and values in the desired way.
*/
private getLogFormatter(): LogFormatter {
return this.logFormatter as LogFormatter;
}
/**
* Get the log level name from the log level number.
*
* For example, if the log level is 16, it will return 'WARN'.
*
* @param logLevel - The log level to get the name of
*/
private getLogLevelNameFromNumber(logLevel: number): Uppercase<LogLevel> {
let found: Uppercase<LogLevel> | undefined;
for (const [key, value] of Object.entries(LogLevelThreshold)) {
if (value === logLevel) {
found = key as Uppercase<LogLevel>;
break;
}
}
return found as Uppercase<LogLevel>;
}
/**
* Get information that will be added in all log item by
* this Logger instance (different from user-provided persistent attributes).
*/
private getPowertoolsLogData(): PowertoolsLogData {
return this.powertoolsLogData;
}
/**
* Check if a given log level is valid.
*
* @param logLevel - The log level to check
*/
private isValidLogLevel(
logLevel?: LogLevel | string