mikser-io 10.6.0 → 10.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/engine.js +30 -0
- package/src/instance.js +32 -3
- package/src/journal.js +1 -2
- package/src/logger.js +94 -18
- package/src/plugins/files.js +1 -1
- package/src/plugins/observer.js +2 -2
- package/src/plugins/resources.js +2 -2
- package/src/source.js +1 -1
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -1593,6 +1593,16 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
1593
1593
|
// undefined here. The logger has no such problem — it writes during the
|
|
1594
1594
|
// run, by which time options exist.
|
|
1595
1595
|
const quietStdout = ['--json', '--tool', '--tools'].some(flag => process.argv.includes(flag))
|
|
1596
|
+
// The banner is an info record, so a level above info must not print it.
|
|
1597
|
+
//
|
|
1598
|
+
// `--log silent` printed the version line and nothing else — the one line
|
|
1599
|
+
// the flag most obviously promises to remove — because this runs before
|
|
1600
|
+
// the level is applied. Only the FORWARDED path was ever silent, and that
|
|
1601
|
+
// is the path the test exercises, so the test could not see it.
|
|
1602
|
+
//
|
|
1603
|
+
// argv again, for the reason quietStdout reads it: commander parses in a
|
|
1604
|
+
// lifecycle hook, which runs after setup() returns.
|
|
1605
|
+
if (!argvWantsInfo()) return runtime
|
|
1596
1606
|
// Through the logger when nobody is watching, so it gets a timestamp.
|
|
1597
1607
|
//
|
|
1598
1608
|
// This line marks a process start, which in a supervisor's log is the
|
|
@@ -1625,6 +1635,26 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
1625
1635
|
return runtime
|
|
1626
1636
|
}
|
|
1627
1637
|
|
|
1638
|
+
// The level asked for on the command line, before commander has parsed.
|
|
1639
|
+
// Both spellings, both flags: `--log warn` and `--log=warn`, and
|
|
1640
|
+
// `--log-install`, which on a run with no instance to forward to sets the
|
|
1641
|
+
// level for this process.
|
|
1642
|
+
function argvLogLevel() {
|
|
1643
|
+
for (const flag of ['--log', '--log-install', '-l']) {
|
|
1644
|
+
const at = process.argv.indexOf(flag)
|
|
1645
|
+
if (at >= 0 && process.argv[at + 1]) return process.argv[at + 1]
|
|
1646
|
+
const inline = process.argv.find(arg => arg.startsWith(`${flag}=`))
|
|
1647
|
+
if (inline) return inline.slice(flag.length + 1)
|
|
1648
|
+
}
|
|
1649
|
+
return null
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function argvWantsInfo() {
|
|
1653
|
+
const level = argvLogLevel()
|
|
1654
|
+
if (!level || !LOG_LEVELS.includes(level)) return true
|
|
1655
|
+
return LOG_LEVELS.indexOf(level) <= LOG_LEVELS.indexOf('info')
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1628
1658
|
export function useLogger() {
|
|
1629
1659
|
return runtime.engine?.logger
|
|
1630
1660
|
}
|
package/src/instance.js
CHANGED
|
@@ -218,10 +218,29 @@ let server = null
|
|
|
218
218
|
// both into one undifferentiated stream throws that away, and the client can
|
|
219
219
|
// only guess — it guessed stderr, so every forwarded document landed where no
|
|
220
220
|
// consumer looks while the command exited 0.
|
|
221
|
-
|
|
221
|
+
// `echoStdout: false` frames the chunk to the client and writes NOTHING
|
|
222
|
+
// locally.
|
|
223
|
+
//
|
|
224
|
+
// The instance wears the client's output so an operator watching it sees what
|
|
225
|
+
// a forwarded request did — right for log lines, wrong for a DOCUMENT. A
|
|
226
|
+
// --json request put its 364-line report into the instance's own console as
|
|
227
|
+
// well as onto the client's stdout, and under pm2 that is 364 lines in the out
|
|
228
|
+
// log for every report an agent asks for.
|
|
229
|
+
//
|
|
230
|
+
// The seam is exact rather than a guess: under --json / --tool the logger
|
|
231
|
+
// writes to stderr and stdout carries only the document (see logger.js), so
|
|
232
|
+
// stdout chunks during a document-carrying request ARE the document.
|
|
233
|
+
function captureOutput(onChunk, { echoStdout = true } = {}) {
|
|
222
234
|
const originals = [process.stdout.write, process.stderr.write]
|
|
223
235
|
const patch = (stream, original, name) => function (chunk, encoding, callback) {
|
|
224
236
|
try { onChunk(typeof chunk === 'string' ? chunk : chunk.toString(), name) } catch { /* client gone */ }
|
|
237
|
+
if (!echoStdout && name === 'stdout') {
|
|
238
|
+
// write()'s callback is the second argument when encoding is
|
|
239
|
+
// omitted. Dropped, a caller awaiting the drain never resumes.
|
|
240
|
+
const done = typeof encoding === 'function' ? encoding : callback
|
|
241
|
+
if (typeof done === 'function') process.nextTick(done)
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
225
244
|
return original.call(stream, chunk, encoding, callback)
|
|
226
245
|
}
|
|
227
246
|
process.stdout.write = patch(process.stdout, originals[0], 'stdout')
|
|
@@ -242,6 +261,12 @@ function captureOutput(onChunk) {
|
|
|
242
261
|
// not what tells the two apart. Different configs are different files.
|
|
243
262
|
// Does this request name a level that does not exist? Pure, so the refusal
|
|
244
263
|
// can be decided before anything is applied.
|
|
264
|
+
// Does this request's answer go on stdout as a document?
|
|
265
|
+
// The same trio logger.js gates the progress bar on, for the same reason.
|
|
266
|
+
function carriesDocument(request) {
|
|
267
|
+
return Boolean(request?.json || request?.tool || request?.tools)
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
function logRequestRefusal(request) {
|
|
246
271
|
for (const [flag, level] of [['--log', request?.log], ['--log-install', request?.logInstall]]) {
|
|
247
272
|
if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
|
|
@@ -296,7 +321,9 @@ async function configStale() {
|
|
|
296
321
|
// the only process that can answer correctly. Same guards as a build: wrong
|
|
297
322
|
// config refuses, drifted config refuses.
|
|
298
323
|
async function serveReport(socket, request, logger) {
|
|
299
|
-
const restore = captureOutput(
|
|
324
|
+
const restore = captureOutput(
|
|
325
|
+
(chunk, stream) => frame(socket, { type: 'log', chunk, stream }),
|
|
326
|
+
{ echoStdout: !carriesDocument(request) })
|
|
300
327
|
let code = 0
|
|
301
328
|
try {
|
|
302
329
|
code = await withRequestOutput(request, () => runReportOnly(request)) ?? 0
|
|
@@ -501,7 +528,9 @@ async function withRequestOutput(request, run) {
|
|
|
501
528
|
}
|
|
502
529
|
|
|
503
530
|
async function serveBuild(socket, request, logger) {
|
|
504
|
-
const restore = captureOutput(
|
|
531
|
+
const restore = captureOutput(
|
|
532
|
+
(chunk, stream) => frame(socket, { type: 'log', chunk, stream }),
|
|
533
|
+
{ echoStdout: !carriesDocument(request) })
|
|
505
534
|
let code = 0
|
|
506
535
|
try {
|
|
507
536
|
// Fire the pending debounce rather than waiting it out.
|
package/src/journal.js
CHANGED
|
@@ -227,9 +227,8 @@ export async function* useJournal(name, operations, signal) {
|
|
|
227
227
|
stopProgress()
|
|
228
228
|
throw new AbortError()
|
|
229
229
|
}
|
|
230
|
-
updateProgress()
|
|
231
|
-
|
|
232
230
|
const entry = rowToEntry(row)
|
|
231
|
+
updateProgress(entry.entity?.id)
|
|
233
232
|
const originalEntity = row.entity // already a JSON string
|
|
234
233
|
|
|
235
234
|
yield entry
|
package/src/logger.js
CHANGED
|
@@ -28,6 +28,7 @@ import pino from 'pino'
|
|
|
28
28
|
import pretty from 'pino-pretty'
|
|
29
29
|
import Gauge from 'gauge'
|
|
30
30
|
import { Writable } from 'node:stream'
|
|
31
|
+
import path from 'node:path'
|
|
31
32
|
import runtime from './runtime.js'
|
|
32
33
|
import { useLogger } from './engine.js'
|
|
33
34
|
import { onLoad, onFinalized } from './lifecycle.js'
|
|
@@ -482,10 +483,65 @@ onLoad(() => {
|
|
|
482
483
|
runtime.engine.logger = createMikserLogger(level)
|
|
483
484
|
})
|
|
484
485
|
|
|
485
|
-
// Progress API
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
486
|
+
// Progress API: trackProgress / updateProgress / stopProgress /
|
|
487
|
+
// updateProgressDetails. A gauge where a terminal is watching, coded records
|
|
488
|
+
// where one is not, and nothing at all for a phase that took no time.
|
|
489
|
+
|
|
490
|
+
// Long phases only, and long means TIME.
|
|
491
|
+
//
|
|
492
|
+
// A record per quartile is right for 800 documents and absurd for 5. Eleven
|
|
493
|
+
// of a plain build's thirteen phases carry exactly five items, so reporting
|
|
494
|
+
// them the way a bar counts took a piped no-op build from 32 lines to 97 —
|
|
495
|
+
// sixty-five progress records, five in the same millisecond to say a phase
|
|
496
|
+
// did nothing. That is written into the log a deployment keeps, which is the
|
|
497
|
+
// log the installed-level expiry exists to protect.
|
|
498
|
+
//
|
|
499
|
+
// A minimum TOTAL would fix the count and get the other half wrong: four PDFs
|
|
500
|
+
// through Chrome is a small phase and a slow one, and it is exactly the phase
|
|
501
|
+
// worth narrating. Elapsed time is what "long" means and it needs no
|
|
502
|
+
// per-phase tuning.
|
|
503
|
+
//
|
|
504
|
+
// It is an INTERVAL rather than a threshold, and quartiles are gone with it.
|
|
505
|
+
// A quartile is a fraction of the WORK, so it says nothing about how often a
|
|
506
|
+
// line appears: four records land in three seconds on a fast phase and four
|
|
507
|
+
// records cover an hour on a slow one. Time is the axis a reader cares about,
|
|
508
|
+
// so one line per interval, however much work passed in between.
|
|
509
|
+
//
|
|
510
|
+
// This gates the RUNNING commentary only. `progress-finished` is not behind
|
|
511
|
+
// it: every phase says that it ran and what it cost, whatever the duration.
|
|
512
|
+
// Gated, a short phase produced no record at all and a build could not say
|
|
513
|
+
// what it had done — and off a TTY that line is the only place phase timings
|
|
514
|
+
// come from.
|
|
515
|
+
export const PROGRESS_INTERVAL_MS = 30_000
|
|
516
|
+
|
|
517
|
+
// Which item, not just how far. A phase name alone says a build is doing
|
|
518
|
+
// something; the thing a stuck build needs to say is WHAT it is stuck on.
|
|
519
|
+
function progressDetail(detail) {
|
|
520
|
+
if (detail === null || detail === undefined || detail === '') return null
|
|
521
|
+
const text = String(detail)
|
|
522
|
+
const root = runtime.options?.workingFolder
|
|
523
|
+
if (!root || !path.isAbsolute(text)) return text
|
|
524
|
+
// An entity id LOOKS absolute and is not a filesystem path.
|
|
525
|
+
// `/documents/p3706.md` relativised against the working folder becomes
|
|
526
|
+
// `../../../documents/p3706.md`, which names nothing anyone can act on.
|
|
527
|
+
// So shorten only what is genuinely INSIDE the folder, and leave every
|
|
528
|
+
// other string exactly as the call site handed it over.
|
|
529
|
+
const relative = path.relative(root, text)
|
|
530
|
+
return relative.startsWith('..') ? text : relative
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// `0s` for anything under a second reports the whole phase as nothing. Below
|
|
534
|
+
// a second the number IS the information.
|
|
535
|
+
function formatDuration(ms) {
|
|
536
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function emitProgress({ name, total, value, detail }) {
|
|
540
|
+
const at = progressDetail(detail)
|
|
541
|
+
const fields = { code: 'progress', phase: name, total, value }
|
|
542
|
+
if (at) useLogger()?.info({ ...fields, detail: at }, '%s: %d/%d — %s', name, value, total, at)
|
|
543
|
+
else useLogger()?.info(fields, '%s: %d/%d', name, value, total)
|
|
544
|
+
}
|
|
489
545
|
|
|
490
546
|
export function trackProgress(name, total) {
|
|
491
547
|
if (!name || !total) return
|
|
@@ -512,26 +568,31 @@ export function trackProgress(name, total) {
|
|
|
512
568
|
// --json would have extended that to every machine reading the output.
|
|
513
569
|
// Losing the graphics is the point; losing the information is not.
|
|
514
570
|
const drawn = !carriesDocument && Boolean(process.stdout.isTTY) && Boolean(runtime.options.info)
|
|
515
|
-
currentBar = { name, total, value: 0, started: Date.now(), drawn,
|
|
571
|
+
currentBar = { name, total, value: 0, started: Date.now(), drawn, lastReport: Date.now(), detail: null }
|
|
516
572
|
if (drawn) ensureGauge().show({ section: name, subsection: `0/${total}` }, 0)
|
|
517
|
-
else logger.info({ code: 'progress', phase: name, total, value: 0 }, '%s: 0/%d', name, total)
|
|
518
573
|
}
|
|
519
574
|
|
|
520
|
-
export function updateProgress() {
|
|
575
|
+
export function updateProgress(detail) {
|
|
521
576
|
if (!currentBar) return
|
|
522
577
|
currentBar.value++
|
|
578
|
+
// The call site hands over whatever identifies the item it is on — a
|
|
579
|
+
// path, an id — and pays one assignment for it. Formatting happens at
|
|
580
|
+
// emit time, at most four times a phase, so naming the work costs the
|
|
581
|
+
// 14k-entity import nothing.
|
|
582
|
+
if (detail !== undefined) currentBar.detail = detail
|
|
523
583
|
const { name, total, value, drawn } = currentBar
|
|
524
584
|
if (drawn) {
|
|
525
585
|
gauge?.show({ section: name, subsection: `${value}/${total}` }, value / total)
|
|
526
586
|
} else {
|
|
527
|
-
//
|
|
528
|
-
// line, a log record does not
|
|
529
|
-
// this counts the way the bar does.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
587
|
+
// One line per interval, not per item and not per quartile: a bar
|
|
588
|
+
// redraws in place and costs one line, a log record does not, so 800
|
|
589
|
+
// documents is 800 lines of noise if this counts the way the bar does.
|
|
590
|
+
// A phase that finishes inside the interval says nothing at all while
|
|
591
|
+
// it runs — its finished line covers it.
|
|
592
|
+
const now = Date.now()
|
|
593
|
+
if (now - currentBar.lastReport >= PROGRESS_INTERVAL_MS && value < total) {
|
|
594
|
+
currentBar.lastReport = now
|
|
595
|
+
emitProgress(currentBar)
|
|
535
596
|
}
|
|
536
597
|
}
|
|
537
598
|
if (value >= total) stopProgress()
|
|
@@ -541,16 +602,31 @@ export function stopProgress() {
|
|
|
541
602
|
if (!currentBar) return
|
|
542
603
|
const logger = useLogger()
|
|
543
604
|
const { name, total, value, started } = currentBar
|
|
605
|
+
const ms = Date.now() - started
|
|
544
606
|
gauge?.hide()
|
|
545
607
|
// Structured either way, so a machine reading --json's stderr gets the
|
|
546
608
|
// same facts a person reads off the bar.
|
|
609
|
+
//
|
|
610
|
+
// This line has to stand alone. With the running commentary on an
|
|
611
|
+
// interval it is the ONLY record most phases produce, and `Documents
|
|
612
|
+
// import finished: 5 0s` said neither what the five were nor how long it
|
|
613
|
+
// took — a count with no subject and a duration rounded away to nothing.
|
|
614
|
+
//
|
|
615
|
+
// The subject is the PHASE, not an item. The last entity a phase happened
|
|
616
|
+
// to walk is not what the phase was about: seven journal phases in a row
|
|
617
|
+
// reported the same `/layouts/page.hbs` because that is where the walk
|
|
618
|
+
// ended, and `Files import finished: 3, last .../social-fb.svg` put a
|
|
619
|
+
// filename into the build log that nothing had anything to say about — it
|
|
620
|
+
// broke a test asserting that file is never mentioned, which is exactly
|
|
621
|
+
// the misreading it invites. `Files import finished: 3 in 3ms` already
|
|
622
|
+
// says what finished, how much of it, and what it cost. The running
|
|
623
|
+
// records keep the item, because there it shows MOVEMENT.
|
|
547
624
|
if (value < total) {
|
|
548
625
|
logger.warn({ code: 'progress-unfinished', phase: name, total, value, missing: total - value },
|
|
549
|
-
'%s unfinished: %d', name, total - value)
|
|
626
|
+
'%s unfinished: %d of %d after %s', name, total - value, total, formatDuration(ms))
|
|
550
627
|
} else {
|
|
551
|
-
const ms = Date.now() - started
|
|
552
628
|
logger.info({ code: 'progress-finished', phase: name, total, ms },
|
|
553
|
-
'%s finished: %d %
|
|
629
|
+
'%s finished: %d in %s', name, total, formatDuration(ms))
|
|
554
630
|
}
|
|
555
631
|
currentBar = null
|
|
556
632
|
}
|
package/src/plugins/files.js
CHANGED
|
@@ -228,7 +228,7 @@ export function files(options = {}) {
|
|
|
228
228
|
// nothing to emit. Progress ticks either way — a gated file
|
|
229
229
|
// was still looked at.
|
|
230
230
|
const newChecksum = await gateChecksum(source, id, { priorChecksums })
|
|
231
|
-
updateProgress()
|
|
231
|
+
updateProgress(id)
|
|
232
232
|
if (newChecksum === null) return
|
|
233
233
|
await createEntity({
|
|
234
234
|
id,
|
package/src/plugins/observer.js
CHANGED
|
@@ -100,7 +100,7 @@ export function observer(options = {}) {
|
|
|
100
100
|
synced++
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
updateProgress()
|
|
103
|
+
updateProgress(meta.id)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const entitiesToRemove = await findEntities({
|
|
@@ -114,7 +114,7 @@ export function observer(options = {}) {
|
|
|
114
114
|
for (let entity of entitiesToRemove) {
|
|
115
115
|
deleteEntity(entity)
|
|
116
116
|
removed++
|
|
117
|
-
updateProgress()
|
|
117
|
+
updateProgress(entity.id)
|
|
118
118
|
}
|
|
119
119
|
if (synced || removed) {
|
|
120
120
|
logger.debug('Syncing api [%s] synced: %d, removed: %d', collection, synced, removed)
|
package/src/plugins/resources.js
CHANGED
|
@@ -145,7 +145,7 @@ export function resources(options = {}) {
|
|
|
145
145
|
logger.error('Resource error: %s %s %s', entity.id, resource, err.message)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
updateProgress()
|
|
148
|
+
updateProgress(resource)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const resourceFiles = await globby('**/*', { cwd: runtime.options.resourcesFolder })
|
|
@@ -226,7 +226,7 @@ export function resources(options = {}) {
|
|
|
226
226
|
checksum: await checksum(resource)
|
|
227
227
|
})
|
|
228
228
|
}
|
|
229
|
-
updateProgress()
|
|
229
|
+
updateProgress(resource)
|
|
230
230
|
}, { concurrency: 10, signal })
|
|
231
231
|
count && logger.info('Downloaded: %d', count)
|
|
232
232
|
}
|
package/src/source.js
CHANGED
|
@@ -431,7 +431,7 @@ export function useSource(core, options) {
|
|
|
431
431
|
// is never the bottleneck.
|
|
432
432
|
await pMap(files, async (file) => {
|
|
433
433
|
await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums })
|
|
434
|
-
if (phase === 'import') updateProgress()
|
|
434
|
+
if (phase === 'import') updateProgress(file)
|
|
435
435
|
}, { concurrency: SCAN_CONCURRENCY })
|
|
436
436
|
|
|
437
437
|
scanStats.deleted = await sweepDeleted(collection, scanned, async (e) => {
|