Skip to content

Commit

Permalink
Create Latency Panel to display Latency Chart for each command (#29)
Browse files Browse the repository at this point in the history
* Add redis-latency-panel

* Update README.md

* Add Logo

* Update default Interval and format

* Organize and Format

* Add Sort by Latency

Co-authored-by: Mikhail <[email protected]>
  • Loading branch information
asimonok and Mikhail authored Jan 4, 2021
1 parent f6362d8 commit abd81d1
Show file tree
Hide file tree
Showing 10 changed files with 812 additions and 5 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@

### What is the Redis Application for Grafana?

The Redis Application, is a plug-in for Grafana that provides custom panels for [Redis Data Source](https://grafana.com/grafana/plugins/redis-datasource).
The Redis Application, is a plug-in for Grafana that provides custom panels for [Redis Data Source](https://grafana.com/grafana/plugins/redis-datasource):

- Command line interface (CLI) panel
- Latency panel

### What Grafana version is supported?

Only Grafana 7.0 and later with a new plug-in platform supported.

### What kind of panels it provides?

- Redis CLI panel

### How to build Application

To learn how to build Redis Application plug-in and register in the new or existing Grafana please take a look at [BUILD](https://github.com/RedisGrafana/grafana-redis-app/blob/master/BUILD.md) instructions.
Expand Down
4 changes: 4 additions & 0 deletions src/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
{
"name": "Redis CLI Panel",
"type": "panel"
},
{
"name": "Redis Latency Panel",
"type": "panel"
}
],
"info": {
Expand Down
1 change: 1 addition & 0 deletions src/redis-latency-panel/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './redis-latency-panel';
359 changes: 359 additions & 0 deletions src/redis-latency-panel/components/redis-latency-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Observable } from 'rxjs';
import { FieldType, toDataFrame } from '@grafana/data';
import { RedisLatencyPanel } from './redis-latency-panel';
import { FieldName, DisplayNameByFieldName } from '../types';

/**
* Query Result
*/
const getDataSourceQueryResult = (fields: Array<{ name: FieldName; type: FieldType; values: number[] }>) => ({
data: [
toDataFrame({
name: 'data',
fields,
}),
],
});

/*
DataSource
*/
const dataSourceMock = {
query: jest.fn().mockImplementation(
() =>
new Observable((subscriber) => {
subscriber.next(
getDataSourceQueryResult([
{
type: FieldType.number,
name: FieldName.Duration,
values: [2000, 3000],
},
{
type: FieldType.number,
name: FieldName.Calls,
values: [10, 20],
},
])
);
subscriber.complete();
})
),
name: 'datasource',
};

const dataSourceSrvGetMock = jest.fn().mockImplementation(() => Promise.resolve(dataSourceMock));

jest.mock('@grafana/runtime', () => ({
getDataSourceSrv: () => ({
get: dataSourceSrvGetMock,
}),
}));

/**
* Latency Panel
*/
describe('RedisLatencyPanel', () => {
const getComponent = (props: any) => <RedisLatencyPanel {...props} />;
describe('getLatencyValue', () => {
it('Should calc correctly', () => {
const duration = 100;
const prevDuration = 50;
const calls = 2;
const prevCalls = 1;
expect(RedisLatencyPanel.getLatencyValue({ duration, prevDuration, calls, prevCalls })).toEqual(
(duration - prevDuration) / (calls - prevCalls)
);
});

it('If calls=prevCalls should handle NaN and return 0', () => {
const duration = 100;
const prevDuration = 50;
const calls = 2;
const prevCalls = 2;
expect(RedisLatencyPanel.getLatencyValue({ duration, prevDuration, calls, prevCalls })).toEqual(0);
});

it('If no prev values should calc correctly', () => {
const duration = 100;
const calls = 2;
expect(RedisLatencyPanel.getLatencyValue({ duration, calls })).toEqual(duration / calls);
});
});

describe('getValuesForCalculation', () => {
it('Should return calls and duration field values', () => {
const dataFrame = toDataFrame({
name: 'dataFrame',
fields: [
{
name: FieldName.Calls,
type: FieldType.number,
values: [1, 2],
},
{
name: FieldName.Command,
type: FieldType.string,
values: ['info', 'get'],
},
{
name: FieldName.Duration,
type: FieldType.number,
values: [100, 200],
},
],
});
expect(RedisLatencyPanel.getValuesForCalculation(dataFrame)).toEqual({
calls: [1, 2],
duration: [100, 200],
});
});
});

describe('getLatencyValues', () => {
it('Should calc values array', () => {
const prevValues = {
calls: [1, 2],
duration: [100, 200],
};
const currentValues = {
calls: [2, 2],
duration: [300, 200],
};
const rowsCount = 2;
const expectedResult = [];
for (let row = 0; row < rowsCount; row++) {
expectedResult.push(
RedisLatencyPanel.getLatencyValue({
duration: currentValues.duration[row],
calls: currentValues.calls[row],
prevDuration: prevValues.duration[row],
prevCalls: prevValues.calls[row],
})
);
}
expect(RedisLatencyPanel.getLatencyValues(prevValues, currentValues, rowsCount)).toEqual(expectedResult);
});
});

describe('getTableDataFrame', () => {
it('Should add new column with latency values', () => {
const fields = [
{
type: FieldType.number,
name: FieldName.Duration,
values: [200, 300],
},
{
type: FieldType.number,
name: FieldName.Calls,
values: [1, 2],
},
];
const prevDataFrame = toDataFrame({
name: 'prev',
fields,
});
const currentDataFrame = toDataFrame({
name: 'current',
fields: fields.map((field) => ({
...field,
values: field.values.map((value) => value * value),
})),
});
const tableDataFrame = RedisLatencyPanel.getTableDataFrame(prevDataFrame, currentDataFrame);
const expectedDataFrame = toDataFrame({
name: 'tableDataFrame',
fields: [
...fields.map((field) => ({
...field,
values: field.values.map((value) => value * value),
})),
{
type: FieldType.number,
name: FieldName.Latency,
values: RedisLatencyPanel.getLatencyValues(
RedisLatencyPanel.getValuesForCalculation(prevDataFrame),
RedisLatencyPanel.getValuesForCalculation(currentDataFrame),
2
),
},
].map((field) => ({
...field,
config: {
displayName: DisplayNameByFieldName[field.name as FieldName],
},
})),
});
expect(tableDataFrame).toEqual(expectedDataFrame);
});
});

describe('RequestData', () => {
const data = {
series: [
toDataFrame({
name: 'data',
fields: [
{
type: FieldType.number,
name: FieldName.Duration,
values: [200, 300],
},
{
type: FieldType.number,
name: FieldName.Calls,
values: [1, 2],
},
],
}),
],
};

describe('Mount', () => {
it('If options.interval is filled should set interval', () => {
const options = {
interval: 1000,
};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const testedMethod = jest
.spyOn(wrapper.instance(), 'setRequestDataInterval')
.mockImplementation(() => Promise.resolve());
wrapper.instance().componentDidMount();
expect(testedMethod).toHaveBeenCalled();
});

it('If options.interval is empty should not set interval', () => {
const options = {};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const testedMethod = jest
.spyOn(wrapper.instance(), 'setRequestDataInterval')
.mockImplementation(() => Promise.resolve());
wrapper.instance().componentDidMount();
expect(testedMethod).not.toHaveBeenCalled();
});
});

describe('Update', () => {
it('If options.interval was changed should set interval', () => {
const options = {
interval: 1000,
};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const testedMethod = jest
.spyOn(wrapper.instance(), 'setRequestDataInterval')
.mockImplementation(() => Promise.resolve());
wrapper.instance().componentDidMount();
expect(testedMethod).toHaveBeenCalled();
testedMethod.mockClear();
wrapper.setProps({ options: { interval: 2000 } });
expect(testedMethod).toHaveBeenCalled();
testedMethod.mockClear();
wrapper.setProps({ options: { interval: 2000 } });
expect(testedMethod).not.toHaveBeenCalled();
});
});

describe('Unmount', () => {
it('Should clear interval', () => {
const options = {
interval: 1000,
};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const testedMethod = jest.spyOn(wrapper.instance(), 'clearRequestDataInterval').mockImplementation(() => {});
wrapper.instance().componentWillUnmount();
expect(testedMethod).toHaveBeenCalled();
});
});

describe('Update tableDataFrame', () => {
it('Should set timer and request data with interval', (done) => {
const options = {
interval: 1000,
};
const getTableDataFrameMock = jest.spyOn(RedisLatencyPanel, 'getTableDataFrame');
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const setStateMock = jest.spyOn(wrapper.instance(), 'setState');

setImmediate(() => {
setStateMock.mockClear();
dataSourceMock.query.mockClear();
getTableDataFrameMock.mockClear();
let checksCount = 2;
const check = () => {
expect(dataSourceMock.query).toHaveBeenCalled();
expect(getTableDataFrameMock).toHaveBeenCalled();
expect(setStateMock).toHaveBeenCalled();

checksCount--;
if (checksCount > 0) {
getTableDataFrameMock.mockClear();
setStateMock.mockClear();
dataSourceMock.query.mockClear();
setTimeout(check, options.interval);
} else {
getTableDataFrameMock.mockReset();
done();
}
};
setTimeout(check, options.interval);
});
});

it('Should clear interval before setting new one', (done) => {
const options = {
interval: 1000,
};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
const testedMethod = jest.spyOn(wrapper.instance(), 'clearRequestDataInterval');
setImmediate(() => {
wrapper.instance().setRequestDataInterval();
expect(testedMethod).toHaveBeenCalled();
done();
});
});

it('Should use passed datasource', (done) => {
const options = {
interval: 1000,
};
const overrideData = {
...data,
request: {
targets: [
{
datasource: 'redis',
},
],
},
};
shallow<RedisLatencyPanel>(getComponent({ data: overrideData, options }));
setImmediate(() => {
expect(dataSourceSrvGetMock).toHaveBeenCalledWith('redis');
done();
});
});
});

describe('clearRequestDataInterval', () => {
it('Should clear interval', (done) => {
const options = {
interval: 1000,
};
const wrapper = shallow<RedisLatencyPanel>(getComponent({ data, options }));
setImmediate(() => {
expect(wrapper.instance().requestDataTimer).toBeDefined();
wrapper.instance().clearRequestDataInterval();
expect(wrapper.instance().requestDataTimer).not.toBeDefined();
done();
});
});
});
});

afterAll(() => {
jest.resetAllMocks();
});
});
Loading

0 comments on commit abd81d1

Please sign in to comment.