React UI: Implement missing TSDB head stats section (#7876)

* React UI: Implement missing TSDB head stats section

Signed-off-by: Dustin Hooten <dhooten@splunk.com>

* Add break

Signed-off-by: Dustin Hooten <dhooten@splunk.com>
This commit is contained in:
Dustin Hooten 2020-09-29 14:05:33 -06:00 committed by GitHub
parent 19c190b406
commit 916dbd4c8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 35 deletions

View file

@ -839,7 +839,6 @@ $ curl http://localhost:9090/api/v1/status/runtimeinfo
"CWD": "/",
"reloadConfigSuccess": true,
"lastConfigTime": "2019-11-02T17:23:59+01:00",
"chunkCount": 873,
"timeSeriesCount": 873,
"corruptionCount": 0,
"goroutineCount": 48,
@ -891,6 +890,11 @@ The following endpoint returns various cardinality statistics about the Promethe
```
GET /api/v1/status/tsdb
```
- **headStats**: This provides the following data about the head block of the TSDB:
- **numSeries**: The number of series.
- **chunkCount**: The number of chunks.
- **minTime**: The current minimum timestamp in milliseconds.
- **maxTime**: The current maximum timestamp in milliseconds.
- **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.
@ -901,6 +905,12 @@ $ curl http://localhost:9090/api/v1/status/tsdb
{
"status": "success",
"data": {
"headStats": {
"numSeries": 508,
"chunkCount": 937,
"minTime": 1591516800000,
"maxTime": 1598896800143,
},
"seriesCountByMetricName": [
{
"name": "net_conntrack_dialer_conn_failed_total",

View file

@ -133,8 +133,6 @@ type RuntimeInfo struct {
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"`
@ -194,6 +192,7 @@ type API struct {
CORSOrigin *regexp.Regexp
buildInfo *PrometheusVersion
runtimeInfo func() (RuntimeInfo, error)
gatherer prometheus.Gatherer
}
func init() {
@ -222,6 +221,7 @@ func NewAPI(
CORSOrigin *regexp.Regexp,
runtimeInfo func() (RuntimeInfo, error),
buildInfo *PrometheusVersion,
gatherer prometheus.Gatherer,
) *API {
return &API{
QueryEngine: qe,
@ -245,6 +245,7 @@ func NewAPI(
CORSOrigin: CORSOrigin,
runtimeInfo: runtimeInfo,
buildInfo: buildInfo,
gatherer: gatherer,
}
}
@ -1153,12 +1154,21 @@ type stat struct {
Value uint64 `json:"value"`
}
// HeadStats has information about the TSDB head.
type HeadStats struct {
NumSeries uint64 `json:"numSeries"`
ChunkCount int64 `json:"chunkCount"`
MinTime int64 `json:"minTime"`
MaxTime int64 `json:"maxTime"`
}
// 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"`
HeadStats HeadStats `json:"headStats"`
SeriesCountByMetricName []stat `json:"seriesCountByMetricName"`
LabelValueCountByLabelName []stat `json:"labelValueCountByLabelName"`
MemoryInBytesByLabelName []stat `json:"memoryInBytesByLabelName"`
SeriesCountByLabelValuePair []stat `json:"seriesCountByLabelValuePair"`
}
func convertStats(stats []index.Stat) []stat {
@ -1175,8 +1185,27 @@ func (api *API) serveTSDBStatus(*http.Request) apiFuncResult {
if err != nil {
return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil}
}
metrics, err := api.gatherer.Gather()
if err != nil {
return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("error gathering runtime status: %s", err)}, nil, nil}
}
chunkCount := int64(math.NaN())
for _, mF := range metrics {
if *mF.Name == "prometheus_tsdb_head_chunks" {
m := *mF.Metric[0]
if m.Gauge != nil {
chunkCount = int64(m.Gauge.GetValue())
break
}
}
}
return apiFuncResult{tsdbStatus{
HeadStats: HeadStats{
NumSeries: s.NumSeries,
ChunkCount: chunkCount,
MinTime: s.MinTime,
MaxTime: s.MaxTime,
},
SeriesCountByMetricName: convertStats(s.IndexPostingStats.CardinalityMetricsStats),
LabelValueCountByLabelName: convertStats(s.IndexPostingStats.CardinalityLabelStats),
MemoryInBytesByLabelName: convertStats(s.IndexPostingStats.LabelValueStats),

View file

@ -2733,7 +2733,7 @@ func TestTSDBStatus(t *testing.T) {
} {
tc := tc
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
api := &API{db: tc.db}
api := &API{db: tc.db, gatherer: prometheus.DefaultGatherer}
endpoint := tc.endpoint(api)
req, err := http.NewRequest(tc.method, fmt.Sprintf("?%s", tc.values.Encode()), nil)
if err != nil {

View file

@ -11,8 +11,6 @@ describe('Status', () => {
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,

View file

@ -21,8 +21,6 @@ export const statusConfig: Record<
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' },

View file

@ -12,6 +12,12 @@ const fakeTSDBStatusResponse: {
} = {
status: 'success',
data: {
headStats: {
numSeries: 508,
chunkCount: 937,
minTime: 1591516800000,
maxTime: 1598896800143,
},
labelValueCountByLabelName: [
{
name: '__name__',
@ -51,22 +57,10 @@ describe('TSDB Stats', () => {
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,
},
fakeTSDBStatusResponse.data.labelValueCountByLabelName,
fakeTSDBStatusResponse.data.seriesCountByMetricName,
fakeTSDBStatusResponse.data.memoryInBytesByLabelName,
fakeTSDBStatusResponse.data.seriesCountByLabelValuePair,
];
const mock = fetchMock.mockResponse(JSON.stringify(fakeTSDBStatusResponse));
@ -81,11 +75,22 @@ describe('TSDB Stats', () => {
credentials: 'same-origin',
});
const headStats = page
.find(Table)
.at(0)
.find('tbody')
.find('td');
['508', '937', '2020-06-07T08:00:00.000Z (1591516800000)', '2020-08-31T18:00:00.143Z (1598896800143)'].forEach(
(value, i) => {
expect(headStats.at(i).text()).toEqual(value);
}
);
for (let i = 0; i < tables.length; i++) {
const data = tables[i].data;
const data = tables[i];
const table = page
.find(Table)
.at(tables[i].table_index)
.at(i + 1)
.find('tbody');
const rows = table.find('tr');
for (let i = 0; i < data.length; i++) {

View file

@ -11,7 +11,15 @@ interface Stats {
value: number;
}
interface HeadStats {
numSeries: number;
chunkCount: number;
minTime: number;
maxTime: number;
}
export interface TSDBMap {
headStats: HeadStats;
seriesCountByMetricName: Stats[];
labelValueCountByLabelName: Stats[];
memoryInBytesByLabelName: Stats[];
@ -19,14 +27,42 @@ export interface TSDBMap {
}
export const TSDBStatusContent: FC<TSDBMap> = ({
headStats,
labelValueCountByLabelName,
seriesCountByMetricName,
memoryInBytesByLabelName,
seriesCountByLabelValuePair,
}) => {
const unixToTime = (unix: number): string => new Date(unix).toISOString();
const { chunkCount, numSeries, minTime, maxTime } = headStats;
const stats = [
{ header: 'Number of Series', value: numSeries },
{ header: 'Number of Chunks', value: chunkCount },
{ header: 'Current Min Time', value: `${unixToTime(minTime)} (${minTime})` },
{ header: 'Current Max Time', value: `${unixToTime(maxTime)} (${maxTime})` },
];
return (
<div>
<h2>TSDB Status</h2>
<h3 className="p-2">Head Stats</h3>
<div className="p-2">
<Table bordered size="sm" striped>
<thead>
<tr>
{stats.map(({ header }) => {
return <th key={header}>{header}</th>;
})}
</tr>
</thead>
<tbody>
<tr>
{stats.map(({ header, value }) => {
return <td key={header}>{value}</td>;
})}
</tr>
</tbody>
</Table>
</div>
<h3 className="p-2">Head Cardinality Stats</h3>
{[
{ title: 'Top 10 label names with value count', stats: labelValueCountByLabelName },

View file

@ -321,6 +321,7 @@ func New(logger log.Logger, o *Options) *Handler {
h.options.CORSOrigin,
h.runtimeInfo,
h.versionInfo,
o.Gatherer,
)
if o.RoutePrefix != "/" {
@ -796,10 +797,6 @@ func (h *Handler) runtimeInfo() (api_v1.RuntimeInfo, error) {
}
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":