Fix parsing timeout (use number instead of string), fixes #1254

This commit is contained in:
Alexander Zobnin
2021-08-11 13:32:37 +03:00
parent 95afc7460d
commit 5ed80a60e7
10 changed files with 143 additions and 176 deletions

View File

@@ -2,16 +2,11 @@ package datasource
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"strconv"
"time"
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
"github.com/alexanderzobnin/grafana-zabbix/pkg/httpclient" "github.com/alexanderzobnin/grafana-zabbix/pkg/httpclient"
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
@@ -33,7 +28,7 @@ type ZabbixDatasource struct {
type ZabbixDatasourceInstance struct { type ZabbixDatasourceInstance struct {
zabbix *zabbix.Zabbix zabbix *zabbix.Zabbix
dsInfo *backend.DataSourceInstanceSettings dsInfo *backend.DataSourceInstanceSettings
Settings *ZabbixDatasourceSettings Settings *settings.ZabbixDatasourceSettings
logger log.Logger logger log.Logger
} }
@@ -46,36 +41,36 @@ func NewZabbixDatasource() *ZabbixDatasource {
} }
// newZabbixDatasourceInstance returns an initialized zabbix datasource instance // newZabbixDatasourceInstance returns an initialized zabbix datasource instance
func newZabbixDatasourceInstance(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { func newZabbixDatasourceInstance(dsSettings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
logger := log.New() logger := log.New()
logger.Debug("Initializing new data source instance") logger.Debug("Initializing new data source instance")
zabbixSettings, err := readZabbixSettings(&settings) zabbixSettings, err := settings.ReadZabbixSettings(&dsSettings)
if err != nil { if err != nil {
logger.Error("Error parsing Zabbix settings", "error", err) logger.Error("Error parsing Zabbix settings", "error", err)
return nil, err return nil, err
} }
client, err := httpclient.New(&settings, zabbixSettings.Timeout) client, err := httpclient.New(&dsSettings, zabbixSettings.Timeout)
if err != nil { if err != nil {
logger.Error("Error initializing HTTP client", "error", err) logger.Error("Error initializing HTTP client", "error", err)
return nil, err return nil, err
} }
zabbixAPI, err := zabbixapi.New(settings.URL, client) zabbixAPI, err := zabbixapi.New(dsSettings.URL, client)
if err != nil { if err != nil {
logger.Error("Error initializing Zabbix API", "error", err) logger.Error("Error initializing Zabbix API", "error", err)
return nil, err return nil, err
} }
zabbixClient, err := zabbix.New(&settings, zabbixAPI) zabbixClient, err := zabbix.New(&dsSettings, zabbixSettings, zabbixAPI)
if err != nil { if err != nil {
logger.Error("Error initializing Zabbix client", "error", err) logger.Error("Error initializing Zabbix client", "error", err)
return nil, err return nil, err
} }
return &ZabbixDatasourceInstance{ return &ZabbixDatasourceInstance{
dsInfo: &settings, dsInfo: &dsSettings,
zabbix: zabbixClient, zabbix: zabbixClient,
Settings: zabbixSettings, Settings: zabbixSettings,
logger: logger, logger: logger,
@@ -153,58 +148,3 @@ func (ds *ZabbixDatasource) getDSInstance(pluginContext backend.PluginContext) (
} }
return instance.(*ZabbixDatasourceInstance), nil return instance.(*ZabbixDatasourceInstance), nil
} }
func readZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) {
zabbixSettingsDTO := &ZabbixDatasourceSettingsDTO{}
err := json.Unmarshal(dsInstanceSettings.JSONData, &zabbixSettingsDTO)
if err != nil {
return nil, err
}
if zabbixSettingsDTO.TrendsFrom == "" {
zabbixSettingsDTO.TrendsFrom = "7d"
}
if zabbixSettingsDTO.TrendsRange == "" {
zabbixSettingsDTO.TrendsRange = "4d"
}
if zabbixSettingsDTO.CacheTTL == "" {
zabbixSettingsDTO.CacheTTL = "1h"
}
if zabbixSettingsDTO.Timeout == "" {
zabbixSettingsDTO.Timeout = "30"
}
trendsFrom, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsFrom)
if err != nil {
return nil, err
}
trendsRange, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsRange)
if err != nil {
return nil, err
}
cacheTTL, err := gtime.ParseInterval(zabbixSettingsDTO.CacheTTL)
if err != nil {
return nil, err
}
timeout, err := strconv.Atoi(zabbixSettingsDTO.Timeout)
if err != nil {
return nil, errors.New("failed to parse timeout: " + err.Error())
}
zabbixSettings := &ZabbixDatasourceSettings{
Trends: zabbixSettingsDTO.Trends,
TrendsFrom: trendsFrom,
TrendsRange: trendsRange,
CacheTTL: cacheTTL,
Timeout: time.Duration(timeout) * time.Second,
DisableDataAlignment: zabbixSettingsDTO.DisableDataAlignment,
DisableReadOnlyUsersAck: zabbixSettingsDTO.DisableReadOnlyUsersAck,
}
return zabbixSettings, nil
}

