Skip to content

Commit

Permalink
improvement(logger): store all message states
Browse files Browse the repository at this point in the history
  • Loading branch information
eysi09 authored and edvald committed Aug 27, 2019
1 parent 3677900 commit b68f343
Show file tree
Hide file tree
Showing 13 changed files with 438 additions and 191 deletions.
7 changes: 2 additions & 5 deletions garden-service/src/commands/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,7 @@ export class LogsCommand extends Command<Args, Opts> {
} catch { }
}

log.info({
section: entry.serviceName,
msg: [timestamp, chalk.white(entry.msg)],
})
log.info({ section: entry.serviceName, msg: `${timestamp}${chalk.white(entry.msg)}` })

if (!follow) {
result.push(entry)
Expand All @@ -98,7 +95,7 @@ export class LogsCommand extends Command<Args, Opts> {
const actions = await garden.getActionHelper()

await Bluebird.map(services, async (service: Service<any>) => {
const voidLog = log.placeholder(LogLevel.silly, { childEntriesInheritLevel: true })
const voidLog = log.placeholder(LogLevel.silly, true)
const runtimeContext = await getServiceRuntimeContext(garden, graph, service)
const status = await actions.getServiceStatus({ log: voidLog, service, hotReload: false, runtimeContext })

Expand Down
239 changes: 143 additions & 96 deletions garden-service/src/logger/log-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@

import * as logSymbols from "log-symbols"
import * as nodeEmoji from "node-emoji"
import { flatten } from "lodash"

import { LogNode, LogLevel } from "./log-node"
import { getChildEntries, findParentEntry, sanitizeObject } from "./util"
import { GardenError } from "../exceptions"
import { LogNode, LogLevel, CreateNodeParams } from "./log-node"
import { Omit } from "../util/util"
import { getChildEntries, findParentEntry } from "./util"
import { GardenError } from "../exceptions"
import { Logger } from "./logger"

export type EmojiName = keyof typeof nodeEmoji.emoji
Expand All @@ -32,106 +31,147 @@ export interface TaskMetadata {
durationMs?: number,
}

export interface UpdateOpts {
msg?: string | string[]
data?: any // to be rendered as e.g. YAML or JSON
section?: string
interface MessageBase {
msg?: string
emoji?: EmojiName
status?: EntryStatus
section?: string
symbol?: LogSymbol
append?: boolean
fromStdStream?: boolean
showDuration?: boolean
error?: GardenError
status?: EntryStatus
indent?: number
childEntriesInheritLevel?: boolean
}

export interface MessageState extends MessageBase {
timestamp: number,
}

export interface UpdateLogEntryParams extends MessageBase {
metadata?: LogEntryMetadata
}

export interface CreateOpts extends UpdateOpts {
export interface LogEntryParams extends UpdateLogEntryParams {
error?: GardenError
data?: any // to be rendered as e.g. YAML or JSON
indent?: number
childEntriesInheritLevel?: boolean
fromStdStream?: boolean
id?: string
}

export type CreateParam = string | CreateOpts

export interface LogEntryConstructor {
export interface LogEntryConstructor extends LogEntryParams {
level: LogLevel
opts: CreateOpts
root: Logger
parent?: LogEntry
isPlaceholder?: boolean
}

// TODO Fix any cast
export function resolveParam<T extends UpdateOpts>(param?: string | T): T {
return typeof param === "string" ? <any>{ msg: param } : param || {}
function resolveParams(params?: string | UpdateLogEntryParams): UpdateLogEntryParams {
if (!params) {
return {}
} else if (typeof params === "string") {
return { msg: params }
} else {
return params
}
}

export class LogEntry extends LogNode {
public opts: UpdateOpts
public root: Logger

constructor({ level, opts, root, parent }: LogEntryConstructor) {
const { id, ...otherOpts } = opts
super(level, parent, id)
this.root = root
this.opts = otherOpts
if (this.level === LogLevel.error) {
this.opts.status = "error"
private messageStates?: MessageState[]
private metadata?: LogEntryMetadata
public readonly root: Logger
public readonly data?: any
public readonly fromStdStream?: boolean
public readonly indent?: number
public readonly errorData?: GardenError
public readonly childEntriesInheritLevel?: boolean
public readonly id?: string

constructor(params: LogEntryConstructor) {
super(params.level, params.parent, params.id)

this.root = params.root
this.data = params.data
this.fromStdStream = params.fromStdStream
this.indent = params.indent
this.errorData = params.error
this.childEntriesInheritLevel = params.childEntriesInheritLevel
this.metadata = params.metadata
this.id = params.id

if (!params.isPlaceholder) {
this.update({
msg: params.msg,
emoji: params.emoji,
section: params.section,
symbol: params.symbol,
status: params.level === LogLevel.error ? "error" : params.status,
})
}
}

private setOwnState(nextOpts: UpdateOpts): void {
let msg: string | string[] | undefined
const { append, msg: nextMsg } = nextOpts
const prevMsg = this.opts.msg
if (prevMsg !== undefined && nextMsg && append) {
msg = flatten([...[prevMsg], ...[nextMsg]])
} else if (nextMsg) {
msg = nextMsg
} else {
msg = prevMsg
/**
* Updates the log entry with a few invariants:
* 1. msg, emoji, section, status, and symbol can only be replaced with a value of same type, not removed
* 2. append is always set explicitly (the next message state does not inherit the previous value)
* 3. next metadata is merged with the previous metadata
*/
protected update(updateParams: UpdateLogEntryParams): void {
const messageState = this.getMessageState()

// Explicitly set all the fields so the shape stays consistent
const nextMessageState: MessageState = {
// Ensure empty string gets set
msg: typeof updateParams.msg === "string" ? updateParams.msg : messageState.msg,
emoji: updateParams.emoji || messageState.emoji,
section: updateParams.section || messageState.section,
status: updateParams.status || messageState.status,
symbol: updateParams.symbol || messageState.symbol,
// Next state does not inherit the append field
append: updateParams.append,
timestamp: Date.now(),
}

// Hack to preserve section alignment if symbols or spinners disappear
const hadSymbolOrSpinner = this.opts.symbol || this.opts.status === "active"
const hasSymbolOrSpinner = nextOpts.symbol || nextOpts.status === "active"
if (this.opts.section && hadSymbolOrSpinner && !hasSymbolOrSpinner) {
nextOpts.symbol = "empty"
// Hack to preserve section alignment if spinner disappears
const hadSpinner = messageState.status === "active"
const hasSymbolOrSpinner = nextMessageState.symbol || nextMessageState.status === "active"
if (nextMessageState.section && hadSpinner && !hasSymbolOrSpinner) {
nextMessageState.symbol = "empty"
}

this.opts = { ...this.opts, ...nextOpts, msg }
this.messageStates = [...this.messageStates || [], nextMessageState]

if (updateParams.metadata) {
this.metadata = { ...this.metadata || {}, ...updateParams.metadata }
}
}

// Update node and child nodes
private deepSetState(opts: UpdateOpts): void {
const wasActive = this.opts.status === "active"
private deepUpdate(updateParams: UpdateLogEntryParams): void {
const wasActive = this.getMessageState().status === "active"

this.setOwnState(opts)
this.update(updateParams)

// Stop active child nodes if parent is no longer active
if (wasActive && this.opts.status !== "active") {
// Stop active child nodes if no longer active
if (wasActive && updateParams.status !== "active") {
getChildEntries(this).forEach(entry => {
if (entry.opts.status === "active") {
entry.setOwnState({ status: "done" })
if (entry.getMessageState().status === "active") {
entry.update({ status: "done" })
}
})
}
}

protected createNode(level: LogLevel, param: CreateParam) {
const opts = {
indent: (this.opts.indent || 0) + 1,
...resolveParam(param),
}
protected createNode(params: CreateNodeParams) {
const indent = params.indent !== undefined ? params.indent : (this.indent || 0) + 1

// If childEntriesInheritLevel is set to true, all children must have a level geq the level
// If childEntriesInheritLevel is set to true, all children must have a level geq to the level
// of the parent entry that set the flag.
const parentWithPreserveFlag = findParentEntry(this, entry => !!entry.opts.childEntriesInheritLevel)
const childLevel = parentWithPreserveFlag ? Math.max(parentWithPreserveFlag.level, level) : level
const parentWithPreserveFlag = findParentEntry(this, entry => !!entry.childEntriesInheritLevel)
const level = parentWithPreserveFlag ? Math.max(parentWithPreserveFlag.level, params.level) : params.level

return new LogEntry({
opts,
level: childLevel,
...params,
indent,
level,
root: this.root,
parent: this,
})
Expand All @@ -141,70 +181,77 @@ export class LogEntry extends LogNode {
this.root.onGraphChange(node)
}

placeholder(level: LogLevel = LogLevel.info, param?: CreateParam): LogEntry {
getMetadata() {
return this.metadata
}

getMessageStates() {
return this.messageStates
}

/**
* Returns a deep copy of the latest message state, if availble.
* Otherwise return an empty object of type MessageState for convenience.
*/
getMessageState() {
if (!this.messageStates) {
return <MessageState>{}
}

// Use spread operator to clone the array
const msgState = [...this.messageStates][this.messageStates.length - 1]
// ...and the object itself
return { ...msgState }
}

placeholder(level: LogLevel = LogLevel.info, childEntriesInheritLevel = false): LogEntry {
// Ensure placeholder child entries align with parent context
const indent = Math.max((this.opts.indent || 0) - 1, - 1)
return this.appendNode(level, { ...resolveParam(param), indent })
const indent = Math.max((this.indent || 0) - 1, - 1)
return this.addNode({ level, indent, childEntriesInheritLevel, isPlaceholder: true })
}

// Preserves status
setState(param?: string | UpdateOpts): LogEntry {
this.deepSetState({ ...resolveParam(param), status: this.opts.status })
setState(params?: string | UpdateLogEntryParams): LogEntry {
this.deepUpdate({ ...resolveParams(params) })
this.root.onGraphChange(this)
return this
}

setDone(param?: string | Omit<UpdateOpts, "status">): LogEntry {
this.deepSetState({ ...resolveParam(param), status: "done" })
setDone(params?: string | Omit<UpdateLogEntryParams, "status">): LogEntry {
this.deepUpdate({ ...resolveParams(params), status: "done" })
this.root.onGraphChange(this)
return this
}

setSuccess(param?: string | Omit<UpdateOpts, "status" & "symbol">): LogEntry {
this.deepSetState({ ...resolveParam(param), symbol: "success", status: "success" })
setSuccess(params?: string | Omit<UpdateLogEntryParams, "status" & "symbol">): LogEntry {
this.deepUpdate({ ...resolveParams(params), symbol: "success", status: "success" })
this.root.onGraphChange(this)
return this
}

setError(param?: string | Omit<UpdateOpts, "status" & "symbol">): LogEntry {
this.deepSetState({ ...resolveParam(param), symbol: "error", status: "error" })
setError(params?: string | Omit<UpdateLogEntryParams, "status" & "symbol">): LogEntry {
this.deepUpdate({ ...resolveParams(params), symbol: "error", status: "error" })
this.root.onGraphChange(this)
return this
}

setWarn(param?: string | Omit<UpdateOpts, "status" & "symbol">): LogEntry {
this.deepSetState({ ...resolveParam(param), symbol: "warning", status: "warn" })
setWarn(param?: string | Omit<UpdateLogEntryParams, "status" & "symbol">): LogEntry {
this.deepUpdate({ ...resolveParams(param), symbol: "warning", status: "warn" })
this.root.onGraphChange(this)
return this
}

fromStdStream(): boolean {
return !!this.opts.fromStdStream
}

stopAll() {
return this.root.stop()
}

stop() {
// Stop gracefully if still in active state
if (this.opts.status === "active") {
this.setOwnState({ symbol: "empty", status: "done" })
if (this.getMessageState().status === "active") {
this.update({ symbol: "empty", status: "done" })
this.root.onGraphChange(this)
}
return this
}

inspect() {
console.log(JSON.stringify(sanitizeObject({
...this.opts,
level: this.level,
children: this.children,
})))
}

filterBySection(section: string): LogEntry[] {
return getChildEntries(this).filter(entry => entry.opts.section === section)
}

}
Loading

0 comments on commit b68f343

Please sign in to comment.