@kryptosai/mcp-observatory 0.12.0 → 0.14.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/README.md +23 -15
- package/dist/src/cli.js +69 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/integrations/index.d.ts +1 -0
- package/dist/src/integrations/index.js +2 -0
- package/dist/src/integrations/index.js.map +1 -0
- package/dist/src/integrations/smithery.d.ts +96 -0
- package/dist/src/integrations/smithery.js +301 -0
- package/dist/src/integrations/smithery.js.map +1 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +3 -0
- package/dist/src/runtime/index.js.map +1 -0
- package/dist/src/runtime/monitor.d.ts +68 -0
- package/dist/src/runtime/monitor.js +162 -0
- package/dist/src/runtime/monitor.js.map +1 -0
- package/dist/src/runtime/wrapper.d.ts +28 -0
- package/dist/src/runtime/wrapper.js +30 -0
- package/dist/src/runtime/wrapper.js.map +1 -0
- package/dist/src/server.js +10 -10
- package/dist/src/server.js.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smithery Registry Integration
|
|
3
|
+
*
|
|
4
|
+
* Generates health-score submissions for the Smithery MCP server registry
|
|
5
|
+
* and resolves Smithery server listings into Observatory target configs.
|
|
6
|
+
*/
|
|
7
|
+
import { generateBadgeSvg } from "../badge.js";
|
|
8
|
+
import { computeHealthScore } from "../score.js";
|
|
9
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
10
|
+
const DEFAULT_BASE_URL = "https://registry.smithery.ai";
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
12
|
+
const RATE_LIMIT_DELAY_MS = 1_500;
|
|
13
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
14
|
+
function baseUrl(config) {
|
|
15
|
+
return config?.baseUrl?.replace(/\/+$/, "") ?? DEFAULT_BASE_URL;
|
|
16
|
+
}
|
|
17
|
+
function buildHeaders(config) {
|
|
18
|
+
const headers = {
|
|
19
|
+
Accept: "application/json",
|
|
20
|
+
"User-Agent": "mcp-observatory/smithery-integration",
|
|
21
|
+
};
|
|
22
|
+
if (config?.apiKey) {
|
|
23
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
24
|
+
}
|
|
25
|
+
return headers;
|
|
26
|
+
}
|
|
27
|
+
async function fetchJson(url, config) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
headers: buildHeaders(config),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
if (response.status === 429) {
|
|
36
|
+
throw new SmitheryRateLimitError("Smithery API rate limit exceeded. Please wait before retrying.");
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new SmitheryApiError(`Smithery API returned ${response.status}: ${response.statusText}`, response.status);
|
|
40
|
+
}
|
|
41
|
+
return (await response.json());
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Sleep helper for rate-limiting between batch requests. */
|
|
48
|
+
function sleep(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
// ── Errors ──────────────────────────────────────────────────────────────────
|
|
52
|
+
export class SmitheryApiError extends Error {
|
|
53
|
+
statusCode;
|
|
54
|
+
constructor(message, statusCode) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.statusCode = statusCode;
|
|
57
|
+
this.name = "SmitheryApiError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class SmitheryRateLimitError extends SmitheryApiError {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(message, 429);
|
|
63
|
+
this.name = "SmitheryRateLimitError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── Core Functions ──────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Generate a submission report for a server.
|
|
69
|
+
* Creates a structured JSON report that could be submitted to Smithery's API
|
|
70
|
+
* or included in a PR to their repository.
|
|
71
|
+
*/
|
|
72
|
+
export function generateSubmission(qualifiedName, artifact) {
|
|
73
|
+
const score = artifact.healthScore ?? computeHealthScore(artifact.checks, artifact.performanceMetrics);
|
|
74
|
+
const badgeSvg = generateBadgeSvg({
|
|
75
|
+
label: "MCP Health",
|
|
76
|
+
score: score.overall,
|
|
77
|
+
grade: score.grade,
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
qualifiedName,
|
|
81
|
+
artifact,
|
|
82
|
+
score,
|
|
83
|
+
badgeSvg,
|
|
84
|
+
submittedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Render a submission as markdown suitable for a Smithery integration PR.
|
|
89
|
+
*/
|
|
90
|
+
export function renderSubmissionMarkdown(submission) {
|
|
91
|
+
const { qualifiedName, artifact, score } = submission;
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(`# MCP Observatory Report: ${qualifiedName}`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(`> Scanned at ${artifact.createdAt} by MCP Observatory v${artifact.toolVersion}`);
|
|
96
|
+
lines.push("");
|
|
97
|
+
// Badge (data URI)
|
|
98
|
+
const encodedSvg = Buffer.from(submission.badgeSvg).toString("base64");
|
|
99
|
+
lines.push(``);
|
|
100
|
+
lines.push("");
|
|
101
|
+
// Overall score
|
|
102
|
+
lines.push("## Health Score");
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(`| Metric | Value |`);
|
|
105
|
+
lines.push(`|--------|-------|`);
|
|
106
|
+
lines.push(`| **Overall Score** | ${score.overall}/100 |`);
|
|
107
|
+
lines.push(`| **Grade** | ${score.grade} |`);
|
|
108
|
+
lines.push(`| **Gate** | ${artifact.gate} |`);
|
|
109
|
+
lines.push("");
|
|
110
|
+
// Dimensions
|
|
111
|
+
lines.push("## Score Breakdown");
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push("| Dimension | Weight | Score | Details |");
|
|
114
|
+
lines.push("|-----------|--------|-------|---------|");
|
|
115
|
+
for (const dim of score.dimensions) {
|
|
116
|
+
const details = dim.details.join("; ");
|
|
117
|
+
lines.push(`| ${dim.name} | ${Math.round(dim.weight * 100)}% | ${dim.score}/100 | ${details} |`);
|
|
118
|
+
}
|
|
119
|
+
lines.push("");
|
|
120
|
+
// Individual checks
|
|
121
|
+
lines.push("## Check Results");
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push("| Check | Status | Duration | Message |");
|
|
124
|
+
lines.push("|-------|--------|----------|---------|");
|
|
125
|
+
for (const check of artifact.checks) {
|
|
126
|
+
const icon = check.status === "pass" ? "pass" : check.status === "fail" ? "FAIL" : check.status;
|
|
127
|
+
lines.push(`| ${check.id} | ${icon} | ${check.durationMs}ms | ${check.message} |`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
// Performance metrics
|
|
131
|
+
if (artifact.performanceMetrics) {
|
|
132
|
+
const pm = artifact.performanceMetrics;
|
|
133
|
+
lines.push("## Performance");
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push(`- **Connect:** ${Math.round(pm.connectMs)}ms`);
|
|
136
|
+
if (pm.toolsListMs !== undefined)
|
|
137
|
+
lines.push(`- **tools/list:** ${Math.round(pm.toolsListMs)}ms`);
|
|
138
|
+
if (pm.promptsListMs !== undefined)
|
|
139
|
+
lines.push(`- **prompts/list:** ${Math.round(pm.promptsListMs)}ms`);
|
|
140
|
+
if (pm.resourcesListMs !== undefined)
|
|
141
|
+
lines.push(`- **resources/list:** ${Math.round(pm.resourcesListMs)}ms`);
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
// Server info
|
|
145
|
+
lines.push("## Server Information");
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push(`| Field | Value |`);
|
|
148
|
+
lines.push(`|-------|-------|`);
|
|
149
|
+
lines.push(`| **Target ID** | ${artifact.target.targetId} |`);
|
|
150
|
+
lines.push(`| **Adapter** | ${artifact.target.adapter} |`);
|
|
151
|
+
if (artifact.target.serverName) {
|
|
152
|
+
lines.push(`| **Server Name** | ${artifact.target.serverName} |`);
|
|
153
|
+
}
|
|
154
|
+
if (artifact.target.serverVersion) {
|
|
155
|
+
lines.push(`| **Server Version** | ${artifact.target.serverVersion} |`);
|
|
156
|
+
}
|
|
157
|
+
lines.push(`| **Platform** | ${artifact.environment.platform} |`);
|
|
158
|
+
lines.push(`| **Node** | ${artifact.environment.nodeVersion} |`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
// Fatal errors
|
|
161
|
+
if (artifact.fatalError) {
|
|
162
|
+
lines.push("## Fatal Error");
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("```");
|
|
165
|
+
lines.push(artifact.fatalError);
|
|
166
|
+
lines.push("```");
|
|
167
|
+
lines.push("");
|
|
168
|
+
}
|
|
169
|
+
lines.push("---");
|
|
170
|
+
lines.push("*Generated by [MCP Observatory](https://github.com/anthropics/mcp-observatory)*");
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a Smithery server listing to an Observatory target config.
|
|
175
|
+
* Fetches server metadata from Smithery and converts to TargetConfig.
|
|
176
|
+
*/
|
|
177
|
+
export async function resolveSmitheryTarget(qualifiedName, config) {
|
|
178
|
+
const url = `${baseUrl(config)}/api/servers/${encodeURIComponent(qualifiedName)}`;
|
|
179
|
+
const entry = await fetchJson(url, config);
|
|
180
|
+
return serverEntryToTarget(entry);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Fetch a page of servers from the Smithery registry.
|
|
184
|
+
*/
|
|
185
|
+
export async function listSmitheryServers(config, options) {
|
|
186
|
+
const params = new URLSearchParams();
|
|
187
|
+
if (options?.page !== undefined)
|
|
188
|
+
params.set("page", String(options.page));
|
|
189
|
+
if (options?.pageSize !== undefined)
|
|
190
|
+
params.set("pageSize", String(options.pageSize));
|
|
191
|
+
const qs = params.toString();
|
|
192
|
+
const url = `${baseUrl(config)}/api/servers${qs ? `?${qs}` : ""}`;
|
|
193
|
+
return fetchJson(url, config);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Batch-scan top N servers from the Smithery registry.
|
|
197
|
+
* Returns an array of submission results (or errors for individual servers).
|
|
198
|
+
*/
|
|
199
|
+
export async function batchScanServers(runTargetFn, config, options) {
|
|
200
|
+
const top = options?.top ?? 10;
|
|
201
|
+
const listResponse = await listSmitheryServers(config, { pageSize: top });
|
|
202
|
+
const servers = listResponse.servers.slice(0, top);
|
|
203
|
+
const results = [];
|
|
204
|
+
for (const server of servers) {
|
|
205
|
+
try {
|
|
206
|
+
const target = serverEntryToTarget(server);
|
|
207
|
+
const artifact = await runTargetFn(target);
|
|
208
|
+
const submission = generateSubmission(server.qualifiedName, artifact);
|
|
209
|
+
results.push({ qualifiedName: server.qualifiedName, submission });
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
results.push({
|
|
213
|
+
qualifiedName: server.qualifiedName,
|
|
214
|
+
error: err instanceof Error ? err.message : String(err),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Rate-limit between scans
|
|
218
|
+
await sleep(RATE_LIMIT_DELAY_MS);
|
|
219
|
+
}
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Render a batch scan report as markdown.
|
|
224
|
+
*/
|
|
225
|
+
export function renderBatchReportMarkdown(results) {
|
|
226
|
+
const lines = [];
|
|
227
|
+
lines.push("# MCP Observatory — Smithery Batch Scan Report");
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(`> Scanned ${results.length} servers at ${new Date().toISOString()}`);
|
|
230
|
+
lines.push("");
|
|
231
|
+
// Summary table
|
|
232
|
+
const successful = results.filter((r) => r.submission);
|
|
233
|
+
const failed = results.filter((r) => r.error);
|
|
234
|
+
lines.push("## Summary");
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push(`- **Scanned:** ${results.length}`);
|
|
237
|
+
lines.push(`- **Successful:** ${successful.length}`);
|
|
238
|
+
lines.push(`- **Failed:** ${failed.length}`);
|
|
239
|
+
lines.push("");
|
|
240
|
+
if (successful.length > 0) {
|
|
241
|
+
// Leaderboard
|
|
242
|
+
const sorted = successful
|
|
243
|
+
.map((r) => ({
|
|
244
|
+
name: r.qualifiedName,
|
|
245
|
+
score: r.submission.score.overall,
|
|
246
|
+
grade: r.submission.score.grade,
|
|
247
|
+
gate: r.submission.artifact.gate,
|
|
248
|
+
}))
|
|
249
|
+
.sort((a, b) => b.score - a.score);
|
|
250
|
+
lines.push("## Leaderboard");
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push("| Rank | Server | Score | Grade | Gate |");
|
|
253
|
+
lines.push("|------|--------|-------|-------|------|");
|
|
254
|
+
sorted.forEach((s, i) => {
|
|
255
|
+
lines.push(`| ${i + 1} | ${s.name} | ${s.score}/100 | ${s.grade} | ${s.gate} |`);
|
|
256
|
+
});
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
if (failed.length > 0) {
|
|
260
|
+
lines.push("## Errors");
|
|
261
|
+
lines.push("");
|
|
262
|
+
for (const f of failed) {
|
|
263
|
+
lines.push(`- **${f.qualifiedName}:** ${f.error}`);
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
lines.push("---");
|
|
268
|
+
lines.push("*Generated by [MCP Observatory](https://github.com/anthropics/mcp-observatory)*");
|
|
269
|
+
return lines.join("\n");
|
|
270
|
+
}
|
|
271
|
+
// ── Internal Helpers ────────────────────────────────────────────────────────
|
|
272
|
+
function serverEntryToTarget(entry) {
|
|
273
|
+
// Check for HTTP/SSE connections first
|
|
274
|
+
const httpConn = entry.connections?.find((c) => c.type === "http" || c.type === "sse" || c.url);
|
|
275
|
+
if (httpConn?.url) {
|
|
276
|
+
return {
|
|
277
|
+
targetId: entry.qualifiedName,
|
|
278
|
+
adapter: "http",
|
|
279
|
+
url: httpConn.url,
|
|
280
|
+
timeoutMs: 15_000,
|
|
281
|
+
metadata: {
|
|
282
|
+
smitheryName: entry.qualifiedName,
|
|
283
|
+
...(entry.displayName ? { displayName: entry.displayName } : {}),
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// Fall back to npx-based local-process target using the qualified name.
|
|
288
|
+
// Smithery packages typically run via `npx @scope/package-name`.
|
|
289
|
+
return {
|
|
290
|
+
targetId: entry.qualifiedName,
|
|
291
|
+
adapter: "local-process",
|
|
292
|
+
command: "npx",
|
|
293
|
+
args: ["-y", entry.qualifiedName],
|
|
294
|
+
timeoutMs: 30_000,
|
|
295
|
+
metadata: {
|
|
296
|
+
smitheryName: entry.qualifiedName,
|
|
297
|
+
...(entry.displayName ? { displayName: entry.displayName } : {}),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=smithery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smithery.js","sourceRoot":"","sources":["../../../src/integrations/smithery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,GAInB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,GAEhB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { ScoreWeights } from "../score.js";
|
|
3
|
+
import type { HealthGrade, HealthScore, TargetConfig } from "../types.js";
|
|
4
|
+
/** Options for configuring the ObservatoryMonitor. */
|
|
5
|
+
export interface MonitorOptions {
|
|
6
|
+
/** How often to check health (ms). Default: 60000 (1 min) */
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
/** Minimum score before triggering onDegraded. Default: 70 */
|
|
9
|
+
degradedThreshold?: number;
|
|
10
|
+
/** Minimum score before triggering onUnhealthy. Default: 40 */
|
|
11
|
+
unhealthyThreshold?: number;
|
|
12
|
+
/** Callback when score drops below degradedThreshold */
|
|
13
|
+
onDegraded?: (score: HealthScore, server: string) => void;
|
|
14
|
+
/** Callback when score drops below unhealthyThreshold */
|
|
15
|
+
onUnhealthy?: (score: HealthScore, server: string) => void;
|
|
16
|
+
/** Callback when score recovers above degradedThreshold */
|
|
17
|
+
onRecovered?: (score: HealthScore, server: string) => void;
|
|
18
|
+
/** Optional: report scores to a remote endpoint */
|
|
19
|
+
reportTo?: string;
|
|
20
|
+
/** Custom score weights */
|
|
21
|
+
weights?: Partial<ScoreWeights>;
|
|
22
|
+
}
|
|
23
|
+
export type ServerStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
|
|
24
|
+
/** Represents a server being monitored with its current state and history. */
|
|
25
|
+
export interface MonitoredServer {
|
|
26
|
+
targetId: string;
|
|
27
|
+
lastScore?: HealthScore;
|
|
28
|
+
lastCheck?: Date;
|
|
29
|
+
status: ServerStatus;
|
|
30
|
+
history: Array<{
|
|
31
|
+
timestamp: Date;
|
|
32
|
+
score: number;
|
|
33
|
+
grade: HealthGrade;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Real-time monitor for MCP server health.
|
|
38
|
+
*
|
|
39
|
+
* Uses `runTarget()` and `computeHealthScore()` under the hood to periodically
|
|
40
|
+
* check servers and fire callbacks when health changes.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ObservatoryMonitor extends EventEmitter {
|
|
43
|
+
private readonly servers;
|
|
44
|
+
private readonly options;
|
|
45
|
+
private timer;
|
|
46
|
+
private disposed;
|
|
47
|
+
constructor(options?: MonitorOptions);
|
|
48
|
+
/** Add a server to monitor. */
|
|
49
|
+
addServer(target: TargetConfig): void;
|
|
50
|
+
/** Remove a server from monitoring. */
|
|
51
|
+
removeServer(targetId: string): void;
|
|
52
|
+
/** Get current status of all monitored servers. */
|
|
53
|
+
getStatus(): MonitoredServer[];
|
|
54
|
+
/** Get status of a specific server. */
|
|
55
|
+
getServerStatus(targetId: string): MonitoredServer | undefined;
|
|
56
|
+
/** Run an immediate health check on all servers. */
|
|
57
|
+
checkNow(): Promise<Map<string, HealthScore>>;
|
|
58
|
+
/** Start periodic monitoring. */
|
|
59
|
+
start(): void;
|
|
60
|
+
/** Stop periodic monitoring. */
|
|
61
|
+
stop(): void;
|
|
62
|
+
/** Dispose and clean up. */
|
|
63
|
+
dispose(): void;
|
|
64
|
+
private checkServer;
|
|
65
|
+
private classifyScore;
|
|
66
|
+
private fireCallbacks;
|
|
67
|
+
private reportScore;
|
|
68
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { runTarget } from "../runner.js";
|
|
3
|
+
import { computeHealthScore } from "../score.js";
|
|
4
|
+
const MAX_HISTORY = 100;
|
|
5
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
6
|
+
const DEFAULT_DEGRADED_THRESHOLD = 70;
|
|
7
|
+
const DEFAULT_UNHEALTHY_THRESHOLD = 40;
|
|
8
|
+
/**
|
|
9
|
+
* Real-time monitor for MCP server health.
|
|
10
|
+
*
|
|
11
|
+
* Uses `runTarget()` and `computeHealthScore()` under the hood to periodically
|
|
12
|
+
* check servers and fire callbacks when health changes.
|
|
13
|
+
*/
|
|
14
|
+
export class ObservatoryMonitor extends EventEmitter {
|
|
15
|
+
servers = new Map();
|
|
16
|
+
options;
|
|
17
|
+
timer = null;
|
|
18
|
+
disposed = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super();
|
|
21
|
+
this.options = {
|
|
22
|
+
intervalMs: options?.intervalMs ?? DEFAULT_INTERVAL_MS,
|
|
23
|
+
degradedThreshold: options?.degradedThreshold ?? DEFAULT_DEGRADED_THRESHOLD,
|
|
24
|
+
unhealthyThreshold: options?.unhealthyThreshold ?? DEFAULT_UNHEALTHY_THRESHOLD,
|
|
25
|
+
...options,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Add a server to monitor. */
|
|
29
|
+
addServer(target) {
|
|
30
|
+
if (this.disposed)
|
|
31
|
+
return;
|
|
32
|
+
if (this.servers.has(target.targetId))
|
|
33
|
+
return;
|
|
34
|
+
const state = {
|
|
35
|
+
targetId: target.targetId,
|
|
36
|
+
status: "unknown",
|
|
37
|
+
history: [],
|
|
38
|
+
};
|
|
39
|
+
this.servers.set(target.targetId, { target, state });
|
|
40
|
+
this.emit("serverAdded", target.targetId);
|
|
41
|
+
}
|
|
42
|
+
/** Remove a server from monitoring. */
|
|
43
|
+
removeServer(targetId) {
|
|
44
|
+
this.servers.delete(targetId);
|
|
45
|
+
this.emit("serverRemoved", targetId);
|
|
46
|
+
}
|
|
47
|
+
/** Get current status of all monitored servers. */
|
|
48
|
+
getStatus() {
|
|
49
|
+
return [...this.servers.values()].map((e) => ({ ...e.state }));
|
|
50
|
+
}
|
|
51
|
+
/** Get status of a specific server. */
|
|
52
|
+
getServerStatus(targetId) {
|
|
53
|
+
const entry = this.servers.get(targetId);
|
|
54
|
+
return entry ? { ...entry.state } : undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Run an immediate health check on all servers. */
|
|
57
|
+
async checkNow() {
|
|
58
|
+
const results = new Map();
|
|
59
|
+
const entries = [...this.servers.values()];
|
|
60
|
+
await Promise.all(entries.map(async (entry) => {
|
|
61
|
+
const score = await this.checkServer(entry);
|
|
62
|
+
if (score) {
|
|
63
|
+
results.set(entry.target.targetId, score);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
/** Start periodic monitoring. */
|
|
69
|
+
start() {
|
|
70
|
+
if (this.disposed || this.timer !== null)
|
|
71
|
+
return;
|
|
72
|
+
this.timer = setInterval(() => {
|
|
73
|
+
void this.checkNow();
|
|
74
|
+
}, this.options.intervalMs);
|
|
75
|
+
this.emit("started");
|
|
76
|
+
}
|
|
77
|
+
/** Stop periodic monitoring. */
|
|
78
|
+
stop() {
|
|
79
|
+
if (this.timer !== null) {
|
|
80
|
+
clearInterval(this.timer);
|
|
81
|
+
this.timer = null;
|
|
82
|
+
this.emit("stopped");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Dispose and clean up. */
|
|
86
|
+
dispose() {
|
|
87
|
+
this.stop();
|
|
88
|
+
this.disposed = true;
|
|
89
|
+
this.servers.clear();
|
|
90
|
+
this.removeAllListeners();
|
|
91
|
+
}
|
|
92
|
+
// ---- internal ----
|
|
93
|
+
async checkServer(entry) {
|
|
94
|
+
const { target, state } = entry;
|
|
95
|
+
const previousStatus = state.status;
|
|
96
|
+
try {
|
|
97
|
+
const artifact = await runTarget(target);
|
|
98
|
+
const score = artifact.healthScore ??
|
|
99
|
+
computeHealthScore(artifact.checks, artifact.performanceMetrics, this.options.weights);
|
|
100
|
+
state.lastScore = score;
|
|
101
|
+
state.lastCheck = new Date();
|
|
102
|
+
// Append to history (capped at MAX_HISTORY)
|
|
103
|
+
state.history.push({
|
|
104
|
+
timestamp: state.lastCheck,
|
|
105
|
+
score: score.overall,
|
|
106
|
+
grade: score.grade,
|
|
107
|
+
});
|
|
108
|
+
if (state.history.length > MAX_HISTORY) {
|
|
109
|
+
state.history.splice(0, state.history.length - MAX_HISTORY);
|
|
110
|
+
}
|
|
111
|
+
// Determine new status
|
|
112
|
+
const newStatus = this.classifyScore(score.overall);
|
|
113
|
+
state.status = newStatus;
|
|
114
|
+
// Fire callbacks
|
|
115
|
+
this.fireCallbacks(previousStatus, newStatus, score, target.targetId);
|
|
116
|
+
// Report if configured
|
|
117
|
+
if (this.options.reportTo) {
|
|
118
|
+
void this.reportScore(score, target.targetId).catch(() => {
|
|
119
|
+
/* best-effort */
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
this.emit("check", target.targetId, score);
|
|
123
|
+
return score;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
state.status = "unhealthy";
|
|
127
|
+
state.lastCheck = new Date();
|
|
128
|
+
this.emit("checkError", target.targetId);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
classifyScore(overall) {
|
|
133
|
+
if (overall >= this.options.degradedThreshold)
|
|
134
|
+
return "healthy";
|
|
135
|
+
if (overall >= this.options.unhealthyThreshold)
|
|
136
|
+
return "degraded";
|
|
137
|
+
return "unhealthy";
|
|
138
|
+
}
|
|
139
|
+
fireCallbacks(previousStatus, newStatus, score, targetId) {
|
|
140
|
+
if (newStatus === "degraded" && previousStatus !== "degraded") {
|
|
141
|
+
this.options.onDegraded?.(score, targetId);
|
|
142
|
+
}
|
|
143
|
+
if (newStatus === "unhealthy" && previousStatus !== "unhealthy") {
|
|
144
|
+
this.options.onUnhealthy?.(score, targetId);
|
|
145
|
+
}
|
|
146
|
+
if (newStatus === "healthy" &&
|
|
147
|
+
(previousStatus === "degraded" || previousStatus === "unhealthy")) {
|
|
148
|
+
this.options.onRecovered?.(score, targetId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async reportScore(score, targetId) {
|
|
152
|
+
const url = this.options.reportTo;
|
|
153
|
+
if (!url)
|
|
154
|
+
return;
|
|
155
|
+
await fetch(url, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({ targetId, score, timestamp: new Date().toISOString() }),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor.js","sourceRoot":"","sources":["../../../src/runtime/monitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TargetConfig } from "../types.js";
|
|
2
|
+
import { ObservatoryMonitor } from "./monitor.js";
|
|
3
|
+
import type { MonitorOptions } from "./monitor.js";
|
|
4
|
+
/** Options for the Observatory wrapper, extending monitor options with fallback support. */
|
|
5
|
+
export interface WrapperOptions extends MonitorOptions {
|
|
6
|
+
/** Automatically switch to fallback server if primary degrades */
|
|
7
|
+
fallbackTargets?: TargetConfig[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wrap a target with observatory monitoring.
|
|
11
|
+
*
|
|
12
|
+
* Returns an `ObservatoryMonitor` instance pre-configured with the target and
|
|
13
|
+
* optional fallback targets. The monitor is NOT started automatically; call
|
|
14
|
+
* `.monitor.start()` when ready.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const { monitor, target } = withObservatory(
|
|
19
|
+
* { targetId: "my-server", adapter: "http", url: "http://localhost:3000" },
|
|
20
|
+
* { intervalMs: 30_000, onDegraded: (score) => console.warn("degraded", score) }
|
|
21
|
+
* );
|
|
22
|
+
* monitor.start();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function withObservatory(target: TargetConfig, options?: WrapperOptions): {
|
|
26
|
+
monitor: ObservatoryMonitor;
|
|
27
|
+
target: TargetConfig;
|
|
28
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ObservatoryMonitor } from "./monitor.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a target with observatory monitoring.
|
|
4
|
+
*
|
|
5
|
+
* Returns an `ObservatoryMonitor` instance pre-configured with the target and
|
|
6
|
+
* optional fallback targets. The monitor is NOT started automatically; call
|
|
7
|
+
* `.monitor.start()` when ready.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const { monitor, target } = withObservatory(
|
|
12
|
+
* { targetId: "my-server", adapter: "http", url: "http://localhost:3000" },
|
|
13
|
+
* { intervalMs: 30_000, onDegraded: (score) => console.warn("degraded", score) }
|
|
14
|
+
* );
|
|
15
|
+
* monitor.start();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function withObservatory(target, options) {
|
|
19
|
+
const monitor = new ObservatoryMonitor(options);
|
|
20
|
+
// Add the primary target
|
|
21
|
+
monitor.addServer(target);
|
|
22
|
+
// Add any fallback targets
|
|
23
|
+
if (options?.fallbackTargets) {
|
|
24
|
+
for (const fallback of options.fallbackTargets) {
|
|
25
|
+
monitor.addServer(fallback);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { monitor, target };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=wrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../../src/runtime/wrapper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASlD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAoB,EACpB,OAAwB;IAExB,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAEhD,yBAAyB;IACzB,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE1B,2BAA2B;IAC3B,IAAI,OAAO,EAAE,eAAe,EAAE,CAAC;QAC7B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YAC/C,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC"}
|