11import { boundMethod } from 'autobind-decorator' ;
22import { EventEmitter } from 'events' ;
33import CloudWatchLogs , {
4+ DescribeLogStreamsResponse ,
5+ LogStream ,
46 InputLogEvent ,
57 PutLogEventsRequest ,
68 PutLogEventsResponse ,
79} from 'aws-sdk/clients/cloudwatchlogs' ;
810import S3 , { PutObjectRequest , PutObjectOutput } from 'aws-sdk/clients/s3' ;
11+ import promiseSequential from 'promise-sequential' ;
912
1013import { SessionProxy } from './proxy' ;
11- import { HandlerRequest , runInSequence } from './utils' ;
14+ import { delay , HandlerRequest } from './utils' ;
1215
1316type Console = globalThis . Console ;
17+ type PromiseFunction = ( ) => Promise < any > ;
1418
1519interface LogOptions {
1620 groupName : string ;
@@ -26,13 +30,14 @@ export class ProviderLogHandler {
2630 private static instance : ProviderLogHandler ;
2731 public emitter : LogEmitter ;
2832 public client : CloudWatchLogs ;
29- public sequenceToken = '' ;
33+ public sequenceToken : string = null ;
3034 public accountId : string ;
3135 public groupName : string ;
3236 public stream : string ;
3337 public logger : Console ;
3438 public clientS3 : S3 ;
35- private stack : Array < Promise < any > > = [ ] ;
39+ private stack : Array < PromiseFunction > = [ ] ;
40+ private isProcessing = false ;
3641
3742 /**
3843 * The ProviderLogHandler's constructor should always be private to prevent direct
@@ -50,28 +55,48 @@ export class ProviderLogHandler {
5055 const logger = options . logger || global . console ;
5156 this . logger = logger ;
5257 this . emitter . on ( 'log' , ( ...args : any [ ] ) => {
53- this . stack . push ( this . deliverLog ( args ) ) ;
58+ // this.logger.debug('Emitting log event...' );
5459 } ) ;
5560 // Create maps of each logger method and then alias that.
5661 Object . entries ( this . logger ) . forEach ( ( [ key , val ] ) => {
5762 if ( typeof val === 'function' ) {
5863 if ( [ 'log' , 'error' , 'warn' , 'info' ] . includes ( key ) ) {
59- this . logger [ key as 'log' | 'error' | 'warn' | 'info' ] = function (
64+ this . logger [ key as 'log' | 'error' | 'warn' | 'info' ] = (
6065 ...args : any [ ]
61- ) : void {
62- // For adding other event watchers later.
63- setImmediate ( ( ) => emitter . emit ( 'log' , ...args ) ) ;
66+ ) : void => {
67+ if ( ! this . isProcessing ) {
68+ const logLevel = key . toUpperCase ( ) ;
69+ // Add log level when not present
70+ if (
71+ args . length &&
72+ args [ 0 ] . substring ( 0 , logLevel . length ) . toUpperCase ( ) !==
73+ logLevel
74+ ) {
75+ args . unshift ( logLevel ) ;
76+ }
77+ this . stack . push ( ( ) =>
78+ this . deliverLog ( args ) . catch ( this . logger . debug )
79+ ) ;
80+ // For adding other event watchers later.
81+ setImmediate ( ( ) => {
82+ this . emitter . emit ( 'log' , ...args ) ;
83+ } ) ;
84+ } else {
85+ this . logger . debug (
86+ 'Logs are being delivered at the moment...'
87+ ) ;
88+ }
6489
6590 // Calls the logger method.
66- val . apply ( this , args ) ;
91+ val . apply ( this . logger , args ) ;
6792 } ;
6893 }
6994 }
7095 } ) ;
7196 }
7297
7398 private async initialize ( ) : Promise < void > {
74- this . sequenceToken = '' ;
99+ this . sequenceToken = null ;
75100 this . stack = [ ] ;
76101 try {
77102 await this . deliverLogCloudWatch ( [ 'Initialize CloudWatch' ] ) ;
@@ -142,11 +167,13 @@ export class ProviderLogHandler {
142167
143168 @boundMethod
144169 public async processLogs ( ) : Promise < void > {
170+ this . isProcessing = true ;
145171 if ( this . stack . length > 0 ) {
146- this . stack . push ( this . deliverLog ( [ 'Log delivery finalized.' ] ) ) ;
172+ this . stack . push ( ( ) => this . deliverLog ( [ 'Log delivery finalized.' ] ) ) ;
147173 }
148- await runInSequence ( this . stack ) ;
174+ await promiseSequential ( this . stack ) ;
149175 this . stack = [ ] ;
176+ this . isProcessing = false ;
150177 }
151178
152179 private async createLogGroup ( ) : Promise < void > {
@@ -199,27 +226,48 @@ export class ProviderLogHandler {
199226 const response : PutLogEventsResponse = await this . client
200227 . putLogEvents ( logEventsParams )
201228 . promise ( ) ;
202- this . sequenceToken = response ?. nextSequenceToken ;
229+ this . sequenceToken = response ?. nextSequenceToken || null ;
203230 this . logger . debug ( 'Response from "putLogEvents"' , response ) ;
204231 return response ;
205232 } catch ( err ) {
206233 const errorCode = err . code || err . name ;
207- this . logger . debug ( 'Error from "deliverLogCloudWatch"' , err ) ;
208- this . logger . debug ( `Error from 'putLogEvents' ${ JSON . stringify ( err ) } ` ) ;
234+ this . logger . debug (
235+ `Error from "putLogEvents" with sequence token ${ this . sequenceToken } ` ,
236+ err
237+ ) ;
209238 if (
210239 errorCode === 'DataAlreadyAcceptedException' ||
211240 errorCode === 'InvalidSequenceTokenException'
212241 ) {
213- this . sequenceToken = ( err . message || '' ) . split ( ' ' ) . pop ( ) ;
214- this . putLogEvents ( record ) ;
242+ this . sequenceToken = null ;
243+ // Delay to avoid throttling
244+ await delay ( 1 ) ;
245+ try {
246+ const response : DescribeLogStreamsResponse = await this . client
247+ . describeLogStreams ( {
248+ logGroupName : this . groupName ,
249+ logStreamNamePrefix : this . stream ,
250+ limit : 1 ,
251+ } )
252+ . promise ( ) ;
253+ this . logger . debug ( 'Response from "describeLogStreams"' , response ) ;
254+ if ( response . logStreams && response . logStreams . length ) {
255+ const logStream = response . logStreams [ 0 ] as LogStream ;
256+ this . sequenceToken = logStream . uploadSequenceToken ;
257+ }
258+ } catch ( err ) {
259+ this . logger . debug ( 'Error from "describeLogStreams"' , err ) ;
260+ }
215261 } else {
216262 throw err ;
217263 }
218264 }
219265 }
220266
221267 @boundMethod
222- private async deliverLogCloudWatch ( messages : any [ ] ) : Promise < PutLogEventsResponse > {
268+ private async deliverLogCloudWatch (
269+ messages : any [ ]
270+ ) : Promise < PutLogEventsResponse | void > {
223271 const currentTime = new Date ( Date . now ( ) ) ;
224272 const record : InputLogEvent = {
225273 message : JSON . stringify ( { messages } ) ,
@@ -236,8 +284,20 @@ export class ProviderLogHandler {
236284 await this . createLogGroup ( ) ;
237285 }
238286 await this . createLogStream ( ) ;
239- return this . putLogEvents ( record ) ;
240- } else {
287+ } else if (
288+ errorCode !== 'DataAlreadyAcceptedException' &&
289+ errorCode !== 'InvalidSequenceTokenException'
290+ ) {
291+ throw err ;
292+ }
293+ try {
294+ const response = await this . putLogEvents ( record ) ;
295+ return response ;
296+ } catch ( err ) {
297+ // Additional retry for sequence token error
298+ if ( this . sequenceToken ) {
299+ return this . putLogEvents ( record ) ;
300+ }
241301 throw err ;
242302 }
243303 }
@@ -316,7 +376,7 @@ export class ProviderLogHandler {
316376 @boundMethod
317377 private async deliverLog (
318378 messages : any [ ]
319- ) : Promise < PutLogEventsResponse | PutObjectOutput > {
379+ ) : Promise < PutLogEventsResponse | PutObjectOutput | void > {
320380 if ( this . clientS3 ) {
321381 return this . deliverLogS3 ( messages ) ;
322382 }
0 commit comments