WIP: status page - API and UI (#6243)

* status page initial commit

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* refactor useFetch

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* refactoring

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* adding tests

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* snapshot testing

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* fix wrong go files formatting

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* change the snapshot library

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* update api paths

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* move test folder outside src

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* useFetches tests

Signed-off-by: blalov <boyko.lalov@tick42.com>

* sticky navbar

Signed-off-by: Boyko Lalov <boyskila@gmail.com>
Signed-off-by: blalov <boyko.lalov@tick42.com>

* handle runtimeInfo error on Gather() and add json tags to RuntimeInfo struct

Signed-off-by: blalov <boyko.lalov@tick42.com>

* refactor alert managers section

Signed-off-by: blalov <boyko.lalov@tick42.com>
This commit is contained in:
Boyko 2019-11-02 17:53:32 +02:00 committed by Julius Volz
parent ca9fce46a3
commit cb7cbad5f9
14 changed files with 876 additions and 20 deletions

View file

@ -108,6 +108,32 @@ type rulesRetriever interface {
AlertingRules() []*rules.AlertingRule
}
// PrometheusVersion contains build information about Prometheus.
type PrometheusVersion struct {
Version string `json:"version"`
Revision string `json:"revision"`
Branch string `json:"branch"`
BuildUser string `json:"buildUser"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
}
// RuntimeInfo contains runtime information about Prometheus.
type RuntimeInfo struct {
StartTime time.Time `json:"startTime"`
CWD string `json:"CWD"`
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
LastConfigTime time.Time `json:"lastConfigTime"`
ChunkCount int64 `json:"chunkCount"`
TimeSeriesCount int64 `json:"timeSeriesCount"`
CorruptionCount int64 `json:"corruptionCount"`
GoroutineCount int `json:"goroutineCount"`
GOMAXPROCS int `json:"GOMAXPROCS"`
GOGC string `json:"GOGC"`
GODEBUG string `json:"GODEBUG"`
StorageRetention string `json:"storageRetention"`
}
type response struct {
Status status `json:"status"`
Data interface{} `json:"data,omitempty"`
@ -154,6 +180,8 @@ type API struct {
remoteReadMaxBytesInFrame int
remoteReadGate *gate.Gate
CORSOrigin *regexp.Regexp
buildInfo *PrometheusVersion
runtimeInfo func() (RuntimeInfo, error)
}
func init() {
@ -178,6 +206,8 @@ func NewAPI(
remoteReadConcurrencyLimit int,
remoteReadMaxBytesInFrame int,
CORSOrigin *regexp.Regexp,
runtimeInfo func() (RuntimeInfo, error),
buildInfo *PrometheusVersion,
) *API {
return &API{
QueryEngine: qe,
@ -197,6 +227,8 @@ func NewAPI(
remoteReadMaxBytesInFrame: remoteReadMaxBytesInFrame,
logger: logger,
CORSOrigin: CORSOrigin,
runtimeInfo: runtimeInfo,
buildInfo: buildInfo,
}
}
@ -242,6 +274,8 @@ func (api *API) Register(r *route.Router) {
r.Get("/alertmanagers", wrap(api.alertmanagers))
r.Get("/status/config", wrap(api.serveConfig))
r.Get("/status/runtimeinfo", wrap(api.serveRuntimeInfo))
r.Get("/status/buildinfo", wrap(api.serveBuildInfo))
r.Get("/status/flags", wrap(api.serveFlags))
r.Post("/read", api.ready(http.HandlerFunc(api.remoteRead)))
@ -832,6 +866,18 @@ type prometheusConfig struct {
YAML string `json:"yaml"`
}
func (api *API) serveRuntimeInfo(r *http.Request) apiFuncResult {
status, err := api.runtimeInfo()
if err != nil {
return apiFuncResult{status, &apiError{errorInternal, err}, nil, nil}
}
return apiFuncResult{status, nil, nil, nil}
}
func (api *API) serveBuildInfo(r *http.Request) apiFuncResult {
return apiFuncResult{api.buildInfo, nil, nil, nil}
}
func (api *API) serveConfig(r *http.Request) apiFuncResult {
cfg := &prometheusConfig{
YAML: api.config().String(),
@ -1176,6 +1222,7 @@ func (api *API) respondError(w http.ResponseWriter, apiErr *apiError, data inter
Error: apiErr.err.Error(),
Data: data,
})
if err != nil {
level.Error(api.logger).Log("msg", "error marshaling json response", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)

View file

@ -7,6 +7,7 @@
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@reach/router": "^1.2.1",
"@testing-library/react-hooks": "^3.1.1",
"@types/jest": "^24.0.20",
"@types/jquery": "^3.3.29",
"@types/node": "^12.11.1",
@ -18,9 +19,11 @@
"@types/sanitize-html": "^1.20.2",
"bootstrap": "^4.2.1",
"downshift": "^3.2.2",
"enzyme-to-json": "^3.4.3",
"flot": "^3.2.13",
"fuzzy": "^0.1.3",
"i": "^0.3.6",
"jest-fetch-mock": "^2.1.2",
"jquery": "^3.3.1",
"jquery.flot.tooltip": "^0.9.0",
"jsdom": "^15.2.0",
@ -32,6 +35,7 @@
"react-dom": "^16.7.0",
"react-resize-detector": "^4.2.1",
"react-scripts": "^3.2.0",
"react-test-renderer": "^16.9.0",
"reactstrap": "^8.0.1",
"sanitize-html": "^1.20.1",
"tempusdominus-bootstrap-4": "^5.1.2",
@ -83,5 +87,10 @@
"prettier": "^1.18.2",
"sinon": "^7.5.0"
},
"proxy": "http://localhost:9090"
"proxy": "http://localhost:9090",
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
}
}

View file

@ -12,6 +12,10 @@ input[type='checkbox']:checked + label {
line-height: 1.8;
}
.capitalize-title::first-letter {
text-transform: capitalize;
}
.expression-input {
margin-bottom: 10px;
}

View file

@ -11,7 +11,7 @@ class App extends Component {
return (
<>
<Navigation />
<Container fluid>
<Container fluid style={{ paddingTop: 70 }}>
<Router basepath="/new">
<PanelList path="/graph" />
<Alerts path="/alerts" />

View file

@ -34,7 +34,7 @@ class Graph extends PureComponent<GraphProps> {
private chartRef = React.createRef<HTMLDivElement>();
renderLabels(labels: { [key: string]: string }) {
let labelStrings: string[] = [];
const labelStrings: string[] = [];
for (const label in labels) {
if (label !== '__name__') {
labelStrings.push('<strong>' + label + '</strong>: ' + escapeHTML(labels[label]));

View file

@ -17,7 +17,7 @@ const Navigation = () => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
return (
<Navbar className="mb-3" dark color="dark" expand="md">
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
<NavbarToggler onClick={toggle} />
<Link className="pt-0 navbar-brand" to="/new/graph">
Prometheus

View file

@ -0,0 +1,55 @@
import useFetches from './useFetches';
import { renderHook } from '@testing-library/react-hooks';
describe('useFetches', () => {
beforeEach(() => {
fetchMock.resetMocks();
});
it('should can handle multiple requests', async done => {
fetchMock.mockResponse(JSON.stringify({ satus: 'success', data: { id: 1 } }));
const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar', '/foo/bar', '/foo/bar'] });
await waitForNextUpdate();
expect(result.current.response).toHaveLength(3);
done();
});
it('should can handle success flow -> isLoading=true, response=[data, data], isLoading=false', async done => {
fetchMock.mockResponse(JSON.stringify({ satus: 'success', data: { id: 1 } }));
const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] });
expect(result.current.isLoading).toEqual(true);
await waitForNextUpdate();
expect(result.current.response).toHaveLength(1);
expect(result.current.isLoading).toEqual(false);
done();
});
it('should isLoading remains true on empty response', async done => {
fetchMock.mockResponse(jest.fn());
const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] });
expect(result.current.isLoading).toEqual(true);
await waitForNextUpdate();
setTimeout(() => {
expect(result.current.isLoading).toEqual(true);
done();
}, 1000);
});
it('should set error message when response fail', async done => {
fetchMock.mockReject(new Error('errr'));
const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: ['/foo/bar'] });
expect(result.current.isLoading).toEqual(true);
await waitForNextUpdate();
expect(result.current.error!.message).toEqual('errr');
expect(result.current.isLoading).toEqual(true);
done();
});
it('should throw an error if array is empty', async done => {
try {
useFetches([]);
const { result, waitForNextUpdate } = renderHook(useFetches, { initialProps: [] });
await waitForNextUpdate().then(done);
expect(result.error.message).toEqual("Doesn't have url to fetch.");
done();
} catch (e) {
} finally {
done();
}
});
});

View file

@ -0,0 +1,36 @@
import { useState, useEffect } from 'react';
const useFetches = <R extends any>(urls: string[], options?: RequestInit) => {
if (!urls.length) {
throw new Error("Doesn't have url to fetch.");
}
const [response, setResponse] = useState<R[]>();
const [error, setError] = useState<Error>();
useEffect(() => {
const fetchData = async () => {
try {
const responses: R[] = await Promise.all(
urls
.map(async url => {
const res = await fetch(url, options);
if (!res.ok) {
throw new Error(res.statusText);
}
const result = await res.json();
return result.data;
})
.filter(Boolean) // Remove falsy values
);
setResponse(responses);
} catch (error) {
setError(error);
}
};
fetchData();
}, [urls, options]);
return { response, error, isLoading: !response || !response.length };
};
export default useFetches;

View file

@ -0,0 +1,72 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { Status } from '.';
import { Alert } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as useFetch from '../hooks/useFetches';
import toJson from 'enzyme-to-json';
describe('Status', () => {
afterEach(() => jest.restoreAllMocks());
it('should render spinner while waiting data', () => {
const wrapper = shallow(<Status />);
expect(wrapper.find(FontAwesomeIcon)).toHaveLength(1);
});
it('should render Alert on error', () => {
(useFetch as any).default = jest.fn().mockImplementation(() => ({ error: new Error('foo') }));
const wrapper = shallow(<Status />);
expect(wrapper.find(Alert)).toHaveLength(1);
});
it('should fetch proper API endpoints', () => {
const useFetchSpy = jest.spyOn(useFetch, 'default');
shallow(<Status />);
expect(useFetchSpy).toHaveBeenCalledWith([
'../api/v1/status/runtimeinfo',
'../api/v1/status/buildinfo',
'../api/v1/alertmanagers',
]);
});
describe('Snapshot testing', () => {
const response = [
{
startTime: '2019-10-30T22:03:23.247913868+02:00',
CWD: '/home/boyskila/Desktop/prometheus',
reloadConfigSuccess: true,
lastConfigTime: '2019-10-30T22:03:23+02:00',
chunkCount: 1383,
timeSeriesCount: 461,
corruptionCount: 0,
goroutineCount: 37,
GOMAXPROCS: 4,
GOGC: '',
GODEBUG: '',
storageRetention: '15d',
},
{
version: '',
revision: '',
branch: '',
buildUser: '',
buildDate: '',
goVersion: 'go1.13.3',
},
{
activeAlertmanagers: [
{ url: 'https://1.2.3.4:9093/api/v1/alerts' },
{ url: 'https://1.2.3.5:9093/api/v1/alerts' },
{ url: 'https://1.2.3.6:9093/api/v1/alerts' },
{ url: 'https://1.2.3.7:9093/api/v1/alerts' },
{ url: 'https://1.2.3.8:9093/api/v1/alerts' },
{ url: 'https://1.2.3.9:9093/api/v1/alerts' },
],
droppedAlertmanagers: [],
},
];
it('should match table snapshot', () => {
(useFetch as any).default = jest.fn().mockImplementation(() => ({ response }));
const wrapper = shallow(<Status />);
expect(toJson(wrapper)).toMatchSnapshot();
jest.restoreAllMocks();
});
});
});

View file

@ -1,6 +1,108 @@
import React, { FC } from 'react';
import React, { FC, Fragment } from 'react';
import { RouteComponentProps } from '@reach/router';
import { Table, Alert } from 'reactstrap';
import useFetches from '../hooks/useFetches';
const Status: FC<RouteComponentProps> = () => <div>Status page</div>;
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
export default Status;
const ENDPOINTS = ['../api/v1/status/runtimeinfo', '../api/v1/status/buildinfo', '../api/v1/alertmanagers'];
const sectionTitles = ['Runtime Information', 'Build Information', 'Alertmanagers'];
interface StatusConfig {
[k: string]: { title?: string; customizeValue?: (v: any) => any; customRow?: boolean; skip?: boolean };
}
type StatusPageState = Array<{ [k: string]: string }>;
export const statusConfig: StatusConfig = {
startTime: { title: 'Start time', customizeValue: (v: string) => new Date(v).toUTCString() },
CWD: { title: 'Working directory' },
reloadConfigSuccess: {
title: 'Configuration reload',
customizeValue: (v: boolean) => (v ? 'Successful' : 'Unsuccessful'),
},
lastConfigTime: { title: 'Last successful configuration reload' },
chunkCount: { title: 'Head chunks' },
timeSeriesCount: { title: 'Head time series' },
corruptionCount: { title: 'WAL corruptions' },
goroutineCount: { title: 'Goroutines' },
storageRetention: { title: 'Storage retention' },
activeAlertmanagers: {
customRow: true,
customizeValue: (alertMgrs: { url: string }[]) => {
return (
<Fragment key="alert-managers">
<tr>
<th>Endpoint</th>
</tr>
{alertMgrs.map(({ url }) => {
const { origin, pathname } = new URL(url);
return (
<tr key={url}>
<td>
<a href={url}>{origin}</a>
{pathname}
</td>
</tr>
);
})}
</Fragment>
);
},
},
droppedAlertmanagers: { skip: true },
};
const Status = () => {
const { response: data, error, isLoading } = useFetches<StatusPageState[]>(ENDPOINTS);
if (error) {
return (
<Alert color="danger">
<strong>Error:</strong> Error fetching status: {error.message}
</Alert>
);
} else if (isLoading) {
return (
<FontAwesomeIcon
size="3x"
icon={faSpinner}
spin
className="position-absolute"
style={{ transform: 'translate(-50%, -50%)', top: '50%', left: '50%' }}
/>
);
}
return data
? data.map((statuses, i) => {
return (
<Fragment key={i}>
<h2>{sectionTitles[i]}</h2>
<Table className="h-auto" size="sm" bordered striped>
<tbody>
{Object.entries(statuses).map(([k, v]) => {
const { title = k, customizeValue = (val: any) => val, customRow, skip } = statusConfig[k] || {};
if (skip) {
return null;
}
if (customRow) {
return customizeValue(v);
}
return (
<tr key={k}>
<th className="capitalize-title" style={{ width: '35%' }}>
{title}
</th>
<td className="text-break">{customizeValue(v)}</td>
</tr>
);
})}
</tbody>
</Table>
</Fragment>
);
})
: null;
};
export default Status as FC<RouteComponentProps>;

View file

@ -0,0 +1,465 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Status Snapshot testing should match table snapshot 1`] = `
Array [
<Fragment
key="0"
>
<h2>
Runtime Information
</h2>
<Table
bordered={true}
className="h-auto"
responsiveTag="div"
size="sm"
striped={true}
tag="table"
>
<tbody>
<tr
key="startTime"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Start time
</th>
<td
className="text-break"
>
Wed, 30 Oct 2019 20:03:23 GMT
</td>
</tr>
<tr
key="CWD"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Working directory
</th>
<td
className="text-break"
>
/home/boyskila/Desktop/prometheus
</td>
</tr>
<tr
key="reloadConfigSuccess"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Configuration reload
</th>
<td
className="text-break"
>
Successful
</td>
</tr>
<tr
key="lastConfigTime"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Last successful configuration reload
</th>
<td
className="text-break"
>
2019-10-30T22:03:23+02:00
</td>
</tr>
<tr
key="chunkCount"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Head chunks
</th>
<td
className="text-break"
>
1383
</td>
</tr>
<tr
key="timeSeriesCount"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Head time series
</th>
<td
className="text-break"
>
461
</td>
</tr>
<tr
key="corruptionCount"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
WAL corruptions
</th>
<td
className="text-break"
>
0
</td>
</tr>
<tr
key="goroutineCount"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Goroutines
</th>
<td
className="text-break"
>
37
</td>
</tr>
<tr
key="GOMAXPROCS"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
GOMAXPROCS
</th>
<td
className="text-break"
>
4
</td>
</tr>
<tr
key="GOGC"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
GOGC
</th>
<td
className="text-break"
/>
</tr>
<tr
key="GODEBUG"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
GODEBUG
</th>
<td
className="text-break"
/>
</tr>
<tr
key="storageRetention"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
Storage retention
</th>
<td
className="text-break"
>
15d
</td>
</tr>
</tbody>
</Table>
</Fragment>,
<Fragment
key="1"
>
<h2>
Build Information
</h2>
<Table
bordered={true}
className="h-auto"
responsiveTag="div"
size="sm"
striped={true}
tag="table"
>
<tbody>
<tr
key="version"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
version
</th>
<td
className="text-break"
/>
</tr>
<tr
key="revision"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
revision
</th>
<td
className="text-break"
/>
</tr>
<tr
key="branch"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
branch
</th>
<td
className="text-break"
/>
</tr>
<tr
key="buildUser"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
buildUser
</th>
<td
className="text-break"
/>
</tr>
<tr
key="buildDate"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
buildDate
</th>
<td
className="text-break"
/>
</tr>
<tr
key="goVersion"
>
<th
className="capitalize-title"
style={
Object {
"width": "35%",
}
}
>
goVersion
</th>
<td
className="text-break"
>
go1.13.3
</td>
</tr>
</tbody>
</Table>
</Fragment>,
<Fragment
key="2"
>
<h2>
Alertmanagers
</h2>
<Table
bordered={true}
className="h-auto"
responsiveTag="div"
size="sm"
striped={true}
tag="table"
>
<tbody>
<tr>
<th>
Endpoint
</th>
</tr>
<tr
key="https://1.2.3.4:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.4:9093/api/v1/alerts"
>
https://1.2.3.4:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.5:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.5:9093/api/v1/alerts"
>
https://1.2.3.5:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.6:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.6:9093/api/v1/alerts"
>
https://1.2.3.6:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.7:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.7:9093/api/v1/alerts"
>
https://1.2.3.7:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.8:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.8:9093/api/v1/alerts"
>
https://1.2.3.8:9093
</a>
/api/v1/alerts
</td>
</tr>
<tr
key="https://1.2.3.9:9093/api/v1/alerts"
>
<td>
<a
href="https://1.2.3.9:9093/api/v1/alerts"
>
https://1.2.3.9:9093
</a>
/api/v1/alerts
</td>
</tr>
</tbody>
</Table>
</Fragment>,
]
`;

View file

@ -20,6 +20,6 @@
"jsx": "preserve"
},
"include": [
"src"
"src", "test"
]
}

