github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/telemetry_test.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_test
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"fmt"
    17  	"regexp"
    18  	"sort"
    19  	"strings"
    20  	"testing"
    21  	"text/tabwriter"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/base"
    24  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    25  	"github.com/cockroachdb/cockroach/pkg/server"
    26  	"github.com/cockroachdb/cockroach/pkg/server/diagnosticspb"
    27  	"github.com/cockroachdb/cockroach/pkg/sql"
    28  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    29  	"github.com/cockroachdb/cockroach/pkg/testutils/diagutils"
    30  	"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
    31  	"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
    32  	"github.com/cockroachdb/cockroach/pkg/util/cloudinfo"
    33  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    34  	"github.com/cockroachdb/cockroach/pkg/util/treeprinter"
    35  	"github.com/cockroachdb/datadriven"
    36  	"github.com/cockroachdb/errors"
    37  )
    38  
    39  // TestTelemetry runs the datadriven telemetry tests. The test sets up a
    40  // database and a testing diagnostics reporting server. The test implements the
    41  // following data-driven commands:
    42  //
    43  //  - exec
    44  //
    45  //    Executes SQL statements against the database. Outputs no results on
    46  //    success. In case of error, outputs the error message.
    47  //
    48  //  - feature-whitelist
    49  //
    50  //    The input for this command is not SQL, but a list of regular expressions.
    51  //    Tests that follow (until the next feature-whitelist command) will only
    52  //    output counters that match a regexp in this white list.
    53  //
    54  //  - feature-usage, feature-counters
    55  //
    56  //    Executes SQL statements and then outputs the feature counters from the
    57  //    white list that have been reported to the diagnostic server. The first
    58  //    variant outputs only the names of the counters that changed; the second
    59  //    variant outputs the counts as well. It is necessary to use
    60  //    feature-whitelist before these commands to avoid test flakes (e.g. because
    61  //    of counters that are changed by looking up descriptors)
    62  //
    63  //  - schema
    64  //
    65  //    Outputs reported schema information.
    66  //
    67  //  - sql-stats
    68  //
    69  //    Executes SQL statements and then outputs information about reported sql
    70  //    statement statistics.
    71  //
    72  func TestTelemetry(t *testing.T) {
    73  	defer leaktest.AfterTest(t)()
    74  
    75  	ctx := context.Background()
    76  	// Note: these tests cannot be run in parallel (with each other or with other
    77  	// tests) because telemetry counters are global.
    78  	datadriven.Walk(t, "testdata/telemetry", func(t *testing.T, path string) {
    79  		// Disable cloud info reporting (it would make these tests really slow).
    80  		defer cloudinfo.Disable()()
    81  
    82  		diagSrv := diagutils.NewServer()
    83  		defer diagSrv.Close()
    84  
    85  		diagSrvURL := diagSrv.URL()
    86  		params := base.TestServerArgs{
    87  			Knobs: base.TestingKnobs{
    88  				Server: &server.TestingKnobs{
    89  					DiagnosticsTestingKnobs: diagnosticspb.TestingKnobs{
    90  						OverrideReportingURL: &diagSrvURL,
    91  					},
    92  				},
    93  			},
    94  		}
    95  		s, sqlConn, _ := serverutils.StartServer(t, params)
    96  		defer s.Stopper().Stop(ctx)
    97  
    98  		runner := sqlutils.MakeSQLRunner(sqlConn)
    99  		// Disable automatic reporting so it doesn't interfere with the test.
   100  		runner.Exec(t, "SET CLUSTER SETTING diagnostics.reporting.enabled = false")
   101  		runner.Exec(t, "SET CLUSTER SETTING diagnostics.reporting.send_crash_reports = false")
   102  		// Disable plan caching to get accurate counts if the same statement is
   103  		// issued multiple times.
   104  		runner.Exec(t, "SET CLUSTER SETTING sql.query_cache.enabled = false")
   105  
   106  		var whitelist featureWhitelist
   107  		datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string {
   108  			switch td.Cmd {
   109  			case "exec":
   110  				_, err := sqlConn.Exec(td.Input)
   111  				if err != nil {
   112  					if errors.HasAssertionFailure(err) {
   113  						td.Fatalf(t, "%+v", err)
   114  					}
   115  					return fmt.Sprintf("error: %v\n", err)
   116  				}
   117  				return ""
   118  
   119  			case "schema":
   120  				s.ReportDiagnostics(ctx)
   121  				last := diagSrv.LastRequestData()
   122  				var buf bytes.Buffer
   123  				for i := range last.Schema {
   124  					buf.WriteString(formatTableDescriptor(&last.Schema[i]))
   125  				}
   126  				return buf.String()
   127  
   128  			case "feature-whitelist":
   129  				var err error
   130  				whitelist, err = makeWhitelist(strings.Split(td.Input, "\n"))
   131  				if err != nil {
   132  					td.Fatalf(t, "error parsing feature regex: %s", err)
   133  				}
   134  				return ""
   135  
   136  			case "feature-usage", "feature-counters":
   137  				// Report diagnostics once to reset the counters.
   138  				s.ReportDiagnostics(ctx)
   139  				_, err := sqlConn.Exec(td.Input)
   140  				var buf bytes.Buffer
   141  				if err != nil {
   142  					fmt.Fprintf(&buf, "error: %v\n", err)
   143  				}
   144  				s.ReportDiagnostics(ctx)
   145  				last := diagSrv.LastRequestData()
   146  				usage := last.FeatureUsage
   147  				keys := make([]string, 0, len(usage))
   148  				for k, v := range usage {
   149  					if v == 0 {
   150  						// Ignore zero values (shouldn't happen in practice)
   151  						continue
   152  					}
   153  					if !whitelist.Match(k) {
   154  						// Feature key not in whitelist.
   155  						continue
   156  					}
   157  					keys = append(keys, k)
   158  				}
   159  				sort.Strings(keys)
   160  				tw := tabwriter.NewWriter(&buf, 2, 1, 2, ' ', 0)
   161  				for _, k := range keys {
   162  					// Report either just the key or the key and the count.
   163  					if td.Cmd == "feature-counters" {
   164  						fmt.Fprintf(tw, "%s\t%d\n", k, usage[k])
   165  					} else {
   166  						fmt.Fprintf(tw, "%s\n", k)
   167  					}
   168  				}
   169  				_ = tw.Flush()
   170  				return buf.String()
   171  
   172  			case "sql-stats":
   173  				// Report diagnostics once to reset the stats.
   174  				s.SQLServer().(*sql.Server).ResetSQLStats(ctx)
   175  				s.ReportDiagnostics(ctx)
   176  
   177  				_, err := sqlConn.Exec(td.Input)
   178  				var buf bytes.Buffer
   179  				if err != nil {
   180  					fmt.Fprintf(&buf, "error: %v\n", err)
   181  				}
   182  				s.SQLServer().(*sql.Server).ResetSQLStats(ctx)
   183  				s.ReportDiagnostics(ctx)
   184  				last := diagSrv.LastRequestData()
   185  				buf.WriteString(formatSQLStats(last.SqlStats))
   186  				return buf.String()
   187  
   188  			default:
   189  				td.Fatalf(t, "unknown command %s", td.Cmd)
   190  				return ""
   191  			}
   192  		})
   193  	})
   194  }
   195  
   196  type featureWhitelist []*regexp.Regexp
   197  
   198  func makeWhitelist(strings []string) (featureWhitelist, error) {
   199  	w := make(featureWhitelist, len(strings))
   200  	for i := range strings {
   201  		var err error
   202  		w[i], err = regexp.Compile("^" + strings[i] + "$")
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  	}
   207  	return w, nil
   208  }
   209  
   210  func (w featureWhitelist) Match(feature string) bool {
   211  	if w == nil {
   212  		// Unset whitelist matches all counters.
   213  		return true
   214  	}
   215  	for _, r := range w {
   216  		if r.MatchString(feature) {
   217  			return true
   218  		}
   219  	}
   220  	return false
   221  }
   222  
   223  func formatTableDescriptor(desc *sqlbase.TableDescriptor) string {
   224  	tp := treeprinter.New()
   225  	n := tp.Childf("table:%s", desc.Name)
   226  	cols := n.Child("columns")
   227  	for _, col := range desc.Columns {
   228  		var colBuf bytes.Buffer
   229  		fmt.Fprintf(&colBuf, "%s:%s", col.Name, col.Type.String())
   230  		if col.DefaultExpr != nil {
   231  			fmt.Fprintf(&colBuf, " default: %s", *col.DefaultExpr)
   232  		}
   233  		if col.ComputeExpr != nil {
   234  			fmt.Fprintf(&colBuf, " computed: %s", *col.ComputeExpr)
   235  		}
   236  		cols.Child(colBuf.String())
   237  	}
   238  	if len(desc.Checks) > 0 {
   239  		checks := n.Child("checks")
   240  		for _, chk := range desc.Checks {
   241  			checks.Childf("%s: %s", chk.Name, chk.Expr)
   242  		}
   243  	}
   244  	return tp.String()
   245  }
   246  
   247  func formatSQLStats(stats []roachpb.CollectedStatementStatistics) string {
   248  	bucketByApp := make(map[string][]roachpb.CollectedStatementStatistics)
   249  	for i := range stats {
   250  		s := &stats[i]
   251  
   252  		if strings.HasPrefix(s.Key.App, sqlbase.InternalAppNamePrefix) {
   253  			// Let's ignore all internal queries for this test.
   254  			continue
   255  		}
   256  		bucketByApp[s.Key.App] = append(bucketByApp[s.Key.App], *s)
   257  	}
   258  	var apps []string
   259  	for app, s := range bucketByApp {
   260  		apps = append(apps, app)
   261  		sort.Slice(s, func(i, j int) bool {
   262  			return s[i].Key.Query < s[j].Key.Query
   263  		})
   264  		bucketByApp[app] = s
   265  	}
   266  	sort.Strings(apps)
   267  	tp := treeprinter.New()
   268  	n := tp.Child("sql-stats")
   269  
   270  	for _, app := range apps {
   271  		nodeApp := n.Child(app)
   272  		for _, s := range bucketByApp[app] {
   273  			var flags []string
   274  			if s.Key.Failed {
   275  				flags = append(flags, "failed")
   276  			}
   277  			if !s.Key.DistSQL {
   278  				flags = append(flags, "nodist")
   279  			}
   280  			var buf bytes.Buffer
   281  			if len(flags) > 0 {
   282  				buf.WriteString("[")
   283  				for i := range flags {
   284  					if i > 0 {
   285  						buf.WriteByte(',')
   286  					}
   287  					buf.WriteString(flags[i])
   288  				}
   289  				buf.WriteString("] ")
   290  			}
   291  			buf.WriteString(s.Key.Query)
   292  			nodeApp.Child(buf.String())
   293  		}
   294  	}
   295  	return tp.String()
   296  }