Skip to content

Commit 6e4961f

Browse files
committed
feat: separate configs for --timing and --loglevel
BREAKING CHANGE: `timing` and `loglevel` changes - `timing` has been removed as a value for `--loglevel` - `--timing` will show timing information regardless of `--loglevel`, except when `--silent` Closes npm/statusboard#455 Closes npm/statusboard#454
1 parent 7563a94 commit 6e4961f

File tree

7 files changed

+98
-23
lines changed

7 files changed

+98
-23
lines changed

docs/lib/content/using-npm/logging.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ All logs are written to a debug log, with the path to that file printed if the e
1414

1515
The default location of the logs directory is a directory named `_logs` inside the npm cache. This can be changed with the `logs-dir` config option.
1616

17+
For example, if you wanted to write all your logs to the current working directory, you could run: `npm install --logs-dir=.`. This is especially helpful in debugging a specific `npm` issue as you can run
18+
a command multiple times with different config values and then diff all the log files.
19+
1720
Log files will be removed from the `logs-dir` when the number of log files exceeds `logs-max`, with the oldest logs being deleted first.
1821

1922
To turn off logs completely set `--logs-max=0`.
@@ -31,7 +34,6 @@ The default value of `loglevel` is `"notice"` but there are several levels/types
3134
- `"warn"`
3235
- `"notice"`
3336
- `"http"`
34-
- `"timing"`
3537
- `"info"`
3638
- `"verbose"`
3739
- `"silly"`
@@ -57,15 +59,21 @@ The `npm` CLI began hiding the output of lifecycle scripts for `npm install` as
5759

5860
### Timing Information
5961

