@thotischner/observability-mcp 1.1.1 → 1.1.3
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/dist/connectors/loki.d.ts +4 -0
- package/dist/connectors/loki.js +69 -12
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ export declare class LokiConnector implements ObservabilityConnector {
|
|
|
7
7
|
private baseUrl;
|
|
8
8
|
private auth?;
|
|
9
9
|
private tlsAgent?;
|
|
10
|
+
private serviceLabels;
|
|
11
|
+
private labelValuesCache;
|
|
10
12
|
connect(config: SourceConfig): Promise<void>;
|
|
11
13
|
getDefaultMetrics(): MetricDefinition[];
|
|
12
14
|
getMetrics(): MetricDefinition[];
|
|
@@ -15,6 +17,8 @@ export declare class LokiConnector implements ObservabilityConnector {
|
|
|
15
17
|
disconnect(): Promise<void>;
|
|
16
18
|
listServices(): Promise<ServiceInfo[]>;
|
|
17
19
|
queryLogs(params: LogQuery): Promise<LogResult>;
|
|
20
|
+
private getLabelValues;
|
|
21
|
+
private resolveServiceSelector;
|
|
18
22
|
private parseLine;
|
|
19
23
|
private extractTopPatterns;
|
|
20
24
|
private parseTimeRange;
|
package/dist/connectors/loki.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { buildTlsAgent } from "./tls.js";
|
|
2
|
+
const DEFAULT_SERVICE_LABELS = ["service_name", "service", "job", "app", "container"];
|
|
3
|
+
const LABEL_CACHE_TTL_MS = 60_000;
|
|
2
4
|
export class LokiConnector {
|
|
3
5
|
type = "loki";
|
|
4
6
|
signalType = "logs";
|
|
@@ -6,11 +8,17 @@ export class LokiConnector {
|
|
|
6
8
|
baseUrl = "";
|
|
7
9
|
auth;
|
|
8
10
|
tlsAgent;
|
|
11
|
+
serviceLabels = DEFAULT_SERVICE_LABELS;
|
|
12
|
+
labelValuesCache = new Map();
|
|
9
13
|
async connect(config) {
|
|
10
14
|
this.name = config.name;
|
|
11
15
|
this.baseUrl = config.url.replace(/\/$/, "");
|
|
12
16
|
this.auth = config.auth;
|
|
13
17
|
this.tlsAgent = buildTlsAgent(config);
|
|
18
|
+
const envLabels = process.env.LOKI_SERVICE_LABELS;
|
|
19
|
+
if (envLabels) {
|
|
20
|
+
this.serviceLabels = envLabels.split(",").map((s) => s.trim()).filter(Boolean);
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
getDefaultMetrics() {
|
|
16
24
|
// Loki is a log backend — no metric definitions by default
|
|
@@ -46,23 +54,40 @@ export class LokiConnector {
|
|
|
46
54
|
}
|
|
47
55
|
async disconnect() { }
|
|
48
56
|
async listServices() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Probe each candidate label and merge values. Loki streams may identify
|
|
58
|
+
// services via service_name, service, job, app, or container depending on
|
|
59
|
+
// the shipper configuration. Walking all candidates ensures historical
|
|
60
|
+
// streams remain reachable when label conventions change over time.
|
|
61
|
+
const seen = new Map();
|
|
62
|
+
for (const label of this.serviceLabels) {
|
|
63
|
+
const values = await this.getLabelValues(label);
|
|
64
|
+
for (const raw of values) {
|
|
65
|
+
// Docker's loki.source.docker writes container names with a leading '/'
|
|
66
|
+
// (Docker API Names[0] convention). Strip it for display so the name
|
|
67
|
+
// matches what the service-name validator and users will pass back in.
|
|
68
|
+
const display = label === "container" ? raw.replace(/^\//, "") : raw;
|
|
69
|
+
if (!seen.has(display)) {
|
|
70
|
+
seen.set(display, {
|
|
71
|
+
name: display,
|
|
72
|
+
source: this.name,
|
|
73
|
+
signalType: "logs",
|
|
74
|
+
labels: { discoveredVia: label },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
}
|
|
79
|
+
return Array.from(seen.values());
|
|
60
80
|
}
|
|
61
81
|
async queryLogs(params) {
|
|
62
82
|
const { start, end } = this.parseTimeRange(params.duration);
|
|
63
83
|
const limit = Math.min(Math.max(params.limit || 100, 1), 1000);
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// Resolve label + actual selector value. For the 'container' label the
|
|
85
|
+
// value stored in Loki may be '/my-app-1' while the caller passes the
|
|
86
|
+
// sanitized 'my-app-1' — return the prefixed form so the LogQL selector
|
|
87
|
+
// matches the real stream.
|
|
88
|
+
const { label: matchedLabel, value: rawValue } = await this.resolveServiceSelector(params.service);
|
|
89
|
+
const service = this.escapeLogQLValue(rawValue);
|
|
90
|
+
let logql = `{${matchedLabel}="${service}"}`;
|
|
66
91
|
if (params.level) {
|
|
67
92
|
const level = this.escapeLogQLValue(params.level);
|
|
68
93
|
logql += ` | json | level="${level}"`;
|
|
@@ -109,6 +134,38 @@ export class LokiConnector {
|
|
|
109
134
|
};
|
|
110
135
|
}
|
|
111
136
|
// --- Private helpers ---
|
|
137
|
+
async getLabelValues(label) {
|
|
138
|
+
const cached = this.labelValuesCache.get(label);
|
|
139
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
140
|
+
return cached.values;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const data = await this.apiGet(`/loki/api/v1/label/${encodeURIComponent(label)}/values`);
|
|
144
|
+
const values = data?.data || [];
|
|
145
|
+
this.labelValuesCache.set(label, {
|
|
146
|
+
values,
|
|
147
|
+
expiresAt: Date.now() + LABEL_CACHE_TTL_MS,
|
|
148
|
+
});
|
|
149
|
+
return values;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
this.labelValuesCache.set(label, { values: [], expiresAt: Date.now() + LABEL_CACHE_TTL_MS });
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async resolveServiceSelector(service) {
|
|
157
|
+
for (const label of this.serviceLabels) {
|
|
158
|
+
const values = await this.getLabelValues(label);
|
|
159
|
+
if (values.includes(service))
|
|
160
|
+
return { label, value: service };
|
|
161
|
+
// Container label values are Docker-prefixed with '/'. The caller can't
|
|
162
|
+
// pass that form (validator rejects '/'), so probe the prefixed variant.
|
|
163
|
+
if (label === "container" && values.includes(`/${service}`)) {
|
|
164
|
+
return { label, value: `/${service}` };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { label: this.serviceLabels[0] || "service_name", value: service };
|
|
168
|
+
}
|
|
112
169
|
parseLine(line) {
|
|
113
170
|
try {
|
|
114
171
|
return JSON.parse(line);
|
package/package.json
CHANGED