github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ts/catalog/catalog_generator.go (about)

     1  // Copyright 2019 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 catalog
    12  
    13  import (
    14  	"regexp"
    15  	"strings"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/ts/tspb"
    18  	"github.com/cockroachdb/cockroach/pkg/util/metric"
    19  	"github.com/cockroachdb/errors"
    20  	prometheusgo "github.com/prometheus/client_model/go"
    21  )
    22  
    23  // catalog_generator.go generates a protobuf describing a set of pre-defined
    24  // Admin UI charts that can aid users in debugging a CockroachDB cluster. This
    25  // file generates the catalog at <Admin UI host>/_admin/v1/chartcatalog. The
    26  // source describing the catalog to be generated is located in pkg/ts/catalog/chart_catalog.go.
    27  // The page that leverages this structure will be added in a subsequent PR.
    28  
    29  // The protobuf, viewed as a catalog, is organized into a hierarchy:
    30  // 1. Top level: This level represents the "layers" of CockroachDB's architecture
    31  //		(for reference https://www.cockroachlabs.com/docs/stable/architecture/overview.html),
    32  // 2. Section: A grouping of similar charts or subsections.
    33  // 3. Subsections: The most granular level of organization in the hierarchy.
    34  
    35  // Each section and subsection can contains individual charts; users will be able
    36  // to view all charts in a section or subsection.
    37  
    38  // These consts represent the top levels of the hierarchy:
    39  const (
    40  	Process            = `Process`
    41  	SQLLayer           = `SQL Layer`
    42  	KVTransactionLayer = `KV Transaction Layer`
    43  	DistributionLayer  = `Distribution Layer`
    44  	ReplicationLayer   = `Replication Layer`
    45  	StorageLayer       = `Storage Layer`
    46  	Timeseries         = `Timeseries`
    47  )
    48  
    49  // sectionDescription describes either a section or subsection of the chart.
    50  // During processing, these are converted in ChartSections (pkg/ts/catalog/chart_catalog.proto).
    51  type sectionDescription struct {
    52  	// Organization identifies where in the hierarchy to insert these charts.
    53  	// The inner array describes where to insert these charts, and the outer
    54  	// array lets you use the same charts in multiple places in the hierarchy
    55  	// without needing to redefine them.
    56  	Organization [][]string
    57  	// Charts describes the specifics of the charts you want to add to the
    58  	// section. At render time, users can choose to view individual charts
    59  	// or all charts at a given level of the hierarchy/organization.
    60  	Charts []chartDescription
    61  }
    62  
    63  // chartDescription describes an individual chart.
    64  // Only Title, Organization, and Metrics must be set; other values have useful
    65  // defaults based on the types of metrics.
    66  // During processing, these are converted in IndividualCharts (pkg/ts/catalog/chart_catalog.proto).
    67  type chartDescription struct {
    68  	// Title of the chart.
    69  	Title string
    70  	// Metrics to include in the chart using their util.metric.Metadata.name;
    71  	// these values are used to generate ChartMetrics.
    72  	// NOTE: All Metrics in a chart must be of the same prometheusgo.MetricType.
    73  	Metrics []string
    74  	// Units in which the chart is viewed, e.g. BYTES for storage.
    75  	// Does not need to be set if all Metrics have the same Unit value.
    76  	Units AxisUnits
    77  	// Axis label for the chart's y-axis.
    78  	// Does not need to be set if all Metrics have the same Measurement value.
    79  	AxisLabel string
    80  	// The downsampler function the chart uses.
    81  	Downsampler DescribeAggregator
    82  	// The aggregator function for the chart's downsampled values.
    83  	Aggregator DescribeAggregator
    84  	// The derivative function the chart uses (e.g. NONE).
    85  	Rate DescribeDerivative
    86  	// Whether or not the chart should be converted into percentiles.
    87  	// True only for Histograms. Unsupported by other metric types.
    88  	Percentiles bool
    89  }
    90  
    91  // chartDefault provides default values to simplify adding charts to the catalog.
    92  type chartDefault struct {
    93  	Downsampler DescribeAggregator
    94  	Aggregator  DescribeAggregator
    95  	Rate        DescribeDerivative
    96  	Percentiles bool
    97  }
    98  
    99  // chartDefaultsPerMetricType defines default values for the chart's
   100  // Downsampler, Aggregator, Rate, and Percentiles based on the metric's type.
   101  var chartDefaultsPerMetricType = map[prometheusgo.MetricType]chartDefault{
   102  	prometheusgo.MetricType_COUNTER: {
   103  		Downsampler: DescribeAggregator_AVG,
   104  		Aggregator:  DescribeAggregator_SUM,
   105  		Rate:        DescribeDerivative_NON_NEGATIVE_DERIVATIVE,
   106  		Percentiles: false,
   107  	},
   108  	prometheusgo.MetricType_GAUGE: {
   109  		Downsampler: DescribeAggregator_AVG,
   110  		Aggregator:  DescribeAggregator_AVG,
   111  		Rate:        DescribeDerivative_NONE,
   112  		Percentiles: false,
   113  	},
   114  	prometheusgo.MetricType_HISTOGRAM: {
   115  		Downsampler: DescribeAggregator_AVG,
   116  		Aggregator:  DescribeAggregator_AVG,
   117  		Rate:        DescribeDerivative_NONE,
   118  		Percentiles: true,
   119  	},
   120  }
   121  
   122  // chartCatalog represents the entire chart catalog, which is an array of
   123  // ChartSections, to which the individual charts and subsections defined above
   124  // are added.
   125  var chartCatalog = []ChartSection{
   126  	{
   127  		Title:           Process,
   128  		LongTitle:       Process,
   129  		CollectionTitle: "process-all",
   130  		Description: `These charts detail the overall performance of the 
   131  		<code>cockroach</code> process running on this server.`,
   132  		Level: 0,
   133  	},
   134  	{
   135  		Title:           SQLLayer,
   136  		LongTitle:       SQLLayer,
   137  		CollectionTitle: "sql-layer-all",
   138  		Description: `In the SQL layer, nodes receive commands and then parse, plan, 
   139  		and execute them. <br/><br/><a class="catalog-link" 
   140  		href="https://www.cockroachlabs.com/docs/stable/architecture/sql-layer.html">
   141  		SQL Layer Architecture Docs >></a>"`,
   142  		Level: 0,
   143  	},
   144  	{
   145  		Title:           KVTransactionLayer,
   146  		LongTitle:       KVTransactionLayer,
   147  		CollectionTitle: "kv-transaction-layer-all",
   148  		Description: `The KV Transaction Layer coordinates concurrent requests as 
   149  		key-value operations. To maintain consistency, this is also where the cluster 
   150  		manages time. <br/><br/><a class="catalog-link" 
   151  		href="https://www.cockroachlabs.com/docs/stable/architecture/transaction-layer.html">
   152  		Transaction Layer Architecture Docs >></a>`,
   153  		Level: 0,
   154  	},
   155  	{
   156  		Title:           DistributionLayer,
   157  		LongTitle:       DistributionLayer,
   158  		CollectionTitle: "distribution-layer-all",
   159  		Description: `The Distribution Layer provides a unified view of your cluster’s data, 
   160  		which are actually broken up into many key-value ranges. <br/><br/><a class="catalog-link" 
   161  		href="https://www.cockroachlabs.com/docs/stable/architecture/distribution-layer.html"> 
   162  		Distribution Layer Architecture Docs >></a>`,
   163  		Level: 0,
   164  	},
   165  	{
   166  		Title:           ReplicationLayer,
   167  		LongTitle:       ReplicationLayer,
   168  		CollectionTitle: "replication-layer-all",
   169  		Description: `The Replication Layer maintains consistency between copies of ranges 
   170  		(known as replicas) through our consensus algorithm, Raft. <br/><br/><a class="catalog-link" 
   171  			href="https://www.cockroachlabs.com/docs/stable/architecture/replication-layer.html"> 
   172  			Replication Layer Architecture Docs >></a>`,
   173  		Level: 0,
   174  	},
   175  	{
   176  		Title:           StorageLayer,
   177  		LongTitle:       StorageLayer,
   178  		CollectionTitle: "replication-layer-all",
   179  		Description: `The Storage Layer reads and writes data to disk, as well as manages 
   180  		garbage collection. <br/><br/><a class="catalog-link" 
   181  		href="https://www.cockroachlabs.com/docs/stable/architecture/storage-layer.html">
   182  		Storage Layer Architecture Docs >></a>`,
   183  		Level: 0,
   184  	},
   185  	{
   186  		Title:           Timeseries,
   187  		LongTitle:       Timeseries,
   188  		CollectionTitle: "timeseries-all",
   189  		Description: `Your cluster collects data about its own performance, which is used to 
   190  		power the very charts you\'re using, among other things.`,
   191  		Level: 0,
   192  	},
   193  }
   194  
   195  var catalogGenerated = false
   196  
   197  // catalogKey provides an index to simplify ordering ChartSections, as well as
   198  // limiting the search space every chart uses when being added to the catalog.
   199  var catalogKey = map[string]int{
   200  	Process:            0,
   201  	SQLLayer:           1,
   202  	KVTransactionLayer: 2,
   203  	DistributionLayer:  3,
   204  	ReplicationLayer:   4,
   205  	StorageLayer:       5,
   206  	Timeseries:         6,
   207  }
   208  
   209  // unitsKey converts between metric.Unit and catalog.AxisUnits which is
   210  // necessary because charts only support a subset of unit types.
   211  var unitsKey = map[metric.Unit]AxisUnits{
   212  	metric.Unit_BYTES:         AxisUnits_BYTES,
   213  	metric.Unit_CONST:         AxisUnits_COUNT,
   214  	metric.Unit_COUNT:         AxisUnits_COUNT,
   215  	metric.Unit_NANOSECONDS:   AxisUnits_DURATION,
   216  	metric.Unit_PERCENT:       AxisUnits_COUNT,
   217  	metric.Unit_SECONDS:       AxisUnits_DURATION,
   218  	metric.Unit_TIMESTAMP_NS:  AxisUnits_DURATION,
   219  	metric.Unit_TIMESTAMP_SEC: AxisUnits_DURATION,
   220  }
   221  
   222  // aggKey converts between catalog.DescribeAggregator to
   223  // tspb.TimeSeriesQueryAggregator which is necessary because
   224  // tspb.TimeSeriesQueryAggregator doesn't have a checkable zero value, which the
   225  // catalog requires to support defaults (DescribeAggregator_UNSET_AGG).
   226  var aggKey = map[DescribeAggregator]tspb.TimeSeriesQueryAggregator{
   227  	DescribeAggregator_AVG: tspb.TimeSeriesQueryAggregator_AVG,
   228  	DescribeAggregator_MAX: tspb.TimeSeriesQueryAggregator_MAX,
   229  	DescribeAggregator_MIN: tspb.TimeSeriesQueryAggregator_MIN,
   230  	DescribeAggregator_SUM: tspb.TimeSeriesQueryAggregator_SUM,
   231  }
   232  
   233  // derKey converts between catalog.DescribeDerivative to
   234  // tspb.TimeSeriesQueryDerivative which is necessary because
   235  // tspb.TimeSeriesQueryDerivative doesn't have a checkable zero value, which the
   236  // catalog requires to support defaults (DescribeDerivative_UNSET_DER).
   237  var derKey = map[DescribeDerivative]tspb.TimeSeriesQueryDerivative{
   238  	DescribeDerivative_DERIVATIVE:              tspb.TimeSeriesQueryDerivative_DERIVATIVE,
   239  	DescribeDerivative_NONE:                    tspb.TimeSeriesQueryDerivative_NONE,
   240  	DescribeDerivative_NON_NEGATIVE_DERIVATIVE: tspb.TimeSeriesQueryDerivative_NON_NEGATIVE_DERIVATIVE,
   241  }
   242  
   243  // GenerateCatalog creates an array of ChartSections, which is served at
   244  // /_admin/v1/chartcatalog.
   245  func GenerateCatalog(metadata map[string]metric.Metadata) ([]ChartSection, error) {
   246  
   247  	if !catalogGenerated {
   248  		for _, sd := range charts {
   249  
   250  			if err := createIndividualCharts(metadata, sd); err != nil {
   251  				return nil, err
   252  			}
   253  		}
   254  		catalogGenerated = true
   255  	}
   256  
   257  	return chartCatalog, nil
   258  }
   259  
   260  // createIndividualChart creates IndividualCharts, and ultimately places them
   261  // in the appropriate place in chartCatalog based on the hierarchy described
   262  // in sd.Organization.
   263  func createIndividualCharts(metadata map[string]metric.Metadata, sd sectionDescription) error {
   264  
   265  	var ics []*IndividualChart
   266  
   267  	for _, cd := range sd.Charts {
   268  
   269  		ic := new(IndividualChart)
   270  
   271  		if err := ic.addMetrics(cd, metadata); err != nil {
   272  			return err
   273  		}
   274  
   275  		// If ic has no Metrics, skip. Note that this isn't necessarily an error
   276  		// e.g. nodes without SSLs do not have certificate expiration timestamps,
   277  		// so those charts should not be added to the catalog.
   278  		if len(ic.Metrics) == 0 {
   279  			continue
   280  		}
   281  
   282  		if err := ic.addDisplayProperties(cd); err != nil {
   283  			return err
   284  		}
   285  
   286  		ic.Title = cd.Title
   287  
   288  		ics = append(ics, ic)
   289  	}
   290  
   291  	for _, org := range sd.Organization {
   292  		// Ensure the organization has both Level 0 and Level 1 organization.
   293  		if len(org) < 2 {
   294  			return errors.Errorf(`Sections must have at least Level 0 and 
   295  				Level 1 organization, but only have %v in %v`, org, sd)
   296  		} else if len(org) > 3 {
   297  			return errors.Errorf(`Sections cannot be more than 3 levels deep,
   298  				but %v has %d`, sd, len(org))
   299  		}
   300  
   301  		for _, ic := range ics {
   302  			ic.addNames(org)
   303  		}
   304  
   305  		// Make sure the organization's top level is valid.
   306  		topLevelCatalogIndex, ok := catalogKey[org[0]]
   307  
   308  		if !ok {
   309  			return errors.Errorf(`Undefined Level 0 organization; you must 
   310  			use a const defined in pkg/ts/catalog/catalog_generator.go for %v`, sd)
   311  		}
   312  
   313  		chartCatalog[topLevelCatalogIndex].addChartAndSubsections(org, ics)
   314  	}
   315  
   316  	return nil
   317  }
   318  
   319  // addMetrics sets the IndividualChart's Metric values by looking up the
   320  // chartDescription metrics in the metadata map.
   321  func (ic *IndividualChart) addMetrics(
   322  	cd chartDescription, metadata map[string]metric.Metadata,
   323  ) error {
   324  	for _, x := range cd.Metrics {
   325  
   326  		md, ok := metadata[x]
   327  
   328  		// If metric is missing from metadata, don't add it to this chart e.g.
   329  		// insecure nodes do not metadata related to SSL certificates, so those metrics
   330  		// should not be added to any charts.
   331  		if !ok {
   332  			continue
   333  		}
   334  
   335  		unit, ok := unitsKey[md.Unit]
   336  
   337  		if !ok {
   338  			return errors.Errorf(
   339  				"%s's metric.Metadata has an unrecognized Unit, %v", md.Name, md.Unit,
   340  			)
   341  		}
   342  
   343  		ic.Metrics = append(ic.Metrics, ChartMetric{
   344  			Name:           md.Name,
   345  			Help:           md.Help,
   346  			AxisLabel:      md.Measurement,
   347  			PreferredUnits: unit,
   348  			MetricType:     md.MetricType,
   349  		})
   350  
   351  		if ic.Metrics[0].MetricType != md.MetricType {
   352  			return errors.Errorf(`%s and %s have different MetricTypes, but are being 
   353  			added to the same chart, %v`, ic.Metrics[0].Name, md.Name, ic)
   354  		}
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  // addNames sets the IndividualChart's Title, Longname, and CollectionName.
   361  func (ic *IndividualChart) addNames(organization []string) {
   362  
   363  	// Find string delimeters that are not dashes, including spaces, slashes, and
   364  	// commas.
   365  	nondashDelimeters := regexp.MustCompile("( )|/|,")
   366  
   367  	// Longnames look like "SQL Layer | SQL | Connections".
   368  	// CollectionNames look like "sql-layer-sql-connections".
   369  	for _, n := range organization {
   370  		ic.LongTitle += n + string(" | ")
   371  		ic.CollectionTitle += nondashDelimeters.ReplaceAllString(strings.ToLower(n), "-") + "-"
   372  	}
   373  
   374  	ic.LongTitle += ic.Title
   375  	ic.CollectionTitle += nondashDelimeters.ReplaceAllString(strings.ToLower(ic.Title), "-")
   376  
   377  }
   378  
   379  // addDisplayProperties sets the IndividualChart's display properties, such as
   380  // its Downsampler and Aggregator.
   381  func (ic *IndividualChart) addDisplayProperties(cd chartDescription) error {
   382  	// All metrics in a chart must have the same MetricType, so each
   383  	// IndividualChart has only one potential set of default values.
   384  	defaults := chartDefaultsPerMetricType[ic.Metrics[0].MetricType]
   385  
   386  	// Create copy of cd to avoid argument mutation.
   387  	cdFull := cd
   388  
   389  	// Set all zero values to the chartDefault's value
   390  	if cdFull.Downsampler == DescribeAggregator_UNSET_AGG {
   391  		cdFull.Downsampler = defaults.Downsampler
   392  	}
   393  	if cdFull.Aggregator == DescribeAggregator_UNSET_AGG {
   394  		cdFull.Aggregator = defaults.Aggregator
   395  	}
   396  	if cdFull.Rate == DescribeDerivative_UNSET_DER {
   397  		cdFull.Rate = defaults.Rate
   398  	}
   399  	if !cdFull.Percentiles {
   400  		cdFull.Percentiles = defaults.Percentiles
   401  	}
   402  
   403  	// Set unspecified AxisUnits to the first metric's value.
   404  	if cdFull.Units == AxisUnits_UNSET_UNITS {
   405  
   406  		pu := ic.Metrics[0].PreferredUnits
   407  		for _, m := range ic.Metrics {
   408  			if m.PreferredUnits != pu {
   409  				return errors.Errorf(`Chart %s has metrics with different preferred 
   410  				units; need to specify Units in its chartDescription: %v`, cd.Title, ic)
   411  			}
   412  		}
   413  
   414  		cdFull.Units = pu
   415  	}
   416  
   417  	// Set unspecified AxisLabels to the first metric's value.
   418  	if cdFull.AxisLabel == "" {
   419  		al := ic.Metrics[0].AxisLabel
   420  
   421  		for _, m := range ic.Metrics {
   422  			if m.AxisLabel != al {
   423  				return errors.Errorf(`Chart %s has metrics with different axis labels; 
   424  				need to specify an AxisLabel in its chartDescription: %v`, cd.Title, ic)
   425  			}
   426  		}
   427  
   428  		cdFull.AxisLabel = al
   429  	}
   430  
   431  	// Populate the IndividualChart values.
   432  	ds, ok := aggKey[cdFull.Downsampler]
   433  	if !ok {
   434  		return errors.Errorf(
   435  			"%s's chartDescription has an unrecognized Downsampler, %v", cdFull.Title, cdFull.Downsampler,
   436  		)
   437  	}
   438  	ic.Downsampler = &ds
   439  
   440  	agg, ok := aggKey[cdFull.Aggregator]
   441  	if !ok {
   442  		return errors.Errorf(
   443  			"%s's chartDescription has an unrecognized Aggregator, %v", cdFull.Title, cdFull.Aggregator,
   444  		)
   445  	}
   446  	ic.Aggregator = &agg
   447  
   448  	der, ok := derKey[cdFull.Rate]
   449  	if !ok {
   450  		return errors.Errorf(
   451  			"%s's chartDescription has an unrecognized Rate, %v", cdFull.Title, cdFull.Rate,
   452  		)
   453  	}
   454  	ic.Derivative = &der
   455  
   456  	ic.Percentiles = cdFull.Percentiles
   457  	ic.Units = cdFull.Units
   458  	ic.AxisLabel = cdFull.AxisLabel
   459  
   460  	return nil
   461  }
   462  
   463  // addChartAndSubsections adds subsections identified in the organization to the calling
   464  // ChartSection until it reaches the last level of organization, where it adds the chart
   465  // to the last subsection.
   466  func (cs *ChartSection) addChartAndSubsections(organization []string, ics []*IndividualChart) {
   467  
   468  	// subsection is either an existing or new element of cs.Subsections that will either contain
   469  	// more Subsections or the IndividualChart we want to add.
   470  	var subsection *ChartSection
   471  
   472  	// subsectionLevel identifies the level of the organization slice we're using; it will always
   473  	// be one greater than its parent's level.
   474  	subsectionLevel := int(cs.Level + 1)
   475  
   476  	// To identify how to treat subsection, we need to search for a cs.Subsections element with
   477  	// the same name as the current organization index.
   478  
   479  	for _, ss := range cs.Subsections {
   480  		if ss.Title == organization[subsectionLevel] {
   481  			subsection = ss
   482  			break
   483  		}
   484  	}
   485  
   486  	// If not found, create a new ChartSection and append it as a subsection.
   487  	if subsection == nil {
   488  
   489  		// Find string delimeters that are not dashes, including spaces, slashes, and
   490  		// commas.
   491  		nondashDelimeters := regexp.MustCompile("( )|/|,")
   492  
   493  		subsection = &ChartSection{
   494  			Title: organization[subsectionLevel],
   495  			// Longnames look like "SQL Layer | SQL".
   496  			LongTitle: "All",
   497  			// CollectionNames look like "sql-layer-sql".
   498  			CollectionTitle: nondashDelimeters.ReplaceAllString(strings.ToLower(organization[0]), "-"),
   499  			Level:           int32(subsectionLevel),
   500  		}
   501  
   502  		// Complete Longname and Colectionname values.
   503  		for i := 1; i <= subsectionLevel; i++ {
   504  			subsection.LongTitle += " " + organization[i]
   505  			subsection.CollectionTitle += "-" + nondashDelimeters.ReplaceAllString(strings.ToLower(organization[i]), "-")
   506  		}
   507  
   508  		cs.Subsections = append(cs.Subsections, subsection)
   509  	}
   510  
   511  	// If this is the last level of the organization, add the IndividualChart here. Otheriwse, recurse.
   512  	if subsectionLevel == (len(organization) - 1) {
   513  		subsection.Charts = append(subsection.Charts, ics...)
   514  	} else {
   515  		subsection.addChartAndSubsections(organization, ics)
   516  	}
   517  }