Skip to content

Commit c81d457

Browse files
committed
feat: add support for external Prometheus URLs
Signed-off-by: Tusharjamdade <tusharnjamdade@gmail.com>
1 parent 9b11486 commit c81d457

File tree

3 files changed

+60
-54
lines changed

3 files changed

+60
-54
lines changed

prometheus/src/components/Settings/Settings.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,12 @@ import { useEffect, useState } from 'react';
1414
import { isHttpUrl } from '../../util';
1515

1616
/**
17-
* Validates if the given address string is in a supported format.
17+
* Validates whether the given address is in a supported format.
18+
* Supports namespace/service:port and HTTP/HTTPS URLs.
19+
* Examples: monitoring/prometheus:9090, https://prometheus.example.com
1820
*
19-
* Supported formats:
20-
* - Kubernetes Service address: namespace/service-name:port
21-
* Example: monitoring/prometheus:9090
22-
*
23-
* - HTTP or HTTPS URL:
24-
* Example: https://prometheus.example.com
25-
* Example: http://mimir.company.net:9009
26-
*
27-
* @param {string} address - The address string to validate.
28-
* @returns {boolean} True if the address is valid, false otherwise.
21+
* @param address - The address string to validate.
22+
* @returns True if the address is valid, false otherwise.
2923
*/
3024
function isValidAddress(address: string): boolean {
3125
if (!address) return true;

prometheus/src/request.tsx

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -298,58 +298,68 @@ export async function fetchMetrics(data: {
298298
subPath?: string;
299299
}): Promise<object> {
300300
const params = new URLSearchParams();
301-
if (data.from) params.append('start', data.from.toString());
302-
if (data.to) params.append('end', data.to.toString());
303-
if (data.step) params.append('step', data.step.toString());
304-
if (data.query) params.append('query', data.query);
301+
if (data.from) {
302+
params.append('start', data.from.toString());
303+
}
304+
if (data.to) {
305+
params.append('end', data.to.toString());
306+
}
307+
if (data.step) {
308+
params.append('step', data.step.toString());
309+
}
310+
if (data.query) {
311+
params.append('query', data.query);
312+
}
305313

306314
const isExternal = isHttpUrl(data.prefix);
307-
let url: string;
315+
var url: string;
308316

309-
// 1. Construct the URL based on connection type
310317
if (isExternal) {
311-
// --- EXTERNAL URL LOGIC ---
312-
const base = data.prefix.replace(/\/$/, '');
318+
let base = data.prefix;
319+
if (base.endsWith('/')) {
320+
base = base.slice(0, -1);
321+
}
322+
313323
let apiPath = 'api/v1/query_range';
314324
if (data.subPath && data.subPath !== '') {
315-
const sub = data.subPath.replace(/^\/|\/$/g, '');
316-
apiPath = `${sub}/api/v1/query_range`;
325+
if (data.subPath.startsWith('/')) {
326+
data.subPath = data.subPath.slice(1);
327+
}
328+
if (data.subPath.endsWith('/')) {
329+
data.subPath = data.subPath.slice(0, -1);
330+
}
331+
apiPath = `${data.subPath}/api/v1/query_range`;
317332
}
333+
318334
url = `${base}/${apiPath}?${params.toString()}`;
319335
} else {
320-
// --- KUBERNETES PROXY LOGIC (FIXED) ---
321-
const sub = data.subPath ? data.subPath.replace(/^\/|\/$/g, '') : '';
322-
const apiPath = sub ? `${sub}/api/v1/query_range` : 'api/v1/query_range';
323-
324-
// data.prefix is expected as "namespace/services/service-name:port"
325-
url = `/api/v1/namespaces/${data.prefix}/proxy/${apiPath}?${params.toString()}`;
326-
}
327-
328-
try {
329-
if (isExternal) {
330-
// Direct fetch for external/local URL
331-
const response = await fetch(url, { method: 'GET' });
332-
333-
if (!response.ok) {
334-
if (response.status === 404) {
335-
throw new Error(`404 Not Found at: ${url}. Check your Endpoint and Subpath.`);
336-
}
337-
throw new Error(`Prometheus Error (${response.status}): ${response.statusText}`);
336+
url = `/api/v1/namespaces/${data.prefix}/proxy/api/v1/query_range?${params.toString()}`;
337+
if (data.subPath && data.subPath !== '') {
338+
if (data.subPath.startsWith('/')) {
339+
data.subPath = data.subPath.slice(1);
338340
}
339-
return await response.json();
340-
} else {
341-
// Headlamp Proxy for K8s services
342-
const response = await request(url, { method: 'GET', isJSON: false });
343-
344-
if (response.status !== 200) {
345-
throw new Error(`K8s Proxy Error ${response.status}: Path was ${url}`);
341+
if (data.subPath.endsWith('/')) {
342+
data.subPath = data.subPath.slice(0, -1);
346343
}
347-
return await response.json();
348-
}
349-
} catch (err: any) {
350-
if (err.message.includes('Failed to fetch')) {
351-
throw new Error(`Connection refused to ${url}. Did you enable --web.cors.origin=".*"?`);
344+
url = `/api/v1/namespaces/${data.prefix}/proxy/${
345+
data.subPath
346+
}/api/v1/query_range?${params.toString()}`;
352347
}
353-
throw err;
354348
}
355-
}
349+
350+
if (isExternal) {
351+
const response = await fetch(url, { method: 'GET' });
352+
return response.json();
353+
}
354+
355+
const response = await request(url, {
356+
method: 'GET',
357+
isJSON: false,
358+
});
359+
if (response.status === 200) {
360+
return response.json();
361+
} else {
362+
const error = new Error(response.statusText);
363+
return Promise.reject(error);
364+
}
365+
}

prometheus/src/util.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export function isMetricsEnabled(cluster: string): boolean {
115115
}
116116

117117
/**
118-
* getPrometheusPrefix returns the prefix for the Prometheus metrics.
118+
* Resolves the base address for accessing Prometheus for a cluster.
119+
* Supports full HTTP/HTTPS URLs or `namespace/service` Kubernetes service proxy paths.
120+
*
119121
* @param {string} cluster - The name of the cluster.
120122
* @returns {Promise<string | null>} The prefix for the Prometheus metrics, or null if not found.
121123
*/

0 commit comments

Comments
 (0)