dsh-my-observability 0.3.0 → 0.3.2
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 +9 -79
- package/README.md +18 -76
- package/lib/ai.js +123 -99
- package/lib/audit-view.js +173 -172
- package/lib/audit.js +175 -166
- package/lib/client.js +1024 -1278
- package/lib/client.src.js +55 -29
- package/lib/constants.js +4 -4
- package/lib/diff.js +61 -56
- package/lib/git.js +97 -98
- package/lib/index.js +46 -47
- package/lib/parts/native-tabs.js +45 -0
- package/lib/resource-monitor.js +82 -134
- package/lib/resource-rules.js +9 -99
- package/lib/review.js +68 -77
- package/lib/routes.js +223 -200
- package/lib/store-persist.js +114 -111
- package/lib/store.js +215 -212
- package/package.json +15 -7
- package/lib/parts/git.js +0 -298
- package/lib/parts/i18n.js +0 -104
- package/lib/parts/replay-ext.js +0 -396
- package/lib/parts/replay.js +0 -386
- package/lib/parts/resource.js +0 -80
- package/lib/parts/styles.js +0 -156
package/lib/store-persist.js
CHANGED
|
@@ -12,165 +12,168 @@
|
|
|
12
12
|
* - **加载规整**:单行解析失败跳过(不崩溃);每会话/全局上限在加载时
|
|
13
13
|
* 与运行时一致生效。
|
|
14
14
|
*/
|
|
15
|
-
import { mkdir, readFile, rename, writeFile, appendFile } from 'node:fs/promises'
|
|
16
|
-
import { join, dirname } from 'node:path'
|
|
17
|
-
import { homedir } from 'node:os'
|
|
18
|
-
|
|
15
|
+
import { mkdir, readFile, rename, writeFile, appendFile } from 'node:fs/promises';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
19
18
|
/** 每会话事件上限(运行时与加载规整共用)。 */
|
|
20
|
-
export const MAX_EVENTS_PER_SESSION = 2000
|
|
19
|
+
export const MAX_EVENTS_PER_SESSION = 2000;
|
|
21
20
|
/** 全局事件上限(运行时与加载规整共用)。 */
|
|
22
|
-
export const MAX_TOTAL_EVENTS = 20000
|
|
21
|
+
export const MAX_TOTAL_EVENTS = 20000;
|
|
23
22
|
/** 追加行数达到该值触发一次 compact(文件大小有界)。 */
|
|
24
|
-
export const COMPACT_LINES = 5000
|
|
25
|
-
|
|
23
|
+
export const COMPACT_LINES = 5000;
|
|
26
24
|
/** 数据根目录:$DSH_HOME(fallback 家目录)。 */
|
|
27
25
|
function dataHome() {
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const home = process.env.DSH_HOME;
|
|
27
|
+
return typeof home === 'string' && home !== '' ? home : homedir();
|
|
30
28
|
}
|
|
31
|
-
|
|
32
29
|
/** 追加日志文件路径(新格式)。 */
|
|
33
30
|
export function jsonlFile() {
|
|
34
|
-
|
|
31
|
+
return join(dataHome(), 'observability', 'audit.jsonl');
|
|
35
32
|
}
|
|
36
|
-
|
|
37
33
|
/** 兼容的旧格式文件路径(0.1.x 整文件 JSON)。 */
|
|
38
34
|
export function legacyFile() {
|
|
39
|
-
|
|
35
|
+
return join(dataHome(), 'observability', 'audit.json');
|
|
40
36
|
}
|
|
41
|
-
|
|
42
37
|
/** 事件结构校验(时间/会话/类型为合理形态)。 */
|
|
43
38
|
function isValidEvent(event) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
39
|
+
const target = event;
|
|
40
|
+
return (target !== null &&
|
|
41
|
+
typeof target === 'object' &&
|
|
42
|
+
typeof target.time === 'number' &&
|
|
43
|
+
typeof target.sessionId === 'string' &&
|
|
44
|
+
typeof target.type === 'string');
|
|
51
45
|
}
|
|
52
|
-
|
|
53
46
|
/**
|
|
54
47
|
* 加载后的状态规整:按会话桶过滤非法事件 + 每会话截断 + 全局超限时
|
|
55
48
|
* 按最早活动会话整桶轮转淘汰(与运行时 appendEvent/evictOldest 语义一致)。
|
|
56
49
|
* 桶形态兼容两种:jsonl 解析的数组桶,与旧格式的 { events } 对象桶。
|
|
57
50
|
*/
|
|
58
51
|
export function normalizeLoaded(bySession) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
(a, b) => firstTimeOf(sessionBuckets[a]) - firstTimeOf(sessionBuckets[b]),
|
|
70
|
-
)
|
|
71
|
-
for (const sessionId of ordered) {
|
|
72
|
-
if (total <= MAX_TOTAL_EVENTS) break
|
|
73
|
-
total -= sessionBuckets[sessionId].events.length
|
|
74
|
-
delete sessionBuckets[sessionId]
|
|
52
|
+
const sessionBuckets = {};
|
|
53
|
+
for (const [sessionId, bucket] of Object.entries(bySession)) {
|
|
54
|
+
const raw = Array.isArray(bucket)
|
|
55
|
+
? bucket
|
|
56
|
+
: Array.isArray(bucket?.events)
|
|
57
|
+
? bucket.events
|
|
58
|
+
: [];
|
|
59
|
+
const events = raw.filter(isValidEvent).slice(-MAX_EVENTS_PER_SESSION);
|
|
60
|
+
if (events.length > 0)
|
|
61
|
+
sessionBuckets[sessionId] = { events };
|
|
75
62
|
}
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (const bucket of Object.values(sessionBuckets))
|
|
65
|
+
total += bucket.events.length;
|
|
66
|
+
if (total > MAX_TOTAL_EVENTS) {
|
|
67
|
+
const ordered = Object.keys(sessionBuckets).sort((a, b) => firstTimeOf(sessionBuckets[a]) - firstTimeOf(sessionBuckets[b]));
|
|
68
|
+
for (const sessionId of ordered) {
|
|
69
|
+
if (total <= MAX_TOTAL_EVENTS)
|
|
70
|
+
break;
|
|
71
|
+
total -= sessionBuckets[sessionId].events.length;
|
|
72
|
+
delete sessionBuckets[sessionId];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { version: 1, bySession: sessionBuckets };
|
|
78
76
|
}
|
|
79
|
-
|
|
80
77
|
function firstTimeOf(bucket) {
|
|
81
|
-
|
|
78
|
+
return bucket.events[0].time;
|
|
82
79
|
}
|
|
83
|
-
|
|
84
80
|
/** 解析 jsonl 文本 → { bySession, lines }(非法行跳过;lines 为有效+无效总行数)。 */
|
|
85
81
|
export function parseJsonl(text) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
const bySession = {};
|
|
83
|
+
let lines = 0;
|
|
84
|
+
for (const line of text.split('\n')) {
|
|
85
|
+
lines += 1;
|
|
86
|
+
if (line === '')
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const event = JSON.parse(line);
|
|
90
|
+
if (!isValidEvent(event))
|
|
91
|
+
continue;
|
|
92
|
+
const bucket = bySession[event.sessionId] ?? (bySession[event.sessionId] = []);
|
|
93
|
+
bucket.push(event);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// 截断/损坏行跳过
|
|
97
|
+
}
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
return { bySession, lines }
|
|
99
|
+
return { bySession, lines };
|
|
101
100
|
}
|
|
102
|
-
|
|
103
101
|
/** 解析旧格式(整文件 JSON)→ { bySession, lines };结构非法返回 null。 */
|
|
104
102
|
export function parseLegacy(text) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(text);
|
|
105
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
106
|
+
return null;
|
|
107
|
+
const bySession = parsed.bySession;
|
|
108
|
+
if (typeof bySession !== 'object' || bySession === null)
|
|
109
|
+
return null;
|
|
110
|
+
return { bySession: bySession, lines: Object.keys(bySession).length };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
112
115
|
}
|
|
113
|
-
|
|
114
116
|
/**
|
|
115
117
|
* 加载持久化状态:优先 jsonl;否则旧格式(标记 migrated);
|
|
116
118
|
* 都缺失返回空状态。文件不存在或损坏均回退(migrated 仅合法旧文件触发)。
|
|
117
119
|
*/
|
|
118
120
|
export async function loadPersisted(file, legacy) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
const jsonl = await readFile(file, 'utf8')
|
|
122
|
+
.then((text) => ({ kind: 'jsonl', text }))
|
|
123
|
+
.catch(() => null);
|
|
124
|
+
if (jsonl !== null) {
|
|
125
|
+
const parsed = parseJsonl(jsonl.text);
|
|
126
|
+
return { state: normalizeLoaded(parsed.bySession), migrated: false, lines: parsed.lines };
|
|
127
|
+
}
|
|
128
|
+
const old = await readFile(legacy, 'utf8')
|
|
129
|
+
.then((text) => parseLegacy(text))
|
|
130
|
+
.catch(() => null);
|
|
131
|
+
if (old !== null) {
|
|
132
|
+
return { state: normalizeLoaded(old.bySession), migrated: true, lines: old.lines };
|
|
133
|
+
}
|
|
134
|
+
return { state: { version: 1, bySession: {} }, migrated: false, lines: 0 };
|
|
133
135
|
}
|
|
134
|
-
|
|
135
136
|
/** 原子快照写(tmp+rename):compact 用。快照内容为 jsonl 行(每行一个事件),
|
|
136
137
|
* 保证重启加载与增补追加读取同一格式。 */
|
|
137
138
|
export async function writeSnapshot(file, state, logger, prefix) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
const dir = dirname(file);
|
|
140
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(dir, { recursive: true });
|
|
143
|
+
await writeFile(tmp, stateToLines(state), 'utf8');
|
|
144
|
+
await rename(tmp, file);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
logger?.warn(`${prefix} snapshot failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
147
149
|
}
|
|
148
|
-
|
|
149
150
|
/** 内存状态 → jsonl 行文本(按会话桶串联,桶内保持事件原序)。 */
|
|
150
151
|
function stateToLines(state) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
const lines = [];
|
|
153
|
+
for (const bucket of Object.values(state.bySession ?? {})) {
|
|
154
|
+
if (!Array.isArray(bucket?.events))
|
|
155
|
+
continue;
|
|
156
|
+
for (const event of bucket.events)
|
|
157
|
+
lines.push(JSON.stringify(event));
|
|
158
|
+
}
|
|
159
|
+
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
157
160
|
}
|
|
158
|
-
|
|
159
161
|
/** 追加写入(jsonl 行);目录自动创建。 */
|
|
160
162
|
export async function appendLines(file, text, logger, prefix) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
try {
|
|
164
|
+
await mkdir(dirname(file), { recursive: true });
|
|
165
|
+
await appendFile(file, text, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logger?.warn(`${prefix} append failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
}
|
|
167
170
|
}
|
|
168
|
-
|
|
169
171
|
/** 迁移完成后移除旧格式文件(rename 为 .migrated 备份;缺失忽略)。 */
|
|
170
172
|
export async function removeLegacyAfterMigration(legacy) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
try {
|
|
174
|
+
await rename(legacy, `${legacy}.migrated`);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// 文件不存在或已移除:忽略
|
|
178
|
+
}
|
|
176
179
|
}
|