@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.
Files changed (59) hide show
  1. package/config/sources.yaml +45 -0
  2. package/dist/analysis/anomaly.d.ts +24 -0
  3. package/dist/analysis/anomaly.js +50 -0
  4. package/dist/analysis/anomaly.test.d.ts +1 -0
  5. package/dist/analysis/anomaly.test.js +87 -0
  6. package/dist/analysis/correlator.d.ts +7 -0
  7. package/dist/analysis/correlator.js +31 -0
  8. package/dist/analysis/correlator.test.d.ts +1 -0
  9. package/dist/analysis/correlator.test.js +53 -0
  10. package/dist/analysis/health.d.ts +19 -0
  11. package/dist/analysis/health.js +34 -0
  12. package/dist/analysis/health.test.d.ts +1 -0
  13. package/dist/analysis/health.test.js +70 -0
  14. package/dist/config/loader.d.ts +5 -0
  15. package/dist/config/loader.js +81 -0
  16. package/dist/config/loader.test.d.ts +1 -0
  17. package/dist/config/loader.test.js +163 -0
  18. package/dist/connectors/interface.d.ts +17 -0
  19. package/dist/connectors/interface.js +1 -0
  20. package/dist/connectors/loki.d.ts +25 -0
  21. package/dist/connectors/loki.js +182 -0
  22. package/dist/connectors/loki.test.d.ts +1 -0
  23. package/dist/connectors/loki.test.js +111 -0
  24. package/dist/connectors/prometheus.d.ts +28 -0
  25. package/dist/connectors/prometheus.js +196 -0
  26. package/dist/connectors/prometheus.test.d.ts +1 -0
  27. package/dist/connectors/prometheus.test.js +103 -0
  28. package/dist/connectors/registry.d.ts +18 -0
  29. package/dist/connectors/registry.js +90 -0
  30. package/dist/connectors/registry.test.d.ts +1 -0
  31. package/dist/connectors/registry.test.js +93 -0
  32. package/dist/connectors/tls.d.ts +7 -0
  33. package/dist/connectors/tls.js +25 -0
  34. package/dist/connectors/tls.test.d.ts +1 -0
  35. package/dist/connectors/tls.test.js +99 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +421 -0
  38. package/dist/tools/detect-anomalies.d.ts +33 -0
  39. package/dist/tools/detect-anomalies.js +137 -0
  40. package/dist/tools/get-service-health.d.ts +25 -0
  41. package/dist/tools/get-service-health.js +111 -0
  42. package/dist/tools/handlers.test.d.ts +1 -0
  43. package/dist/tools/handlers.test.js +138 -0
  44. package/dist/tools/list-services.d.ts +22 -0
  45. package/dist/tools/list-services.js +57 -0
  46. package/dist/tools/list-sources.d.ts +15 -0
  47. package/dist/tools/list-sources.js +27 -0
  48. package/dist/tools/query-logs.d.ts +49 -0
  49. package/dist/tools/query-logs.js +93 -0
  50. package/dist/tools/query-metrics.d.ts +44 -0
  51. package/dist/tools/query-metrics.js +91 -0
  52. package/dist/tools/validation.d.ts +17 -0
  53. package/dist/tools/validation.js +45 -0
  54. package/dist/tools/validation.test.d.ts +1 -0
  55. package/dist/tools/validation.test.js +84 -0
  56. package/dist/types.d.ts +171 -0
  57. package/dist/types.js +1 -0
  58. package/dist/ui/index.html +675 -0
  59. package/package.json +35 -0
