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 }