mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
Adding TSDB Stats Page in React UI (#6281)
Signed-off-by: Sharad Gaur <sgaur@splunk.com>
This commit is contained in:
parent
fc309a35bb
commit
a85e7aac0e
|
@ -797,6 +797,67 @@ $ curl http://localhost:9090/api/v1/status/buildinfo
|
|||
|
||||
**NOTE**: The exact returned build properties may change without notice between Prometheus versions.
|
||||
|
||||
### TSDB Stats
|
||||
|
||||
The following endpoint returns various cardinality statistics about the Prometheus TSDB:
|
||||
|
||||
```
|
||||
GET /api/v1/status/tsdb
|
||||
```
|
||||
- **seriesCountByMetricName:** This will provide a list of metrics names and their series count.
|
||||
- **labelValueCountByLabelName:** This will provide a list of the label names and their value count.
|
||||
- **memoryInBytesByLabelName** This will provide a list of the label names and memory used in bytes. Memory usage is calculated by adding the length of all values for a given label name.
|
||||
- **seriesCountByLabelPair** This will provide a list of label value pairs and their series count.
|
||||
|
||||
```json
|
||||
$ curl http://localhost:9090/api/v1/status/tsdb
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"seriesCountByMetricName": [
|
||||
{
|
||||
"name": "net_conntrack_dialer_conn_failed_total",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"name": "prometheus_http_request_duration_seconds_bucket",
|
||||
"value": 20
|
||||
}
|
||||
],
|
||||
"labelValueCountByLabelName": [
|
||||
{
|
||||
"name": "__name__",
|
||||
"value": 211
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"value": 3
|
||||
}
|
||||
],
|
||||
"memoryInBytesByLabelName": [
|
||||
{
|
||||
"name": "__name__",
|
||||
"value": 8266
|
||||
},
|
||||
{
|
||||
"name": "instance",
|
||||
"value": 28
|
||||
}
|
||||
],
|
||||
"seriesCountByLabelValuePair": [
|
||||
{
|
||||
"name": "job=prometheus",
|
||||
"value": 425
|
||||
},
|
||||
{
|
||||
"name": "instance=localhost:9090",
|
||||
"value": 425
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*New in v2.14*
|
||||
|
||||
## TSDB Admin APIs
|
||||
|
|
|
@ -36,6 +36,8 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/common/route"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
"github.com/prometheus/prometheus/tsdb/index"
|
||||
tsdbLabels "github.com/prometheus/prometheus/tsdb/labels"
|
||||
|
||||
"github.com/prometheus/prometheus/config"
|
||||
|
@ -158,6 +160,7 @@ type TSDBAdmin interface {
|
|||
Delete(mint, maxt int64, ms ...tsdbLabels.Matcher) error
|
||||
Dir() string
|
||||
Snapshot(dir string, withHead bool) error
|
||||
Head() *tsdb.Head
|
||||
}
|
||||
|
||||
// API can register a set of endpoints in a router and handle
|
||||
|
@ -278,6 +281,7 @@ func (api *API) Register(r *route.Router) {
|
|||
r.Get("/status/runtimeinfo", wrap(api.serveRuntimeInfo))
|
||||
r.Get("/status/buildinfo", wrap(api.serveBuildInfo))
|
||||
r.Get("/status/flags", wrap(api.serveFlags))
|
||||
r.Get("/status/tsdb", wrap(api.serveTSDBStatus))
|
||||
r.Post("/read", api.ready(http.HandlerFunc(api.remoteRead)))
|
||||
|
||||
r.Get("/alerts", wrap(api.alerts))
|
||||
|
@ -915,6 +919,45 @@ func (api *API) serveFlags(r *http.Request) apiFuncResult {
|
|||
return apiFuncResult{api.flagsMap, nil, nil, nil}
|
||||
}
|
||||
|
||||
// stat holds the information about individual cardinality.
|
||||
type stat struct {
|
||||
Name string `json:"name"`
|
||||
Value uint64 `json:"value"`
|
||||
}
|
||||
|
||||
// tsdbStatus has information of cardinality statistics from postings.
|
||||
type tsdbStatus struct {
|
||||
SeriesCountByMetricName []stat `json:"seriesCountByMetricName"`
|
||||
LabelValueCountByLabelName []stat `json:"labelValueCountByLabelName"`
|
||||
MemoryInBytesByLabelName []stat `json:"memoryInBytesByLabelName"`
|
||||
SeriesCountByLabelValuePair []stat `json:"seriesCountByLabelValuePair"`
|
||||
}
|
||||
|
||||
func (api *API) serveTSDBStatus(r *http.Request) apiFuncResult {
|
||||
db := api.db()
|
||||
if db == nil {
|
||||
return apiFuncResult{nil, &apiError{errorUnavailable, errors.New("TSDB not ready")}, nil, nil}
|
||||
}
|
||||
convert := func(stats []index.Stat) []stat {
|
||||
result := make([]stat, 0, len(stats))
|
||||
for _, item := range stats {
|
||||
item := stat{Name: item.Name, Value: item.Count}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
posting := db.Head().PostingsCardinalityStats(model.MetricNameLabel)
|
||||
response := tsdbStatus{
|
||||
SeriesCountByMetricName: convert(posting.CardinalityMetricsStats),
|
||||
LabelValueCountByLabelName: convert(posting.CardinalityLabelStats),
|
||||
MemoryInBytesByLabelName: convert(posting.LabelValueStats),
|
||||
SeriesCountByLabelValuePair: convert(posting.LabelValuePairsStats),
|
||||
}
|
||||
|
||||
return apiFuncResult{response, nil, nil, nil}
|
||||
}
|
||||
|
||||
func (api *API) remoteRead(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := api.remoteReadGate.Start(ctx); err != nil {
|
||||
|
|
|
@ -39,6 +39,7 @@ import (
|
|||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/common/promlog"
|
||||
"github.com/prometheus/common/route"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
tsdbLabels "github.com/prometheus/prometheus/tsdb/labels"
|
||||
|
||||
"github.com/prometheus/prometheus/config"
|
||||
|
@ -1332,6 +1333,10 @@ func (f *fakeDB) Dir() string {
|
|||
return dir
|
||||
}
|
||||
func (f *fakeDB) Snapshot(dir string, withHead bool) error { return f.err }
|
||||
func (f *fakeDB) Head() *tsdb.Head {
|
||||
h, _ := tsdb.NewHead(nil, nil, nil, 1000)
|
||||
return h
|
||||
}
|
||||
|
||||
func TestAdminEndpoints(t *testing.T) {
|
||||
tsdb, tsdbWithError := &fakeDB{}, &fakeDB{err: errors.New("some error")}
|
||||
|
@ -1842,6 +1847,47 @@ func TestRespond(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTSDBStatus(t *testing.T) {
|
||||
tsdb := &fakeDB{}
|
||||
tsdbStatusAPI := func(api *API) apiFunc { return api.serveTSDBStatus }
|
||||
|
||||
for i, tc := range []struct {
|
||||
db *fakeDB
|
||||
endpoint func(api *API) apiFunc
|
||||
method string
|
||||
values url.Values
|
||||
|
||||
errType errorType
|
||||
}{
|
||||
// Tests for the TSDB Status endpoint.
|
||||
{
|
||||
db: tsdb,
|
||||
endpoint: tsdbStatusAPI,
|
||||
|
||||
errType: errorNone,
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
api := &API{
|
||||
db: func() TSDBAdmin {
|
||||
if tc.db != nil {
|
||||
return tc.db
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
endpoint := tc.endpoint(api)
|
||||
req, err := http.NewRequest(tc.method, fmt.Sprintf("?%s", tc.values.Encode()), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error when creating test request: %s", err)
|
||||
}
|
||||
res := endpoint(req)
|
||||
assertAPIError(t, res.err, tc.errType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is a global to avoid the benchmark being optimized away.
|
||||
var testResponseWriter = httptest.ResponseRecorder{}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import App from './App';
|
|||
import Navigation from './Navbar';
|
||||
import { Container } from 'reactstrap';
|
||||
import { Router } from '@reach/router';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, TSDBStatus, PanelList } from './pages';
|
||||
|
||||
describe('App', () => {
|
||||
const app = shallow(<App pathPrefix="/path/prefix" />);
|
||||
|
@ -13,7 +13,7 @@ describe('App', () => {
|
|||
expect(app.find(Navigation)).toHaveLength(1);
|
||||
});
|
||||
it('routes', () => {
|
||||
[Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList].forEach(component => {
|
||||
[Alerts, Config, Flags, Rules, Services, Status, Targets, TSDBStatus, PanelList].forEach(component => {
|
||||
const c = app.find(component);
|
||||
expect(c).toHaveLength(1);
|
||||
expect(c.prop('pathPrefix')).toBe('/path/prefix');
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Container } from 'reactstrap';
|
|||
|
||||
import './App.css';
|
||||
import { Router, Redirect } from '@reach/router';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList } from './pages';
|
||||
import { Alerts, Config, Flags, Rules, Services, Status, Targets, TSDBStatus, PanelList } from './pages';
|
||||
import PathPrefixProps from './PathPrefixProps';
|
||||
|
||||
const App: FC<PathPrefixProps> = ({ pathPrefix }) => {
|
||||
|
@ -22,6 +22,7 @@ const App: FC<PathPrefixProps> = ({ pathPrefix }) => {
|
|||
<Rules path="/rules" pathPrefix={pathPrefix} />
|
||||
<Services path="/service-discovery" pathPrefix={pathPrefix} />
|
||||
<Status path="/status" pathPrefix={pathPrefix} />
|
||||
<TSDBStatus path="/tsdb-status" pathPrefix={pathPrefix} />
|
||||
<Targets path="/targets" pathPrefix={pathPrefix} />
|
||||
</Router>
|
||||
</Container>
|
||||
|
|
|
@ -43,6 +43,9 @@ const Navigation: FC<PathPrefixProps> = ({ pathPrefix }) => {
|
|||
<DropdownItem tag={Link} to={`${pathPrefix}/new/status`}>
|
||||
Runtime & Build Information
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/tsdb-status`}>
|
||||
TSDB Status
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to={`${pathPrefix}/new/flags`}>
|
||||
Command-Line Flags
|
||||
</DropdownItem>
|
||||
|
|
125
web/ui/react-app/src/pages/TSDBStatus.test.tsx
Normal file
125
web/ui/react-app/src/pages/TSDBStatus.test.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
import * as React from 'react';
|
||||
import { mount, shallow, ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Alert, Table, Badge } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { TSDBStatus } from '.';
|
||||
import { TSDBMap, Stats } from './TSDBStatus';
|
||||
|
||||
const fakeTSDBStatusResponse: {
|
||||
status: string;
|
||||
data: TSDBMap;
|
||||
} = {
|
||||
status: 'success',
|
||||
data: {
|
||||
labelValueCountByLabelName: [
|
||||
{
|
||||
name: '__name__',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
seriesCountByMetricName: [
|
||||
{
|
||||
name: 'scrape_duration_seconds',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'scrape_samples_scraped',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
memoryInBytesByLabelName: [
|
||||
{
|
||||
name: '__name__',
|
||||
value: 103,
|
||||
},
|
||||
],
|
||||
seriesCountByLabelValuePair: [
|
||||
{
|
||||
name: 'instance=localhost:9100',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('TSDB Stats', () => {
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
it('before data is returned', () => {
|
||||
const tsdbStatus = shallow(<TSDBStatus />);
|
||||
const icon = tsdbStatus.find(FontAwesomeIcon);
|
||||
expect(icon.prop('icon')).toEqual(faSpinner);
|
||||
expect(icon.prop('spin')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when an error is returned', () => {
|
||||
it('displays an alert', async () => {
|
||||
const mock = fetch.mockReject(new Error('error loading tsdb status'));
|
||||
|
||||
let page: ReactWrapper;
|
||||
await act(async () => {
|
||||
page = mount(<TSDBStatus pathPrefix="/path/prefix" />);
|
||||
});
|
||||
page.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('/path/prefix/api/v1/status/tsdb', undefined);
|
||||
const alert = page.find(Alert);
|
||||
expect(alert.prop('color')).toBe('danger');
|
||||
expect(alert.text()).toContain('error loading tsdb status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table Data Validation', () => {
|
||||
it('Table Test', async () => {
|
||||
const tables = [
|
||||
{
|
||||
data: fakeTSDBStatusResponse.data.labelValueCountByLabelName,
|
||||
table_index: 0,
|
||||
},
|
||||
{
|
||||
data: fakeTSDBStatusResponse.data.seriesCountByMetricName,
|
||||
table_index: 1,
|
||||
},
|
||||
{
|
||||
data: fakeTSDBStatusResponse.data.memoryInBytesByLabelName,
|
||||
table_index: 2,
|
||||
},
|
||||
{
|
||||
data: fakeTSDBStatusResponse.data.seriesCountByLabelValuePair,
|
||||
table_index: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const mock = fetch.mockResponse(JSON.stringify(fakeTSDBStatusResponse));
|
||||
let page: ReactWrapper;
|
||||
await act(async () => {
|
||||
page = mount(<TSDBStatus pathPrefix="/path/prefix" />);
|
||||
});
|
||||
page.update();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith('/path/prefix/api/v1/status/tsdb', undefined);
|
||||
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
let data = tables[i].data;
|
||||
let table = page
|
||||
.find(Table)
|
||||
.at(tables[i].table_index)
|
||||
.find('tbody');
|
||||
let rows = table.find('tr');
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const firstRowColumns = rows
|
||||
.at(i)
|
||||
.find('td')
|
||||
.map(column => column.text());
|
||||
expect(rows.length).toBe(data.length);
|
||||
expect(firstRowColumns[0]).toBe(data[i].name);
|
||||
expect(firstRowColumns[1]).toBe(data[i].value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
87
web/ui/react-app/src/pages/TSDBStatus.tsx
Normal file
87
web/ui/react-app/src/pages/TSDBStatus.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import React, { FC, Fragment } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { Alert, Table } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useFetch } from '../utils/useFetch';
|
||||
import PathPrefixProps from '../PathPrefixProps';
|
||||
|
||||
export interface Stats {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TSDBMap {
|
||||
seriesCountByMetricName: Array<Stats>;
|
||||
labelValueCountByLabelName: Array<Stats>;
|
||||
memoryInBytesByLabelName: Array<Stats>;
|
||||
seriesCountByLabelValuePair: Array<Stats>;
|
||||
}
|
||||
|
||||
const paddingStyle = {
|
||||
padding: '10px',
|
||||
};
|
||||
|
||||
function createTable(title: string, unit: string, stats: Array<Stats>) {
|
||||
return (
|
||||
<div style={paddingStyle}>
|
||||
<h3>{title}</h3>
|
||||
<Table bordered={true} size="sm" striped={true}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>{unit}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.map((element: Stats, i: number) => {
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<tr>
|
||||
<td>{element.name}</td>
|
||||
<td>{element.value}</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TSDBStatus: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||
const { response, error } = useFetch(`${pathPrefix}/api/v1/status/tsdb`);
|
||||
const headStats = () => {
|
||||
const stats: TSDBMap = response && response.data;
|
||||
if (error) {
|
||||
return (
|
||||
<Alert color="danger">
|
||||
<strong>Error:</strong> Error fetching TSDB Status: {error.message}
|
||||
</Alert>
|
||||
);
|
||||
} else if (stats) {
|
||||
return (
|
||||
<div>
|
||||
<div style={paddingStyle}>
|
||||
<h3>Head Cardinality Stats</h3>
|
||||
</div>
|
||||
{createTable('Top 10 label names with value count', 'Count', stats.labelValueCountByLabelName)}
|
||||
{createTable('Top 10 series count by metric names', 'Count', stats.seriesCountByMetricName)}
|
||||
{createTable('Top 10 label names with high memory usage', 'Bytes', stats.memoryInBytesByLabelName)}
|
||||
{createTable('Top 10 series count by label value pairs', 'Count', stats.seriesCountByLabelValuePair)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <FontAwesomeIcon icon={faSpinner} spin />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>TSDB Status</h2>
|
||||
{headStats()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TSDBStatus;
|
|
@ -6,5 +6,6 @@ import Services from './Services';
|
|||
import Status from './Status';
|
||||
import Targets from './targets/Targets';
|
||||
import PanelList from './PanelList';
|
||||
import TSDBStatus from './TSDBStatus';
|
||||
|
||||
export { Alerts, Config, Flags, Rules, Services, Status, Targets, PanelList };
|
||||
export { Alerts, Config, Flags, Rules, Services, Status, Targets, TSDBStatus, PanelList };
|
||||
|
|
Loading…
Reference in a new issue