dsh-all-usage 1.2.3 → 1.2.4
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/CHANGELOG.md +22 -0
- package/README.de.md +316 -0
- package/README.en.md +52 -14
- package/README.es.md +316 -0
- package/README.fr.md +316 -0
- package/README.ja.md +316 -0
- package/README.md +55 -16
- package/README.pt.md +316 -0
- package/README.ru.md +316 -0
- package/assets/cover-en.png +0 -0
- package/assets/cover-zh.png +0 -0
- package/assets/readme-architecture-en.svg +95 -95
- package/assets/readme-architecture-zh.svg +95 -95
- package/assets/readme-sync-en.svg +81 -81
- package/assets/readme-sync-zh.svg +81 -81
- package/assets/screenshots/en/01-overview.png +0 -0
- package/assets/screenshots/en/02-trend-bars.png +0 -0
- package/assets/screenshots/en/02-trend.png +0 -0
- package/assets/screenshots/en/03-heatmap.png +0 -0
- package/assets/screenshots/en/04-model-stats.png +0 -0
- package/assets/screenshots/en/05-request-logs.png +0 -0
- package/assets/screenshots/en/06-cost-settings.png +0 -0
- package/assets/screenshots/en/07-maintenance.png +0 -0
- package/assets/screenshots/zh/01-overview.png +0 -0
- package/assets/screenshots/zh/02-trend-bars.png +0 -0
- package/assets/screenshots/zh/02-trend.png +0 -0
- package/assets/screenshots/zh/03-heatmap.png +0 -0
- package/assets/screenshots/zh/04-model-stats.png +0 -0
- package/assets/screenshots/zh/05-request-logs.png +0 -0
- package/assets/screenshots/zh/06-cost-settings.png +0 -0
- package/assets/screenshots/zh/07-maintenance.png +0 -0
- package/lib/archive.js +399 -0
- package/lib/client.js +1 -1
- package/lib/http.js +27 -1
- package/lib/ledger.js +362 -54
- package/package.json +10 -1
- package/screenshots.json +5 -3
- package/scripts/archive-ledger.mjs +156 -0
- package/assets/screenshots/en/04-model-usage.png +0 -0
- package/assets/screenshots/zh/04-model-usage.png +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cold-archives the usage ledger.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/archive-ledger.mjs --root <dir> [--older-than-days 90] [--apply]
|
|
5
|
+
//
|
|
6
|
+
// Every session record whose updatedAt is older than the cutoff is folded into
|
|
7
|
+
// <root>/archive/<YYYY-MM>.jsonl.gz; only after that month's record file, its key
|
|
8
|
+
// index and the manifest are on disk is the per-record document removed. The
|
|
9
|
+
// default is a dry run that prints the plan and writes nothing at all.
|
|
10
|
+
//
|
|
11
|
+
// The whole run happens behind the ledger's maintenance lock. Losing that race is
|
|
12
|
+
// not an error to retry blindly: another process is migrating or archiving the same
|
|
13
|
+
// root, so this command exits 2 without touching a single file.
|
|
14
|
+
import { readFile, readdir, unlink } from 'node:fs/promises'
|
|
15
|
+
import { join, resolve } from 'node:path'
|
|
16
|
+
import { archiveRecords } from '../lib/archive.js'
|
|
17
|
+
import { withLedgerMaintenanceLock } from '../lib/ledger.js'
|
|
18
|
+
|
|
19
|
+
const USAGE = 'usage: node scripts/archive-ledger.mjs --root <dir> [--older-than-days 90] [--apply]'
|
|
20
|
+
const DEFAULT_DAYS = 90
|
|
21
|
+
const DAY_MS = 86400000
|
|
22
|
+
// The per-record trees the ledger keeps next to the archive. The legacy whole-unit
|
|
23
|
+
// files (all_usage_ledger_NN.json) are not trees and are left alone: the ledger
|
|
24
|
+
// retires them itself once the trees hold their rows.
|
|
25
|
+
const SHARD_DIR_PATTERN = /^all_usage_ledger_\d{2}$/
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const options = { root: '', days: DEFAULT_DAYS, apply: false }
|
|
29
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
30
|
+
const arg = argv[index]
|
|
31
|
+
if (arg === '--apply') { options.apply = true; continue }
|
|
32
|
+
if (arg === '--help' || arg === '-h') return null
|
|
33
|
+
const equals = arg.indexOf('=')
|
|
34
|
+
const name = equals === -1 ? arg : arg.slice(0, equals)
|
|
35
|
+
if (name !== '--root' && name !== '--older-than-days') throw new Error('unknown argument: ' + arg)
|
|
36
|
+
let value
|
|
37
|
+
if (equals !== -1) value = arg.slice(equals + 1)
|
|
38
|
+
else {
|
|
39
|
+
value = argv[index + 1]
|
|
40
|
+
if (value === undefined) throw new Error('missing value for ' + name)
|
|
41
|
+
index += 1
|
|
42
|
+
}
|
|
43
|
+
if (name === '--root') { options.root = value; continue }
|
|
44
|
+
const days = Number(value)
|
|
45
|
+
if (!Number.isFinite(days) || days < 0) throw new Error('--older-than-days must be a non-negative number of days')
|
|
46
|
+
options.days = days
|
|
47
|
+
}
|
|
48
|
+
if (options.root === '') throw new Error('--root is required')
|
|
49
|
+
return options
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The ledger's hot copy without a DSH storage backend: the per-record trees under
|
|
53
|
+
// <root>/all_usage_ledger_NN/sessions. The shard a session hashes to is not
|
|
54
|
+
// recomputed here - one listing of every tree answers listKeys() and readRecord()
|
|
55
|
+
// together, and a document can only sit in the tree it was found in.
|
|
56
|
+
async function openTreeStore(root) {
|
|
57
|
+
const trees = []
|
|
58
|
+
for (const entry of await readdir(root, { withFileTypes: true }).catch(() => [])) {
|
|
59
|
+
if (entry.isDirectory() && SHARD_DIR_PATTERN.test(entry.name)) trees.push(join(root, entry.name, 'sessions'))
|
|
60
|
+
}
|
|
61
|
+
const paths = new Map()
|
|
62
|
+
async function index() {
|
|
63
|
+
paths.clear()
|
|
64
|
+
for (const dir of trees) {
|
|
65
|
+
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
66
|
+
if (!entry.isFile() || !entry.name.endsWith('.json') || entry.name.startsWith('__')) continue
|
|
67
|
+
paths.set(entry.name.slice(0, -'.json'.length), join(dir, entry.name))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
async listKeys() {
|
|
73
|
+
await index()
|
|
74
|
+
return Array.from(paths.keys())
|
|
75
|
+
},
|
|
76
|
+
async readRecord(key) {
|
|
77
|
+
const path = paths.get(key)
|
|
78
|
+
if (path === undefined) return undefined
|
|
79
|
+
const doc = JSON.parse(await readFile(path, 'utf8'))
|
|
80
|
+
if (doc === null || typeof doc !== 'object' || doc.version !== 0 || doc.record === undefined) return undefined
|
|
81
|
+
return doc.record
|
|
82
|
+
},
|
|
83
|
+
async deleteRecord(key, expected) {
|
|
84
|
+
const path = paths.get(key)
|
|
85
|
+
if (path === undefined) return false
|
|
86
|
+
// Same optimistic check as the ledger's own store: a session that published a
|
|
87
|
+
// newer revision while this run was planning keeps its document, and the next
|
|
88
|
+
// run archives that revision instead of losing it.
|
|
89
|
+
if (expected !== undefined) {
|
|
90
|
+
if (expected === null || typeof expected !== 'object') return false
|
|
91
|
+
let doc = null
|
|
92
|
+
try { doc = JSON.parse(await readFile(path, 'utf8')) } catch (error) { return false }
|
|
93
|
+
if (doc === null || typeof doc !== 'object' || doc.record === null || typeof doc.record !== 'object') return false
|
|
94
|
+
if (doc.record.updatedAt !== expected.updatedAt) return false
|
|
95
|
+
if (JSON.stringify(doc.record) !== JSON.stringify(expected)) return false
|
|
96
|
+
}
|
|
97
|
+
paths.delete(key)
|
|
98
|
+
return unlink(path).then(() => true, () => false)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatBytes(bytes) {
|
|
104
|
+
if (bytes < 1024) return bytes + ' B'
|
|
105
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
106
|
+
return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
let options
|
|
111
|
+
try {
|
|
112
|
+
options = parseArgs(process.argv.slice(2))
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('archive-ledger: ' + error.message)
|
|
115
|
+
console.error(USAGE)
|
|
116
|
+
return 2
|
|
117
|
+
}
|
|
118
|
+
if (options === null) {
|
|
119
|
+
console.log(USAGE)
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
const root = resolve(options.root)
|
|
123
|
+
const cutoffMs = Date.now() - options.days * DAY_MS
|
|
124
|
+
const mode = options.apply ? 'apply' : 'dry run (pass --apply to write)'
|
|
125
|
+
console.log('all-usage ledger archive')
|
|
126
|
+
console.log(' root: ' + root)
|
|
127
|
+
console.log(' cutoff: ' + new Date(cutoffMs).toISOString() + ' (' + options.days + ' days old or older)')
|
|
128
|
+
console.log(' mode: ' + mode)
|
|
129
|
+
const store = await openTreeStore(root)
|
|
130
|
+
try {
|
|
131
|
+
const outcome = await withLedgerMaintenanceLock(root, () => archiveRecords({ root, store, cutoffMs, dryRun: !options.apply }))
|
|
132
|
+
if (!outcome.acquired) {
|
|
133
|
+
console.error('[all-usage] another process holds the maintenance lock in ' + root + ': nothing was written')
|
|
134
|
+
return 2
|
|
135
|
+
}
|
|
136
|
+
const result = outcome.value
|
|
137
|
+
if (result.months.length === 0 && result.archived === 0) {
|
|
138
|
+
console.log(' nothing to archive: no record is older than the cutoff')
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
console.log(' months: ' + result.months.join(', '))
|
|
142
|
+
console.log(' records: ' + result.archived)
|
|
143
|
+
console.log(' bytes: ' + formatBytes(result.bytes) + ' of records' + (options.apply ? ' -> ' + formatBytes(result.compressed) + ' of month files' : ''))
|
|
144
|
+
if (result.errors.length > 0) {
|
|
145
|
+
for (const error of result.errors) console.error('[all-usage] archive failed for ' + (error.month === undefined ? error.key : error.month) + ': ' + error.message)
|
|
146
|
+
console.error('[all-usage] ' + result.errors.length + ' record group(s) stayed in the ledger trees')
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
return 0
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('[all-usage] archive-ledger failed: ' + (error && error.stack ? error.stack : error))
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
process.exitCode = await main()
|
|
Binary file
|
|
Binary file
|