1- import type { Logger } from './types.js' ;
1+ import type { Logger , TaskLogContext , StartupContext } from './types.js' ;
22import { isLocalSupabaseEnv } from '../shared/localDetection.js' ;
33
44/**
@@ -11,6 +11,215 @@ export type LogFormat = 'fancy' | 'simple';
1111 */
1212export type LoggingEnv = Record < string , string | undefined > ;
1313
14+ // ============================================================
15+ // ANSI Color Codes (16-color safe palette)
16+ // ============================================================
17+
18+ const ANSI = {
19+ // Colors
20+ blue : '\x1b[34m' ,
21+ green : '\x1b[32m' ,
22+ red : '\x1b[31m' ,
23+ yellow : '\x1b[33m' ,
24+ dim : '\x1b[2m' ,
25+ gray : '\x1b[90m' ,
26+ white : '\x1b[37m' ,
27+
28+ // Formatting
29+ reset : '\x1b[0m' ,
30+ bold : '\x1b[1m' ,
31+ } ;
32+
33+ /**
34+ * Apply ANSI color to text if colors are enabled
35+ */
36+ function colorize ( text : string , color : string , enabled : boolean ) : string {
37+ return enabled ? `${ color } ${ text } ${ ANSI . reset } ` : text ;
38+ }
39+
40+ // ============================================================
41+ // Fancy Formatter (Local Dev) - Phase 3b
42+ // ============================================================
43+
44+ class FancyFormatter {
45+ constructor (
46+ private colorsEnabled : boolean ,
47+ private getWorkerName : ( ) => string ,
48+ private isDebugLevel : ( ) => boolean
49+ ) { }
50+
51+ private workerPrefix ( workerName ?: string ) : string {
52+ const name = workerName ?? this . getWorkerName ( ) ;
53+ return colorize ( `${ name } :` , ANSI . blue , this . colorsEnabled ) ;
54+ }
55+
56+ private flowStepPath ( flowSlug : string , stepSlug : string ) : string {
57+ return `${ flowSlug } /${ stepSlug } ` ;
58+ }
59+
60+ private identifiers ( ctx : TaskLogContext ) : string {
61+ // Only show identifiers at debug level (Phase 3b)
62+ if ( ! this . isDebugLevel ( ) ) return '' ;
63+ return colorize (
64+ `run_id=${ ctx . runId } msg_id=${ ctx . msgId } worker_id=${ ctx . workerId } ` ,
65+ ANSI . dim ,
66+ this . colorsEnabled
67+ ) ;
68+ }
69+
70+ taskStarted ( ctx : TaskLogContext ) : string {
71+ const prefix = this . workerPrefix ( ctx . workerName ) ;
72+ const icon = colorize ( '›' , ANSI . dim , this . colorsEnabled ) ;
73+ const path = colorize ( this . flowStepPath ( ctx . flowSlug , ctx . stepSlug ) , ANSI . dim , this . colorsEnabled ) ;
74+ const ids = this . identifiers ( ctx ) ;
75+
76+ const parts = [ prefix , icon , path ] ;
77+ if ( ids ) parts . push ( ids ) ;
78+ return parts . join ( ' ' ) ;
79+ }
80+
81+ taskCompleted ( ctx : TaskLogContext , durationMs : number ) : string {
82+ const prefix = this . workerPrefix ( ctx . workerName ) ;
83+ const icon = colorize ( '✓' , ANSI . green , this . colorsEnabled ) ;
84+ const path = colorize ( this . flowStepPath ( ctx . flowSlug , ctx . stepSlug ) , ANSI . green , this . colorsEnabled ) ;
85+ const duration = `${ durationMs } ms` ;
86+
87+ // Add retry info if present
88+ const retryInfo = ctx . retryAttempt && ctx . retryAttempt > 1
89+ ? colorize ( `retry ${ ctx . retryAttempt - 1 } ` , ANSI . dim , this . colorsEnabled )
90+ : '' ;
91+
92+ const ids = this . identifiers ( ctx ) ;
93+
94+ const parts = [ prefix , icon , path , duration ] ;
95+ if ( retryInfo ) parts . push ( retryInfo ) ;
96+ if ( ids ) parts . push ( ids ) ;
97+
98+ return parts . join ( ' ' ) ;
99+ }
100+
101+ taskFailed ( ctx : TaskLogContext , error : Error ) : string {
102+ const prefix = this . workerPrefix ( ctx . workerName ) ;
103+ const icon = colorize ( '✗' , ANSI . red , this . colorsEnabled ) ;
104+ const path = colorize ( this . flowStepPath ( ctx . flowSlug , ctx . stepSlug ) , ANSI . red , this . colorsEnabled ) ;
105+ const errorMsg = colorize ( error . message , ANSI . red , this . colorsEnabled ) ;
106+ const ids = this . identifiers ( ctx ) ;
107+
108+ let result = `${ prefix } ${ icon } ${ path } ` ;
109+ if ( ids ) result += ` ${ ids } ` ;
110+ result += `\n${ prefix } ${ errorMsg } ` ;
111+ return result ;
112+ }
113+
114+ polling ( ) : string {
115+ const prefix = this . workerPrefix ( ) ;
116+ return `${ prefix } Polling...` ;
117+ }
118+
119+ taskCount ( count : number ) : string {
120+ const prefix = this . workerPrefix ( ) ;
121+
122+ if ( count === 0 ) {
123+ const message = colorize ( 'No tasks' , ANSI . dim , this . colorsEnabled ) ;
124+ return `${ prefix } ${ message } ` ;
125+ }
126+
127+ return `${ prefix } Starting ${ colorize ( count . toString ( ) , ANSI . white , this . colorsEnabled ) } tasks` ;
128+ }
129+
130+ startupBanner ( ctx : StartupContext ) : string [ ] {
131+ const arrow = colorize ( '➜' , ANSI . green , this . colorsEnabled ) ;
132+ const workerName = colorize ( ctx . workerName , ANSI . bold , this . colorsEnabled ) ;
133+ const workerId = colorize ( `[${ ctx . workerId } ]` , ANSI . dim , this . colorsEnabled ) ;
134+
135+ const lines : string [ ] = [
136+ `${ arrow } ${ workerName } ${ workerId } ` ,
137+ ` Queue: ${ ctx . queueName } ` ,
138+ ] ;
139+
140+ // Multi-flow banner with aligned list (Phase 3b)
141+ ctx . flows . forEach ( ( flow , index ) => {
142+ const statusIcon = flow . compilationStatus === 'compiled' || flow . compilationStatus === 'verified'
143+ ? colorize ( '✓' , ANSI . green , this . colorsEnabled )
144+ : colorize ( '!' , ANSI . yellow , this . colorsEnabled ) ;
145+
146+ const statusText = colorize ( `(${ flow . compilationStatus } )` , ANSI . dim , this . colorsEnabled ) ;
147+ const label = index === 0 ? ' Flows:' : ' ' ;
148+ lines . push ( `${ label } ${ statusIcon } ${ flow . flowSlug } ${ statusText } ` ) ;
149+ } ) ;
150+
151+ return lines ;
152+ }
153+
154+ shutdown ( phase : 'deprecating' | 'waiting' | 'stopped' ) : string {
155+ const prefix = this . workerPrefix ( ) ;
156+ const icon = colorize ( 'ℹ' , ANSI . blue , this . colorsEnabled ) ;
157+
158+ if ( phase === 'deprecating' ) {
159+ return `${ prefix } ${ icon } Marked for deprecation\n${ prefix } -> Stopped accepting new messages` ;
160+ } else if ( phase === 'waiting' ) {
161+ return `${ prefix } -> Waiting for pending tasks...` ;
162+ } else {
163+ const checkmark = colorize ( '✓' , ANSI . green , this . colorsEnabled ) ;
164+ return `${ prefix } ${ checkmark } Stopped gracefully` ;
165+ }
166+ }
167+ }
168+
169+ // ============================================================
170+ // Simple Formatter (Hosted) - Phase 3b
171+ // ============================================================
172+
173+ class SimpleFormatter {
174+ constructor ( private getWorkerName : ( ) => string ) { }
175+
176+ taskStarted ( ctx : TaskLogContext ) : string {
177+ // Phase 3b: worker=X queue=Y flow=Z step=W format
178+ return `[DEBUG] worker=${ ctx . workerName } queue=${ ctx . queueName } flow=${ ctx . flowSlug } step=${ ctx . stepSlug } status=started run_id=${ ctx . runId } msg_id=${ ctx . msgId } worker_id=${ ctx . workerId } ` ;
179+ }
180+
181+ taskCompleted ( ctx : TaskLogContext , durationMs : number ) : string {
182+ const retry = ctx . retryAttempt && ctx . retryAttempt > 1 ? ` retry_attempt=${ ctx . retryAttempt } ` : '' ;
183+ // Phase 3b: worker=X queue=Y flow=Z step=W format
184+ return `[VERBOSE] worker=${ ctx . workerName } queue=${ ctx . queueName } flow=${ ctx . flowSlug } step=${ ctx . stepSlug } status=completed duration_ms=${ durationMs } run_id=${ ctx . runId } msg_id=${ ctx . msgId } worker_id=${ ctx . workerId } ${ retry } ` ;
185+ }
186+
187+ taskFailed ( ctx : TaskLogContext , error : Error ) : string {
188+ // Phase 3b: worker=X queue=Y flow=Z step=W format
189+ return `[VERBOSE] worker=${ ctx . workerName } queue=${ ctx . queueName } flow=${ ctx . flowSlug } step=${ ctx . stepSlug } status=failed error="${ error . message } " run_id=${ ctx . runId } msg_id=${ ctx . msgId } worker_id=${ ctx . workerId } ` ;
190+ }
191+
192+ polling ( ) : string {
193+ return `[VERBOSE] worker=${ this . getWorkerName ( ) } status=polling` ;
194+ }
195+
196+ taskCount ( count : number ) : string {
197+ if ( count === 0 ) {
198+ return `[VERBOSE] worker=${ this . getWorkerName ( ) } status=no_tasks` ;
199+ }
200+ return `[VERBOSE] worker=${ this . getWorkerName ( ) } status=starting task_count=${ count } ` ;
201+ }
202+
203+ startupBanner ( ctx : StartupContext ) : string [ ] {
204+ // Phase 3b: Multi-flow support
205+ const lines : string [ ] = [ ] ;
206+ for ( const flow of ctx . flows ) {
207+ lines . push ( `[INFO] worker=${ ctx . workerName } queue=${ ctx . queueName } flow=${ flow . flowSlug } status=${ flow . compilationStatus } worker_id=${ ctx . workerId } ` ) ;
208+ }
209+ return lines ;
210+ }
211+
212+ shutdown ( phase : 'deprecating' | 'waiting' | 'stopped' ) : string {
213+ if ( phase === 'deprecating' ) {
214+ return `[INFO] worker=${ this . getWorkerName ( ) } status=deprecating` ;
215+ } else if ( phase === 'waiting' ) {
216+ return `[INFO] worker=${ this . getWorkerName ( ) } status=waiting` ;
217+ } else {
218+ return `[INFO] worker=${ this . getWorkerName ( ) } status=stopped` ;
219+ }
220+ }
221+ }
222+
14223/**
15224 * Creates a logging factory with dynamic workerId support and environment-based configuration
16225 * @param env - Optional environment variables for auto-configuration (NO_COLOR, EDGE_WORKER_LOG_FORMAT, etc.)
@@ -33,6 +242,7 @@ export function createLoggingFactory(env?: LoggingEnv) {
33242
34243 // Shared state for all loggers
35244 let sharedWorkerId = 'unknown' ;
245+ let sharedWorkerName = 'unknown' ;
36246
37247 // All created logger instances - using Map for efficient lookup
38248 const loggers : Map < string , Logger > = new Map ( ) ;
@@ -41,6 +251,20 @@ export function createLoggingFactory(env?: LoggingEnv) {
41251 // Hierarchy: error < warn < info < verbose < debug
42252 const levels = { error : 0 , warn : 1 , info : 2 , verbose : 3 , debug : 4 } ;
43253
254+ // Helper to check if current log level is debug
255+ const isDebugLevel = ( ) => {
256+ const levelValue = levels [ logLevel as keyof typeof levels ] ?? levels . info ;
257+ return levelValue >= levels . debug ;
258+ } ;
259+
260+ // Helper to get worker name
261+ const getWorkerName = ( ) => sharedWorkerName ;
262+
263+ // Create formatter instance based on format
264+ const formatter = format === 'fancy'
265+ ? new FancyFormatter ( colorsEnabled , getWorkerName , isDebugLevel )
266+ : new SimpleFormatter ( getWorkerName ) ;
267+
44268 /**
45269 * Creates a new logger for a specific module
46270 */
@@ -100,6 +324,64 @@ export function createLoggingFactory(env?: LoggingEnv) {
100324 ) ;
101325 }
102326 } ,
327+
328+ // Structured logging methods
329+ taskStarted : ( ctx : TaskLogContext ) => {
330+ const levelValue =
331+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
332+ if ( levelValue >= levels . debug ) {
333+ console . debug ( formatter . taskStarted ( ctx ) ) ;
334+ }
335+ } ,
336+
337+ taskCompleted : ( ctx : TaskLogContext , durationMs : number ) => {
338+ const levelValue =
339+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
340+ if ( levelValue >= levels . verbose ) {
341+ console . log ( formatter . taskCompleted ( ctx , durationMs ) ) ;
342+ }
343+ } ,
344+
345+ taskFailed : ( ctx : TaskLogContext , error : Error ) => {
346+ const levelValue =
347+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
348+ if ( levelValue >= levels . verbose ) {
349+ console . log ( formatter . taskFailed ( ctx , error ) ) ;
350+ }
351+ } ,
352+
353+ polling : ( ) => {
354+ const levelValue =
355+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
356+ if ( levelValue >= levels . verbose ) {
357+ console . log ( formatter . polling ( ) ) ;
358+ }
359+ } ,
360+
361+ taskCount : ( count : number ) => {
362+ const levelValue =
363+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
364+ if ( levelValue >= levels . verbose ) {
365+ console . log ( formatter . taskCount ( count ) ) ;
366+ }
367+ } ,
368+
369+ startupBanner : ( ctx : StartupContext ) => {
370+ const levelValue =
371+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
372+ if ( levelValue >= levels . info ) {
373+ const lines = formatter . startupBanner ( ctx ) ;
374+ lines . forEach ( line => console . info ( line ) ) ;
375+ }
376+ } ,
377+
378+ shutdown : ( phase : 'deprecating' | 'waiting' | 'stopped' ) => {
379+ const levelValue =
380+ levels [ logLevel as keyof typeof levels ] ?? levels . info ;
381+ if ( levelValue >= levels . info ) {
382+ console . info ( formatter . shutdown ( phase ) ) ;
383+ }
384+ } ,
103385 } ;
104386
105387 // Store the logger in our registry using module as key
@@ -116,6 +398,13 @@ export function createLoggingFactory(env?: LoggingEnv) {
116398 sharedWorkerId = workerId ;
117399 } ;
118400
401+ /**
402+ * Updates the worker name for all loggers (Phase 3b)
403+ */
404+ const setWorkerName = ( workerName : string ) : void => {
405+ sharedWorkerName = workerName ;
406+ } ;
407+
119408 /**
120409 * Updates the log level for all loggers
121410 */
@@ -126,6 +415,7 @@ export function createLoggingFactory(env?: LoggingEnv) {
126415 return {
127416 createLogger,
128417 setWorkerId,
418+ setWorkerName,
129419 setLogLevel,
130420 // Expose configuration for inspection/testing
131421 get colorsEnabled ( ) {
0 commit comments