Skip to content

Commit 3445da0

Browse files
committed
feat: timings are now written alongside debug log files
BREAKING CHANGE: `--timing` file changes: - When run with the `--timing` flag, `npm` now writes timing data to a file alongside the debug log data, respecting the `logs-dir` option and falling back to `<CACHE>/_logs/` dir, instead of directly inside the cache directory. - The timing file data is no longer newline delimited JSON, and instead each run will create a uniquely named `<ID>-timing.json` file, with the `<ID>` portion being the same as the debug log. - Finally, the data inside the file now has three top level keys, `metadata`, `timers, and `unfinishedTimers` instead of everything being a top level key. Closes npm/statusboard#456
1 parent 84598fe commit 3445da0

File tree

15 files changed

+118
-131
lines changed

15 files changed

+118
-131
lines changed

docs/content/using-npm/config.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -1707,12 +1707,11 @@ particular, use care when overriding this setting for public packages.
17071707
* Default: false
17081708
* Type: Boolean
17091709

1710-
If true, writes a debug log to `logs-dir` and timing information to
1711-
`_timing.json` in the cache, even if the command completes successfully.
1712-
`_timing.json` is a newline delimited list of JSON objects.
1710+
If true, writes timing information to a process specific json file in the
1711+
cache or `logs-dir`. The file name ends with `-timing.json`.
17131712

17141713
You can quickly view it with this [json](https://npm.im/json) command line:
1715-
`npm exec -- json -g < ~/.npm/_timing.json`.
1714+
`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g`.
17161715

17171716
<!-- automatically generated, do not edit manually -->
17181717
<!-- see lib/utils/config/definitions.js -->

docs/content/using-npm/logging.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ The `npm` CLI has various mechanisms for showing different levels of information
1212

1313
All logs are written to a debug log, with the path to that file printed if the execution of a command fails.
1414

15-
The default location of the logs directory is a directory named `_logs` inside the npm cache. This can be changed
16-
with the `logs-dir` config option.
15+
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.
1716

1817
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.
1918

@@ -62,9 +61,10 @@ The [`--timing` config](/using-npm/config#timing) can be set which does two
6261
things:
6362

6463
1. Always shows the full path to the debug log regardless of command exit status
65-
1. Write timing information to a timing file in the cache or `logs-dir`
64+
1. Write timing information to a process specific timing file in the cache or `logs-dir`
6665

67-
This file is a newline delimited list of JSON objects that can be inspected to see timing data for each task in a `npm` CLI run.
66+
This file contains a `timers` object where the keys are an identifier for the
67+
portion of the process being timed and the value is the number of milliseconds it took to complete.
6868

6969
### Registry Response Headers
7070

lib/npm.js

+5-11
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ const replaceInfo = require('./utils/replace-info.js')
1818
const updateNotifier = require('./utils/update-notifier.js')
1919
const pkg = require('../package.json')
2020
const cmdList = require('./utils/cmd-list.js')
21+
const runId = require('./utils/run-id.js')
2122

2223
let warnedNonDashArg = false
2324
const _load = Symbol('_load')
25+
const RUN_ID = runId()
2426

2527
class Npm extends EventEmitter {
2628
static get version () {
@@ -38,9 +40,10 @@ class Npm extends EventEmitter {
3840
#argvClean = []
3941
#chalk = null
4042

41-
#logFile = new LogFile()
43+
#logFile = new LogFile({ id: RUN_ID })
4244
#display = new Display()
4345
#timers = new Timers({
46+
id: RUN_ID,
4447
start: 'npm',
4548
listener: (name, ms) => {
4649
const args = ['timing', name, `Completed in ${ms}ms`]
@@ -207,10 +210,6 @@ class Npm extends EventEmitter {
207210
writeTimingFile () {
208211
this.#timers.writeFile({
209212
command: this.#argvClean,
210-
// We used to only ever report a single log file
211-
// so to be backwards compatible report the last logfile
212-
// XXX: remove this in npm 9 or just keep it forever
213-
logfile: this.logFiles[this.logFiles.length - 1],
214213
logfiles: this.logFiles,
215214
version: this.version,
216215
})
@@ -298,7 +297,7 @@ class Npm extends EventEmitter {
298297

299298
this.time('npm:load:timers', () =>
300299
this.#timers.load({
301-
dir: this.config.get('timing') ? this.timingDir : null,
300+
dir: this.config.get('timing') ? this.logsDir : null,
302301
})
303302
)
304303

@@ -376,11 +375,6 @@ class Npm extends EventEmitter {
376375
return this.#timers.file
377376
}
378377

379-
get timingDir () {
380-
// XXX(npm9): make this always in logs-dir
381-
return this.config.get('logs-dir') || this.cache
382-
}
383-
384378
get cache () {
385379
return this.config.get('cache')
386380
}

lib/utils/config/definitions.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -2062,13 +2062,11 @@ define('timing', {
20622062
default: false,
20632063
type: Boolean,
20642064
description: `
2065-
If true, writes a debug log to \`logs-dir\` and timing information
2066-
to \`_timing.json\` in the cache, even if the command completes
2067-
successfully. \`_timing.json\` is a newline delimited list of JSON
2068-
objects.
2065+
If true, writes timing information to a process specific json file in
2066+
the cache or \`logs-dir\`. The file name ends with \`-timing.json\`.
20692067
20702068
You can quickly view it with this [json](https://npm.im/json) command
2071-
line: \`npm exec -- json -g < ~/.npm/_timing.json\`.
2069+
line: \`cat ~/.npm/_logs/*-timing.json | npm exec -- json -g\`.
20722070
`,
20732071
})
20742072

