github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/explain_bundle.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package sql
    12  
    13  import (
    14  	"archive/zip"
    15  	"bytes"
    16  	"context"
    17  	"fmt"
    18  	"io"
    19  	"strings"
    20  
    21  	"github.com/cockroachdb/cockroach/pkg/kv"
    22  	"github.com/cockroachdb/cockroach/pkg/settings"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    27  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    28  	"github.com/cockroachdb/cockroach/pkg/util/tracing"
    29  	"github.com/cockroachdb/errors"
    30  	"github.com/gogo/protobuf/jsonpb"
    31  )
    32  
    33  // setExplainBundleResult creates the diagnostics and returns the bundle
    34  // information for an EXPLAIN ANALYZE (DEBUG) statement.
    35  //
    36  // Returns an error if information rows couldn't be added to the result.
    37  func setExplainBundleResult(
    38  	ctx context.Context,
    39  	res RestrictedCommandResult,
    40  	ast tree.Statement,
    41  	trace tracing.Recording,
    42  	plan *planTop,
    43  	ie *InternalExecutor,
    44  	execCfg *ExecutorConfig,
    45  ) error {
    46  	res.ResetStmtType(&tree.ExplainAnalyzeDebug{})
    47  	res.SetColumns(ctx, sqlbase.ExplainAnalyzeDebugColumns)
    48  
    49  	var text []string
    50  	func() {
    51  		bundle, err := buildStatementBundle(ctx, execCfg.DB, ie, plan, trace)
    52  		if err != nil {
    53  			// TODO(radu): we cannot simply set an error on the result here without
    54  			// changing the executor logic (e.g. an implicit transaction could have
    55  			// committed already). Just show the error in the result.
    56  			text = []string{fmt.Sprintf("Error generating bundle: %v", err)}
    57  			return
    58  		}
    59  
    60  		fingerprint := tree.AsStringWithFlags(ast, tree.FmtHideConstants)
    61  		stmtStr := tree.AsString(ast)
    62  
    63  		diagID, err := execCfg.StmtDiagnosticsRecorder.InsertStatementDiagnostics(
    64  			ctx,
    65  			fingerprint,
    66  			stmtStr,
    67  			bundle.trace,
    68  			bundle.zip,
    69  		)
    70  		if err != nil {
    71  			text = []string{fmt.Sprintf("Error recording bundle: %v", err)}
    72  			return
    73  		}
    74  
    75  		text = []string{
    76  			"Statement diagnostics bundle generated. Download from the Admin UI (Advanced",
    77  			"Debug -> Statement Diagnostics History) or use the direct link below.",
    78  			fmt.Sprintf("Admin UI: %s", execCfg.AdminURL()),
    79  			fmt.Sprintf("Direct link: %s/_admin/v1/stmtbundle/%d", execCfg.AdminURL(), diagID),
    80  		}
    81  	}()
    82  
    83  	if err := res.Err(); err != nil {
    84  		// Add the bundle information as a detail to the query error.
    85  		//
    86  		// TODO(radu): if the statement gets auto-retried, we will generate a
    87  		// bundle for each iteration. If the statement eventually succeeds we
    88  		// will have a link to the last iteration's bundle. It's not clear what
    89  		// the ideal behavior is here; if we keep all bundles we should try to
    90  		// list them all in the final message.
    91  		res.SetError(errors.WithDetail(err, strings.Join(text, "\n")))
    92  		return nil
    93  	}
    94  
    95  	for _, line := range text {
    96  		if err := res.AddRow(ctx, tree.Datums{tree.NewDString(line)}); err != nil {
    97  			return err
    98  		}
    99  	}
   100  	return nil
   101  }
   102  
   103  // traceToJSON converts a trace to a JSON datum suitable for the
   104  // system.statement_diagnostics.trace column. In case of error, the returned
   105  // datum is DNull. Also returns the string representation of the trace.
   106  //
   107  // traceToJSON assumes that the first span in the recording contains all the
   108  // other spans.
   109  func traceToJSON(trace tracing.Recording) (tree.Datum, string, error) {
   110  	root := normalizeSpan(trace[0], trace)
   111  	marshaller := jsonpb.Marshaler{
   112  		Indent: "\t",
   113  	}
   114  	str, err := marshaller.MarshalToString(&root)
   115  	if err != nil {
   116  		return tree.DNull, "", err
   117  	}
   118  	d, err := tree.ParseDJSON(str)
   119  	if err != nil {
   120  		return tree.DNull, "", err
   121  	}
   122  	return d, str, nil
   123  }
   124  
   125  func normalizeSpan(s tracing.RecordedSpan, trace tracing.Recording) tracing.NormalizedSpan {
   126  	var n tracing.NormalizedSpan
   127  	n.Operation = s.Operation
   128  	n.StartTime = s.StartTime
   129  	n.Duration = s.Duration
   130  	n.Tags = s.Tags
   131  	n.Logs = s.Logs
   132  
   133  	for _, ss := range trace {
   134  		if ss.ParentSpanID != s.SpanID {
   135  			continue
   136  		}
   137  		n.Children = append(n.Children, normalizeSpan(ss, trace))
   138  	}
   139  	return n
   140  }
   141  
   142  // diagnosticsBundle contains diagnostics information collected for a statement.
   143  type diagnosticsBundle struct {
   144  	zip   []byte
   145  	trace tree.Datum
   146  }
   147  
   148  // buildStatementBundle collects metadata related the planning and execution of
   149  // the statement. It generates a bundle for storage in
   150  // system.statement_diagnostics.
   151  func buildStatementBundle(
   152  	ctx context.Context, db *kv.DB, ie *InternalExecutor, plan *planTop, trace tracing.Recording,
   153  ) (diagnosticsBundle, error) {
   154  	if plan == nil {
   155  		return diagnosticsBundle{}, errors.AssertionFailedf("execution terminated early")
   156  	}
   157  	b := makeStmtBundleBuilder(db, ie, plan, trace)
   158  
   159  	b.addStatement()
   160  	b.addOptPlans()
   161  	b.addExecPlan()
   162  	b.addDistSQLDiagrams()
   163  	traceJSON := b.addTrace()
   164  	b.addEnv(ctx)
   165  
   166  	buf, err := b.finalize()
   167  	if err != nil {
   168  		return diagnosticsBundle{}, err
   169  	}
   170  	return diagnosticsBundle{trace: traceJSON, zip: buf.Bytes()}, nil
   171  }
   172  
   173  // stmtBundleBuilder is a helper for building a statement bundle.
   174  type stmtBundleBuilder struct {
   175  	db *kv.DB
   176  	ie *InternalExecutor
   177  
   178  	plan  *planTop
   179  	trace tracing.Recording
   180  
   181  	z memZipper
   182  }
   183  
   184  func makeStmtBundleBuilder(
   185  	db *kv.DB, ie *InternalExecutor, plan *planTop, trace tracing.Recording,
   186  ) stmtBundleBuilder {
   187  	b := stmtBundleBuilder{db: db, ie: ie, plan: plan, trace: trace}
   188  	b.z.Init()
   189  	return b
   190  }
   191  
   192  // addStatement adds the pretty-printed statement as file statement.txt.
   193  func (b *stmtBundleBuilder) addStatement() {
   194  	cfg := tree.DefaultPrettyCfg()
   195  	cfg.UseTabs = false
   196  	cfg.LineWidth = 100
   197  	cfg.TabWidth = 2
   198  	cfg.Simplify = true
   199  	cfg.Align = tree.PrettyNoAlign
   200  	cfg.JSONFmt = true
   201  
   202  	b.z.AddFile("statement.txt", cfg.Pretty(b.plan.stmt.AST))
   203  }
   204  
   205  // addOptPlans adds the EXPLAIN (OPT) variants as files opt.txt, opt-v.txt,
   206  // opt-vv.txt.
   207  func (b *stmtBundleBuilder) addOptPlans() {
   208  	if b.plan.mem == nil {
   209  		// No optimizer plans; an error must have occurred during planning.
   210  		return
   211  	}
   212  
   213  	b.z.AddFile("opt.txt", b.plan.formatOptPlan(memo.ExprFmtHideAll))
   214  	b.z.AddFile("opt-v.txt", b.plan.formatOptPlan(
   215  		memo.ExprFmtHideQualifications|memo.ExprFmtHideScalars|memo.ExprFmtHideTypes,
   216  	))
   217  	b.z.AddFile("opt-vv.txt", b.plan.formatOptPlan(memo.ExprFmtHideQualifications))
   218  }
   219  
   220  // addExecPlan adds the EXPLAIN (VERBOSE) plan as file plan.txt.
   221  func (b *stmtBundleBuilder) addExecPlan() {
   222  	if plan := b.plan.instrumentation.planString; plan != "" {
   223  		b.z.AddFile("plan.txt", plan)
   224  	}
   225  }
   226  
   227  func (b *stmtBundleBuilder) addDistSQLDiagrams() {
   228  	for i, d := range b.plan.distSQLDiagrams {
   229  		d.AddSpans(b.trace)
   230  		_, url, err := d.ToURL()
   231  
   232  		var contents string
   233  		if err != nil {
   234  			contents = err.Error()
   235  		} else {
   236  			contents = fmt.Sprintf(`<meta http-equiv="Refresh" content="0; url=%s">`, url.String())
   237  		}
   238  
   239  		var filename string
   240  		if len(b.plan.distSQLDiagrams) == 1 {
   241  			filename = "distsql.html"
   242  		} else {
   243  			// TODO(radu): it would be great if we could distinguish between
   244  			// subqueries/main query/postqueries here.
   245  			filename = fmt.Sprintf("distsql-%d.html", i+1)
   246  		}
   247  		b.z.AddFile(filename, contents)
   248  	}
   249  }
   250  
   251  // addTrace adds two files to the bundle: one is a json representation of the
   252  // trace, the other one is a human-readable representation.
   253  func (b *stmtBundleBuilder) addTrace() tree.Datum {
   254  	traceJSON, traceJSONStr, err := traceToJSON(b.trace)
   255  	if err != nil {
   256  		b.z.AddFile("trace.json", err.Error())
   257  	} else {
   258  		b.z.AddFile("trace.json", traceJSONStr)
   259  	}
   260  
   261  	cfg := tree.DefaultPrettyCfg()
   262  	cfg.UseTabs = false
   263  	cfg.LineWidth = 100
   264  	cfg.TabWidth = 2
   265  	cfg.Simplify = true
   266  	cfg.Align = tree.PrettyNoAlign
   267  	cfg.JSONFmt = true
   268  	stmt := cfg.Pretty(b.plan.stmt.AST)
   269  
   270  	// The JSON is not very human-readable, so we include another format too.
   271  	b.z.AddFile("trace.txt", fmt.Sprintf("%s\n\n\n\n%s", stmt, b.trace.String()))
   272  
   273  	// Note that we're going to include the non-anonymized statement in the trace.
   274  	// But then again, nothing in the trace is anonymized.
   275  	jaegerJSON, err := b.trace.ToJaegerJSON(stmt)
   276  	if err != nil {
   277  		b.z.AddFile("trace-jaeger.txt", err.Error())
   278  	} else {
   279  		b.z.AddFile("trace-jaeger.json", jaegerJSON)
   280  	}
   281  
   282  	return traceJSON
   283  }
   284  
   285  func (b *stmtBundleBuilder) addEnv(ctx context.Context) {
   286  	c := makeStmtEnvCollector(ctx, b.ie)
   287  
   288  	var buf bytes.Buffer
   289  	if err := c.PrintVersion(&buf); err != nil {
   290  		fmt.Fprintf(&buf, "-- error getting version: %v\n", err)
   291  	}
   292  	fmt.Fprintf(&buf, "\n")
   293  
   294  	// Show the values of any non-default session variables that can impact
   295  	// planning decisions.
   296  	if err := c.PrintSettings(&buf); err != nil {
   297  		fmt.Fprintf(&buf, "-- error getting settings: %v\n", err)
   298  	}
   299  	b.z.AddFile("env.sql", buf.String())
   300  
   301  	mem := b.plan.mem
   302  	if mem == nil {
   303  		// No optimizer plans; an error must have occurred during planning.
   304  		return
   305  	}
   306  	buf.Reset()
   307  
   308  	var tables, sequences, views []tree.TableName
   309  	err := b.db.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
   310  		var err error
   311  		tables, sequences, views, err = mem.Metadata().AllDataSourceNames(
   312  			func(ds cat.DataSource) (cat.DataSourceName, error) {
   313  				return b.plan.catalog.fullyQualifiedNameWithTxn(ctx, ds, txn)
   314  			},
   315  		)
   316  		return err
   317  	})
   318  	if err != nil {
   319  		b.z.AddFile("schema.sql", fmt.Sprintf("-- error getting data source names: %v\n", err))
   320  		return
   321  	}
   322  
   323  	if len(tables) == 0 && len(sequences) == 0 && len(views) == 0 {
   324  		return
   325  	}
   326  
   327  	first := true
   328  	blankLine := func() {
   329  		if !first {
   330  			buf.WriteByte('\n')
   331  		}
   332  		first = false
   333  	}
   334  	for i := range sequences {
   335  		blankLine()
   336  		if err := c.PrintCreateSequence(&buf, &sequences[i]); err != nil {
   337  			fmt.Fprintf(&buf, "-- error getting schema for sequence %s: %v\n", sequences[i].String(), err)
   338  		}
   339  	}
   340  	for i := range tables {
   341  		blankLine()
   342  		if err := c.PrintCreateTable(&buf, &tables[i]); err != nil {
   343  			fmt.Fprintf(&buf, "-- error getting schema for table %s: %v\n", tables[i].String(), err)
   344  		}
   345  	}
   346  	for i := range views {
   347  		blankLine()
   348  		if err := c.PrintCreateView(&buf, &views[i]); err != nil {
   349  			fmt.Fprintf(&buf, "-- error getting schema for view %s: %v\n", views[i].String(), err)
   350  		}
   351  	}
   352  	b.z.AddFile("schema.sql", buf.String())
   353  	for i := range tables {
   354  		buf.Reset()
   355  		if err := c.PrintTableStats(&buf, &tables[i], false /* hideHistograms */); err != nil {
   356  			fmt.Fprintf(&buf, "-- error getting statistics for table %s: %v\n", tables[i].String(), err)
   357  		}
   358  		b.z.AddFile(fmt.Sprintf("stats-%s.sql", tables[i].String()), buf.String())
   359  	}
   360  }
   361  
   362  // finalize generates the zipped bundle and returns it as a buffer.
   363  func (b *stmtBundleBuilder) finalize() (*bytes.Buffer, error) {
   364  	return b.z.Finalize()
   365  }
   366  
   367  // memZipper builds a zip file into an in-memory buffer.
   368  type memZipper struct {
   369  	buf *bytes.Buffer
   370  	z   *zip.Writer
   371  	err error
   372  }
   373  
   374  func (z *memZipper) Init() {
   375  	z.buf = &bytes.Buffer{}
   376  	z.z = zip.NewWriter(z.buf)
   377  }
   378  
   379  func (z *memZipper) AddFile(name string, contents string) {
   380  	if z.err != nil {
   381  		return
   382  	}
   383  	w, err := z.z.CreateHeader(&zip.FileHeader{
   384  		Name:     name,
   385  		Method:   zip.Deflate,
   386  		Modified: timeutil.Now(),
   387  	})
   388  	if err != nil {
   389  		z.err = err
   390  		return
   391  	}
   392  	_, z.err = w.Write([]byte(contents))
   393  }
   394  
   395  func (z *memZipper) Finalize() (*bytes.Buffer, error) {
   396  	if z.err != nil {
   397  		return nil, z.err
   398  	}
   399  	if err := z.z.Close(); err != nil {
   400  		return nil, err
   401  	}
   402  	buf := z.buf
   403  	*z = memZipper{}
   404  	return buf, nil
   405  }
   406  
   407  // stmtEnvCollector helps with gathering information about the "environment" in
   408  // which a statement was planned or run: version, relevant session settings,
   409  // schema, table statistics.
   410  type stmtEnvCollector struct {
   411  	ctx context.Context
   412  	ie  *InternalExecutor
   413  }
   414  
   415  func makeStmtEnvCollector(ctx context.Context, ie *InternalExecutor) stmtEnvCollector {
   416  	return stmtEnvCollector{ctx: ctx, ie: ie}
   417  }
   418  
   419  // environmentQuery is a helper to run a query that returns a single string
   420  // value.
   421  func (c *stmtEnvCollector) query(query string) (string, error) {
   422  	var row tree.Datums
   423  	row, err := c.ie.QueryRowEx(
   424  		c.ctx,
   425  		"stmtEnvCollector",
   426  		nil, /* txn */
   427  		sqlbase.NoSessionDataOverride,
   428  		query,
   429  	)
   430  	if err != nil {
   431  		return "", err
   432  	}
   433  
   434  	if len(row) != 1 {
   435  		return "", errors.AssertionFailedf(
   436  			"expected env query %q to return a single column, returned %d",
   437  			query, len(row),
   438  		)
   439  	}
   440  
   441  	s, ok := row[0].(*tree.DString)
   442  	if !ok {
   443  		return "", errors.AssertionFailedf(
   444  			"expected env query %q to return a DString, returned %T",
   445  			query, row[0],
   446  		)
   447  	}
   448  
   449  	return string(*s), nil
   450  }
   451  
   452  var testingOverrideExplainEnvVersion string
   453  
   454  // TestingOverrideExplainEnvVersion overrides the version reported by
   455  // EXPLAIN (OPT, ENV). Used for testing.
   456  func TestingOverrideExplainEnvVersion(ver string) func() {
   457  	prev := testingOverrideExplainEnvVersion
   458  	testingOverrideExplainEnvVersion = ver
   459  	return func() { testingOverrideExplainEnvVersion = prev }
   460  }
   461  
   462  // PrintVersion appends a row of the form:
   463  //  -- Version: CockroachDB CCL v20.1.0 ...
   464  func (c *stmtEnvCollector) PrintVersion(w io.Writer) error {
   465  	version, err := c.query("SELECT version()")
   466  	if err != nil {
   467  		return err
   468  	}
   469  	if testingOverrideExplainEnvVersion != "" {
   470  		version = testingOverrideExplainEnvVersion
   471  	}
   472  	fmt.Fprintf(w, "-- Version: %s\n", version)
   473  	return err
   474  }
   475  
   476  // PrintSettings appends information about session settings that can impact
   477  // planning decisions.
   478  func (c *stmtEnvCollector) PrintSettings(w io.Writer) error {
   479  	relevantSettings := []struct {
   480  		sessionSetting string
   481  		clusterSetting settings.WritableSetting
   482  	}{
   483  		{sessionSetting: "reorder_joins_limit", clusterSetting: ReorderJoinsLimitClusterValue},
   484  		{sessionSetting: "enable_zigzag_join", clusterSetting: zigzagJoinClusterMode},
   485  		{sessionSetting: "optimizer_foreign_keys", clusterSetting: optDrivenFKChecksClusterMode},
   486  		{sessionSetting: "experimental_optimizer_foreign_key_cascades", clusterSetting: optDrivenFKCascadesClusterMode},
   487  		{sessionSetting: "optimizer_use_histograms", clusterSetting: optUseHistogramsClusterMode},
   488  		{sessionSetting: "optimizer_use_multicol_stats", clusterSetting: optUseMultiColStatsClusterMode},
   489  		// TODO(mgartner): remove this once partial indexes are fully supported.
   490  		{sessionSetting: "experimental_partial_indexes", clusterSetting: partialIndexClusterMode},
   491  	}
   492  
   493  	for _, s := range relevantSettings {
   494  		value, err := c.query(fmt.Sprintf("SHOW %s", s.sessionSetting))
   495  		if err != nil {
   496  			return err
   497  		}
   498  		// Get the default value for the cluster setting.
   499  		def := s.clusterSetting.EncodedDefault()
   500  		// Convert true/false to on/off to match what SHOW returns.
   501  		switch def {
   502  		case "true":
   503  			def = "on"
   504  		case "false":
   505  			def = "off"
   506  		}
   507  
   508  		if value == def {
   509  			fmt.Fprintf(w, "-- %s has the default value: %s\n", s.sessionSetting, value)
   510  		} else {
   511  			fmt.Fprintf(w, "SET %s = %s;  -- default value: %s\n", s.sessionSetting, value, def)
   512  		}
   513  	}
   514  	return nil
   515  }
   516  
   517  func (c *stmtEnvCollector) PrintCreateTable(w io.Writer, tn *tree.TableName) error {
   518  	createStatement, err := c.query(
   519  		fmt.Sprintf("SELECT create_statement FROM [SHOW CREATE TABLE %s]", tn.String()),
   520  	)
   521  	if err != nil {
   522  		return err
   523  	}
   524  	fmt.Fprintf(w, "%s;\n", createStatement)
   525  	return nil
   526  }
   527  
   528  func (c *stmtEnvCollector) PrintCreateSequence(w io.Writer, tn *tree.TableName) error {
   529  	createStatement, err := c.query(fmt.Sprintf(
   530  		"SELECT create_statement FROM [SHOW CREATE SEQUENCE %s]", tn.String(),
   531  	))
   532  	if err != nil {
   533  		return err
   534  	}
   535  	fmt.Fprintf(w, "%s;\n", createStatement)
   536  	return nil
   537  }
   538  
   539  func (c *stmtEnvCollector) PrintCreateView(w io.Writer, tn *tree.TableName) error {
   540  	createStatement, err := c.query(fmt.Sprintf(
   541  		"SELECT create_statement FROM [SHOW CREATE VIEW %s]", tn.String(),
   542  	))
   543  	if err != nil {
   544  		return err
   545  	}
   546  	fmt.Fprintf(w, "%s;\n", createStatement)
   547  	return nil
   548  }
   549  
   550  func (c *stmtEnvCollector) PrintTableStats(
   551  	w io.Writer, tn *tree.TableName, hideHistograms bool,
   552  ) error {
   553  	var maybeRemoveHistoBuckets string
   554  	if hideHistograms {
   555  		maybeRemoveHistoBuckets = " - 'histo_buckets'"
   556  	}
   557  
   558  	stats, err := c.query(fmt.Sprintf(
   559  		`SELECT jsonb_pretty(COALESCE(json_agg(stat), '[]'))
   560  		 FROM (
   561  			 SELECT json_array_elements(statistics)%s AS stat
   562  			 FROM [SHOW STATISTICS USING JSON FOR TABLE %s]
   563  		 )`,
   564  		maybeRemoveHistoBuckets, tn.String(),
   565  	))
   566  	if err != nil {
   567  		return err
   568  	}
   569  
   570  	stats = strings.Replace(stats, "'", "''", -1)
   571  	fmt.Fprintf(w, "ALTER TABLE %s INJECT STATISTICS '%s';\n", tn.String(), stats)
   572  	return nil
   573  }