influx: support retention policy for long-term stored data

This commit is contained in:
Alexander Zobnin
2018-10-31 20:56:36 +03:00
parent d5a224d4fc
commit 089700d227
6 changed files with 83 additions and 34 deletions

View File

@@ -31,6 +31,7 @@ export class ZabbixDSConfigController {
this.dbDataSources = this.getSupportedDBDataSources(); this.dbDataSources = this.getSupportedDBDataSources();
this.zabbixVersions = _.cloneDeep(zabbixVersions); this.zabbixVersions = _.cloneDeep(zabbixVersions);
this.autoDetectZabbixVersion(); this.autoDetectZabbixVersion();
console.log(this.dbDataSources);
} }
getSupportedDBDataSources() { getSupportedDBDataSources() {
@@ -40,6 +41,12 @@ export class ZabbixDSConfigController {
}); });
} }
getCurrentDatasourceType() {
const dsId = this.current.jsonData.dbConnectionDatasourceId;
const currentDs = _.find(this.dbDataSources, { 'id': dsId });
return currentDs ? currentDs.type : null;
}
autoDetectZabbixVersion() { autoDetectZabbixVersion() {
if (!this.current.id) { if (!this.current.id) {
return; return;

View File

@@ -58,6 +58,7 @@ export class ZabbixDatasource {
this.enableDirectDBConnection = jsonData.dbConnectionEnable || false; this.enableDirectDBConnection = jsonData.dbConnectionEnable || false;
this.dbConnectionDatasourceId = jsonData.dbConnectionDatasourceId; this.dbConnectionDatasourceId = jsonData.dbConnectionDatasourceId;
this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName; this.dbConnectionDatasourceName = jsonData.dbConnectionDatasourceName;
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
let zabbixOptions = { let zabbixOptions = {
url: this.url, url: this.url,
@@ -69,7 +70,8 @@ export class ZabbixDatasource {
cacheTTL: this.cacheTTL, cacheTTL: this.cacheTTL,
enableDirectDBConnection: this.enableDirectDBConnection, enableDirectDBConnection: this.enableDirectDBConnection,
dbConnectionDatasourceId: this.dbConnectionDatasourceId, dbConnectionDatasourceId: this.dbConnectionDatasourceId,
dbConnectionDatasourceName: this.dbConnectionDatasourceName dbConnectionDatasourceName: this.dbConnectionDatasourceName,
dbConnectionRetentionPolicy: this.dbConnectionRetentionPolicy,
}; };
this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv); this.zabbix = new Zabbix(zabbixOptions, datasourceSrv, backendSrv);

View File

@@ -92,22 +92,38 @@
</gf-form-switch> </gf-form-switch>
<div ng-if="ctrl.current.jsonData.dbConnectionEnable"> <div ng-if="ctrl.current.jsonData.dbConnectionEnable">
<div class="gf-form max-width-30"> <div class="gf-form max-width-30">
<span class="gf-form-label width-12"> <span class="gf-form-label width-12">
Data Source Data Source
<info-popover mode="right-normal"> <info-popover mode="right-normal">
Select Data Source for Zabbix history database. Select Data Source for Zabbix history database.
In order to use this feature it should be <a href="/datasources/new" target="_blank">created</a> and In order to use this feature it should be <a href="/datasources/new" target="_blank">created</a> and
configured first. Zabbix plugin uses this data source for querying history data directly from the database. configured first. Zabbix plugin uses this data source for querying history data directly from the database.
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
amount of data transfered. amount of data transfered.
</info-popover> </info-popover>
</span> </span>
<div class="gf-form-select-wrapper max-width-16"> <div class="gf-form-select-wrapper max-width-16">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnectionDatasourceId" <select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnectionDatasourceId"
ng-options="ds.id as ds.name for ds in ctrl.dbDataSources"> ng-options="ds.id as ds.name for ds in ctrl.dbDataSources">
</select> </select>
</div>
</div> </div>
</div> </div>
<div ng-if="ctrl.getCurrentDatasourceType() === 'influxdb'">
<div class="gf-form max-width-30">
<span class="gf-form-label width-12">
Retention Policy
<info-popover mode="right-normal">
Specify retention policy name for fetching long-term stored data (optional).
Leave it blank if only default retention policy is using.
</info-popover>
</span>
<input class="gf-form-input max-width-16"
type="text"
ng-model='ctrl.current.jsonData.dbConnectionRetentionPolicy'
placeholder="Retention policy name">
</input>
</div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ describe('InfluxDBConnector', () => {
let ctx = {}; let ctx = {};
beforeEach(() => { beforeEach(() => {
ctx.options = { datasourceName: 'InfluxDB DS' }; ctx.options = { datasourceName: 'InfluxDB DS', retentionPolicy: 'longterm' };
ctx.datasourceSrvMock = { ctx.datasourceSrvMock = {
loadDatasource: jest.fn().mockResolvedValue( loadDatasource: jest.fn().mockResolvedValue(
{ id: 42, name: 'InfluxDB DS', meta: {} } { id: 42, name: 'InfluxDB DS', meta: {} }
@@ -15,15 +15,16 @@ describe('InfluxDBConnector', () => {
ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]); ctx.influxDBConnector.invokeInfluxDBQuery = jest.fn().mockResolvedValue([]);
ctx.defaultQueryParams = { ctx.defaultQueryParams = {
itemids: ['123', '234'], itemids: ['123', '234'],
timeFrom: 15000, timeTill: 15100, intervalSec: 5, range: { timeFrom: 15000, timeTill: 15100 },
intervalSec: 5,
table: 'history', aggFunction: 'MAX' table: 'history', aggFunction: 'MAX'
}; };
}); });
describe('When building InfluxDB query', () => { describe('When building InfluxDB query', () => {
it('should build proper query', () => { it('should build proper query', () => {
const { itemids, timeFrom, timeTill, intervalSec, table, aggFunction } = ctx.defaultQueryParams; const { itemids, range, intervalSec, table, aggFunction } = ctx.defaultQueryParams;
const query = ctx.influxDBConnector.buildHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction); const query = ctx.influxDBConnector.buildHistoryQuery(itemids, table, range, intervalSec, aggFunction);
const expected = compactQuery(`SELECT MAX("value") const expected = compactQuery(`SELECT MAX("value")
FROM "history" WHERE ("itemid" = '123' OR "itemid" = '234') AND "time" >= 15000s AND "time" <= 15100s FROM "history" WHERE ("itemid" = '123' OR "itemid" = '234') AND "time" >= 15000s AND "time" <= 15100s
GROUP BY time(5s), "itemid" fill(none) GROUP BY time(5s), "itemid" fill(none)
@@ -32,9 +33,9 @@ describe('InfluxDBConnector', () => {
}); });
it('should use MEAN instead of AVG', () => { it('should use MEAN instead of AVG', () => {
const { itemids, timeFrom, timeTill, intervalSec, table } = ctx.defaultQueryParams; const { itemids, range, intervalSec, table } = ctx.defaultQueryParams;
const aggFunction = 'AVG'; const aggFunction = 'AVG';
const query = ctx.influxDBConnector.buildHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction); const query = ctx.influxDBConnector.buildHistoryQuery(itemids, table, range, intervalSec, aggFunction);
const expected = compactQuery(`SELECT MEAN("value") const expected = compactQuery(`SELECT MEAN("value")
FROM "history" WHERE ("itemid" = '123' OR "itemid" = '234') AND "time" >= 15000s AND "time" <= 15100s FROM "history" WHERE ("itemid" = '123' OR "itemid" = '234') AND "time" >= 15000s AND "time" <= 15100s
GROUP BY time(5s), "itemid" fill(none) GROUP BY time(5s), "itemid" fill(none)
@@ -45,7 +46,7 @@ describe('InfluxDBConnector', () => {
describe('When invoking InfluxDB query', () => { describe('When invoking InfluxDB query', () => {
it('should query proper table depending on item type', () => { it('should query proper table depending on item type', () => {
const { timeFrom, timeTill} = ctx.defaultQueryParams; const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 }; const options = { intervalMs: 5000 };
const items = [ const items = [
{ itemid: '123', value_type: 3 } { itemid: '123', value_type: 3 }
@@ -59,7 +60,7 @@ describe('InfluxDBConnector', () => {
}); });
it('should split query if different item types are used', () => { it('should split query if different item types are used', () => {
const { timeFrom, timeTill} = ctx.defaultQueryParams; const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 }; const options = { intervalMs: 5000 };
const items = [ const items = [
{ itemid: '123', value_type: 0 }, { itemid: '123', value_type: 0 },
@@ -78,8 +79,9 @@ describe('InfluxDBConnector', () => {
expect(ctx.influxDBConnector.invokeInfluxDBQuery).toHaveBeenNthCalledWith(2, expectedQuerySecond); expect(ctx.influxDBConnector.invokeInfluxDBQuery).toHaveBeenNthCalledWith(2, expectedQuerySecond);
}); });
it('should use the same table for trends query', () => { it('should use the same table for trends query if no retention policy set', () => {
const { timeFrom, timeTill} = ctx.defaultQueryParams; ctx.influxDBConnector.retentionPolicy = '';
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 }; const options = { intervalMs: 5000 };
const items = [ const items = [
{ itemid: '123', value_type: 3 } { itemid: '123', value_type: 3 }
@@ -91,5 +93,19 @@ describe('InfluxDBConnector', () => {
ctx.influxDBConnector.getTrends(items, timeFrom, timeTill, options); ctx.influxDBConnector.getTrends(items, timeFrom, timeTill, options);
expect(ctx.influxDBConnector.invokeInfluxDBQuery).toHaveBeenCalledWith(expectedQuery); expect(ctx.influxDBConnector.invokeInfluxDBQuery).toHaveBeenCalledWith(expectedQuery);
}); });
it('should use retention policy name for trends query if it was set', () => {
const { timeFrom, timeTill } = ctx.defaultQueryParams.range;
const options = { intervalMs: 5000 };
const items = [
{ itemid: '123', value_type: 3 }
];
const expectedQuery = compactQuery(`SELECT MEAN("value")
FROM "longterm"."history_uint" WHERE ("itemid" = '123') AND "time" >= 15000s AND "time" <= 15100s
GROUP BY time(5s), "itemid" fill(none)
`);
ctx.influxDBConnector.getTrends(items, timeFrom, timeTill, options);
expect(ctx.influxDBConnector.invokeInfluxDBQuery).toHaveBeenCalledWith(expectedQuery);
});
}); });
}); });

View File

@@ -5,6 +5,7 @@ import { DBConnector, HISTORY_TO_TABLE_MAP, consolidateByFunc } from '../dbConne
export class InfluxDBConnector extends DBConnector { export class InfluxDBConnector extends DBConnector {
constructor(options, datasourceSrv) { constructor(options, datasourceSrv) {
super(options, datasourceSrv); super(options, datasourceSrv);
this.retentionPolicy = options.retentionPolicy;
super.loadDBDataSource().then(ds => { super.loadDBDataSource().then(ds => {
this.influxDS = ds; this.influxDS = ds;
return ds; return ds;
@@ -19,9 +20,10 @@ export class InfluxDBConnector extends DBConnector {
} }
getHistory(items, timeFrom, timeTill, options) { getHistory(items, timeFrom, timeTill, options) {
let {intervalMs, consolidateBy} = options; let { intervalMs, consolidateBy, retentionPolicy } = options;
const intervalSec = Math.ceil(intervalMs / 1000); const intervalSec = Math.ceil(intervalMs / 1000);
const range = { timeFrom, timeTill };
consolidateBy = consolidateBy || 'avg'; consolidateBy = consolidateBy || 'avg';
const aggFunction = consolidateByFunc[consolidateBy] || consolidateBy; const aggFunction = consolidateByFunc[consolidateBy] || consolidateBy;
@@ -30,7 +32,7 @@ export class InfluxDBConnector extends DBConnector {
const promises = _.map(grouped_items, (items, value_type) => { const promises = _.map(grouped_items, (items, value_type) => {
const itemids = _.map(items, 'itemid'); const itemids = _.map(items, 'itemid');
const table = HISTORY_TO_TABLE_MAP[value_type]; const table = HISTORY_TO_TABLE_MAP[value_type];
const query = this.buildHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction); const query = this.buildHistoryQuery(itemids, table, range, intervalSec, aggFunction, retentionPolicy);
return this.invokeInfluxDBQuery(query); return this.invokeInfluxDBQuery(query);
}); });
@@ -42,13 +44,16 @@ export class InfluxDBConnector extends DBConnector {
} }
getTrends(items, timeFrom, timeTill, options) { getTrends(items, timeFrom, timeTill, options) {
options.retentionPolicy = this.retentionPolicy;
return this.getHistory(items, timeFrom, timeTill, options); return this.getHistory(items, timeFrom, timeTill, options);
} }
buildHistoryQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { buildHistoryQuery(itemids, table, range, intervalSec, aggFunction, retentionPolicy) {
const { timeFrom, timeTill } = range;
const measurement = retentionPolicy ? `"${retentionPolicy}"."${table}"` : `"${table}"`;
const AGG = aggFunction === 'AVG' ? 'MEAN' : aggFunction; const AGG = aggFunction === 'AVG' ? 'MEAN' : aggFunction;
const where_clause = this.buildWhereClause(itemids); const where_clause = this.buildWhereClause(itemids);
const query = `SELECT ${AGG}("value") FROM "${table}" const query = `SELECT ${AGG}("value") FROM ${measurement}
WHERE ${where_clause} AND "time" >= ${timeFrom}s AND "time" <= ${timeTill}s WHERE ${where_clause} AND "time" >= ${timeFrom}s AND "time" <= ${timeTill}s
GROUP BY time(${intervalSec}s), "itemid" fill(none)`; GROUP BY time(${intervalSec}s), "itemid" fill(none)`;
return compactQuery(query); return compactQuery(query);

View File

@@ -37,6 +37,7 @@ export class Zabbix {
enableDirectDBConnection, enableDirectDBConnection,
dbConnectionDatasourceId, dbConnectionDatasourceId,
dbConnectionDatasourceName, dbConnectionDatasourceName,
dbConnectionRetentionPolicy,
} = options; } = options;
this.enableDirectDBConnection = enableDirectDBConnection; this.enableDirectDBConnection = enableDirectDBConnection;
@@ -55,7 +56,8 @@ export class Zabbix {
this.bindRequests(); this.bindRequests();
if (enableDirectDBConnection) { if (enableDirectDBConnection) {
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv) const connectorOptions = { dbConnectionRetentionPolicy };
this.initDBConnector(dbConnectionDatasourceId, dbConnectionDatasourceName, datasourceSrv, connectorOptions)
.then(() => { .then(() => {
this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector); this.getHistoryDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getHistory, 'getHistory', this.dbConnector);
this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector); this.getTrendsDB = this.cachingProxy.proxyfyWithCache(this.dbConnector.getTrends, 'getTrends', this.dbConnector);
@@ -63,14 +65,15 @@ export class Zabbix {
} }
} }
initDBConnector(datasourceId, datasourceName, datasourceSrv) { initDBConnector(datasourceId, datasourceName, datasourceSrv, options) {
return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv) return DBConnector.loadDatasource(datasourceId, datasourceName, datasourceSrv)
.then(ds => { .then(ds => {
const options = { datasourceId, datasourceName }; let connectorOptions = { datasourceId, datasourceName };
if (ds.type === 'influxdb') { if (ds.type === 'influxdb') {
this.dbConnector = new InfluxDBConnector(options, datasourceSrv); connectorOptions.retentionPolicy = options.dbConnectionRetentionPolicy;
this.dbConnector = new InfluxDBConnector(connectorOptions, datasourceSrv);
} else { } else {
this.dbConnector = new SQLConnector(options, datasourceSrv); this.dbConnector = new SQLConnector(connectorOptions, datasourceSrv);
} }
return this.dbConnector; return this.dbConnector;
}); });