crow-timeseries
v1.1.0
Published
a very tiny inline CDN
Readme
crow-timeseries
Crow-timeseries is a pea-sized timeseries database for single-instance servers, connecting crow-metrics (or the metrics library of your choice) with yeri to store your server's historical metric data and display them with flexibly-configured SVG graphs.
It uses sqlite3 for storing the timeseries data.
Build
npm install
npm testUse
This sample code creates a new crow Metrics object, attaches it to a timeseries database, and generates an SVG graph of median and 99% response times for web requests over the past day:
import { Metrics } from "crow-metrics";
import { TimeSeriesDatabase } from "crow-timeseries";
// create a new crow metrics instance for collecting and reporting metrics.
// make sure a distribution named `web-response` exists. (we assume timings
// are reported elsewhere in the code.)
const metrics = Metrics.create();
const responseTime = metrics.distribution("web-response");
// attach crow to our timeseries database to collect the minutely reports.
// the database will be created and initialized if it doesn't exist yet.
const db = await TimeSeriesDatabase.build("./data/timeseries.db");
db.attach(metrics);
// generate an SVG graph of median & 99th-percentile web response times over
// the past day.
const svg = await db.generateSvgGraph(
[
"web-response{p=0.5}:name('median'):coalesce(max)",
"web-response{p=0.99}:name('99th percentile'):coalesce(max)",
],
{ units: "sec" },
"day"
);The resulting image may look something like:
API
TimeSeriesDatabase.build(filename: string, public options: { log?: (message: string) => void, init?: boolean }): Promise<TimeSeriesDatabase>
Open a time series database from a sqlite file. If the file doesn't exist, or the init option is set, the initial schema and tables will be created too. If a log function is passed in, it's used to log when a snapshot is ingested.
db.close()
Close the database file (for a clean shutdown).
db.attach(metrics: Metrics)
Attach to this crow Metrics object, receive the minutely snapshots, and automatically ingest them into the timeseries database. This just forwards the snapshot object to add().
db.add(snapshot: Snapshot)
Ingest the (delta) metric values from a crow snapshot.
db.addRaw(timestamp: number, metrics: RawMetric[])
If you don't use crow, call this method periodically to ingest new metric values. Each RawMetric has these fields:
name: stringtype: "counter" | "gauge" | "distribution"value: number
Check the crow documentation for an explanation of the counter, gauge, and distribution types.
db.getMetricNames(): Name[]
Return a list of all the metric names that have appeared in the database. Each Name has these fields:
key: numbername: stringtype: string
The key is unique to the name and is just used to optimize database storage. (Each name is only stored once in the database. All metrics are saved as tuples of timestamp, key, and value.) The name and type are the same as used in addRaw().
db.getMetric(name: string, start: number, end: number): TimeSeries
Fetch time series data for the named metric between start (inclusive) and end (inclusive). The start and end times are given in epoch seconds, not javascript milliseconds. TimeSeries is a class from yeri that includes a name, a timestamps array (in seconds), and a values array.
db.exportOldMetrics(before: number, otherDb: TimeSeriesDatabase, options: { deleteAfter?: boolean } = {}): number
Collect all metrics with a timestamp before before (in epoch seconds) and write them into another database file. Optionally, after copying the metrics over, delete them from this database. This can be useful if you want to keep one primary database for "current" metrics small and fast, and another one as an archive. Returns the number of rows deleted, if deleteAfter was true.
db.exportOldMetricsCsv(before: number, filename: string, options: { deleteAfter?: boolean } = {}): Promise<number>
Same as exportOldMetrics, but writes the old metrics into a CSV (text) file.
db.generateSvgGraph(metrics: GraphPlan[] | string[], options: GraphOptions, timeSpan: TimeSpan, now?: number): Promise<string>
Render one or more time series into an SVG image using yeri.
Each metric will receive its own line (and color), and is described using a string expression. The expression syntax is shown below. You may optionally pre-compile the expressions into GraphPlans.
Options has these fields:
timezone?: stringTimezone to use for the x (time) axis labels. Uses "local time" if left blank.
coalesceInterval?: numberNumber of seconds to coalesce data points (default 60).
lineWidth?: numberSet the stroke width of lines in the graph (default: 7).
dotWidth?: numberSet the width of a dot in the graph, when highlighting a point in a tooltip (default: 15).
units?: stringUnits to use on the y axis for values (optional))
binary?: booleanIf true, the units become milli/kilo/mega (with prefix letters) using powers of 1024 instead of 1000. This is used for computer units like bytes (of memory or disk).
Time span is one of:
"hour"- an hour ago until now, coalesced to each minute (61 points per graph)"day"- 24 hours ago until now, coalesced every 5 minutes (289 points per graph)"7day"- 7 days ago until now, coalesced every 30 minutes (337 points per graph)"30day"- 30 days ago until now, coalesced every 2 hours (361 points per graph)- or, an object with fields for setting a specific time span:
startTime: number- an epoch time in seconds, inclusiveendTime: number- an epoch time in seconds, inclusiveinterval: number- number of seconds to coalesce points across
Coalescing data points combines every value within an interval into a single value. For example, if you store metrics every 15 seconds, then a coalesce interval of 60 will combine every set of 4 data values into 1. The coalescing strategy can be set on each metric as an option in its expression, and defaults to choosing the first point.
Unless an explicit start and end time are given, the time span ends at now, which will be the current time (in seconds) unless overridden.
generateGraphData(metrics: GraphPlan[] | string[], options: GraphOptions, timeSpan: TimeSpan, now?: number): Promise<GraphData>
Follow the same procedure as generateSvgGraph, but instead of rendering a graph, return all the processed data that would have been used to generate the graph. GraphData has these fields:
timeSeriesList: TimeSeries[]Values fetched from the database, with a
TimeSeriesfor each of the metrics inmetrics.TimeSeriesis a class from yeri that includes aname, atimestampsarray (in seconds), and avaluesarray.graphConfig: Partial<SvgGraphConfig>Configuration for drawing the graph (colors, line widths, and so on) that can be passed directly to yeri's SVG generating functions.
startTime: numberendTime: numberinterval: numberThe start, end, and coalesce intervals (in seconds) computed from
timeSpan.
Metrics graphing expressions
A metrics expression is either a single named metric (like job-queue-size) or a function of multiple named metrics that will be combined into a single time series. It may be followed by options (separated by ":") for setting things like the color, fill, or coalesce strategy.
For those who like formal descriptions:
metric-expr := expr (":" option)*
expr := metric-name | function-name ("(" (expr ","?)* ")")?
metric-name := /^[A-Za-z][A-Za-z0-9\{\}\=\._-]*$/
function-name := "sum" | "max" | "min" | "count" | "diff"
option := "fill" |
"name(" string ")" |
"color(" color ")" |
"coalesce(" coalesce-strategy ")" |
"interval(" number ")" |
"scale(" number ")"
coalesce-strategy := "min" | "max" | "average" | "median" | "first" | "last" | "sum"
color := "#" /[0-9A-Fa-f]{3,6}/ | color-name | string
string := /'[^']*'/For example, if you had two gauges named workers-active and workers-busy, you could draw a red line showing the maximum busy workers across time as:
workers-busy:coalesce(max):color(red)And you could compute the idle workers by subtracting busy workers from the total active workers:
diff(workers-active, workers-busy):coalesce(min)Each function takes 2 or more expressions as arguments, separated by commas. The arguments may be also be function calls.
The functions, options, and coalesce strategies are described below.
Functions
Since the arguments to functions are themselves time series (either individual metrics or other functions), the function is applied at each point in time, across the values of the arguments, to generate a new time series. For example, if time series A is (10, 11, 12) and time series B is (20, 30, 40), then the "sum" function across them will return the new time series (30, 41, 52).
sum- add all of the nested expressions togethermax- use only the maximum value of the nested expressions at each timemin- use only the minimum value of the nested expressions at each timecount- generate a time series of the count of nested expressions that had a defined value at each timediff- subtract the 2nd and all following expressions from the 1st expression
Options
fill- Fill in the area under the line when graphing.name(...)- Use this name in the legend of the graph. Defaults to the expression string.color(...)- Set the color of the line & fill. This can be a hex code (starting with "#"), a common color name like "blue", or a string in quotes ('string here') which will be inserted directly into the SVG.coalesce- Set the strategy to use when combining multiple values into one point. (See below for a description of the coalesce strategies.)interval- Override the time interval between points (in seconds). Any values within the same interval will be coalesced.scale- Multiply the value by this constant before graphing. This can be useful if the metric is stored is milliseconds, but you'd like the graph axis to be labeled in seconds (sec, msec, usec, Ksec, ...).
Coalesce strategies
min- minimum of the values (10, 20, 36 -> 10)max- maximum of the values (10, 20, 36 -> 36)average- average of the values (10, 20, 36 -> 22)median- median of the values (10, 20, 36 -> 20)first- the value with the earliest timestamplast- the value with the latest timestampsum- sum of the values (10, 20, 36 -> 66)
To-do
- API call to truncate the database to wipe data older than a certain timestamp
- API call to export older data as a new sqlite3 database or csv
- allow simple constant math (like "/ 1000") on metrics in expressions?
License
This code is licensed under either the prosperity license (LICENSE-prosperity.md) or the anti-capitalist license (LICENSE-anti-capitalist.txt) at your preference, for personal or non-commercial use.
Authors
- https://messydesk.social/@robey - Robey Pointer <[email protected]>
