/* * Copyright 2018 Xiaomi, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package common import ( "bufio" "bytes" "encoding/json" "flag" "fmt" "io" "io/ioutil" "os" "path/filepath" "regexp" "runtime" "strings" "time" "github.com/go-sql-driver/mysql" yaml "gopkg.in/yaml.v2" ) var ( // BlackList 黑名单中的SQL不会被评审 BlackList []string // PrintConfig -print-config PrintConfig bool // PrintVersion -print-config PrintVersion bool // CheckConfig -check-config CheckConfig bool // 防止 readCmdFlags 函数重入 hasParsed bool ) // Configuration 配置文件定义结构体 type Configuration struct { // +++++++++++++++测试环境+++++++++++++++++ OnlineDSN *Dsn `yaml:"online-dsn"` // 线上环境数据库配置 TestDSN *Dsn `yaml:"test-dsn"` // 测试环境数据库配置 AllowOnlineAsTest bool `yaml:"allow-online-as-test"` // 允许 Online 环境也可以当作 Test 环境 DisableVersionCheck bool `yaml:"disable-version-check"` // 是否禁用环境检测,开启后表示允许测试环境版本低于线上环境 不建议开启,可能会导致语句执行异常 DropTestTemporary bool `yaml:"drop-test-temporary"` // 是否清理Test环境产生的临时库表 CleanupTestDatabase bool `yaml:"cleanup-test-database"` // 清理残余的测试数据库(程序异常退出或未开启drop-test-temporary) issue #48 OnlySyntaxCheck bool `yaml:"only-syntax-check"` // 只做语法检查不输出优化建议 SamplingStatisticTarget int `yaml:"sampling-statistic-target"` // 数据采样因子,对应 PostgreSQL 的 default_statistics_target Sampling bool `yaml:"sampling"` // 数据采样开关 SamplingCondition string `yaml:"sampling-condition"` // 指定采样条件,如:WHERE xxx LIMIT xxx; Profiling bool `yaml:"profiling"` // 在开启数据采样的情况下,在测试环境执行进行profile Trace bool `yaml:"trace"` // 在开启数据采样的情况下,在测试环境执行进行Trace Explain bool `yaml:"explain"` // Explain开关 Delimiter string `yaml:"delimiter"` // SQL分隔符 // +++++++++++++++日志相关+++++++++++++++++ // 日志级别,这里使用了 beego 的 log 包 // [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug] LogLevel int `yaml:"log-level"` // 日志输出位置,默认日志输出到控制台 // 目前只支持['console', 'file']两种形式,如非console形式这里需要指定文件的路径,可以是相对路径 LogOutput string `yaml:"log-output"` // 优化建议输出格式,目前支持: json, text, markdown格式,如指定其他格式会给 pretty.Println 的输出 ReportType string `yaml:"report-type"` // 当 ReportType 为 html 格式时使用的 css 风格,如不指定会提供一个默认风格。CSS可 以是本地文件,也可以是一个URL ReportCSS string `yaml:"report-css"` // 当 ReportType 为 html 格式时使用的 javascript 脚本,如不指定默认会加载SQL pretty 使用的 javascript。像CSS一样可以是本地文件,也可以是一个URL ReportJavascript string `yaml:"report-javascript"` // 当ReportType 为 html 格式时,HTML 的 title ReportTitle string `yaml:"report-title"` // blackfriday markdown2html config MarkdownExtensions int `yaml:"markdown-extensions"` // markdown 转 html 支持的扩展包, 参考blackfriday MarkdownHTMLFlags int `yaml:"markdown-html-flags"` // markdown 转 html 支持的 flag, 参考blackfriday, default 0 // ++++++++++++++优化建议相关++++++++++++++ IgnoreRules []string `yaml:"ignore-rules"` // 忽略的优化建议规则 RewriteRules []string `yaml:"rewrite-rules"` // 生效的重写规则 BlackList string `yaml:"blacklist"` // blacklist 中的 SQL 不会被评审,可以是指纹,也可以是正则 MaxJoinTableCount int `yaml:"max-join-table-count"` // 单条 SQL 中 JOIN 表的最大数量 MaxGroupByColsCount int `yaml:"max-group-by-cols-count"` // 单条 SQL 中 GroupBy 包含列的最大数量 MaxDistinctCount int `yaml:"max-distinct-count"` // 单条 SQL 中 Distinct 的最大数量 MaxIdxColsCount int `yaml:"max-index-cols-count"` // 复合索引中包含列的最大数量 MaxTextColsCount int `yaml:"max-text-cols-count"` // 表中含有的 text/blob 列的最大数量 MaxTotalRows uint64 `yaml:"max-total-rows"` // 计算散粒度时,当数据行数大于 MaxTotalRows 即开启数据库保护模式,散粒度返回结果可信度下降 MaxQueryCost int64 `yaml:"max-query-cost"` // last_query_cost 超过该值时将给予警告 SpaghettiQueryLength int `yaml:"spaghetti-query-length"` // SQL最大长度警告,超过该长度会给警告 AllowDropIndex bool `yaml:"allow-drop-index"` // 允许输出删除重复索引的建议 MaxInCount int `yaml:"max-in-count"` // IN()最大数量 MaxIdxBytesPerColumn int `yaml:"max-index-bytes-percolumn"` // 索引中单列最大字节数,默认767 MaxIdxBytes int `yaml:"max-index-bytes"` // 索引总长度限制,默认3072 AllowCharsets []string `yaml:"allow-charsets"` // 允许使用的 DEFAULT CHARSET AllowCollates []string `yaml:"allow-collates"` // 允许使用的 COLLATE AllowEngines []string `yaml:"allow-engines"` // 允许使用的存储引擎 MaxIdxCount int `yaml:"max-index-count"` // 单张表允许最多索引数 MaxColCount int `yaml:"max-column-count"` // 单张表允许最大列数 MaxValueCount int `yaml:"max-value-count"` // INSERT/REPLACE 单次允许批量写入的行数 IdxPrefix string `yaml:"index-prefix"` // 普通索引建议使用的前缀 UkPrefix string `yaml:"unique-key-prefix"` // 唯一键建议使用的前缀 MaxSubqueryDepth int `yaml:"max-subquery-depth"` // 子查询最大尝试 MaxVarcharLength int `yaml:"max-varchar-length"` // varchar最大长度 ColumnNotAllowType []string `yaml:"column-not-allow-type"` // 字段不允许使用的数据类型 MinCardinality float64 `yaml:"min-cardinality"` // 添加索引散粒度阈值,范围 0~100 // ++++++++++++++EXPLAIN检查项+++++++++++++ ExplainSQLReportType string `yaml:"explain-sql-report-type"` // EXPLAIN markdown 格式输出 SQL 样式,支持 sample, fingerprint, pretty 等 ExplainType string `yaml:"explain-type"` // EXPLAIN方式 [traditional, extended, partitions] ExplainFormat string `yaml:"explain-format"` // FORMAT=[json, traditional] ExplainWarnSelectType []string `yaml:"explain-warn-select-type"` // 哪些 select_type 不建议使用 ExplainWarnAccessType []string `yaml:"explain-warn-access-type"` // 哪些 access type 不建议使用 ExplainMaxKeyLength int `yaml:"explain-max-keys"` // 最大 key_len ExplainMinPossibleKeys int `yaml:"explain-min-keys"` // 最小 possible_keys 警告 ExplainMaxRows int64 `yaml:"explain-max-rows"` // 最大扫描行数警告 ExplainWarnExtra []string `yaml:"explain-warn-extra"` // 哪些 extra 信息会给警告 ExplainMaxFiltered float64 `yaml:"explain-max-filtered"` // filtered 大于该配置给出警告 ExplainWarnScalability []string `yaml:"explain-warn-scalability"` // 复杂度警告名单 ShowWarnings bool `yaml:"show-warnings"` // explain extended with show warnings ShowLastQueryCost bool `yaml:"show-last-query-cost"` // switch with show status like 'last_query_cost' // ++++++++++++++其他配置项+++++++++++++++ Query string `yaml:"query"` // 需要进行调优的SQL ListHeuristicRules bool `yaml:"list-heuristic-rules"` // 打印支持的评审规则列表 ListRewriteRules bool `yaml:"list-rewrite-rules"` // 打印重写规则 ListTestSqls bool `yaml:"list-test-sqls"` // 打印测试case用于测试 ListReportTypes bool `yaml:"list-report-types"` // 打印支持的报告输出类型 Verbose bool `yaml:"verbose"` // verbose模式,会多输出一些信息 DryRun bool `yaml:"dry-run"` // 是否在预演环境执行 MaxPrettySQLLength int `yaml:"max-pretty-sql-length"` // 超出该长度的SQL会转换成指纹输出 } // Config 默认设置 var Config = &Configuration{ OnlineDSN: newDSN(nil), TestDSN: newDSN(nil), AllowOnlineAsTest: false, DropTestTemporary: true, CleanupTestDatabase: false, DryRun: true, OnlySyntaxCheck: false, SamplingStatisticTarget: 100, Sampling: false, Profiling: false, Trace: false, Explain: true, Delimiter: ";", MinCardinality: 0, MaxJoinTableCount: 5, MaxGroupByColsCount: 5, MaxDistinctCount: 5, MaxIdxColsCount: 5, MaxTextColsCount: 2, MaxIdxBytesPerColumn: 767, MaxIdxBytes: 3072, MaxTotalRows: 9999999, MaxQueryCost: 9999, SpaghettiQueryLength: 2048, AllowDropIndex: false, LogLevel: 3, LogOutput: "soar.log", ReportType: "markdown", ReportCSS: "", ReportJavascript: "", ReportTitle: "SQL优化分析报告", BlackList: "", AllowCharsets: []string{"utf8", "utf8mb4"}, AllowCollates: []string{}, AllowEngines: []string{"innodb"}, MaxIdxCount: 10, MaxColCount: 40, MaxValueCount: 100, MaxInCount: 10, IdxPrefix: "idx_", UkPrefix: "uk_", MaxSubqueryDepth: 5, MaxVarcharLength: 1024, ColumnNotAllowType: []string{"boolean"}, MarkdownExtensions: 94, MarkdownHTMLFlags: 0, ExplainSQLReportType: "pretty", ExplainType: "extended", ExplainFormat: "traditional", ExplainWarnSelectType: []string{""}, ExplainWarnAccessType: []string{"ALL"}, ExplainMaxKeyLength: 3, ExplainMinPossibleKeys: 0, ExplainMaxRows: 10000, ExplainWarnExtra: []string{"Using temporary", "Using filesort"}, ExplainMaxFiltered: 100.0, ExplainWarnScalability: []string{"O(n)"}, ShowWarnings: false, ShowLastQueryCost: false, IgnoreRules: []string{ "COL.011", }, RewriteRules: []string{ "delimiter", "orderbynull", "groupbyconst", "dmlorderby", "having", "star2columns", "insertcolumns", "distinctstar", }, ListHeuristicRules: false, ListRewriteRules: false, ListTestSqls: false, ListReportTypes: false, MaxPrettySQLLength: 1024, } // Dsn Data source name type Dsn struct { User string `yaml:"user"` // Usernames Password string `yaml:"password"` // Password (requires User) Net string `yaml:"net"` // Network type Addr string `yaml:"addr"` // Network address (requires Net) Schema string `yaml:"schema"` // Database name Charset string `yaml:"charset"` // SET NAMES charset Collation string `yaml:"collation"` // Connection collation Loc string `yaml:"loc"` // Location for time.Time values TLS string `yaml:"tls"` // TLS configuration name ServerPubKey string `yaml:"server-public-key"` // Server public key name MaxAllowedPacket int `yaml:"max-allowed-packet"` // Max packet size allowed Params map[string]string `yaml:"params"` // Other Connection parameters, `SET param=val`, `SET NAMES charset` Timeout string `yaml:"timeout"` // Dial timeout ReadTimeout string `yaml:"read-timeout"` // I/O read timeout WriteTimeout string `yaml:"write-timeout"` // I/O write timeout AllowNativePasswords bool `yaml:"allow-native-passwords"` // Allows the native password authentication method AllowOldPasswords bool `yaml:"allow-old-passwords"` // Allows the old insecure password method Disable bool `yaml:"disable"` Version int `yaml:"-"` // 版本自动检查,不可配置 } // newDSN create default Dsn struct func newDSN(cfg *mysql.Config) *Dsn { dsn := &Dsn{ Net: "tcp", Schema: "information_schema", Charset: "utf8", Timeout: "3s", AllowNativePasswords: true, Params: make(map[string]string), MaxAllowedPacket: 4 << 20, // 4 MiB // Disable: true, Version: 99999, } if cfg == nil { return dsn } dsn.User = cfg.User dsn.Password = cfg.Passwd dsn.Net = cfg.Net dsn.Addr = cfg.Addr dsn.Schema = cfg.DBName dsn.Params = make(map[string]string) for k, v := range cfg.Params { dsn.Params[k] = v } if _, ok := cfg.Params["charset"]; ok { dsn.Charset = cfg.Params["charset"] } dsn.Collation = cfg.Collation dsn.Loc = cfg.Loc.String() dsn.MaxAllowedPacket = cfg.MaxAllowedPacket dsn.ServerPubKey = cfg.ServerPubKey dsn.TLS = cfg.TLSConfig dsn.Timeout = cfg.Timeout.String() dsn.ReadTimeout = cfg.ReadTimeout.String() dsn.WriteTimeout = cfg.WriteTimeout.String() dsn.AllowNativePasswords = cfg.AllowNativePasswords dsn.AllowOldPasswords = cfg.AllowOldPasswords return dsn } // newMySQLConfig convert Dsn to go-sql-drive Config func (env *Dsn) newMySQLConfig() (*mysql.Config, error) { var err error dsn := mysql.NewConfig() dsn.User = env.User dsn.Passwd = env.Password dsn.Net = env.Net dsn.Addr = env.Addr dsn.DBName = env.Schema dsn.Params = make(map[string]string) for k, v := range env.Params { dsn.Params[k] = v } dsn.Params["charset"] = env.Charset dsn.Collation = env.Collation dsn.Loc, err = time.LoadLocation(env.Loc) if err != nil { return nil, err } dsn.MaxAllowedPacket = env.MaxAllowedPacket dsn.ServerPubKey = env.ServerPubKey dsn.TLSConfig = env.TLS if env.Timeout != "" { dsn.Timeout, err = time.ParseDuration(env.Timeout) LogIfError(err, "timeout: '%s'", env.Timeout) } if env.WriteTimeout != "" { dsn.WriteTimeout, err = time.ParseDuration(env.WriteTimeout) LogIfError(err, "writeTimeout: '%s'", env.WriteTimeout) } if env.ReadTimeout != "" { dsn.ReadTimeout, err = time.ParseDuration(env.ReadTimeout) LogIfError(err, "readTimeout: '%s'", env.ReadTimeout) } dsn.AllowNativePasswords = env.AllowNativePasswords dsn.AllowOldPasswords = env.AllowOldPasswords return dsn, err } // 解析命令行DSN输入 func parseDSN(odbc string, d *Dsn) *Dsn { dsn := newDSN(nil) var addr, user, password, schema, charset, timeout string if odbc == FormatDSN(d) { return d } if d != nil { // 原来有个判断,后来判断条件被删除了就导致第一次addr无论如何都会被修改。所以这边先注释掉 // addr = d.Addr user = d.User password = d.Password schema = d.Schema charset = d.Charset timeout = d.Timeout } // 设置为空表示禁用环境 odbc = strings.TrimSpace(odbc) if odbc == "" { return &Dsn{Disable: true} } var userInfo, hostInfo, query string // DSN 格式匹配 if res := regexp.MustCompile(`^(.*)@(.*?)/(.*?)($|\?)(.*)`).FindStringSubmatch(odbc); len(res) > 5 { // userInfo@hostInfo/database userInfo = res[1] hostInfo = res[2] schema = res[3] query = res[5] } else if res := regexp.MustCompile(`^(.*)/(.*?)($|\?)(.*)`).FindStringSubmatch(odbc); len(res) > 4 { // hostInfo/database hostInfo = res[1] schema = res[2] query = res[4] } else if res := regexp.MustCompile(`^(.*)@(.*?)($|\?)(.*)`).FindStringSubmatch(odbc); len(res) > 4 { // userInfo@hostInfo userInfo = res[1] hostInfo = res[2] query = res[4] } else if res := regexp.MustCompile(`^(.*?)($|\?)(.*)`).FindStringSubmatch(odbc); len(res) > 3 { // hostInfo hostInfo = res[1] query = res[3] } // 解析用户信息 if userInfo != "" { user = strings.Split(userInfo, ":")[0] // 防止密码中含有与用户名相同的字符, 所以用正则替换, 剩下的就是密码 password = strings.TrimLeft(regexp.MustCompile("^"+user).ReplaceAllString(userInfo, ""), ":") } // 解析主机信息 host := strings.Split(hostInfo, ":")[0] port := strings.TrimLeft(strings.Replace(hostInfo, host, "", 1), ":") if host == "" { host = "127.0.0.1" } if port == "" { port = "3306" } addr = host + ":" + port // 解析查询字符串 if query != "" { params := strings.Split(query, "&") for _, f := range params { attr := strings.Split(f, "=") if len(attr) > 1 { arg := strings.TrimSpace(attr[0]) val := strings.TrimSpace(attr[1]) switch arg { case "charset": charset = val case "timeout": timeout = val default: } } } } // 默认用information_schema库 if schema == "" { schema = "information_schema" } // 默认 utf8 使用字符集 if charset == "" { charset = "utf8" } // 默认连接数据库超时时间 3s if timeout == "" { timeout = "3s" } dsn.Addr = addr dsn.User = user dsn.Password = password dsn.Schema = schema dsn.Charset = charset dsn.Timeout = timeout return dsn } // ParseDSN compatible with old version soar < 0.11.0 func ParseDSN(odbc string, d *Dsn) *Dsn { cfg, err := mysql.ParseDSN(odbc) if err != nil { // Log.Debug("go-sql-driver/mysql.ParseDSN Error: %s, DSN: %s, try to use old version parseDSN", err.Error(), odbc) return parseDSN(odbc, d) } return newDSN(cfg) } // FormatDSN 格式化打印DSN func FormatDSN(env *Dsn) string { if env == nil || env.Disable { return "" } dsn, err := env.newMySQLConfig() if err != nil { return "" } return dsn.FormatDSN() } // SoarVersion soar version information func SoarVersion() { fmt.Println("Version:", Version) fmt.Println("Branch:", Branch) fmt.Println("Compile:", Compile) fmt.Println("GitDirty:", GitDirty) } // 因为vitess sqlparser 使用了 glog 中也会使用 flag,为了不让用户困扰我们单独写一个 usage func usage() { regPwd := regexp.MustCompile(`:.*@`) vitessHelp := []string{ "-alsologtostderr", "log to standard error as well as files", "-log_backtrace_at value", "when logging hits line file:N, emit a stack trace", "-log_dir string", "If non-empty, write log files in this directory", "-logtostderr", "log to standard error instead of files", "-sql-max-length-errors int", "truncate queries in error logs to the given length (default unlimited)", "-sql-max-length-ui int", "truncate queries in debug UIs to the given length (default 512) (default 512)", "-stderrthreshold value", "logs at or above this threshold go to stderr", "-v value", "log level for V logs", "-vmodule value", "comma-separated list of pattern=N settings for file-filtered logging", } // io redirect restoreStdout := os.Stdout restoreStderr := os.Stderr stdin, stdout, _ := os.Pipe() os.Stderr = stdout os.Stdout = stdout flag.PrintDefaults() // copy the output in a separate goroutine so printing can't block indefinitely outC := make(chan string) go func() { var buf bytes.Buffer _, err := io.Copy(&buf, stdin) if err != nil { fmt.Println(err.Error()) } outC <- buf.String() }() // back to normal state stdout.Close() os.Stdout = restoreStdout // restoring the real stderr os.Stderr = restoreStderr fmt.Printf("Usage of %s:\n", os.Args[0]) // reading our temp stdout out := <-outC for _, line := range strings.Split(out, "\n") { found := false for _, ignore := range vitessHelp { if strings.TrimSpace(line) == strings.TrimSpace(ignore) { found = true } if regPwd.MatchString(line) && !Config.Verbose { line = regPwd.ReplaceAllString(line, ":********@") } } if !found { fmt.Println(line) } } } // PrintConfiguration for `-print-config` flag func PrintConfiguration() { // 打印配置的时候密码不显示 if !Config.Verbose { Config.OnlineDSN.Password = "********" Config.TestDSN.Password = "********" } data, _ := yaml.Marshal(Config) fmt.Print(string(data)) } // 加载配置文件 func (conf *Configuration) readConfigFile(path string) error { configFile, err := os.Open(path) if err != nil { Log.Warning("readConfigFile(%s) os.Open failed: %v", path, err) return err } defer configFile.Close() content, err := ioutil.ReadAll(configFile) if err != nil { Log.Warning("readConfigFile(%s) ioutil.ReadAll failed: %v", path, err) return err } err = yaml.Unmarshal(content, Config) if err != nil { Log.Warning("readConfigFile(%s) yaml.Unmarshal failed: %v", path, err) return err } return nil } // 从命令行参数读配置 func readCmdFlags() error { if hasParsed { Log.Debug("Skip read cmd flags.") return nil } _ = flag.String("config", "", "Config file path") // +++++++++++++++测试环境+++++++++++++++++ onlineDSN := flag.String("online-dsn", FormatDSN(Config.OnlineDSN), "OnlineDSN, 线上环境数据库配置, username:password@tcp(ip:port)/schema") testDSN := flag.String("test-dsn", FormatDSN(Config.TestDSN), "TestDSN, 测试环境数据库配置, username:password@tcp(ip:port)/schema") allowOnlineAsTest := flag.Bool("allow-online-as-test", Config.AllowOnlineAsTest, "AllowOnlineAsTest, 允许线上环境也可以当作测试环境") dropTestTemporary := flag.Bool("drop-test-temporary", Config.DropTestTemporary, "DropTestTemporary, 是否清理测试环境产生的临时库表") cleanupTestDatabase := flag.Bool("cleanup-test-database", Config.CleanupTestDatabase, "单次运行清理历史1小时前残余的测试库。") onlySyntaxCheck := flag.Bool("only-syntax-check", Config.OnlySyntaxCheck, "OnlySyntaxCheck, 只做语法检查不输出优化建议") profiling := flag.Bool("profiling", Config.Profiling, "Profiling, 开启数据采样的情况下在测试环境执行Profile") trace := flag.Bool("trace", Config.Trace, "Trace, 开启数据采样的情况下在测试环境执行Trace") explain := flag.Bool("explain", Config.Explain, "Explain, 是否开启Explain执行计划分析") sampling := flag.Bool("sampling", Config.Sampling, "Sampling, 数据采样开关") samplingStatisticTarget := flag.Int("sampling-statistic-target", Config.SamplingStatisticTarget, "SamplingStatisticTarget, 数据采样因子,对应 PostgreSQL 的 default_statistics_target") samplingCondition := flag.String("sampling-condition", Config.SamplingCondition, "SamplingCondition, 数据采样条件,如: WHERE xxx LIMIT xxx") delimiter := flag.String("delimiter", Config.Delimiter, "Delimiter, SQL分隔符") minCardinality := flag.Float64("min-cardinality", Config.MinCardinality, "MinCardinality,索引列散粒度最低阈值,散粒度低于该值的列不添加索引,建议范围0.0 ~ 100.0") // +++++++++++++++日志相关+++++++++++++++++ logLevel := flag.Int("log-level", Config.LogLevel, "LogLevel, 日志级别, [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug]") logOutput := flag.String("log-output", Config.LogOutput, "LogOutput, 日志输出位置") reportType := flag.String("report-type", Config.ReportType, "ReportType, 优化建议输出格式,目前支持: json, text, markdown, html等") reportCSS := flag.String("report-css", Config.ReportCSS, "ReportCSS, 当 ReportType 为 html 格式时使用的 css 风格,如不指定会提供一个默认风格。CSS可以是本地文件,也可以是一个URL") reportJavascript := flag.String("report-javascript", Config.ReportJavascript, "ReportJavascript, 当 ReportType 为 html 格式时使用的javascript脚本,如不指定默认会加载SQL pretty 使用的 javascript。像CSS一样可以是本地文件,也可以是一个URL") reportTitle := flag.String("report-title", Config.ReportTitle, "ReportTitle, 当 ReportType 为 html 格式时,HTML 的 title") // +++++++++++++++markdown+++++++++++++++++ markdownExtensions := flag.Int("markdown-extensions", Config.MarkdownExtensions, "MarkdownExtensions, markdown 转 html支持的扩展包, 参考blackfriday") markdownHTMLFlags := flag.Int("markdown-html-flags", Config.MarkdownHTMLFlags, "MarkdownHTMLFlags, markdown 转 html 支持的 flag, 参考blackfriday") // ++++++++++++++优化建议相关++++++++++++++ ignoreRules := flag.String("ignore-rules", strings.Join(Config.IgnoreRules, ","), "IgnoreRules, 忽略的优化建议规则") rewriteRules := flag.String("rewrite-rules", strings.Join(Config.RewriteRules, ","), "RewriteRules, 生效的重写规则") blackList := flag.String("blacklist", Config.BlackList, "指定 blacklist 配置文件的位置,文件中的 SQL 不会被评审。一行一条SQL,可以是指纹,也可以是正则") maxJoinTableCount := flag.Int("max-join-table-count", Config.MaxJoinTableCount, "MaxJoinTableCount, 单条 SQL 中 JOIN 表的最大数量") maxGroupByColsCount := flag.Int("max-group-by-cols-count", Config.MaxGroupByColsCount, "MaxGroupByColsCount, 单条 SQL 中 GroupBy 包含列的最大数量") maxDistinctCount := flag.Int("max-distinct-count", Config.MaxDistinctCount, "MaxDistinctCount, 单条 SQL 中 Distinct 的最大数量") maxIdxColsCount := flag.Int("max-index-cols-count", Config.MaxIdxColsCount, "MaxIdxColsCount, 复合索引中包含列的最大数量") maxTextColsCount := flag.Int("max-text-cols-count", Config.MaxTextColsCount, "MaxTextColsCount, 表中含有的 text/blob 列的最大数量") maxTotalRows := flag.Uint64("max-total-rows", Config.MaxTotalRows, "MaxTotalRows, 计算散粒度时,当数据行数大于MaxTotalRows即开启数据库保护模式,不计算散粒度") maxQueryCost := flag.Int64("max-query-cost", Config.MaxQueryCost, "MaxQueryCost, last_query_cost 超过该值时将给予警告") spaghettiQueryLength := flag.Int("spaghetti-query-length", Config.SpaghettiQueryLength, "SpaghettiQueryLength, SQL最大长度警告,超过该长度会给警告") allowDropIdx := flag.Bool("allow-drop-index", Config.AllowDropIndex, "AllowDropIndex, 允许输出删除重复索引的建议") maxInCount := flag.Int("max-in-count", Config.MaxInCount, "MaxInCount, IN()最大数量") maxIdxBytesPerColumn := flag.Int("max-index-bytes-percolumn", Config.MaxIdxBytesPerColumn, "MaxIdxBytesPerColumn, 索引中单列最大字节数") maxIdxBytes := flag.Int("max-index-bytes", Config.MaxIdxBytes, "MaxIdxBytes, 索引总长度限制") allowCharsets := flag.String("allow-charsets", strings.ToLower(strings.Join(Config.AllowCharsets, ",")), "AllowCharsets") allowCollates := flag.String("allow-collates", strings.ToLower(strings.Join(Config.AllowCollates, ",")), "AllowCollates") allowEngines := flag.String("allow-engines", strings.ToLower(strings.Join(Config.AllowEngines, ",")), "AllowEngines") maxIdxCount := flag.Int("max-index-count", Config.MaxIdxCount, "MaxIdxCount, 单表最大索引个数") maxColCount := flag.Int("max-column-count", Config.MaxColCount, "MaxColCount, 单表允许的最大列数") maxValueCount := flag.Int("max-value-count", Config.MaxValueCount, "MaxValueCount, INSERT/REPLACE 单次批量写入允许的行数") idxPrefix := flag.String("index-prefix", Config.IdxPrefix, "IdxPrefix") ukPrefix := flag.String("unique-key-prefix", Config.UkPrefix, "UkPrefix") maxSubqueryDepth := flag.Int("max-subquery-depth", Config.MaxSubqueryDepth, "MaxSubqueryDepth") maxVarcharLength := flag.Int("max-varchar-length", Config.MaxVarcharLength, "MaxVarcharLength") columnNotAllowType := flag.String("column-not-allow-type", strings.Join(Config.ColumnNotAllowType, ","), "ColumnNotAllowType") // ++++++++++++++EXPLAIN检查项+++++++++++++ explainSQLReportType := flag.String("explain-sql-report-type", strings.ToLower(Config.ExplainSQLReportType), "ExplainSQLReportType [pretty, sample, fingerprint]") explainType := flag.String("explain-type", strings.ToLower(Config.ExplainType), "ExplainType [extended, partitions, traditional]") explainFormat := flag.String("explain-format", strings.ToLower(Config.ExplainFormat), "ExplainFormat [json, traditional]") explainWarnSelectType := flag.String("explain-warn-select-type", strings.Join(Config.ExplainWarnSelectType, ","), "ExplainWarnSelectType, 哪些select_type不建议使用") explainWarnAccessType := flag.String("explain-warn-access-type", strings.Join(Config.ExplainWarnAccessType, ","), "ExplainWarnAccessType, 哪些access type不建议使用") explainMaxKeyLength := flag.Int("explain-max-keys", Config.ExplainMaxKeyLength, "ExplainMaxKeyLength, 最大key_len") explainMinPossibleKeys := flag.Int("explain-min-keys", Config.ExplainMinPossibleKeys, "ExplainMinPossibleKeys, 最小possible_keys警告") explainMaxRows := flag.Int64("explain-max-rows", Config.ExplainMaxRows, "ExplainMaxRows, 最大扫描行数警告") explainWarnExtra := flag.String("explain-warn-extra", strings.Join(Config.ExplainWarnExtra, ","), "ExplainWarnExtra, 哪些extra信息会给警告") explainMaxFiltered := flag.Float64("explain-max-filtered", Config.ExplainMaxFiltered, "ExplainMaxFiltered, filtered大于该配置给出警告") explainWarnScalability := flag.String("explain-warn-scalability", strings.Join(Config.ExplainWarnScalability, ","), "ExplainWarnScalability, 复杂度警告名单, 支持O(n),O(log n),O(1),O(?)") showWarnings := flag.Bool("show-warnings", Config.ShowWarnings, "ShowWarnings") showLastQueryCost := flag.Bool("show-last-query-cost", Config.ShowLastQueryCost, "ShowLastQueryCost") // +++++++++++++++++其他+++++++++++++++++++ printConfig := flag.Bool("print-config", false, "Print configs") checkConfig := flag.Bool("check-config", false, "Check configs") printVersion := flag.Bool("version", false, "Print version info") query := flag.String("query", Config.Query, "待评审的 SQL 或 SQL 文件,如 SQL 中包含特殊字符建议使用文件名。") listHeuristicRules := flag.Bool("list-heuristic-rules", Config.ListHeuristicRules, "ListHeuristicRules, 打印支持的评审规则列表") listRewriteRules := flag.Bool("list-rewrite-rules", Config.ListRewriteRules, "ListRewriteRules, 打印支持的重写规则列表") listTestSQLs := flag.Bool("list-test-sqls", Config.ListTestSqls, "ListTestSqls, 打印测试case用于测试") listReportTypes := flag.Bool("list-report-types", Config.ListReportTypes, "ListReportTypes, 打印支持的报告输出类型") verbose := flag.Bool("verbose", Config.Verbose, "Verbose") dryrun := flag.Bool("dry-run", Config.DryRun, "是否在预演环境执行") maxPrettySQLLength := flag.Int("max-pretty-sql-length", Config.MaxPrettySQLLength, "MaxPrettySQLLength, 超出该长度的SQL会转换成指纹输出") // 一个不存在 log-level,用于更新 usage。 // 因为 vitess 里面也用了 flag,这些 vitess 的参数我们不需要关注 if !Config.Verbose && runtime.GOOS != "windows" { flag.Usage = usage } flag.Parse() Config.OnlineDSN = ParseDSN(*onlineDSN, Config.OnlineDSN) Config.TestDSN = ParseDSN(*testDSN, Config.TestDSN) Config.AllowOnlineAsTest = *allowOnlineAsTest Config.DropTestTemporary = *dropTestTemporary Config.CleanupTestDatabase = *cleanupTestDatabase Config.OnlySyntaxCheck = *onlySyntaxCheck Config.Profiling = *profiling Config.Trace = *trace Config.Explain = *explain Config.Sampling = *sampling Config.SamplingStatisticTarget = *samplingStatisticTarget Config.SamplingCondition = *samplingCondition Config.LogLevel = *logLevel if filepath.IsAbs(*logOutput) || *logOutput == "" { Config.LogOutput = *logOutput } else { Config.LogOutput = filepath.Join(BaseDir, *logOutput) } Config.ReportType = strings.ToLower(*reportType) if Config.ReportType == "tables" && Config.TestDSN.Schema == "information_schema" { Config.TestDSN.Schema = "unknown" } Config.ReportCSS = *reportCSS Config.ReportJavascript = *reportJavascript Config.ReportTitle = *reportTitle Config.MarkdownExtensions = *markdownExtensions Config.MarkdownHTMLFlags = *markdownHTMLFlags Config.IgnoreRules = strings.Split(*ignoreRules, ",") Config.RewriteRules = strings.Split(*rewriteRules, ",") *blackList = strings.TrimSpace(*blackList) Config.MinCardinality = *minCardinality if filepath.IsAbs(*blackList) || *blackList == "" { Config.BlackList = *blackList } else { Config.BlackList = filepath.Join(BaseDir, *blackList) } Config.MaxJoinTableCount = *maxJoinTableCount Config.MaxGroupByColsCount = *maxGroupByColsCount Config.MaxDistinctCount = *maxDistinctCount if *maxIdxColsCount < 16 { Config.MaxIdxColsCount = *maxIdxColsCount } else { Config.MaxIdxColsCount = 16 } Config.MaxTextColsCount = *maxTextColsCount Config.MaxIdxBytesPerColumn = *maxIdxBytesPerColumn Config.MaxIdxBytes = *maxIdxBytes if *allowCharsets != "" { Config.AllowCharsets = strings.Split(strings.ToLower(*allowCharsets), ",") } if *allowCollates != "" { Config.AllowCollates = strings.Split(strings.ToLower(*allowCollates), ",") } if *allowEngines != "" { Config.AllowEngines = strings.Split(strings.ToLower(*allowEngines), ",") } Config.MaxIdxCount = *maxIdxCount Config.MaxColCount = *maxColCount Config.MaxValueCount = *maxValueCount Config.IdxPrefix = *idxPrefix Config.UkPrefix = *ukPrefix Config.MaxSubqueryDepth = *maxSubqueryDepth Config.MaxTotalRows = *maxTotalRows Config.MaxQueryCost = *maxQueryCost Config.AllowDropIndex = *allowDropIdx Config.MaxInCount = *maxInCount Config.SpaghettiQueryLength = *spaghettiQueryLength Config.Query = *query Config.Delimiter = *delimiter Config.ExplainSQLReportType = strings.ToLower(*explainSQLReportType) Config.ExplainType = strings.ToLower(*explainType) Config.ExplainFormat = strings.ToLower(*explainFormat) Config.ExplainWarnSelectType = strings.Split(*explainWarnSelectType, ",") Config.ExplainWarnAccessType = strings.Split(*explainWarnAccessType, ",") Config.ExplainMaxKeyLength = *explainMaxKeyLength Config.ExplainMinPossibleKeys = *explainMinPossibleKeys Config.ExplainMaxRows = *explainMaxRows Config.ExplainWarnExtra = strings.Split(*explainWarnExtra, ",") Config.ExplainMaxFiltered = *explainMaxFiltered Config.ExplainWarnScalability = strings.Split(*explainWarnScalability, ",") Config.ShowWarnings = *showWarnings Config.ShowLastQueryCost = *showLastQueryCost Config.ListHeuristicRules = *listHeuristicRules Config.ListRewriteRules = *listRewriteRules Config.ListTestSqls = *listTestSQLs Config.ListReportTypes = *listReportTypes Config.Verbose = *verbose Config.DryRun = *dryrun Config.MaxPrettySQLLength = *maxPrettySQLLength Config.MaxVarcharLength = *maxVarcharLength if *columnNotAllowType != "" { Config.ColumnNotAllowType = strings.Split(strings.ToLower(*columnNotAllowType), ",") } PrintVersion = *printVersion PrintConfig = *printConfig CheckConfig = *checkConfig hasParsed = true return nil } // ParseConfig 加载配置文件和命令行参数 func ParseConfig(configFile string) error { var err error var configs []string // 指定了配置文件优先读配置文件,未指定配置文件按如下顺序加载,先找到哪个加载哪个 if configFile == "" { configs = []string{ "/etc/soar.yaml", filepath.Join(BaseDir, "etc", "soar.yaml"), filepath.Join(BaseDir, "soar.yaml"), } } else { configs = []string{ configFile, } } for _, config := range configs { if _, err = os.Stat(config); err == nil { err = Config.readConfigFile(config) if err != nil { Log.Error("ParseConfig Config.readConfigFile Error: %v", err) } // LogOutput now is "console", if add Log.Debug here will print into stdout anyway. // Log.Debug("ParseConfig use config file: %s", config) break } } err = readCmdFlags() if err != nil { Log.Error("ParseConfig readCmdFlags Error: %v", err) return err } // parse blacklist & ignore blacklist file parse error if _, e := os.Stat(Config.BlackList); e == nil { var blFd *os.File blFd, err = os.Open(Config.BlackList) if err == nil { bl := bufio.NewReader(blFd) for { rule, e := bl.ReadString('\n') if e != nil { break } rule = strings.TrimSpace(rule) if strings.HasPrefix(rule, "#") || rule == "" { continue } BlackList = append(BlackList, rule) } } defer blFd.Close() } LoggerInit() return err } // ReportType 元数据结构定义 type ReportType struct { Name string `json:"Name"` Description string `json:"Description"` Example string `json:"Example"` } // ReportTypes 命令行-report-type支持的形式 var ReportTypes = []ReportType{ { Name: "lint", Description: "参考sqlint格式,以插件形式集成到代码编辑器,显示输出更加友好", Example: `soar -report-type lint -query test.sql`, }, { Name: "markdown", Description: "该格式为默认输出格式,以markdown格式展现,可以用网页浏览器插件直接打开,也可以用markdown编辑器打开", Example: `echo "select * from film" | soar`, }, { Name: "rewrite", Description: "SQL重写功能,配合-rewrite-rules参数一起使用,可以通过-list-rewrite-rules 查看所有支持的 SQL 重写规则", Example: `echo "select * from film" | soar -rewrite-rules star2columns,delimiter -report-type rewrite`, }, { Name: "ast", Description: "输出 SQL 的抽象语法树,主要用于测试", Example: `echo "select * from film" | soar -report-type ast`, }, { Name: "ast-json", Description: "以 JSON 格式输出 SQL 的抽象语法树,主要用于测试", Example: `echo "select * from film" | soar -report-type ast-json`, }, { Name: "tiast", Description: "输出 SQL 的 TiDB抽象语法树,主要用于测试", Example: `echo "select * from film" | soar -report-type tiast`, }, { Name: "tiast-json", Description: "以 JSON 格式输出 SQL 的 TiDB抽象语法树,主要用于测试", Example: `echo "select * from film" | soar -report-type tiast-json`, }, { Name: "tables", Description: "以 JSON 格式输出 SQL 使用的库表名", Example: `echo "select * from film" | soar -report-type tables`, }, { Name: "query-type", Description: "SQL 语句的请求类型", Example: `echo "select * from film" | soar -report-type query-type`, }, { Name: "fingerprint", Description: "输出SQL的指纹", Example: `echo "select * from film where language_id=1" | soar -report-type fingerprint`, }, { Name: "md2html", Description: "markdown 格式转 html 格式小工具", Example: `soar -list-heuristic-rules | soar -report-type md2html > heuristic_rules.html`, }, { Name: "explain-digest", Description: "输入为EXPLAIN的表格,JSON 或 Vertical格式,对其进行分析,给出分析结果", Example: `soar -report-type explain-digest << EOF +----+-------------+-------+------+---------------+------+---------+------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ | 1 | SIMPLE | film | ALL | NULL | NULL | NULL | NULL | 1131 | | +----+-------------+-------+------+---------------+------+---------+------+------+-------+ EOF`, }, { Name: "duplicate-key-checker", Description: "对 OnlineDsn 中指定的 database 进行索引重复检查", Example: `soar -report-type duplicate-key-checker -online-dsn user:password@127.0.0.1:3306/db`, }, { Name: "html", Description: "以HTML格式输出报表", Example: `echo "select * from film" | soar -report-type html`, }, { Name: "json", Description: "输出JSON格式报表,方便应用程序处理", Example: `echo "select * from film" | soar -report-type json`, }, { Name: "tokenize", Description: "对SQL进行切词,主要用于测试", Example: `echo "select * from film" | soar -report-type tokenize`, }, { Name: "compress", Description: "SQL压缩小工具,使用内置SQL压缩逻辑,测试中的功能", Example: `echo "select * from film" | soar -report-type compress`, }, { Name: "pretty", Description: "使用kr/pretty打印报告,主要用于测试", Example: `echo "select * from film" | soar -report-type pretty`, }, { Name: "remove-comment", Description: "去除SQL语句中的注释,支持单行多行注释的去除", Example: `echo "select/*comment*/ * from film" | soar -report-type remove-comment`, }, { Name: "chardet", Description: "猜测输入的 SQL 使用的字符集", Example: "echo '中文' | soar -report-type chardet", }, } // ListReportTypes 查看所有支持的report-type func ListReportTypes() { switch Config.ReportType { case "json": js, err := json.MarshalIndent(ReportTypes, "", " ") if err == nil { fmt.Println(string(js)) } default: fmt.Print("# 支持的报告类型\n\n[toc]\n\n") for _, r := range ReportTypes { fmt.Print("## ", MarkdownEscape(r.Name), "\n* **Description**:", r.Description+"\n", "\n* **Example**:\n\n```bash\n", r.Example, "\n```\n") } } } // ArgConfig get -config arg value from cli func ArgConfig() string { var configFile string if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-config") { if os.Args[1] == "-config" && len(os.Args) > 2 { if os.Args[2] == "=" && len(os.Args) > 3 { // -config = soar.yaml not support fmt.Println("wrong format, no space between '=', eg: -config=soar.yaml") } else { // -config soar.yaml configFile = os.Args[2] } if strings.HasPrefix(configFile, "=") { // -config =soar.yaml configFile = strings.Split(configFile, "=")[1] } } if strings.Contains(os.Args[1], "=") { // -config=soar.yaml configFile = strings.Split(os.Args[1], "=")[1] } } else { for i, c := range os.Args { if strings.HasPrefix(c, "-config") && i != 1 { fmt.Println("-config must be the first arg") } } } return configFile }