@goodandready/dsh-subscriptions 0.7.9 → 0.7.10
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/bin/dsh-subscriptions.mjs +2 -0
- package/lib/health-report.js +21 -0
- package/lib/metrics.js +62 -0
- package/lib/routes/metrics.js +33 -0
- package/lib/routes/sources.js +28 -25
- package/lib/routes/status.js +15 -1
- package/lib/routes.js +2 -0
- package/package.json +1 -1
|
@@ -105,6 +105,8 @@ export function formatDoctor(payload) {
|
|
|
105
105
|
lines.push('doctor: ' + (doctorFailed(payload) ? 'degraded' : 'ok'))
|
|
106
106
|
lines.push('errors: ' + errors + ' conflicts: ' + conflicts)
|
|
107
107
|
lines.push('key-rotation: ' + (kr.available ? (kr.healthyKeys + '/' + kr.totalKeys + ' healthy keys') : ('unavailable' + (kr.reason ? ' (' + kr.reason + ')' : ''))))
|
|
108
|
+
const snapshots = diag.vaultSnapshots || {}
|
|
109
|
+
lines.push('snapshots: ' + (snapshots.count || 0) + ' (' + (snapshots.bytes || 0) + ' bytes)')
|
|
108
110
|
for (const row of (sources.providers || [])) lines.push(row.provider + ': ' + (row.selected || 'none'))
|
|
109
111
|
return lines.join('\n')
|
|
110
112
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// #745: health is about having a usable source, not about uptime alone.
|
|
2
|
+
export function assessHealth({ accounts = [], sources = null, snapshots = null, webhookUrl = '', ollama = false } = {}) {
|
|
3
|
+
const list = Array.isArray(accounts) ? accounts : []
|
|
4
|
+
const connectedAccounts = list.filter((a) => a && a.configured).length
|
|
5
|
+
const sourceRows = sources && Array.isArray(sources.providers) ? sources.providers : []
|
|
6
|
+
const selectedSources = sourceRows.filter((row) => row && row.selected).length
|
|
7
|
+
const keyRotation = Boolean(sources && sources.keyRotation && sources.keyRotation.available)
|
|
8
|
+
const ok = connectedAccounts > 0 || keyRotation || Boolean(ollama)
|
|
9
|
+
return {
|
|
10
|
+
ok,
|
|
11
|
+
status: ok ? 'healthy' : 'degraded',
|
|
12
|
+
checks: {
|
|
13
|
+
connectedAccounts,
|
|
14
|
+
keyRotation,
|
|
15
|
+
ollama: Boolean(ollama),
|
|
16
|
+
webhook: Boolean(webhookUrl),
|
|
17
|
+
snapshots: snapshots ? Number(snapshots.count) || 0 : 0,
|
|
18
|
+
selectedSources,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/metrics.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// #744: Prometheus text exposition. No secrets, labels escaped, deterministic order.
|
|
2
|
+
function label(value) {
|
|
3
|
+
return String(value == null ? '' : value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function add(lines, name, help, type, samples) {
|
|
7
|
+
lines.push('# HELP ' + name + ' ' + help)
|
|
8
|
+
lines.push('# TYPE ' + name + ' ' + type)
|
|
9
|
+
for (const sample of samples) lines.push(name + (sample.labels ? '{' + sample.labels + '}' : '') + ' ' + sample.value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatMetrics({ accounts = [], rows = [], alerts = [], sources = null } = {}) {
|
|
13
|
+
const lines = []
|
|
14
|
+
const accountRows = Array.isArray(accounts) ? accounts : []
|
|
15
|
+
add(lines, 'dsh_subscriptions_connected_accounts', 'Accounts with a configured credential.', 'gauge', [
|
|
16
|
+
{ value: accountRows.filter((a) => a && a.configured).length },
|
|
17
|
+
])
|
|
18
|
+
const configured = []
|
|
19
|
+
const usage = []
|
|
20
|
+
const cooldown = []
|
|
21
|
+
for (const account of accountRows) {
|
|
22
|
+
if (!account || !account.provider) continue
|
|
23
|
+
const labels = 'provider="' + label(account.provider) + '",index="' + label(account.index || 1) + '"'
|
|
24
|
+
configured.push({ labels, value: account.configured ? 1 : 0 })
|
|
25
|
+
if (account.usagePercent != null) usage.push({ labels, value: Number(account.usagePercent) })
|
|
26
|
+
const until = Number(account.cooldownUntil) || 0
|
|
27
|
+
cooldown.push({ labels, value: until > Date.now() ? Math.round((until - Date.now()) / 1000) : 0 })
|
|
28
|
+
}
|
|
29
|
+
add(lines, 'dsh_subscriptions_account_configured', '1 when the slot has a credential.', 'gauge', configured)
|
|
30
|
+
add(lines, 'dsh_subscriptions_account_usage_percent', 'Last observed quota usage.', 'gauge', usage)
|
|
31
|
+
add(lines, 'dsh_subscriptions_account_cooldown_seconds', 'Remaining cooldown.', 'gauge', cooldown)
|
|
32
|
+
|
|
33
|
+
const selected = []
|
|
34
|
+
for (const row of (sources && Array.isArray(sources.providers) ? sources.providers : [])) {
|
|
35
|
+
if (!row || !row.selected) continue
|
|
36
|
+
selected.push({ labels: 'provider="' + label(row.provider) + '",source="' + label(row.selected) + '"', value: 1 })
|
|
37
|
+
}
|
|
38
|
+
add(lines, 'dsh_subscriptions_source_selected', 'Selected source for a provider.', 'gauge', selected)
|
|
39
|
+
|
|
40
|
+
const byRequest = new Map()
|
|
41
|
+
for (const row of (Array.isArray(rows) ? rows : [])) {
|
|
42
|
+
const provider = row && row.provider ? row.provider : 'unknown'
|
|
43
|
+
const status = row && row.status != null ? row.status : 'unknown'
|
|
44
|
+
const key = provider + '|' + status
|
|
45
|
+
byRequest.set(key, (byRequest.get(key) || 0) + 1)
|
|
46
|
+
}
|
|
47
|
+
add(lines, 'dsh_subscriptions_requests_total', 'Recorded requests by provider and status.', 'counter',
|
|
48
|
+
[...byRequest.entries()].sort().map(([key, value]) => {
|
|
49
|
+
const [provider, status] = key.split('|')
|
|
50
|
+
return { labels: 'provider="' + label(provider) + '",status="' + label(status) + '"', value }
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
const bySeverity = new Map()
|
|
54
|
+
for (const alert of (Array.isArray(alerts) ? alerts : [])) {
|
|
55
|
+
const severity = alert && alert.severity ? alert.severity : 'warn'
|
|
56
|
+
bySeverity.set(severity, (bySeverity.get(severity) || 0) + 1)
|
|
57
|
+
}
|
|
58
|
+
add(lines, 'dsh_subscriptions_alerts_total', 'Recent alerts by severity.', 'counter',
|
|
59
|
+
[...bySeverity.entries()].sort().map(([severity, value]) => ({ labels: 'severity="' + label(severity) + '"', value })))
|
|
60
|
+
|
|
61
|
+
return lines.join('\n') + '\n'
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getRecentAlerts } from '../alerts.js'
|
|
2
|
+
import { isLoopback, isTrustedSettingsRequest } from '../http.js'
|
|
3
|
+
import { formatMetrics } from '../metrics.js'
|
|
4
|
+
import { collectSourceChain } from './sources.js'
|
|
5
|
+
|
|
6
|
+
export function registerMetricsRoutes(ctx, state) {
|
|
7
|
+
ctx.effect(() => ctx.webServer.register({
|
|
8
|
+
kind: 'exact',
|
|
9
|
+
path: '/dsh-subscriptions/metrics',
|
|
10
|
+
handler: async (req, res) => {
|
|
11
|
+
if (req.method !== 'GET') {
|
|
12
|
+
res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' })
|
|
13
|
+
res.end('method not allowed\n')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
if (!isLoopback(req) && !isTrustedSettingsRequest(req, ctx)) {
|
|
17
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' })
|
|
18
|
+
res.end('forbidden\n')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const accounts = await state.accountsView().catch(() => [])
|
|
22
|
+
const sources = await collectSourceChain(state, req.socket && req.socket.localPort)
|
|
23
|
+
const text = formatMetrics({
|
|
24
|
+
accounts,
|
|
25
|
+
rows: typeof state.history?.all === 'function' ? state.history.all() : [],
|
|
26
|
+
alerts: getRecentAlerts(1000),
|
|
27
|
+
sources,
|
|
28
|
+
})
|
|
29
|
+
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' })
|
|
30
|
+
res.end(text)
|
|
31
|
+
},
|
|
32
|
+
}), 'dsh-subscriptions: /metrics')
|
|
33
|
+
}
|
package/lib/routes/sources.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
|
|
2
2
|
import { buildSourceChain, readKeyRotationStatus, subscriptionCandidate } from '../key-rotation-bridge.js'
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export async function collectSourceChain(state, port) {
|
|
5
5
|
const { accountsView, live } = state
|
|
6
|
+
let subscriptions = []
|
|
7
|
+
try {
|
|
8
|
+
const rows = await accountsView()
|
|
9
|
+
subscriptions = (rows || []).map(subscriptionCandidate)
|
|
10
|
+
} catch { /* keep the chain available */ }
|
|
11
|
+
const keyRotation = port
|
|
12
|
+
? await readKeyRotationStatus({ baseUrl: 'http://127.0.0.1:' + port })
|
|
13
|
+
: { available: false, reason: 'no local port', providers: [], totalKeys: 0, healthyKeys: 0 }
|
|
14
|
+
const chain = buildSourceChain({
|
|
15
|
+
subscriptions,
|
|
16
|
+
keyRotation,
|
|
17
|
+
ollama: { available: live().ollamaFallback !== false },
|
|
18
|
+
})
|
|
19
|
+
return {
|
|
20
|
+
keyRotation: {
|
|
21
|
+
available: keyRotation.available,
|
|
22
|
+
reason: keyRotation.reason || null,
|
|
23
|
+
totalKeys: keyRotation.totalKeys,
|
|
24
|
+
healthyKeys: keyRotation.healthyKeys,
|
|
25
|
+
},
|
|
26
|
+
...chain,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerSourcesRoutes(ctx, state) {
|
|
6
31
|
|
|
7
32
|
ctx.effect(() => ctx.webServer.register({
|
|
8
33
|
kind: 'exact',
|
|
@@ -16,30 +41,8 @@ export function registerSourcesRoutes(ctx, state) {
|
|
|
16
41
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
17
42
|
return
|
|
18
43
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const rows = await accountsView()
|
|
22
|
-
subscriptions = (rows || []).map(subscriptionCandidate)
|
|
23
|
-
} catch { /* keep the chain available */ }
|
|
24
|
-
const port = req.socket && req.socket.localPort
|
|
25
|
-
const keyRotation = port
|
|
26
|
-
? await readKeyRotationStatus({ baseUrl: 'http://127.0.0.1:' + port })
|
|
27
|
-
: { available: false, reason: 'no local port', providers: [], totalKeys: 0, healthyKeys: 0 }
|
|
28
|
-
const chain = buildSourceChain({
|
|
29
|
-
subscriptions,
|
|
30
|
-
keyRotation,
|
|
31
|
-
ollama: { available: live().ollamaFallback !== false },
|
|
32
|
-
})
|
|
33
|
-
writeJson(res, 200, {
|
|
34
|
-
ok: true,
|
|
35
|
-
keyRotation: {
|
|
36
|
-
available: keyRotation.available,
|
|
37
|
-
reason: keyRotation.reason || null,
|
|
38
|
-
totalKeys: keyRotation.totalKeys,
|
|
39
|
-
healthyKeys: keyRotation.healthyKeys,
|
|
40
|
-
},
|
|
41
|
-
...chain,
|
|
42
|
-
})
|
|
44
|
+
const payload = await collectSourceChain(state, req.socket && req.socket.localPort)
|
|
45
|
+
writeJson(res, 200, { ok: true, ...payload })
|
|
43
46
|
}),
|
|
44
47
|
}), 'dsh-subscriptions: /sources')
|
|
45
48
|
}
|
package/lib/routes/status.js
CHANGED
|
@@ -26,6 +26,9 @@ import { quotaSnapshot } from '../ratelimit.js'
|
|
|
26
26
|
import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
|
|
27
27
|
import { getVendor } from '../vendors/index.js'
|
|
28
28
|
import { defaultErrorBuffer } from '../error-buffer.js'
|
|
29
|
+
import { assessHealth } from '../health-report.js'
|
|
30
|
+
import { defaultSnapshotStore } from '../vault-snapshots.js'
|
|
31
|
+
import { collectSourceChain } from './sources.js'
|
|
29
32
|
|
|
30
33
|
export function registerStatusRoutes(ctx, state) {
|
|
31
34
|
const {
|
|
@@ -82,7 +85,16 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
82
85
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
83
86
|
return
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
const accounts = await accountsView().catch(() => [])
|
|
89
|
+
const sources = await collectSourceChain(state, req.socket && req.socket.localPort)
|
|
90
|
+
const health = assessHealth({
|
|
91
|
+
accounts,
|
|
92
|
+
sources,
|
|
93
|
+
snapshots: defaultSnapshotStore.stats(),
|
|
94
|
+
webhookUrl: live().webhookUrl,
|
|
95
|
+
ollama: live().ollamaFallback !== false,
|
|
96
|
+
})
|
|
97
|
+
writeJson(res, 200, { ok: true, status: health.status, version: pkgVersion || 'unknown', checks: health.checks })
|
|
86
98
|
}),
|
|
87
99
|
}), 'dsh-subscriptions: /health')
|
|
88
100
|
|
|
@@ -310,6 +322,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
310
322
|
report: await diagnosticsReport(),
|
|
311
323
|
conflicts: getActiveConflicts(),
|
|
312
324
|
route: buildRouteReport(last, accounts),
|
|
325
|
+
sources: await collectSourceChain(state, req.socket && req.socket.localPort),
|
|
326
|
+
vaultSnapshots: defaultSnapshotStore.stats(),
|
|
313
327
|
})
|
|
314
328
|
}),
|
|
315
329
|
}), 'dsh-subscriptions: /diagnostics')
|
package/lib/routes.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerSourcesRoutes } from './routes/sources.js'
|
|
|
9
9
|
import { registerPacksRoutes } from './routes/packs.js'
|
|
10
10
|
import { registerWebhookRoutes } from './routes/webhook.js'
|
|
11
11
|
import { registerVaultRoutes } from './routes/vault.js'
|
|
12
|
+
import { registerMetricsRoutes } from './routes/metrics.js'
|
|
12
13
|
import { registerPluginUpdater } from './updater.js'
|
|
13
14
|
|
|
14
15
|
export function registerRoutes(ctx, state) {
|
|
@@ -117,4 +118,5 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'
|
|
|
117
118
|
registerPacksRoutes(ctx)
|
|
118
119
|
registerWebhookRoutes(ctx, state)
|
|
119
120
|
registerVaultRoutes(ctx, state)
|
|
121
|
+
registerMetricsRoutes(ctx, state)
|
|
120
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|