@thotischner/observability-mcp 1.0.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/config/sources.yaml +45 -0
- package/dist/analysis/anomaly.d.ts +24 -0
- package/dist/analysis/anomaly.js +50 -0
- package/dist/analysis/anomaly.test.d.ts +1 -0
- package/dist/analysis/anomaly.test.js +87 -0
- package/dist/analysis/correlator.d.ts +7 -0
- package/dist/analysis/correlator.js +31 -0
- package/dist/analysis/correlator.test.d.ts +1 -0
- package/dist/analysis/correlator.test.js +53 -0
- package/dist/analysis/health.d.ts +19 -0
- package/dist/analysis/health.js +34 -0
- package/dist/analysis/health.test.d.ts +1 -0
- package/dist/analysis/health.test.js +70 -0
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.js +81 -0
- package/dist/config/loader.test.d.ts +1 -0
- package/dist/config/loader.test.js +163 -0
- package/dist/connectors/interface.d.ts +17 -0
- package/dist/connectors/interface.js +1 -0
- package/dist/connectors/loki.d.ts +25 -0
- package/dist/connectors/loki.js +182 -0
- package/dist/connectors/loki.test.d.ts +1 -0
- package/dist/connectors/loki.test.js +111 -0
- package/dist/connectors/prometheus.d.ts +28 -0
- package/dist/connectors/prometheus.js +196 -0
- package/dist/connectors/prometheus.test.d.ts +1 -0
- package/dist/connectors/prometheus.test.js +103 -0
- package/dist/connectors/registry.d.ts +18 -0
- package/dist/connectors/registry.js +90 -0
- package/dist/connectors/registry.test.d.ts +1 -0
- package/dist/connectors/registry.test.js +93 -0
- package/dist/connectors/tls.d.ts +7 -0
- package/dist/connectors/tls.js +25 -0
- package/dist/connectors/tls.test.d.ts +1 -0
- package/dist/connectors/tls.test.js +99 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +421 -0
- package/dist/tools/detect-anomalies.d.ts +33 -0
- package/dist/tools/detect-anomalies.js +137 -0
- package/dist/tools/get-service-health.d.ts +25 -0
- package/dist/tools/get-service-health.js +111 -0
- package/dist/tools/handlers.test.d.ts +1 -0
- package/dist/tools/handlers.test.js +138 -0
- package/dist/tools/list-services.d.ts +22 -0
- package/dist/tools/list-services.js +57 -0
- package/dist/tools/list-sources.d.ts +15 -0
- package/dist/tools/list-sources.js +27 -0
- package/dist/tools/query-logs.d.ts +49 -0
- package/dist/tools/query-logs.js +93 -0
- package/dist/tools/query-metrics.d.ts +44 -0
- package/dist/tools/query-metrics.js +91 -0
- package/dist/tools/validation.d.ts +17 -0
- package/dist/tools/validation.js +45 -0
- package/dist/tools/validation.test.d.ts +1 -0
- package/dist/tools/validation.test.js +84 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.js +1 -0
- package/dist/ui/index.html +675 -0
- package/package.json +35 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { calculateHealthScore } from "../analysis/health.js";
|
|
2
|
+
import { detectRecentAnomaly } from "../analysis/anomaly.js";
|
|
3
|
+
let _thresholds = null;
|
|
4
|
+
export function setHealthThresholds(t) {
|
|
5
|
+
_thresholds = t;
|
|
6
|
+
}
|
|
7
|
+
export const getServiceHealthDefinition = {
|
|
8
|
+
name: "get_service_health",
|
|
9
|
+
description: "Get an aggregated health overview for a service combining metrics AND logs. Returns a health score (0-100), status (healthy/degraded/critical), key metric values, log error summary, detected anomalies, and cross-signal correlations.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
service: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Service name to check health for",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["service"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export async function getServiceHealthHandler(registry, args) {
|
|
22
|
+
const metricsConnectors = registry.getBySignal("metrics");
|
|
23
|
+
const logConnectors = registry.getBySignal("logs");
|
|
24
|
+
// Gather metrics
|
|
25
|
+
let cpu = 0, memory = 0, errorRate = 0, latencyP99 = 0;
|
|
26
|
+
const anomalies = [];
|
|
27
|
+
for (const connector of metricsConnectors) {
|
|
28
|
+
if (!connector.queryMetrics)
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const cpuResult = await connector.queryMetrics({ service: args.service, metric: "cpu", duration: "5m" });
|
|
32
|
+
cpu = cpuResult.summary.current;
|
|
33
|
+
checkAnomaly(cpuResult.values.map(v => v.value), "cpu", args.service, connector.name, anomalies);
|
|
34
|
+
const memResult = await connector.queryMetrics({ service: args.service, metric: "memory", duration: "5m" });
|
|
35
|
+
memory = memResult.summary.current / 1_000_000; // Convert to MB for display
|
|
36
|
+
const errResult = await connector.queryMetrics({ service: args.service, metric: "error_rate", duration: "5m" });
|
|
37
|
+
errorRate = errResult.summary.current;
|
|
38
|
+
checkAnomaly(errResult.values.map(v => v.value), "error_rate", args.service, connector.name, anomalies);
|
|
39
|
+
const latResult = await connector.queryMetrics({ service: args.service, metric: "latency_p99", duration: "5m" });
|
|
40
|
+
latencyP99 = latResult.summary.current;
|
|
41
|
+
checkAnomaly(latResult.values.map(v => v.value), "latency_p99", args.service, connector.name, anomalies);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(`Health check metrics failed for ${args.service}:`, err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Gather logs
|
|
48
|
+
let logErrorRate = 0;
|
|
49
|
+
let topErrors = [];
|
|
50
|
+
const correlations = [];
|
|
51
|
+
for (const connector of logConnectors) {
|
|
52
|
+
if (!connector.queryLogs)
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const logs = await connector.queryLogs({ service: args.service, duration: "5m", limit: 200 });
|
|
56
|
+
logErrorRate = logs.summary.errorCount; // errors in 5m window
|
|
57
|
+
topErrors = logs.summary.topPatterns;
|
|
58
|
+
// Cross-signal correlation
|
|
59
|
+
if (logErrorRate > 0 && anomalies.length > 0) {
|
|
60
|
+
correlations.push(`${anomalies.length} metric anomal${anomalies.length === 1 ? "y" : "ies"} detected alongside ${logErrorRate} error logs in the last 5 minutes`);
|
|
61
|
+
if (topErrors.length > 0) {
|
|
62
|
+
correlations.push(`Top error pattern: ${topErrors[0]}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(`Health check logs failed for ${args.service}:`, err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Calculate health score
|
|
71
|
+
const { DEFAULT_HEALTH_THRESHOLDS } = await import("../config/loader.js");
|
|
72
|
+
const health = calculateHealthScore({
|
|
73
|
+
cpu,
|
|
74
|
+
memory,
|
|
75
|
+
errorRate,
|
|
76
|
+
latencyP99,
|
|
77
|
+
logErrorRate,
|
|
78
|
+
}, _thresholds || DEFAULT_HEALTH_THRESHOLDS);
|
|
79
|
+
const result = {
|
|
80
|
+
service: args.service,
|
|
81
|
+
status: health.status,
|
|
82
|
+
score: health.score,
|
|
83
|
+
signals: {
|
|
84
|
+
metrics: { cpu, memory, errorRate, latencyP99 },
|
|
85
|
+
logs: { errorRate: logErrorRate, topErrors },
|
|
86
|
+
},
|
|
87
|
+
anomalies,
|
|
88
|
+
correlations,
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function checkAnomaly(values, metric, service, source, anomalies) {
|
|
95
|
+
const result = detectRecentAnomaly(values);
|
|
96
|
+
if (result.isAnomaly) {
|
|
97
|
+
const deviationPercent = result.baselineAvg === 0
|
|
98
|
+
? 100
|
|
99
|
+
: Math.round(((result.recentAvg - result.baselineAvg) / result.baselineAvg) * 100);
|
|
100
|
+
anomalies.push({
|
|
101
|
+
metric,
|
|
102
|
+
severity: Math.abs(result.zScore) >= 3 ? "high" : Math.abs(result.zScore) >= 2 ? "medium" : "low",
|
|
103
|
+
description: `${metric} is ${result.zScore.toFixed(1)}σ ${result.zScore > 0 ? "above" : "below"} baseline (${result.baselineAvg.toFixed(2)} → ${result.recentAvg.toFixed(2)})`,
|
|
104
|
+
currentValue: result.recentAvg,
|
|
105
|
+
baselineValue: result.baselineAvg,
|
|
106
|
+
deviationPercent,
|
|
107
|
+
source,
|
|
108
|
+
service,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { ConnectorRegistry } from "../connectors/registry.js";
|
|
4
|
+
import { listSourcesHandler } from "./list-sources.js";
|
|
5
|
+
import { listServicesHandler } from "./list-services.js";
|
|
6
|
+
// --- Mock Connector ---
|
|
7
|
+
function createMockConnector(overrides) {
|
|
8
|
+
return {
|
|
9
|
+
connect: async () => { },
|
|
10
|
+
disconnect: async () => { },
|
|
11
|
+
healthCheck: async () => ({ status: "up", latencyMs: 5 }),
|
|
12
|
+
getDefaultMetrics: () => [],
|
|
13
|
+
getMetrics: () => [],
|
|
14
|
+
listServices: async () => [],
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Helper to inject mock connectors into registry
|
|
19
|
+
function createRegistryWithMocks(mocks) {
|
|
20
|
+
const reg = new ConnectorRegistry();
|
|
21
|
+
// Inject directly via internal maps
|
|
22
|
+
for (const mock of mocks) {
|
|
23
|
+
reg.connectors.set(mock.name, mock);
|
|
24
|
+
reg.sourceConfigs.set(mock.name, {
|
|
25
|
+
name: mock.name, type: mock.type, url: "http://mock", enabled: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return reg;
|
|
29
|
+
}
|
|
30
|
+
describe("listSourcesHandler", () => {
|
|
31
|
+
it("returns empty sources for empty registry", async () => {
|
|
32
|
+
const reg = new ConnectorRegistry();
|
|
33
|
+
const result = await listSourcesHandler(reg);
|
|
34
|
+
const data = JSON.parse(result.content[0].text);
|
|
35
|
+
assert.deepEqual(data.sources, []);
|
|
36
|
+
});
|
|
37
|
+
it("returns sources with health status", async () => {
|
|
38
|
+
const reg = createRegistryWithMocks([
|
|
39
|
+
createMockConnector({
|
|
40
|
+
name: "prom1", type: "prometheus", signalType: "metrics",
|
|
41
|
+
healthCheck: async () => ({ status: "up", latencyMs: 12 }),
|
|
42
|
+
}),
|
|
43
|
+
createMockConnector({
|
|
44
|
+
name: "loki1", type: "loki", signalType: "logs",
|
|
45
|
+
healthCheck: async () => ({ status: "down", latencyMs: 0, message: "connection refused" }),
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
const result = await listSourcesHandler(reg);
|
|
49
|
+
const data = JSON.parse(result.content[0].text);
|
|
50
|
+
assert.equal(data.sources.length, 2);
|
|
51
|
+
assert.equal(data.sources[0].name, "prom1");
|
|
52
|
+
assert.equal(data.sources[0].status, "up");
|
|
53
|
+
assert.equal(data.sources[0].latencyMs, 12);
|
|
54
|
+
assert.equal(data.sources[1].name, "loki1");
|
|
55
|
+
assert.equal(data.sources[1].status, "down");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("listServicesHandler", () => {
|
|
59
|
+
it("returns empty for no connectors", async () => {
|
|
60
|
+
const reg = new ConnectorRegistry();
|
|
61
|
+
const result = await listServicesHandler(reg, {});
|
|
62
|
+
const data = JSON.parse(result.content[0].text);
|
|
63
|
+
assert.equal(data.total, 0);
|
|
64
|
+
assert.deepEqual(data.services, []);
|
|
65
|
+
});
|
|
66
|
+
it("deduplicates services from multiple connectors", async () => {
|
|
67
|
+
const reg = createRegistryWithMocks([
|
|
68
|
+
createMockConnector({
|
|
69
|
+
name: "prom1", type: "prometheus", signalType: "metrics",
|
|
70
|
+
listServices: async () => [
|
|
71
|
+
{ name: "api-gateway", source: "prom1", signalType: "metrics" },
|
|
72
|
+
{ name: "payment-service", source: "prom1", signalType: "metrics" },
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
createMockConnector({
|
|
76
|
+
name: "loki1", type: "loki", signalType: "logs",
|
|
77
|
+
listServices: async () => [
|
|
78
|
+
{ name: "api-gateway", source: "loki1", signalType: "logs" },
|
|
79
|
+
{ name: "order-service", source: "loki1", signalType: "logs" },
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
const result = await listServicesHandler(reg, {});
|
|
84
|
+
const data = JSON.parse(result.content[0].text);
|
|
85
|
+
assert.equal(data.total, 3);
|
|
86
|
+
const apiGw = data.services.find((s) => s.name === "api-gateway");
|
|
87
|
+
assert.ok(apiGw);
|
|
88
|
+
assert.deepEqual(apiGw.sources.sort(), ["loki1", "prom1"]);
|
|
89
|
+
assert.deepEqual(apiGw.signalTypes.sort(), ["logs", "metrics"]);
|
|
90
|
+
});
|
|
91
|
+
it("filters services case-insensitively", async () => {
|
|
92
|
+
const reg = createRegistryWithMocks([
|
|
93
|
+
createMockConnector({
|
|
94
|
+
name: "prom1", type: "prometheus", signalType: "metrics",
|
|
95
|
+
listServices: async () => [
|
|
96
|
+
{ name: "API-Gateway", source: "prom1", signalType: "metrics" },
|
|
97
|
+
{ name: "payment-service", source: "prom1", signalType: "metrics" },
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
const result = await listServicesHandler(reg, { filter: "api" });
|
|
102
|
+
const data = JSON.parse(result.content[0].text);
|
|
103
|
+
assert.equal(data.total, 1);
|
|
104
|
+
assert.equal(data.services[0].name, "API-Gateway");
|
|
105
|
+
});
|
|
106
|
+
it("handles connector errors gracefully", async () => {
|
|
107
|
+
const reg = createRegistryWithMocks([
|
|
108
|
+
createMockConnector({
|
|
109
|
+
name: "prom1", type: "prometheus", signalType: "metrics",
|
|
110
|
+
listServices: async () => { throw new Error("connection failed"); },
|
|
111
|
+
}),
|
|
112
|
+
createMockConnector({
|
|
113
|
+
name: "loki1", type: "loki", signalType: "logs",
|
|
114
|
+
listServices: async () => [
|
|
115
|
+
{ name: "order-service", source: "loki1", signalType: "logs" },
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
]);
|
|
119
|
+
const result = await listServicesHandler(reg, {});
|
|
120
|
+
const data = JSON.parse(result.content[0].text);
|
|
121
|
+
// Should still return results from working connector
|
|
122
|
+
assert.equal(data.total, 1);
|
|
123
|
+
assert.equal(data.services[0].name, "order-service");
|
|
124
|
+
});
|
|
125
|
+
it("returns empty when filter matches nothing", async () => {
|
|
126
|
+
const reg = createRegistryWithMocks([
|
|
127
|
+
createMockConnector({
|
|
128
|
+
name: "prom1", type: "prometheus", signalType: "metrics",
|
|
129
|
+
listServices: async () => [
|
|
130
|
+
{ name: "payment-service", source: "prom1", signalType: "metrics" },
|
|
131
|
+
],
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
const result = await listServicesHandler(reg, { filter: "nonexistent" });
|
|
135
|
+
const data = JSON.parse(result.content[0].text);
|
|
136
|
+
assert.equal(data.total, 0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ConnectorRegistry } from "../connectors/registry.js";
|
|
2
|
+
export declare const listServicesDefinition: {
|
|
3
|
+
name: "list_services";
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
filter: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function listServicesHandler(registry: ConnectorRegistry, args: {
|
|
16
|
+
filter?: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const listServicesDefinition = {
|
|
2
|
+
name: "list_services",
|
|
3
|
+
description: "List all monitored services discovered across all connected backends. Returns service names, their data sources, and signal types (metrics/logs).",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
filter: {
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "Optional filter to match service names",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export async function listServicesHandler(registry, args) {
|
|
15
|
+
const connectors = registry.getAll();
|
|
16
|
+
const allServices = [];
|
|
17
|
+
for (const connector of connectors) {
|
|
18
|
+
try {
|
|
19
|
+
const services = await connector.listServices();
|
|
20
|
+
allServices.push(...services);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error(`Failed to list services from ${connector.name}:`, err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Deduplicate by name, merge signal types
|
|
27
|
+
const merged = new Map();
|
|
28
|
+
for (const svc of allServices) {
|
|
29
|
+
const existing = merged.get(svc.name);
|
|
30
|
+
if (existing) {
|
|
31
|
+
if (!existing.sources.includes(svc.source))
|
|
32
|
+
existing.sources.push(svc.source);
|
|
33
|
+
if (!existing.signalTypes.includes(svc.signalType))
|
|
34
|
+
existing.signalTypes.push(svc.signalType);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
merged.set(svc.name, {
|
|
38
|
+
name: svc.name,
|
|
39
|
+
sources: [svc.source],
|
|
40
|
+
signalTypes: [svc.signalType],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let services = Array.from(merged.values());
|
|
45
|
+
if (args.filter) {
|
|
46
|
+
const f = args.filter.toLowerCase();
|
|
47
|
+
services = services.filter((s) => s.name.toLowerCase().includes(f));
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: JSON.stringify({ services, total: services.length }, null, 2),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConnectorRegistry } from "../connectors/registry.js";
|
|
2
|
+
export declare const listSourcesDefinition: {
|
|
3
|
+
name: "list_sources";
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {};
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare function listSourcesHandler(registry: ConnectorRegistry): Promise<{
|
|
11
|
+
content: {
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const listSourcesDefinition = {
|
|
2
|
+
name: "list_sources",
|
|
3
|
+
description: "List all configured observability backends and their connection status. Use this to discover what data sources are available.",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {},
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
export async function listSourcesHandler(registry) {
|
|
10
|
+
const healthResults = await registry.healthCheckAll();
|
|
11
|
+
const connectors = registry.getAll();
|
|
12
|
+
const sources = connectors.map((c) => ({
|
|
13
|
+
name: c.name,
|
|
14
|
+
type: c.type,
|
|
15
|
+
signalType: c.signalType,
|
|
16
|
+
status: healthResults[c.name]?.status || "unknown",
|
|
17
|
+
latencyMs: healthResults[c.name]?.latencyMs,
|
|
18
|
+
}));
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: JSON.stringify({ sources }, null, 2),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConnectorRegistry } from "../connectors/registry.js";
|
|
2
|
+
export declare const queryLogsDefinition: {
|
|
3
|
+
name: "query_logs";
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
service: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
query: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
duration: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
level: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
limit: {
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
required: string[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export declare function queryLogsHandler(registry: ConnectorRegistry, args: {
|
|
33
|
+
service: string;
|
|
34
|
+
query?: string;
|
|
35
|
+
duration?: string;
|
|
36
|
+
level?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
content: {
|
|
40
|
+
type: "text";
|
|
41
|
+
text: string;
|
|
42
|
+
}[];
|
|
43
|
+
isError: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
content: {
|
|
46
|
+
type: "text";
|
|
47
|
+
text: string;
|
|
48
|
+
}[];
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { validateDuration, validateServiceName, errorResponse } from "./validation.js";
|
|
2
|
+
export const queryLogsDefinition = {
|
|
3
|
+
name: "query_logs",
|
|
4
|
+
description: "Query logs for a service over a given timeframe. Returns log entries with a summary including error/warning counts and top error patterns. Supports filtering by log level and search query.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
service: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "Service name (e.g. 'payment-service')",
|
|
11
|
+
},
|
|
12
|
+
query: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Optional search query to filter log messages (regex supported)",
|
|
15
|
+
},
|
|
16
|
+
duration: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Time range to query (e.g. '5m', '1h', '24h'). Default: '5m'",
|
|
19
|
+
},
|
|
20
|
+
level: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Filter by log level: 'error', 'warn', 'info', 'debug'",
|
|
23
|
+
},
|
|
24
|
+
limit: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Maximum number of log entries to return. Default: 100",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["service"],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export async function queryLogsHandler(registry, args) {
|
|
33
|
+
const svcErr = validateServiceName(args.service);
|
|
34
|
+
if (svcErr)
|
|
35
|
+
return errorResponse(svcErr);
|
|
36
|
+
const duration = args.duration || "5m";
|
|
37
|
+
const durationErr = validateDuration(duration);
|
|
38
|
+
if (durationErr)
|
|
39
|
+
return errorResponse(durationErr);
|
|
40
|
+
const connectors = registry.getBySignal("logs");
|
|
41
|
+
if (connectors.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{ type: "text", text: JSON.stringify({ error: "No log backends configured" }) },
|
|
45
|
+
],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const results = [];
|
|
50
|
+
const errors = [];
|
|
51
|
+
for (const connector of connectors) {
|
|
52
|
+
if (!connector.queryLogs)
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const result = await connector.queryLogs({
|
|
56
|
+
service: args.service,
|
|
57
|
+
query: args.query,
|
|
58
|
+
duration,
|
|
59
|
+
level: args.level,
|
|
60
|
+
limit: args.limit,
|
|
61
|
+
});
|
|
62
|
+
results.push(result);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
+
console.error(`Log query failed on ${connector.name}:`, msg);
|
|
67
|
+
errors.push(`${connector.name}: ${msg}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (results.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
error: errors.length > 0 ? `Query failed: ${errors.join("; ")}` : "No logs returned",
|
|
77
|
+
service: args.service,
|
|
78
|
+
duration,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
isError: errors.length > 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify(results.length === 1 ? results[0] : results, null, 2),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ConnectorRegistry } from "../connectors/registry.js";
|
|
2
|
+
export declare const queryMetricsDefinition: {
|
|
3
|
+
name: "query_metrics";
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {
|
|
8
|
+
service: {
|
|
9
|
+
type: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
metric: {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
duration: {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
source: {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
required: string[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare function queryMetricsHandler(registry: ConnectorRegistry, args: {
|
|
29
|
+
service: string;
|
|
30
|
+
metric: string;
|
|
31
|
+
duration?: string;
|
|
32
|
+
source?: string;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
content: {
|
|
35
|
+
type: "text";
|
|
36
|
+
text: string;
|
|
37
|
+
}[];
|
|
38
|
+
isError: boolean;
|
|
39
|
+
} | {
|
|
40
|
+
content: {
|
|
41
|
+
type: "text";
|
|
42
|
+
text: string;
|
|
43
|
+
}[];
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { validateDuration, validateMetricName, validateServiceName, errorResponse } from "./validation.js";
|
|
2
|
+
export const queryMetricsDefinition = {
|
|
3
|
+
name: "query_metrics",
|
|
4
|
+
description: "Query a specific metric for a service over a given timeframe. Returns time-series data with pre-computed summary statistics (current, average, min, max, trend). Available metrics: cpu, memory, error_rate, request_rate, latency_p99, latency_p50, latency_avg.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
service: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "Service name (e.g. 'api-gateway', 'payment-service')",
|
|
11
|
+
},
|
|
12
|
+
metric: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Metric name: cpu, memory, error_rate, request_rate, latency_p99, latency_p50, latency_avg",
|
|
15
|
+
},
|
|
16
|
+
duration: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Time range to query (e.g. '5m', '1h', '24h'). Default: '5m'",
|
|
19
|
+
},
|
|
20
|
+
source: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Specific source name to query. If omitted, queries all metrics backends.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["service", "metric"],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export async function queryMetricsHandler(registry, args) {
|
|
29
|
+
const svcErr = validateServiceName(args.service);
|
|
30
|
+
if (svcErr)
|
|
31
|
+
return errorResponse(svcErr);
|
|
32
|
+
const duration = args.duration || "5m";
|
|
33
|
+
const durationErr = validateDuration(duration);
|
|
34
|
+
if (durationErr)
|
|
35
|
+
return errorResponse(durationErr);
|
|
36
|
+
const metricErr = validateMetricName(args.metric, registry);
|
|
37
|
+
if (metricErr)
|
|
38
|
+
return errorResponse(metricErr);
|
|
39
|
+
const connectors = args.source
|
|
40
|
+
? [registry.getByName(args.source)].filter(Boolean)
|
|
41
|
+
: registry.getBySignal("metrics");
|
|
42
|
+
if (connectors.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: JSON.stringify({ error: "No metrics backends configured" }) }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const results = [];
|
|
49
|
+
const errors = [];
|
|
50
|
+
for (const connector of connectors) {
|
|
51
|
+
if (!connector?.queryMetrics)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const result = await connector.queryMetrics({
|
|
55
|
+
service: args.service,
|
|
56
|
+
metric: args.metric,
|
|
57
|
+
duration,
|
|
58
|
+
});
|
|
59
|
+
results.push(result);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
console.error(`Metrics query failed on ${connector.name}:`, msg);
|
|
64
|
+
errors.push(`${connector.name}: ${msg}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (results.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
error: errors.length > 0 ? `Query failed: ${errors.join("; ")}` : "No data returned",
|
|
74
|
+
service: args.service,
|
|
75
|
+
metric: args.metric,
|
|
76
|
+
duration,
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
isError: errors.length > 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: JSON.stringify(results.length === 1 ? results[0] : results, null, 2),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectorRegistry } from "../connectors/registry.js";
|
|
2
|
+
export declare function validateDuration(duration: string): string | null;
|
|
3
|
+
export declare function validateMetricName(metric: string, registry: ConnectorRegistry): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize a label value for use in PromQL/LogQL queries.
|
|
6
|
+
* Only allows alphanumeric, hyphens, underscores, dots, colons.
|
|
7
|
+
* Rejects anything that could be used for injection.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeLabelValue(value: string): string | null;
|
|
10
|
+
export declare function validateServiceName(service: string): string | null;
|
|
11
|
+
export declare function errorResponse(message: string): {
|
|
12
|
+
content: {
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
isError: boolean;
|
|
17
|
+
};
|