View File

@@ -21,30 +21,6 @@ const (
MODE_PROBLEMS = "5" MODE_PROBLEMS = "5"
) )
// ZabbixDatasourceSettingsDTO model
type ZabbixDatasourceSettingsDTO struct {
Trends bool `json:"trends"`
TrendsFrom string `json:"trendsFrom"`
TrendsRange string `json:"trendsRange"`
CacheTTL string `json:"cacheTTL"`
Timeout string `json:"timeout"`
DisableDataAlignment bool `json:"disableDataAlignment"`
DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
}
// ZabbixDatasourceSettings model
type ZabbixDatasourceSettings struct {
Trends bool
TrendsFrom time.Duration
TrendsRange time.Duration
CacheTTL time.Duration
Timeout time.Duration
DisableDataAlignment bool `json:"disableDataAlignment"`
DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
}
type DBConnectionPostProcessingRequest struct { type DBConnectionPostProcessingRequest struct {
Query QueryModel `json:"query"` Query QueryModel `json:"query"`
TimeRange TimeRangePostProcessingRequest `json:"timeRange"` TimeRange TimeRangePostProcessingRequest `json:"timeRange"`

View File

@@ -1,6 +1,7 @@
package datasource package datasource
import ( import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/log"
@@ -23,7 +24,7 @@ func mockZabbixQuery(method string, params zabbix.ZabbixAPIParams) *zabbix.Zabbi
} }
func MockZabbixDataSource(body string, statusCode int) *ZabbixDatasourceInstance { func MockZabbixDataSource(body string, statusCode int) *ZabbixDatasourceInstance {
zabbixSettings, _ := readZabbixSettings(basicDatasourceInfo) zabbixSettings, _ := settings.ReadZabbixSettings(basicDatasourceInfo)
zabbixClient, _ := zabbix.MockZabbixClient(basicDatasourceInfo, body, statusCode) zabbixClient, _ := zabbix.MockZabbixClient(basicDatasourceInfo, body, statusCode)
return &ZabbixDatasourceInstance{ return &ZabbixDatasourceInstance{

27
pkg/settings/models.go Normal file
View File

@@ -0,0 +1,27 @@
package settings
import "time"
// ZabbixDatasourceSettingsDTO model
type ZabbixDatasourceSettingsDTO struct {
Trends bool `json:"trends"`
TrendsFrom string `json:"trendsFrom"`
TrendsRange string `json:"trendsRange"`
CacheTTL string `json:"cacheTTL"`
Timeout interface{} `json:"timeout"`
DisableDataAlignment bool `json:"disableDataAlignment"`
DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
}
// ZabbixDatasourceSettings model
type ZabbixDatasourceSettings struct {
Trends bool
TrendsFrom time.Duration
TrendsRange time.Duration
CacheTTL time.Duration
Timeout time.Duration
DisableDataAlignment bool `json:"disableDataAlignment"`
DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"`
}

View File

@@ -1,16 +1,15 @@
package zabbix package settings
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"strconv"
"time"
"github.com/alexanderzobnin/grafana-zabbix/pkg/gtime" "github.com/alexanderzobnin/grafana-zabbix/pkg/gtime"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"strconv"
"time"
) )
func readZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) { func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) {
zabbixSettingsDTO := &ZabbixDatasourceSettingsDTO{} zabbixSettingsDTO := &ZabbixDatasourceSettingsDTO{}
err := json.Unmarshal(dsInstanceSettings.JSONData, &zabbixSettingsDTO) err := json.Unmarshal(dsInstanceSettings.JSONData, &zabbixSettingsDTO)
@@ -28,9 +27,9 @@ func readZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
zabbixSettingsDTO.CacheTTL = "1h" zabbixSettingsDTO.CacheTTL = "1h"
} }
if zabbixSettingsDTO.Timeout == "" { //if zabbixSettingsDTO.Timeout == 0 {
zabbixSettingsDTO.Timeout = "30" // zabbixSettingsDTO.Timeout = 30
} //}
trendsFrom, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsFrom) trendsFrom, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsFrom)
if err != nil { if err != nil {
@@ -47,17 +46,32 @@ func readZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings)
return nil, err return nil, err
} }
timeout, err := strconv.Atoi(zabbixSettingsDTO.Timeout) var timeout int64
if err != nil { switch t := zabbixSettingsDTO.Timeout.(type) {
return nil, errors.New("failed to parse timeout: " + err.Error()) case string:
if t == "" {
timeout = 30
break
}
timeoutInt, err := strconv.Atoi(t)
if err != nil {
return nil, errors.New("failed to parse timeout: " + err.Error())
}
timeout = int64(timeoutInt)
case float64:
timeout = int64(t)
default:
timeout = 30
} }
zabbixSettings := &ZabbixDatasourceSettings{ zabbixSettings := &ZabbixDatasourceSettings{
Trends: zabbixSettingsDTO.Trends, Trends: zabbixSettingsDTO.Trends,
TrendsFrom: trendsFrom, TrendsFrom: trendsFrom,
TrendsRange: trendsRange, TrendsRange: trendsRange,
CacheTTL: cacheTTL, CacheTTL: cacheTTL,
Timeout: time.Duration(timeout) * time.Second, Timeout: time.Duration(timeout) * time.Second,
DisableDataAlignment: zabbixSettingsDTO.DisableDataAlignment,
DisableReadOnlyUsersAck: zabbixSettingsDTO.DisableReadOnlyUsersAck,
} }
return zabbixSettings, nil return zabbixSettings, nil