View file

@ -857,7 +857,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4":
version "7.6.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f"
integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==
@ -1282,6 +1282,14 @@
"@svgr/plugin-svgo" "^4.3.1"
loader-utils "^1.2.3"
"@testing-library/react-hooks@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.1.1.tgz#5c93e463c0252bea6ac237ec8d9c982c27d67208"
integrity sha512-HANnmA68/i6RwZn9j7pcbAg438PoDToftRQ1CH0j893WuQGtENFm57GKTagtmXXDN5gKh3rVbN1GH6HDvHbk6A==
dependencies:
"@babel/runtime" "^7.5.4"
"@types/testing-library__react-hooks" "^2.0.0"
"@types/babel__core@^7.1.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
@ -1470,6 +1478,13 @@
dependencies:
"@types/react" "*"
"@types/react-test-renderer@*":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.1.tgz#9d432c46c515ebe50c45fa92c6fb5acdc22e39c4"
integrity sha512-nCXQokZN1jp+QkoDNmDZwoWpKY8HDczqevIDO4Uv9/s9rbGPbSpy8Uaxa5ixHKkcm/Wt0Y9C3wCxZivh4Al+rQ==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.8.2":
version "16.9.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.9.tgz#a62c6f40f04bc7681be5e20975503a64fe783c3a"
@ -1508,6 +1523,14 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/testing-library__react-hooks@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-2.0.0.tgz#7b289d64945517ae8ba9cbcb0c5b282432aaeffa"
integrity sha512-YUVqXGCChJKEJ4aAnMXqPCq0NfPAFVsJeGIb2y/iiMjxwyu+45+vR+AHOwjJHHKEHeC0ZhOGrZ5gSEmaJe4tyQ==
dependencies:
"@types/react" "*"
"@types/react-test-renderer" "*"
"@types/yargs-parser@*":
version "13.1.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228"
@ -3968,6 +3991,13 @@ enzyme-shallow-equal@^1.0.0:
has "^1.0.3"
object-is "^1.0.1"
enzyme-to-json@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.3.tgz#ed4386f48768ed29e2d1a2910893542c34e7e0af"
integrity sha512-jqNEZlHqLdz7OTpXSzzghArSS3vigj67IU/fWkPyl1c0TCj9P5s6Ze0kRkYZWNEoCqCR79xlQbigYlMx5erh8A==
dependencies:
lodash "^4.17.15"
enzyme@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6"
@ -9104,7 +9134,7 @@ react-scripts@^3.2.0:
optionalDependencies:
fsevents "2.0.7"
react-test-renderer@^16.0.0-0:
react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
version "16.11.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8"
integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag==

