Skip to content

Commit

Permalink
feat: provide customizable logger (#417)
Browse files Browse the repository at this point in the history
* feat: provide customizable logger

* fix: export log as experimental API
  • Loading branch information
antongolub committed May 31, 2022
1 parent e652772 commit 2c8711c
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 36 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,49 @@ outputs.

Or use a CLI argument `--quiet` to set `$.verbose = false`.

### `$.logOutput`

Specifies zx debug channel: `stdout/stderr`. Defaults to `stderr`.

### `$.logFormat`

Specifies zx log output formatter. Defaults to `identity`.

```js
// Nice place to add masker, if you pass creds to zx methods
$.logFormat = (msg) => msg.map(m => m.toUpperCase())
```

### `$.logIgnore`

Specifies log events to filter out. Defaults to `''`, so everything is being logged.

```js
$.logIgnore = ['cd', 'fetch']
cd('/tmp/foo')
$.fetch('https://example.com')
// `$ cd /tmp/foo` is omitted
// `$ fetch https://example.com` is not printed too
$.logIgnore = 'cmd'
$`echo 'test'`
// prints `test` w/o `$ echo 'test'`
```

### `$.logPrint`

Specifies event logging stream. Defaults to `process.stdout/process.stderr`.

```js
let stdout = ''
let stderr = ''

$.logPrint = (data, err) => {
if (data) stdout += data
if (err) stderr += err
}
```
### `$.env`
Specifies env map. Defaults to `process.env`.
Expand Down Expand Up @@ -461,6 +504,18 @@ ctx(async ($) => {
})
```
### `log()`
The logger. Accepts config via `$.log*` options.
```js
import {log} from 'zx/experimental'

log({scope: 'foo', verbose: 1, output: 'stderr'}, 'some', 'data')
// some data
```
## FAQ
### Passing env variables
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"chalk": "^5.0.1",
"fs-extra": "^10.1.0",
"globby": "^13.1.1",
"ignore": "^5.2.0",
"minimist": "^1.2.6",
"node-fetch": "^3.2.4",
"ps-tree": "^1.2.0",
Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import { randomId } from './util.js'
import './globals.js'

await (async function main() {
$.verbose = !argv.quiet
$.verbose = +(argv.verbose || argv.quiet ? 0 : $.verbose)

if (typeof argv.shell === 'string') {
$.shell = argv.shell
}
Expand Down Expand Up @@ -220,7 +221,8 @@ function printUsage() {
zx [options] <script>
Options:
--quiet : don't echo commands
--verbose : verbosity level 0|1|2
--quiet : don't echo commands, same as --verbose=0
--shell=<path> : custom shell binary
--prefix=<command> : prefix all commands
--experimental : enable new api proposals
Expand Down
10 changes: 6 additions & 4 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
import { AsyncLocalStorage } from 'node:async_hooks'

export type Options = {
verbose: boolean
verbose: boolean | number
cwd: string
env: NodeJS.ProcessEnv
prefix: string
shell: string
maxBuffer: number
quote: (v: string) => string
logOutput?: 'stdout' | 'stderr'
logFormat?: (...msg: any[]) => string | string[]
logPrint?: (data: any, err?: any) => void
logIgnore?: string | string[]
}

export type Context = Options & {
Expand All @@ -46,6 +50,4 @@ export function setRootCtx(ctx: Options) {
export function getRootCtx() {
return root
}
export function runInCtx(ctx: Options, cb: any) {
return storage.run(ctx, cb)
}
export const runInCtx = storage.run.bind(storage)
12 changes: 6 additions & 6 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { spawn } from 'node:child_process'

import { chalk, which } from './goods.js'
import { runInCtx, getCtx, setRootCtx, Context } from './context.js'
import { printStd, printCmd } from './print.js'
import { printCmd, log } from './print.js'
import { quote, substitute } from './guards.js'

import psTreeModule from 'ps-tree'
Expand Down Expand Up @@ -61,20 +61,20 @@ export function $(pieces: TemplateStringsArray, ...args: any[]) {
return promise
}

setRootCtx($)

$.cwd = process.cwd()
$.env = process.env
$.quote = quote
$.spawn = spawn
$.verbose = true
$.verbose = 2
$.maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
$.prefix = '' // Bash not found, no prefix.
try {
$.shell = which.sync('bash')
$.prefix = 'set -euo pipefail;'
} catch (e) {}

setRootCtx($)

export class ProcessPromise extends Promise<ProcessOutput> {
child?: ChildProcessByStdio<Writable, Readable, Readable>
_resolved = false
Expand Down Expand Up @@ -219,12 +219,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
stderr = '',
combined = ''
let onStdout = (data: any) => {
printStd(data)
log({ scope: 'cmd', output: 'stdout', raw: true, verbose: 2 }, data)
stdout += data
combined += data
}
let onStderr = (data: any) => {
printStd(null, data)
log({ scope: 'cmd', output: 'stderr', raw: true, verbose: 2 }, data)
stderr += data
combined += data
}
Expand Down
12 changes: 5 additions & 7 deletions src/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { ProcessOutput, $ } from './core.js'
import { sleep } from './goods.js'
import { isString } from './util.js'
import { getCtx, runInCtx } from './context.js'
import { log } from './print.js'

export { log }

// Retries a command a few times. Will return after the first
// successful attempt, or will throw after specifies attempts count.
Expand Down Expand Up @@ -78,13 +81,8 @@ export function startSpinner(title = '') {
)(setInterval(spin, 100))
}

export function ctx(
cb: Parameters<typeof runInCtx>[1]
): ReturnType<typeof runInCtx> {
export function ctx<R extends any>(cb: (_$: typeof $) => R): R {
const _$ = Object.assign($.bind(null), getCtx())
function _cb() {
return cb(_$)
}

return runInCtx(_$, _cb)
return runInCtx(_$, cb, _$)
}
62 changes: 46 additions & 16 deletions src/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { getCtx } from './context.js'
import { getCtx, getRootCtx } from './context.js'
import { chalk } from './goods.js'

export function printCmd(cmd: string) {
if (!getCtx()?.verbose) return
if (/\n/.test(cmd)) {
console.log(
cmd
.split('\n')
.map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
.join('\n')
)
} else {
console.log('$', colorize(cmd))
}
}
import { default as ignore } from 'ignore'
import { asArray } from './util.js'

export function printStd(data: any, err?: any) {
if (!getCtx()?.verbose) return
if (data) process.stdout.write(data)
if (err) process.stderr.write(err)
}
Expand All @@ -40,3 +27,46 @@ export function colorize(cmd: string) {
return chalk.greenBright(substr)
})
}

export function log(
opts: {
scope: string
verbose?: 0 | 1 | 2
output?: 'stdout' | 'stderr'
raw?: boolean
},
...msg: any[]
) {
let { scope, verbose = 1, output, raw } = opts
let ctx = getCtx()
let {
logOutput = output,
logFormat = () => msg,
logPrint = printStd,
logIgnore = [],
} = ctx
let level = Math.min(+getRootCtx().verbose, +ctx.verbose)
if (verbose > level) return

const ig = ignore().add(logIgnore)

if (!ig.ignores(scope)) {
msg = raw ? msg[0] : asArray(logFormat(msg)).join(' ') + '\n'
// @ts-ignore
logPrint(...(logOutput === 'stdout' ? [msg] : [null, msg]))
}
}

export function printCmd(cmd: string) {
if (/\n/.test(cmd)) {
log(
{ scope: 'cmd' },
cmd
.split('\n')
.map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
.join('\n')
)
} else {
log({ scope: 'cmd' }, '$', colorize(cmd))
}
}
3 changes: 3 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export function randomId() {
export function isString(obj: any) {
return typeof obj === 'string'
}

export const asArray = <T>(value: T[] | T | undefined): T[] =>
value ? (Array.isArray(value) ? value : [value]) : []
2 changes: 1 addition & 1 deletion test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { test } from 'uvu'
import * as assert from 'uvu/assert'
import '../build/globals.js'

$.verbose = false
$.verbose = 0

test('supports `-v` flag / prints version', async () => {
assert.match((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
Expand Down
5 changes: 5 additions & 0 deletions test/experimental.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
startSpinner,
withTimeout,
ctx,
log,
} from '../build/experimental.js'

import chalk from 'chalk'
Expand Down Expand Up @@ -110,4 +111,8 @@ test('ctx() provides isolates running scopes', async () => {
$.verbose = false
})

test('log() API is available', () => {
assert.ok(typeof log === 'function')
})

test.run()
50 changes: 50 additions & 0 deletions test/log.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { test } from 'uvu'
import * as assert from 'uvu/assert'
import { log } from '../build/print.js'

test('logger works', async () => {
$.verbose = 1
let stdout = ''
let stderr = ''
$.logFormat = (msg) => msg.map((m) => m.toUpperCase())
$.logPrint = (data, err) => {
if (data) stdout += data
if (err) stderr += err
}
$.logIgnore = ['b*', 'fetch']
log({ scope: 'foo' }, 'foo-test')
log({ scope: 'bar' }, 'bar-test')
log({ scope: 'baz' }, 'baz-test')
log({ scope: 'fetch' }, 'example.com')

assert.ok(stderr.includes('FOO-TEST'))
assert.ok(!stderr.includes('BAR-TEST'))
assert.ok(!stderr.includes('BAZ-TEST'))
assert.ok(!stderr.includes('EXAMPLE.COM'))

$.logOutput = 'stdout'
log({ scope: 'foo' }, 'foo')
assert.ok(stdout.includes('FOO'))

delete $.logPrint
delete $.logFormat
delete $.logIgnore
delete $.logOutput
$.verbose = 0
})

test.run()

0 comments on commit 2c8711c

Please sign in to comment.