mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			413 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			413 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| 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
 | |
| 		} else if tlsConfig.InsecureSkipVerify {
 | |
| 			cnnstr += "&tls=skip-verify"
 | |
| 		}
 | |
| 
 | |
| 		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
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| }
 |