Serving metrics is one of the main tasks done by the GraphQL API server. Metrics are also used internally for computing alerts and for computing other metrics and checks.
Over time, the drawbacks of having a separate Elixir function and GraphQL query
for every metric, each having different field names (price_usd, nvt,
transaction_volume, etc.) made it so adding new metrics was slow and repetitive
process. Another important drawback was that adding new metrics could be done
only by a backend developer who knew the project. The remanants of this approach
can still be seen in the etherbi_resolver.ex
, clickhouse_resolver.ex
and
their corresponding type/queries modules.
Apart from the Elixir part, the database tables evolved in a similar direction as well. Now all metrics are stored in a handful of tables with similar schema, so fetching the data can be done by a single SQL query (with a few dynamic parts)
Apart from a few metrics that have still not been migrated, all metrics are served by the Sanbase.Metric module and the get_metric GraphQL query.
The Sanbase.Metric
module does not do the actual metric fetching but only
dispatches to the proper metric adapter module or aggregates data from the
metric adapter modules.
The true metric and metric metdata fetching is done by the so-called
MetricAdapter
modules. A few examples for such modules are the Clickhouse
MetricAdapter, Github
MetricAdapter or Price
MetricAdapter.
The Clickhouse MetricAdapter is the biggest, most complex with the most SQL queries written. It also serves the biggest amount of metrics.
At time of writing this, there 601 metrics served by the
Sanbase.Metric
module and 551 of those are coming from the Clickhouse MetricAdapter.
Adding new metrics does not require modifying neither the metric module nor the resolver. Every MetricAdapter is responsible for a different source of metric. This can be a different database, internal service API or just a table with a different format than any of the others.
When adding a new metric, the first thing to decide is whether the metric is
timeseries or histogram. Timeseries metrics are all metrics that are
represented as {datetime, number}
pairs. Histogram metrics do not have a
specific format - they can vary from {string, number}
pairs, {string, string, number}
pairs, {[datetime, datetime], string}
pairs, etc.
The first step is to add a JSON map describing the metric in one of the JSON
files in the ../lib/sanbase/clickhouse/metric/metric_files
directory. If there
is no proper JSON file do define the new metric, a new JSON file can be created
and injected in the file
handler file by adding a new
line like:
@external_resource Path.join(__DIR__, "metric_files/new_metrics_file.json")
If the metric added is a timeseries metric then defining the JSON map is all that needs to be done apart from adding the metric to the tests.
A JSON map describing a metric looks like this:
{
"human_readable_name": "Age Consumed",
"name": "age_consumed",
"metric": "stack_age_consumed_5min",
"version": "2019-01-01",
"access": "restricted",
"selectors": ["slug"],
"min_plan": {
"SANAPI": "free",
"SANBASE": "free"
},
"aggregation": "sum",
"min_interval": "5m",
"table": "intraday_metrics",
"has_incomplete_data": false,
"data_type": "timeseries"
}
human_readable_name
- Shows how the metric will be displayed in places where a human readable name is needed like the payload of an alertname
- The name with which the metric will be exposed in theSanbase.Metric
module and public API.metric
- The name of the metric used internally in the database. This should correspond to a row in themetric_metadata
Clickhouse table.version
- Theversion
of the metric stored in themetric_metadata
Clickhouse tableaccess
-free
orrestricted
. Ifrestricted
is chosen, the access will be restricted based on the subscription plan the user has.selectors
- How to uniquely identify the metric in the database. Metrics like price are uniquely identified by the project slug. Other metrics might have labels and/or other columns that form the unique key.min_plan
- Requires two fields -SANAPI
andSANBASE
. It choose the minimal subscription plan for which the metric is accessible. If instead offree
, the value ispro
the metric will be visible only to users withpro
or higher subscription plan.aggregation
- The default aggregation which is used if many data points are used to determine a single value. For example ifprice_usd
metric is fetched with a daily interval, theaggregation
will choose whether to show the average/min/max/highest/lowest/first/last/etc. price for that day.min_interval
- What is the granularity of the data in the database. There are mainly daily metrics with 1 value per day available and intraday metrics with 1 value per 5 minutes/1 hour/etc.table
- The table in which the metric is storedhas_incomplete_data
- Mostly used for daily metrics. Shows whether the last data point stored is computed by not having all the necessary data and can change. For example fetching thedaily_active_addresses
for today cannot include the full day data before the next day comes. The value ofdaily_active_addresses
for today will include only data for 1 hour, 2 hours, 3 hours, and so until the day finishes.data_type
-"timeseries"
or"histogram"
Adding a new histogram metric is more complicated. A few steps must be done:
- Add a function header matching the new metric name in
Sanbase.Clickhouse.MetricAdapter.HistogramMetric
- Add an SQL query for this new metric and use it in this new function
- If the result format does not match any of the existing formats in the metric
types (in the
:value_list
union), a new type must be added as well as extending theresolve_type
function argument.
The metric must be added in the proper list (free or restricted) in the
MetricAccessLevelTest
test. Furthermore, if needed, separate tests for this
metric can be added.