|
1 | | -import type { PromInstantResult, PromRuleState, PromSeriesResult } from "@opsremedy/core/types"; |
2 | | -import type { PromClient, PromInstantQuery, PromRangeQuery } from "../types.ts"; |
| 1 | +import type { |
| 2 | + PromInstantResult, |
| 3 | + PromMetricMetadata, |
| 4 | + PromRuleState, |
| 5 | + PromSeriesResult, |
| 6 | + PromTarget, |
| 7 | +} from "@opsremedy/core/types"; |
| 8 | +import type { |
| 9 | + PromClient, |
| 10 | + PromInstantQuery, |
| 11 | + PromMetadataQuery, |
| 12 | + PromMetricsListQuery, |
| 13 | + PromRangeQuery, |
| 14 | + PromTargetsQuery, |
| 15 | +} from "../types.ts"; |
3 | 16 |
|
4 | 17 | export interface RealPromClientOptions { |
5 | 18 | baseUrl: string; |
@@ -42,6 +55,30 @@ interface PromRulesData { |
42 | 55 | }>; |
43 | 56 | } |
44 | 57 |
|
| 58 | +/** |
| 59 | + * `/api/v1/metadata` returns either Record<metric, MetadataItem[]> (vanilla |
| 60 | + * Prom) or a flat array (some downstreams). We normalise to the array form. |
| 61 | + */ |
| 62 | +type PromMetadataRaw = |
| 63 | + | Record<string, Array<{ type?: string; help?: string; unit?: string }>> |
| 64 | + | Array<{ metric?: string; type?: string; help?: string; unit?: string }>; |
| 65 | + |
| 66 | +interface PromTargetRaw { |
| 67 | + discoveredLabels?: Record<string, string>; |
| 68 | + labels?: Record<string, string>; |
| 69 | + scrapePool?: string; |
| 70 | + scrapeUrl?: string; |
| 71 | + globalUrl?: string; |
| 72 | + lastError?: string; |
| 73 | + lastScrape?: string; |
| 74 | + health?: "up" | "down" | "unknown"; |
| 75 | +} |
| 76 | + |
| 77 | +interface PromTargetsData { |
| 78 | + activeTargets?: PromTargetRaw[]; |
| 79 | + droppedTargets?: PromTargetRaw[]; |
| 80 | +} |
| 81 | + |
45 | 82 | /** |
46 | 83 | * Prometheus HTTP API client. Uses native fetch. |
47 | 84 | * Supports bearer token or HTTP basic auth. |
@@ -106,6 +143,51 @@ export class RealPromClient implements PromClient { |
106 | 143 | return out; |
107 | 144 | } |
108 | 145 |
|
| 146 | + async listMetrics(q: PromMetricsListQuery): Promise<string[]> { |
| 147 | + // Prom + GMP both expose label values via /api/v1/label/<label>/values. |
| 148 | + const data = await this.get<string[]>("/api/v1/label/__name__/values", new URLSearchParams(), q.signal); |
| 149 | + let names = Array.isArray(data) ? data : []; |
| 150 | + if (q.contains) { |
| 151 | + const needle = q.contains.toLowerCase(); |
| 152 | + names = names.filter((n) => n.toLowerCase().includes(needle)); |
| 153 | + } |
| 154 | + const limit = q.limit ?? 200; |
| 155 | + return names.slice(0, limit); |
| 156 | + } |
| 157 | + |
| 158 | + async metricMetadata(q: PromMetadataQuery): Promise<PromMetricMetadata[]> { |
| 159 | + const params = new URLSearchParams(); |
| 160 | + if (q.metric) params.set("metric", q.metric); |
| 161 | + if (q.perMetricLimit !== undefined) params.set("limit_per_metric", String(q.perMetricLimit)); |
| 162 | + const data = await this.get<PromMetadataRaw>("/api/v1/metadata", params, q.signal); |
| 163 | + return normaliseMetadata(data); |
| 164 | + } |
| 165 | + |
| 166 | + async targets(q: PromTargetsQuery): Promise<PromTarget[]> { |
| 167 | + const params = new URLSearchParams(); |
| 168 | + params.set("state", q.state ?? "active"); |
| 169 | + const data = await this.get<PromTargetsData>("/api/v1/targets", params, q.signal); |
| 170 | + const sources: PromTargetRaw[] = []; |
| 171 | + if (data.activeTargets) sources.push(...data.activeTargets); |
| 172 | + if (q.state === "dropped" || q.state === "any") { |
| 173 | + if (data.droppedTargets) sources.push(...data.droppedTargets); |
| 174 | + } |
| 175 | + const out: PromTarget[] = sources.map((t) => { |
| 176 | + const labels = t.labels ?? {}; |
| 177 | + const target: PromTarget = { |
| 178 | + job: labels.job ?? t.scrapePool ?? "", |
| 179 | + instance: labels.instance ?? "", |
| 180 | + health: t.health ?? "unknown", |
| 181 | + labels, |
| 182 | + }; |
| 183 | + if (t.scrapeUrl !== undefined) target.scrapeUrl = t.scrapeUrl; |
| 184 | + if (t.lastError !== undefined && t.lastError.length > 0) target.lastError = t.lastError; |
| 185 | + if (t.lastScrape !== undefined) target.lastScrape = t.lastScrape; |
| 186 | + return target; |
| 187 | + }); |
| 188 | + return q.job ? out.filter((t) => t.job === q.job) : out; |
| 189 | + } |
| 190 | + |
109 | 191 | uiUrl(kind: "graph" | "alerts", query?: string): string { |
110 | 192 | if (kind === "alerts") return `${this.baseUrl}/alerts`; |
111 | 193 | if (!query) return `${this.baseUrl}/graph`; |
@@ -141,6 +223,33 @@ export class RealPromClient implements PromClient { |
141 | 223 | } |
142 | 224 | } |
143 | 225 |
|
| 226 | +function normaliseMetadata(raw: PromMetadataRaw): PromMetricMetadata[] { |
| 227 | + const out: PromMetricMetadata[] = []; |
| 228 | + if (Array.isArray(raw)) { |
| 229 | + for (const item of raw) { |
| 230 | + if (!item.metric) continue; |
| 231 | + out.push({ |
| 232 | + metric: item.metric, |
| 233 | + type: (item.type ?? "unknown") as PromMetricMetadata["type"], |
| 234 | + help: item.help ?? "", |
| 235 | + ...(item.unit !== undefined && item.unit.length > 0 && { unit: item.unit }), |
| 236 | + }); |
| 237 | + } |
| 238 | + return out; |
| 239 | + } |
| 240 | + for (const [metric, items] of Object.entries(raw)) { |
| 241 | + const first = items?.[0]; |
| 242 | + if (!first) continue; |
| 243 | + out.push({ |
| 244 | + metric, |
| 245 | + type: (first.type ?? "unknown") as PromMetricMetadata["type"], |
| 246 | + help: first.help ?? "", |
| 247 | + ...(first.unit !== undefined && first.unit.length > 0 && { unit: first.unit }), |
| 248 | + }); |
| 249 | + } |
| 250 | + return out; |
| 251 | +} |
| 252 | + |
144 | 253 | function pickInstantSample(s: { value?: [number, string]; values?: Array<[number, string]> }): { |
145 | 254 | timestamp: number; |
146 | 255 | value: number; |
|
0 commit comments