SENDLINE open source
open source / metrics4s / dashboards

Dashboards

metrics4s-grafana builds the dashboard JSON from the definitions:

scala
val dashboard = Dashboard("processor", "Line / processor", "line", ServiceFilter.regex("([a-z]+-)?processor.*"))(
  Stats(Stat.podsUp, Stat.rate(ProcessorMetrics.processed), Stat.p95(ProcessorMetrics.processTime)),
  Row("Processing")(
    Panel.rate(ProcessorMetrics.processed).byAll,
    Panel.latency(ProcessorMetrics.processTime),
    Panel.gauge(ProcessorMetrics.cacheSize, Pods.Max),
  ),
  Rows.catsEffect,
  Rows.jvm,
)

dashboard.render     // Either[GrafanaError, String]
dashboard.writeTo(Paths.get("metrics"))
  • The generator applies the guide's rules itself:
    • no __inputs, and a datasource variable;
    • namespace, service and pod variables read from up, and every query filtered by all three;
    • $__rate_interval everywhere, and sum by (le) in every quantile;
    • the unit taken from the definition;
    • a fixed uid, unique ids, and a 24-wide grid with nothing overlapping;
    • the group tag and links. A custom query that breaks one of these rules fails render.
  • .by(_.outcome) groups by a label field and does not compile for a field that does not exist. .byAll groups by every label, and .byLabel("outcome") takes the name as a string.
  • .where(_.outcome =!= Outcome.Ok) keeps the series whose label matches. The values are rendered through the label's own LabelValue, so a renamed case cannot silently match nothing. ===, =!=, .in(a, b) and .notIn(a, b) compare one label field with values of its type, and anything else does not compile. Stats take the same filters: Stat.rate(processed).where(_.source === Source.Kafka) is the rate of one source.
  • Panel.quantile(histogram, 0.95) draws one quantile, by any labels, where Panel.latency draws p50, p95 and p99. Stat.quantile(histogram, 0.95) is the stat, and Stat.p95 its shorthand.
  • Panel.share(counter)(_.result === CacheResult.Hit) and Stat.share(counter)(...) draw the share of the counter's rate that matches, in percentunit.
  • .withThresholds(Color.Green, 0.01 -> Color.Orange, 0.05 -> Color.Red) colours a stat by its value: green below 1%, orange from 1%, red from 5%. The values must be finite and increasing, or render fails. A stat is green otherwise, and Stat.podsUp is red below one pod.
  • dashboard.missingSeries(definitions) lists the series the dashboard's own panels and stats read that none of the definitions serves, such as a panel left behind by a renamed metric. One line in a test gives every service the check. The shared rows are left out: the library checks them against its own definitions.
  • Rows.catsEffect, Rows.zio, Rows.kyo, Rows.jvm, Rows.httpServer, Rows.httpClient and Rows.mongo are the shared rows. The cats-effect and JVM rows query exactly what the hand-written merger and descriptions dashboards query.
  • Keep the rendered JSON in the repository and compare it in a test, so a metric changed without its dashboard fails the build.
  • MetricsDoc.markdown(definitions*) writes the Metric | Type | Labels | What table for metrics/README.md, with the label values listed.

The integration tests import a generated dashboard into Grafana 12. Every query of every panel then has to return data through Grafana's Prometheus datasource, scraped by Prometheus 3.5 from a running service.

built from README.md at v0.2.0 edit this page on GitHub