6
6
7
7
const util = require ( 'util' ) ;
8
8
const fs = require ( 'fs' ) ;
9
+ const Errors = require ( './Errors' ) ;
9
10
10
- const levels = Object . freeze ( {
11
- INFO : { name : 'INFO' } ,
12
- DEBUG : { name : 'DEBUG' } ,
13
- WARN : { name : 'WARN' } ,
14
- ERROR : { name : 'ERROR' } ,
15
- TRACE : { name : 'TRACE' } ,
16
- FATAL : { name : 'FATAL' } ,
17
- } ) ;
11
+ const structuredConsole = { } ;
12
+
13
+ const jsonErrorReplacer = ( _ , value ) => {
14
+ if ( value instanceof Error ) {
15
+ let serializedErr = Object . assign (
16
+ {
17
+ errorType : value ?. constructor ?. name ?? 'UnknownError' ,
18
+ errorMessage : value . message ,
19
+ stackTrace :
20
+ typeof value . stack === 'string'
21
+ ? value . stack . split ( '\n' )
22
+ : value . stack ,
23
+ } ,
24
+ value ,
25
+ ) ;
26
+ return serializedErr ;
27
+ }
28
+ return value ;
29
+ } ;
30
+
31
+ function formatJsonMessage ( requestId , timestamp , level , ...messageParams ) {
32
+ let result = {
33
+ timestamp : timestamp ,
34
+ level : level . name ,
35
+ requestId : requestId ,
36
+ } ;
37
+
38
+ if ( messageParams . length === 1 ) {
39
+ result . message = messageParams [ 0 ] ;
40
+ try {
41
+ return JSON . stringify ( result , jsonErrorReplacer ) ;
42
+ } catch ( _ ) {
43
+ result . message = util . format ( result . message ) ;
44
+ return JSON . stringify ( result ) ;
45
+ }
46
+ }
47
+
48
+ result . message = util . format ( ...messageParams ) ;
49
+ for ( const param of messageParams ) {
50
+ if ( param instanceof Error ) {
51
+ result . errorType = param ?. constructor ?. name ?? 'UnknownError' ;
52
+ result . errorMessage = param . message ;
53
+ result . stackTrace =
54
+ typeof param . stack === 'string' ? param . stack . split ( '\n' ) : [ ] ;
55
+ break ;
56
+ }
57
+ }
58
+ return JSON . stringify ( result ) ;
59
+ }
18
60
19
61
/* Use a unique symbol to provide global access without risk of name clashes. */
20
62
const REQUEST_ID_SYMBOL = Symbol . for ( 'aws.lambda.runtime.requestId' ) ;
@@ -26,10 +68,21 @@ let _currentRequestId = {
26
68
/**
27
69
* Write logs to stdout.
28
70
*/
29
- let _logToStdout = ( level , message ) => {
71
+ let logTextToStdout = ( level , message , ...params ) => {
72
+ let time = new Date ( ) . toISOString ( ) ;
73
+ let requestId = _currentRequestId . get ( ) ;
74
+ let line = `${ time } \t${ requestId } \t${ level . name } \t${ util . format (
75
+ message ,
76
+ ...params ,
77
+ ) } `;
78
+ line = line . replace ( / \n / g, '\r' ) ;
79
+ process . stdout . write ( line + '\n' ) ;
80
+ } ;
81
+
82
+ let logJsonToStdout = ( level , message , ...params ) => {
30
83
let time = new Date ( ) . toISOString ( ) ;
31
84
let requestId = _currentRequestId . get ( ) ;
32
- let line = ` ${ time } \t ${ requestId } \t ${ level . name } \t ${ message } ` ;
85
+ let line = formatJsonMessage ( requestId , time , level , message , ... params ) ;
33
86
line = line . replace ( / \n / g, '\r' ) ;
34
87
process . stdout . write ( line + '\n' ) ;
35
88
} ;
@@ -46,15 +99,41 @@ let _logToStdout = (level, message) => {
46
99
* The next 8 bytes are the UNIX timestamp of the message with microseconds precision.
47
100
* The remaining bytes ar ethe message itself. Byte order is big-endian.
48
101
*/
49
- let _logToFd = function ( logTarget ) {
102
+ let logTextToFd = function ( logTarget ) {
50
103
let typeAndLength = Buffer . alloc ( 16 ) ;
51
- typeAndLength . writeUInt32BE ( 0xa55a0003 , 0 ) ;
104
+ return ( level , message , ...params ) => {
105
+ let date = new Date ( ) ;
106
+ let time = date . toISOString ( ) ;
107
+ let requestId = _currentRequestId . get ( ) ;
108
+ let enrichedMessage = `${ time } \t${ requestId } \t${ level . name } \t${ util . format (
109
+ message ,
110
+ ...params ,
111
+ ) } \n`;
52
112
53
- return ( level , message ) => {
113
+ typeAndLength . writeUInt32BE ( ( 0xa55a0003 | level . tlvMask ) >>> 0 , 0 ) ;
114
+ let messageBytes = Buffer . from ( enrichedMessage , 'utf8' ) ;
115
+ typeAndLength . writeInt32BE ( messageBytes . length , 4 ) ;
116
+ typeAndLength . writeBigInt64BE ( BigInt ( date . valueOf ( ) ) * 1000n , 8 ) ;
117
+ fs . writeSync ( logTarget , typeAndLength ) ;
118
+ fs . writeSync ( logTarget , messageBytes ) ;
119
+ } ;
120
+ } ;
121
+
122
+ let logJsonToFd = function ( logTarget ) {
123
+ let typeAndLength = Buffer . alloc ( 16 ) ;
124
+ return ( level , message , ...params ) => {
54
125
let date = new Date ( ) ;
55
126
let time = date . toISOString ( ) ;
56
127
let requestId = _currentRequestId . get ( ) ;
57
- let enrichedMessage = `${ time } \t${ requestId } \t${ level . name } \t${ message } \n` ;
128
+ let enrichedMessage = formatJsonMessage (
129
+ requestId ,
130
+ time ,
131
+ level ,
132
+ message ,
133
+ ...params ,
134
+ ) ;
135
+
136
+ typeAndLength . writeUInt32BE ( ( 0xa55a0002 | level . tlvMask ) >>> 0 , 0 ) ;
58
137
let messageBytes = Buffer . from ( enrichedMessage , 'utf8' ) ;
59
138
typeAndLength . writeInt32BE ( messageBytes . length , 4 ) ;
60
139
typeAndLength . writeBigInt64BE ( BigInt ( date . valueOf ( ) ) * 1000n , 8 ) ;
@@ -66,45 +145,100 @@ let _logToFd = function (logTarget) {
66
145
/**
67
146
* Replace console functions with a log function.
68
147
* @param {Function(level, String) } log
148
+ * Apply log filters, based on `AWS_LAMBDA_LOG_LEVEL` env var
69
149
*/
70
150
function _patchConsoleWith ( log ) {
71
- console . log = ( msg , ...params ) => {
72
- log ( levels . INFO , util . format ( msg , ...params ) ) ;
73
- } ;
74
- console . debug = ( msg , ...params ) => {
75
- log ( levels . DEBUG , util . format ( msg , ...params ) ) ;
76
- } ;
77
- console . info = ( msg , ...params ) => {
78
- log ( levels . INFO , util . format ( msg , ...params ) ) ;
79
- } ;
80
- console . warn = ( msg , ...params ) => {
81
- log ( levels . WARN , util . format ( msg , ...params ) ) ;
82
- } ;
83
- console . error = ( msg , ...params ) => {
84
- log ( levels . ERROR , util . format ( msg , ...params ) ) ;
85
- } ;
86
- console . trace = ( msg , ...params ) => {
87
- log ( levels . TRACE , util . format ( msg , ...params ) ) ;
88
- } ;
151
+ const NopLog = ( _message , ..._params ) => { } ;
152
+ const levels = Object . freeze ( {
153
+ TRACE : { name : 'TRACE' , priority : 1 , tlvMask : 0b00100 } ,
154
+ DEBUG : { name : 'DEBUG' , priority : 2 , tlvMask : 0b01000 } ,
155
+ INFO : { name : 'INFO' , priority : 3 , tlvMask : 0b01100 } ,
156
+ WARN : { name : 'WARN' , priority : 4 , tlvMask : 0b10000 } ,
157
+ ERROR : { name : 'ERROR' , priority : 5 , tlvMask : 0b10100 } ,
158
+ FATAL : { name : 'FATAL' , priority : 6 , tlvMask : 0b11000 } ,
159
+ } ) ;
160
+ let awsLambdaLogLevel =
161
+ levels [ process . env [ 'AWS_LAMBDA_LOG_LEVEL' ] ?. toUpperCase ( ) ] ?? levels . TRACE ;
162
+
163
+ if ( levels . TRACE . priority >= awsLambdaLogLevel . priority ) {
164
+ console . trace = ( msg , ...params ) => {
165
+ log ( levels . TRACE , msg , ...params ) ;
166
+ } ;
167
+ } else {
168
+ console . trace = NopLog ;
169
+ }
170
+ if ( levels . DEBUG . priority >= awsLambdaLogLevel . priority ) {
171
+ console . debug = ( msg , ...params ) => {
172
+ log ( levels . DEBUG , msg , ...params ) ;
173
+ } ;
174
+ } else {
175
+ console . debug = NopLog ;
176
+ }
177
+ if ( levels . INFO . priority >= awsLambdaLogLevel . priority ) {
178
+ console . info = ( msg , ...params ) => {
179
+ log ( levels . INFO , msg , ...params ) ;
180
+ } ;
181
+ } else {
182
+ console . info = NopLog ;
183
+ }
184
+ console . log = console . info ;
185
+ if ( levels . WARN . priority >= awsLambdaLogLevel . priority ) {
186
+ console . warn = ( msg , ...params ) => {
187
+ log ( levels . WARN , msg , ...params ) ;
188
+ } ;
189
+ } else {
190
+ console . warn = NopLog ;
191
+ }
192
+ if ( levels . ERROR . priority >= awsLambdaLogLevel . priority ) {
193
+ console . error = ( msg , ...params ) => {
194
+ log ( levels . ERROR , msg , ...params ) ;
195
+ } ;
196
+ } else {
197
+ console . error = NopLog ;
198
+ }
89
199
console . fatal = ( msg , ...params ) => {
90
- log ( levels . FATAL , util . format ( msg , ...params ) ) ;
200
+ log ( levels . FATAL , msg , ...params ) ;
91
201
} ;
92
202
}
93
203
94
204
let _patchConsole = ( ) => {
205
+ const JsonName = 'JSON' ,
206
+ TextName = 'TEXT' ;
207
+ let awsLambdaLogFormat =
208
+ process . env [ 'AWS_LAMBDA_LOG_FORMAT' ] ?. toUpperCase ( ) === JsonName
209
+ ? JsonName
210
+ : TextName ;
211
+ let jsonErrorLogger = ( _ , err ) => {
212
+ console . error ( Errors . intoError ( err ) ) ;
213
+ } ,
214
+ textErrorLogger = ( msg , err ) => {
215
+ console . error ( msg , Errors . toFormatted ( Errors . intoError ( err ) ) ) ;
216
+ } ;
217
+
218
+ /**
219
+ Resolve log format here, instead of inside log function.
220
+ This avoids conditional statements in the log function hot path.
221
+ **/
222
+ let logger ;
95
223
if (
96
224
process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] != null &&
97
225
process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] != undefined
98
226
) {
99
227
let logFd = parseInt ( process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] ) ;
100
- _patchConsoleWith ( _logToFd ( logFd ) ) ;
101
228
delete process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] ;
229
+ logger =
230
+ awsLambdaLogFormat === JsonName ? logJsonToFd ( logFd ) : logTextToFd ( logFd ) ;
102
231
} else {
103
- _patchConsoleWith ( _logToStdout ) ;
232
+ logger =
233
+ awsLambdaLogFormat === JsonName ? logJsonToStdout : logTextToStdout ;
104
234
}
235
+ _patchConsoleWith ( logger ) ;
236
+ structuredConsole . logError =
237
+ awsLambdaLogFormat === JsonName ? jsonErrorLogger : textErrorLogger ;
105
238
} ;
106
239
107
240
module . exports = {
108
241
setCurrentRequestId : _currentRequestId . set ,
109
242
patchConsole : _patchConsole ,
243
+ structuredConsole : structuredConsole ,
110
244
} ;
0 commit comments