File size: 5,411 Bytes
d47b053
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**

 * Logger Utility

 * 统一日志工具

 */

import { appConfig } from '../config/app'

/**

 * 日志级别

 */
export enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3
}

/**

 * 日志级别映射

 */
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
  debug: LogLevel.DEBUG,
  info: LogLevel.INFO,
  warn: LogLevel.WARN,
  error: LogLevel.ERROR
}

function parseBooleanEnv(value: string | undefined): boolean | undefined {
  if (typeof value !== 'string') {
    return undefined
  }

  const normalized = value.trim().toLowerCase()
  if (['1', 'true', 'yes', 'on'].includes(normalized)) {
    return true
  }
  if (['0', 'false', 'no', 'off'].includes(normalized)) {
    return false
  }
  return undefined
}

/**
 * 当前日志级别
 */
const currentLogLevel = LOG_LEVEL_MAP[appConfig.logging.level] || LogLevel.INFO
const prodSummaryLogOnly =
  appConfig.nodeEnv === 'production' &&
  (parseBooleanEnv(process.env.PROD_SUMMARY_LOG_ONLY) ?? true)

/**

 * 格式化时间戳

 */
function formatTimestamp(): string {
  return new Date().toISOString()
}

/**

 * 安全的 JSON 序列化,处理大对象和循环引用

 */
function safeStringify(obj: any, maxLength?: number): string {
  try {
    const seen = new WeakSet()
    const json = JSON.stringify(obj, (key, value) => {
      // 处理循环引用
      if (typeof value === 'object' && value !== null) {
        if (seen.has(value)) {
          return '[Circular]'
        }
        seen.add(value)
      }
      return value
    }, 2)
    
    // 如果指定了最大长度且超出,则截断
    if (maxLength && json.length > maxLength) {
      return json.slice(0, maxLength) + `...(truncated, total ${json.length} chars)`
    }
    
    return json
  } catch (error) {
    return '[Serialization Error]'
  }
}

/**

 * 格式化日志消息

 */
function formatMessage(level: string, message: string, meta?: any): string {
  const timestamp = formatTimestamp()
  
  // 智能处理 meta 数据
  let metaStr = ''
  if (meta) {
    // 对于大文本字段,不进行截断(如 AI 响应、代码等)
    const largeTextFields = ['content', 'code', 'response', 'aiResponse', 'stdout', 'stderr', 'output']
    const hasLargeText = largeTextFields.some(field => field in meta)
    
    if (hasLargeText) {
      // 包含大文本,使用更大的限制或不限制
      metaStr = ` ${safeStringify(meta)}`
    } else {
      // 普通元数据,可以适当限制
      metaStr = ` ${safeStringify(meta, 5000)}`
    }
  }
  
  if (appConfig.logging.pretty) {
    // 开发环境:带颜色的格式
    const colors: Record<string, string> = {
      DEBUG: '\x1b[36m',  // Cyan
      INFO: '\x1b[32m',   // Green
      WARN: '\x1b[33m',   // Yellow
      ERROR: '\x1b[31m'   // Red
    }
    const reset = '\x1b[0m'
    const color = colors[level] || ''
    
    return `${color}[${timestamp}] ${level}${reset}: ${message}${metaStr}`
  } else {
    // 生产环境:JSON 格式
    return JSON.stringify({
      timestamp,
      level,
      message,
      ...meta
    })
  }
}

/**

 * Logger 类

 */
class Logger {
  private context?: string

  constructor(context?: string) {
    this.context = context
  }

  /**

   * 添加上下文信息

   */
  private addContext(meta?: any): any {
    if (!this.context) return meta
    return { ...meta, context: this.context }
  }

  private shouldEmit(level: LogLevel, meta?: any): boolean {
    if (!prodSummaryLogOnly) {
      return true
    }

    // In production summary-only mode, always keep warnings/errors visible.
    if (level >= LogLevel.WARN) {
      return true
    }

    return meta && typeof meta === 'object' && meta._logType === 'job_summary'
  }

  /**

   * 调试日志

   */
  debug(message: string, meta?: any): void {
    const contextMeta = this.addContext(meta)
    if (currentLogLevel <= LogLevel.DEBUG && this.shouldEmit(LogLevel.DEBUG, contextMeta)) {
      console.debug(formatMessage('DEBUG', message, contextMeta))
    }
  }

  /**

   * 信息日志

   */
  info(message: string, meta?: any): void {
    const contextMeta = this.addContext(meta)
    if (currentLogLevel <= LogLevel.INFO && this.shouldEmit(LogLevel.INFO, contextMeta)) {
      console.log(formatMessage('INFO', message, contextMeta))
    }
  }

  /**

   * 警告日志

   */
  warn(message: string, meta?: any): void {
    const contextMeta = this.addContext(meta)
    if (currentLogLevel <= LogLevel.WARN && this.shouldEmit(LogLevel.WARN, contextMeta)) {
      console.warn(formatMessage('WARN', message, contextMeta))
    }
  }

  /**

   * 错误日志

   */
  error(message: string, meta?: any): void {
    const contextMeta = this.addContext(meta)
    if (currentLogLevel <= LogLevel.ERROR && this.shouldEmit(LogLevel.ERROR, contextMeta)) {
      console.error(formatMessage('ERROR', message, contextMeta))
    }
  }

  /**

   * 创建子 logger 带上下文

   */
  child(context: string): Logger {
    const childContext = this.context ? `${this.context}:${context}` : context
    return new Logger(childContext)
  }
}

/**

 * 默认 logger 实例

 */
export const logger = new Logger()

/**

 * 创建带上下文的 logger

 */
export function createLogger(context: string): Logger {
  return new Logger(context)
}