60-
The [`--timing` config](/using-npm/config#timing) can be set which does two
62+
The [`--timing` config](/using-npm/config#timing) can be set which does a few
6163
things:
6264

6365
1. Always shows the full path to the debug log regardless of command exit status
6466
1. Write timing information to a process specific timing file in the cache or `logs-dir`
67+
1. Output timing information to the terminal
6568

6669
This file contains a `timers` object where the keys are an identifier for the
6770
portion of the process being timed and the value is the number of milliseconds it took to complete.
6871

72+
Sometimes it is helpful to get timing information without outputting anything to the terminal. For
73+
example, the performance might be affected by writing to the terminal. In this case you can use
74+
`--timing --silent` which will still write the timing file, but not output anything to the terminal
75+
while running.
76+
6977
### Registry Response Headers
7078

7179
#### `npm-notice`

lib/utils/config/definitions.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1273,7 +1273,6 @@ define('loglevel', {
12731273
'warn',
12741274
'notice',
12751275
'http',
1276-
'timing',
12771276
'info',
12781277
'verbose',
12791278
'silly',
@@ -2086,6 +2085,9 @@ define('timing', {
20862085
20872086
You can quickly view it with this [json](https://npm.im/json) command
20882087
line: \`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`.
2088+
2089+
Timing information will also be reported in the terminal. To suppress this
2090+
while still writing the timing file, use \`--silent\`.
20892091
`,
20902092
})
20912093

lib/utils/display.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@ class Display {
3434
heading = 'npm',
3535
} = config
3636

37-
// XXX: decouple timing from loglevel
38-
if (timing && loglevel === 'notice') {
39-
log.level = 'timing'
40-
} else {
41-
log.level = loglevel
42-
}
43-
37+
// npmlog is still going away someday, so this is a hack to dynamically
38+
// set the loglevel of timing based on the timing flag, instead of making
39+
// a breaking change to npmlog. The result is that timing logs are never
40+
// shown except when the --timing flag is set. We also need to change
41+
// the index of the silly level since otherwise it is set to -Infinity
42+
// and we can't go any lower than that. silent is still set to Infinify
43+
// because we DO want silent to hide timing levels. This allows for the
44+
// special case of getting timing information while hiding all CLI output
45+
// in order to get perf information that might be affected by writing to
46+
// a terminal. XXX(npmlog): this will be removed along with npmlog
47+
log.levels.silly = -10000
48+
log.levels.timing = log.levels[loglevel] + (timing ? 1 : -1)
49+
50+
log.level = loglevel
4451
log.heading = heading
4552

4653
if (color) {

tap-snapshots/test/lib/docs.js.test.cjs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1258,8 +1258,8 @@ Ideal if all users are on npm version 7 and higher.
12581258
#### \`loglevel\`
12591259
12601260
* Default: "notice"
1261-
* Type: "silent", "error", "warn", "notice", "http", "timing", "info",
1262-
"verbose", or "silly"
1261+
* Type: "silent", "error", "warn", "notice", "http", "info", "verbose", or
1262+
"silly"
12631263
12641264
What level of logs to report. All logs are written to a debug log, with the
12651265
path to that file printed if the execution of a command fails.
@@ -1754,6 +1754,9 @@ cache or \`logs-dir\`. The file name ends with \`-timing.json\`.
17541754
You can quickly view it with this [json](https://npm.im/json) command line:
17551755
\`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`.
17561756
1757+
Timing information will also be reported in the terminal. To suppress this
1758+
while still writing the timing file, use \`--silent\`.
1759+
17571760
#### \`umask\`
17581761
17591762
* Default: 0

test/fixtures/mock-logs.js

+49-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
const NPMLOG = require('npmlog')
33
const { LEVELS } = require('proc-log')
44

5+
const npmEmitLog = NPMLOG.emitLog.bind(NPMLOG)
6+
const npmLog = NPMLOG.log.bind(NPMLOG)
7+
58
const merge = (...objs) => objs.reduce((acc, obj) => ({ ...acc, ...obj }))
69

710
const mockLogs = (otherMocks = {}) => {
@@ -22,6 +25,25 @@ const mockLogs = (otherMocks = {}) => {
2225
}, {})
2326
)
2427

28+
// the above logs array is anything logged and it not filtered by level.
29+
// this display array is filtered and will not include items that
30+
// would not be shown in the terminal
31+
const display = Object.defineProperties(
32+
[],
33+
['timing', ...LEVELS].reduce((acc, level) => {
34+
acc[level] = {
35+
get () {
36+
return this
37+
.filter(([l]) => level === l)
38+
.map(([l, ...args]) => args)
39+
},
40+
}
41+
return acc
42+
}, {})
43+
)
44+
45+
const npmLogBuffer = []
46+
2547
// This returns an object with mocked versions of all necessary
2648
// logging modules. It mocks them with methods that add logs
2749
// to an array which it also returns. The reason it also returns
@@ -53,15 +75,33 @@ const mockLogs = (otherMocks = {}) => {
5375
// that reflected in the npmlog singleton.
5476
// XXX: remove with npmlog
5577
npmlog: Object.assign(NPMLOG, merge(
56-
// no-op all npmlog methods by default so tests
57-
// dont output anything to the terminal
58-
Object.keys(NPMLOG.levels).reduce((acc, k) => {
59-
acc[k] = () => {}
60-
return acc
61-
}, {}),
62-
// except collect timing logs
6378
{
64-
timing: (...args) => logs.push(['timing', ...args]),
79+
log: (level, ...args) => {
80+
// timing does not exist on proclog, so if it got logged
81+
// with npmlog we need to push it to our logs
82+
if (level === 'timing') {
83+
logs.push([level, ...args])
84+
}
85+
npmLog(level, ...args)
86+
},
87+
write: (msg) => {
88+
// npmlog.write is what outputs to the terminal.
89+
// it writes in chunks so we push each chunk to an
90+
// array that we will log and zero out
91+
npmLogBuffer.push(msg)
92+
},
93+
emitLog: (m) => {
94+
// this calls the original emitLog method
95+
// which will filter based on loglevel
96+
npmEmitLog(m)
97+
// if anything was logged then we push to our display
98+
// array which we can assert against in tests
99+
if (npmLogBuffer.length) {
100+
// first two parts are 'npm' and a single space
101+
display.push(npmLogBuffer.slice(2))
102+
}
103+
npmLogBuffer.length = 0
104+
},
65105
newItem: () => {
66106
return {
67107
info: (...p) => {
@@ -85,7 +125,7 @@ const mockLogs = (otherMocks = {}) => {
85125
)),
86126
}
87127

88-
return { logs, logMocks }
128+
return { logs, logMocks, display }
89129
}
90130

91131
module.exports = mockLogs

test/lib/npm.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -534,11 +534,26 @@ t.test('timings', async t => {
534534

535535
t.test('does not write timings file with timers:false', async t => {
536536
const { npm, timingFile } = await loadMockNpm(t, {
537-
config: { false: true },
537+
config: { timing: false },
538538
})
539539
npm.writeTimingFile()
540540
await t.rejects(() => timingFile())
541541
})
542+
543+
const timingDisplay = [
544+
[{ loglevel: 'silly' }, true, false],
545+
[{ loglevel: 'silly', timing: true }, true, true],
546+
[{ loglevel: 'silent', timing: true }, false, false],
547+
]
548+
549+
for (const [config, expectedDisplay, expectedTiming] of timingDisplay) {
550+
const msg = `${JSON.stringify(config)}, display:${expectedDisplay}, timing:${expectedTiming}`
551+
await t.test(`timing display: ${msg}`, async t => {
552+
const { display } = await loadMockNpm(t, { config })
553+
t.equal(!!display.length, expectedDisplay, 'display')
554+
t.equal(!!display.timing.length, expectedTiming, 'timing display')
555+
})
556+
}
542557
})
543558

544559
t.test('output clears progress and console.logs the message', async t => {

test/lib/utils/display.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ t.test('setup', async (t) => {
1818
const { display } = mockDisplay(t)
1919

2020
display.load({ timing: true, loglevel: 'notice' })
21-
t.equal(log.level, 'timing')
21+
t.equal(log.level, 'notice')
2222

2323
display.load({ timing: false, loglevel: 'notice' })
2424
t.equal(log.level, 'notice')

0 commit comments

Comments
 (0)