lib/utils/display.js

+12-17
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ const npmlog = require('npmlog')
33
const log = require('./log-shim.js')
44
const { explain } = require('./explain-eresolve.js')
55

6-
const _logHandler = Symbol('logHandler')
7-
const _eresolveWarn = Symbol('eresolveWarn')
8-
const _log = Symbol('log')
9-
const _npmlog = Symbol('npmlog')
10-
116
class Display {
127
constructor () {
138
// pause by default until config is loaded
@@ -16,11 +11,11 @@ class Display {
1611
}
1712

1813
on () {
19-
process.on('log', this[_logHandler])
14+
process.on('log', this.#logHandler)
2015
}
2116

2217
off () {
23-
process.off('log', this[_logHandler])
18+
process.off('log', this.#logHandler)
2419
// Unbalanced calls to enable/disable progress
2520
// will leave change listeners on the tracker
2621
// This pretty much only happens in tests but
@@ -72,44 +67,44 @@ class Display {
7267
}
7368

7469
log (...args) {
75-
this[_logHandler](...args)
70+
this.#logHandler(...args)
7671
}
7772

78-
[_logHandler] = (level, ...args) => {
73+
#logHandler = (level, ...args) => {
7974
try {
80-
this[_log](level, ...args)
75+
this.#log(level, ...args)
8176
} catch (ex) {
8277
try {
8378
// if it crashed once, it might again!
84-
this[_npmlog]('verbose', `attempt to log ${inspect(args)} crashed`, ex)
79+
this.#npmlog('verbose', `attempt to log ${inspect(args)} crashed`, ex)
8580
} catch (ex2) {
8681
// eslint-disable-next-line no-console
8782
console.error(`attempt to log ${inspect(args)} crashed`, ex, ex2)
8883
}
8984
}
9085
}
9186

92-
[_log] (...args) {
93-
return this[_eresolveWarn](...args) || this[_npmlog](...args)
87+
#log (...args) {
88+
return this.#eresolveWarn(...args) || this.#npmlog(...args)
9489
}
9590

9691
// Explicitly call these on npmlog and not log shim
9792
// This is the final place we should call npmlog before removing it.
98-
[_npmlog] (level, ...args) {
93+
#npmlog (level, ...args) {
9994
npmlog[level](...args)
10095
}
10196

10297
// Also (and this is a really inexcusable kludge), we patch the
10398
// log.warn() method so that when we see a peerDep override
10499
// explanation from Arborist, we can replace the object with a
105100
// highly abbreviated explanation of what's being overridden.
106-
[_eresolveWarn] (level, heading, message, expl) {
101+
#eresolveWarn (level, heading, message, expl) {
107102
if (level === 'warn' &&
108103
heading === 'ERESOLVE' &&
109104
expl && typeof expl === 'object'
110105
) {
111-
this[_npmlog](level, heading, message)
112-
this[_npmlog](level, '', explain(expl, log.useColor(), 2))
106+
this.#npmlog(level, heading, message)
107+
this.#npmlog(level, '', explain(expl, log.useColor(), 2))
113108
// Return true to short circuit other log in chain
114109
return true
115110
}

lib/utils/log-file.js

+22-32
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,11 @@ const MiniPass = require('minipass')
77
const fsMiniPass = require('fs-minipass')
88
const fs = require('@npmcli/fs')
99
const log = require('./log-shim')
10+
const runId = require('./run-id')
1011

1112
const padZero = (n, length) => n.toString().padStart(length.toString().length, '0')
1213
const globify = pattern => pattern.split('\\').join('/')
1314

14-
const _logHandler = Symbol('logHandler')
15-
const _formatLogItem = Symbol('formatLogItem')
16-
const _getLogFilePath = Symbol('getLogFilePath')
17-
const _openLogFile = Symbol('openLogFile')
18-
const _cleanLogs = Symbol('cleanlogs')
19-
const _endStream = Symbol('endStream')
20-
const _isBuffered = Symbol('isBuffered')
21-
2215
class LogFiles {
2316
// If we write multiple log files we want them all to have the same
2417
// identifier for sorting and matching purposes
@@ -46,19 +39,16 @@ class LogFiles {
4639
#files = []
4740

4841
constructor ({
42+
id = runId(),
4943
maxLogsPerFile = 50_000,
5044
maxFilesPerProcess = 5,
5145
} = {}) {
52-
this.#logId = LogFiles.logId(new Date())
46+
this.#logId = id
5347
this.#MAX_LOGS_PER_FILE = maxLogsPerFile
5448
this.#MAX_FILES_PER_PROCESS = maxFilesPerProcess
5549
this.on()
5650
}
5751

58-
static logId (d) {
59-
return d.toISOString().replace(/[.:]/g, '_')
60-
}
61-
6252
static format (count, level, title, ...args) {
6353
let prefix = `${count} ${level}`
6454
if (title) {
@@ -75,12 +65,12 @@ class LogFiles {
7565

7666
on () {
7767
this.#logStream = new MiniPass()
78-
process.on('log', this[_logHandler])
68+
process.on('log', this.#logHandler)
7969
}
8070

8171
off () {
82-
process.off('log', this[_logHandler])
83-
this[_endStream]()
72+
process.off('log', this.#logHandler)
73+
this.#endStream()
8474
}
8575

8676
load ({ dir, logsMax = Infinity } = {}) {
@@ -100,7 +90,7 @@ class LogFiles {
10090
// set that as the new log logstream for future writes
10191
// if logs max is 0 then the user does not want a log file
10292
if (this.#logsMax > 0) {
103-
const initialFile = this[_openLogFile]()
93+
const initialFile = this.#openLogFile()
10494
if (initialFile) {
10595
this.#logStream = this.#logStream.pipe(initialFile)
10696
}
@@ -109,29 +99,29 @@ class LogFiles {
10999
// Kickoff cleaning process, even if we aren't writing a logfile.
110100
// This is async but it will always ignore the current logfile
111101
// Return the result so it can be awaited in tests
112-
return this[_cleanLogs]()
102+
return this.#cleanLogs()
113103
}
114104

115105
log (...args) {
116-
this[_logHandler](...args)
106+
this.#logHandler(...args)
117107
}
118108

119109
get files () {
120110
return this.#files
121111
}
122112

123-
get [_isBuffered] () {
113+
get #isBuffered () {
124114
return this.#logStream instanceof MiniPass
125115
}
126116

127-
[_endStream] (output) {
117+
#endStream (output) {
128118
if (this.#logStream) {
129119
this.#logStream.end(output)
130120
this.#logStream = null
131121
}
132122
}
133123

134-
[_logHandler] = (level, ...args) => {
124+
#logHandler = (level, ...args) => {
135125
// Ignore pause and resume events since we
136126
// write everything to the log file
137127
if (level === 'pause' || level === 'resume') {
@@ -143,9 +133,9 @@ class LogFiles {
143133
return
144134
}
145135

146-
const logOutput = this[_formatLogItem](level, ...args)
136+
const logOutput = this.#formatLogItem(level, ...args)
147137

148-
if (this[_isBuffered]) {
138+
if (this.#isBuffered) {
149139
// Cant do anything but buffer the output if we dont
150140
// have a file stream yet
151141
this.#logStream.write(logOutput)
@@ -155,29 +145,29 @@ class LogFiles {
155145
// Open a new log file if we've written too many logs to this one
156146
if (this.#fileLogCount >= this.#MAX_LOGS_PER_FILE) {
157147
// Write last chunk to the file and close it
158-
this[_endStream](logOutput)
148+
this.#endStream(logOutput)
159149
if (this.#files.length >= this.#MAX_FILES_PER_PROCESS) {
160150
// but if its way too many then we just stop listening
161151
this.off()
162152
} else {
163153
// otherwise we are ready for a new file for the next event
164-
this.#logStream = this[_openLogFile]()
154+
this.#logStream = this.#openLogFile()
165155
}
166156
} else {
167157
this.#logStream.write(logOutput)
168158
}
169159
}
170160

171-
[_formatLogItem] (...args) {
161+
#formatLogItem (...args) {
172162
this.#fileLogCount += 1
173163
return LogFiles.format(this.#totalLogCount++, ...args)
174164
}
175165

176-
[_getLogFilePath] (count = '') {
166+
#getLogFilePath (count = '') {
177167
return path.resolve(this.#dir, `${this.#logId}-debug-${count}.log`)
178168
}
179169

180-
[_openLogFile] () {
170+
#openLogFile () {
181171
// Count in filename will be 0 indexed
182172
const count = this.#files.length
183173

@@ -186,7 +176,7 @@ class LogFiles {
186176
// We never want to write files ending in `-9.log` and `-10.log` because
187177
// log file cleaning is done by deleting the oldest so in this example
188178
// `-10.log` would be deleted next
189-
const f = this[_getLogFilePath](padZero(count, this.#MAX_FILES_PER_PROCESS))
179+
const f = this.#getLogFilePath(padZero(count, this.#MAX_FILES_PER_PROCESS))
190180
// Some effort was made to make the async, but we need to write logs
191181
// during process.on('exit') which has to be synchronous. So in order
192182
// to never drop log messages, it is easiest to make it sync all the time
@@ -210,15 +200,15 @@ class LogFiles {
210200
}
211201
}
212202

213-
async [_cleanLogs] () {
203+
async #cleanLogs () {
214204
// module to clean out the old log files
215205
// this is a best-effort attempt. if a rm fails, we just
216206
// log a message about it and move on. We do return a
217207
// Promise that succeeds when we've tried to delete everything,
218208
// just for the benefit of testing this function properly.
219209

220210
try {
221-
const logPath = this[_getLogFilePath]()
211+
const logPath = this.#getLogFilePath()
222212
const logGlob = path.join(path.dirname(logPath), path.basename(logPath)
223213
// tell glob to only match digits
224214
.replace(/\d/g, '[0123456789]')

lib/utils/run-id.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = (d = new Date()) => {
2+
return d.toISOString().replace(/[.:]/g, '_')
3+
}

0 commit comments

Comments
 (0)