-
Notifications
You must be signed in to change notification settings - Fork 273
/
Copy pathlog-entry.ts
485 lines (441 loc) · 13.8 KB
/
log-entry.ts
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import logSymbols from "log-symbols"
import { cloneDeep, round } from "lodash"
import { LogLevel } from "./logger"
import { Omit } from "../util/util"
import { Logger } from "./logger"
import uniqid from "uniqid"
import chalk from "chalk"
import { GardenError } from "../exceptions"
import hasAnsi from "has-ansi"
import { omitUndefined } from "../util/objects"
import { renderDuration } from "./util"
export type LogSymbol = keyof typeof logSymbols | "empty"
export type TaskLogStatus = "active" | "success" | "error"
export interface LogMetadata {
// TODO Remove this in favour of reading the task data from the (action) context.
task?: TaskMetadata
workflowStep?: WorkflowStepMetadata
}
export interface TaskMetadata {
type: string
key: string
status: TaskLogStatus
uid: string
inputVersion: string
outputVersion?: string
durationMs?: number
}
export interface WorkflowStepMetadata {
index: number
}
interface BaseContext {
/**
* Reference to what created the log message, e.g. tool that generated it (such as "docker")
*/
origin?: string
type: "coreLog" | "actionLog"
/**
* A session ID, to identify the log entry as part of a specific command execution.
*/
sessionId?: string
/**
* If applicable, the session ID of the parent command (e.g. serve or dev)
*/
parentSessionId?: string
/**
* The key of a Garden instance, if applicable.
*/
gardenKey?: string
}
export interface CoreLogContext extends BaseContext {
type: "coreLog"
/**
* The name of the log context. Will be printed as the "section" part of the log lines
* belonging to this context.
*/
name?: string
}
export interface ActionLogContext extends BaseContext {
type: "actionLog"
/**
* The name of the action that produced the log entry. Is printed in the "section" part of the log lines.
*/
actionName: string
/**
* The kind of the action that produced the log entry. Is printed in the "section" part of the log lines.
*/
actionKind: string
}
export type LogContext = CoreLogContext | ActionLogContext
/**
* Common Log config that the class implements and other interfaces pick / omit from.
*/
interface LogConfig<C extends BaseContext> {
/**
* A unique ID that's assigned to the config when it's created.
*/
key: string
timestamp: string
/**
* Additional metadata to pass to the log context. The metadata gets added to
* all log entries which can optionally extend it.
*/
metadata?: LogMetadata
/**
* Fix the level of all log entries created by this Log such that they're
* geq to this value.
*
* Useful to enforce the level in a given log context, e.g.:
* const debugLog = log.createLog({ fixLevel: LogLevel.debug })
*/
fixLevel?: LogLevel
context: C
/**
* Append the duration from when the log context was created and until the
* success or error methods are called to the success/error message.
* E.g.: If calling `log.sucess(Done!)`, then the log message becomes "Done! (in 4 sec)" if showDuration=true.
*/
showDuration?: boolean
}
interface LogConstructor<C extends BaseContext> extends Omit<LogConfig<C>, "key" | "timestamp"> {
root: Logger
parentConfigs: LogConfig<LogContext>[]
}
interface CreateLogParams
extends Pick<LogConfig<LogContext>, "metadata" | "fixLevel" | "showDuration">,
Pick<LogContext, "origin"> {}
interface CreateCoreLogParams
extends Pick<LogConfig<CoreLogContext>, "metadata" | "fixLevel" | "showDuration">,
Pick<CoreLogContext, "name" | "origin"> {
name?: string
origin?: string
}
export interface LogEntry<C extends BaseContext = LogContext>
extends Pick<LogConfig<C>, "key" | "timestamp" | "metadata" | "context"> {
/**
* The unique ID of the log context that created the log entry.
*/
parentLogKey: string
level: LogLevel
/**
* The actual text of the log message.
*/
msg?: string
/**
* A symbol that's printed with the log message to indicate it's type (e.g. "error" or "success").
*/
symbol?: LogSymbol
data?: any
dataFormat?: "json" | "yaml"
error?: GardenError
skipEmit?: boolean
}
interface LogParams
extends Pick<LogEntry, "metadata" | "msg" | "symbol" | "data" | "dataFormat" | "error" | "skipEmit">,
Pick<LogContext, "origin">,
Pick<LogConfig<LogContext>, "showDuration"> {}
interface CreateLogEntryParams extends LogParams {
level: LogLevel
}
/**
* A helper function for creating instances of ActionLogs. That is, the log class required
* by most actions.
*
* It differs from the "normal" CoreLog class in that it's context type is "ActionLogContext"
* which includes the action name and aciton kind.
*/
export function createActionLog({
log,
actionName,
actionKind,
origin,
fixLevel,
}: {
log: Log
actionName: string
actionKind: string
origin?: string
fixLevel?: LogLevel
}) {
return new ActionLog({
parentConfigs: [...log.parentConfigs, log.getConfig()],
metadata: log.metadata,
root: log.root,
fixLevel,
context: {
...omitUndefined(log.context),
type: "actionLog",
origin,
actionName,
actionKind,
},
})
}
/**
* The abstract log class which the CoreLog, ActionLog, and others extends.
*
* Contains all the methods the log classes use for writing logs at different levels
* a long with a handful of helper methods.
*/
export abstract class Log<C extends BaseContext = LogContext> implements LogConfig<C> {
public readonly showDuration?: boolean
public readonly metadata?: LogMetadata
public readonly key: string
public readonly parentConfigs: LogConfig<LogContext>[]
public readonly timestamp: string
public readonly root: Logger
public readonly fixLevel?: LogLevel
public readonly entries: LogEntry[]
public readonly context: C
constructor(params: LogConstructor<C>) {
this.key = uniqid()
this.entries = []
this.timestamp = new Date().toISOString()
this.parentConfigs = params.parentConfigs || []
this.root = params.root
this.fixLevel = params.fixLevel
this.metadata = params.metadata
this.context = params.context
this.showDuration = params.showDuration || false
}
/**
* Helper method for creating the actual log entry shape that gets passed to the root
* logger for writing.
*/
private createLogEntry(params: CreateLogEntryParams): LogEntry<C> {
const level = this.fixLevel ? Math.max(this.fixLevel, params.level) : params.level
let metadata: LogMetadata | undefined = undefined
if (this.metadata || params.metadata) {
metadata = { ...cloneDeep(this.metadata || {}), ...(params.metadata || {}) }
}
return {
...params,
parentLogKey: this.key,
context: {
...this.context,
origin: params.origin || this.context.origin,
},
level,
timestamp: new Date().toISOString(),
metadata,
key: uniqid(),
}
}
/**
* Helper method for creating the basic log config that gets passed down to child logs
* when creating new log instances.
*/
protected makeLogConfig(params: CreateLogParams) {
return {
metadata: params.metadata || this.metadata,
fixLevel: params.fixLevel || this.fixLevel,
showDuration: params.showDuration || false,
context: {
...this.context,
origin: params.origin || this.context.origin,
},
root: this.root,
parentConfigs: [...this.parentConfigs, this.getConfig()],
}
}
/**
* Create a new log instance of the same type as the parent log.
*/
abstract createLog(params?: CreateLogParams | CreateCoreLogParams): CoreLog | ActionLog
private log(params: CreateLogEntryParams) {
const entry = this.createLogEntry(params) as LogEntry
if (this.root.storeEntries) {
this.entries.push(entry)
}
this.root.log(entry)
return this
}
/**
* Append the duration to the log message if showDuration=true.
*
* That is, the time from when the log instance got created until now.
*/
private getMsgWithDuration(params: CreateLogEntryParams) {
// If params.showDuration is set, it takes precedence over this.duration (since it's set at the call site for the
// log line in question).
const showDuration = params.showDuration !== undefined
? params.showDuration
: this.showDuration
if (showDuration && params.msg) {
const msg = hasAnsi(params.msg) ? params.msg : chalk.green(params.msg)
return msg + " " + chalk.white(renderDuration(this.getDuration(1)))
}
return params.msg
}
private resolveCreateParams(level: LogLevel, params: string | LogParams): CreateLogEntryParams {
if (typeof params === "string") {
return { msg: params, level }
}
return { ...params, level }
}
/**
* Render a log entry at the silly level. This is the highest verbosity.
*/
silly(params: string | LogParams) {
return this.log(this.resolveCreateParams(LogLevel.silly, params))
}
/**
* Render a log entry at the debug level. Intended for internal information
* which can be useful for debugging.
*/
debug(params: string | LogParams) {
return this.log(this.resolveCreateParams(LogLevel.debug, params))
}
/**
* Render a log entry at the verbose level. Intended for logs generated when
* actios are executed. E.g. logs from Kubernetes.
*/
verbose(params: string | LogParams) {
return this.log(this.resolveCreateParams(LogLevel.verbose, params))
}
/**
* Render a log entry at the info level. Intended for framework level logs
* such as information about the action being executed.
*/
info(params: string | (LogParams & { symbol?: Extract<LogSymbol, "info" | "empty" | "success"> })) {
return this.log(this.resolveCreateParams(LogLevel.info, params))
}
/**
* Render a log entry at the warning level.
*/
warn(params: string | Omit<LogParams, "symbol">) {
return this.log({
...this.resolveCreateParams(LogLevel.warn, params),
symbol: "warning" as LogSymbol,
})
}
/**
* Render a log entry at the error level.
* Appends the duration to the message if showDuration=true.
*/
error(params: string | Omit<LogParams, "symbol">) {
const resolved = {
...this.resolveCreateParams(LogLevel.error, params),
symbol: "error" as LogSymbol,
}
return this.log({
...resolved,
msg: this.getMsgWithDuration(resolved),
})
}
/**
* Render a log entry at the info level with "success" styling.
* Appends the duration to the message if showDuration=true.
*
* TODO @eysi: This should really happen in the renderer and the parent log context
* timestamp, the log entry timestamp, and showDuration should just be fields on the entry.
*/
success(params: string | Omit<LogParams, "symbol">) {
const resolved = {
...this.resolveCreateParams(LogLevel.info, params),
symbol: "success" as LogSymbol,
}
const msgWithDuration = this.getMsgWithDuration(resolved)
const msg = hasAnsi(msgWithDuration || "") ? msgWithDuration : chalk.green(msgWithDuration)
return this.log({
...resolved,
msg,
})
}
getConfig(): LogConfig<C> {
return {
context: this.context,
metadata: this.metadata,
timestamp: this.timestamp,
key: this.key,
fixLevel: this.fixLevel,
}
}
/**
* Get the latest entry for this particular log context.
*/
getLatestEntry() {
return this.entries.slice(-1)[0]
}
getChildLogEntries() {
return this.entries
}
/**
* Get all log entries, from this and other contexts, via the root logger.
*/
getAllLogEntries() {
return this.root.getLogEntries()
}
/**
* Dumps child entries as a string, optionally filtering the entries with `filter`.
* For example, to dump all the logs of level info or higher:
*
* log.toString((entry) => entry.level <= LogLevel.info)
*/
toString(filter?: (log: LogEntry) => boolean) {
return this.getChildLogEntries()
.filter((entry) => (filter ? filter(entry) : true))
.map((entry) => entry.msg)
.join("\n")
}
/**
* Returns the duration in seconds, defaults to 2 decimal precision
*/
getDuration(precision: number = 2): number {
return round((new Date().getTime() - new Date(this.timestamp).getTime()) / 1000, precision)
}
toSanitizedValue() {
// TODO: add a bit more info here
return "<Log>"
}
}
/**
* This is the default log class and mostly used for log entries created before invoking
* actions and plugins.
*
* The corresponding log context has a name which is used in the section part when printing log
* lines.
*
* The log context can be overwritten when creating child logs.
*/
export class CoreLog extends Log<CoreLogContext> {
/**
* Create a new CoreLog instance, optionally overwriting the context.
*/
createLog(params: CreateCoreLogParams = {}): CoreLog {
return new CoreLog({
...this.makeLogConfig(params),
context: {
...this.context,
// Allow overwriting name
name: params.name || this.context.name,
// Allow overwriting origin
origin: params.origin || this.context.origin,
},
})
}
}
/**
* The ActionLog class is used for log entries created by actions.
*
* The corresponding log context requires 'actionName' and 'actionKind' fields
* which are used in the section part when printing log lines.
*
* The 'actionName' and 'actionKind' cannot be overwritten when creating child logs.
*/
export class ActionLog extends Log<ActionLogContext> {
showDuration = true
/**
* Create a new ActionLog instance. The new instance inherits the parent context.
*/
createLog(params: CreateLogParams = {}): ActionLog {
return new ActionLog(this.makeLogConfig(params))
}
}