Skip to content

Commit

Permalink
Add Redis CPU Usage panel (#96)
Browse files Browse the repository at this point in the history
* Add Redis CPU Usage panel

* Fix tests
  • Loading branch information
mikhail-vl authored Dec 7, 2021
1 parent 6f17eac commit df85652
Show file tree
Hide file tree
Showing 16 changed files with 1,230 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Fix Docker plugins provisioning (#90)
- Upgrade to Grafana 8.3.0 (#93)
- Fix LGTM and Update Panel Options (#95)
- Add Redis CPU Usage panel (#96)

## 2.1.0 (2021-11-10)

Expand Down
Binary file added src/img/redis-cpu-usage-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
{
"name": "RedisGears Panel",
"type": "panel"
},
{
"name": "Redis CPU Panel",
"type": "panel"
}
],
"info": {
Expand Down Expand Up @@ -125,6 +129,10 @@
{
"name": "Max Memory Keys Panel",
"path": "img/redis-keys-panel.png"
},
{
"name": "Redis CPU Panel",
"path": "img/redis-cpu-usage-graph.png"
}
],
"updated": "%TODAY%",
Expand Down
2 changes: 2 additions & 0 deletions src/redis-cpu-panel/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './redis-cpu-panel-graph';
export * from './redis-cpu-panel';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './redis-cpu-panel-graph';
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { shallow } from 'enzyme';
import React from 'react';
import { DataFrame, dateTime, dateTimeParse } from '@grafana/data';
import { RedisCPUPanelGraph } from './redis-cpu-panel-graph';

/**
* Table View
*/
describe('RedisCPUPanelGraph', () => {
/**
* getGraphSeries
*/
describe('getGraphSeries', () => {
it('Should return series for each command', () => {
const seriesMap = {
get: [
{
time: dateTime(),
value: 0,
},
{
time: dateTime(),
value: 0,
},
],
info: [
{
time: dateTime(),
value: 10,
},
{
time: dateTime().add(10, 'seconds'),
value: 20,
},
],
};
const result: DataFrame[] = RedisCPUPanelGraph.getGraphDataFrame(seriesMap);
expect(result[0].length).toEqual(2);
expect(result[0].fields[0].values.length).toEqual(2);
expect(result[1].length).toEqual(2);
});
});

/**
* Get Time Range
*/
describe('getTimeRange', () => {
it('Should apply timeRange.raw.from and find series with the biggest items and take time', () => {
const timeRange = {
from: dateTime(),
to: dateTime(),
raw: {
from: '6h',
to: 'now',
},
};
const result = RedisCPUPanelGraph.getTimeRange(timeRange, 'browser');
expect(result.from.valueOf()).toEqual(dateTimeParse('6h').valueOf());
expect(result.to.startOf('hour').valueOf()).toEqual(dateTime().startOf('hour').valueOf());
});
});

/**
* Getting new props
*/
describe('Getting new props', () => {
const getComponent = (props: any = {}) => <RedisCPUPanelGraph {...props} />;

it('Should update timeRange when gets a new seriesMap or timeRange', () => {
const wrapper = shallow<RedisCPUPanelGraph>(
getComponent({
seriesMap: { get: [{ time: dateTime(), value: 1 }] },
timeRange: { raw: { from: dateTime() } },
})
);
const currentTimeRange = wrapper.state().timeRange;
wrapper.setProps({
seriesMap: { get: [{ time: dateTime(), value: 2 }] },
});
expect(currentTimeRange !== wrapper.state().timeRange).toBeTruthy();
});

it('Should return gathering results div if data frame is empty', () => {
const wrapper = shallow<RedisCPUPanelGraph>(
getComponent({
seriesMap: {},
timeRange: { raw: { from: dateTime() } },
})
);

const div = wrapper.findWhere((node) => node.name() === 'div');
expect(div.exists()).toBeTruthy();
});

it('Should return Time Series if data frame has data', () => {
const wrapper = shallow<RedisCPUPanelGraph>(
getComponent({
seriesMap: { get: [{ time: dateTime(), value: 1 }] },
timeRange: { raw: { from: dateTime() } },
})
);

const timeSeries = wrapper.findWhere((node) => node.name() === 'TimeSeries');
expect(timeSeries.exists()).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, { PureComponent } from 'react';
import {
DataFrame,
DateTime,
dateTimeParse,
FieldColorModeId,
FieldType,
getDisplayProcessor,
GraphSeriesValue,
PanelProps,
TimeRange,
TimeZone,
toDataFrame,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { colors, LegendDisplayMode, TimeSeries, TooltipDisplayMode, TooltipPlugin } from '@grafana/ui';
import { PanelOptions, SeriesMap, SeriesValue } from '../../types';

/**
* Graph Properties
*/
interface Props extends PanelProps<PanelOptions> {
/**
* Series
*
* @type {SeriesMap}
*/
seriesMap: SeriesMap;
}

/**
* State
*/
interface State {
/**
* Time Range
*
* @type {TimeRange}
*/
timeRange: TimeRange;
}

/**
* Graph View
*/
export class RedisCPUPanelGraph extends PureComponent<Props, State> {
/**
* Convert seriesMap to Data Frames
* @param seriesMap
*/
static getGraphDataFrame(seriesMap: SeriesMap): DataFrame[] {
return Object.entries(seriesMap).reduce(
(acc: DataFrame[], [command, seriesValues]: [string, SeriesValue[]], index) => {
const { times, values } = seriesValues.reduce(
(acc: { times: DateTime[]; values: number[] }, { time, value }) => {
return {
times: acc.times.concat([time]),
values: acc.values.concat([value]),
};
},
{ times: [], values: [] }
);

/**
* Color
*/
const color = colors[index % colors.length];

/**
* Data Frame
*/
const seriesDataFrame = toDataFrame({
name: command,
fields: [
{
type: FieldType.time,
name: 'time',
values: times,
},
{
type: FieldType.number,
name: ' ',
values,
config: {
unit: 'percent',
color: {
fixedColor: color,
mode: FieldColorModeId.Fixed,
},
},
},
],
});

/**
* Fields
*/
seriesDataFrame.fields = seriesDataFrame.fields.map((field) => ({
...field,
display: getDisplayProcessor({ field, theme: config.theme2 }),
}));

/**
* Push values
*/
const data: GraphSeriesValue[][] = [];
for (let i = 0; i < times.length; i++) {
data.push([times[i].valueOf(), values[i]]);
}

return acc.concat(seriesDataFrame);
},
[]
);
}

/**
* Get timeRange from timeRange.raw
*
* @param timeRange
*/
static getTimeRange(timeRange: TimeRange, timeZone: TimeZone): TimeRange {
let fromTime = dateTimeParse(timeRange.raw.from, { timeZone });
const toTime = dateTimeParse(timeRange.raw.to, { timeZone });

return {
from: fromTime,
to: toTime,
raw: {
from: timeRange.raw.from,
to: toTime,
},
};
}

/**
* State
*/
state = {
timeRange: RedisCPUPanelGraph.getTimeRange(this.props.timeRange, this.props.timeZone),
};

/**
* getDerivedStateFromProps
*
* @param props
*/
static getDerivedStateFromProps(props: Readonly<Props>) {
return {
timeRange: RedisCPUPanelGraph.getTimeRange(props.timeRange, props.timeZone),
};
}

/**
* Render
*/
render() {
const { width, height, seriesMap } = this.props;
const { timeRange } = this.state;

/**
* Convert to Data Frames
*/
const dataFrames = RedisCPUPanelGraph.getGraphDataFrame(seriesMap);
if (!dataFrames.length) {
return <div>Gathering usage data...</div>;
}

/**
* Return Time Series
*/
return (
<TimeSeries
frames={dataFrames}
width={width}
height={height}
timeRange={timeRange}
legend={{ displayMode: LegendDisplayMode?.List, placement: 'bottom', calcs: [] }}
timeZone={this.props.timeZone}
>
{(config, alignedDataFrame) => {
return (
<TooltipPlugin
config={config}
data={alignedDataFrame}
mode={TooltipDisplayMode.Multi}
timeZone={this.props.timeZone}
/>
);
}}
</TimeSeries>
);
}
}
1 change: 1 addition & 0 deletions src/redis-cpu-panel/components/redis-cpu-panel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './redis-cpu-panel';
Loading

0 comments on commit df85652

Please sign in to comment.