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 }