View file

@ -163,6 +163,9 @@ func (m *metrics) instrumentHandler(handlerName string, handler http.HandlerFunc
)
}
// PrometheusVersion contains build information about Prometheus.
type PrometheusVersion = api_v1.PrometheusVersion
// Handler serves various HTTP endpoints of the Prometheus server
type Handler struct {
logger log.Logger
@ -206,16 +209,6 @@ func (h *Handler) ApplyConfig(conf *config.Config) error {
return nil
}
// PrometheusVersion contains build information about Prometheus.
type PrometheusVersion struct {
Version string `json:"version"`
Revision string `json:"revision"`
Branch string `json:"branch"`
BuildUser string `json:"buildUser"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
}
// Options for the web Handler.
type Options struct {
Context context.Context
@ -310,6 +303,8 @@ func New(logger log.Logger, o *Options) *Handler {
h.options.RemoteReadConcurrencyLimit,
h.options.RemoteReadBytesInFrame,
h.options.CORSOrigin,
h.runtimeInfo,
h.versionInfo,
)
if o.RoutePrefix != "/" {
@ -744,6 +739,47 @@ func (h *Handler) status(w http.ResponseWriter, r *http.Request) {
h.executeTemplate(w, "status.html", status)
}
func (h *Handler) runtimeInfo() (api_v1.RuntimeInfo, error) {
status := api_v1.RuntimeInfo{
StartTime: h.birth,
CWD: h.cwd,
GoroutineCount: runtime.NumGoroutine(),
GOMAXPROCS: runtime.GOMAXPROCS(0),
GOGC: os.Getenv("GOGC"),
GODEBUG: os.Getenv("GODEBUG"),
}
if h.options.TSDBCfg.RetentionDuration != 0 {
status.StorageRetention = h.options.TSDBCfg.RetentionDuration.String()
}
if h.options.TSDBCfg.MaxBytes != 0 {
if status.StorageRetention != "" {
status.StorageRetention = status.StorageRetention + " or "
}
status.StorageRetention = status.StorageRetention + h.options.TSDBCfg.MaxBytes.String()
}
metrics, err := h.gatherer.Gather()
if err != nil {
return status, errors.Errorf("error gathering runtime status: %s", err)
}
for _, mF := range metrics {
switch *mF.Name {
case "prometheus_tsdb_head_chunks":
status.ChunkCount = int64(toFloat64(mF))
case "prometheus_tsdb_head_series":
status.TimeSeriesCount = int64(toFloat64(mF))
case "prometheus_tsdb_wal_corruptions_total":
status.CorruptionCount = int64(toFloat64(mF))
case "prometheus_config_last_reload_successful":
status.ReloadConfigSuccess = toFloat64(mF) != 0
case "prometheus_config_last_reload_success_timestamp_seconds":
status.LastConfigTime = time.Unix(int64(toFloat64(mF)), 0)
}
}
return status, nil
}
func toFloat64(f *io_prometheus_client.MetricFamily) float64 {
m := *f.Metric[0]
if m.Gauge != nil {