66
77const util = require ( 'util' ) ;
88const fs = require ( 'fs' ) ;
9+ const Errors = require ( './Errors' ) ;
910
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+ }
1860
1961/* Use a unique symbol to provide global access without risk of name clashes. */
2062const REQUEST_ID_SYMBOL = Symbol . for ( 'aws.lambda.runtime.requestId' ) ;
@@ -26,10 +68,21 @@ let _currentRequestId = {
2668/**
2769 * Write logs to stdout.
2870 */
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 ) => {
3083 let time = new Date ( ) . toISOString ( ) ;
3184 let requestId = _currentRequestId . get ( ) ;
32- let line = ` ${ time } \t ${ requestId } \t ${ level . name } \t ${ message } ` ;
85+ let line = formatJsonMessage ( requestId , time , level , message , ... params ) ;
3386 line = line . replace ( / \n / g, '\r' ) ;
3487 process . stdout . write ( line + '\n' ) ;
3588} ;
@@ -46,15 +99,41 @@ let _logToStdout = (level, message) => {
4699 * The next 8 bytes are the UNIX timestamp of the message with microseconds precision.
47100 * The remaining bytes ar ethe message itself. Byte order is big-endian.
48101 */
49- let _logToFd = function ( logTarget ) {
102+ let logTextToFd = function ( logTarget ) {
50103 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`;
52112
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 ) => {
54125 let date = new Date ( ) ;
55126 let time = date . toISOString ( ) ;
56127 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 ) ;
58137 let messageBytes = Buffer . from ( enrichedMessage , 'utf8' ) ;
59138 typeAndLength . writeInt32BE ( messageBytes . length , 4 ) ;
60139 typeAndLength . writeBigInt64BE ( BigInt ( date . valueOf ( ) ) * 1000n , 8 ) ;
@@ -66,45 +145,100 @@ let _logToFd = function (logTarget) {
66145/**
67146 * Replace console functions with a log function.
68147 * @param {Function(level, String) } log
148+ * Apply log filters, based on `AWS_LAMBDA_LOG_LEVEL` env var
69149 */
70150function _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+ }
89199 console . fatal = ( msg , ...params ) => {
90- log ( levels . FATAL , util . format ( msg , ...params ) ) ;
200+ log ( levels . FATAL , msg , ...params ) ;
91201 } ;
92202}
93203
94204let _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 ;
95223 if (
96224 process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] != null &&
97225 process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] != undefined
98226 ) {
99227 let logFd = parseInt ( process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] ) ;
100- _patchConsoleWith ( _logToFd ( logFd ) ) ;
101228 delete process . env [ '_LAMBDA_TELEMETRY_LOG_FD' ] ;
229+ logger =
230+ awsLambdaLogFormat === JsonName ? logJsonToFd ( logFd ) : logTextToFd ( logFd ) ;
102231 } else {
103- _patchConsoleWith ( _logToStdout ) ;
232+ logger =
233+ awsLambdaLogFormat === JsonName ? logJsonToStdout : logTextToStdout ;
104234 }
235+ _patchConsoleWith ( logger ) ;
236+ structuredConsole . logError =
237+ awsLambdaLogFormat === JsonName ? jsonErrorLogger : textErrorLogger ;
105238} ;
106239
107240module . exports = {
108241 setCurrentRequestId : _currentRequestId . set ,
109242 patchConsole : _patchConsole ,
243+ structuredConsole : structuredConsole ,
110244} ;
0 commit comments