Skip to content

Commit df85652

Browse files
authored
Add Redis CPU Usage panel (#96)
* Add Redis CPU Usage panel * Fix tests
1 parent 6f17eac commit df85652

File tree

16 files changed

+1230
-0
lines changed

16 files changed

+1230
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Fix Docker plugins provisioning (#90)
1010
- Upgrade to Grafana 8.3.0 (#93)
1111
- Fix LGTM and Update Panel Options (#95)
12+
- Add Redis CPU Usage panel (#96)
1213

1314
## 2.1.0 (2021-11-10)
1415

src/img/redis-cpu-usage-graph.png

135 KB
Loading

src/plugin.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@
7878
{
7979
"name": "RedisGears Panel",
8080
"type": "panel"
81+
},
82+
{
83+
"name": "Redis CPU Panel",
84+
"type": "panel"
8185
}
8286
],
8387
"info": {
@@ -125,6 +129,10 @@
125129
{
126130
"name": "Max Memory Keys Panel",
127131
"path": "img/redis-keys-panel.png"
132+
},
133+
{
134+
"name": "Redis CPU Panel",
135+
"path": "img/redis-cpu-usage-graph.png"
128136
}
129137
],
130138
"updated": "%TODAY%",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './redis-cpu-panel-graph';
2+
export * from './redis-cpu-panel';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './redis-cpu-panel-graph';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { shallow } from 'enzyme';
2+
import React from 'react';
3+
import { DataFrame, dateTime, dateTimeParse } from '@grafana/data';
4+
import { RedisCPUPanelGraph } from './redis-cpu-panel-graph';
5+
6+
/**
7+
* Table View
8+
*/
9+
describe('RedisCPUPanelGraph', () => {
10+
/**
11+
* getGraphSeries
12+
*/
13+
describe('getGraphSeries', () => {
14+
it('Should return series for each command', () => {
15+
const seriesMap = {
16+
get: [
17+
{
18+
time: dateTime(),
19+
value: 0,
20+
},
21+
{
22+
time: dateTime(),
23+
value: 0,
24+
},
25+
],
26+
info: [
27+
{
28+
time: dateTime(),
29+
value: 10,
30+
},
31+
{
32+
time: dateTime().add(10, 'seconds'),
33+
value: 20,
34+
},
35+
],
36+
};
37+
const result: DataFrame[] = RedisCPUPanelGraph.getGraphDataFrame(seriesMap);
38+
expect(result[0].length).toEqual(2);
39+
expect(result[0].fields[0].values.length).toEqual(2);
40+
expect(result[1].length).toEqual(2);
41+
});
42+
});
43+
44+
/**
45+
* Get Time Range
46+
*/
47+
describe('getTimeRange', () => {
48+
it('Should apply timeRange.raw.from and find series with the biggest items and take time', () => {
49+
const timeRange = {
50+
from: dateTime(),
51+
to: dateTime(),
52+
raw: {
53+
from: '6h',
54+
to: 'now',
55+
},
56+
};
57+
const result = RedisCPUPanelGraph.getTimeRange(timeRange, 'browser');
58+
expect(result.from.valueOf()).toEqual(dateTimeParse('6h').valueOf());
59+
expect(result.to.startOf('hour').valueOf()).toEqual(dateTime().startOf('hour').valueOf());
60+
});
61+
});
62+
63+
/**
64+
* Getting new props
65+
*/
66+
describe('Getting new props', () => {
67+
const getComponent = (props: any = {}) => <RedisCPUPanelGraph {...props} />;
68+
69+
it('Should update timeRange when gets a new seriesMap or timeRange', () => {
70+
const wrapper = shallow<RedisCPUPanelGraph>(
71+
getComponent({
72+
seriesMap: { get: [{ time: dateTime(), value: 1 }] },
73+
timeRange: { raw: { from: dateTime() } },
74+
})
75+
);
76+
const currentTimeRange = wrapper.state().timeRange;
77+
wrapper.setProps({
78+
seriesMap: { get: [{ time: dateTime(), value: 2 }] },
79+
});
80+
expect(currentTimeRange !== wrapper.state().timeRange).toBeTruthy();
81+
});
82+
83+
it('Should return gathering results div if data frame is empty', () => {
84+
const wrapper = shallow<RedisCPUPanelGraph>(
85+
getComponent({
86+
seriesMap: {},
87+
timeRange: { raw: { from: dateTime() } },
88+
})
89+
);
90+
91+
const div = wrapper.findWhere((node) => node.name() === 'div');
92+
expect(div.exists()).toBeTruthy();
93+
});
94+
95+
it('Should return Time Series if data frame has data', () => {
96+
const wrapper = shallow<RedisCPUPanelGraph>(
97+
getComponent({
98+
seriesMap: { get: [{ time: dateTime(), value: 1 }] },
99+
timeRange: { raw: { from: dateTime() } },
100+
})
101+
);
102+
103+
const timeSeries = wrapper.findWhere((node) => node.name() === 'TimeSeries');
104+
expect(timeSeries.exists()).toBeTruthy();
105+
});
106+
});
107+
});
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React, { PureComponent } from 'react';
2+
import {
3+
DataFrame,
4+
DateTime,
5+
dateTimeParse,
6+
FieldColorModeId,
7+
FieldType,
8+
getDisplayProcessor,
9+
GraphSeriesValue,
10+
PanelProps,
11+
TimeRange,
12+
TimeZone,
13+
toDataFrame,
14+
} from '@grafana/data';
15+
import { config } from '@grafana/runtime';
16+
import { colors, LegendDisplayMode, TimeSeries, TooltipDisplayMode, TooltipPlugin } from '@grafana/ui';
17+
import { PanelOptions, SeriesMap, SeriesValue } from '../../types';
18+
19+
/**
20+
* Graph Properties
21+
*/
22+
interface Props extends PanelProps<PanelOptions> {
23+
/**
24+
* Series
25+
*
26+
* @type {SeriesMap}
27+
*/
28+
seriesMap: SeriesMap;
29+
}
30+
31+
/**
32+
* State
33+
*/
34+
interface State {
35+
/**
36+
* Time Range
37+
*
38+
* @type {TimeRange}
39+
*/
40+
timeRange: TimeRange;
41+
}
42+
43+
/**
44+
* Graph View
45+
*/
46+
export class RedisCPUPanelGraph extends PureComponent<Props, State> {
47+
/**
48+
* Convert seriesMap to Data Frames
49+
* @param seriesMap
50+
*/
51+
static getGraphDataFrame(seriesMap: SeriesMap): DataFrame[] {
52+
return Object.entries(seriesMap).reduce(
53+
(acc: DataFrame[], [command, seriesValues]: [string, SeriesValue[]], index) => {
54+
const { times, values } = seriesValues.reduce(
55+
(acc: { times: DateTime[]; values: number[] }, { time, value }) => {
56+
return {
57+
times: acc.times.concat([time]),
58+
values: acc.values.concat([value]),
59+
};
60+
},
61+
{ times: [], values: [] }
62+
);
63+
64+
/**
65+
* Color
66+
*/
67+
const color = colors[index % colors.length];
68+
69+
/**
70+
* Data Frame
71+
*/
72+
const seriesDataFrame = toDataFrame({
73+
name: command,
74+
fields: [
75+
{
76+
type: FieldType.time,
77+
name: 'time',
78+
values: times,
79+
},
80+
{
81+
type: FieldType.number,
82+
name: ' ',
83+
values,
84+
config: {
85+
unit: 'percent',
86+
color: {
87+
fixedColor: color,
88+
mode: FieldColorModeId.Fixed,
89+
},
90+
},
91+
},
92+
],
93+
});
94+
95+
/**
96+
* Fields
97+
*/
98+
seriesDataFrame.fields = seriesDataFrame.fields.map((field) => ({
99+
...field,
100+
display: getDisplayProcessor({ field, theme: config.theme2 }),
101+
}));
102+
103+
/**
104+
* Push values
105+
*/
106+
const data: GraphSeriesValue[][] = [];
107+
for (let i = 0; i < times.length; i++) {
108+
data.push([times[i].valueOf(), values[i]]);
109+
}
110+
111+
return acc.concat(seriesDataFrame);
112+
},
113+
[]
114+
);
115+
}
116+
117+
/**
118+
* Get timeRange from timeRange.raw
119+
*
120+
* @param timeRange
121+
*/
122+
static getTimeRange(timeRange: TimeRange, timeZone: TimeZone): TimeRange {
123+
let fromTime = dateTimeParse(timeRange.raw.from, { timeZone });
124+
const toTime = dateTimeParse(timeRange.raw.to, { timeZone });
125+
126+
return {
127+
from: fromTime,
128+
to: toTime,
129+
raw: {
130+
from: timeRange.raw.from,
131+
to: toTime,
132+
},
133+
};
134+
}
135+
136+
/**
137+
* State
138+
*/
139+
state = {
140+
timeRange: RedisCPUPanelGraph.getTimeRange(this.props.timeRange, this.props.timeZone),
141+
};
142+
143+
/**
144+
* getDerivedStateFromProps
145+
*
146+
* @param props
147+
*/
148+
static getDerivedStateFromProps(props: Readonly<Props>) {
149+
return {
150+
timeRange: RedisCPUPanelGraph.getTimeRange(props.timeRange, props.timeZone),
151+
};
152+
}
153+
154+
/**
155+
* Render
156+
*/
157+
render() {
158+
const { width, height, seriesMap } = this.props;
159+
const { timeRange } = this.state;
160+
161+
/**
162+
* Convert to Data Frames
163+
*/
164+
const dataFrames = RedisCPUPanelGraph.getGraphDataFrame(seriesMap);
165+
if (!dataFrames.length) {
166+
return <div>Gathering usage data...</div>;
167+
}
168+
169+
/**
170+
* Return Time Series
171+
*/
172+
return (
173+
<TimeSeries
174+
frames={dataFrames}
175+
width={width}
176+
height={height}
177+
timeRange={timeRange}
178+
legend={{ displayMode: LegendDisplayMode?.List, placement: 'bottom', calcs: [] }}
179+
timeZone={this.props.timeZone}
180+
>
181+
{(config, alignedDataFrame) => {
182+
return (
183+
<TooltipPlugin
184+
config={config}
185+
data={alignedDataFrame}
186+
mode={TooltipDisplayMode.Multi}
187+
timeZone={this.props.timeZone}
188+
/>
189+
);
190+
}}
191+
</TimeSeries>
192+
);
193+
}
194+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './redis-cpu-panel';

0 commit comments

Comments
 (0)