package mysql import ( "context" "database/sql" "encoding/json" "errors" "fmt" "net/url" "reflect" "strconv" "strings" "time" "github.com/VividCortex/mysqlerr" "github.com/go-sql-driver/mysql" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana/pkg/tsdb/mysql/sqleng" ) const ( dateFormat = "2006-01-02" dateTimeFormat1 = "2006-01-02 15:04:05" dateTimeFormat2 = "2006-01-02T15:04:05Z" ) func characterEscape(s string, escapeChar string) string { return strings.ReplaceAll(s, escapeChar, url.QueryEscape(escapeChar)) } func NewInstanceSettings(logger log.Logger) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { cfg := backend.GrafanaConfigFromContext(ctx) sqlCfg, err := cfg.SQL() if err != nil { return nil, err } jsonData := sqleng.JsonData{ MaxOpenConns: sqlCfg.DefaultMaxOpenConns, MaxIdleConns: sqlCfg.DefaultMaxIdleConns, ConnMaxLifetime: sqlCfg.DefaultMaxConnLifetimeSeconds, SecureDSProxy: false, AllowCleartextPasswords: false, } err = json.Unmarshal(settings.JSONData, &jsonData) if err != nil { return nil, fmt.Errorf("error reading settings: %w", err) } database := jsonData.Database if database == "" { database = settings.Database } dsInfo := sqleng.DataSourceInfo{ JsonData: jsonData, URL: settings.URL, User: settings.User, Database: database, ID: settings.ID, Updated: settings.Updated, UID: settings.UID, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } protocol := "tcp" if strings.HasPrefix(dsInfo.URL, "/") { protocol = "unix" } proxyClient, err := settings.ProxyClient(ctx) if err != nil { return nil, err } // register the secure socks proxy dialer context, if enabled if proxyClient.SecureSocksProxyEnabled() { dialer, err := proxyClient.NewSecureSocksProxyContextDialer() if err != nil { return nil, err } // UID is only unique per org, the only way to ensure uniqueness is to do it by connection information uniqueIdentifier := dsInfo.User + dsInfo.DecryptedSecureJSONData["password"] + dsInfo.URL + dsInfo.Database protocol, err = registerProxyDialerContext(protocol, uniqueIdentifier, dialer) if err != nil { return nil, err } } cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true", characterEscape(dsInfo.User, ":"), dsInfo.DecryptedSecureJSONData["password"], protocol, characterEscape(dsInfo.URL, ")"), characterEscape(dsInfo.Database, "?"), ) if dsInfo.JsonData.AllowCleartextPasswords { cnnstr += "&allowCleartextPasswords=true" } opts, err := settings.HTTPClientOptions(ctx) if err != nil { return nil, err } tlsConfig, err := sdkhttpclient.GetTLSConfig(opts) if err != nil { return nil, err } if tlsConfig.RootCAs != nil || len(tlsConfig.Certificates) > 0 { tlsConfigString := fmt.Sprintf("ds%d", settings.ID) if err := mysql.RegisterTLSConfig(tlsConfigString, tlsConfig); err != nil { return nil, err } cnnstr += "&tls=" + tlsConfigString } if dsInfo.JsonData.Timezone != "" { cnnstr += fmt.Sprintf("&time_zone='%s'", url.QueryEscape(dsInfo.JsonData.Timezone)) } config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, TimeColumnNames: []string{"time", "time_sec"}, MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}, RowLimit: sqlCfg.RowLimit, } userFacingDefaultError, err := cfg.UserFacingDefaultError() if err != nil { return nil, err } rowTransformer := mysqlQueryResultTransformer{ userError: userFacingDefaultError, } db, err := sql.Open("mysql", cnnstr) if err != nil { return nil, err } db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) return sqleng.NewQueryDataHandler(userFacingDefaultError, db, config, &rowTransformer, newMysqlMacroEngine(logger, userFacingDefaultError), logger) } } type mysqlQueryResultTransformer struct { userError string } func (t *mysqlQueryResultTransformer) TransformQueryError(logger log.Logger, err error) error { var driverErr *mysql.MySQLError if errors.As(err, &driverErr) { if driverErr.Number != mysqlerr.ER_PARSE_ERROR && driverErr.Number != mysqlerr.ER_BAD_FIELD_ERROR && driverErr.Number != mysqlerr.ER_NO_SUCH_TABLE { logger.Error("Query error", "error", err) return fmt.Errorf(("query failed - %s"), t.userError) } } return err } func (t *mysqlQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { // For the MySQL driver , we have these possible data types: // https://www.w3schools.com/sql/sql_datatypes.asp#:~:text=In%20MySQL%20there%20are%20three,numeric%2C%20and%20date%20and%20time. // Since by default, we convert all into String, we need only to handle the Numeric data types return []sqlutil.StringConverter{ { Name: "handle DOUBLE", InputScanKind: reflect.Struct, InputTypeName: "DOUBLE", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableFloat64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseFloat(*in, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle BIGINT", InputScanKind: reflect.Struct, InputTypeName: "BIGINT", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableInt64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseInt(*in, 10, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle DECIMAL", InputScanKind: reflect.Slice, InputTypeName: "DECIMAL", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableFloat64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseFloat(*in, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle DATETIME", InputScanKind: reflect.Struct, InputTypeName: "DATETIME", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableTime, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := time.Parse(dateTimeFormat1, *in) if err == nil { return &v, nil } v, err = time.Parse(dateTimeFormat2, *in) if err == nil { return &v, nil } return nil, err }, }, }, { Name: "handle DATE", InputScanKind: reflect.Struct, InputTypeName: "DATE", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableTime, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := time.Parse(dateFormat, *in) if err == nil { return &v, nil } v, err = time.Parse(dateTimeFormat1, *in) if err == nil { return &v, nil } v, err = time.Parse(dateTimeFormat2, *in) if err == nil { return &v, nil } return nil, err }, }, }, { Name: "handle TIMESTAMP", InputScanKind: reflect.Struct, InputTypeName: "TIMESTAMP", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableTime, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := time.Parse(dateTimeFormat1, *in) if err == nil { return &v, nil } v, err = time.Parse(dateTimeFormat2, *in) if err == nil { return &v, nil } return nil, err }, }, }, { Name: "handle YEAR", InputScanKind: reflect.Struct, InputTypeName: "YEAR", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableInt64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseInt(*in, 10, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle TINYINT", InputScanKind: reflect.Struct, InputTypeName: "TINYINT", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableInt64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseInt(*in, 10, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle SMALLINT", InputScanKind: reflect.Struct, InputTypeName: "SMALLINT", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableInt64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseInt(*in, 10, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle INT", InputScanKind: reflect.Struct, InputTypeName: "INT", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableInt64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseInt(*in, 10, 64) if err != nil { return nil, err } return &v, nil }, }, }, { Name: "handle FLOAT", InputScanKind: reflect.Struct, InputTypeName: "FLOAT", ConversionFunc: func(in *string) (*string, error) { return in, nil }, Replacer: &sqlutil.StringFieldReplacer{ OutputFieldType: data.FieldTypeNullableFloat64, ReplaceFunc: func(in *string) (any, error) { if in == nil { return nil, nil } v, err := strconv.ParseFloat(*in, 64) if err != nil { return nil, err } return &v, nil }, }, }, } }