dsh-my-observability 0.1.7 → 0.3.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/CHANGELOG.md +18 -0
- package/lib/audit.js +15 -11
- package/lib/resource-monitor.js +34 -3
- package/lib/routes.js +58 -0
- package/package.json +2 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 dsh-my-observability 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
|
|
9
|
+
- feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
|
|
10
|
+
- feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
|
|
11
|
+
- feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
|
|
12
|
+
- chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
|
|
13
|
+
|
|
14
|
+
## [0.2.0] - 2026-09-07
|
|
15
|
+
|
|
16
|
+
### 变更
|
|
17
|
+
|
|
18
|
+
- feat(observability): #155 错误上报统一——plugin_error 事件类型 + /errors API (#177)
|
|
19
|
+
- feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
|
|
20
|
+
- feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
|
|
21
|
+
- chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
|
|
22
|
+
|
|
5
23
|
## [0.1.7] - 2026-09-06
|
|
6
24
|
|
|
7
25
|
### 变更
|
package/lib/audit.js
CHANGED
|
@@ -71,17 +71,21 @@ function summarizeParams(payload) {
|
|
|
71
71
|
function handlePluginEvent(name, payload, record) {
|
|
72
72
|
const sessionId = payload?.sessionId
|
|
73
73
|
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const { type, data } = buildPluginEventData(name, payload)
|
|
75
|
+
record({ type, sessionId, data })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildPluginEventData(name, payload) {
|
|
79
|
+
const data = {
|
|
80
|
+
plugin: pluginNameOf(name),
|
|
81
|
+
event: eventNameOf(name),
|
|
82
|
+
action: truncate(String(payload?.action ?? '')),
|
|
83
|
+
reason: truncate(String(payload?.reason ?? '')),
|
|
84
|
+
params: summarizeParams(payload),
|
|
85
|
+
}
|
|
86
|
+
const error = typeof payload?.error === 'string' && payload.error !== '' ? truncate(payload.error) : null
|
|
87
|
+
if (error !== null) data.error = error
|
|
88
|
+
return { type: error !== null ? 'plugin_error' : 'plugin_event', data }
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
package/lib/resource-monitor.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* 采样自身开销:15s 一次 process.cpuUsage/memoryUsage + fs.stat(<0.01% CPU、
|
|
15
15
|
* 零分配大对象),远低于「监控不能放大被监控对象」的护栏(resource-budget-review)。
|
|
16
16
|
*/
|
|
17
|
-
import { statSync } from 'node:fs'
|
|
17
|
+
import { statSync, readdirSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
18
20
|
import { evaluateResourceAlerts, shouldEnterDegrade, shouldExitDegrade, DEFAULT_LIMITS } from './resource-rules.js'
|
|
19
21
|
import { jsonlFile } from './store-persist.js'
|
|
20
22
|
|
|
@@ -31,6 +33,7 @@ export function createResourceMonitor(ctx, options = {}) {
|
|
|
31
33
|
const state = {
|
|
32
34
|
timer: null,
|
|
33
35
|
file: jsonlFile(),
|
|
36
|
+
dshHome: process.env.DSH_HOME || join(homedir(), '.dsh'),
|
|
34
37
|
lastSample: null,
|
|
35
38
|
lastCpu: process.cpuUsage(),
|
|
36
39
|
history: [],
|
|
@@ -59,6 +62,12 @@ function sample(state, limits, onDegrade, onRecover) {
|
|
|
59
62
|
} catch {
|
|
60
63
|
// 审计文件尚未创建:字节为 0
|
|
61
64
|
}
|
|
65
|
+
let homeBytes = 0
|
|
66
|
+
try {
|
|
67
|
+
homeBytes = dshHomeSize(state.dshHome)
|
|
68
|
+
} catch {
|
|
69
|
+
// $DSH_HOME 不可达
|
|
70
|
+
}
|
|
62
71
|
const prev = state.lastSample
|
|
63
72
|
if (prev !== null) {
|
|
64
73
|
const deltaMs = Math.max(now - prev.time, 1)
|
|
@@ -66,7 +75,7 @@ function sample(state, limits, onDegrade, onRecover) {
|
|
|
66
75
|
const cpuPercent = (cpuDelta / 1000 / deltaMs) * 100
|
|
67
76
|
const byteDelta = fileBytes - prev.fileBytes
|
|
68
77
|
const writeRateBytesPerHour = byteDelta > 0 ? (byteDelta / deltaMs) * 3600 * 1000 : 0
|
|
69
|
-
const sample = { time: now, cpuPercent, memoryBytes, fileBytes, writeRateBytesPerHour }
|
|
78
|
+
const sample = { time: now, cpuPercent, memoryBytes, fileBytes, writeRateBytesPerHour, homeBytes }
|
|
70
79
|
state.history.push(sample)
|
|
71
80
|
if (state.history.length > MAX_HISTORY) state.history.splice(0, state.history.length - MAX_HISTORY)
|
|
72
81
|
state.lastSample = sample
|
|
@@ -78,7 +87,7 @@ function sample(state, limits, onDegrade, onRecover) {
|
|
|
78
87
|
degraded: state.degraded,
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
|
-
state.lastSample = { time: now, fileBytes, memoryBytes, cpuPercent: 0, writeRateBytesPerHour: 0 }
|
|
90
|
+
state.lastSample = { time: now, fileBytes, memoryBytes, cpuPercent: 0, writeRateBytesPerHour: 0, homeBytes }
|
|
82
91
|
return { ...state.lastSample, history: [...state.history], alerts: [], degraded: state.degraded }
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -116,3 +125,25 @@ function stopMonitor(state) {
|
|
|
116
125
|
state.timer = null
|
|
117
126
|
}
|
|
118
127
|
}
|
|
128
|
+
|
|
129
|
+
/** 递归计算目录总字节(best-effort,跳过不可达文件)。 */
|
|
130
|
+
function dshHomeSize(dir) {
|
|
131
|
+
let total = 0
|
|
132
|
+
try {
|
|
133
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
const full = join(dir, entry.name)
|
|
135
|
+
if (entry.isDirectory()) {
|
|
136
|
+
total += dshHomeSize(full)
|
|
137
|
+
} else if (entry.isFile()) {
|
|
138
|
+
try {
|
|
139
|
+
total += statSync(full).size
|
|
140
|
+
} catch {
|
|
141
|
+
// 文件不可达
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// 目录不可达
|
|
147
|
+
}
|
|
148
|
+
return total
|
|
149
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -67,6 +67,13 @@ function isMethod(method, request, name, verb) {
|
|
|
67
67
|
|
|
68
68
|
/** 按 method 分派到具体 handler;未识别返回 false(调用方回 404)。 */
|
|
69
69
|
async function dispatchMethod(method, request, response, url, ctx, store, monitor, options) {
|
|
70
|
+
const handled = await dispatchCore(method, request, response, url, ctx, store, monitor, options)
|
|
71
|
+
if (handled) return true
|
|
72
|
+
return dispatchExtended(method, request, response, url, ctx, store)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 核心路由(复杂度 ≤10)。 */
|
|
76
|
+
async function dispatchCore(method, request, response, url, ctx, store, monitor, options) {
|
|
70
77
|
if (isMethod(method, request, 'sessions', 'GET')) {
|
|
71
78
|
writeJson(response, 200, { ok: true, value: store.sessions() })
|
|
72
79
|
return true
|
|
@@ -105,6 +112,19 @@ async function dispatchMethod(method, request, response, url, ctx, store, monito
|
|
|
105
112
|
return false
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
/** 扩展路由(#154/#155 新增)。 */
|
|
116
|
+
function dispatchExtended(method, request, response, url, ctx, store) {
|
|
117
|
+
if (isMethod(method, request, 'plugin-status', 'GET')) {
|
|
118
|
+
void handlePluginStatus(ctx, response)
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
if (isMethod(method, request, 'errors', 'GET')) {
|
|
122
|
+
handleErrors(store, url, response)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
// ── handlers ───────────────────────────────────────────────────────────────
|
|
109
129
|
|
|
110
130
|
/** 状态:审计统计 + 功能开关(aiReview 只暴露开关)。 */
|
|
@@ -117,6 +137,44 @@ function statusValue(store, options) {
|
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 插件状态聚合:广播 plugin:status-query 事件,收集所有插件状态。
|
|
142
|
+
* 每个插件返回 { plugin, config, running, lastActions }(config 脱敏,lastActions ≤5)。
|
|
143
|
+
* 超时 3s 未响应的插件标记为 { plugin, running: false, error: 'timeout' }。
|
|
144
|
+
*/
|
|
145
|
+
async function handlePluginStatus(ctx, response) {
|
|
146
|
+
const TIMEOUT_MS = 3000
|
|
147
|
+
const results = []
|
|
148
|
+
try {
|
|
149
|
+
// 广播查询事件,带超时
|
|
150
|
+
const statuses = await Promise.allSettled(
|
|
151
|
+
(ctx.bundler?.plugins ?? []).map(async (p) => {
|
|
152
|
+
const name = p.name ?? p.constructor?.name ?? 'unknown'
|
|
153
|
+
try {
|
|
154
|
+
const result = await Promise.race([
|
|
155
|
+
ctx.emit('plugin:status-query', { plugin: name }),
|
|
156
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)),
|
|
157
|
+
])
|
|
158
|
+
return result ?? { plugin: name, running: true, config: {}, lastActions: [] }
|
|
159
|
+
} catch {
|
|
160
|
+
return { plugin: name, running: false, error: 'timeout' }
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
for (const s of statuses) {
|
|
165
|
+
results.push(s.status === 'fulfilled' ? s.value : { plugin: 'unknown', running: false, error: 'rejected' })
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// bundler 不可用时返回空列表
|
|
169
|
+
}
|
|
170
|
+
writeJson(response, 200, { ok: true, value: results })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 错误事件查询(#155 错误上报统一):返回 plugin_error 类型事件。 */
|
|
174
|
+
function handleErrors(store, url, response) {
|
|
175
|
+
writeJson(response, 200, { ok: true, value: store.events(null, 'plugin_error', limitOf(url)) })
|
|
176
|
+
}
|
|
177
|
+
|
|
120
178
|
/** git status:非仓库路径 400。 */
|
|
121
179
|
async function handleGitStatus(response, repoPath) {
|
|
122
180
|
if (repoPath === '') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-my-observability",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "DSH 可观测性 + Git 工程工具插件:会话轨迹回放(时间轴)、事件审计(agent 行为记录)、结构化 Git 提交(Conventional Commits)、增量 diff 审查(提交前审查)。DSH web plugin: trajectory replay timeline, event audit log, structured git commits, pre-commit incremental diff review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -37,10 +37,7 @@
|
|
|
37
37
|
"patch": "./cordis.patch.yml"
|
|
38
38
|
},
|
|
39
39
|
"client": {
|
|
40
|
-
"platform": "web"
|
|
41
|
-
"inject": [
|
|
42
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
43
|
-
]
|
|
40
|
+
"platform": "web"
|
|
44
41
|
}
|
|
45
42
|
},
|
|
46
43
|
"peerDependencies": {
|