View File

@@ -1,8 +1,10 @@
package zabbix package zabbix
import ( import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"time"
) )
func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, statusCode int) (*Zabbix, error) { func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, statusCode int) (*Zabbix, error) {
@@ -10,8 +12,11 @@ func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, s
if err != nil { if err != nil {
return nil, err return nil, err
} }
zabbixSettings := &settings.ZabbixDatasourceSettings{
Timeout: 10 * time.Second,
}
client, err := New(dsInfo, zabbixAPI) client, err := New(dsInfo, zabbixSettings, zabbixAPI)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/alexanderzobnin/grafana-zabbix/pkg/settings"
"github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi"
"github.com/bitly/go-simplejson" "github.com/bitly/go-simplejson"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -22,15 +23,8 @@ type Zabbix struct {
} }
// New returns new instance of Zabbix client. // New returns new instance of Zabbix client.
func New(dsInfo *backend.DataSourceInstanceSettings, zabbixAPI *zabbixapi.ZabbixAPI) (*Zabbix, error) { func New(dsInfo *backend.DataSourceInstanceSettings, zabbixSettings *settings.ZabbixDatasourceSettings, zabbixAPI *zabbixapi.ZabbixAPI) (*Zabbix, error) {
logger := log.New() logger := log.New()
zabbixSettings, err := readZabbixSettings(dsInfo)
if err != nil {
logger.Error("Error parsing Zabbix settings", "error", err)
return nil, err
}
zabbixCache := NewZabbixCache(zabbixSettings.CacheTTL, 10*time.Minute) zabbixCache := NewZabbixCache(zabbixSettings.CacheTTL, 10*time.Minute)
return &Zabbix{ return &Zabbix{

View File

@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data';
import { DataSourceHttpSettings, LegacyForms, Field, Input, Button, InlineFormLabel, Select } from '@grafana/ui'; import { Button, DataSourceHttpSettings, InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
const { FormField, Switch } = LegacyForms;
import { ZabbixDSOptions, ZabbixSecureJSONData } from '../types'; import { ZabbixDSOptions, ZabbixSecureJSONData } from '../types';
const { FormField, Switch } = LegacyForms;
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb']; const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
export type Props = DataSourcePluginOptionsEditorProps<ZabbixDSOptions, ZabbixSecureJSONData>; export type Props = DataSourcePluginOptionsEditorProps<ZabbixDSOptions, ZabbixSecureJSONData>;
@@ -34,7 +35,7 @@ export const ConfigEditor = (props: Props) => {
trendsFrom: '', trendsFrom: '',
trendsRange: '', trendsRange: '',
cacheTTL: '', cacheTTL: '',
timeout: '', timeout: undefined,
disableDataAlignment: false, disableDataAlignment: false,
...restJsonData, ...restJsonData,
}, },
@@ -99,7 +100,7 @@ export const ConfigEditor = (props: Props) => {
placeholder="Configured" placeholder="Configured"
/> />
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button> <Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
</>: </> :
<FormField <FormField
labelWidth={7} labelWidth={7}
inputWidth={15} inputWidth={15}
@@ -118,32 +119,32 @@ export const ConfigEditor = (props: Props) => {
onChange={jsonDataSwitchHandler('trends', options, onOptionsChange)} onChange={jsonDataSwitchHandler('trends', options, onOptionsChange)}
/> />
{options.jsonData.trends && {options.jsonData.trends &&
<> <>
<div className="gf-form"> <div className="gf-form">
<FormField <FormField
labelWidth={7} labelWidth={7}
inputWidth={4} inputWidth={4}
label="After" label="After"
value={options.jsonData.trendsFrom || ''} value={options.jsonData.trendsFrom || ''}
placeholder="7d" placeholder="7d"
onChange={jsonDataChangeHandler('trendsFrom', options, onOptionsChange)} onChange={jsonDataChangeHandler('trendsFrom', options, onOptionsChange)}
tooltip="Time after which trends will be used. tooltip="Time after which trends will be used.
Best practice is to set this value to your history storage period (7d, 30d, etc)." Best practice is to set this value to your history storage period (7d, 30d, etc)."
/> />
</div> </div>
<div className="gf-form"> <div className="gf-form">
<FormField <FormField
labelWidth={7} labelWidth={7}
inputWidth={4} inputWidth={4}
label="Range" label="Range"
value={options.jsonData.trendsRange || ''} value={options.jsonData.trendsRange || ''}
placeholder="4d" placeholder="4d"
onChange={jsonDataChangeHandler('trendsRange', options, onOptionsChange)} onChange={jsonDataChangeHandler('trendsRange', options, onOptionsChange)}
tooltip="Time range width after which trends will be used instead of history. tooltip="Time range width after which trends will be used instead of history.
It's better to set this value in range of 4 to 7 days to prevent loading large amount of history data." It's better to set this value in range of 4 to 7 days to prevent loading large amount of history data."
/> />
</div> </div>
</> </>
} }
<div className="gf-form"> <div className="gf-form">
<FormField <FormField
@@ -160,10 +161,15 @@ export const ConfigEditor = (props: Props) => {
<FormField <FormField
labelWidth={7} labelWidth={7}
inputWidth={4} inputWidth={4}
type="number"
label="Timeout" label="Timeout"
value={options.jsonData.timeout || ''} value={options.jsonData.timeout}
placeholder="30" onChange={(event) => {
onChange={jsonDataChangeHandler('timeout', options, onOptionsChange)} onOptionsChange({
...options,
jsonData: { ...options.jsonData, timeout: parseInt(event.currentTarget.value, 10) },
});
}}
tooltip="Zabbix API connection timeout in seconds. Default is 30." tooltip="Zabbix API connection timeout in seconds. Default is 30."
/> />
</div> </div>
@@ -178,31 +184,31 @@ export const ConfigEditor = (props: Props) => {
onChange={jsonDataSwitchHandler('dbConnectionEnable', options, onOptionsChange)} onChange={jsonDataSwitchHandler('dbConnectionEnable', options, onOptionsChange)}
/> />
{options.jsonData.dbConnectionEnable && {options.jsonData.dbConnectionEnable &&
<> <>
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel width={9}>Data Source</InlineFormLabel> <InlineFormLabel width={9}>Data Source</InlineFormLabel>
<Select <Select
width={32} width={32}
options={getDirectDBDSOptions()} options={getDirectDBDSOptions()}
value={selectedDBDatasource} value={selectedDBDatasource}
onChange={directDBDatasourceChanegeHandler(options, onOptionsChange, setSelectedDBDatasource, setCurrentDSType)} onChange={directDBDatasourceChanegeHandler(options, onOptionsChange, setSelectedDBDatasource, setCurrentDSType)}
/> />
</div> </div>
{currentDSType === 'influxdb' && {currentDSType === 'influxdb' &&
<div className="gf-form"> <div className="gf-form">
<FormField <FormField
labelWidth={9} labelWidth={9}
inputWidth={16} inputWidth={16}
label="Retention Policy" label="Retention Policy"
value={options.jsonData.dbConnectionRetentionPolicy || ''} value={options.jsonData.dbConnectionRetentionPolicy || ''}
placeholder="Retention policy name" placeholder="Retention policy name"
onChange={jsonDataChangeHandler('dbConnectionRetentionPolicy', options, onOptionsChange)} onChange={jsonDataChangeHandler('dbConnectionRetentionPolicy', options, onOptionsChange)}
tooltip="Specify retention policy name for fetching long-term stored data (optional). tooltip="Specify retention policy name for fetching long-term stored data (optional).
Leave it blank if only default retention policy used." Leave it blank if only default retention policy used."
/> />
</div> </div>
} }
</> </>
} }
</div> </div>

View File

@@ -106,7 +106,7 @@ function convertToRegex(str) {
} }
} }
export const DS_CONFIG_SCHEMA = 2; export const DS_CONFIG_SCHEMA = 3;
export function migrateDSConfig(jsonData) { export function migrateDSConfig(jsonData) {
if (!jsonData) { if (!jsonData) {
@@ -127,6 +127,10 @@ export function migrateDSConfig(jsonData) {
delete jsonData.dbConnection; delete jsonData.dbConnection;
} }
if (oldVersion < 3) {
jsonData.timeout = (jsonData.timeout as string) === "" ? null : Number(jsonData.timeout as string);
}
return jsonData; return jsonData;
} }
@@ -134,7 +138,7 @@ function shouldMigrateDSConfig(jsonData): boolean {
if (jsonData.dbConnection && !_.isEmpty(jsonData.dbConnection)) { if (jsonData.dbConnection && !_.isEmpty(jsonData.dbConnection)) {
return true; return true;
} }
if (jsonData.schema && jsonData.schema !== DS_CONFIG_SCHEMA) { if (jsonData.schema && jsonData.schema < DS_CONFIG_SCHEMA) {
return true; return true;
} }
return false; return false;

View File

@@ -7,7 +7,7 @@ export interface ZabbixDSOptions extends DataSourceJsonData {
trendsFrom: string; trendsFrom: string;
trendsRange: string; trendsRange: string;
cacheTTL: string; cacheTTL: string;
timeout?: string; timeout?: number;
dbConnectionEnable: boolean; dbConnectionEnable: boolean;
dbConnectionDatasourceId?: number; dbConnectionDatasourceId?: number;
dbConnectionDatasourceName?: string; dbConnectionDatasourceName?: string;