Refactor API calls

This commit is contained in:
Alexander Zobnin
2020-05-29 12:31:43 +03:00
parent efb4d41da5
commit 9f344cb867
10 changed files with 66 additions and 254 deletions

View File

@@ -170,6 +170,12 @@ func GetQueryType(tsdbReq *datasource.DatasourceRequest) (string, error) {
// }, nil // }, nil
// } // }
func BuildAPIResponse(responseData *interface{}) (*ZabbixAPIResourceResponse, error) {
return &ZabbixAPIResourceResponse{
Result: *responseData,
}, nil
}
// BuildResponse transforms a Zabbix API response to a DatasourceResponse // BuildResponse transforms a Zabbix API response to a DatasourceResponse
func BuildResponse(responseData interface{}) (*datasource.DatasourceResponse, error) { func BuildResponse(responseData interface{}) (*datasource.DatasourceResponse, error) {
jsonBytes, err := json.Marshal(responseData) jsonBytes, err := json.Marshal(responseData)

View File

@@ -115,6 +115,10 @@ type ZabbixAPIRequest struct {
Params map[string]interface{} `json:"params,omitempty"` Params map[string]interface{} `json:"params,omitempty"`
} }
type ZabbixAPIResourceResponse struct {
Result interface{} `json:"result,omitempty"`
}
func (r *ZabbixAPIRequest) String() string { func (r *ZabbixAPIRequest) String() string {
jsonRequest, _ := json.Marshal(r.Params) jsonRequest, _ := json.Marshal(r.Params)
return r.Method + string(jsonRequest) return r.Method + string(jsonRequest)

View File

@@ -32,16 +32,17 @@ func (ds *ZabbixDatasource) zabbixAPIHandler(rw http.ResponseWriter, req *http.R
var reqData ZabbixAPIResourceRequest var reqData ZabbixAPIResourceRequest
err = json.Unmarshal(body, &reqData) err = json.Unmarshal(body, &reqData)
if err != nil { if err != nil {
ds.logger.Error("Cannot unmarshal request", "error", err.Error())
WriteError(rw, http.StatusInternalServerError, err) WriteError(rw, http.StatusInternalServerError, err)
return return
} }
pluginCxt := httpadapter.PluginConfigFromContext(req.Context()) pluginCxt := httpadapter.PluginConfigFromContext(req.Context())
ds.logger.Debug("Received Zabbix API call", "ds", pluginCxt.DataSourceInstanceSettings.Name)
dsInstance, err := ds.GetDatasource(pluginCxt.OrgID, pluginCxt.DataSourceInstanceSettings) dsInstance, err := ds.GetDatasource(pluginCxt.OrgID, pluginCxt.DataSourceInstanceSettings)
ds.logger.Debug("Data source found", "ds", dsInstance.dsInfo.Name) ds.logger.Debug("Data source found", "ds", dsInstance.dsInfo.Name)
ds.logger.Debug("Invoke Zabbix API call", "ds", pluginCxt.DataSourceInstanceSettings.Name, "method", reqData.Method)
apiReq := &ZabbixAPIRequest{Method: reqData.Method, Params: reqData.Params} apiReq := &ZabbixAPIRequest{Method: reqData.Method, Params: reqData.Params}
result, err := dsInstance.ZabbixAPIQuery(req.Context(), apiReq) result, err := dsInstance.ZabbixAPIQuery(req.Context(), apiReq)
if err != nil { if err != nil {
@@ -52,7 +53,7 @@ func (ds *ZabbixDatasource) zabbixAPIHandler(rw http.ResponseWriter, req *http.R
WriteResponse(rw, result) WriteResponse(rw, result)
} }
func WriteResponse(rw http.ResponseWriter, result *interface{}) { func WriteResponse(rw http.ResponseWriter, result *ZabbixAPIResourceResponse) {
resultJson, err := json.Marshal(*result) resultJson, err := json.Marshal(*result)
if err != nil { if err != nil {
WriteError(rw, http.StatusInternalServerError, err) WriteError(rw, http.StatusInternalServerError, err)

View File

@@ -25,7 +25,7 @@ type FunctionCategories struct {
} }
// ZabbixAPIQuery handles query requests to Zabbix // ZabbixAPIQuery handles query requests to Zabbix
func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, apiReq *ZabbixAPIRequest) (*interface{}, error) { func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context, apiReq *ZabbixAPIRequest) (*ZabbixAPIResourceResponse, error) {
var result interface{} var result interface{}
var err error var err error
var queryExistInCache bool var queryExistInCache bool
@@ -41,8 +41,7 @@ func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context,
} }
} }
// return BuildResponse(result) return BuildAPIResponse(&result)
return &result, nil
} }
func (ds *ZabbixDatasourceInstance) ZabbixAPIQueryOld(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { func (ds *ZabbixDatasourceInstance) ZabbixAPIQueryOld(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) {

View File

@@ -68,6 +68,11 @@ func (ds *ZabbixDatasourceInstance) ZabbixRequest(ctx context.Context, method st
var result *simplejson.Json var result *simplejson.Json
var err error var err error
// Skip auth for methods that are not required it
if method == "apiinfo.version" {
return ds.ZabbixAPIRequest(ctx, method, params, ds.authToken)
}
for attempt := 0; attempt <= 3; attempt++ { for attempt := 0; attempt <= 3; attempt++ {
if ds.authToken == "" { if ds.authToken == "" {
// Authenticate // Authenticate

View File

@@ -10,19 +10,16 @@ import dataProcessor from './dataProcessor';
import responseHandler from './responseHandler'; import responseHandler from './responseHandler';
import problemsHandler from './problemsHandler'; import problemsHandler from './problemsHandler';
import { Zabbix } from './zabbix/zabbix'; import { Zabbix } from './zabbix/zabbix';
import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPICore'; import { ZabbixAPIError } from './zabbix/connectors/zabbix_api/zabbixAPIConnector';
import { VariableQueryTypes, ShowProblemTypes } from './types'; import { VariableQueryTypes, ShowProblemTypes } from './types';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceApi, DataSourceInstanceSettings } from '@grafana/data';
export class ZabbixDatasource extends DataSourceApi { export class ZabbixDatasource extends DataSourceApi {
name: string; name: string;
url: string;
basicAuth: any; basicAuth: any;
withCredentials: any; withCredentials: any;
username: string;
password: string;
trends: boolean; trends: boolean;
trendsFrom: string; trendsFrom: string;
trendsRange: string; trendsRange: string;
@@ -56,16 +53,11 @@ export class ZabbixDatasource extends DataSourceApi {
// General data source settings // General data source settings
this.datasourceId = instanceSettings.id; this.datasourceId = instanceSettings.id;
this.name = instanceSettings.name; this.name = instanceSettings.name;
this.url = instanceSettings.url;
this.basicAuth = instanceSettings.basicAuth; this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials; this.withCredentials = instanceSettings.withCredentials;
const jsonData = migrations.migrateDSConfig(instanceSettings.jsonData); const jsonData = migrations.migrateDSConfig(instanceSettings.jsonData);
// Zabbix API credentials
this.username = jsonData.username;
this.password = jsonData.password;
// Use trends instead history since specified time // Use trends instead history since specified time
this.trends = jsonData.trends; this.trends = jsonData.trends;
this.trendsFrom = jsonData.trendsFrom || '7d'; this.trendsFrom = jsonData.trendsFrom || '7d';
@@ -90,9 +82,6 @@ export class ZabbixDatasource extends DataSourceApi {
this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy; this.dbConnectionRetentionPolicy = jsonData.dbConnectionRetentionPolicy;
const zabbixOptions = { const zabbixOptions = {
url: this.url,
username: this.username,
password: this.password,
basicAuth: this.basicAuth, basicAuth: this.basicAuth,
withCredentials: this.withCredentials, withCredentials: this.withCredentials,
cacheTTL: this.cacheTTL, cacheTTL: this.cacheTTL,

View File

@@ -2,10 +2,9 @@ import _ from 'lodash';
import semver from 'semver'; import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn'; import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils'; import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types'; import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { JSONRPCRequestParams } from './types'; import { GFHTTPRequest, JSONRPCError } from './types';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
const DEFAULT_ZABBIX_VERSION = '3.0.0'; const DEFAULT_ZABBIX_VERSION = '3.0.0';
@@ -16,39 +15,22 @@ const DEFAULT_ZABBIX_VERSION = '3.0.0';
* Wraps API calls and provides high-level methods. * Wraps API calls and provides high-level methods.
*/ */
export class ZabbixAPIConnector { export class ZabbixAPIConnector {
url: string; backendAPIUrl: string;
username: string;
password: string;
auth: string;
requestOptions: { basicAuth: any; withCredentials: boolean; }; requestOptions: { basicAuth: any; withCredentials: boolean; };
loginPromise: Promise<string>;
loginErrorCount: number;
maxLoginAttempts: number;
zabbixAPICore: ZabbixAPICore;
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>; getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
version: string; version: string;
getVersionPromise: Promise<string>; getVersionPromise: Promise<string>;
datasourceId: number; datasourceId: number;
constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) { constructor(basicAuth: any, withCredentials: boolean, datasourceId: number) {
this.url = api_url; this.datasourceId = datasourceId;
this.username = username; this.backendAPIUrl = `/api/datasources/${this.datasourceId}/resources/zabbix-api`;
this.password = password;
this.auth = '';
this.requestOptions = { this.requestOptions = {
basicAuth: basicAuth, basicAuth: basicAuth,
withCredentials: withCredentials withCredentials: withCredentials
}; };
this.datasourceId = datasourceId;
this.loginPromise = null;
this.loginErrorCount = 0;
this.maxLoginAttempts = 3;
this.zabbixAPICore = new ZabbixAPICore();
this.getTrend = this.getTrend_ZBXNEXT1193; this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30; //getTrend = getTrend_30;
@@ -59,113 +41,42 @@ export class ZabbixAPIConnector {
// Core method wrappers // // Core method wrappers //
////////////////////////// //////////////////////////
request(method, params) { request(method: string, params?: any) {
return this.tsdbRequest(method, params).then(response => { return this.backendAPIRequest(method, params).then(response => {
// const result = this.handleTsdbResponse(response); return response?.data?.result;
const result = this.handleZabbixAPIResourceResponse(response);
return result;
}); });
} }
tsdbRequest(method, params) { backendAPIRequest(method: string, params: any = {}) {
const tsdbRequestData = { const requestOptions: GFHTTPRequest = {
queries: [{ url: this.backendAPIUrl,
datasourceId: this.datasourceId,
queryType: 'zabbixAPI',
target: {
method,
params,
},
}],
};
return getBackendSrv().datasourceRequest({
url: `/api/datasources/${this.datasourceId}/resources/zabbix-api`,
method: 'POST', method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: { data: {
datasourceId: this.datasourceId, datasourceId: this.datasourceId,
method, method,
params, params,
}, },
}); };
// return getBackendSrv().datasourceRequest({ // Set request options for basic auth
// url: '/api/tsdb/query', if (this.requestOptions.basicAuth || this.requestOptions.withCredentials) {
// method: 'POST', requestOptions.withCredentials = true;
// data: tsdbRequestData }
// }); if (this.requestOptions.basicAuth) {
requestOptions.headers.Authorization = this.requestOptions.basicAuth;
} }
_request(method: string, params: JSONRPCRequestParams): Promise<any> { return getBackendSrv().datasourceRequest(requestOptions);
if (!this.version) {
return this.initVersion().then(() => this.request(method, params));
}
return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth)
.catch(error => {
if (isNotInitialized(error.data)) {
// If API not initialized yet (auth is empty), login first
return this.loginOnce()
.then(() => this.request(method, params));
} else if (isNotAuthorized(error.data)) {
// Handle auth errors
this.loginErrorCount++;
if (this.loginErrorCount > this.maxLoginAttempts) {
this.loginErrorCount = 0;
return Promise.resolve();
} else {
return this.loginOnce()
.then(() => this.request(method, params));
}
} else {
return Promise.reject(error);
}
});
}
handleTsdbResponse(response) {
if (!response || !response.data || !response.data.results) {
return [];
}
return response.data.results['zabbixAPI'].meta;
}
handleZabbixAPIResourceResponse(response) {
return response?.data;
}
/**
* When API unauthenticated or auth token expired each request produce login()
* call. But auth token is common to all requests. This function wraps login() method
* and call it once. If login() already called just wait for it (return its promise).
*/
loginOnce(): Promise<string> {
if (!this.loginPromise) {
this.loginPromise = Promise.resolve(
this.login().then(auth => {
this.auth = auth;
this.loginPromise = null;
return auth;
})
);
}
return this.loginPromise;
}
/**
* Get authentication token.
*/
login(): Promise<string> {
return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
} }
/** /**
* Get Zabbix API version * Get Zabbix API version
*/ */
getVersion() { getVersion() {
return this.zabbixAPICore.getVersion(this.url, this.requestOptions); return this.request('apiinfo.version');
} }
initVersion(): Promise<string> { initVersion(): Promise<string> {
@@ -730,18 +641,6 @@ function filterTriggersByAcknowledge(triggers, acknowledged) {
} }
} }
function isNotAuthorized(message) {
return (
message === "Session terminated, re-login, please." ||
message === "Not authorised." ||
message === "Not authorized."
);
}
function isNotInitialized(message) {
return message === "Not initialized";
}
function getSLAInterval(intervalMs) { function getSLAInterval(intervalMs) {
// Too many intervals may cause significant load on the database, so decrease number of resulting points // Too many intervals may cause significant load on the database, so decrease number of resulting points
const resolutionRatio = 100; const resolutionRatio = 100;
@@ -767,3 +666,22 @@ function buildSLAIntervals(timeRange, interval) {
return intervals; return intervals;
} }
// Define zabbix API exception type
export class ZabbixAPIError {
code: number;
name: string;
data: string;
message: string;
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || "";
this.data = error.data || "";
this.message = "Zabbix API Error: " + this.name + " " + this.data;
}
toString() {
return this.name + " " + this.data;
}
}

View File

@@ -1,105 +0,0 @@
/**
* General Zabbix API methods
*/
import { getBackendSrv } from '@grafana/runtime';
import { JSONRPCRequest, ZabbixRequestResponse, JSONRPCError, APILoginResponse, GFHTTPRequest, GFRequestOptions } from './types';
export class ZabbixAPICore {
/**
* Request data from Zabbix API
* @return {object} response.result
*/
request(api_url: string, method: string, params: any, options: GFRequestOptions, auth?: string) {
const requestData: JSONRPCRequest = {
jsonrpc: '2.0',
method: method,
params: params,
id: 1
};
if (auth === "") {
// Reject immediately if not authenticated
return Promise.reject(new ZabbixAPIError({data: "Not initialized"}));
} else if (auth) {
// Set auth parameter only if it needed
requestData.auth = auth;
}
const requestOptions: GFHTTPRequest = {
method: 'POST',
url: api_url,
data: requestData,
headers: {
'Content-Type': 'application/json'
}
};
// Set request options for basic auth
if (options.basicAuth || options.withCredentials) {
requestOptions.withCredentials = true;
}
if (options.basicAuth) {
requestOptions.headers.Authorization = options.basicAuth;
}
return this.datasourceRequest(requestOptions);
}
datasourceRequest(requestOptions) {
return getBackendSrv().datasourceRequest(requestOptions)
.then((response: ZabbixRequestResponse) => {
if (!response?.data) {
return Promise.reject(new ZabbixAPIError({data: "General Error, no data"}));
} else if (response?.data.error) {
// Handle Zabbix API errors
return Promise.reject(new ZabbixAPIError(response.data.error));
}
// Success
return response?.data.result;
});
}
/**
* Get authentication token.
* @return {string} auth token
*/
login(api_url: string, username: string, password: string, options: GFRequestOptions): Promise<APILoginResponse> {
const params = {
user: username,
password: password
};
return this.request(api_url, 'user.login', params, options, null);
}
/**
* Get Zabbix API version
* Matches the version of Zabbix starting from Zabbix 2.0.4
*/
getVersion(api_url: string, options: GFRequestOptions): Promise<string> {
return this.request(api_url, 'apiinfo.version', [], options).catch(err => {
console.error(err);
return undefined;
});
}
}
// Define zabbix API exception type
export class ZabbixAPIError {
code: number;
name: string;
data: string;
message: string;
constructor(error: JSONRPCError) {
this.code = error.code || null;
this.name = error.message || "";
this.data = error.data || "";
this.message = "Zabbix API Error: " + this.name + " " + this.data;
}
toString() {
return this.name + " " + this.data;
}
}

View File

@@ -13,7 +13,6 @@ export interface ZabbixConnector {
getExtendedEventData: (eventids) => Promise<any>; getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>; getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
login: () => Promise<any>;
getGroups: (groupFilter?) => any; getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any; getHosts: (groupFilter?, hostFilter?) => any;

View File

@@ -29,7 +29,7 @@ const REQUESTS_TO_CACHE = [
const REQUESTS_TO_BIND = [ const REQUESTS_TO_BIND = [
'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts',
'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', 'getAcknowledges', 'getITService', 'getVersion', 'acknowledgeEvent', 'getProxies', 'getEventAlerts',
'getExtendedEventData' 'getExtendedEventData'
]; ];
@@ -55,13 +55,9 @@ export class Zabbix implements ZabbixConnector {
getExtendedEventData: (eventids) => Promise<any>; getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>; getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
login: () => Promise<any>;
constructor(options) { constructor(options) {
const { const {
url,
username,
password,
basicAuth, basicAuth,
withCredentials, withCredentials,
cacheTTL, cacheTTL,
@@ -81,7 +77,7 @@ export class Zabbix implements ZabbixConnector {
}; };
this.cachingProxy = new CachingProxy(cacheOptions); this.cachingProxy = new CachingProxy(cacheOptions);
this.zabbixAPI = new ZabbixAPIConnector(url, username, password, basicAuth, withCredentials, datasourceId); this.zabbixAPI = new ZabbixAPIConnector(basicAuth, withCredentials, datasourceId);
this.proxyfyRequests(); this.proxyfyRequests();
this.cacheRequests(); this.cacheRequests();