diff --git a/src/bin/garden.ts b/src/bin/garden.ts index c8e36cbac1..58f6de6c5f 100755 --- a/src/bin/garden.ts +++ b/src/bin/garden.ts @@ -7,6 +7,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { run } from "../cli" +import { run } from "../cli/cli" run() diff --git a/src/cli.ts b/src/cli/cli.ts similarity index 53% rename from src/cli.ts rename to src/cli/cli.ts index ac3b23042d..1d3762d631 100644 --- a/src/cli.ts +++ b/src/cli/cli.ts @@ -6,53 +6,68 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { safeDump } from "js-yaml" import * as sywac from "sywac" -import chalk from "chalk" -import { RunCommand } from "./commands/run" -import { ScanCommand } from "./commands/scan" -import { DeepPrimitiveMap } from "./types/common" +import { difference, merge, intersection } from "lodash" +import { resolve } from "path" +import { safeDump } from "js-yaml" +import stringify = require("json-stringify-safe") + +import { DeepPrimitiveMap } from "../types/common" import { enumToArray, shutdown, sleep, -} from "./util" -import { difference, flatten, merge, intersection, reduce } from "lodash" +} from "../util" import { BooleanParameter, Command, ChoicesParameter, - ParameterValues, Parameter, StringParameter, EnvironmentOption, CommandResult, -} from "./commands/base" -import { ValidateCommand } from "./commands/validate" +} from "../commands/base" import { GardenError, - InternalError, PluginError, toGardenError, -} from "./exceptions" -import { Garden } from "./garden" -import { FileWriter } from "./logger/writers/file-writer" -import { getLogger, RootLogNode } from "./logger" -import { resolve } from "path" -import { BuildCommand } from "./commands/build" -import { EnvironmentCommand } from "./commands/environment/index" -import { DeployCommand } from "./commands/deploy" -import { CallCommand } from "./commands/call" -import { TestCommand } from "./commands/test" -import { DevCommand } from "./commands/dev" -import { LogsCommand } from "./commands/logs" -import { LogLevel } from "./logger/types" -import { ConfigCommand } from "./commands/config" -import { StatusCommand } from "./commands/status" -import { PushCommand } from "./commands/push" -import { LoginCommand } from "./commands/login" -import { LogoutCommand } from "./commands/logout" -import stringify = require("json-stringify-safe") +} from "../exceptions" +import { Garden } from "../garden" + +import { BuildCommand } from "../commands/build" +import { CallCommand } from "../commands/call" +import { ConfigCommand } from "../commands/config" +import { DeployCommand } from "../commands/deploy" +import { DevCommand } from "../commands/dev" +import { EnvironmentCommand } from "../commands/environment/index" +import { PushCommand } from "../commands/push" +import { LoginCommand } from "../commands/login" +import { LogoutCommand } from "../commands/logout" +import { LogsCommand } from "../commands/logs" +import { RunCommand } from "../commands/run" +import { ScanCommand } from "../commands/scan" +import { StatusCommand } from "../commands/status" +import { TestCommand } from "../commands/test" +import { ValidateCommand } from "../commands/validate" + +import { RootLogNode, getLogger } from "../logger" +import { LogLevel, LoggerType } from "../logger/types" +import { BasicTerminalWriter } from "../logger/writers/basic-terminal-writer" +import { FancyTerminalWriter } from "../logger/writers/fancy-terminal-writer" +import { FileWriter } from "../logger/writers/file-writer" +import { Writer } from "../logger/writers/base" + +import { + falsifyConflictingParams, + getAliases, + getArgSynopsis, + getKeys, + getOptionSynopsis, + filterByArray, + prepareArgConfig, + prepareOptionConfig, + styleConfig, +} from "./helpers" const OUTPUT_RENDERERS = { json: (data: DeepPrimitiveMap) => { @@ -88,59 +103,8 @@ const GLOBAL_OPTIONS = { }), } const GLOBAL_OPTIONS_GROUP_NAME = "Global options" -const STYLE_CONFIG = { - usagePrefix: str => ( - ` -${chalk.bold(str.slice(0, 5).toUpperCase())} - - ${chalk.italic(str.slice(7))}` - ), - usageCommandPlaceholder: str => chalk.blue(str), - usagePositionals: str => chalk.magenta(str), - usageArgsPlaceholder: str => chalk.magenta(str), - usageOptionsPlaceholder: str => chalk.yellow(str), - group: (str: string) => { - const cleaned = str.endsWith(":") ? str.slice(0, -1) : str - return chalk.bold(cleaned.toUpperCase()) + "\n" - }, - flags: (str, _type) => { - const style = str.startsWith("-") ? chalk.green : chalk.magenta - return style(str) - }, - hints: str => chalk.gray(str), - groupError: str => chalk.red.bold(str), - flagsError: str => chalk.red.bold(str), - descError: str => chalk.yellow.bold(str), - hintsError: str => chalk.red(str), - messages: str => chalk.red.bold(str), // these are error messages -} const ERROR_LOG_FILENAME = "error.log" - -// Helper functions -const getKeys = (obj): string[] => Object.keys(obj || {}) -const filterByArray = (obj: any, arr: string[]): any => { - return arr.reduce((memo, key) => { - if (obj[key]) { - memo[key] = obj[key] - } - return memo - }, {}) -} - -// Parameter types T which map between the Parameter class and the Sywac cli library -// In case we add types that aren't supported natively by Sywac, see: http://sywac.io/docs/sync-config.html#custom -const VALID_PARAMETER_TYPES = ["boolean", "number", "choice", "string"] - -type FalsifiedParams = { [key: string]: false } - -interface OptConfig { - desc: string | string[] - type: string - defaultValue?: any - choices?: any[] - required?: boolean - strict: true -} +const DEFAULT_CLI_LOGGER_TYPE = LoggerType.fancy export interface ParseResults { argv: any @@ -150,90 +114,15 @@ export interface ParseResults { interface SywacParseResults extends ParseResults { output: string - details: { result?: CommandResult } -} - -function makeOptSynopsis(key: string, param: Parameter): string { - return param.alias ? `-${param.alias}, --${key}` : `--${key}` -} - -function makeArgSynopsis(key: string, param: Parameter) { - return param.required ? `<${key}>` : `[${key}]` -} - -function makeArgConfig(param: Parameter) { - return { - desc: param.help, - params: [makeOptConfig(param)], - } -} - -function makeOptConfig(param: Parameter): OptConfig { - const { - defaultValue, - help: desc, - required, - type, - } = param - if (!VALID_PARAMETER_TYPES.includes(type)) { - throw new InternalError(`Invalid parameter type for cli: ${type}`, { - type, - validParameterTypes: VALID_PARAMETER_TYPES, - }) - } - let config: OptConfig = { - defaultValue, - desc, - required, - type, - strict: true, - } - if (type === "choice") { - config.type = "enum" - config.choices = (param).choices - } - return config -} - -function getAliases(params: ParameterValues) { - return flatten(Object.entries(params) - .map(([key, param]) => param.alias ? [key, param.alias] : [key])) -} - -/** - * Returns the params that need to be overridden set to false - */ -function falsifyConflictingParams(argv, params: ParameterValues): FalsifiedParams { - return reduce(argv, (acc: {}, val: any, key: string) => { - const param = params[key] - const overrides = (param || {}).overrides || [] - // argv always contains the "_" key which is irrelevant here - if (key === "_" || !param || !val || !(overrides.length > 0)) { - return acc - } - const withAliases = overrides.reduce((_, keyToOverride: string): string[] => { - if (!params[keyToOverride]) { - throw new InternalError(`Cannot override non-existing parameter: ${keyToOverride}`, { - keyToOverride, - availableKeys: Object.keys(params), - }) - } - return [keyToOverride, ...params[keyToOverride].alias] - }, []) - - withAliases.forEach(keyToOverride => acc[keyToOverride] = false) - return acc - }, {}) + details: { logger: RootLogNode, result?: CommandResult } } export class GardenCli { program: any commands: { [key: string]: Command } = {} - logger: RootLogNode constructor() { - const version = require("../package.json").version - this.logger = getLogger() + const version = require("../../package.json").version this.program = sywac .help("-h, --help", { group: GLOBAL_OPTIONS_GROUP_NAME, @@ -249,7 +138,7 @@ export class GardenCli { // NOTE: Need to mutate argv! merge(argv, falsifyConflictingParams(argv, GLOBAL_OPTIONS)) }) - .style(STYLE_CONFIG) + .style(styleConfig) const commands = [ new BuildCommand(), @@ -276,8 +165,8 @@ export class GardenCli { } addGlobalOption(key: string, option: Parameter): void { - this.program.option(makeOptSynopsis(key, option), { - ...makeOptConfig(option), + this.program.option(getOptionSynopsis(key, option), { + ...prepareOptionConfig(option), group: GLOBAL_OPTIONS_GROUP_NAME, }) } @@ -292,9 +181,13 @@ export class GardenCli { this.commands[fullName] = command - const args = >command.arguments || {} - const options = >command.options || {} - const subCommands = command.subCommands || [] + const { + arguments: args = {}, + loggerType = DEFAULT_CLI_LOGGER_TYPE, + options = {}, + subCommands = [], + } = command + const argKeys = getKeys(args) const optKeys = getKeys(options) const globalKeys = getKeys(GLOBAL_OPTIONS) @@ -306,10 +199,10 @@ export class GardenCli { const action = async (argv, cliContext) => { // Sywac returns positional args and options in a single object which we separate into args and opts - const argsForAction = filterByArray(argv, argKeys) - const optsForAction = filterByArray(argv, optKeys.concat(globalKeys)) - const root = resolve(process.cwd(), optsForAction.root) - const env = optsForAction.env + const parsedArgs = filterByArray(argv, argKeys) + const parsedOpts = filterByArray(argv, optKeys.concat(globalKeys)) + const root = resolve(process.cwd(), parsedOpts.root) + const { env, loglevel, silent, output } = parsedOpts // Validate options (feels like the parser should handle this) const builtinOptions = ["help", "h", "version", "v"] @@ -324,13 +217,18 @@ export class GardenCli { return } - // Configure logger - const logger = this.logger - const { loglevel, silent, output } = optsForAction + // Init logger const level = LogLevel[loglevel] - logger.level = level - if (!silent && !output) { - logger.writers.push( + let writers: Writer[] = [] + + if (!silent && !output && loggerType !== LoggerType.quiet) { + if (loggerType === LoggerType.fancy) { + writers.push(new FancyTerminalWriter()) + } else if (loggerType === LoggerType.basic) { + writers.push(new BasicTerminalWriter()) + } + + writers.push( await FileWriter.factory({ root, level, @@ -349,14 +247,13 @@ export class GardenCli { truncatePrevious: true, }), ) - } else { - logger.writers = [] } + const logger = RootLogNode.initialize({ level, writers }) const garden = await Garden.factory(root, { env, logger }) // TODO: enforce that commands always output DeepPrimitiveMap - const result = await command.action(garden.pluginContext, argsForAction, optsForAction) + const result = await command.action(garden.pluginContext, parsedArgs, parsedOpts) // We attach the action result to cli context so that we can process it in the parse method cliContext.details.result = result @@ -365,8 +262,8 @@ export class GardenCli { // Command specific positional args and options are set inside the builder function const setup = parser => { subCommands.forEach(subCommandCls => this.addCommand(new subCommandCls(command), parser)) - argKeys.forEach(key => parser.positional(makeArgSynopsis(key, args[key]), makeArgConfig(args[key]))) - optKeys.forEach(key => parser.option(makeOptSynopsis(key, options[key]), makeOptConfig(options[key]))) + argKeys.forEach(key => parser.positional(getArgSynopsis(key, args[key]), prepareArgConfig(args[key]))) + optKeys.forEach(key => parser.option(getOptionSynopsis(key, options[key]), prepareOptionConfig(options[key]))) } const commandOpts = { @@ -382,14 +279,24 @@ export class GardenCli { async parse(): Promise { const parseResult: SywacParseResults = await this.program.parse() const { argv, details, errors, output: cliOutput } = parseResult - const commandResult = details.result + const { result: commandResult } = details const { output } = argv - - let code = parseResult.code + let { code } = parseResult + let logger + + // Logger might not have been initialised if process exits early + try { + logger = getLogger() + } catch (_) { + logger = RootLogNode.initialize({ + level: LogLevel.info, + writers: [new BasicTerminalWriter()], + }) + } // --help or --version options were called so we log the cli output and exit if (cliOutput && errors.length < 1) { - this.logger.stop() + logger.stop() console.log(cliOutput) process.exit(parseResult.code) } @@ -411,19 +318,19 @@ export class GardenCli { } if (gardenErrors.length > 0) { - gardenErrors.forEach(error => this.logger.error({ + gardenErrors.forEach(error => logger.error({ msg: error.message, error, })) - if (this.logger.writers.find(w => w instanceof FileWriter)) { - this.logger.info(`\nSee ${ERROR_LOG_FILENAME} for detailed error message`) + if (logger.writers.find(w => w instanceof FileWriter)) { + logger.info(`\nSee ${ERROR_LOG_FILENAME} for detailed error message`) } code = 1 } - this.logger.stop() + logger.stop() return { argv, code, errors } } diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts new file mode 100644 index 0000000000..9ec346817d --- /dev/null +++ b/src/cli/helpers.ts @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 chalk from "chalk" +import { flatten, reduce } from "lodash" +import { + ChoicesParameter, + ParameterValues, + Parameter, +} from "../commands/base" +import { + InternalError, +} from "../exceptions" + +// Parameter types T which map between the Parameter class and the Sywac cli library. +// In case we add types that aren't supported natively by Sywac, see: http://sywac.io/docs/sync-config.html#custom +const VALID_PARAMETER_TYPES = ["boolean", "number", "choice", "string"] + +export const styleConfig = { + usagePrefix: str => ( + ` +${chalk.bold(str.slice(0, 5).toUpperCase())} + + ${chalk.italic(str.slice(7))}` + ), + usageCommandPlaceholder: str => chalk.blue(str), + usagePositionals: str => chalk.magenta(str), + usageArgsPlaceholder: str => chalk.magenta(str), + usageOptionsPlaceholder: str => chalk.yellow(str), + group: (str: string) => { + const cleaned = str.endsWith(":") ? str.slice(0, -1) : str + return chalk.bold(cleaned.toUpperCase()) + "\n" + }, + flags: (str, _type) => { + const style = str.startsWith("-") ? chalk.green : chalk.magenta + return style(str) + }, + hints: str => chalk.gray(str), + groupError: str => chalk.red.bold(str), + flagsError: str => chalk.red.bold(str), + descError: str => chalk.yellow.bold(str), + hintsError: str => chalk.red(str), + messages: str => chalk.red.bold(str), // these are error messages +} + +// Helper functions +export const getKeys = (obj): string[] => Object.keys(obj || {}) +export const filterByArray = (obj: any, arr: string[]): any => { + return arr.reduce((memo, key) => { + if (obj[key]) { + memo[key] = obj[key] + } + return memo + }, {}) +} + +export function getAliases(params: ParameterValues) { + return flatten(Object.entries(params) + .map(([key, param]) => param.alias ? [key, param.alias] : [key])) +} + +export type FalsifiedParams = { [key: string]: false } + +/** + * Returns the params that need to be overridden set to false + */ +export function falsifyConflictingParams(argv, params: ParameterValues): FalsifiedParams { + return reduce(argv, (acc: {}, val: any, key: string) => { + const param = params[key] + const overrides = (param || {}).overrides || [] + // argv always contains the "_" key which is irrelevant here + if (key === "_" || !param || !val || !(overrides.length > 0)) { + return acc + } + const withAliases = overrides.reduce((_, keyToOverride: string): string[] => { + if (!params[keyToOverride]) { + throw new InternalError(`Cannot override non-existing parameter: ${keyToOverride}`, { + keyToOverride, + availableKeys: Object.keys(params), + }) + } + return [keyToOverride, ...params[keyToOverride].alias] + }, []) + + withAliases.forEach(keyToOverride => acc[keyToOverride] = false) + return acc + }, {}) +} + +// Sywac transformers +export function getOptionSynopsis(key: string, param: Parameter): string { + return param.alias ? `-${param.alias}, --${key}` : `--${key}` +} + +export function getArgSynopsis(key: string, param: Parameter) { + return param.required ? `<${key}>` : `[${key}]` +} + +export function prepareArgConfig(param: Parameter) { + return { + desc: param.help, + params: [prepareOptionConfig(param)], + } +} + +export interface SywacOptionConfig { + desc: string | string[] + type: string + defaultValue?: any + choices?: any[] + required?: boolean + strict: true +} + +export function prepareOptionConfig(param: Parameter): SywacOptionConfig { + const { + defaultValue, + help: desc, + required, + type, + } = param + if (!VALID_PARAMETER_TYPES.includes(type)) { + throw new InternalError(`Invalid parameter type for cli: ${type}`, { + type, + validParameterTypes: VALID_PARAMETER_TYPES, + }) + } + let config: SywacOptionConfig = { + defaultValue, + desc, + required, + type, + strict: true, + } + if (type === "choice") { + config.type = "enum" + config.choices = (param).choices + } + return config +} diff --git a/src/commands/base.ts b/src/commands/base.ts index 3cf1bafdb4..52e84252d4 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -12,6 +12,7 @@ import { } from "../exceptions" import { PluginContext } from "../plugin-context" import { TaskResults } from "../task-graph" +import { LoggerType } from "../logger/types" export class ValidationError extends Error { } @@ -135,6 +136,7 @@ export abstract class Command { arguments = logsArgs options = logsOpts + loggerType = LoggerType.basic async action(ctx: PluginContext, args: Args, opts: Opts): Promise> { const names = args.service ? args.service.split(",") : undefined diff --git a/src/logger/index.ts b/src/logger/index.ts index 74dd0d2a0f..c5c1685773 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -25,24 +25,28 @@ import { LogEntryOpts, LogSymbolType, } from "./types" +import { Writer } from "./writers/base" +import { ParameterError, InternalError } from "../exceptions" import { BasicTerminalWriter } from "./writers/basic-terminal-writer" import { FancyTerminalWriter } from "./writers/fancy-terminal-writer" -import { Writer } from "./writers/base" -import { ParameterError } from "../exceptions" const ROOT_DEPTH = -1 -const CONFIG_TYPES: { [key in LoggerType]: LoggerConfig } = { - [LoggerType.fancy]: { - level: LogLevel.info, - writers: [new FancyTerminalWriter()], - }, - [LoggerType.basic]: { - level: LogLevel.info, - writers: [new BasicTerminalWriter()], - }, - [LoggerType.quiet]: { - level: LogLevel.info, - }, + +function getCommonConfig(loggerType: LoggerType): LoggerConfig { + const configs: { [key in LoggerType]: LoggerConfig } = { + [LoggerType.fancy]: { + level: LogLevel.info, + writers: [new FancyTerminalWriter()], + }, + [LoggerType.basic]: { + level: LogLevel.info, + writers: [new BasicTerminalWriter()], + }, + [LoggerType.quiet]: { + level: LogLevel.info, + }, + } + return configs[loggerType] } export interface LoggerConfig { @@ -59,10 +63,6 @@ export interface LogEntryConstructor { parentEntry?: LogEntry } -let loggerInstance: RootLogNode -let loggerType: LoggerType = LoggerType.fancy -let loggerConfig: LoggerConfig = CONFIG_TYPES[loggerType] - function createLogEntry(level: LogLevel, opts: LogEntryOpts, parentNode: LogNode): LogEntry { const { depth, root } = parentNode const key = uniqid() @@ -86,16 +86,16 @@ export type CreateLogEntry = (entryVal: CreateLogEntryParam) => LogEntry export type UpdateLogEntryParam = string | LogEntryOpts | undefined export type UpdateLogEntry = (entryVal?: UpdateLogEntryParam) => LogEntry -function makeLogOpts(entryVal: CreateLogEntryParam | UpdateLogEntryParam): LogEntryOpts { +function prepareLogOpts(entryVal: CreateLogEntryParam | UpdateLogEntryParam): LogEntryOpts { return typeof entryVal === "string" ? { msg: entryVal } : entryVal || {} } export abstract class LogNode { - public root: RootLogNode - public timestamp: number - public level: LogLevel - public depth: number - public children: LogEntry[] + public readonly root: RootLogNode + public readonly timestamp: number + public readonly level: LogLevel + public readonly depth: number + public readonly children: LogEntry[] constructor(level: LogLevel, depth: number) { this.timestamp = Date.now() @@ -112,27 +112,27 @@ export abstract class LogNode { } public silly: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.silly, makeLogOpts(entryVal)) + return this.addNode(LogLevel.silly, prepareLogOpts(entryVal)) } public debug: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.debug, makeLogOpts(entryVal)) + return this.addNode(LogLevel.debug, prepareLogOpts(entryVal)) } public verbose: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.verbose, makeLogOpts(entryVal)) + return this.addNode(LogLevel.verbose, prepareLogOpts(entryVal)) } public info: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.info, makeLogOpts(entryVal)) + return this.addNode(LogLevel.info, prepareLogOpts(entryVal)) } public warn: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.warn, makeLogOpts(entryVal)) + return this.addNode(LogLevel.warn, prepareLogOpts(entryVal)) } public error: CreateLogEntry = (entryVal: CreateLogEntryParam): LogEntry => { - return this.addNode(LogLevel.error, { ...makeLogOpts(entryVal), entryStyle: EntryStyle.error }) + return this.addNode(LogLevel.error, { ...prepareLogOpts(entryVal), entryStyle: EntryStyle.error }) } public findById(id: string): LogEntry | void { @@ -148,13 +148,13 @@ export abstract class LogNode { export class LogEntry extends LogNode { public opts: LogEntryOpts public status: EntryStatus - public root: RootLogNode - public timestamp: number - public level: LogLevel - public depth: number - public key: string - public parentEntry: LogEntry | undefined - public children: LogEntry[] + public readonly root: RootLogNode + public readonly timestamp: number + public readonly level: LogLevel + public readonly depth: number + public readonly key: string + public readonly parentEntry: LogEntry | undefined + public readonly children: LogEntry[] constructor({ level, opts, depth, root, parentEntry, key }: LogEntryConstructor) { super(level, depth) @@ -207,31 +207,31 @@ export class LogEntry extends LogNode { // Preserves status public setState: UpdateLogEntry = (entryVal: UpdateLogEntryParam = {}): LogEntry => { - this.deepSetState(makeLogOpts(entryVal), this.status) + this.deepSetState(prepareLogOpts(entryVal), this.status) this.root.onGraphChange(this) return this } public setDone: UpdateLogEntry = (entryVal: UpdateLogEntryParam = {}): LogEntry => { - this.deepSetState(makeLogOpts(entryVal), EntryStatus.DONE) + this.deepSetState(prepareLogOpts(entryVal), EntryStatus.DONE) this.root.onGraphChange(this) return this } public setSuccess: UpdateLogEntry = (entryVal: UpdateLogEntryParam = {}): LogEntry => { - this.deepSetState({ ...makeLogOpts(entryVal), symbol: LogSymbolType.success }, EntryStatus.SUCCESS) + this.deepSetState({ ...prepareLogOpts(entryVal), symbol: LogSymbolType.success }, EntryStatus.SUCCESS) this.root.onGraphChange(this) return this } public setError: UpdateLogEntry = (entryVal: UpdateLogEntryParam = {}): LogEntry => { - this.deepSetState({ ...makeLogOpts(entryVal), symbol: LogSymbolType.error }, EntryStatus.ERROR) + this.deepSetState({ ...prepareLogOpts(entryVal), symbol: LogSymbolType.error }, EntryStatus.ERROR) this.root.onGraphChange(this) return this } public setWarn: UpdateLogEntry = (entryVal: UpdateLogEntryParam = {}): LogEntry => { - this.deepSetState({ ...makeLogOpts(entryVal), symbol: LogSymbolType.warn }, EntryStatus.WARN) + this.deepSetState({ ...prepareLogOpts(entryVal), symbol: LogSymbolType.warn }, EntryStatus.WARN) this.root.onGraphChange(this) return this } @@ -260,10 +260,47 @@ export class LogEntry extends LogNode { } export class RootLogNode extends LogNode { - public root: RootLogNode - public writers: Writer[] + public readonly root: RootLogNode + public readonly writers: Writer[] + + private static instance: RootLogNode + + static getInstance() { + if (!RootLogNode.instance) { + throw new InternalError("Logger not initialized", {}) + } + return RootLogNode.instance + } + + static initialize(config: LoggerConfig) { + if (RootLogNode.instance) { + throw new InternalError("Logger already initialized", {}) + } + + let instance + + // If GARDEN_LOGGER_TYPE env variable is set it takes precedence over the config param + if (process.env.GARDEN_LOGGER_TYPE) { + const loggerType = LoggerType[process.env.GARDEN_LOGGER_TYPE] + + if (!loggerType) { + throw new ParameterError(`Invalid logger type specified: ${process.env.GARDEN_LOGGER_TYPE}`, { + loggerType: process.env.GARDEN_LOGGER_TYPE, + availableTypes: Object.keys(LoggerType), + }) + } - constructor(config: LoggerConfig) { + instance = new RootLogNode(getCommonConfig(loggerType)) + instance.debug(`Setting logger type to ${loggerType} (from GARDEN_LOGGER_TYPE)`) + } else { + instance = new RootLogNode(config) + } + + RootLogNode.instance = instance + return instance + } + + private constructor(config: LoggerConfig) { super(config.level, ROOT_DEPTH) this.root = this this.writers = config.writers || [] @@ -309,31 +346,5 @@ export class RootLogNode extends LogNode { } export function getLogger() { - if (!loggerInstance) { - loggerInstance = new RootLogNode(loggerConfig) - } - - return loggerInstance -} - -export function setLoggerType(type: LoggerType) { - loggerType = type - loggerConfig = CONFIG_TYPES[type] -} - -// allow configuring logger type via environment variable -// TODO: we may want a more generalized mechanism for these types of env flags -if (process.env.GARDEN_LOGGER_TYPE) { - const type = LoggerType[process.env.GARDEN_LOGGER_TYPE] - - if (!type) { - throw new ParameterError(`Invalid logger type specified: ${process.env.GARDEN_LOGGER_TYPE}`, { - loggerType: process.env.GARDEN_LOGGER_TYPE, - availableTypes: Object.keys(LoggerType), - }) - } - - setLoggerType(type) - - getLogger().debug(`Setting logger type to ${type} (from GARDEN_LOGGER_TYPE)`) + return RootLogNode.getInstance() } diff --git a/src/logger/types.ts b/src/logger/types.ts index 0b682ef928..40e884fdfe 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -22,9 +22,9 @@ export enum LogLevel { } export enum LoggerType { + quiet = "quiet", basic = "basic", fancy = "fancy", - quiet = "quiet", } export enum EntryStyle { diff --git a/test/setup.ts b/test/setup.ts index f5f7ea92cb..d3884d25ae 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,13 +1,9 @@ import * as td from "testdouble" -import { LoggerType } from "../src/logger/types" -import { setLoggerType } from "../src/logger" +import { LogLevel } from "../src/logger/types" +import { RootLogNode } from "../src/logger" import { Module } from "../src/types/module" // Global before hooks -before(() => { - setLoggerType(LoggerType.quiet) -}) - beforeEach(() => { td.replace(Module.prototype, "getVersion", () => ({ versionString: "0000000000", diff --git a/test/src/logger.ts b/test/src/logger.ts index 62052a9ab3..0fea6d8a01 100644 --- a/test/src/logger.ts +++ b/test/src/logger.ts @@ -4,13 +4,26 @@ import { expect } from "chai" import { LogLevel, EntryStatus, LogSymbolType, EntryStyle } from "../../src/logger/types" import { BasicTerminalWriter } from "../../src/logger/writers/basic-terminal-writer" import { FancyTerminalWriter } from "../../src/logger/writers/fancy-terminal-writer" -import { RootLogNode } from "../../src/logger" +import { RootLogNode, getLogger } from "../../src/logger" import { getChildNodes } from "../../src/logger/util" +let logger +// A fail-safe in case the logger has already been initialised (it's a singleton +// class that can only be initialised once) +try { + logger = RootLogNode.initialize({ level: LogLevel.info }) +} catch (_) { + logger = getLogger() +} + +beforeEach(() => { + logger["children"] = [] +}) + describe("LogNode", () => { + describe("findById", () => { it("should return the first log entry with a matching id and undefined otherwise", () => { - const logger = new RootLogNode({ level: LogLevel.info }) logger.info({msg: "0"}) logger.info({msg: "a1", id: "a"}) logger.info({msg: "a2", id: "a"}) @@ -21,7 +34,6 @@ describe("LogNode", () => { describe("filterBySection", () => { it("should return an array of all entries with the matching section name", () => { - const logger = new RootLogNode({ level: LogLevel.info }) logger.info({section: "s0"}) logger.info({section: "s1", id: "a"}) logger.info({section: "s2"}) @@ -36,17 +48,15 @@ describe("LogNode", () => { }) describe("RootLogNode", () => { - const logger = new RootLogNode({ level: LogLevel.info }) - - logger.error("error") - logger.warn("warn") - logger.info("info") - logger.verbose("verbose") - logger.debug("debug") - logger.silly("silly") - describe("getLogEntries", () => { it("should return an ordered list of log entries", () => { + logger.error("error") + logger.warn("warn") + logger.info("info") + logger.verbose("verbose") + logger.debug("debug") + logger.silly("silly") + const entries = logger.getLogEntries() const levels = entries.map(e => e.level) @@ -64,6 +74,13 @@ describe("RootLogNode", () => { describe("addNode", () => { it("should add new child entries to the respective node", () => { + logger.error("error") + logger.warn("warn") + logger.info("info") + logger.verbose("verbose") + logger.debug("debug") + logger.silly("silly") + const prevLength = logger.children.length const entry = logger.children[0] const nested = entry.info("nested") @@ -81,28 +98,24 @@ describe("RootLogNode", () => { describe("BasicTerminalWriter.render", () => { it("should return a string if level is geq than entry level and entry contains a message", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const writer = new BasicTerminalWriter() const entry = logger.info("") const out = writer.render(entry, logger) expect(out).to.eql("\n") }) it("should override root level if level is set", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const writer = new BasicTerminalWriter({ level: LogLevel.verbose }) const entry = logger.verbose("") const out = writer.render(entry, logger) expect(out).to.eql("\n") }) it("should return null if entry level is geq to writer level", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const writer = new BasicTerminalWriter() const entry = logger.verbose("") const out = writer.render(entry, logger) expect(out).to.eql(null) }) it("should return null if entry has no message", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const writer = new BasicTerminalWriter() const entry = logger.info({}) const out = writer.render(entry, logger) @@ -116,7 +129,6 @@ describe("FancyTerminalWriter.toTerminalEntries", () => { writer.stop() verboseWriter.stop() it("should map a LogNode into an array of entries with line numbers and spinner positions", () => { - const logger = new RootLogNode({ level: LogLevel.info }) logger.info("1 line") // 0 logger.info("2 lines\n") // 1 logger.info("1 line") // 3 @@ -130,19 +142,16 @@ describe("FancyTerminalWriter.toTerminalEntries", () => { expect(spinners).to.eql([[0, 7], [3, 8]]) }) it("should override root level if level is set", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const entry = logger.verbose("") const terminalEntries = verboseWriter.toTerminalEntries(logger) expect(terminalEntries[0].key).to.eql(entry.key) }) it("should skip entry if entry level is geq to writer level", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const entry = logger.verbose("") const terminalEntries = writer.toTerminalEntries(logger) expect(terminalEntries).to.eql([]) }) it("should skip entry if entry has no message", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const entry = logger.info({}) const terminalEntries = writer.toTerminalEntries(logger) expect(terminalEntries).to.eql([]) @@ -150,7 +159,6 @@ describe("FancyTerminalWriter.toTerminalEntries", () => { }) describe("LogEntry", () => { - const logger = new RootLogNode({ level: LogLevel.info }) const entry = logger.info("") describe("setState", () => { it("should update entry state and optionally append new msg to previous msg", () => {