Skip to content

Commit 8daf348

Browse files
authored
Allow verbose option to be a function for custom logging (#1130)
1 parent 78edcb9 commit 8daf348

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1720
-165
lines changed

docs/api.md

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,12 +1033,14 @@ More info [here](ipc.md#send-an-initial-message) and [there](input.md#any-input-
10331033

10341034
### options.verbose
10351035

1036-
_Type:_ `'none' | 'short' | 'full'`\
1036+
_Type:_ `'none' | 'short' | 'full' | Function`\
10371037
_Default:_ `'none'`
10381038

10391039
If `verbose` is `'short'`, prints the command on [`stderr`](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)): its file, arguments, duration and (if it failed) error message.
10401040

1041-
If `verbose` is `'full'`, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and [IPC messages](ipc.md) are also printed.
1041+
If `verbose` is `'full'` or a function, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and [IPC messages](ipc.md) are also printed.
1042+
1043+
A [function](#verbose-function) can be passed to customize logging. Please see [this page](debugging.md#custom-logging) for more information.
10421044

10431045
By default, this applies to both `stdout` and `stderr`, but [different values can also be passed](output.md#stdoutstderr-specific-options).
10441046

@@ -1170,6 +1172,84 @@ If `false`, escapes the command arguments on Windows.
11701172

11711173
[More info.](windows.md#cmdexe-escaping)
11721174

1175+
## Verbose function
1176+
1177+
_Type_: `(string, VerboseObject) => string | undefined`
1178+
1179+
Function passed to the [`verbose`](#optionsverbose) option to customize logging.
1180+
1181+
[More info.](debugging.md#custom-logging)
1182+
1183+
### Verbose object
1184+
1185+
_Type_: `VerboseObject` or `SyncVerboseObject`
1186+
1187+
Subprocess event object, for logging purpose, using the [`verbose`](#optionsverbose) option.
1188+
1189+
#### verboseObject.type
1190+
1191+
_Type_: `string`
1192+
1193+
Event type. This can be:
1194+
- `'command'`: subprocess start
1195+
- `'output'`: `stdout`/`stderr` [output](output.md#stdout-and-stderr)
1196+
- `'ipc'`: IPC [output](ipc.md#retrieve-all-messages)
1197+
- `'error'`: subprocess [failure](errors.md#subprocess-failure)
1198+
- `'duration'`: subprocess success or failure
1199+
1200+
#### verboseObject.message
1201+
1202+
_Type_: `string`
1203+
1204+
Depending on [`verboseObject.type`](#verboseobjecttype), this is:
1205+
- `'command'`: the [`result.escapedCommand`](#resultescapedcommand)
1206+
- `'output'`: one line from [`result.stdout`](#resultstdout) or [`result.stderr`](#resultstderr)
1207+
- `'ipc'`: one IPC message from [`result.ipcOutput`](#resultipcoutput)
1208+
- `'error'`: the [`error.shortMessage`](#errorshortmessage)
1209+
- `'duration'`: the [`result.durationMs`](#resultdurationms)
1210+
1211+
#### verboseObject.escapedCommand
1212+
1213+
_Type_: `string`
1214+
1215+
The file and [arguments](input.md#command-arguments) that were run. This is the same as [`result.escapedCommand`](#resultescapedcommand).
1216+
1217+
#### verboseObject.options
1218+
1219+
_Type_: [`Options`](#options-1) or [`SyncOptions`](#options-1)
1220+
1221+
The [options](#options-1) passed to the subprocess.
1222+
1223+
#### verboseObject.commandId
1224+
1225+
_Type_: `string`
1226+
1227+
Serial number identifying the subprocess within the current process. It is incremented from `'0'`.
1228+
1229+
This is helpful when multiple subprocesses are running at the same time.
1230+
1231+
This is similar to a [PID](https://en.wikipedia.org/wiki/Process_identifier) except it has no maximum limit, which means it never repeats. Also, it is usually shorter.
1232+
1233+
#### verboseObject.timestamp
1234+
1235+
_Type_: `Date`
1236+
1237+
Event date/time.
1238+
1239+
#### verboseObject.result
1240+
1241+
_Type_: [`Result`](#result), [`SyncResult`](#result) or `undefined`
1242+
1243+
Subprocess [result](#result).
1244+
1245+
This is `undefined` if [`verboseObject.type`](#verboseobjecttype) is `'command'`, `'output'` or `'ipc'`.
1246+
1247+
#### verboseObject.piped
1248+
1249+
_Type_: `boolean`
1250+
1251+
Whether another subprocess is [piped](pipe.md) into this subprocess. This is `false` when [`result.pipedFrom`](#resultfailed) is empty.
1252+
11731253
## Transform options
11741254

11751255
A transform or an [array of transforms](transform.md#combining) can be passed to the [`stdin`](#optionsstdin), [`stdout`](#optionsstdout), [`stderr`](#optionsstderr) or [`stdio`](#optionsstdio) option.

docs/debugging.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,96 @@ When printed to a terminal, the verbose mode uses colors.
106106

107107
<img alt="execa verbose output" src="../media/verbose.png" width="603">
108108

109+
## Custom logging
110+
111+
### Verbose function
112+
113+
The [`verbose`](api.md#optionsverbose) option can be a function to customize logging.
114+
115+
It is called once per log line. The first argument is the default log line string. The second argument is the same information but as an object instead (documented [here](api.md#verbose-object)).
116+
117+
If a string is returned, it is printed on `stderr`. If `undefined` is returned, nothing is printed.
118+
119+
### Filter logs
120+
121+
```js
122+
import {execa as execa_} from 'execa';
123+
124+
// Only print log lines showing the subprocess duration
125+
const execa = execa_({
126+
verbose(verboseLine, {type}) {
127+
return type === 'duration' ? verboseLine : undefined;
128+
},
129+
});
130+
```
131+
132+
### Transform logs
133+
134+
```js
135+
import {execa as execa_} from 'execa';
136+
137+
// Prepend current process' PID
138+
const execa = execa_({
139+
verbose(verboseLine) {
140+
return `[${process.pid}] ${verboseLine}`
141+
},
142+
});
143+
```
144+
145+
### Custom log format
146+
147+
```js
148+
import {execa as execa_} from 'execa';
149+
150+
// Use a different format for the timestamp
151+
const execa = execa_({
152+
verbose(verboseLine, {timestamp}) {
153+
return verboseLine.replace(timestampRegExp, timestamp.toISOString());
154+
},
155+
});
156+
157+
// Timestamp at the start of each log line
158+
const timestampRegExp = /\d{2}:\d{2}:\d{2}\.\d{3}/;
159+
```
160+
161+
### JSON logging
162+
163+
```js
164+
import {execa as execa_} from 'execa';
165+
166+
const execa = execa_({
167+
verbose(verboseLine, verboseObject) {
168+
return JSON.stringify(verboseObject)
169+
},
170+
});
171+
```
172+
173+
### Advanced logging
174+
175+
```js
176+
import {execa as execa_} from 'execa';
177+
import {createLogger, transports} from 'winston';
178+
179+
// Log to a file using Winston
180+
const transport = new transports.File({filename: 'logs.txt'});
181+
const logger = createLogger({transports: [transport]});
182+
183+
const execa = execa_({
184+
verbose(verboseLine, {type, message, ...verboseObject}) {
185+
const level = LOG_LEVELS[type];
186+
logger[level](message, verboseObject);
187+
},
188+
});
189+
190+
const LOG_LEVELS = {
191+
command: 'info',
192+
output: 'verbose',
193+
ipc: 'verbose',
194+
error: 'error',
195+
duration: 'info',
196+
};
197+
```
198+
109199
<hr>
110200

111201
[**Next**: 📎 Windows](windows.md)\

docs/typescript.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## Available types
1010

11-
The following types can be imported: [`ResultPromise`](api.md#return-value), [`Subprocess`](api.md#subprocess), [`Result`](api.md#result), [`ExecaError`](api.md#execaerror), [`Options`](api.md#options-1), [`StdinOption`](api.md#optionsstdin), [`StdoutStderrOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`Message`](api.md#subprocesssendmessagemessage-sendmessageoptions), [`ExecaMethod`](api.md#execaoptions), [`ExecaNodeMethod`](api.md#execanodeoptions) and [`ExecaScriptMethod`](api.md#options).
11+
The following types can be imported: [`ResultPromise`](api.md#return-value), [`Subprocess`](api.md#subprocess), [`Result`](api.md#result), [`ExecaError`](api.md#execaerror), [`Options`](api.md#options-1), [`StdinOption`](api.md#optionsstdin), [`StdoutStderrOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`Message`](api.md#subprocesssendmessagemessage-sendmessageoptions), [`VerboseObject`](api.md#verbose-object), [`ExecaMethod`](api.md#execaoptions), [`ExecaNodeMethod`](api.md#execanodeoptions) and [`ExecaScriptMethod`](api.md#options).
1212

1313
```ts
1414
import {
@@ -21,6 +21,7 @@ import {
2121
type StdoutStderrOption,
2222
type TemplateExpression,
2323
type Message,
24+
type VerboseObject,
2425
type ExecaMethod,
2526
} from 'execa';
2627

@@ -32,6 +33,9 @@ const options: Options = {
3233
stderr: 'pipe' satisfies StdoutStderrOption,
3334
timeout: 1000,
3435
ipc: true,
36+
verbose(verboseLine: string, verboseObject: VerboseObject) {
37+
return verboseObject.type === 'duration' ? verboseLine : undefined;
38+
},
3539
};
3640
const task: TemplateExpression = 'build';
3741
const message: Message = 'hello world';
@@ -50,7 +54,7 @@ try {
5054

5155
## Synchronous execution
5256

53-
Their [synchronous](#synchronous-execution) counterparts are [`SyncResult`](api.md#result), [`ExecaSyncError`](api.md#execasyncerror), [`SyncOptions`](api.md#options-1), [`StdinSyncOption`](api.md#optionsstdin), [`StdoutStderrSyncOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`ExecaSyncMethod`](api.md#execasyncoptions) and [`ExecaScriptSyncMethod`](api.md#syncoptions).
57+
Their [synchronous](#synchronous-execution) counterparts are [`SyncResult`](api.md#result), [`ExecaSyncError`](api.md#execasyncerror), [`SyncOptions`](api.md#options-1), [`StdinSyncOption`](api.md#optionsstdin), [`StdoutStderrSyncOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`SyncVerboseObject`](api.md#verbose-object), [`ExecaSyncMethod`](api.md#execasyncoptions) and [`ExecaScriptSyncMethod`](api.md#syncoptions).
5458

5559
```ts
5660
import {
@@ -61,6 +65,7 @@ import {
6165
type StdinSyncOption,
6266
type StdoutStderrSyncOption,
6367
type TemplateExpression,
68+
type SyncVerboseObject,
6469
type ExecaSyncMethod,
6570
} from 'execa';
6671

@@ -71,6 +76,9 @@ const options: SyncOptions = {
7176
stdout: 'pipe' satisfies StdoutStderrSyncOption,
7277
stderr: 'pipe' satisfies StdoutStderrSyncOption,
7378
timeout: 1000,
79+
verbose(verboseLine: string, verboseObject: SyncVerboseObject) {
80+
return verboseObject.type === 'duration' ? verboseLine : undefined;
81+
},
7482
};
7583
const task: TemplateExpression = 'build';
7684

@@ -93,6 +101,7 @@ import {
93101
execa as execa_,
94102
ExecaError,
95103
type Result,
104+
type VerboseObject,
96105
} from 'execa';
97106

98107
const execa = execa_({preferLocal: true});
@@ -107,6 +116,9 @@ const options = {
107116
stderr: 'pipe',
108117
timeout: 1000,
109118
ipc: true,
119+
verbose(verboseLine: string, verboseObject: VerboseObject) {
120+
return verboseObject.type === 'duration' ? verboseLine : undefined;
121+
},
110122
} as const;
111123
const task = 'build';
112124
const message = 'hello world';

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export {
2424
getCancelSignal,
2525
type Message,
2626
} from './types/ipc.js';
27+
export type {VerboseObject, SyncVerboseObject} from './types/verbose.js';

lib/arguments/command.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {joinCommand} from './escape.js';
55
import {normalizeFdSpecificOption} from './specific.js';
66

77
// Compute `result.command`, `result.escapedCommand` and `verbose`-related information
8-
export const handleCommand = (filePath, rawArguments, {piped, ...rawOptions}) => {
8+
export const handleCommand = (filePath, rawArguments, rawOptions) => {
99
const startTime = getStartTime();
1010
const {command, escapedCommand} = joinCommand(filePath, rawArguments);
1111
const verbose = normalizeFdSpecificOption(rawOptions, 'verbose');
1212
const verboseInfo = getVerboseInfo(verbose, escapedCommand, {...rawOptions});
13-
logCommand(escapedCommand, verboseInfo, piped);
13+
logCommand(escapedCommand, verboseInfo);
1414
return {
1515
command,
1616
escapedCommand,

lib/io/contents.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const logOutputAsync = async ({stream, onStreamEnd, fdNumber, encoding, allMixed
6464
stripFinalNewline: true,
6565
allMixed,
6666
});
67-
await logLines(linesIterable, stream, verboseInfo);
67+
await logLines(linesIterable, stream, fdNumber, verboseInfo);
6868
};
6969

7070
// When using `buffer: false`, users need to read `subprocess.stdout|stderr|all` right away

lib/io/output-sync.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const transformOutputResultSync = (
5151
logOutputSync({
5252
serializedResult,
5353
fdNumber,
54+
state,
5455
verboseInfo,
5556
encoding,
5657
stdioItems,
@@ -101,7 +102,7 @@ const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline
101102
return {serializedResult};
102103
};
103104

104-
const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdioItems, objectMode}) => {
105+
const logOutputSync = ({serializedResult, fdNumber, state, verboseInfo, encoding, stdioItems, objectMode}) => {
105106
if (!shouldLogOutput({
106107
stdioItems,
107108
encoding,
@@ -112,7 +113,12 @@ const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdio
112113
}
113114

114115
const linesArray = splitLinesSync(serializedResult, false, objectMode);
115-
logLinesSync(linesArray, verboseInfo);
116+
117+
try {
118+
logLinesSync(linesArray, fdNumber, verboseInfo);
119+
} catch (error) {
120+
state.error ??= error;
121+
}
116122
};
117123

118124
// When the `std*` target is a file path/URL or a file descriptor

lib/methods/main-async.js

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {pipeOutputAsync} from '../io/output-async.js';
1414
import {subprocessKill} from '../terminate/kill.js';
1515
import {cleanupOnExit} from '../terminate/cleanup.js';
1616
import {pipeToSubprocess} from '../pipe/setup.js';
17-
import {logEarlyResult} from '../verbose/complete.js';
1817
import {makeAllStream} from '../resolve/all-async.js';
1918
import {waitForSubprocessResult} from '../resolve/wait-subprocess.js';
2019
import {addConvertedStreams} from '../convert/add.js';
@@ -48,25 +47,19 @@ export const execaCoreAsync = (rawFile, rawArguments, rawOptions, createNested)
4847
// Compute arguments to pass to `child_process.spawn()`
4948
const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => {
5049
const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions);
51-
52-
try {
53-
const {file, commandArguments, options: normalizedOptions} = normalizeOptions(rawFile, rawArguments, rawOptions);
54-
const options = handleAsyncOptions(normalizedOptions);
55-
const fileDescriptors = handleStdioAsync(options, verboseInfo);
56-
return {
57-
file,
58-
commandArguments,
59-
command,
60-
escapedCommand,
61-
startTime,
62-
verboseInfo,
63-
options,
64-
fileDescriptors,
65-
};
66-
} catch (error) {
67-
logEarlyResult(error, startTime, verboseInfo);
68-
throw error;
69-
}
50+
const {file, commandArguments, options: normalizedOptions} = normalizeOptions(rawFile, rawArguments, rawOptions);
51+
const options = handleAsyncOptions(normalizedOptions);
52+
const fileDescriptors = handleStdioAsync(options, verboseInfo);
53+
return {
54+
file,
55+
commandArguments,
56+
command,
57+
escapedCommand,
58+
startTime,
59+
verboseInfo,
60+
options,
61+
fileDescriptors,
62+
};
7063
};
7164

7265
// Options normalization logic specific to async methods.

0 commit comments

Comments
 (0)