@@ -0,0 +1,163 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ // We test the helper functions by importing the module fresh with different env vars.
7
+ // Since the config path is resolved at import time, we use dynamic imports.
8
+ const TMP_DIR = join(tmpdir(), "observability-mcp-test-" + Date.now());
9
+ describe("config/loader", () => {
10
+ beforeEach(() => {
11
+ mkdirSync(TMP_DIR, { recursive: true });
12
+ });
13
+ afterEach(() => {
14
+ rmSync(TMP_DIR, { recursive: true, force: true });
15
+ delete process.env.CONFIG_PATH;
16
+ delete process.env.PROMETHEUS_URL;
17
+ delete process.env.LOKI_URL;
18
+ });
19
+ describe("loadConfig with CONFIG_PATH", () => {
20
+ it("loads sources from YAML file", async () => {
21
+ const configPath = join(TMP_DIR, "sources.yaml");
22
+ writeFileSync(configPath, `
23
+ sources:
24
+ - name: prom1
25
+ type: prometheus
26
+ url: http://localhost:9090
27
+ enabled: true
28
+ `);
29
+ process.env.CONFIG_PATH = configPath;
30
+ // Dynamic import to pick up new env
31
+ const mod = await import("./loader.js?" + Date.now());
32
+ // loadConfig is already called at module level for CONFIG_PATH, but we call it again
33
+ const config = mod.loadConfig();
34
+ assert.equal(config.sources.length, 1);
35
+ assert.equal(config.sources[0].name, "prom1");
36
+ assert.equal(config.sources[0].url, "http://localhost:9090");
37
+ });
38
+ it("falls back to env vars when config file does not exist", async () => {
39
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
40
+ process.env.PROMETHEUS_URL = "http://p1:9090";
41
+ process.env.LOKI_URL = "http://l1:3100";
42
+ const mod = await import("./loader.js?" + Date.now());
43
+ const config = mod.loadConfig();
44
+ assert.equal(config.sources.length, 2);
45
+ assert.equal(config.sources[0].name, "prometheus");
46
+ assert.equal(config.sources[0].url, "http://p1:9090");
47
+ assert.equal(config.sources[1].name, "loki");
48
+ assert.equal(config.sources[1].url, "http://l1:3100");
49
+ });
50
+ it("returns empty sources when no config and no env vars", async () => {
51
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
52
+ const mod = await import("./loader.js?" + Date.now());
53
+ const config = mod.loadConfig();
54
+ assert.equal(config.sources.length, 0);
55
+ });
56
+ });
57
+ describe("env var parsing - comma-separated URLs", () => {
58
+ it("creates multiple prometheus sources from comma-separated URLs", async () => {
59
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
60
+ process.env.PROMETHEUS_URL = "http://p1:9090,http://p2:9090,http://p3:9090";
61
+ const mod = await import("./loader.js?" + Date.now());
62
+ const config = mod.loadConfig();
63
+ assert.equal(config.sources.length, 3);
64
+ assert.equal(config.sources[0].name, "prometheus-1");
65
+ assert.equal(config.sources[0].url, "http://p1:9090");
66
+ assert.equal(config.sources[1].name, "prometheus-2");
67
+ assert.equal(config.sources[1].url, "http://p2:9090");
68
+ assert.equal(config.sources[2].name, "prometheus-3");
69
+ assert.equal(config.sources[2].url, "http://p3:9090");
70
+ });
71
+ it("uses plain name for single URL (no suffix)", async () => {
72
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
73
+ process.env.PROMETHEUS_URL = "http://p1:9090";
74
+ const mod = await import("./loader.js?" + Date.now());
75
+ const config = mod.loadConfig();
76
+ assert.equal(config.sources.length, 1);
77
+ assert.equal(config.sources[0].name, "prometheus");
78
+ });
79
+ it("trims whitespace from URLs", async () => {
80
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
81
+ process.env.LOKI_URL = " http://l1:3100 , http://l2:3100 ";
82
+ const mod = await import("./loader.js?" + Date.now());
83
+ const config = mod.loadConfig();
84
+ assert.equal(config.sources.length, 2);
85
+ assert.equal(config.sources[0].url, "http://l1:3100");
86
+ assert.equal(config.sources[1].url, "http://l2:3100");
87
+ });
88
+ it("combines prometheus and loki sources", async () => {
89
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
90
+ process.env.PROMETHEUS_URL = "http://p1:9090,http://p2:9090";
91
+ process.env.LOKI_URL = "http://l1:3100";
92
+ const mod = await import("./loader.js?" + Date.now());
93
+ const config = mod.loadConfig();
94
+ assert.equal(config.sources.length, 3);
95
+ assert.equal(config.sources[0].name, "prometheus-1");
96
+ assert.equal(config.sources[1].name, "prometheus-2");
97
+ assert.equal(config.sources[2].name, "loki");
98
+ });
99
+ });
100
+ describe("saveConfig", () => {
101
+ it("creates directory and writes YAML file", async () => {
102
+ const configPath = join(TMP_DIR, "sub", "dir", "sources.yaml");
103
+ process.env.CONFIG_PATH = configPath;
104
+ const mod = await import("./loader.js?" + Date.now());
105
+ const config = mod.loadConfig();
106
+ config.sources.push({ name: "test", type: "prometheus", url: "http://test:9090", enabled: true });
107
+ mod.saveConfig(config);
108
+ assert.ok(existsSync(configPath));
109
+ // Re-load and verify
110
+ const reloaded = mod.loadConfig();
111
+ assert.equal(reloaded.sources.length, 1);
112
+ assert.equal(reloaded.sources[0].name, "test");
113
+ });
114
+ });
115
+ describe("default settings", () => {
116
+ it("has correct defaults", async () => {
117
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
118
+ const mod = await import("./loader.js?" + Date.now());
119
+ const config = mod.loadConfig();
120
+ assert.equal(config.settings.checkIntervalMs, 30000);
121
+ assert.equal(config.settings.defaultSensitivity, "medium");
122
+ });
123
+ it("has correct default health thresholds", async () => {
124
+ process.env.CONFIG_PATH = join(TMP_DIR, "nonexistent.yaml");
125
+ const mod = await import("./loader.js?" + Date.now());
126
+ const config = mod.loadConfig();
127
+ assert.equal(config.healthThresholds.weights.errorRate, 0.35);
128
+ assert.equal(config.healthThresholds.cpu.crit, 95);
129
+ assert.equal(config.healthThresholds.statusBoundaries.healthy, 80);
130
+ });
131
+ });
132
+ describe("config merging", () => {
133
+ it("merges partial settings with defaults", async () => {
134
+ const configPath = join(TMP_DIR, "partial.yaml");
135
+ writeFileSync(configPath, `
136
+ sources: []
137
+ settings:
138
+ checkIntervalMs: 60000
139
+ `);
140
+ process.env.CONFIG_PATH = configPath;
141
+ const mod = await import("./loader.js?" + Date.now());
142
+ const config = mod.loadConfig();
143
+ assert.equal(config.settings.checkIntervalMs, 60000);
144
+ assert.equal(config.settings.defaultSensitivity, "medium"); // default preserved
145
+ });
146
+ it("deep-merges health thresholds", async () => {
147
+ const configPath = join(TMP_DIR, "thresholds.yaml");
148
+ writeFileSync(configPath, `
149
+ sources: []
150
+ healthThresholds:
151
+ cpu:
152
+ crit: 99
153
+ `);
154
+ process.env.CONFIG_PATH = configPath;
155
+ const mod = await import("./loader.js?" + Date.now());
156
+ const config = mod.loadConfig();
157
+ assert.equal(config.healthThresholds.cpu.crit, 99); // overridden
158
+ assert.equal(config.healthThresholds.cpu.good, 50); // default preserved
159
+ assert.equal(config.healthThresholds.cpu.warn, 80); // default preserved
160
+ assert.equal(config.healthThresholds.weights.errorRate, 0.35); // other sections preserved
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,17 @@
1
+ import type { SignalType, ConnectorHealth, ServiceInfo, MetricInfo, MetricQuery, MetricResult, LogQuery, LogResult, SourceConfig, MetricDefinition } from "../types.js";
2
+ export interface ObservabilityConnector {
3
+ readonly name: string;
4
+ readonly type: string;
5
+ readonly signalType: SignalType;
6
+ connect(config: SourceConfig): Promise<void>;
7
+ healthCheck(): Promise<ConnectorHealth>;
8
+ disconnect(): Promise<void>;
9
+ /** Returns the default metric definitions for this connector type */
10
+ getDefaultMetrics(): MetricDefinition[];
11
+ /** Returns the active metrics (user-configured or defaults) */
12
+ getMetrics(): MetricDefinition[];
13
+ listServices(): Promise<ServiceInfo[]>;
14
+ listAvailableMetrics?(service: string): Promise<MetricInfo[]>;
15
+ queryMetrics?(params: MetricQuery): Promise<MetricResult>;
16
+ queryLogs?(params: LogQuery): Promise<LogResult>;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { ObservabilityConnector } from "./interface.js";
2
+ import type { SourceConfig, ConnectorHealth, ServiceInfo, MetricDefinition, LogQuery, LogResult, SignalType } from "../types.js";
3
+ export declare class LokiConnector implements ObservabilityConnector {
4
+ readonly type = "loki";
5
+ readonly signalType: SignalType;
6
+ name: string;
7
+ private baseUrl;
8
+ private auth?;
9
+ private tlsAgent?;
10
+ connect(config: SourceConfig): Promise<void>;
11
+ getDefaultMetrics(): MetricDefinition[];
12
+ getMetrics(): MetricDefinition[];
13
+ private fetchOptions;
14
+ healthCheck(): Promise<ConnectorHealth>;
15
+ disconnect(): Promise<void>;
16
+ listServices(): Promise<ServiceInfo[]>;
17
+ queryLogs(params: LogQuery): Promise<LogResult>;
18
+ private parseLine;
19
+ private extractTopPatterns;
20
+ private parseTimeRange;
21
+ private escapeLogQLValue;
22
+ private escapeLogQLRegex;
23
+ private buildAuthHeaders;
24
+ private apiGet;
25
+ }
@@ -0,0 +1,182 @@
1
+ import { buildTlsAgent } from "./tls.js";
2
+ export class LokiConnector {
3
+ type = "loki";
4
+ signalType = "logs";
5
+ name = "";
6
+ baseUrl = "";
7
+ auth;
8
+ tlsAgent;
9
+ async connect(config) {
10
+ this.name = config.name;
11
+ this.baseUrl = config.url.replace(/\/$/, "");
12
+ this.auth = config.auth;
13
+ this.tlsAgent = buildTlsAgent(config);
14
+ }
15
+ getDefaultMetrics() {
16
+ // Loki is a log backend — no metric definitions by default
17
+ return [];
18
+ }
19
+ getMetrics() {
20
+ return [];
21
+ }
22
+ fetchOptions() {
23
+ const opts = { headers: this.buildAuthHeaders() };
24
+ if (this.tlsAgent) {
25
+ // @ts-expect-error Node.js extension for native fetch
26
+ opts.dispatcher = this.tlsAgent;
27
+ }
28
+ return opts;
29
+ }
30
+ async healthCheck() {
31
+ const start = Date.now();
32
+ try {
33
+ const res = await fetch(`${this.baseUrl}/ready`, this.fetchOptions());
34
+ const text = await res.text();
35
+ const isReady = res.ok && text.trim() === "ready";
36
+ return {
37
+ status: isReady ? "up" : "down",
38
+ latencyMs: Date.now() - start,
39
+ message: isReady ? "Loki is ready" : `HTTP ${res.status}: ${text}`,
40
+ };
41
+ }
42
+ catch (err) {
43
+ return { status: "down", latencyMs: Date.now() - start, message: String(err) };
44
+ }
45
+ }
46
+ async disconnect() { }
47
+ async listServices() {
48
+ try {
49
+ const data = await this.apiGet("/loki/api/v1/label/service/values");
50
+ return (data?.data || []).map((name) => ({
51
+ name,
52
+ source: this.name,
53
+ signalType: "logs",
54
+ }));
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
60
+ async queryLogs(params) {
61
+ const { start, end } = this.parseTimeRange(params.duration);
62
+ const limit = Math.min(Math.max(params.limit || 100, 1), 1000);
63
+ const service = this.escapeLogQLValue(params.service);
64
+ let logql = `{service="${service}"}`;
65
+ if (params.level) {
66
+ const level = this.escapeLogQLValue(params.level);
67
+ logql += ` | json | level="${level}"`;
68
+ }
69
+ else {
70
+ logql += ` | json`;
71
+ }
72
+ if (params.query) {
73
+ const query = this.escapeLogQLRegex(params.query);
74
+ logql += ` |~ \`${query}\``;
75
+ }
76
+ const url = `/loki/api/v1/query_range?query=${encodeURIComponent(logql)}` +
77
+ `&start=${start}000000000&end=${end}000000000&limit=${limit}`;
78
+ const data = await this.apiGet(url);
79
+ const entries = [];
80
+ for (const stream of data?.data?.result || []) {
81
+ const labels = stream.stream;
82
+ for (const [ts, line] of stream.values) {
83
+ const parsed = this.parseLine(line);
84
+ entries.push({
85
+ timestamp: new Date(parseInt(ts) / 1_000_000).toISOString(),
86
+ level: parsed.level || labels.level || "unknown",
87
+ message: parsed.msg || line,
88
+ labels,
89
+ });
90
+ }
91
+ }
92
+ // Sort newest first
93
+ entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
94
+ // Compute summary
95
+ const errorCount = entries.filter((e) => e.level === "error").length;
96
+ const warnCount = entries.filter((e) => e.level === "warn").length;
97
+ const topPatterns = this.extractTopPatterns(entries.filter((e) => e.level === "error"));
98
+ return {
99
+ source: this.name,
100
+ service: params.service,
101
+ entries,
102
+ summary: {
103
+ total: entries.length,
104
+ errorCount,
105
+ warnCount,
106
+ topPatterns,
107
+ },
108
+ };
109
+ }
110
+ // --- Private helpers ---
111
+ parseLine(line) {
112
+ try {
113
+ return JSON.parse(line);
114
+ }
115
+ catch {
116
+ return { msg: line };
117
+ }
118
+ }
119
+ extractTopPatterns(errorEntries) {
120
+ const patterns = new Map();
121
+ for (const entry of errorEntries) {
122
+ // Use first 100 chars of message as pattern key
123
+ const key = entry.message.slice(0, 100);
124
+ patterns.set(key, (patterns.get(key) || 0) + 1);
125
+ }
126
+ return Array.from(patterns.entries())
127
+ .sort((a, b) => b[1] - a[1])
128
+ .slice(0, 5)
129
+ .map(([pattern, count]) => `${pattern} (${count}x)`);
130
+ }
131
+ parseTimeRange(duration) {
132
+ const now = Math.floor(Date.now() / 1000);
133
+ const match = duration.match(/^(\d+)([mhd])$/);
134
+ if (!match)
135
+ throw new Error(`Invalid duration: ${duration}`);
136
+ const value = parseInt(match[1]);
137
+ const unit = match[2];
138
+ const seconds = unit === "m" ? value * 60 : unit === "h" ? value * 3600 : value * 86400;
139
+ return { start: now - seconds, end: now };
140
+ }
141
+ escapeLogQLValue(value) {
142
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
143
+ }
144
+ escapeLogQLRegex(value) {
145
+ // Escape backticks which would break the LogQL regex delimiter
146
+ return value.replace(/`/g, "\\`");
147
+ }
148
+ buildAuthHeaders() {
149
+ if (!this.auth || this.auth.type === "none")
150
+ return {};
151
+ if (this.auth.type === "bearer" && this.auth.token) {
152
+ return { Authorization: `Bearer ${this.auth.token}` };
153
+ }
154
+ if (this.auth.type === "basic" && this.auth.username) {
155
+ const encoded = Buffer.from(`${this.auth.username}:${this.auth.password || ""}`).toString("base64");
156
+ return { Authorization: `Basic ${encoded}` };
157
+ }
158
+ return {};
159
+ }
160
+ async apiGet(path, timeoutMs = 10000) {
161
+ const controller = new AbortController();
162
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
163
+ try {
164
+ const res = await fetch(`${this.baseUrl}${path}`, {
165
+ ...this.fetchOptions(),
166
+ signal: controller.signal,
167
+ });
168
+ if (!res.ok)
169
+ throw new Error(`Loki API error: ${res.status} ${res.statusText}`);
170
+ return res.json();
171
+ }
172
+ catch (err) {
173
+ if (err instanceof DOMException && err.name === "AbortError") {
174
+ throw new Error(`Loki query timed out after ${timeoutMs}ms`);
175
+ }
176
+ throw err;
177
+ }
178
+ finally {
179
+ clearTimeout(timer);
180
+ }
181
+ }
182
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,111 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { LokiConnector } from "./loki.js";
4
+ const proto = LokiConnector.prototype;
5
+ describe("LokiConnector", () => {
6
+ describe("parseLine", () => {
7
+ it("parses valid JSON", () => {
8
+ const result = proto.parseLine('{"level":"error","msg":"timeout"}');
9
+ assert.equal(result.level, "error");
10
+ assert.equal(result.msg, "timeout");
11
+ });
12
+ it("returns msg wrapper for invalid JSON", () => {
13
+ const result = proto.parseLine("plain text log line");
14
+ assert.equal(result.msg, "plain text log line");
15
+ });
16
+ it("handles empty string", () => {
17
+ const result = proto.parseLine("");
18
+ assert.equal(result.msg, "");
19
+ });
20
+ it("parses complex JSON", () => {
21
+ const result = proto.parseLine('{"level":"info","msg":"ok","nested":{"key":"val"}}');
22
+ assert.equal(result.level, "info");
23
+ assert.deepEqual(result.nested, { key: "val" });
24
+ });
25
+ });
26
+ describe("extractTopPatterns", () => {
27
+ it("returns empty for no entries", () => {
28
+ assert.deepEqual(proto.extractTopPatterns([]), []);
29
+ });
30
+ it("counts duplicate patterns", () => {
31
+ const entries = [
32
+ { message: "connection timeout" },
33
+ { message: "connection timeout" },
34
+ { message: "connection timeout" },
35
+ { message: "null pointer" },
36
+ ];
37
+ const patterns = proto.extractTopPatterns(entries);
38
+ assert.equal(patterns.length, 2);
39
+ assert.ok(patterns[0].includes("connection timeout"));
40
+ assert.ok(patterns[0].includes("3x"));
41
+ assert.ok(patterns[1].includes("null pointer"));
42
+ assert.ok(patterns[1].includes("1x"));
43
+ });
44
+ it("limits to top 5 patterns", () => {
45
+ const entries = [];
46
+ for (let i = 0; i < 10; i++) {
47
+ entries.push({ message: `error type ${i}` });
48
+ }
49
+ const patterns = proto.extractTopPatterns(entries);
50
+ assert.equal(patterns.length, 5);
51
+ });
52
+ it("sorts by count descending", () => {
53
+ const entries = [
54
+ { message: "rare error" },
55
+ { message: "common error" },
56
+ { message: "common error" },
57
+ { message: "common error" },
58
+ ];
59
+ const patterns = proto.extractTopPatterns(entries);
60
+ assert.ok(patterns[0].includes("common error"));
61
+ assert.ok(patterns[0].includes("3x"));
62
+ });
63
+ it("truncates long messages to 100 chars for pattern key", () => {
64
+ const longMsg = "x".repeat(200);
65
+ const entries = [{ message: longMsg }, { message: longMsg }];
66
+ const patterns = proto.extractTopPatterns(entries);
67
+ assert.equal(patterns.length, 1);
68
+ assert.ok(patterns[0].includes("2x"));
69
+ });
70
+ });
71
+ describe("parseTimeRange", () => {
72
+ it("parses minutes", () => {
73
+ const { start, end } = proto.parseTimeRange("10m");
74
+ assert.ok(end - start >= 599 && end - start <= 601);
75
+ });
76
+ it("parses hours", () => {
77
+ const { start, end } = proto.parseTimeRange("2h");
78
+ assert.ok(end - start >= 7199 && end - start <= 7201);
79
+ });
80
+ it("parses days", () => {
81
+ const { start, end } = proto.parseTimeRange("1d");
82
+ assert.ok(end - start >= 86399 && end - start <= 86401);
83
+ });
84
+ it("throws on invalid duration", () => {
85
+ assert.throws(() => proto.parseTimeRange("invalid"));
86
+ assert.throws(() => proto.parseTimeRange("5s"));
87
+ });
88
+ });
89
+ describe("escapeLogQLValue", () => {
90
+ it("returns value unchanged when no escaping needed", () => {
91
+ assert.equal(proto.escapeLogQLValue("api-gateway"), "api-gateway");
92
+ });
93
+ it("escapes backslashes", () => {
94
+ assert.equal(proto.escapeLogQLValue("path\\to\\file"), "path\\\\to\\\\file");
95
+ });
96
+ it("escapes double quotes", () => {
97
+ assert.equal(proto.escapeLogQLValue('say "hello"'), 'say \\"hello\\"');
98
+ });
99
+ it("escapes both", () => {
100
+ assert.equal(proto.escapeLogQLValue('a\\b"c'), 'a\\\\b\\"c');
101
+ });
102
+ });
103
+ describe("escapeLogQLRegex", () => {
104
+ it("returns value unchanged when no backticks", () => {
105
+ assert.equal(proto.escapeLogQLRegex("error.*timeout"), "error.*timeout");
106
+ });
107
+ it("escapes backticks", () => {
108
+ assert.equal(proto.escapeLogQLRegex("error`test`"), "error\\`test\\`");
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,28 @@
1
+ import type { ObservabilityConnector } from "./interface.js";
2
+ import type { SourceConfig, ConnectorHealth, ServiceInfo, MetricInfo, MetricQuery, MetricResult, MetricDefinition, SignalType } from "../types.js";
3
+ export declare class PrometheusConnector implements ObservabilityConnector {
4
+ readonly type = "prometheus";
5
+ readonly signalType: SignalType;
6
+ name: string;
7
+ private baseUrl;
8
+ private auth?;
9
+ private tlsAgent?;
10
+ private metrics;
11
+ connect(config: SourceConfig): Promise<void>;
12
+ getDefaultMetrics(): MetricDefinition[];
13
+ getMetrics(): MetricDefinition[];
14
+ setMetrics(metrics: MetricDefinition[]): void;
15
+ healthCheck(): Promise<ConnectorHealth>;
16
+ disconnect(): Promise<void>;
17
+ listServices(): Promise<ServiceInfo[]>;
18
+ listAvailableMetrics(_service: string): Promise<MetricInfo[]>;
19
+ queryMetrics(params: MetricQuery): Promise<MetricResult>;
20
+ private buildQuery;
21
+ private getUnit;
22
+ private parseTimeRange;
23
+ private computeSummary;
24
+ private computeTrend;
25
+ private buildAuthHeaders;
26
+ private fetchOptions;
27
+ private apiGet;
28
+ }