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
// }
func BuildAPIResponse(responseData *interface{}) (*ZabbixAPIResourceResponse, error) {
return &ZabbixAPIResourceResponse{
Result: *responseData,
}, nil
}
// BuildResponse transforms a Zabbix API response to a DatasourceResponse
func BuildResponse(responseData interface{}) (*datasource.DatasourceResponse, error) {
jsonBytes, err := json.Marshal(responseData)

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ type FunctionCategories struct {
}
// 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 err error
var queryExistInCache bool
@@ -41,8 +41,7 @@ func (dsInstance *ZabbixDatasourceInstance) ZabbixAPIQuery(ctx context.Context,
}
}
// return BuildResponse(result)
return &result, nil
return BuildAPIResponse(&result)
}
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 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++ {
if ds.authToken == "" {
// Authenticate

View File

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

View File

@@ -2,10 +2,9 @@ import _ from 'lodash';
import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { JSONRPCRequestParams } from './types';
import { GFHTTPRequest, JSONRPCError } from './types';
import { getBackendSrv } from '@grafana/runtime';
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.
*/
export class ZabbixAPIConnector {
url: string;
username: string;
password: string;
auth: string;
backendAPIUrl: string;
requestOptions: { basicAuth: any; withCredentials: boolean; };
loginPromise: Promise<string>;
loginErrorCount: number;
maxLoginAttempts: number;
zabbixAPICore: ZabbixAPICore;
getTrend: (items: any, timeFrom: any, timeTill: any) => Promise<any[]>;
version: string;
getVersionPromise: Promise<string>;
datasourceId: number;
constructor(api_url: string, username: string, password: string, basicAuth: any, withCredentials: boolean, datasourceId: number) {
this.url = api_url;
this.username = username;
this.password = password;
this.auth = '';
constructor(basicAuth: any, withCredentials: boolean, datasourceId: number) {
this.datasourceId = datasourceId;
this.backendAPIUrl = `/api/datasources/${this.datasourceId}/resources/zabbix-api`;
this.requestOptions = {
basicAuth: basicAuth,
withCredentials: withCredentials
};
this.datasourceId = datasourceId;
this.loginPromise = null;
this.loginErrorCount = 0;
this.maxLoginAttempts = 3;
this.zabbixAPICore = new ZabbixAPICore();
this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30;
@@ -59,113 +41,42 @@ export class ZabbixAPIConnector {
// Core method wrappers //
//////////////////////////
request(method, params) {
return this.tsdbRequest(method, params).then(response => {
// const result = this.handleTsdbResponse(response);
const result = this.handleZabbixAPIResourceResponse(response);
return result;
request(method: string, params?: any) {
return this.backendAPIRequest(method, params).then(response => {
return response?.data?.result;
});
}
tsdbRequest(method, params) {
const tsdbRequestData = {
queries: [{
datasourceId: this.datasourceId,
queryType: 'zabbixAPI',
target: {
method,
params,
},
}],
};
return getBackendSrv().datasourceRequest({
url: `/api/datasources/${this.datasourceId}/resources/zabbix-api`,
backendAPIRequest(method: string, params: any = {}) {
const requestOptions: GFHTTPRequest = {
url: this.backendAPIUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: {
datasourceId: this.datasourceId,
method,
params,
},
});
};
// return getBackendSrv().datasourceRequest({
// url: '/api/tsdb/query',
// method: 'POST',
// data: tsdbRequestData
// });
// Set request options for basic auth
if (this.requestOptions.basicAuth || this.requestOptions.withCredentials) {
requestOptions.withCredentials = true;
}
if (this.requestOptions.basicAuth) {
requestOptions.headers.Authorization = this.requestOptions.basicAuth;
}
_request(method: string, params: JSONRPCRequestParams): Promise<any> {
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);
return getBackendSrv().datasourceRequest(requestOptions);
}
/**
* Get Zabbix API version
*/
getVersion() {
return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
return this.request('apiinfo.version');
}
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) {
// Too many intervals may cause significant load on the database, so decrease number of resulting points
const resolutionRatio = 100;
@@ -767,3 +666,22 @@ function buildSLAIntervals(timeRange, interval) {
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>;
getMacros: (hostids: any[]) => Promise<any>;
getVersion: () => Promise<string>;
login: () => Promise<any>;
getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any;

View File

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