From cb7cbad5f9a2823a622aaa668833ca04f50a0ea7 Mon Sep 17 00:00:00 2001 From: Boyko Date: Sat, 2 Nov 2019 17:53:32 +0200 Subject: [PATCH] WIP: status page - API and UI (#6243) * status page initial commit Signed-off-by: Boyko Lalov Signed-off-by: blalov * refactor useFetch Signed-off-by: Boyko Lalov Signed-off-by: blalov * refactoring Signed-off-by: Boyko Lalov Signed-off-by: blalov * adding tests Signed-off-by: Boyko Lalov Signed-off-by: blalov * snapshot testing Signed-off-by: Boyko Lalov Signed-off-by: blalov * fix wrong go files formatting Signed-off-by: Boyko Lalov Signed-off-by: blalov * change the snapshot library Signed-off-by: Boyko Lalov Signed-off-by: blalov * update api paths Signed-off-by: Boyko Lalov Signed-off-by: blalov * move test folder outside src Signed-off-by: Boyko Lalov Signed-off-by: blalov * useFetches tests Signed-off-by: blalov * sticky navbar Signed-off-by: Boyko Lalov Signed-off-by: blalov * handle runtimeInfo error on Gather() and add json tags to RuntimeInfo struct Signed-off-by: blalov * refactor alert managers section Signed-off-by: blalov --- web/api/v1/api.go | 47 ++ web/ui/react-app/package.json | 11 +- web/ui/react-app/src/App.css | 4 + web/ui/react-app/src/App.tsx | 2 +- web/ui/react-app/src/Graph.tsx | 2 +- web/ui/react-app/src/Navbar.tsx | 2 +- .../react-app/src/hooks/useFetches.test.tsx | 55 +++ web/ui/react-app/src/hooks/useFetches.tsx | 36 ++ web/ui/react-app/src/pages/Status.test.tsx | 72 +++ web/ui/react-app/src/pages/Status.tsx | 108 +++- .../pages/__snapshots__/Status.test.tsx.snap | 465 ++++++++++++++++++ web/ui/react-app/tsconfig.json | 2 +- web/ui/react-app/yarn.lock | 34 +- web/web.go | 56 ++- 14 files changed, 876 insertions(+), 20 deletions(-) create mode 100644 web/ui/react-app/src/hooks/useFetches.test.tsx create mode 100644 web/ui/react-app/src/hooks/useFetches.tsx create mode 100644 web/ui/react-app/src/pages/Status.test.tsx create mode 100644 web/ui/react-app/src/pages/__snapshots__/Status.test.tsx.snap diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 7d04f62ca1..61865d70cc 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -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) diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json index 66c12eda94..1a96845d15 100644 --- a/web/ui/react-app/package.json +++ b/web/ui/react-app/package.json @@ -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" + ] + } } diff --git a/web/ui/react-app/src/App.css b/web/ui/react-app/src/App.css index 5f8066f10a..536d959a8d 100644 --- a/web/ui/react-app/src/App.css +++ b/web/ui/react-app/src/App.css @@ -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; } diff --git a/web/ui/react-app/src/App.tsx b/web/ui/react-app/src/App.tsx index 56c1afa4c4..1bb258bb4a 100755 --- a/web/ui/react-app/src/App.tsx +++ b/web/ui/react-app/src/App.tsx @@ -11,7 +11,7 @@ class App extends Component { return ( <> - + diff --git a/web/ui/react-app/src/Graph.tsx b/web/ui/react-app/src/Graph.tsx index 5be47cdcb0..abb3877220 100644 --- a/web/ui/react-app/src/Graph.tsx +++ b/web/ui/react-app/src/Graph.tsx @@ -34,7 +34,7 @@ class Graph extends PureComponent { private chartRef = React.createRef(); renderLabels(labels: { [key: string]: string }) { - let labelStrings: string[] = []; + const labelStrings: string[] = []; for (const label in labels) { if (label !== '__name__') { labelStrings.push('' + label + ': ' + escapeHTML(labels[label])); diff --git a/web/ui/react-app/src/Navbar.tsx b/web/ui/react-app/src/Navbar.tsx index 92cdaf5eab..d62ed5f2e6 100644 --- a/web/ui/react-app/src/Navbar.tsx +++ b/web/ui/react-app/src/Navbar.tsx @@ -17,7 +17,7 @@ const Navigation = () => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); return ( - + Prometheus diff --git a/web/ui/react-app/src/hooks/useFetches.test.tsx b/web/ui/react-app/src/hooks/useFetches.test.tsx new file mode 100644 index 0000000000..f874d67ef3 --- /dev/null +++ b/web/ui/react-app/src/hooks/useFetches.test.tsx @@ -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(); + } + }); +}); diff --git a/web/ui/react-app/src/hooks/useFetches.tsx b/web/ui/react-app/src/hooks/useFetches.tsx new file mode 100644 index 0000000000..24b2c08fd3 --- /dev/null +++ b/web/ui/react-app/src/hooks/useFetches.tsx @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; + +const useFetches = (urls: string[], options?: RequestInit) => { + if (!urls.length) { + throw new Error("Doesn't have url to fetch."); + } + const [response, setResponse] = useState(); + const [error, setError] = useState(); + + 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; diff --git a/web/ui/react-app/src/pages/Status.test.tsx b/web/ui/react-app/src/pages/Status.test.tsx new file mode 100644 index 0000000000..38b2281dff --- /dev/null +++ b/web/ui/react-app/src/pages/Status.test.tsx @@ -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(); + 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(); + expect(wrapper.find(Alert)).toHaveLength(1); + }); + it('should fetch proper API endpoints', () => { + const useFetchSpy = jest.spyOn(useFetch, 'default'); + shallow(); + 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(); + expect(toJson(wrapper)).toMatchSnapshot(); + jest.restoreAllMocks(); + }); + }); +}); diff --git a/web/ui/react-app/src/pages/Status.tsx b/web/ui/react-app/src/pages/Status.tsx index 7fbaffac41..de901af743 100644 --- a/web/ui/react-app/src/pages/Status.tsx +++ b/web/ui/react-app/src/pages/Status.tsx @@ -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 = () =>
Status page
; +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 ( + + + Endpoint + + {alertMgrs.map(({ url }) => { + const { origin, pathname } = new URL(url); + return ( + + + {origin} + {pathname} + + + ); + })} + + ); + }, + }, + droppedAlertmanagers: { skip: true }, +}; + +const Status = () => { + const { response: data, error, isLoading } = useFetches(ENDPOINTS); + if (error) { + return ( + + Error: Error fetching status: {error.message} + + ); + } else if (isLoading) { + return ( + + ); + } + return data + ? data.map((statuses, i) => { + return ( + +

{sectionTitles[i]}

+ + + {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 ( + + + + + ); + })} + +
+ {title} + {customizeValue(v)}
+
+ ); + }) + : null; +}; + +export default Status as FC; diff --git a/web/ui/react-app/src/pages/__snapshots__/Status.test.tsx.snap b/web/ui/react-app/src/pages/__snapshots__/Status.test.tsx.snap new file mode 100644 index 0000000000..35bff1123f --- /dev/null +++ b/web/ui/react-app/src/pages/__snapshots__/Status.test.tsx.snap @@ -0,0 +1,465 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status Snapshot testing should match table snapshot 1`] = ` +Array [ + +

+ Runtime Information +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Start time + + Wed, 30 Oct 2019 20:03:23 GMT +
+ Working directory + + /home/boyskila/Desktop/prometheus +
+ Configuration reload + + Successful +
+ Last successful configuration reload + + 2019-10-30T22:03:23+02:00 +
+ Head chunks + + 1383 +
+ Head time series + + 461 +
+ WAL corruptions + + 0 +
+ Goroutines + + 37 +
+ GOMAXPROCS + + 4 +
+ GOGC + +
+ GODEBUG + +
+ Storage retention + + 15d +
+
, + +

+ Build Information +

+ + + + + + + + + + + + + + + + + + + + + + +
+ version + +
+ revision + +
+ branch + +
+ buildUser + +
+ buildDate + +
+ goVersion + + go1.13.3 +
+
, + +

+ Alertmanagers +

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ Endpoint +
+ + https://1.2.3.4:9093 + + /api/v1/alerts +
+ + https://1.2.3.5:9093 + + /api/v1/alerts +
+ + https://1.2.3.6:9093 + + /api/v1/alerts +
+ + https://1.2.3.7:9093 + + /api/v1/alerts +
+ + https://1.2.3.8:9093 + + /api/v1/alerts +
+ + https://1.2.3.9:9093 + + /api/v1/alerts +
+
, +] +`; diff --git a/web/ui/react-app/tsconfig.json b/web/ui/react-app/tsconfig.json index 0980b23fa1..46eac8d0ba 100644 --- a/web/ui/react-app/tsconfig.json +++ b/web/ui/react-app/tsconfig.json @@ -20,6 +20,6 @@ "jsx": "preserve" }, "include": [ - "src" + "src", "test" ] } diff --git a/web/ui/react-app/yarn.lock b/web/ui/react-app/yarn.lock index b2cab3bbc2..5a14afce32 100644 --- a/web/ui/react-app/yarn.lock +++ b/web/ui/react-app/yarn.lock @@ -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== diff --git a/web/web.go b/web/web.go index 37ec4ee91a..c644c66245 100644 --- a/web/web.go +++ b/web/web.go @@ -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 {