@@ -15,116 +15,149 @@ interface LogDisplayerOptions {
15
15
type ?: string
16
16
}
17
17
18
- function readLogs ( logplexURL : string , isTail : boolean , recreateSessionTimeout ?: number ) {
19
- return new Promise < void > ( ( resolve , reject ) => {
20
- const userAgent = process . env . HEROKU_DEBUG_USER_AGENT || 'heroku-run'
21
- const proxy = process . env . https_proxy || process . env . HTTPS_PROXY
22
-
23
- // Custom fetch function to handle headers and proxy
24
- const customFetch = async ( input : RequestInfo | URL , init ?: RequestInit ) => {
25
- const headers = new Headers ( init ?. headers )
26
- headers . set ( 'User-Agent' , userAgent )
27
-
28
- const fetchOptions : RequestInit = {
29
- ...init ,
30
- headers,
31
- }
32
-
33
- // If proxy is set, we need to handle it through environment variables
34
- // The fetch implementation will automatically use https_proxy/HTTPS_PROXY
35
- return fetch ( input , fetchOptions )
36
- }
18
+ export class LogDisplayer {
19
+ private heroku : APIClient
20
+ // private options: LogDisplayerOptions
37
21
38
- const es = new EventSource ( logplexURL , {
39
- fetch : customFetch ,
40
- } )
22
+ constructor ( heroku : APIClient ) {
23
+ this . heroku = heroku
24
+ }
41
25
42
- es . addEventListener ( 'error' , ( err : Event ) => {
43
- // The new eventsource package provides message and code properties on errors
44
- const errorEvent = err as any
45
- if ( errorEvent && ( errorEvent . code || errorEvent . message ) ) {
46
- const msg = ( isTail && ( errorEvent . code === 404 || errorEvent . code === 403 ) )
47
- ? 'Log stream timed out. Please try again.'
48
- : `Logs eventsource failed with: ${ errorEvent . code } ${ errorEvent . message ? ` ${ errorEvent . message } ` : '' } `
49
- reject ( new Error ( msg ) )
50
- es . close ( )
26
+ async display ( options : LogDisplayerOptions ) : Promise < void > {
27
+ this . setupProcessHandlers ( )
28
+
29
+ const firApp = ( await this . getGenerationByAppId ( options ) ) === 'fir'
30
+ const isTail = firApp || options . tail
31
+
32
+ const requestBodyParameters = this . buildRequestBodyParameters ( firApp , options )
33
+
34
+ let recreateLogSession = false
35
+ do {
36
+ const logSession = await this . createLogSession ( requestBodyParameters , options . app )
37
+
38
+ try {
39
+ await this . readLogs (
40
+ logSession . logplex_url ,
41
+ isTail ,
42
+ firApp ? Number ( process . env . HEROKU_LOG_STREAM_TIMEOUT || '15' ) * 60 * 1000 : undefined ,
43
+ )
44
+ } catch ( error : unknown ) {
45
+ const { message} = error as Error
46
+ if ( message === 'Fir log stream timeout' )
47
+ recreateLogSession = true
48
+ else
49
+ ux . error ( message , { exit : 1 } )
51
50
}
51
+ } while ( recreateLogSession )
52
+ }
52
53
53
- if ( ! isTail ) {
54
- resolve ( )
55
- es . close ( )
54
+ private setupProcessHandlers ( ) : void {
55
+ process . stdout . on ( 'error' , err => {
56
+ if ( err . code === 'EPIPE' ) {
57
+ // eslint-disable-next-line n/no-process-exit
58
+ process . exit ( 0 )
59
+ } else {
60
+ ux . error ( err . stack , { exit : 1 } )
56
61
}
57
-
58
- // should only land here if --tail and no error status or message
59
62
} )
63
+ }
60
64
61
- es . addEventListener ( 'message' , ( e : MessageEvent ) => {
62
- e . data . trim ( ) . split ( / \n + / ) . forEach ( ( line : string ) => {
63
- ux . stdout ( colorize ( line ) )
64
- } )
65
- } )
65
+ private async getGenerationByAppId ( options : LogDisplayerOptions ) : Promise < string > {
66
+ const generation = await getGenerationByAppId ( options . app , this . heroku )
67
+ return generation || ''
68
+ }
66
69
67
- if ( isTail && recreateSessionTimeout ) {
68
- setTimeout ( ( ) => {
69
- reject ( new Error ( 'Fir log stream timeout' ) )
70
- es . close ( )
71
- } , recreateSessionTimeout )
70
+ private buildRequestBodyParameters ( firApp : boolean , options : LogDisplayerOptions ) : Record < string , any > {
71
+ const requestBodyParameters = {
72
+ source : options . source ,
72
73
}
73
- } )
74
- }
75
74
76
- async function logDisplayer ( heroku : APIClient , options : LogDisplayerOptions ) {
77
- process . stdout . on ( 'error' , err => {
78
- if ( err . code === 'EPIPE' ) {
79
- // eslint-disable-next-line n/no-process-exit
80
- process . exit ( 0 )
75
+ if ( firApp ) {
76
+ process . stderr . write ( color . cyan . bold ( 'Fetching logs...\n\n' ) )
77
+ Object . assign ( requestBodyParameters , {
78
+ dyno : options . dyno ,
79
+ type : options . type ,
80
+ } )
81
81
} else {
82
- ux . error ( err . stack , { exit : 1 } )
82
+ Object . assign ( requestBodyParameters , {
83
+ dyno : options . dyno || options . type ,
84
+ lines : options . lines ,
85
+ tail : options . tail ,
86
+ } )
83
87
}
84
- } )
85
88
86
- const firApp = ( await getGenerationByAppId ( options . app , heroku ) ) === 'fir'
87
- const isTail = firApp || options . tail
88
-
89
- const requestBodyParameters = {
90
- source : options . source ,
91
- }
92
-
93
- if ( firApp ) {
94
- process . stderr . write ( color . cyan . bold ( 'Fetching logs...\n\n' ) )
95
- Object . assign ( requestBodyParameters , {
96
- dyno : options . dyno ,
97
- type : options . type ,
98
- } )
99
- } else {
100
- Object . assign ( requestBodyParameters , {
101
- dyno : options . dyno || options . type ,
102
- lines : options . lines ,
103
- tail : options . tail ,
104
- } )
89
+ return requestBodyParameters
105
90
}
106
91
107
- let recreateLogSession = false
108
- do {
109
- const { body : logSession } = await heroku . post < LogSession > ( `/apps/${ options . app } /log-sessions` , {
92
+ private async createLogSession ( requestBodyParameters : Record < string , any > , app : string ) : Promise < LogSession > {
93
+ const { body : logSession } = await this . heroku . post < LogSession > ( `/apps/${ app } /log-sessions` , {
110
94
body : requestBodyParameters ,
111
95
headers : { Accept : 'application/vnd.heroku+json; version=3.sdk' } ,
112
96
} )
97
+ return logSession
98
+ }
113
99
114
- try {
115
- await readLogs (
116
- logSession . logplex_url ,
117
- isTail ,
118
- firApp ? Number ( process . env . HEROKU_LOG_STREAM_TIMEOUT || '15' ) * 60 * 1000 : undefined ,
119
- )
120
- } catch ( error : unknown ) {
121
- const { message} = error as Error
122
- if ( message === 'Fir log stream timeout' )
123
- recreateLogSession = true
124
- else
125
- ux . error ( message , { exit : 1 } )
126
- }
127
- } while ( recreateLogSession )
100
+ private readLogs ( logplexURL : string , isTail : boolean , recreateSessionTimeout ?: number ) : Promise < void > {
101
+ return new Promise < void > ( ( resolve , reject ) => {
102
+ const userAgent = process . env . HEROKU_DEBUG_USER_AGENT || 'heroku-run'
103
+ const proxy = process . env . https_proxy || process . env . HTTPS_PROXY
104
+
105
+ // Custom fetch function to handle headers and proxy
106
+ const customFetch = async ( input : RequestInfo | URL , init ?: RequestInit ) => {
107
+ const headers = new Headers ( init ?. headers )
108
+ headers . set ( 'User-Agent' , userAgent )
109
+
110
+ const fetchOptions : RequestInit = {
111
+ ...init ,
112
+ headers,
113
+ }
114
+
115
+ // If proxy is set, we need to handle it through environment variables
116
+ // The fetch implementation will automatically use https_proxy/HTTPS_PROXY
117
+ return fetch ( input , fetchOptions )
118
+ }
119
+
120
+ const es = new EventSource ( logplexURL , {
121
+ fetch : customFetch ,
122
+ } )
123
+
124
+ es . addEventListener ( 'error' , ( err : Event ) => {
125
+ // The new eventsource package provides message and code properties on errors
126
+ const errorEvent = err as any
127
+ if ( errorEvent && ( errorEvent . code || errorEvent . message ) ) {
128
+ const msg = ( isTail && ( errorEvent . code === 404 || errorEvent . code === 403 ) )
129
+ ? 'Log stream timed out. Please try again.'
130
+ : `Logs eventsource failed with: ${ errorEvent . code } ${ errorEvent . message ? ` ${ errorEvent . message } ` : '' } `
131
+ reject ( new Error ( msg ) )
132
+ es . close ( )
133
+ }
134
+
135
+ if ( ! isTail ) {
136
+ resolve ( )
137
+ es . close ( )
138
+ }
139
+
140
+ // should only land here if --tail and no error status or message
141
+ } )
142
+
143
+ es . addEventListener ( 'message' , ( e : MessageEvent ) => {
144
+ e . data . trim ( ) . split ( / \n + / ) . forEach ( ( line : string ) => {
145
+ ux . stdout ( colorize ( line ) )
146
+ } )
147
+ } )
148
+
149
+ if ( isTail && recreateSessionTimeout ) {
150
+ setTimeout ( ( ) => {
151
+ reject ( new Error ( 'Fir log stream timeout' ) )
152
+ es . close ( )
153
+ } , recreateSessionTimeout )
154
+ }
155
+ } )
156
+ }
128
157
}
129
158
130
- export default logDisplayer
159
+ // Default export for backward compatibility
160
+ export default async function logDisplayer ( heroku : APIClient , options : LogDisplayerOptions ) : Promise < void > {
161
+ const displayer = new LogDisplayer ( heroku )
162
+ await displayer . display ( options )
163
+ }
0 commit comments