diff options
Diffstat (limited to 'src/web/api')
94 files changed, 27787 insertions, 0 deletions
diff --git a/src/web/api/README.md b/src/web/api/README.md new file mode 100644 index 000000000..7ad1a7ad4 --- /dev/null +++ b/src/web/api/README.md @@ -0,0 +1,12 @@ +# API + +## Netdata agent REST API + +The complete documentation of the Netdata agent's REST API is documented in the OpenAPI format [in our GitHub repository](https://raw.githubusercontent.com/netdata/netdata/master/src/web/api/netdata-swagger.yaml). + +You can explore it using the **[Swagger UI](https://learn.netdata.cloud/api)**, or the **[Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/netdata/netdata/master/src/web/api/netdata-swagger.yaml)**. + +## Netdata cloud API + +A very basic Netdata cloud REST API supports the [Grafana data source plugin](https://github.com/netdata/netdata-grafana-datasource-plugin/blob/master/README.md), +but has not yet been expanded for wider use. We intend to provide a properly documented API in the future. diff --git a/src/web/api/badges/README.md b/src/web/api/badges/README.md new file mode 100644 index 000000000..458aab791 --- /dev/null +++ b/src/web/api/badges/README.md @@ -0,0 +1,369 @@ +<!-- +title: "Netdata badges" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/badges/README.md +sidebar_label: "Netdata badges" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api" +--> + +# Netdata badges + +**Badges are cool!** + +Netdata can generate badges for any chart and any dimension at any time-frame. Badges come in `SVG` and can be added to any web page using an `<IMG>` HTML tag. + +**Netdata badges are powerful**! + +Given that Netdata collects from **1.000** to **5.000** metrics per server (depending on the number of network interfaces, disks, cpu cores, applications running, users logged in, containers running, etc) and that Netdata already has data reduction/aggregation functions embedded, the badges can be quite powerful. + +For each metric/dimension and for arbitrary time-frames badges can show **min**, **max** or **average** value, but also **sum** or **incremental-sum** to have their **volume**. + +For example, there is [a chart in Netdata that shows the current requests/s of nginx](http://london.my-netdata.io/#nginx_local_nginx). Using this chart alone we can show the following badges (we could add more time-frames, like **today**, **yesterday**, etc): + +<a href="https://registry.my-netdata.io/#nginx_local_nginx"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=nginx_local.connections&dimensions=active&value_color=grey:null%7Cblue&label=nginx%20active%20connections%20now&units=null&precision=0"/></a> <a href="https://registry.my-netdata.io/#nginx_local_nginx"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=nginx_local.connections&dimensions=active&after=-3600&value_color=orange&label=last%20hour%20average&units=null&options=unaligned&precision=0"/></a> <a href="https://registry.my-netdata.io/#nginx_local_nginx"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=nginx_local.connections&dimensions=active&group=max&after=-3600&value_color=red&label=last%20hour%20max&units=null&options=unaligned&precision=0"/></a> + +Similarly, there is [a chart that shows outbound bandwidth per class](http://london.my-netdata.io/#tc_eth0), using QoS data. So it shows `kilobits/s` per class. Using this chart we can show: + +<a href="https://registry.my-netdata.io/#tc_eth0"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=tc.world_out&dimensions=web_server&value_color=green&label=web%20server%20sends%20now&units=kbps"/></a> <a href="https://registry.my-netdata.io/#tc_eth0"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=tc.world_out&dimensions=web_server&after=-86400&options=unaligned&group=sum÷=8388608&value_color=blue&label=web%20server%20sent%20today&units=GB"/></a> + +The right one is a **volume** calculation. Netdata calculated the total of the last 86.400 seconds (a day) which gives `kilobits`, then divided it by 8 to make it KB, then by 1024 to make it MB and then by 1024 to make it GB. Calculations like this are quite accurate, since for every value collected, every second, Netdata interpolates it to second boundary using microsecond calculations. + +Let's see a few more badge examples (they come from the [Netdata registry](https://github.com/netdata/netdata/blob/master/src/registry/README.md)): + +- **cpu usage of user `root`** (you can pick any user; 100% = 1 core). This will be `green <10%`, `yellow <20%`, `orange <50%`, `blue <100%` (1 core), `red` otherwise (you define thresholds and colors on the URL). + + <a href="https://registry.my-netdata.io/#apps_cpu"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=users.cpu&dimensions=root&value_color=grey:null%7Cgreen%3C10%7Cyellow%3C20%7Corange%3C50%7Cblue%3C100%7Cred&label=root%20user%20cpu%20now&units=%25"></img></a> <a href="https://registry.my-netdata.io/#apps_cpu"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=users.cpu&dimensions=root&after=-3600&value_color=grey:null%7Cgreen%3C10%7Cyellow%3C20%7Corange%3C50%7Cblue%3C100%7Cred&label=root%20user%20average%20cpu%20last%20hour&units=%25"></img></a> + +- **mysql queries per second** + + <a href="https://registry.my-netdata.io/#mysql_local"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=mysql_local.queries&dimensions=questions&label=mysql%20queries%20now&value_color=red&units=%5Cs"></img></a> <a href="https://registry.my-netdata.io/#mysql_local"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=mysql_local.queries&dimensions=questions&after=-3600&options=unaligned&group=sum&label=mysql%20queries%20this%20hour&value_color=green&units=null"></img></a> <a href="https://registry.my-netdata.io/#mysql_local"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=mysql_local.queries&dimensions=questions&after=-86400&options=unaligned&group=sum&label=mysql%20queries%20today&value_color=blue&units=null"></img></a> + + niche ones: **mysql SELECT statements with JOIN, which did full table scans**: + + <a href="https://registry.my-netdata.io/#mysql_local_issues"><img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=mysql_local.join_issues&dimensions=scan&after=-3600&label=full%20table%20scans%20the%20last%20hour&value_color=orange&group=sum&units=null"></img></a> + +--- + +> So, every single line on the charts of a [Netdata dashboard](http://london.my-netdata.io/), can become a badge and this badge can calculate **average**, **min**, **max**, or **volume** for any time-frame! And you can also vary the badge color using conditions on the calculated value. + +--- + +## How to create badges + +The basic URL is `http://your.netdata:19999/api/v1/badge.svg?option1&option2&option3&...`. + +Here is what you can put for `options` (these are standard Netdata API options): + +- `chart=CHART.NAME` + + The chart to get the values from. + + **This is the only parameter required** and with just this parameter, Netdata will return the sum of the latest values of all chart dimensions. + + Example: + +```html + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu"></img> + </a> +``` + + Which produces this: + + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu"></img> + </a> + +- `alarm=NAME` + + Render the current value and status of an alert linked to the chart. This option can be ignored if the badge to be generated is not related to an alert. + + The current value of the alert will be rendered. The color of the badge will indicate the status of the alert. + + For alert badges, **both `chart` and `alarm` parameters are required**. + +- `dimensions=DIMENSION1|DIMENSION2|...` + + The dimensions of the chart to use. If you don't set any dimension, all will be used. When multiple dimensions are used, Netdata will sum their values. You can append `options=absolute` if you want this sum to convert all values to positive before adding them. + + Pipes in HTML have to escaped with `%7C`. + + Example: + +```html + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&dimensions=system%7Cnice"></img> + </a> +``` + + Which produces this: + + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&dimensions=system%7Cnice"></img> + </a> + +- `before=SECONDS` and `after=SECONDS` + + The timeframe. These can be absolute unix timestamps, or relative to now, number of seconds. By default `before=0` and `after=-1` (1 second in the past). + + To get the last minute set `after=-60`. This will give the average of the last complete minute (XX:XX:00 - XX:XX:59). + + To get the max of the last hour set `after=-3600&group=max`. This will give the maximum value of the last complete hour (XX:00:00 - XX:59:59) + + Example: + +```html + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60"></img> + </a> +``` + + Which produces the average of last complete minute (XX:XX:00 - XX:XX:59): + + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60"></img> + </a> + + While this is the previous minute (one minute before the last one, again aligned XX:XX:00 - XX:XX:59): + +```html + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&before=-60&after=-60"></img> + </a> +``` + + It produces this: + + <a href="#"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&before=-60&after=-60"></img> + </a> + +- `group=min` or `group=max` or `group=average` (the default) or `group=sum` or `group=incremental-sum` + + If Netdata will have to reduce (aggregate) the data to calculate the value, which aggregation method to use. + + - `max` will find the max value for the timeframe. This works on both positive and negative dimensions. It will find the most extreme value. + + - `min` will find the min value for the timeframe. This works on both positive and negative dimensions. It will find the number closest to zero. + + - `average` will calculate the average value for the timeframe. + + - `sum` will sum all the values for the timeframe. This is nice for finding the volume of dimensions for a timeframe. So if you have a dimension that reports `X per second`, you can find the volume of the dimension in a timeframe, by adding its values in that timeframe. + + - `incremental-sum` will sum the difference of each value to its next. Let's assume you have a dimension that does not measure the rate of something, but the absolute value of it. So it has values like this "1, 5, 3, 7, 4". `incremental-sum` will calculate the difference of adjacent values. In this example, they will be `(5 - 1) + (3 - 5) + (7 - 3) + (4 - 7) = 3` (which is equal to the last value minus the first = 4 - 1). + +- `options=opt1|opt2|opt3|...` + + These fine tune various options of the API. Here is what you can use for badges (the API has more option, but only these are useful for badges): + + - `percentage`, instead of returning a value, calculate the percentage of the sum of the values of the selected dimensions (selected sum / total sum * 100). This also sets the units to `%`. + + - `absolute` or `abs`, turn all values positive and then sum them. + + - `display_absolute` or `display-absolute`, to use the signed value during color calculation, but display the absolute value on the badge. + + - `min2max`, when multiple dimensions are given, do not sum them, but take their `max - min`. + + - `unaligned`, when data are reduced / aggregated (e.g. the request is about the average of the last minute, or hour), Netdata by default aligns them so that the charts will have a constant shape (so average per minute returns always XX:XX:00 - XX:XX:59). Setting the `unaligned` option, Netdata will aggregate data without any alignment, so if the request is for 60 seconds, it will aggregate the latest 60 seconds of collected data. + +These are options dedicated to badges: + +- `label=TEXT` + + The label of the badge. + +- `units=TEXT` + + The units of the badge. If you want to put a `/`, please put a `\`. This is because Netdata allows badges parameters to be given as path in URL, instead of query string. You can also use `null` or `empty` to show it without any units. + + The units `seconds`, `minutes` and `hours` trigger special formatting. The value has to be in this unit, and Netdata will automatically change it to show a more pretty duration. + +- `multiply=NUMBER` + + Multiply the value with this number. The default is `1`. + +- `divide=NUMBER` + + Divide the value with this number. The default is `1`. + +- Color customization parameters + + The following parameters specify colors of each individual part of the badge. Each parameter is documented in detail + below. + + | Area of badge | Background color parameter | Text color parameter | + | ---: | :------------------------: | :------------------: | + | Label (left) part | `label_color` | `text_color_lbl` | + | Value (right) part | `value_color` | `text_color_val` | + + - `label_color=COLOR` + + The color of the label (the left part). You can use any HTML color in `RGB` or `RRGGBB` hex notation (without + the `#` character at the beginning). Additionally, you can use one of the following predefined colors (and you + can use them by their name): + + - `green` + - `brightgreen` + - `yellow` + - `yellowgreen` + - `orange` + - `red` + - `blue` + - `grey` + - `gray` + - `lightgrey` + - `lightgray` + + These colors are taken from <https://github.com/badges/shields>, which makes them compatible with standard + badges. + + - `value_color=COLOR:null|COLOR<VALUE|COLOR>VALUE|COLOR>=VALUE|COLOR<=VALUE|...` + + You can add a pipe delimited list of conditions to pick the value color. The first matching (left to right) will + be used. + + Example: `value_color=grey:null|green<10|yellow<100|orange<1000|blue<10000|red` + + The above will set `grey` if no value exists (not collected within the `gap when lost iterations above` in + `netdata.conf` for the chart), `green` if the value is less than 10, `yellow` if the value is less than 100, and + so on. Netdata will use `red` if no other conditions match. Only integers are supported as values. + + The supported operators are `<`, `>`, `<=`, `>=`, `=` (or `:`), and `!=` (or `<>`). + + You can also use the same syntax as the `label_color` parameter to define each of these colors. You can + reference a predefined color by name or `RGB`/`RRGGBB` hex notation. + + - `text_color_lbl=RGB` or `text_color_lbl=RRGGBB` or `text_color_lbl=color_by_name` + + This value specifies the font color for the font of left/label side of the badge. The syntax is the same as the + `label_color` parameter. If not given, or given with an empty value, Netdata will use the default color. + + - `text_color_val=RGB` or `text_color_val=RRGGBB` or `text_color_lbl=color_by_name` + + This value specifies the font color for the font of right/value side of the badge. The syntax is the same as the + `label_color` parameter. If not given, or given with an empty value, Netdata will use the default color. + +- `precision=NUMBER` + + The number of decimal digits of the value. By default Netdata will add: + + - no decimal digits for values > 1000 + - 1 decimal digit for values > 100 + - 2 decimal digits for values > 1 + - 3 decimal digits for values > 0.1 + - 4 decimal digits for values \<= 0.1 + + Using the `precision=NUMBER` you can set your preference per badge. + +- `scale=XXX` + + This option scales the svg image. It accepts values above or equal to 100 (100% is the default scale). For example, lets get a few different sizes: + + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60&scale=100"></img> original<br/> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60&scale=125"></img> `scale=125`<br/> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60&scale=150"></img> `scale=150`<br/> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60&scale=175"></img> `scale=175`<br/> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=system.cpu&after=-60&scale=200"></img> `scale=200` + +- `fixed_width_lbl=NUMBER` and `fixed_width_val=NUMBER` + + This parameter overrides auto-sizing of badges and displays them at fixed widths. `fixed_width_lbl` determines the size of the label's left side (label/name). `fixed_width_val` determines the size of the label's right side (value). You must set both parameters together, or they will be ignored. + + You should set the label/value widths wide enough to provide space for all the possible values/contents of the badge you're requesting. In case the text cannot fit the space given it will be clipped. + + The `scale` parameter still applies on the values you give to `fixed_width_lbl` and `fixed_width_val`. + +- `refresh=auto` or `refresh=SECONDS` + + This option enables auto-refreshing of images. Netdata will send the HTTP header `Refresh: SECONDS` to the web browser, thus requesting automatic refresh of the images at regular intervals. + + `auto` will calculate the proper `SECONDS` to avoid unnecessary refreshes. If `SECONDS` is zero, this feature is disabled (it is also disabled by default). + + Auto-refreshing like this, works only if you access the badge directly. So, you may have to put it an `embed` or `iframe` for it to be auto-refreshed. Use something like this: + +```html +<embed src="BADGE_URL" type="image/svg+xml" height="20" /> +``` + + Another way is to use javascript to auto-refresh them. You can auto-refresh all the Netdata badges on a page using javascript. You have to add a class to all the Netdata badges, like this `<img class="netdata-badge" src="..."/>`. Then add this javascript code to your page (it requires jquery): + +```html +<script> + var NETDATA_BADGES_AUTOREFRESH_SECONDS = 5; + function refreshNetdataBadges() { + var now = new Date().getTime().toString(); + $('.netdata-badge').each(function() { + this.src = this.src.replace(/\&_=\d*/, '') + '&_=' + now; + }); + setTimeout(refreshNetdataBadges, NETDATA_BADGES_AUTOREFRESH_SECONDS * 1000); + } + setTimeout(refreshNetdataBadges, NETDATA_BADGES_AUTOREFRESH_SECONDS * 1000); +</script> +``` + +A more advanced badges refresh method is to include `http://your.netdata.ip:19999/refresh-badges.js` in your page. + +--- + +## Escaping URLs + +Keep in mind that if you add badge URLs to your HTML pages you have to escape the special characters: + +|character|name|escape sequence| +|:-------:|:--:|:-------------:| +|``|space (in labels and units)|`%20`| +|`#`|hash (for colors)|`%23`| +|`%`|percent (in units)|`%25`| +|`<`|less than|`%3C`| +|`>`|greater than|`%3E`| +|`\`|backslash (when you need a `/`)|`%5C`| +|`\|`|pipe (delimiting parameters)|`%7C`| + +## FAQ + +#### Is it fast? + +On modern hardware, Netdata can generate about **2.000 badges per second per core**, before noticing any delays. It generates a badge in about half a millisecond! + +Of course these timing are for badges that use recent data. If you need badges that do calculations over long durations (a day, or more), timing will differ. Netdata logs its timings at its `access.log`, so take a look there before adding a heavy badge on a busy web site. Of course, you can cache such badges or have a cron job get them from Netdata and save them at your web server at regular intervals. + +#### Embedding badges in GitHub + +You have 2 options: +- SVG images with markdown +- SVG images with HTML (directly in .md files) + +For example, this is the cpu badge shown above: + +- Markdown example: + +```md +[![A nice name](https://registry.my-netdata.io/api/v1/badge.svg?chart=users.cpu&dimensions=root&value_color=grey:null%7Cgreen%3C10%7Cyellow%3C20%7Corange%3C50%7Cblue%3C100%7Cred&label=root%20user%20cpu%20now&units=%25)](https://registry.my-netdata.io/#apps_cpu) +``` + +- HTML example: + +```html +<a href="https://registry.my-netdata.io/#apps_cpu"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=users.cpu&dimensions=root&value_color=grey:null%7Cgreen%3C10%7Cyellow%3C20%7Corange%3C50%7Cblue%3C100%7Cred&label=root%20user%20cpu%20now&units=%25"></img> +</a> +``` + +Both produce this: + +<a href="https://registry.my-netdata.io/#apps_cpu"> + <img src="https://registry.my-netdata.io/api/v1/badge.svg?chart=users.cpu&dimensions=root&value_color=grey:null%7Cgreen%3C10%7Cyellow%3C20%7Corange%3C50%7Cblue%3C100%7Cred&label=root%20user%20cpu%20now&units=%25"></img> +</a> + +#### Auto-refreshing badges in GitHub + +Unfortunately it cannot be done. GitHub fetches all the images using a proxy and rewrites all the URLs to be served by the proxy. + +You can refresh them from your browser console though. Press F12 to open the web browser console (switch to the console too), paste the following and press enter. They will refresh: + +```js +var len = document.images.length; while(len--) { document.images[len].src = document.images[len].src.replace(/\?cacheBuster=\d*/, "") + "?cacheBuster=" + new Date().getTime().toString(); }; +``` + + diff --git a/src/web/api/badges/web_buffer_svg.c b/src/web/api/badges/web_buffer_svg.c new file mode 100644 index 000000000..23dc96d10 --- /dev/null +++ b/src/web/api/badges/web_buffer_svg.c @@ -0,0 +1,1159 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web_buffer_svg.h" + +#define BADGE_HORIZONTAL_PADDING 4 +#define VERDANA_KERNING 0.2 +#define VERDANA_PADDING 1.0 + +/* + * verdana11_widths[] has been generated with this method: + * https://github.com/badges/shields/blob/master/measure-text.js +*/ + +static double verdana11_widths[128] = { + [0] = 0.0, + [1] = 0.0, + [2] = 0.0, + [3] = 0.0, + [4] = 0.0, + [5] = 0.0, + [6] = 0.0, + [7] = 0.0, + [8] = 0.0, + [9] = 0.0, + [10] = 0.0, + [11] = 0.0, + [12] = 0.0, + [13] = 0.0, + [14] = 0.0, + [15] = 0.0, + [16] = 0.0, + [17] = 0.0, + [18] = 0.0, + [19] = 0.0, + [20] = 0.0, + [21] = 0.0, + [22] = 0.0, + [23] = 0.0, + [24] = 0.0, + [25] = 0.0, + [26] = 0.0, + [27] = 0.0, + [28] = 0.0, + [29] = 0.0, + [30] = 0.0, + [31] = 0.0, + [32] = 3.8671874999999996, // + [33] = 4.3291015625, // ! + [34] = 5.048828125, // " + [35] = 9.001953125, // # + [36] = 6.9931640625, // $ + [37] = 11.837890625, // % + [38] = 7.992187499999999, // & + [39] = 2.9541015625, // ' + [40] = 4.9951171875, // ( + [41] = 4.9951171875, // ) + [42] = 6.9931640625, // * + [43] = 9.001953125, // + + [44] = 4.00146484375, // , + [45] = 4.9951171875, // - + [46] = 4.00146484375, // . + [47] = 4.9951171875, // / + [48] = 6.9931640625, // 0 + [49] = 6.9931640625, // 1 + [50] = 6.9931640625, // 2 + [51] = 6.9931640625, // 3 + [52] = 6.9931640625, // 4 + [53] = 6.9931640625, // 5 + [54] = 6.9931640625, // 6 + [55] = 6.9931640625, // 7 + [56] = 6.9931640625, // 8 + [57] = 6.9931640625, // 9 + [58] = 4.9951171875, // : + [59] = 4.9951171875, // ; + [60] = 9.001953125, // < + [61] = 9.001953125, // = + [62] = 9.001953125, // > + [63] = 5.99951171875, // ? + [64] = 11.0, // @ + [65] = 7.51953125, // A + [66] = 7.541015625, // B + [67] = 7.680664062499999, // C + [68] = 8.4755859375, // D + [69] = 6.95556640625, // E + [70] = 6.32177734375, // F + [71] = 8.529296875, // G + [72] = 8.26611328125, // H + [73] = 4.6298828125, // I + [74] = 5.00048828125, // J + [75] = 7.62158203125, // K + [76] = 6.123046875, // L + [77] = 9.2705078125, // M + [78] = 8.228515625, // N + [79] = 8.658203125, // O + [80] = 6.63330078125, // P + [81] = 8.658203125, // Q + [82] = 7.6484375, // R + [83] = 7.51953125, // S + [84] = 6.7783203125, // T + [85] = 8.05126953125, // U + [86] = 7.51953125, // V + [87] = 10.87646484375, // W + [88] = 7.53564453125, // X + [89] = 6.767578125, // Y + [90] = 7.53564453125, // Z + [91] = 4.9951171875, // [ + [92] = 4.9951171875, // backslash + [93] = 4.9951171875, // ] + [94] = 9.001953125, // ^ + [95] = 6.9931640625, // _ + [96] = 6.9931640625, // ` + [97] = 6.6064453125, // a + [98] = 6.853515625, // b + [99] = 5.73095703125, // c + [100] = 6.853515625, // d + [101] = 6.552734375, // e + [102] = 3.8671874999999996, // f + [103] = 6.853515625, // g + [104] = 6.9609375, // h + [105] = 3.0185546875, // i + [106] = 3.78662109375, // j + [107] = 6.509765625, // k + [108] = 3.0185546875, // l + [109] = 10.69921875, // m + [110] = 6.9609375, // n + [111] = 6.67626953125, // o + [112] = 6.853515625, // p + [113] = 6.853515625, // q + [114] = 4.6943359375, // r + [115] = 5.73095703125, // s + [116] = 4.33447265625, // t + [117] = 6.9609375, // u + [118] = 6.509765625, // v + [119] = 9.001953125, // w + [120] = 6.509765625, // x + [121] = 6.509765625, // y + [122] = 5.779296875, // z + [123] = 6.982421875, // { + [124] = 4.9951171875, // | + [125] = 6.982421875, // } + [126] = 9.001953125, // ~ + [127] = 0.0 +}; + +// find the width of the string using the verdana 11points font +static inline double verdana11_width(const char *s, float em_size) { + double w = 0.0; + + while(*s) { + // if UTF8 multibyte char found and guess it's width equal 1em + // as label width will be updated with JavaScript this is not so important + + // TODO: maybe move UTF8 functions from url.c to separate util in libnetdata + // then use url_utf8_get_byte_length etc. + if(IS_UTF8_STARTBYTE(*s)) { + s++; + while(IS_UTF8_BYTE(*s) && !IS_UTF8_STARTBYTE(*s)){ + s++; + } + w += em_size; + } + else { + if(likely(!(*s & 0x80))){ // Byte 1XXX XXXX is not valid in UTF8 + double t = verdana11_widths[(unsigned char)*s]; + if(t != 0.0) + w += t + VERDANA_KERNING; + } + s++; + } + } + + w -= VERDANA_KERNING; + w += VERDANA_PADDING; + return w; +} + +static inline size_t escape_xmlz(char *dst, const char *src, size_t len) { + size_t i = len; + + // required escapes from + // https://github.com/badges/shields/blob/master/badge.js + while(*src && i) { + switch(*src) { + case '\\': + *dst++ = '/'; + src++; + i--; + break; + + case '&': + if(i > 5) { + strcpy(dst, "&"); + i -= 5; + dst += 5; + src++; + } + else goto cleanup; + break; + + case '<': + if(i > 4) { + strcpy(dst, "<"); + i -= 4; + dst += 4; + src++; + } + else goto cleanup; + break; + + case '>': + if(i > 4) { + strcpy(dst, ">"); + i -= 4; + dst += 4; + src++; + } + else goto cleanup; + break; + + case '"': + if(i > 6) { + strcpy(dst, """); + i -= 6; + dst += 6; + src++; + } + else goto cleanup; + break; + + case '\'': + if(i > 6) { + strcpy(dst, "'"); + i -= 6; + dst += 6; + src++; + } + else goto cleanup; + break; + + default: + i--; + *dst++ = *src++; + break; + } + } + +cleanup: + *dst = '\0'; + return len - i; +} + +static inline char *format_value_with_precision_and_unit(char *value_string, size_t value_string_len, + NETDATA_DOUBLE value, const char *units, int precision) { + if(unlikely(isnan(value) || isinf(value))) + value = 0.0; + + char *separator = ""; + if(unlikely(isalnum(*units))) + separator = " "; + + if(precision < 0) { + int len, lstop = 0, trim_zeros = 1; + + NETDATA_DOUBLE abs = value; + if(isless(value, 0)) { + lstop = 1; + abs = fabsndd(value); + } + + if(isgreaterequal(abs, 1000)) { + len = snprintfz(value_string, value_string_len, "%0.0" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + trim_zeros = 0; + } + else if(isgreaterequal(abs, 10)) len = snprintfz(value_string, value_string_len, "%0.1" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else if(isgreaterequal(abs, 1)) len = snprintfz(value_string, value_string_len, "%0.2" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else if(isgreaterequal(abs, 0.1)) len = snprintfz(value_string, value_string_len, "%0.2" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else if(isgreaterequal(abs, 0.01)) len = snprintfz(value_string, value_string_len, "%0.4" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else if(isgreaterequal(abs, 0.001)) len = snprintfz(value_string, value_string_len, "%0.5" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else if(isgreaterequal(abs, 0.0001)) len = snprintfz(value_string, value_string_len, "%0.6" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + else len = snprintfz(value_string, value_string_len, "%0.7" NETDATA_DOUBLE_MODIFIER, (NETDATA_DOUBLE) value); + + if(unlikely(trim_zeros)) { + int l; + // remove trailing zeros from the decimal part + for(l = len - 1; l > lstop; l--) { + if(likely(value_string[l] == '0')) { + value_string[l] = '\0'; + len--; + } + + else if(unlikely(value_string[l] == '.')) { + value_string[l] = '\0'; + len--; + break; + } + + else + break; + } + } + + if(unlikely(len <= 0)) len = 1; + snprintfz(&value_string[len], value_string_len - len, "%s%s", separator, units); + } + else { + if(precision > 50) precision = 50; + snprintfz(value_string, value_string_len, "%0.*" NETDATA_DOUBLE_MODIFIER "%s%s", precision, (NETDATA_DOUBLE) value, separator, units); + } + + return value_string; +} + +typedef enum badge_units_format { + UNITS_FORMAT_NONE, + UNITS_FORMAT_SECONDS, + UNITS_FORMAT_SECONDS_AGO, + UNITS_FORMAT_MINUTES, + UNITS_FORMAT_MINUTES_AGO, + UNITS_FORMAT_HOURS, + UNITS_FORMAT_HOURS_AGO, + UNITS_FORMAT_ONOFF, + UNITS_FORMAT_UPDOWN, + UNITS_FORMAT_OKERROR, + UNITS_FORMAT_OKFAILED, + UNITS_FORMAT_EMPTY, + UNITS_FORMAT_PERCENT +} UNITS_FORMAT; + + +static struct units_formatter { + const char *units; + uint32_t hash; + UNITS_FORMAT format; +} badge_units_formatters[] = { + { "seconds", 0, UNITS_FORMAT_SECONDS }, + { "seconds ago", 0, UNITS_FORMAT_SECONDS_AGO }, + { "minutes", 0, UNITS_FORMAT_MINUTES }, + { "minutes ago", 0, UNITS_FORMAT_MINUTES_AGO }, + { "hours", 0, UNITS_FORMAT_HOURS }, + { "hours ago", 0, UNITS_FORMAT_HOURS_AGO }, + { "on/off", 0, UNITS_FORMAT_ONOFF }, + { "on-off", 0, UNITS_FORMAT_ONOFF }, + { "onoff", 0, UNITS_FORMAT_ONOFF }, + { "up/down", 0, UNITS_FORMAT_UPDOWN }, + { "up-down", 0, UNITS_FORMAT_UPDOWN }, + { "updown", 0, UNITS_FORMAT_UPDOWN }, + { "ok/error", 0, UNITS_FORMAT_OKERROR }, + { "ok-error", 0, UNITS_FORMAT_OKERROR }, + { "okerror", 0, UNITS_FORMAT_OKERROR }, + { "ok/failed", 0, UNITS_FORMAT_OKFAILED }, + { "ok-failed", 0, UNITS_FORMAT_OKFAILED }, + { "okfailed", 0, UNITS_FORMAT_OKFAILED }, + { "empty", 0, UNITS_FORMAT_EMPTY }, + { "null", 0, UNITS_FORMAT_EMPTY }, + { "percentage", 0, UNITS_FORMAT_PERCENT }, + { "percent", 0, UNITS_FORMAT_PERCENT }, + { "pcent", 0, UNITS_FORMAT_PERCENT }, + + // terminator + { NULL, 0, UNITS_FORMAT_NONE } +}; + +inline char *format_value_and_unit(char *value_string, size_t value_string_len, + NETDATA_DOUBLE value, const char *units, int precision) { + static int max = -1; + int i; + + if(unlikely(max == -1)) { + for(i = 0; badge_units_formatters[i].units; i++) + badge_units_formatters[i].hash = simple_hash(badge_units_formatters[i].units); + + max = i; + } + + if(unlikely(!units)) units = ""; + uint32_t hash_units = simple_hash(units); + + UNITS_FORMAT format = UNITS_FORMAT_NONE; + for(i = 0; i < max; i++) { + struct units_formatter *ptr = &badge_units_formatters[i]; + + if(hash_units == ptr->hash && !strcmp(units, ptr->units)) { + format = ptr->format; + break; + } + } + + if(unlikely(format == UNITS_FORMAT_SECONDS || format == UNITS_FORMAT_SECONDS_AGO)) { + if(value == 0.0) { + snprintfz(value_string, value_string_len, "%s", "now"); + return value_string; + } + else if(isnan(value) || isinf(value)) { + snprintfz(value_string, value_string_len, "%s", "undefined"); + return value_string; + } + + const char *suffix = (format == UNITS_FORMAT_SECONDS_AGO)?" ago":""; + + size_t s = (size_t)value; + size_t d = s / 86400; + s = s % 86400; + + size_t h = s / 3600; + s = s % 3600; + + size_t m = s / 60; + s = s % 60; + + if(d) + snprintfz(value_string, value_string_len, "%zu %s %02zu:%02zu:%02zu%s", d, (d == 1)?"day":"days", h, m, s, suffix); + else + snprintfz(value_string, value_string_len, "%02zu:%02zu:%02zu%s", h, m, s, suffix); + + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_MINUTES || format == UNITS_FORMAT_MINUTES_AGO)) { + if(value == 0.0) { + snprintfz(value_string, value_string_len, "%s", "now"); + return value_string; + } + else if(isnan(value) || isinf(value)) { + snprintfz(value_string, value_string_len, "%s", "undefined"); + return value_string; + } + + const char *suffix = (format == UNITS_FORMAT_MINUTES_AGO)?" ago":""; + + size_t m = (size_t)value; + size_t d = m / (60 * 24); + m = m % (60 * 24); + + size_t h = m / 60; + m = m % 60; + + if(d) + snprintfz(value_string, value_string_len, "%zud %02zuh %02zum%s", d, h, m, suffix); + else + snprintfz(value_string, value_string_len, "%zuh %zum%s", h, m, suffix); + + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_HOURS || format == UNITS_FORMAT_HOURS_AGO)) { + if(value == 0.0) { + snprintfz(value_string, value_string_len, "%s", "now"); + return value_string; + } + else if(isnan(value) || isinf(value)) { + snprintfz(value_string, value_string_len, "%s", "undefined"); + return value_string; + } + + const char *suffix = (format == UNITS_FORMAT_HOURS_AGO)?" ago":""; + + size_t h = (size_t)value; + size_t d = h / 24; + h = h % 24; + + if(d) + snprintfz(value_string, value_string_len, "%zud %zuh%s", d, h, suffix); + else + snprintfz(value_string, value_string_len, "%zuh%s", h, suffix); + + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_ONOFF)) { + snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"on":"off"); + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_UPDOWN)) { + snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"up":"down"); + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_OKERROR)) { + snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"ok":"error"); + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_OKFAILED)) { + snprintfz(value_string, value_string_len, "%s", (value != 0.0)?"ok":"failed"); + return value_string; + } + + else if(unlikely(format == UNITS_FORMAT_EMPTY)) + units = ""; + + else if(unlikely(format == UNITS_FORMAT_PERCENT)) + units = "%"; + + if(unlikely(isnan(value) || isinf(value))) { + strcpy(value_string, "-"); + return value_string; + } + + return format_value_with_precision_and_unit(value_string, value_string_len, value, units, precision); +} + +static struct badge_color { + const char *name; + uint32_t hash; + const char *color; +} badge_colors[] = { + + // colors from: + // https://github.com/badges/shields/blob/master/colorscheme.json + + { "brightgreen", 0, "4c1" }, + { "green", 0, "97CA00" }, + { "yellow", 0, "dfb317" }, + { "yellowgreen", 0, "a4a61d" }, + { "orange", 0, "fe7d37" }, + { "red", 0, "e05d44" }, + { "blue", 0, "007ec6" }, + { "grey", 0, "555" }, + { "gray", 0, "555" }, + { "lightgrey", 0, "9f9f9f" }, + { "lightgray", 0, "9f9f9f" }, + + // terminator + { NULL, 0, NULL } +}; + +static inline const char *color_map(const char *color, const char *def) { + static int max = -1; + int i; + + if(unlikely(max == -1)) { + for(i = 0; badge_colors[i].name ;i++) + badge_colors[i].hash = simple_hash(badge_colors[i].name); + + max = i; + } + + uint32_t hash = simple_hash(color); + + for(i = 0; i < max; i++) { + struct badge_color *ptr = &badge_colors[i]; + + if(hash == ptr->hash && !strcmp(color, ptr->name)) + return ptr->color; + } + + return def; +} + +typedef enum color_comparison { + COLOR_COMPARE_EQUAL, + COLOR_COMPARE_NOTEQUAL, + COLOR_COMPARE_LESS, + COLOR_COMPARE_LESSEQUAL, + COLOR_COMPARE_GREATER, + COLOR_COMPARE_GREATEREQUAL, +} BADGE_COLOR_COMPARISON; + +static inline void calc_colorz(const char *color, char *final, size_t len, NETDATA_DOUBLE value) { + if(isnan(value) || isinf(value)) + value = NAN; + + char color_buffer[256 + 1] = ""; + char value_buffer[256 + 1] = ""; + BADGE_COLOR_COMPARISON comparison = COLOR_COMPARE_GREATER; + + // example input: + // color<max|color>min|color:null... + + const char *c = color; + while(*c) { + char *dc = color_buffer, *dv = NULL; + size_t ci = 0, vi = 0; + + const char *t = c; + + while(*t && *t != '|') { + switch(*t) { + case '!': + if(t[1] == '=') t++; + comparison = COLOR_COMPARE_NOTEQUAL; + dv = value_buffer; + break; + + case '=': + case ':': + comparison = COLOR_COMPARE_EQUAL; + dv = value_buffer; + break; + + case '}': + case ')': + case '>': + if(t[1] == '=') { + comparison = COLOR_COMPARE_GREATEREQUAL; + t++; + } + else + comparison = COLOR_COMPARE_GREATER; + dv = value_buffer; + break; + + case '{': + case '(': + case '<': + if(t[1] == '=') { + comparison = COLOR_COMPARE_LESSEQUAL; + t++; + } + else if(t[1] == '>' || t[1] == ')' || t[1] == '}') { + comparison = COLOR_COMPARE_NOTEQUAL; + t++; + } + else + comparison = COLOR_COMPARE_LESS; + dv = value_buffer; + break; + + default: + if(dv) { + if(vi < 256) { + vi++; + *dv++ = *t; + } + } + else { + if(ci < 256) { + ci++; + *dc++ = *t; + } + } + break; + } + + t++; + } + + // prepare for next iteration + if(*t == '|') t++; + c = t; + + // do the math + *dc = '\0'; + if(dv) { + *dv = '\0'; + NETDATA_DOUBLE v; + + if(!*value_buffer || !strcmp(value_buffer, "null")) { + v = NAN; + } + else { + v = str2l(value_buffer); + if(isnan(v) || isinf(v)) + v = NAN; + } + + if(unlikely(isnan(value) || isnan(v))) { + if(isnan(value) && isnan(v)) + break; + } + else { + if (unlikely(comparison == COLOR_COMPARE_LESS && isless(value, v))) break; + else if (unlikely(comparison == COLOR_COMPARE_LESSEQUAL && islessequal(value, v))) break; + else if (unlikely(comparison == COLOR_COMPARE_GREATER && isgreater(value, v))) break; + else if (unlikely(comparison == COLOR_COMPARE_GREATEREQUAL && isgreaterequal(value, v))) break; + else if (unlikely(comparison == COLOR_COMPARE_EQUAL && !islessgreater(value, v))) break; + else if (unlikely(comparison == COLOR_COMPARE_NOTEQUAL && islessgreater(value, v))) break; + } + } + else + break; + } + + const char *b; + if(color_buffer[0]) + b = color_buffer; + else + b = color; + + strncpyz(final, b, len); +} + +// value + units +#define VALUE_STRING_SIZE 100 + +// label +#define LABEL_STRING_SIZE 200 + +// colors +#define COLOR_STRING_SIZE 100 + +static inline int allowed_hexa_char(char x) { + return ( (x >= '0' && x <= '9') || + (x >= 'a' && x <= 'f') || + (x >= 'A' && x <= 'F') + ); +} + +static int html_color_check(const char *str) { + int i = 0; + while(str[i]) { + if(!allowed_hexa_char(str[i])) + return 0; + if(unlikely(i >= 6)) + return 0; + i++; + } + // want to allow either RGB or RRGGBB + return ( i == 6 || i == 3 ); +} + +// Will parse color arg as #RRGGBB or #RGB or one of the colors +// from color_map hash table +// if parsing fails (argument error) it will return default color +// given as default parameter (def) +// in any case it will return either color in "RRGGBB" or "RGB" format as string +// or whatever is given as def (without checking - caller responsible to give sensible +// safely escaped default) as default if it fails +// in any case this function must always return something we can put directly in XML +// so no escaping is necessary anymore (with exception of default where caller is responsible) +// to give sensible default +#define BADGE_SVG_COLOR_ARG_MAXLEN 20 + +static const char *parse_color_argument(const char *arg, const char *def) +{ + if( !arg ) + return def; + size_t len = strnlen(arg, BADGE_SVG_COLOR_ARG_MAXLEN); + if( len < 2 || len >= BADGE_SVG_COLOR_ARG_MAXLEN ) + return def; + if( html_color_check(arg) ) + return arg; + return color_map(arg, def); +} + +void buffer_svg(BUFFER *wb, const char *label, + NETDATA_DOUBLE value, const char *units, const char *label_color, const char *value_color, int precision, int scale, uint32_t options, int fixed_width_lbl, int fixed_width_val, const char* text_color_lbl, const char* text_color_val) { + char value_color_buffer[COLOR_STRING_SIZE + 1] + , value_string[VALUE_STRING_SIZE + 1] + , label_escaped[LABEL_STRING_SIZE + 1] + , value_escaped[VALUE_STRING_SIZE + 1]; + + const char *label_color_parsed; + const char *value_color_parsed; + + double label_width = (double)fixed_width_lbl, value_width = (double)fixed_width_val, total_width; + double height = 20.0, font_size = 11.0, text_offset = 5.8, round_corner = 3.0; + + if(scale < 100) scale = 100; + + if(unlikely(!value_color || !*value_color)) + value_color = (isnan(value) || isinf(value))?"999":"4c1"; + + calc_colorz(value_color, value_color_buffer, COLOR_STRING_SIZE, value); + format_value_and_unit(value_string, VALUE_STRING_SIZE, (options & RRDR_OPTION_DISPLAY_ABS)? fabsndd(value):value, units, precision); + + if(fixed_width_lbl <= 0 || fixed_width_val <= 0) { + label_width = verdana11_width(label, font_size) + (BADGE_HORIZONTAL_PADDING * 2); + value_width = verdana11_width(value_string, font_size) + (BADGE_HORIZONTAL_PADDING * 2); + } + total_width = label_width + value_width; + + escape_xmlz(label_escaped, label, LABEL_STRING_SIZE); + escape_xmlz(value_escaped, value_string, VALUE_STRING_SIZE); + + label_color_parsed = parse_color_argument(label_color, "555"); + value_color_parsed = parse_color_argument(value_color_buffer, "555"); + + wb->content_type = CT_IMAGE_SVG_XML; + + total_width = total_width * scale / 100.0; + height = height * scale / 100.0; + font_size = font_size * scale / 100.0; + text_offset = text_offset * scale / 100.0; + label_width = label_width * scale / 100.0; + value_width = value_width * scale / 100.0; + round_corner = round_corner * scale / 100.0; + + // svg template from: + // https://raw.githubusercontent.com/badges/shields/master/templates/flat-template.svg + buffer_sprintf(wb, + "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"%0.2f\" height=\"%0.2f\">" + "<linearGradient id=\"smooth\" x2=\"0\" y2=\"100%%\">" + "<stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>" + "<stop offset=\"1\" stop-opacity=\".1\"/>" + "</linearGradient>" + "<mask id=\"round\">" + "<rect class=\"bdge-ttl-width\" width=\"%0.2f\" height=\"%0.2f\" rx=\"%0.2f\" fill=\"#fff\"/>" + "</mask>" + "<g mask=\"url(#round)\">" + "<rect class=\"bdge-rect-lbl\" width=\"%0.2f\" height=\"%0.2f\" fill=\"#%s\"/>", + total_width, height, + total_width, height, round_corner, + label_width, height, label_color_parsed); //<rect class="bdge-rect-lbl" + + if(fixed_width_lbl > 0 && fixed_width_val > 0) { + buffer_sprintf(wb, + "<clipPath id=\"lbl-rect\">" + "<rect class=\"bdge-rect-lbl\" width=\"%0.2f\" height=\"%0.2f\"/>" + "</clipPath>", + label_width, height); //<clipPath id="lbl-rect"> <rect class="bdge-rect-lbl" + } + + buffer_sprintf(wb, + "<rect class=\"bdge-rect-val\" x=\"%0.2f\" width=\"%0.2f\" height=\"%0.2f\" fill=\"#%s\"/>", + label_width, value_width, height, value_color_parsed); + + if(fixed_width_lbl > 0 && fixed_width_val > 0) { + buffer_sprintf(wb, + "<clipPath id=\"val-rect\">" + "<rect class=\"bdge-rect-val\" x=\"%0.2f\" width=\"%0.2f\" height=\"%0.2f\"/>" + "</clipPath>", + label_width, value_width, height); + } + + buffer_sprintf(wb, + "<rect class=\"bdge-ttl-width\" width=\"%0.2f\" height=\"%0.2f\" fill=\"url(#smooth)\"/>" + "</g>" + "<g text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"%0.2f\">" + "<text class=\"bdge-lbl-lbl\" x=\"%0.2f\" y=\"%0.0f\" fill=\"#010101\" fill-opacity=\".3\" clip-path=\"url(#lbl-rect)\">%s</text>" + "<text class=\"bdge-lbl-lbl\" x=\"%0.2f\" y=\"%0.0f\" fill=\"#%s\" clip-path=\"url(#lbl-rect)\">%s</text>" + "<text class=\"bdge-lbl-val\" x=\"%0.2f\" y=\"%0.0f\" fill=\"#010101\" fill-opacity=\".3\" clip-path=\"url(#val-rect)\">%s</text>" + "<text class=\"bdge-lbl-val\" x=\"%0.2f\" y=\"%0.0f\" fill=\"#%s\" clip-path=\"url(#val-rect)\">%s</text>" + "</g>", + total_width, height, + font_size, + label_width / 2, ceil(height - text_offset), label_escaped, + label_width / 2, ceil(height - text_offset - 1.0), parse_color_argument(text_color_lbl, "fff"), label_escaped, + label_width + value_width / 2 -1, ceil(height - text_offset), value_escaped, + label_width + value_width / 2 -1, ceil(height - text_offset - 1.0), parse_color_argument(text_color_val, "fff"), value_escaped); + + if(fixed_width_lbl <= 0 || fixed_width_val <= 0){ + buffer_sprintf(wb, + "<script type=\"text/javascript\">" + "var bdg_horiz_padding = %d;" + "function netdata_bdge_each(list, attr, value){" + "Array.prototype.forEach.call(list, function(el){" + "el.setAttribute(attr, value);" + "});" + "};" + "var this_svg = document.currentScript.closest(\"svg\");" + "var elem_lbl = this_svg.getElementsByClassName(\"bdge-lbl-lbl\");" + "var elem_val = this_svg.getElementsByClassName(\"bdge-lbl-val\");" + "var lbl_size = elem_lbl[0].getBBox();" + "var val_size = elem_val[0].getBBox();" + "var width_total = lbl_size.width + bdg_horiz_padding*2;" + "this_svg.getElementsByClassName(\"bdge-rect-lbl\")[0].setAttribute(\"width\", width_total);" + "netdata_bdge_each(elem_lbl, \"x\", (lbl_size.width / 2) + bdg_horiz_padding);" + "netdata_bdge_each(elem_val, \"x\", width_total + (val_size.width / 2) + bdg_horiz_padding);" + "var val_rect = this_svg.getElementsByClassName(\"bdge-rect-val\")[0];" + "val_rect.setAttribute(\"width\", val_size.width + bdg_horiz_padding*2);" + "val_rect.setAttribute(\"x\", width_total);" + "width_total += val_size.width + bdg_horiz_padding*2;" + "var width_update_elems = this_svg.getElementsByClassName(\"bdge-ttl-width\");" + "netdata_bdge_each(width_update_elems, \"width\", width_total);" + "this_svg.setAttribute(\"width\", width_total);" + "</script>", + BADGE_HORIZONTAL_PADDING); + } + buffer_sprintf(wb, "</svg>"); +} + +#define BADGE_URL_ARG_LBL_COLOR "text_color_lbl" +#define BADGE_URL_ARG_VAL_COLOR "text_color_val" + +int web_client_api_request_v1_badge(RRDHOST *host, struct web_client *w, char *url) { + int ret = HTTP_RESP_BAD_REQUEST; + buffer_flush(w->response.data); + + BUFFER *dimensions = NULL; + + const char *chart = NULL + , *before_str = NULL + , *after_str = NULL + , *points_str = NULL + , *multiply_str = NULL + , *divide_str = NULL + , *label = NULL + , *units = NULL + , *label_color = NULL + , *value_color = NULL + , *refresh_str = NULL + , *precision_str = NULL + , *scale_str = NULL + , *alarm = NULL + , *fixed_width_lbl_str = NULL + , *fixed_width_val_str = NULL + , *text_color_lbl_str = NULL + , *text_color_val_str = NULL + , *group_options = NULL; + + int group = RRDR_GROUPING_AVERAGE; + uint32_t options = 0x00000000; + + const RRDCALC_ACQUIRED *rca = NULL; + RRDCALC *rc = NULL; + RRDSET *st = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 badge.svg query param '%s' with value '%s'", w->id, name, value); + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "chart")) chart = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) + dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + else if(!strcmp(name, "after")) after_str = value; + else if(!strcmp(name, "before")) before_str = value; + else if(!strcmp(name, "points")) points_str = value; + else if(!strcmp(name, "group_options")) group_options = value; + else if(!strcmp(name, "group")) { + group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + } + else if(!strcmp(name, "options")) { + options |= rrdr_options_parse(value); + } + else if(!strcmp(name, "label")) label = value; + else if(!strcmp(name, "units")) units = value; + else if(!strcmp(name, "label_color")) label_color = value; + else if(!strcmp(name, "value_color")) value_color = value; + else if(!strcmp(name, "multiply")) multiply_str = value; + else if(!strcmp(name, "divide")) divide_str = value; + else if(!strcmp(name, "refresh")) refresh_str = value; + else if(!strcmp(name, "precision")) precision_str = value; + else if(!strcmp(name, "scale")) scale_str = value; + else if(!strcmp(name, "fixed_width_lbl")) fixed_width_lbl_str = value; + else if(!strcmp(name, "fixed_width_val")) fixed_width_val_str = value; + else if(!strcmp(name, "alarm")) alarm = value; + else if(!strcmp(name, BADGE_URL_ARG_LBL_COLOR)) text_color_lbl_str = value; + else if(!strcmp(name, BADGE_URL_ARG_VAL_COLOR)) text_color_val_str = value; + } + + int fixed_width_lbl = -1; + int fixed_width_val = -1; + + if(fixed_width_lbl_str && *fixed_width_lbl_str + && fixed_width_val_str && *fixed_width_val_str) { + fixed_width_lbl = str2i(fixed_width_lbl_str); + fixed_width_val = str2i(fixed_width_val_str); + } + + if(!chart || !*chart) { + buffer_no_cacheable(w->response.data); + buffer_sprintf(w->response.data, "No chart id is given at the request."); + goto cleanup; + } + + int scale = (scale_str && *scale_str)?str2i(scale_str):100; + + st = rrdset_find(host, chart); + if(!st) st = rrdset_find_byname(host, chart); + if(!st) { + buffer_no_cacheable(w->response.data); + buffer_svg(w->response.data, "chart not found", NAN, "", NULL, NULL, -1, scale, 0, -1, -1, NULL, NULL); + ret = HTTP_RESP_OK; + goto cleanup; + } + st->last_accessed_time_s = now_realtime_sec(); + + if(alarm) { + rca = rrdcalc_from_rrdset_get(st, alarm); + rc = rrdcalc_acquired_to_rrdcalc(rca); + + if (!rc) { + buffer_no_cacheable(w->response.data); + buffer_svg(w->response.data, "alarm not found", NAN, "", NULL, NULL, -1, scale, 0, -1, -1, NULL, NULL); + ret = HTTP_RESP_OK; + goto cleanup; + } + } + + long long multiply = (multiply_str && *multiply_str )?str2l(multiply_str):1; + long long divide = (divide_str && *divide_str )?str2l(divide_str):1; + long long before = (before_str && *before_str )?str2l(before_str):0; + long long after = (after_str && *after_str )?str2l(after_str):-st->update_every; + int points = (points_str && *points_str )?str2i(points_str):1; + int precision = (precision_str && *precision_str)?str2i(precision_str):-1; + + if(!multiply) multiply = 1; + if(!divide) divide = 1; + + int refresh = 0; + if(refresh_str && *refresh_str) { + if(!strcmp(refresh_str, "auto")) { + if(rc) refresh = rc->config.update_every; + else if(options & RRDR_OPTION_NOT_ALIGNED) + refresh = st->update_every; + else { + refresh = (int)(before - after); + if(refresh < 0) refresh = -refresh; + } + } + else { + refresh = str2i(refresh_str); + if(refresh < 0) refresh = -refresh; + } + } + + if(!label) { + if(alarm) { + char *s = (char *)alarm; + while(*s) { + if(*s == '_') *s = ' '; + s++; + } + label = alarm; + } + else if(dimensions) { + const char *dim = buffer_tostring(dimensions); + if(*dim == '|') dim++; + label = dim; + } + else + label = rrdset_name(st); + } + if(!units) { + if(alarm) { + if(rc->config.units) + units = rrdcalc_units(rc); + else + units = ""; + } + else if(options & RRDR_OPTION_PERCENTAGE) + units = "%"; + else + units = rrdset_units(st); + } + + netdata_log_debug(D_WEB_CLIENT, "%llu: API command 'badge.svg' for chart '%s', alarm '%s', dimensions '%s', after '%lld', before '%lld', points '%d', group '%d', options '0x%08x'" + , w->id + , chart + , alarm?alarm:"" + , (dimensions)?buffer_tostring(dimensions):"" + , after + , before + , points + , group + , options + ); + + if(rc) { + if (refresh > 0) { + buffer_sprintf(w->response.header, "Refresh: %d\r\n", refresh); + w->response.data->date = now_realtime_sec(); + w->response.data->expires = w->response.data->date + refresh; + buffer_cacheable(w->response.data); + } + else + buffer_no_cacheable(w->response.data); + + if(!value_color) { + switch(rc->status) { + case RRDCALC_STATUS_CRITICAL: + value_color = "red"; + break; + + case RRDCALC_STATUS_WARNING: + value_color = "orange"; + break; + + case RRDCALC_STATUS_CLEAR: + value_color = "brightgreen"; + break; + + case RRDCALC_STATUS_UNDEFINED: + value_color = "lightgrey"; + break; + + case RRDCALC_STATUS_UNINITIALIZED: + value_color = "#000"; + break; + + default: + value_color = "grey"; + break; + } + } + + buffer_svg(w->response.data, + label, + (isnan(rc->value)||isinf(rc->value)) ? rc->value : rc->value * multiply / divide, + units, + label_color, + value_color, + precision, + scale, + options, + fixed_width_lbl, + fixed_width_val, + text_color_lbl_str, + text_color_val_str + ); + ret = HTTP_RESP_OK; + } + else { + time_t latest_timestamp = 0; + int value_is_null = 1; + NETDATA_DOUBLE n = NAN; + ret = HTTP_RESP_INTERNAL_SERVER_ERROR; + + // if the collected value is too old, don't calculate its value + if (rrdset_last_entry_s(st) >= (now_realtime_sec() - (st->update_every * gap_when_lost_iterations_above))) + ret = rrdset2value_api_v1(st, w->response.data, &n, + (dimensions) ? buffer_tostring(dimensions) : NULL, + points, after, before, group, group_options, 0, options, + NULL, &latest_timestamp, + NULL, NULL, NULL, + &value_is_null, NULL, 0, 0, + QUERY_SOURCE_API_BADGE, STORAGE_PRIORITY_NORMAL); + + // if the value cannot be calculated, show empty badge + if (ret != HTTP_RESP_OK) { + buffer_no_cacheable(w->response.data); + value_is_null = 1; + n = 0; + ret = HTTP_RESP_OK; + } + else if (refresh > 0) { + buffer_sprintf(w->response.header, "Refresh: %d\r\n", refresh); + w->response.data->expires = now_realtime_sec() + refresh; + } + else buffer_no_cacheable(w->response.data); + + // render the badge + buffer_svg(w->response.data, + label, + (value_is_null)?NAN:(n * multiply / divide), + units, + label_color, + value_color, + precision, + scale, + options, + fixed_width_lbl, + fixed_width_val, + text_color_lbl_str, + text_color_val_str + ); + } + +cleanup: + rrdcalc_from_rrdset_release(st, rca); + buffer_free(dimensions); + return ret; +} diff --git a/src/web/api/badges/web_buffer_svg.h b/src/web/api/badges/web_buffer_svg.h new file mode 100644 index 000000000..71857811f --- /dev/null +++ b/src/web/api/badges/web_buffer_svg.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_BUFFER_SVG_H +#define NETDATA_WEB_BUFFER_SVG_H 1 + +#include "libnetdata/libnetdata.h" +#include "web/server/web_client.h" + +void buffer_svg(BUFFER *wb, const char *label, + NETDATA_DOUBLE value, const char *units, const char *label_color, const char *value_color, int precision, int scale, uint32_t options, int fixed_width_lbl, int fixed_width_val, const char* text_color_lbl, const char* text_color_val); +char *format_value_and_unit(char *value_string, size_t value_string_len, + NETDATA_DOUBLE value, const char *units, int precision); + +int web_client_api_request_v1_badge(struct rrdhost *host, struct web_client *w, char *url); + +#include "web/api/web_api_v1.h" + +#endif /* NETDATA_WEB_BUFFER_SVG_H */ diff --git a/src/web/api/exporters/README.md b/src/web/api/exporters/README.md new file mode 100644 index 000000000..206937967 --- /dev/null +++ b/src/web/api/exporters/README.md @@ -0,0 +1,14 @@ +<!-- +title: "Exporters" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/README.md +sidebar_label: "Exporters" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api" +--> + +# Exporters + +TBD + + diff --git a/src/web/api/exporters/allmetrics.c b/src/web/api/exporters/allmetrics.c new file mode 100644 index 000000000..cad52a7d5 --- /dev/null +++ b/src/web/api/exporters/allmetrics.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "allmetrics.h" + +struct prometheus_output_options { + char *name; + PROMETHEUS_OUTPUT_OPTIONS flag; +} prometheus_output_flags_root[] = { + { "help", PROMETHEUS_OUTPUT_HELP }, + { "types", PROMETHEUS_OUTPUT_TYPES }, + { "names", PROMETHEUS_OUTPUT_NAMES }, + { "timestamps", PROMETHEUS_OUTPUT_TIMESTAMPS }, + { "variables", PROMETHEUS_OUTPUT_VARIABLES }, + { "oldunits", PROMETHEUS_OUTPUT_OLDUNITS }, + { "hideunits", PROMETHEUS_OUTPUT_HIDEUNITS }, + // terminator + { NULL, PROMETHEUS_OUTPUT_NONE }, +}; + +inline int web_client_api_request_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url) { + int format = ALLMETRICS_SHELL; + const char *filter = NULL; + const char *prometheus_server = w->client_ip; + + uint32_t prometheus_exporting_options; + if (prometheus_exporter_instance) + prometheus_exporting_options = prometheus_exporter_instance->config.options; + else + prometheus_exporting_options = global_exporting_options; + + PROMETHEUS_OUTPUT_OPTIONS prometheus_output_options = + PROMETHEUS_OUTPUT_TIMESTAMPS | + ((prometheus_exporting_options & EXPORTING_OPTION_SEND_NAMES) ? PROMETHEUS_OUTPUT_NAMES : 0); + + const char *prometheus_prefix; + if (prometheus_exporter_instance) + prometheus_prefix = prometheus_exporter_instance->config.prefix; + else + prometheus_prefix = global_exporting_prefix; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + if(!strcmp(name, "format")) { + if(!strcmp(value, ALLMETRICS_FORMAT_SHELL)) + format = ALLMETRICS_SHELL; + else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS)) + format = ALLMETRICS_PROMETHEUS; + else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS)) + format = ALLMETRICS_PROMETHEUS_ALL_HOSTS; + else if(!strcmp(value, ALLMETRICS_FORMAT_JSON)) + format = ALLMETRICS_JSON; + else + format = 0; + } + else if(!strcmp(name, "filter")) { + filter = value; + } + else if(!strcmp(name, "server")) { + prometheus_server = value; + } + else if(!strcmp(name, "prefix")) { + prometheus_prefix = value; + } + else if(!strcmp(name, "data") || !strcmp(name, "source") || !strcmp(name, "data source") || !strcmp(name, "data-source") || !strcmp(name, "data_source") || !strcmp(name, "datasource")) { + prometheus_exporting_options = exporting_parse_data_source(value, prometheus_exporting_options); + } + else { + int i; + for(i = 0; prometheus_output_flags_root[i].name ; i++) { + if(!strcmp(name, prometheus_output_flags_root[i].name)) { + if(!strcmp(value, "yes") || !strcmp(value, "1") || !strcmp(value, "true")) + prometheus_output_options |= prometheus_output_flags_root[i].flag; + else + prometheus_output_options &= ~prometheus_output_flags_root[i].flag; + + break; + } + } + } + } + + buffer_flush(w->response.data); + buffer_no_cacheable(w->response.data); + + switch(format) { + case ALLMETRICS_JSON: + w->response.data->content_type = CT_APPLICATION_JSON; + rrd_stats_api_v1_charts_allmetrics_json(host, filter, w->response.data); + return HTTP_RESP_OK; + + case ALLMETRICS_SHELL: + w->response.data->content_type = CT_TEXT_PLAIN; + rrd_stats_api_v1_charts_allmetrics_shell(host, filter, w->response.data); + return HTTP_RESP_OK; + + case ALLMETRICS_PROMETHEUS: + w->response.data->content_type = CT_PROMETHEUS; + rrd_stats_api_v1_charts_allmetrics_prometheus_single_host( + host + , filter + , w->response.data + , prometheus_server + , prometheus_prefix + , prometheus_exporting_options + , prometheus_output_options + ); + return HTTP_RESP_OK; + + case ALLMETRICS_PROMETHEUS_ALL_HOSTS: + w->response.data->content_type = CT_PROMETHEUS; + rrd_stats_api_v1_charts_allmetrics_prometheus_all_hosts( + host + , filter + , w->response.data + , prometheus_server + , prometheus_prefix + , prometheus_exporting_options + , prometheus_output_options + ); + return HTTP_RESP_OK; + + default: + w->response.data->content_type = CT_TEXT_PLAIN; + buffer_strcat(w->response.data, "Which format? '" ALLMETRICS_FORMAT_SHELL "', '" ALLMETRICS_FORMAT_PROMETHEUS "', '" ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "' and '" ALLMETRICS_FORMAT_JSON "' are currently supported."); + return HTTP_RESP_BAD_REQUEST; + } +} diff --git a/src/web/api/exporters/allmetrics.h b/src/web/api/exporters/allmetrics.h new file mode 100644 index 000000000..3afc42e28 --- /dev/null +++ b/src/web/api/exporters/allmetrics.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_ALLMETRICS_H +#define NETDATA_API_ALLMETRICS_H + +#include "web/api/formatters/rrd2json.h" +#include "shell/allmetrics_shell.h" +#include "web/server/web_client.h" + +int web_client_api_request_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url); + +#endif //NETDATA_API_ALLMETRICS_H diff --git a/src/web/api/exporters/prometheus/README.md b/src/web/api/exporters/prometheus/README.md new file mode 100644 index 000000000..f4bb90a56 --- /dev/null +++ b/src/web/api/exporters/prometheus/README.md @@ -0,0 +1,14 @@ +<!-- +title: "Prometheus exporter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/prometheus/README.md +sidebar_label: "Prometheus exporter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Exporters" +--> + +# Prometheus exporter + +Read the Prometheus exporter documentation: [Using Netdata with Prometheus](https://github.com/netdata/netdata/blob/master/src/exporting/prometheus/README.md). + + diff --git a/src/web/api/exporters/shell/README.md b/src/web/api/exporters/shell/README.md new file mode 100644 index 000000000..86b774f1b --- /dev/null +++ b/src/web/api/exporters/shell/README.md @@ -0,0 +1,73 @@ +<!-- +title: "Shell exporter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/shell/README.md +sidebar_label: "Shell exporter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Exporters" +--> + +# Shell exporter + +Shell scripts can now query Netdata: + +```sh +eval "$(curl -s 'http://localhost:19999/api/v1/allmetrics')" +``` + +after this command, all the Netdata metrics are exposed to shell. Check: + +```sh +# source the metrics +eval "$(curl -s 'http://localhost:19999/api/v1/allmetrics')" + +# let's see if there are variables exposed by Netdata for system.cpu +set | grep "^NETDATA_SYSTEM_CPU" + +NETDATA_SYSTEM_CPU_GUEST=0 +NETDATA_SYSTEM_CPU_GUEST_NICE=0 +NETDATA_SYSTEM_CPU_IDLE=95 +NETDATA_SYSTEM_CPU_IOWAIT=0 +NETDATA_SYSTEM_CPU_IRQ=0 +NETDATA_SYSTEM_CPU_NICE=0 +NETDATA_SYSTEM_CPU_SOFTIRQ=0 +NETDATA_SYSTEM_CPU_STEAL=0 +NETDATA_SYSTEM_CPU_SYSTEM=1 +NETDATA_SYSTEM_CPU_USER=4 +NETDATA_SYSTEM_CPU_VISIBLETOTAL=5 + +# let's see the total cpu utilization of the system +echo ${NETDATA_SYSTEM_CPU_VISIBLETOTAL} +5 + +# what about alerts? +set | grep "^NETDATA_ALARM_SYSTEM_SWAP_" +NETDATA_ALARM_SYSTEM_SWAP_USED_SWAP_STATUS=CLEAR +NETDATA_ALARM_SYSTEM_SWAP_USED_SWAP_VALUE=51 + +# let's get the current status of the alert 'used swap' +echo ${NETDATA_ALARM_SYSTEM_SWAP_USED_SWAP_STATUS} +CLEAR + +# is it fast? +time curl -s 'http://localhost:19999/api/v1/allmetrics' >/dev/null + +real 0m0,070s +user 0m0,000s +sys 0m0,007s + +# it is... +# 0.07 seconds for curl to be loaded, connect to Netdata and fetch the response back... +``` + +The `_VISIBLETOTAL` variable sums up all the dimensions of each chart. + +The format of the variables is: + +```sh +NETDATA_${chart_id^^}_${dimension_id^^}="${value}" +``` + +The value is rounded to the closest integer, since shell script cannot process decimal numbers. + + diff --git a/src/web/api/exporters/shell/allmetrics_shell.c b/src/web/api/exporters/shell/allmetrics_shell.c new file mode 100644 index 000000000..c8248c148 --- /dev/null +++ b/src/web/api/exporters/shell/allmetrics_shell.c @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "allmetrics_shell.h" + +// ---------------------------------------------------------------------------- +// BASH +// /api/v1/allmetrics?format=bash + +static inline size_t shell_name_copy(char *d, const char *s, size_t usable) { + size_t n; + + for(n = 0; *s && n < usable ; d++, s++, n++) { + register char c = *s; + + if(unlikely(!isalnum(c))) *d = '_'; + else *d = (char)toupper(c); + } + *d = '\0'; + + return n; +} + +#define SHELL_ELEMENT_MAX 100 + +void rrd_stats_api_v1_charts_allmetrics_shell(RRDHOST *host, const char *filter_string, BUFFER *wb) { + analytics_log_shell(); + SIMPLE_PATTERN *filter = simple_pattern_create(filter_string, NULL, SIMPLE_PATTERN_EXACT, true); + + // for each chart + RRDSET *st; + rrdset_foreach_read(st, host) { + if (filter && !simple_pattern_matches_string(filter, st->name)) + continue; + if (rrdset_is_available_for_viewers(st)) { + NETDATA_DOUBLE total = 0.0; + + char chart[SHELL_ELEMENT_MAX + 1]; + shell_name_copy(chart, st->name ? rrdset_name(st) : rrdset_id(st), SHELL_ELEMENT_MAX); + + buffer_sprintf(wb, "\n# chart: %s (name: %s)\n", rrdset_id(st), rrdset_name(st)); + + // for each dimension + RRDDIM *rd; + rrddim_foreach_read(rd, st) { + if(rd->collector.counter && !rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE)) { + char dimension[SHELL_ELEMENT_MAX + 1]; + shell_name_copy(dimension, rd->name?rrddim_name(rd):rrddim_id(rd), SHELL_ELEMENT_MAX); + + NETDATA_DOUBLE n = rd->collector.last_stored_value; + + if(isnan(n) || isinf(n)) + buffer_sprintf(wb, "NETDATA_%s_%s=\"\" # %s\n", chart, dimension, rrdset_units(st)); + else { + if(rd->multiplier < 0 || rd->divisor < 0) n = -n; + n = roundndd(n); + if(!rrddim_option_check(rd, RRDDIM_OPTION_HIDDEN)) total += n; + buffer_sprintf(wb, "NETDATA_%s_%s=\"" NETDATA_DOUBLE_FORMAT_ZERO "\" # %s\n", chart, dimension, n, rrdset_units(st)); + } + } + } + rrddim_foreach_done(rd); + + total = roundndd(total); + buffer_sprintf(wb, "NETDATA_%s_VISIBLETOTAL=\"" NETDATA_DOUBLE_FORMAT_ZERO "\" # %s\n", chart, total, rrdset_units(st)); + } + } + rrdset_foreach_done(st); + + buffer_strcat(wb, "\n# NETDATA ALARMS RUNNING\n"); + + RRDCALC *rc; + foreach_rrdcalc_in_rrdhost_read(host, rc) { + if(!rc->rrdset) continue; + + char chart[SHELL_ELEMENT_MAX + 1]; + shell_name_copy(chart, rc->rrdset->name?rrdset_name(rc->rrdset):rrdset_id(rc->rrdset), SHELL_ELEMENT_MAX); + + char alarm[SHELL_ELEMENT_MAX + 1]; + shell_name_copy(alarm, rrdcalc_name(rc), SHELL_ELEMENT_MAX); + + NETDATA_DOUBLE n = rc->value; + + if(isnan(n) || isinf(n)) + buffer_sprintf(wb, "NETDATA_ALARM_%s_%s_VALUE=\"\" # %s\n", chart, alarm, rrdcalc_units(rc)); + else { + n = roundndd(n); + buffer_sprintf(wb, "NETDATA_ALARM_%s_%s_VALUE=\"" NETDATA_DOUBLE_FORMAT_ZERO "\" # %s\n", chart, alarm, n, rrdcalc_units(rc)); + } + + buffer_sprintf(wb, "NETDATA_ALARM_%s_%s_STATUS=\"%s\"\n", chart, alarm, rrdcalc_status2string(rc->status)); + } + foreach_rrdcalc_in_rrdhost_done(rc); + + simple_pattern_free(filter); +} + +// ---------------------------------------------------------------------------- + +void rrd_stats_api_v1_charts_allmetrics_json(RRDHOST *host, const char *filter_string, BUFFER *wb) { + analytics_log_json(); + SIMPLE_PATTERN *filter = simple_pattern_create(filter_string, NULL, SIMPLE_PATTERN_EXACT, true); + + buffer_strcat(wb, "{"); + + size_t chart_counter = 0; + size_t dimension_counter = 0; + + // for each chart + RRDSET *st; + rrdset_foreach_read(st, host) { + if (filter && !(simple_pattern_matches_string(filter, st->id) || simple_pattern_matches_string(filter, st->name))) + continue; + + if(rrdset_is_available_for_viewers(st)) { + buffer_sprintf( + wb, + "%s\n" + "\t\"%s\": {\n" + "\t\t\"name\":\"%s\",\n" + "\t\t\"family\":\"%s\",\n" + "\t\t\"context\":\"%s\",\n" + "\t\t\"units\":\"%s\",\n" + "\t\t\"last_updated\": %"PRId64",\n" + "\t\t\"dimensions\": {", + chart_counter ? "," : "", + rrdset_id(st), + rrdset_name(st), + rrdset_family(st), + rrdset_context(st), + rrdset_units(st), + (int64_t) rrdset_last_entry_s(st)); + + chart_counter++; + dimension_counter = 0; + + // for each dimension + RRDDIM *rd; + rrddim_foreach_read(rd, st) { + if(rd->collector.counter && !rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE)) { + buffer_sprintf( + wb, + "%s\n" + "\t\t\t\"%s\": {\n" + "\t\t\t\t\"name\": \"%s\",\n" + "\t\t\t\t\"value\": ", + dimension_counter ? "," : "", + rrddim_id(rd), + rrddim_name(rd)); + + if(isnan(rd->collector.last_stored_value)) + buffer_strcat(wb, "null"); + else + buffer_sprintf(wb, NETDATA_DOUBLE_FORMAT, rd->collector.last_stored_value); + + buffer_strcat(wb, "\n\t\t\t}"); + + dimension_counter++; + } + } + rrddim_foreach_done(rd); + + buffer_strcat(wb, "\n\t\t}\n\t}"); + } + } + rrdset_foreach_done(st); + + buffer_strcat(wb, "\n}"); + simple_pattern_free(filter); +} + diff --git a/src/web/api/exporters/shell/allmetrics_shell.h b/src/web/api/exporters/shell/allmetrics_shell.h new file mode 100644 index 000000000..d6598e08d --- /dev/null +++ b/src/web/api/exporters/shell/allmetrics_shell.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_ALLMETRICS_SHELL_H +#define NETDATA_API_ALLMETRICS_SHELL_H + +#include "../allmetrics.h" + +#define ALLMETRICS_FORMAT_SHELL "shell" +#define ALLMETRICS_FORMAT_PROMETHEUS "prometheus" +#define ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "prometheus_all_hosts" +#define ALLMETRICS_FORMAT_JSON "json" + +#define ALLMETRICS_SHELL 1 +#define ALLMETRICS_PROMETHEUS 2 +#define ALLMETRICS_JSON 3 +#define ALLMETRICS_PROMETHEUS_ALL_HOSTS 4 + +void rrd_stats_api_v1_charts_allmetrics_json(RRDHOST *host, const char *filter_string, BUFFER *wb); +void rrd_stats_api_v1_charts_allmetrics_shell(RRDHOST *host, const char *filter_string, BUFFER *wb); + +#endif //NETDATA_API_ALLMETRICS_SHELL_H diff --git a/src/web/api/formatters/README.md b/src/web/api/formatters/README.md new file mode 100644 index 000000000..df1ae7867 --- /dev/null +++ b/src/web/api/formatters/README.md @@ -0,0 +1,82 @@ +<!-- +title: "Query formatting" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/README.md +sidebar_label: "Query formatting" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Formatters" +--> + +# Query formatting + +API data queries need to be formatted before returned to the caller. +Using API parameters, the caller may define the format he/she wishes to get back. + +The following formats are supported: + +| format|module|content type|description| +|:----:|:----:|:----------:|:----------| +| `array`|[ssv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/ssv/README.md)|application/json|a JSON array| +| `csv`|[csv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/csv/README.md)|text/plain|a text table, comma separated, with a header line (dimension names) and `\r\n` at the end of the lines| +| `csvjsonarray`|[csv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/csv/README.md)|application/json|a JSON array, with each row as another array (the first row has the dimension names)| +| `datasource`|[json](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/json/README.md)|application/json|a Google Visualization Provider `datasource` javascript callback| +| `datatable`|[json](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/json/README.md)|application/json|a Google `datatable`| +| `html`|[csv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/csv/README.md)|text/html|an html table| +| `json`|[json](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/json/README.md)|application/json|a JSON object| +| `jsonp`|[json](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/json/README.md)|application/json|a JSONP javascript callback| +| `markdown`|[csv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/csv/README.md)|text/plain|a markdown table| +| `ssv`|[ssv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/ssv/README.md)|text/plain|a space separated list of values| +| `ssvcomma`|[ssv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/ssv/README.md)|text/plain|a comma separated list of values| +| `tsv`|[csv](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/csv/README.md)|text/plain|a TAB delimited `csv` (MS Excel flavor)| + +For examples of each format, check the relative module documentation. + +## Metadata with the `jsonwrap` option + +All data queries can be encapsulated to JSON object having metadata about the query and the results. + +This is done by adding the `options=jsonwrap` to the API URL (if there are other `options` append +`,jsonwrap` to the existing ones). + +This is such an object: + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=system.cpu&after=-3600&points=6&group=average&format=csv&options=nonzero,jsonwrap' +{ + "api": 1, + "id": "system.cpu", + "name": "system.cpu", + "view_update_every": 600, + "update_every": 1, + "first_entry": 1540387074, + "last_entry": 1540647070, + "before": 1540647000, + "after": 1540644000, + "dimension_names": ["steal", "softirq", "user", "system", "iowait"], + "dimension_ids": ["steal", "softirq", "user", "system", "iowait"], + "latest_values": [0, 0.2493766, 1.745636, 0.4987531, 0], + "view_latest_values": [0.0158314, 0.0516506, 0.866549, 0.7196127, 0.0050002], + "dimensions": 5, + "points": 6, + "format": "csv", + "result": "time,steal,softirq,user,system,iowait\n2018-10-27 13:30:00,0.0158314,0.0516506,0.866549,0.7196127,0.0050002\n2018-10-27 13:20:00,0.0149856,0.0529183,0.8673155,0.7121144,0.0049979\n2018-10-27 13:10:00,0.0137501,0.053315,0.8578097,0.7197613,0.0054209\n2018-10-27 13:00:00,0.0154252,0.0554688,0.899432,0.7200638,0.0067252\n2018-10-27 12:50:00,0.0145866,0.0495922,0.8404341,0.7011141,0.0041688\n2018-10-27 12:40:00,0.0162366,0.0595954,0.8827475,0.7020573,0.0041636\n", + "min": 0, + "max": 0 +} +``` + +## Downloading data query result files + +Following the [Google Visualization Provider guidelines](https://developers.google.com/chart/interactive/docs/dev/implementing_data_source), +Netdata supports parsing `tqx` options. + +Using these options, any Netdata data query can instruct the web browser to download +the result and save it under a given filename. + +For example, to download a CSV file with CPU utilization of the last hour, +[click here](https://registry.my-netdata.io/api/v1/data?chart=system.cpu&after=-3600&format=csv&options=nonzero&tqx=outFileName:system+cpu+utilization+of+the+last_hour.csv). + +This is done by appending `&tqx=outFileName:FILENAME` to any data query. +The output will be in the format given with `&format=`. + + diff --git a/src/web/api/formatters/charts2json.c b/src/web/api/formatters/charts2json.c new file mode 100644 index 000000000..cab4debae --- /dev/null +++ b/src/web/api/formatters/charts2json.c @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "charts2json.h" + +// generate JSON for the /api/v1/charts API call + +const char* get_release_channel() { + static int use_stable = -1; + + if (use_stable == -1) { + char filename[FILENAME_MAX + 1]; + snprintfz(filename, FILENAME_MAX, "%s/.environment", netdata_configured_user_config_dir); + procfile *ff = procfile_open(filename, "=", PROCFILE_FLAG_NO_ERROR_ON_FILE_IO); + if (ff) { + procfile_set_quotes(ff, "'\""); + ff = procfile_readall(ff); + if (ff) { + unsigned int i; + for (i = 0; i < procfile_lines(ff); i++) { + if (!procfile_linewords(ff, i)) + continue; + if (!strcmp(procfile_lineword(ff, i, 0), "RELEASE_CHANNEL")) { + if (!strcmp(procfile_lineword(ff, i, 1), "stable")) + use_stable = 1; + else if (!strcmp(procfile_lineword(ff, i, 1), "nightly")) + use_stable = 0; + break; + } + } + procfile_close(ff); + } + } + if (use_stable == -1) + use_stable = strchr(program_version, '-') ? 0 : 1; + } + return (use_stable)?"stable":"nightly"; +} + +void charts2json(RRDHOST *host, BUFFER *wb) { + static char *custom_dashboard_info_js_filename = NULL; + size_t c = 0, dimensions = 0, memory = 0, alarms = 0; + RRDSET *st; + + time_t now = now_realtime_sec(); + + if(unlikely(!custom_dashboard_info_js_filename)) + custom_dashboard_info_js_filename = config_get(CONFIG_SECTION_WEB, "custom dashboard_info.js", ""); + + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + + buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); + buffer_json_member_add_string(wb, "version", rrdhost_program_version(host)); + buffer_json_member_add_string(wb, "release_channel", get_release_channel()); + buffer_json_member_add_string(wb, "os", rrdhost_os(host)); + buffer_json_member_add_string(wb, "timezone", rrdhost_timezone(host)); + buffer_json_member_add_int64(wb, "update_every", host->rrd_update_every); + buffer_json_member_add_int64(wb, "history", host->rrd_history_entries); + buffer_json_member_add_string(wb, "memory_mode", rrd_memory_mode_name(host->rrd_memory_mode)); + buffer_json_member_add_string(wb, "custom_info", custom_dashboard_info_js_filename); + + buffer_json_member_add_object(wb, "charts"); + rrdset_foreach_read(st, host) { + if (rrdset_is_available_for_viewers(st)) { + + buffer_json_member_add_object(wb, rrdset_id(st)); + rrdset2json(st, wb, &dimensions, &memory); + buffer_json_object_close(wb); + st->last_accessed_time_s = now; + c++; + } + } + rrdset_foreach_done(st); + buffer_json_object_close(wb); + + RRDCALC *rc; + foreach_rrdcalc_in_rrdhost_read(host, rc) { + if(rc->rrdset) + alarms++; + } + foreach_rrdcalc_in_rrdhost_done(rc); + + buffer_json_member_add_int64(wb, "charts_count", (int64_t) c); + buffer_json_member_add_int64(wb, "dimensions_count", (int64_t) dimensions); + buffer_json_member_add_int64(wb, "alarms_count", (int64_t)alarms); + buffer_json_member_add_int64(wb, "rrd_memory_bytes", (int64_t)memory); + buffer_json_member_add_int64(wb, "hosts_count", (int64_t) rrdhost_hosts_available()); + + buffer_json_member_add_array(wb, "hosts"); + { + rrd_rdlock(); + RRDHOST *h; + rrdhost_foreach_read(h) { + if(!rrdhost_should_be_removed(h, host, now) /*&& !rrdhost_flag_check(h, RRDHOST_FLAG_ARCHIVED) */) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(h)); + buffer_json_object_close(wb); + } + } + rrd_unlock(); + } + buffer_json_array_close(wb); + + buffer_json_finalize(wb); +} diff --git a/src/web/api/formatters/charts2json.h b/src/web/api/formatters/charts2json.h new file mode 100644 index 000000000..7b07af5a0 --- /dev/null +++ b/src/web/api/formatters/charts2json.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_CHARTS2JSON_H +#define NETDATA_API_FORMATTER_CHARTS2JSON_H + +#include "rrd2json.h" + +void charts2json(RRDHOST *host, BUFFER *wb); +const char* get_release_channel(); + +#endif //NETDATA_API_FORMATTER_CHARTS2JSON_H diff --git a/src/web/api/formatters/csv/README.md b/src/web/api/formatters/csv/README.md new file mode 100644 index 000000000..ee5e3666f --- /dev/null +++ b/src/web/api/formatters/csv/README.md @@ -0,0 +1,148 @@ +<!-- +title: "CSV formatter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/csv/README.md +sidebar_label: "CSV formatter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Formatters" +--> + +# CSV formatter + +The CSV formatter presents [results of database queries](https://github.com/netdata/netdata/blob/master/src/web/api/queries/README.md) in the following formats: + +| format|content type|description| +| :----:|:----------:|:----------| +| `csv`|text/plain|a text table, comma separated, with a header line (dimension names) and `\r\n` at the end of the lines| +| `csvjsonarray`|application/json|a JSON array, with each row as another array (the first row has the dimension names)| +| `tsv`|text/plain|like `csv` but TAB is used instead of comma to separate values (MS Excel flavor)| +| `html`|text/html|an html table| +| `markdown`|text/plain|markdown table| + +In all formats the date and time is the first column. + +The CSV formatter respects the following API `&options=`: + +| option|supported|description| +|:----:|:-------:|:----------| +| `nonzero`|yes|to return only the dimensions that have at least a non-zero value| +| `flip`|yes|to return the rows older to newer (the default is newer to older)| +| `seconds`|yes|to return the date and time in unix timestamp| +| `ms`|yes|to return the date and time in unit timestamp as milliseconds| +| `percent`|yes|to replace all values with their percentage over the row total| +| `abs`|yes|to turn all values positive| +| `null2zero`|yes|to replace gaps with zeros (the default prints the string `null`| + +## Examples + +Get the system total bandwidth for all physical network interfaces, over the last hour, +in 6 rows (one for every 10 minutes), in `csv` format: + +Netdata always returns bandwidth in `kilobits`. + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=system.net&format=csv&after=-3600&group=sum&points=6&options=abs' +time,received,sent +2018-10-26 23:50:00,90214.67847,215137.79762 +2018-10-26 23:40:00,90126.32286,238587.57522 +2018-10-26 23:30:00,86061.22688,213389.23526 +2018-10-26 23:20:00,85590.75164,206129.01608 +2018-10-26 23:10:00,83163.30691,194311.77384 +2018-10-26 23:00:00,85167.29657,197538.07773 +``` + +--- + +Get the max RAM used by the SQL server and any cron jobs, over the last hour, in 2 rows (one for every 30 +minutes), in `tsv` format, and format the date and time as unix timestamp: + +Netdata always returns memory in `MB`. + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=apps.mem&format=tsv&after=-3600&group=max&points=2&options=nonzero,seconds&dimensions=sql,cron' +time sql cron +1540598400 61.95703 0.25 +1540596600 61.95703 0.25 +``` + +--- + +Get an HTML table of the last 4 values (4 seconds) of system CPU utilization: + +Netdata always returns CPU utilization as `%`. + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=system.cpu&format=html&after=-4&options=nonzero' +<html> +<center> +<table border="0" cellpadding="5" cellspacing="5"> +<tr><td>time</td><td>softirq</td><td>user</td><td>system</td></tr> +<tr><td>2018-10-27 00:16:07</td><td>0.25</td><td>1</td><td>0.75</td></tr> +<tr><td>2018-10-27 00:16:06</td><td>0</td><td>1.0025063</td><td>0.5012531</td></tr> +<tr><td>2018-10-27 00:16:05</td><td>0</td><td>1</td><td>0.75</td></tr> +<tr><td>2018-10-27 00:16:04</td><td>0</td><td>1.0025063</td><td>0.7518797</td></tr> +</table> +</center> +</html> +``` + +This is how it looks when rendered by a web browser: + +![image](https://user-images.githubusercontent.com/2662304/47597887-bafbf480-d99c-11e8-864a-d880bb8d2e5b.png) + +--- + +Get a JSON array with the average bandwidth rate of the mysql server, over the last hour, in 6 values +(one every 10 minutes), and return the date and time in milliseconds: + +Netdata always returns bandwidth rates in `kilobits/s`. + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=mysql_local.net&format=csvjsonarray&after=-3600&points=6&group=average&options=abs,ms' +[ +["time","in","out"], +[1540599600000,0.7499986,120.2810185], +[1540599000000,0.7500019,120.2815509], +[1540598400000,0.7499999,120.2812319], +[1540597800000,0.7500044,120.2819634], +[1540597200000,0.7499968,120.2807337], +[1540596600000,0.7499988,120.2810527] +] +``` + +--- + +Get the number of processes started per minute, for the last 10 minutes, in `markdown` format: + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=system.forks&format=markdown&after=-600&points=10&group=sum' +time | started +:---: |:---: +2018-10-27 03:52:00| 245.1706149 +2018-10-27 03:51:00| 152.6654636 +2018-10-27 03:50:00| 163.1755789 +2018-10-27 03:49:00| 176.1574766 +2018-10-27 03:48:00| 178.0137076 +2018-10-27 03:47:00| 183.8306543 +2018-10-27 03:46:00| 264.1635621 +2018-10-27 03:45:00| 205.001551 +2018-10-27 03:44:00| 7026.9852167 +2018-10-27 03:43:00| 205.9904794 +``` + +And this is how it looks when formatted: + +| time | started | +|:--:|:-----:| +| 2018-10-27 03:52:00 | 245.1706149 | +| 2018-10-27 03:51:00 | 152.6654636 | +| 2018-10-27 03:50:00 | 163.1755789 | +| 2018-10-27 03:49:00 | 176.1574766 | +| 2018-10-27 03:48:00 | 178.0137076 | +| 2018-10-27 03:47:00 | 183.8306543 | +| 2018-10-27 03:46:00 | 264.1635621 | +| 2018-10-27 03:45:00 | 205.001551 | +| 2018-10-27 03:44:00 | 7026.9852167 | +| 2018-10-27 03:43:00 | 205.9904794 | + + diff --git a/src/web/api/formatters/csv/csv.c b/src/web/api/formatters/csv/csv.c new file mode 100644 index 000000000..d81ddb34e --- /dev/null +++ b/src/web/api/formatters/csv/csv.c @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "libnetdata/libnetdata.h" +#include "csv.h" + +void rrdr2csv(RRDR *r, BUFFER *wb, uint32_t format, RRDR_OPTIONS options, const char *startline, const char *separator, const char *endline, const char *betweenlines) { + //netdata_log_info("RRD2CSV(): %s: BEGIN", r->st->id); + long c, i; + const long used = (long)r->d; + + // print the csv header + for(c = 0, i = 0; c < used ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + if(!i) { + buffer_strcat(wb, startline); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + buffer_strcat(wb, "time"); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + } + buffer_strcat(wb, separator); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + buffer_strcat(wb, string2str(r->dn[c])); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + i++; + } + buffer_strcat(wb, endline); + + if(format == DATASOURCE_CSV_MARKDOWN) { + // print the --- line after header + for(c = 0, i = 0; c < used ;c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + if(!i) { + buffer_strcat(wb, startline); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + buffer_strcat(wb, ":---:"); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + } + buffer_strcat(wb, separator); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + buffer_strcat(wb, ":---:"); + if(options & RRDR_OPTION_LABEL_QUOTES) buffer_strcat(wb, "\""); + i++; + } + buffer_strcat(wb, endline); + } + + if(!i) { + // no dimensions present + return; + } + + long start = 0, end = rrdr_rows(r), step = 1; + if(!(options & RRDR_OPTION_REVERSED)) { + start = rrdr_rows(r) - 1; + end = -1; + step = -1; + } + + // for each line in the array + for(i = start; i != end ;i += step) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + RRDR_VALUE_FLAGS *co = &r->o[ i * r->d ]; + + buffer_strcat(wb, betweenlines); + buffer_strcat(wb, startline); + + time_t now = r->t[i]; + + if((options & RRDR_OPTION_SECONDS) || (options & RRDR_OPTION_MILLISECONDS)) { + // print the timestamp of the line + buffer_print_netdata_double(wb, (NETDATA_DOUBLE) now); + // in ms + if(options & RRDR_OPTION_MILLISECONDS) buffer_strcat(wb, "000"); + } + else { + // generate the local date time + struct tm tmbuf, *tm = localtime_r(&now, &tmbuf); + if(!tm) { + netdata_log_error("localtime() failed."); continue; } + buffer_date(wb, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + } + + // for each dimension + for(c = 0; c < used ;c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_strcat(wb, separator); + + NETDATA_DOUBLE n = cn[c]; + + if(co[c] & RRDR_VALUE_EMPTY) { + if(options & RRDR_OPTION_NULL2ZERO) + buffer_strcat(wb, "0"); + else + buffer_strcat(wb, "null"); + } + else + buffer_print_netdata_double(wb, n); + } + + buffer_strcat(wb, endline); + } + //netdata_log_info("RRD2CSV(): %s: END", r->st->id); +} diff --git a/src/web/api/formatters/csv/csv.h b/src/web/api/formatters/csv/csv.h new file mode 100644 index 000000000..666d4c660 --- /dev/null +++ b/src/web/api/formatters/csv/csv.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_CSV_H +#define NETDATA_API_FORMATTER_CSV_H + +#include "web/api/queries/rrdr.h" + +void rrdr2csv(RRDR *r, BUFFER *wb, uint32_t format, RRDR_OPTIONS options, const char *startline, const char *separator, const char *endline, const char *betweenlines); + +#include "../rrd2json.h" + +#endif //NETDATA_API_FORMATTER_CSV_H diff --git a/src/web/api/formatters/json/README.md b/src/web/api/formatters/json/README.md new file mode 100644 index 000000000..b0037cb2e --- /dev/null +++ b/src/web/api/formatters/json/README.md @@ -0,0 +1,160 @@ +<!-- +title: "JSON formatter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/json/README.md +sidebar_label: "JSON formatter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Formatters" +--> + +# JSON formatter + +The CSV formatter presents [results of database queries](https://github.com/netdata/netdata/blob/master/src/web/api/queries/README.md) in the following formats: + +| format | content type | description| +|:----:|:----------:|:----------| +| `json` | application/json | return the query result as a json object| +| `jsonp` | application/json | return the query result as a JSONP javascript callback| +| `datatable` | application/json | return the query result as a Google `datatable`| +| `datasource` | application/json | return the query result as a Google Visualization Provider `datasource` javascript callback| + +The CSV formatter respects the following API `&options=`: + +| option | supported | description| +|:----:|:-------:|:----------| +| `google_json` | yes | enable the Google flavor of JSON (using double quotes for strings and `Date()` function for dates| +| `objectrows` | yes | return each row as an object, instead of an array| +| `nonzero` | yes | to return only the dimensions that have at least a non-zero value| +| `flip` | yes | to return the rows older to newer (the default is newer to older)| +| `seconds` | yes | to return the date and time in unix timestamp| +| `ms` | yes | to return the date and time in unit timestamp as milliseconds| +| `percent` | yes | to replace all values with their percentage over the row total| +| `abs` | yes | to turn all values positive| +| `null2zero` | yes | to replace gaps with zeros (the default prints the string `null`| + +## Examples + +To show the differences between each format, in the following examples we query the same +chart (having just one dimension called `active`), changing only the query `format` and its `options`. + +> Using `format=json` and `options=` + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&format=json&options=' +{ + "labels": ["time", "active"], + "data": + [ + [ 1540644600, 224.2516667], + [ 1540644000, 229.29], + [ 1540643400, 222.41], + [ 1540642800, 226.6816667], + [ 1540642200, 246.4083333], + [ 1540641600, 241.0966667] + ] +} +``` + +> Using `format=json` and `options=objectrows` + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&format=json&options=objectrows' +{ + "labels": ["time", "active"], + "data": + [ + { "time": 1540644600, "active": 224.2516667}, + { "time": 1540644000, "active": 229.29}, + { "time": 1540643400, "active": 222.41}, + { "time": 1540642800, "active": 226.6816667}, + { "time": 1540642200, "active": 246.4083333}, + { "time": 1540641600, "active": 241.0966667} + ] +} +``` + +> Using `format=json` and `options=objectrows,google_json` + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&formatjson&options=objectrows,google_json' +{ + "labels": ["time", "active"], + "data": + [ + { "time": new Date(2018,9,27,12,50,0), "active": 224.2516667}, + { "time": new Date(2018,9,27,12,40,0), "active": 229.29}, + { "time": new Date(2018,9,27,12,30,0), "active": 222.41}, + { "time": new Date(2018,9,27,12,20,0), "active": 226.6816667}, + { "time": new Date(2018,9,27,12,10,0), "active": 246.4083333}, + { "time": new Date(2018,9,27,12,0,0), "active": 241.0966667} + ] +} +``` + +> Using `format=jsonp` and `options=` + +```bash +curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&formjsonp&options=' +callback({ + "labels": ["time", "active"], + "data": + [ + [ 1540645200, 235.885], + [ 1540644600, 224.2516667], + [ 1540644000, 229.29], + [ 1540643400, 222.41], + [ 1540642800, 226.6816667], + [ 1540642200, 246.4083333] + ] +}); +``` + +> Using `format=datatable` and `options=` + +```bash +curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&formdatatable&options=' +{ + "cols": + [ + {"id":"","label":"time","pattern":"","type":"datetime"}, + {"id":"","label":"","pattern":"","type":"string","p":{"role":"annotation"}}, + {"id":"","label":"","pattern":"","type":"string","p":{"role":"annotationText"}}, + {"id":"","label":"active","pattern":"","type":"number"} + ], + "rows": + [ + {"c":[{"v":"Date(2018,9,27,13,0,0)"},{"v":null},{"v":null},{"v":235.885}]}, + {"c":[{"v":"Date(2018,9,27,12,50,0)"},{"v":null},{"v":null},{"v":224.2516667}]}, + {"c":[{"v":"Date(2018,9,27,12,40,0)"},{"v":null},{"v":null},{"v":229.29}]}, + {"c":[{"v":"Date(2018,9,27,12,30,0)"},{"v":null},{"v":null},{"v":222.41}]}, + {"c":[{"v":"Date(2018,9,27,12,20,0)"},{"v":null},{"v":null},{"v":226.6816667}]}, + {"c":[{"v":"Date(2018,9,27,12,10,0)"},{"v":null},{"v":null},{"v":246.4083333}]} + ] +} +``` + +> Using `format=datasource` and `options=` + +```bash +curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&after=-3600&points=6&group=average&format=datasource&options=' +google.visualization.Query.setResponse({version:'0.6',reqId:'0',status:'ok',sig:'1540645368',table:{ + "cols": + [ + {"id":"","label":"time","pattern":"","type":"datetime"}, + {"id":"","label":"","pattern":"","type":"string","p":{"role":"annotation"}}, + {"id":"","label":"","pattern":"","type":"string","p":{"role":"annotationText"}}, + {"id":"","label":"active","pattern":"","type":"number"} + ], + "rows": + [ + {"c":[{"v":"Date(2018,9,27,13,0,0)"},{"v":null},{"v":null},{"v":235.885}]}, + {"c":[{"v":"Date(2018,9,27,12,50,0)"},{"v":null},{"v":null},{"v":224.2516667}]}, + {"c":[{"v":"Date(2018,9,27,12,40,0)"},{"v":null},{"v":null},{"v":229.29}]}, + {"c":[{"v":"Date(2018,9,27,12,30,0)"},{"v":null},{"v":null},{"v":222.41}]}, + {"c":[{"v":"Date(2018,9,27,12,20,0)"},{"v":null},{"v":null},{"v":226.6816667}]}, + {"c":[{"v":"Date(2018,9,27,12,10,0)"},{"v":null},{"v":null},{"v":246.4083333}]} + ] +}}); +``` + + diff --git a/src/web/api/formatters/json/json.c b/src/web/api/formatters/json/json.c new file mode 100644 index 000000000..7e3f400e9 --- /dev/null +++ b/src/web/api/formatters/json/json.c @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "json.h" + +#define JSON_DATES_JS 1 +#define JSON_DATES_TIMESTAMP 2 + +void rrdr2json(RRDR *r, BUFFER *wb, RRDR_OPTIONS options, int datatable) { + //netdata_log_info("RRD2JSON(): %s: BEGIN", r->st->id); + int row_annotations = 0, dates, dates_with_new = 0; + char kq[2] = "", // key quote + sq[2] = "", // string quote + pre_label[101] = "", // before each label + post_label[101] = "", // after each label + pre_date[101] = "", // the beginning of line, to the date + post_date[101] = "", // closing the date + pre_value[101] = "", // before each value + post_value[101] = "", // after each value + post_line[101] = "", // at the end of each row + normal_annotation[201] = "", // default row annotation + overflow_annotation[201] = "", // overflow row annotation + data_begin[101] = "", // between labels and values + finish[101] = "", // at the end of everything + object_rows_time[101] = ""; + + if(datatable) { + dates = JSON_DATES_JS; + if( options & RRDR_OPTION_GOOGLE_JSON ) { + kq[0] = '\0'; + sq[0] = '\''; + } + else { + kq[0] = '"'; + sq[0] = '"'; + } + row_annotations = 1; + snprintfz(pre_date, 100, " {%sc%s:[{%sv%s:%s", kq, kq, kq, kq, sq); + snprintfz(post_date, 100, "%s}", sq); + snprintfz(pre_label, 100, ",\n {%sid%s:%s%s,%slabel%s:%s", kq, kq, sq, sq, kq, kq, sq); + snprintfz(post_label, 100, "%s,%spattern%s:%s%s,%stype%s:%snumber%s}", sq, kq, kq, sq, sq, kq, kq, sq, sq); + snprintfz(pre_value, 100, ",{%sv%s:", kq, kq); + strcpy(post_value, "}"); + strcpy(post_line, "]}"); + snprintfz(data_begin, 100, "\n ],\n %srows%s:\n [\n", kq, kq); + strcpy(finish, "\n ]\n }"); + + snprintfz(overflow_annotation, 200, ",{%sv%s:%sRESET OR OVERFLOW%s},{%sv%s:%sThe counters have been wrapped.%s}", kq, kq, sq, sq, kq, kq, sq, sq); + snprintfz(normal_annotation, 200, ",{%sv%s:null},{%sv%s:null}", kq, kq, kq, kq); + + buffer_sprintf(wb, "{\n %scols%s:\n [\n", kq, kq); + buffer_sprintf(wb, " {%sid%s:%s%s,%slabel%s:%stime%s,%spattern%s:%s%s,%stype%s:%sdatetime%s},\n", kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq); + buffer_sprintf(wb, " {%sid%s:%s%s,%slabel%s:%s%s,%spattern%s:%s%s,%stype%s:%sstring%s,%sp%s:{%srole%s:%sannotation%s}},\n", kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, kq, kq, sq, sq); + buffer_sprintf(wb, " {%sid%s:%s%s,%slabel%s:%s%s,%spattern%s:%s%s,%stype%s:%sstring%s,%sp%s:{%srole%s:%sannotationText%s}}", kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, sq, sq, kq, kq, kq, kq, sq, sq); + + // remove the valueobjects flag + // google wants its own keys + if(options & RRDR_OPTION_OBJECTSROWS) + options &= ~RRDR_OPTION_OBJECTSROWS; + } + else { + kq[0] = '"'; + sq[0] = '"'; + if(options & RRDR_OPTION_GOOGLE_JSON) { + dates = JSON_DATES_JS; + dates_with_new = 1; + } + else { + dates = JSON_DATES_TIMESTAMP; + dates_with_new = 0; + } + if( options & RRDR_OPTION_OBJECTSROWS ) + strcpy(pre_date, " {"); + else + strcpy(pre_date, " ["); + strcpy(pre_label, ",\""); + strcpy(post_label, "\""); + strcpy(pre_value, ","); + if( options & RRDR_OPTION_OBJECTSROWS ) + strcpy(post_line, "}"); + else + strcpy(post_line, "]"); + snprintfz(data_begin, 100, "],\n %sdata%s:[\n", kq, kq); + strcpy(finish, "\n ]\n }"); + + buffer_sprintf(wb, "{\n %slabels%s:[", kq, kq); + buffer_sprintf(wb, "%stime%s", sq, sq); + + if( options & RRDR_OPTION_OBJECTSROWS ) + snprintfz(object_rows_time, 100, "%stime%s: ", kq, kq); + + } + + size_t pre_value_len = strlen(pre_value); + size_t post_value_len = strlen(post_value); + size_t pre_label_len = strlen(pre_label); + size_t post_label_len = strlen(post_label); + size_t pre_date_len = strlen(pre_date); + size_t post_date_len = strlen(post_date); + size_t post_line_len = strlen(post_line); + size_t normal_annotation_len = strlen(normal_annotation); + size_t overflow_annotation_len = strlen(overflow_annotation); + size_t object_rows_time_len = strlen(object_rows_time); + + // ------------------------------------------------------------------------- + // print the JSON header + + long c, i; + const long used = (long)r->d; + + // print the header lines + for(c = 0, i = 0; c < used ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_fast_strcat(wb, pre_label, pre_label_len); + buffer_strcat(wb, string2str(r->dn[c])); + buffer_fast_strcat(wb, post_label, post_label_len); + i++; + } + + if(!i) { + buffer_fast_strcat(wb, pre_label, pre_label_len); + buffer_fast_strcat(wb, "no data", 7); + buffer_fast_strcat(wb, post_label, post_label_len); + } + size_t total_number_of_dimensions = i; + + // print the beginning of row data + buffer_strcat(wb, data_begin); + + // if all dimensions are hidden, print a null + if(!i) { + buffer_strcat(wb, finish); + return; + } + + long start = 0, end = rrdr_rows(r), step = 1; + if(!(options & RRDR_OPTION_REVERSED)) { + start = rrdr_rows(r) - 1; + end = -1; + step = -1; + } + + // pre-allocate a large enough buffer for us + // this does not need to be accurate - it is just a hint to avoid multiple realloc(). + buffer_need_bytes(wb, + ( 20 * rrdr_rows(r)) // timestamp + json overhead + + ( (pre_value_len + post_value_len + 4) * total_number_of_dimensions * rrdr_rows(r) ) // number + ); + + // for each line in the array + for(i = start; i != end ;i += step) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + RRDR_VALUE_FLAGS *co = &r->o[ i * r->d ]; + NETDATA_DOUBLE *ar = &r->ar[ i * r->d ]; + + time_t now = r->t[i]; + + if(dates == JSON_DATES_JS) { + // generate the local date time + struct tm tmbuf, *tm = localtime_r(&now, &tmbuf); + if(!tm) { + netdata_log_error("localtime_r() failed."); continue; } + + if(likely(i != start)) buffer_fast_strcat(wb, ",\n", 2); + buffer_fast_strcat(wb, pre_date, pre_date_len); + + if( options & RRDR_OPTION_OBJECTSROWS ) + buffer_fast_strcat(wb, object_rows_time, object_rows_time_len); + + if(unlikely(dates_with_new)) + buffer_fast_strcat(wb, "new ", 4); + + buffer_jsdate(wb, tm->tm_year + 1900, tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); + + buffer_fast_strcat(wb, post_date, post_date_len); + + if(unlikely(row_annotations)) { + // google supports one annotation per row + int annotation_found = 0; + for(c = 0; c < used ; c++) { + if(unlikely(!(r->od[c] & RRDR_DIMENSION_QUERIED))) continue; + + if(unlikely(co[c] & RRDR_VALUE_RESET)) { + buffer_fast_strcat(wb, overflow_annotation, overflow_annotation_len); + annotation_found = 1; + break; + } + } + if(likely(!annotation_found)) + buffer_fast_strcat(wb, normal_annotation, normal_annotation_len); + } + } + else { + // print the timestamp of the line + if(likely(i != start)) + buffer_fast_strcat(wb, ",\n", 2); + + buffer_fast_strcat(wb, pre_date, pre_date_len); + + if(unlikely( options & RRDR_OPTION_OBJECTSROWS )) + buffer_fast_strcat(wb, object_rows_time, object_rows_time_len); + + buffer_print_netdata_double(wb, (NETDATA_DOUBLE) r->t[i]); + + // in ms + if(unlikely(options & RRDR_OPTION_MILLISECONDS)) + buffer_fast_strcat(wb, "000", 3); + + buffer_fast_strcat(wb, post_date, post_date_len); + } + + // for each dimension + for(c = 0; c < used ;c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + NETDATA_DOUBLE n; + if(unlikely(options & RRDR_OPTION_INTERNAL_AR)) + n = ar[c]; + else + n = cn[c]; + + buffer_fast_strcat(wb, pre_value, pre_value_len); + + if(unlikely( options & RRDR_OPTION_OBJECTSROWS )) + buffer_sprintf(wb, "%s%s%s: ", kq, string2str(r->dn[c]), kq); + + if(co[c] & RRDR_VALUE_EMPTY && !(options & (RRDR_OPTION_INTERNAL_AR))) { + if(unlikely(options & RRDR_OPTION_NULL2ZERO)) + buffer_fast_strcat(wb, "0", 1); + else + buffer_fast_strcat(wb, "null", 4); + } + else + buffer_print_netdata_double(wb, n); + + buffer_fast_strcat(wb, post_value, post_value_len); + } + + buffer_fast_strcat(wb, post_line, post_line_len); + } + + buffer_strcat(wb, finish); + //netdata_log_info("RRD2JSON(): %s: END", r->st->id); +} + +void rrdr2json_v2(RRDR *r, BUFFER *wb) { + QUERY_TARGET *qt = r->internal.qt; + RRDR_OPTIONS options = qt->window.options; + + bool send_count = query_target_aggregatable(qt); + bool send_hidden = send_count && r->vh && query_has_group_by_aggregation_percentage(qt); + + buffer_json_member_add_object(wb, "result"); + + buffer_json_member_add_array(wb, "labels"); + buffer_json_add_array_item_string(wb, "time"); + long d, i; + const long used = (long)r->d; + for(d = 0, i = 0; d < used ; d++) { + if(!rrdr_dimension_should_be_exposed(r->od[d], options)) + continue; + + buffer_json_add_array_item_string(wb, string2str(r->di[d])); + i++; + } + buffer_json_array_close(wb); // labels + + buffer_json_member_add_object(wb, "point"); + { + size_t point_count = 0; + buffer_json_member_add_uint64(wb, "value", point_count++); + buffer_json_member_add_uint64(wb, "arp", point_count++); + buffer_json_member_add_uint64(wb, "pa", point_count++); + if (send_count) + buffer_json_member_add_uint64(wb, "count", point_count++); + if (send_hidden) + buffer_json_member_add_uint64(wb, "hidden", point_count++); + } + buffer_json_object_close(wb); // point + + buffer_json_member_add_array(wb, "data"); + if(i) { + long start = 0, end = rrdr_rows(r), step = 1; + if (!(options & RRDR_OPTION_REVERSED)) { + start = rrdr_rows(r) - 1; + end = -1; + step = -1; + } + + // for each line in the array + for (i = start; i != end; i += step) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + NETDATA_DOUBLE *ch = send_hidden ? &r->vh[i * r->d ] : NULL; + RRDR_VALUE_FLAGS *co = &r->o[ i * r->d ]; + NETDATA_DOUBLE *ar = &r->ar[ i * r->d ]; + uint32_t *gbc = &r->gbc [ i * r->d ]; + time_t now = r->t[i]; + + buffer_json_add_array_item_array(wb); // row + + if (options & RRDR_OPTION_MILLISECONDS) + buffer_json_add_array_item_time_ms(wb, now); // the time + else + buffer_json_add_array_item_time_t(wb, now); // the time + + for (d = 0; d < used; d++) { + if (!rrdr_dimension_should_be_exposed(r->od[d], options)) + continue; + + RRDR_VALUE_FLAGS o = co[d]; + + buffer_json_add_array_item_array(wb); // point + + // add the value + NETDATA_DOUBLE n = cn[d]; + + if(o & RRDR_VALUE_EMPTY) { + if (unlikely(options & RRDR_OPTION_NULL2ZERO)) + buffer_json_add_array_item_double(wb, 0); + else + buffer_json_add_array_item_double(wb, NAN); + } + else + buffer_json_add_array_item_double(wb, n); + + // add the anomaly + buffer_json_add_array_item_double(wb, ar[d]); + + // add the point annotations + buffer_json_add_array_item_uint64(wb, o); + + // add the count + if(send_count) + buffer_json_add_array_item_uint64(wb, gbc[d]); + if(send_hidden) + buffer_json_add_array_item_double(wb, ch[d]); + + buffer_json_array_close(wb); // point + } + + buffer_json_array_close(wb); // row + } + } + + buffer_json_array_close(wb); // data + + buffer_json_object_close(wb); // annotations +} diff --git a/src/web/api/formatters/json/json.h b/src/web/api/formatters/json/json.h new file mode 100644 index 000000000..d1ab4f901 --- /dev/null +++ b/src/web/api/formatters/json/json.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_JSON_H +#define NETDATA_API_FORMATTER_JSON_H + +#include "../rrd2json.h" + +void rrdr2json(RRDR *r, BUFFER *wb, RRDR_OPTIONS options, int datatable); +void rrdr2json_v2(RRDR *r, BUFFER *wb); + +#endif //NETDATA_API_FORMATTER_JSON_H diff --git a/src/web/api/formatters/json_wrapper.c b/src/web/api/formatters/json_wrapper.c new file mode 100644 index 000000000..fca5a0b83 --- /dev/null +++ b/src/web/api/formatters/json_wrapper.c @@ -0,0 +1,1578 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "json_wrapper.h" + +static void jsonwrap_query_metric_plan(BUFFER *wb, QUERY_METRIC *qm) { + buffer_json_member_add_array(wb, "plans"); + for (size_t p = 0; p < qm->plan.used; p++) { + QUERY_PLAN_ENTRY *qp = &qm->plan.array[p]; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_uint64(wb, "tr", qp->tier); + buffer_json_member_add_time_t(wb, "af", qp->after); + buffer_json_member_add_time_t(wb, "bf", qp->before); + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "tiers"); + for (size_t tier = 0; tier < storage_tiers; tier++) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_uint64(wb, "tr", tier); + buffer_json_member_add_time_t(wb, "fe", qm->tiers[tier].db_first_time_s); + buffer_json_member_add_time_t(wb, "le", qm->tiers[tier].db_last_time_s); + buffer_json_member_add_int64(wb, "wg", qm->tiers[tier].weight); + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); +} + +void jsonwrap_query_plan(RRDR *r, BUFFER *wb) { + QUERY_TARGET *qt = r->internal.qt; + + buffer_json_member_add_object(wb, "query_plan"); + for(size_t m = 0; m < qt->query.used; m++) { + QUERY_METRIC *qm = query_metric(qt, m); + buffer_json_member_add_object(wb, query_metric_id(qt, qm)); + jsonwrap_query_metric_plan(wb, qm); + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); +} + +static inline size_t rrdr_dimension_names(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + const size_t dimensions = r->d; + size_t c, i; + + buffer_json_member_add_array(wb, key); + for(c = 0, i = 0; c < dimensions ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_string(wb, string2str(r->dn[c])); + i++; + } + buffer_json_array_close(wb); + + return i; +} + +static inline size_t rrdr_dimension_ids(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + const size_t dimensions = r->d; + size_t c, i; + + buffer_json_member_add_array(wb, key); + for(c = 0, i = 0; c < dimensions ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_string(wb, string2str(r->di[c])); + i++; + } + buffer_json_array_close(wb); + + return i; +} + +static inline long jsonwrap_v1_chart_ids(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + QUERY_TARGET *qt = r->internal.qt; + const long query_used = qt->query.used; + long c, i; + + buffer_json_member_add_array(wb, key); + for (c = 0, i = 0; c < query_used; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + QUERY_METRIC *qm = query_metric(qt, c); + QUERY_INSTANCE *qi = query_instance(qt, qm->link.query_instance_id); + buffer_json_add_array_item_string(wb, rrdinstance_acquired_id(qi->ria)); + i++; + } + buffer_json_array_close(wb); + + return i; +} + +struct summary_total_counts { + size_t selected; + size_t excluded; + size_t queried; + size_t failed; +}; + +static inline void aggregate_into_summary_totals(struct summary_total_counts *totals, QUERY_METRICS_COUNTS *metrics) { + if(unlikely(!totals || !metrics)) + return; + + if(metrics->selected) { + totals->selected++; + + if(metrics->queried) + totals->queried++; + + else if(metrics->failed) + totals->failed++; + } + else + totals->excluded++; +} + +static inline void query_target_total_counts(BUFFER *wb, const char *key, struct summary_total_counts *totals) { + if(!totals->selected && !totals->queried && !totals->failed && !totals->excluded) + return; + + buffer_json_member_add_object(wb, key); + + if(totals->selected) + buffer_json_member_add_uint64(wb, "sl", totals->selected); + + if(totals->excluded) + buffer_json_member_add_uint64(wb, "ex", totals->excluded); + + if(totals->queried) + buffer_json_member_add_uint64(wb, "qr", totals->queried); + + if(totals->failed) + buffer_json_member_add_uint64(wb, "fl", totals->failed); + + buffer_json_object_close(wb); +} + +static inline void query_target_metric_counts(BUFFER *wb, QUERY_METRICS_COUNTS *metrics) { + if(!metrics->selected && !metrics->queried && !metrics->failed && !metrics->excluded) + return; + + buffer_json_member_add_object(wb, "ds"); + + if(metrics->selected) + buffer_json_member_add_uint64(wb, "sl", metrics->selected); + + if(metrics->excluded) + buffer_json_member_add_uint64(wb, "ex", metrics->excluded); + + if(metrics->queried) + buffer_json_member_add_uint64(wb, "qr", metrics->queried); + + if(metrics->failed) + buffer_json_member_add_uint64(wb, "fl", metrics->failed); + + buffer_json_object_close(wb); +} + +static inline void query_target_instance_counts(BUFFER *wb, QUERY_INSTANCES_COUNTS *instances) { + if(!instances->selected && !instances->queried && !instances->failed && !instances->excluded) + return; + + buffer_json_member_add_object(wb, "is"); + + if(instances->selected) + buffer_json_member_add_uint64(wb, "sl", instances->selected); + + if(instances->excluded) + buffer_json_member_add_uint64(wb, "ex", instances->excluded); + + if(instances->queried) + buffer_json_member_add_uint64(wb, "qr", instances->queried); + + if(instances->failed) + buffer_json_member_add_uint64(wb, "fl", instances->failed); + + buffer_json_object_close(wb); +} + +static inline void query_target_alerts_counts(BUFFER *wb, QUERY_ALERTS_COUNTS *alerts, const char *name, bool array) { + if(!alerts->clear && !alerts->other && !alerts->critical && !alerts->warning) + return; + + if(array) + buffer_json_add_array_item_object(wb); + else + buffer_json_member_add_object(wb, "al"); + + if(name) + buffer_json_member_add_string(wb, "nm", name); + + if(alerts->clear) + buffer_json_member_add_uint64(wb, "cl", alerts->clear); + + if(alerts->warning) + buffer_json_member_add_uint64(wb, "wr", alerts->warning); + + if(alerts->critical) + buffer_json_member_add_uint64(wb, "cr", alerts->critical); + + if(alerts->other) + buffer_json_member_add_uint64(wb, "ot", alerts->other); + + buffer_json_object_close(wb); +} + +static inline void query_target_points_statistics(BUFFER *wb, QUERY_TARGET *qt, STORAGE_POINT *sp) { + if(!sp->count) + return; + + buffer_json_member_add_object(wb, "sts"); + + buffer_json_member_add_double(wb, "min", sp->min); + buffer_json_member_add_double(wb, "max", sp->max); + + if(query_target_aggregatable(qt)) { + buffer_json_member_add_uint64(wb, "cnt", sp->count); + + if(sp->sum != 0.0) { + buffer_json_member_add_double(wb, "sum", sp->sum); + buffer_json_member_add_double(wb, "vol", sp->sum * (NETDATA_DOUBLE) query_view_update_every(qt)); + } + + if(sp->anomaly_count != 0) + buffer_json_member_add_uint64(wb, "arc", sp->anomaly_count); + } + else { + NETDATA_DOUBLE avg = (sp->count) ? sp->sum / (NETDATA_DOUBLE)sp->count : 0.0; + if(avg != 0.0) + buffer_json_member_add_double(wb, "avg", avg); + + NETDATA_DOUBLE arp = storage_point_anomaly_rate(*sp); + if(arp != 0.0) + buffer_json_member_add_double(wb, "arp", arp); + + NETDATA_DOUBLE con = (qt->query_points.sum > 0.0) ? sp->sum * 100.0 / qt->query_points.sum : 0.0; + if(con != 0.0) + buffer_json_member_add_double(wb, "con", con); + } + buffer_json_object_close(wb); +} + +static void query_target_summary_nodes_v2(BUFFER *wb, QUERY_TARGET *qt, const char *key, struct summary_total_counts *totals) { + buffer_json_member_add_array(wb, key); + for (size_t c = 0; c < qt->nodes.used; c++) { + QUERY_NODE *qn = query_node(qt, c); + RRDHOST *host = qn->rrdhost; + buffer_json_add_array_item_object(wb); + buffer_json_node_add_v2(wb, host, qn->slot, qn->duration_ut, true); + query_target_instance_counts(wb, &qn->instances); + query_target_metric_counts(wb, &qn->metrics); + query_target_alerts_counts(wb, &qn->alerts, NULL, false); + query_target_points_statistics(wb, qt, &qn->query_points); + buffer_json_object_close(wb); + + aggregate_into_summary_totals(totals, &qn->metrics); + } + buffer_json_array_close(wb); +} + +static size_t query_target_summary_contexts_v2(BUFFER *wb, QUERY_TARGET *qt, const char *key, struct summary_total_counts *totals) { + buffer_json_member_add_array(wb, key); + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + + struct { + STORAGE_POINT query_points; + QUERY_INSTANCES_COUNTS instances; + QUERY_METRICS_COUNTS metrics; + QUERY_ALERTS_COUNTS alerts; + } *z; + + for (long c = 0; c < (long) qt->contexts.used; c++) { + QUERY_CONTEXT *qc = query_context(qt, c); + + z = dictionary_set(dict, rrdcontext_acquired_id(qc->rca), NULL, sizeof(*z)); + + z->instances.selected += qc->instances.selected; + z->instances.excluded += qc->instances.selected; + z->instances.queried += qc->instances.queried; + z->instances.failed += qc->instances.failed; + + z->metrics.selected += qc->metrics.selected; + z->metrics.excluded += qc->metrics.excluded; + z->metrics.queried += qc->metrics.queried; + z->metrics.failed += qc->metrics.failed; + + z->alerts.clear += qc->alerts.clear; + z->alerts.warning += qc->alerts.warning; + z->alerts.critical += qc->alerts.critical; + + storage_point_merge_to(z->query_points, qc->query_points); + } + + size_t unique_contexts = dictionary_entries(dict); + dfe_start_read(dict, z) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", z_dfe.name); + query_target_instance_counts(wb, &z->instances); + query_target_metric_counts(wb, &z->metrics); + query_target_alerts_counts(wb, &z->alerts, NULL, false); + query_target_points_statistics(wb, qt, &z->query_points); + buffer_json_object_close(wb); + + aggregate_into_summary_totals(totals, &z->metrics); + } + dfe_done(z); + buffer_json_array_close(wb); + dictionary_destroy(dict); + + return unique_contexts; +} + +static void query_target_summary_instances_v1(BUFFER *wb, QUERY_TARGET *qt, const char *key) { + char name[RRD_ID_LENGTH_MAX * 2 + 2]; + + buffer_json_member_add_array(wb, key); + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + for (long c = 0; c < (long) qt->instances.used; c++) { + QUERY_INSTANCE *qi = query_instance(qt, c); + + snprintfz(name, RRD_ID_LENGTH_MAX * 2 + 1, "%s:%s", + rrdinstance_acquired_id(qi->ria), + rrdinstance_acquired_name(qi->ria)); + + bool *set = dictionary_set(dict, name, NULL, sizeof(*set)); + if (!*set) { + *set = true; + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, rrdinstance_acquired_id(qi->ria)); + buffer_json_add_array_item_string(wb, rrdinstance_acquired_name(qi->ria)); + buffer_json_array_close(wb); + } + } + dictionary_destroy(dict); + buffer_json_array_close(wb); +} + +static void query_target_summary_instances_v2(BUFFER *wb, QUERY_TARGET *qt, const char *key, struct summary_total_counts *totals) { + buffer_json_member_add_array(wb, key); + for (long c = 0; c < (long) qt->instances.used; c++) { + QUERY_INSTANCE *qi = query_instance(qt, c); +// QUERY_HOST *qh = query_host(qt, qi->query_host_id); + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", rrdinstance_acquired_id(qi->ria)); + + if(!rrdinstance_acquired_id_and_name_are_same(qi->ria)) + buffer_json_member_add_string(wb, "nm", rrdinstance_acquired_name(qi->ria)); + + buffer_json_member_add_uint64(wb, "ni", qi->query_host_id); +// buffer_json_member_add_string(wb, "id", string2str(qi->id_fqdn)); +// buffer_json_member_add_string(wb, "nm", string2str(qi->name_fqdn)); +// buffer_json_member_add_string(wb, "lc", rrdinstance_acquired_name(qi->ria)); +// buffer_json_member_add_string(wb, "mg", qh->host->machine_guid); +// if(qh->node_id[0]) +// buffer_json_member_add_string(wb, "nd", qh->node_id); + query_target_metric_counts(wb, &qi->metrics); + query_target_alerts_counts(wb, &qi->alerts, NULL, false); + query_target_points_statistics(wb, qt, &qi->query_points); + buffer_json_object_close(wb); + + aggregate_into_summary_totals(totals, &qi->metrics); + } + buffer_json_array_close(wb); +} + +struct dimensions_sorted_walkthrough_data { + BUFFER *wb; + struct summary_total_counts *totals; + QUERY_TARGET *qt; +}; + +struct dimensions_sorted_entry { + const char *id; + const char *name; + STORAGE_POINT query_points; + QUERY_METRICS_COUNTS metrics; + uint32_t priority; +}; + +static int dimensions_sorted_walktrhough_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + struct dimensions_sorted_walkthrough_data *sdwd = data; + BUFFER *wb = sdwd->wb; + struct summary_total_counts *totals = sdwd->totals; + QUERY_TARGET *qt = sdwd->qt; + struct dimensions_sorted_entry *z = value; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", z->id); + if (z->id != z->name && z->name) + buffer_json_member_add_string(wb, "nm", z->name); + + query_target_metric_counts(wb, &z->metrics); + query_target_points_statistics(wb, qt, &z->query_points); + buffer_json_member_add_uint64(wb, "pri", z->priority); + buffer_json_object_close(wb); + + aggregate_into_summary_totals(totals, &z->metrics); + + return 1; +} + +int dimensions_sorted_compar(const DICTIONARY_ITEM **item1, const DICTIONARY_ITEM **item2) { + struct dimensions_sorted_entry *z1 = dictionary_acquired_item_value(*item1); + struct dimensions_sorted_entry *z2 = dictionary_acquired_item_value(*item2); + + if(z1->priority == z2->priority) + return strcmp(dictionary_acquired_item_name(*item1), dictionary_acquired_item_name(*item2)); + else if(z1->priority < z2->priority) + return -1; + else + return 1; +} + +static void query_target_summary_dimensions_v12(BUFFER *wb, QUERY_TARGET *qt, const char *key, bool v2, struct summary_total_counts *totals) { + char buf[RRD_ID_LENGTH_MAX * 2 + 2]; + + buffer_json_member_add_array(wb, key); + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + struct dimensions_sorted_entry *z; + size_t q = 0; + for (long c = 0; c < (long) qt->dimensions.used; c++) { + QUERY_DIMENSION * qd = query_dimension(qt, c); + RRDMETRIC_ACQUIRED *rma = qd->rma; + + QUERY_METRIC *qm = NULL; + for( ; q < qt->query.used ;q++) { + QUERY_METRIC *tqm = query_metric(qt, q); + QUERY_DIMENSION *tqd = query_dimension(qt, tqm->link.query_dimension_id); + if(tqd->rma != rma) break; + qm = tqm; + } + + const char *k, *id, *name; + + if(v2) { + k = rrdmetric_acquired_name(rma); + id = k; + name = k; + } + else { + snprintfz(buf, RRD_ID_LENGTH_MAX * 2 + 1, "%s:%s", + rrdmetric_acquired_id(rma), + rrdmetric_acquired_name(rma)); + k = buf; + id = rrdmetric_acquired_id(rma); + name = rrdmetric_acquired_name(rma); + } + + z = dictionary_set(dict, k, NULL, sizeof(*z)); + if(!z->id) { + z->id = id; + z->name = name; + z->priority = qd->priority; + } + else { + if(qd->priority < z->priority) + z->priority = qd->priority; + } + + if(qm) { + z->metrics.selected += (qm->status & RRDR_DIMENSION_SELECTED) ? 1 : 0; + z->metrics.failed += (qm->status & RRDR_DIMENSION_FAILED) ? 1 : 0; + + if(qm->status & RRDR_DIMENSION_QUERIED) { + z->metrics.queried++; + storage_point_merge_to(z->query_points, qm->query_points); + } + } + else + z->metrics.excluded++; + } + + if(v2) { + struct dimensions_sorted_walkthrough_data t = { + .wb = wb, + .totals = totals, + .qt = qt, + }; + dictionary_sorted_walkthrough_rw(dict, DICTIONARY_LOCK_READ, dimensions_sorted_walktrhough_cb, + &t, dimensions_sorted_compar); + } + else { + // v1 + dfe_start_read(dict, z) { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, z->id); + buffer_json_add_array_item_string(wb, z->name); + buffer_json_array_close(wb); + } + dfe_done(z); + } + dictionary_destroy(dict); + buffer_json_array_close(wb); +} + +struct rrdlabels_formatting_v2 { + DICTIONARY *keys; + QUERY_INSTANCE *qi; + bool v2; +}; + +struct rrdlabels_keys_dict_entry { + const char *name; + DICTIONARY *values; + STORAGE_POINT query_points; + QUERY_METRICS_COUNTS metrics; +}; + +struct rrdlabels_key_value_dict_entry { + const char *key; + const char *value; + STORAGE_POINT query_points; + QUERY_METRICS_COUNTS metrics; +}; + +static int rrdlabels_formatting_v2(const char *name, const char *value, RRDLABEL_SRC ls __maybe_unused, void *data) { + struct rrdlabels_formatting_v2 *t = data; + + struct rrdlabels_keys_dict_entry *d = dictionary_set(t->keys, name, NULL, sizeof(*d)); + if(!d->values) { + d->name = name; + d->values = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + } + + char n[RRD_ID_LENGTH_MAX * 2 + 2]; + snprintfz(n, RRD_ID_LENGTH_MAX * 2, "%s:%s", name, value); + + struct rrdlabels_key_value_dict_entry *z = dictionary_set(d->values, n, NULL, sizeof(*z)); + if(!z->key) { + z->key = name; + z->value = value; + } + + if(t->v2) { + QUERY_INSTANCE *qi = t->qi; + + z->metrics.selected += qi->metrics.selected; + z->metrics.excluded += qi->metrics.excluded; + z->metrics.queried += qi->metrics.queried; + z->metrics.failed += qi->metrics.failed; + + d->metrics.selected += qi->metrics.selected; + d->metrics.excluded += qi->metrics.excluded; + d->metrics.queried += qi->metrics.queried; + d->metrics.failed += qi->metrics.failed; + + storage_point_merge_to(z->query_points, qi->query_points); + storage_point_merge_to(d->query_points, qi->query_points); + } + + return 1; +} + +static void query_target_summary_labels_v12(BUFFER *wb, QUERY_TARGET *qt, const char *key, bool v2, struct summary_total_counts *key_totals, struct summary_total_counts *value_totals) { + buffer_json_member_add_array(wb, key); + struct rrdlabels_formatting_v2 t = { + .keys = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE), + .v2 = v2, + }; + for (long c = 0; c < (long) qt->instances.used; c++) { + QUERY_INSTANCE *qi = query_instance(qt, c); + RRDINSTANCE_ACQUIRED *ria = qi->ria; + t.qi = qi; + rrdlabels_walkthrough_read(rrdinstance_acquired_labels(ria), rrdlabels_formatting_v2, &t); + } + struct rrdlabels_keys_dict_entry *d; + dfe_start_read(t.keys, d) { + if(v2) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", d_dfe.name); + query_target_metric_counts(wb, &d->metrics); + query_target_points_statistics(wb, qt, &d->query_points); + aggregate_into_summary_totals(key_totals, &d->metrics); + buffer_json_member_add_array(wb, "vl"); + } + struct rrdlabels_key_value_dict_entry *z; + dfe_start_read(d->values, z){ + if (v2) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", z->value); + query_target_metric_counts(wb, &z->metrics); + query_target_points_statistics(wb, qt, &z->query_points); + buffer_json_object_close(wb); + aggregate_into_summary_totals(value_totals, &z->metrics); + } else { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, z->key); + buffer_json_add_array_item_string(wb, z->value); + buffer_json_array_close(wb); + } + } + dfe_done(z); + dictionary_destroy(d->values); + if(v2) { + buffer_json_array_close(wb); + buffer_json_object_close(wb); + } + } + dfe_done(d); + dictionary_destroy(t.keys); + buffer_json_array_close(wb); +} + +static void query_target_summary_alerts_v2(BUFFER *wb, QUERY_TARGET *qt, const char *key) { + buffer_json_member_add_array(wb, key); + QUERY_ALERTS_COUNTS *z; + + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + for (long c = 0; c < (long) qt->instances.used; c++) { + QUERY_INSTANCE *qi = query_instance(qt, c); + RRDSET *st = rrdinstance_acquired_rrdset(qi->ria); + if (st) { + rw_spinlock_read_lock(&st->alerts.spinlock); + if (st->alerts.base) { + for (RRDCALC *rc = st->alerts.base; rc; rc = rc->next) { + z = dictionary_set(dict, string2str(rc->config.name), NULL, sizeof(*z)); + + switch(rc->status) { + case RRDCALC_STATUS_CLEAR: + z->clear++; + break; + + case RRDCALC_STATUS_WARNING: + z->warning++; + break; + + case RRDCALC_STATUS_CRITICAL: + z->critical++; + break; + + default: + case RRDCALC_STATUS_UNINITIALIZED: + case RRDCALC_STATUS_UNDEFINED: + case RRDCALC_STATUS_REMOVED: + z->other++; + break; + } + } + } + rw_spinlock_read_unlock(&st->alerts.spinlock); + } + } + dfe_start_read(dict, z) + query_target_alerts_counts(wb, z, z_dfe.name, true); + dfe_done(z); + dictionary_destroy(dict); + buffer_json_array_close(wb); // alerts +} + +static inline void query_target_functions(BUFFER *wb, const char *key, RRDR *r) { + QUERY_TARGET *qt = r->internal.qt; + const long query_used = qt->query.used; + + DICTIONARY *funcs = dictionary_create(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE); + RRDINSTANCE_ACQUIRED *ria = NULL; + for (long c = 0; c < query_used ; c++) { + QUERY_METRIC *qm = query_metric(qt, c); + QUERY_INSTANCE *qi = query_instance(qt, qm->link.query_instance_id); + if(qi->ria == ria) + continue; + + ria = qi->ria; + chart_functions_to_dict(rrdinstance_acquired_functions(ria), funcs, NULL, 0); + } + + buffer_json_member_add_array(wb, key); + void *t; (void)t; + dfe_start_read(funcs, t) + buffer_json_add_array_item_string(wb, t_dfe.name); + dfe_done(t); + dictionary_destroy(funcs); + buffer_json_array_close(wb); +} + +static inline long query_target_chart_labels_filter_v1(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + QUERY_TARGET *qt = r->internal.qt; + const long query_used = qt->query.used; + long c, i = 0; + + buffer_json_member_add_object(wb, key); + + SIMPLE_PATTERN *pattern = qt->instances.chart_label_key_pattern; + char *label_key = NULL; + while (pattern && (label_key = simple_pattern_iterate(&pattern))) { + buffer_json_member_add_array(wb, label_key); + + for (c = 0, i = 0; c < query_used; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + QUERY_METRIC *qm = query_metric(qt, c); + QUERY_INSTANCE *qi = query_instance(qt, qm->link.query_instance_id); + rrdlabels_value_to_buffer_array_item_or_null(rrdinstance_acquired_labels(qi->ria), wb, label_key); + i++; + } + buffer_json_array_close(wb); + } + + buffer_json_object_close(wb); + + return i; +} + +static inline long query_target_metrics_latest_values(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + QUERY_TARGET *qt = r->internal.qt; + const long query_used = qt->query.used; + long c, i; + + buffer_json_member_add_array(wb, key); + + for(c = 0, i = 0; c < query_used ;c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + QUERY_METRIC *qm = query_metric(qt, c); + QUERY_DIMENSION *qd = query_dimension(qt, qm->link.query_dimension_id); + buffer_json_add_array_item_double(wb, rrdmetric_acquired_last_stored_value(qd->rma)); + i++; + } + + buffer_json_array_close(wb); + + return i; +} + +static inline size_t rrdr_dimension_view_latest_values(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + buffer_json_member_add_array(wb, key); + + size_t c, i; + for(c = 0, i = 0; c < r->d ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + i++; + + NETDATA_DOUBLE *cn = &r->v[ (rrdr_rows(r) - 1) * r->d ]; + RRDR_VALUE_FLAGS *co = &r->o[ (rrdr_rows(r) - 1) * r->d ]; + NETDATA_DOUBLE n = cn[c]; + + if(co[c] & RRDR_VALUE_EMPTY) { + if(options & RRDR_OPTION_NULL2ZERO) + buffer_json_add_array_item_double(wb, 0.0); + else + buffer_json_add_array_item_double(wb, NAN); + } + else + buffer_json_add_array_item_double(wb, n); + } + + buffer_json_array_close(wb); + + return i; +} + +static inline void rrdr_dimension_query_points_statistics(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options, bool dview) { + STORAGE_POINT *sp = (dview) ? r->dview : r->dqp; + NETDATA_DOUBLE anomaly_rate_multiplier = (dview) ? RRDR_DVIEW_ANOMALY_COUNT_MULTIPLIER : 1.0; + + if(unlikely(!sp)) + return; + + if(key) + buffer_json_member_add_object(wb, key); + + buffer_json_member_add_array(wb, "min"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_double(wb, sp[c].min); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "max"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_double(wb, sp[c].max); + } + buffer_json_array_close(wb); + + if(options & RRDR_OPTION_RETURN_RAW) { + buffer_json_member_add_array(wb, "sum"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_double(wb, sp[c].sum); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "cnt"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_uint64(wb, sp[c].count); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "arc"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_uint64(wb, storage_point_anomaly_rate(sp[c]) / anomaly_rate_multiplier / 100.0 * sp[c].count); + } + buffer_json_array_close(wb); + } + else { + NETDATA_DOUBLE sum = 0.0; + for(size_t c = 0; c < r->d ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + sum += ABS(sp[c].sum); + } + + buffer_json_member_add_array(wb, "avg"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_double(wb, storage_point_average_value(sp[c])); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "arp"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_double(wb, storage_point_anomaly_rate(sp[c]) / anomaly_rate_multiplier); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "con"); + for(size_t c = 0; c < r->d ; c++) { + if (!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + NETDATA_DOUBLE con = (sum > 0.0) ? ABS(sp[c].sum) * 100.0 / sum : 0.0; + buffer_json_add_array_item_double(wb, con); + } + buffer_json_array_close(wb); + } + + if(key) + buffer_json_object_close(wb); +} + +void rrdr_json_wrapper_begin(RRDR *r, BUFFER *wb) { + QUERY_TARGET *qt = r->internal.qt; + DATASOURCE_FORMAT format = qt->request.format; + RRDR_OPTIONS options = qt->window.options; + + long rows = rrdr_rows(r); + + char kq[2] = "", // key quote + sq[2] = ""; // string quote + + if( options & RRDR_OPTION_GOOGLE_JSON ) { + kq[0] = '\0'; + sq[0] = '\''; + } + else { + kq[0] = '"'; + sq[0] = '"'; + } + + buffer_json_initialize( + wb, kq, sq, 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + + buffer_json_member_add_uint64(wb, "api", 1); + buffer_json_member_add_string(wb, "id", qt->id); + buffer_json_member_add_string(wb, "name", qt->id); + buffer_json_member_add_time_t(wb, "view_update_every", r->view.update_every); + buffer_json_member_add_time_t(wb, "update_every", qt->db.minimum_latest_update_every_s); + buffer_json_member_add_time_t(wb, "first_entry", qt->db.first_time_s); + buffer_json_member_add_time_t(wb, "last_entry", qt->db.last_time_s); + buffer_json_member_add_time_t(wb, "after", r->view.after); + buffer_json_member_add_time_t(wb, "before", r->view.before); + buffer_json_member_add_string(wb, "group", time_grouping_tostring(qt->request.time_group_method)); + rrdr_options_to_buffer_json_array(wb, "options", options); + + if(!rrdr_dimension_names(wb, "dimension_names", r, options)) + rows = 0; + + if(!rrdr_dimension_ids(wb, "dimension_ids", r, options)) + rows = 0; + + if (options & RRDR_OPTION_ALL_DIMENSIONS) { + query_target_summary_instances_v1(wb, qt, "full_chart_list"); + query_target_summary_dimensions_v12(wb, qt, "full_dimension_list", false, NULL); + query_target_summary_labels_v12(wb, qt, "full_chart_labels", false, NULL, NULL); + } + + query_target_functions(wb, "functions", r); + + if (!qt->request.st && !jsonwrap_v1_chart_ids(wb, "chart_ids", r, options)) + rows = 0; + + if (qt->instances.chart_label_key_pattern && !query_target_chart_labels_filter_v1(wb, "chart_labels", r, options)) + rows = 0; + + if(!query_target_metrics_latest_values(wb, "latest_values", r, options)) + rows = 0; + + size_t dimensions = rrdr_dimension_view_latest_values(wb, "view_latest_values", r, options); + if(!dimensions) + rows = 0; + + buffer_json_member_add_uint64(wb, "dimensions", dimensions); + buffer_json_member_add_uint64(wb, "points", rows); + buffer_json_member_add_string(wb, "format", rrdr_format_to_string(format)); + + buffer_json_member_add_array(wb, "db_points_per_tier"); + for(size_t tier = 0; tier < storage_tiers ; tier++) + buffer_json_add_array_item_uint64(wb, qt->db.tiers[tier].points); + buffer_json_array_close(wb); + + if(options & RRDR_OPTION_DEBUG) + jsonwrap_query_plan(r, wb); +} + +static void rrdset_rrdcalc_entries_v2(BUFFER *wb, RRDINSTANCE_ACQUIRED *ria) { + RRDSET *st = rrdinstance_acquired_rrdset(ria); + if(st) { + rw_spinlock_read_lock(&st->alerts.spinlock); + if(st->alerts.base) { + buffer_json_member_add_object(wb, "alerts"); + for(RRDCALC *rc = st->alerts.base; rc ;rc = rc->next) { + if(rc->status < RRDCALC_STATUS_CLEAR) + continue; + + buffer_json_member_add_object(wb, string2str(rc->config.name)); + buffer_json_member_add_string(wb, "st", rrdcalc_status2string(rc->status)); + buffer_json_member_add_double(wb, "vl", rc->value); + buffer_json_member_add_string(wb, "un", string2str(rc->config.units)); + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); + } + rw_spinlock_read_unlock(&st->alerts.spinlock); + } +} + +static void query_target_combined_units_v2(BUFFER *wb, QUERY_TARGET *qt, size_t contexts, bool ignore_percentage) { + if(!ignore_percentage && query_target_has_percentage_units(qt)) { + buffer_json_member_add_string(wb, "units", "%"); + } + else if(contexts == 1) { + buffer_json_member_add_string(wb, "units", rrdcontext_acquired_units(qt->contexts.array[0].rca)); + } + else if(contexts > 1) { + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + for(size_t c = 0; c < qt->contexts.used ;c++) + dictionary_set(dict, rrdcontext_acquired_units(qt->contexts.array[c].rca), NULL, 0); + + if(dictionary_entries(dict) == 1) + buffer_json_member_add_string(wb, "units", rrdcontext_acquired_units(qt->contexts.array[0].rca)); + else { + buffer_json_member_add_array(wb, "units"); + const char *s; + dfe_start_read(dict, s) + buffer_json_add_array_item_string(wb, s_dfe.name); + dfe_done(s); + buffer_json_array_close(wb); + } + dictionary_destroy(dict); + } +} + +static void query_target_combined_chart_type(BUFFER *wb, QUERY_TARGET *qt, size_t contexts) { + if(contexts >= 1) + buffer_json_member_add_string(wb, "chart_type", rrdset_type_name(rrdcontext_acquired_chart_type(qt->contexts.array[0].rca))); +} + +static void rrdr_grouped_by_array_v2(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options __maybe_unused) { + QUERY_TARGET *qt = r->internal.qt; + + buffer_json_member_add_array(wb, key); + + // find the deeper group-by + ssize_t g = 0; + for(g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if(qt->request.group_by[g].group_by == RRDR_GROUP_BY_NONE) + break; + } + + if(g > 0) + g--; + + RRDR_GROUP_BY group_by = qt->request.group_by[g].group_by; + + if(group_by & RRDR_GROUP_BY_SELECTED) + buffer_json_add_array_item_string(wb, "selected"); + + else if(group_by & RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE) + buffer_json_add_array_item_string(wb, "percentage-of-instance"); + + else { + + if(group_by & RRDR_GROUP_BY_DIMENSION) + buffer_json_add_array_item_string(wb, "dimension"); + + if(group_by & RRDR_GROUP_BY_INSTANCE) + buffer_json_add_array_item_string(wb, "instance"); + + if(group_by & RRDR_GROUP_BY_LABEL) { + BUFFER *b = buffer_create(0, NULL); + for (size_t l = 0; l < qt->group_by[g].used; l++) { + buffer_flush(b); + buffer_fast_strcat(b, "label:", 6); + buffer_strcat(b, qt->group_by[g].label_keys[l]); + buffer_json_add_array_item_string(wb, buffer_tostring(b)); + } + buffer_free(b); + } + + if(group_by & RRDR_GROUP_BY_NODE) + buffer_json_add_array_item_string(wb, "node"); + + if(group_by & RRDR_GROUP_BY_CONTEXT) + buffer_json_add_array_item_string(wb, "context"); + + if(group_by & RRDR_GROUP_BY_UNITS) + buffer_json_add_array_item_string(wb, "units"); + } + + buffer_json_array_close(wb); // group_by_order +} + +static void rrdr_dimension_units_array_v2(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options, bool ignore_percentage) { + if(!r->du) + return; + + bool percentage = !ignore_percentage && query_target_has_percentage_units(r->internal.qt); + + buffer_json_member_add_array(wb, key); + for(size_t c = 0; c < r->d ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + if(percentage) + buffer_json_add_array_item_string(wb, "%"); + else + buffer_json_add_array_item_string(wb, string2str(r->du[c])); + } + buffer_json_array_close(wb); +} + +static void rrdr_dimension_priority_array_v2(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + if(!r->dp) + return; + + buffer_json_member_add_array(wb, key); + for(size_t c = 0; c < r->d ; c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_uint64(wb, r->dp[c]); + } + buffer_json_array_close(wb); +} + +static void rrdr_dimension_aggregated_array_v2(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + if(!r->dgbc) + return; + + buffer_json_member_add_array(wb, key); + for(size_t c = 0; c < r->d ;c++) { + if(!rrdr_dimension_should_be_exposed(r->od[c], options)) + continue; + + buffer_json_add_array_item_uint64(wb, r->dgbc[c]); + } + buffer_json_array_close(wb); +} + +static void query_target_title(BUFFER *wb, QUERY_TARGET *qt, size_t contexts) { + if(contexts == 1) { + buffer_json_member_add_string(wb, "title", rrdcontext_acquired_title(qt->contexts.array[0].rca)); + } + else if(contexts > 1) { + BUFFER *t = buffer_create(0, NULL); + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + + buffer_strcat(t, "Chart for contexts: "); + + size_t added = 0; + for(size_t c = 0; c < qt->contexts.used ;c++) { + bool *set = dictionary_set(dict, rrdcontext_acquired_id(qt->contexts.array[c].rca), NULL, sizeof(*set)); + if(!*set) { + *set = true; + if(added) + buffer_fast_strcat(t, ", ", 2); + + buffer_strcat(t, rrdcontext_acquired_id(qt->contexts.array[c].rca)); + added++; + } + } + buffer_json_member_add_string(wb, "title", buffer_tostring(t)); + dictionary_destroy(dict); + buffer_free(t); + } +} + +static void query_target_detailed_objects_tree(BUFFER *wb, RRDR *r, RRDR_OPTIONS options) { + QUERY_TARGET *qt = r->internal.qt; + buffer_json_member_add_object(wb, "nodes"); + + time_t now_s = now_realtime_sec(); + RRDHOST *last_host = NULL; + RRDCONTEXT_ACQUIRED *last_rca = NULL; + RRDINSTANCE_ACQUIRED *last_ria = NULL; + + size_t h = 0, c = 0, i = 0, m = 0, q = 0; + for(; h < qt->nodes.used ; h++) { + QUERY_NODE *qn = query_node(qt, h); + RRDHOST *host = qn->rrdhost; + + for( ;c < qt->contexts.used ;c++) { + QUERY_CONTEXT *qc = query_context(qt, c); + RRDCONTEXT_ACQUIRED *rca = qc->rca; + if(!rrdcontext_acquired_belongs_to_host(rca, host)) break; + + for( ;i < qt->instances.used ;i++) { + QUERY_INSTANCE *qi = query_instance(qt, i); + RRDINSTANCE_ACQUIRED *ria = qi->ria; + if(!rrdinstance_acquired_belongs_to_context(ria, rca)) break; + + for( ; m < qt->dimensions.used ; m++) { + QUERY_DIMENSION *qd = query_dimension(qt, m); + RRDMETRIC_ACQUIRED *rma = qd->rma; + if(!rrdmetric_acquired_belongs_to_instance(rma, ria)) break; + + QUERY_METRIC *qm = NULL; + bool queried = false; + for( ; q < qt->query.used ;q++) { + QUERY_METRIC *tqm = query_metric(qt, q); + QUERY_DIMENSION *tqd = query_dimension(qt, tqm->link.query_dimension_id); + if(tqd->rma != rma) break; + + queried = tqm->status & RRDR_DIMENSION_QUERIED; + qm = tqm; + } + + if(!queried & !(options & RRDR_OPTION_ALL_DIMENSIONS)) + continue; + + if(host != last_host) { + if(last_host) { + if(last_rca) { + if(last_ria) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // instance + last_ria = NULL; + } + buffer_json_object_close(wb); // instances + buffer_json_object_close(wb); // context + last_rca = NULL; + } + buffer_json_object_close(wb); // contexts + buffer_json_object_close(wb); // host + last_host = NULL; + } + + buffer_json_member_add_object(wb, host->machine_guid); + if(qn->node_id[0]) + buffer_json_member_add_string(wb, "nd", qn->node_id); + buffer_json_member_add_uint64(wb, "ni", qn->slot); + buffer_json_member_add_string(wb, "nm", rrdhost_hostname(host)); + buffer_json_member_add_object(wb, "contexts"); + + last_host = host; + } + + if(rca != last_rca) { + if(last_rca) { + if(last_ria) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // instance + last_ria = NULL; + } + buffer_json_object_close(wb); // instances + buffer_json_object_close(wb); // context + last_rca = NULL; + } + + buffer_json_member_add_object(wb, rrdcontext_acquired_id(rca)); + buffer_json_member_add_object(wb, "instances"); + + last_rca = rca; + } + + if(ria != last_ria) { + if(last_ria) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // instance + last_ria = NULL; + } + + buffer_json_member_add_object(wb, rrdinstance_acquired_id(ria)); + buffer_json_member_add_string(wb, "nm", rrdinstance_acquired_name(ria)); + buffer_json_member_add_time_t(wb, "ue", rrdinstance_acquired_update_every(ria)); + RRDLABELS *labels = rrdinstance_acquired_labels(ria); + if(labels) { + buffer_json_member_add_object(wb, "labels"); + rrdlabels_to_buffer_json_members(labels, wb); + buffer_json_object_close(wb); + } + rrdset_rrdcalc_entries_v2(wb, ria); + buffer_json_member_add_object(wb, "dimensions"); + + last_ria = ria; + } + + buffer_json_member_add_object(wb, rrdmetric_acquired_id(rma)); + { + buffer_json_member_add_string(wb, "nm", rrdmetric_acquired_name(rma)); + buffer_json_member_add_uint64(wb, "qr", queried ? 1 : 0); + time_t first_entry_s = rrdmetric_acquired_first_entry(rma); + time_t last_entry_s = rrdmetric_acquired_last_entry(rma); + buffer_json_member_add_time_t(wb, "fe", first_entry_s); + buffer_json_member_add_time_t(wb, "le", last_entry_s ? last_entry_s : now_s); + + if(qm) { + if(qm->status & RRDR_DIMENSION_GROUPED) { + // buffer_json_member_add_string(wb, "grouped_as_id", string2str(qm->grouped_as.id)); + buffer_json_member_add_string(wb, "as", string2str(qm->grouped_as.name)); + } + + query_target_points_statistics(wb, qt, &qm->query_points); + + if(options & RRDR_OPTION_DEBUG) + jsonwrap_query_metric_plan(wb, qm); + } + } + buffer_json_object_close(wb); // metric + } + } + } + } + + if(last_host) { + if(last_rca) { + if(last_ria) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // instance + last_ria = NULL; + } + buffer_json_object_close(wb); // instances + buffer_json_object_close(wb); // context + last_rca = NULL; + } + buffer_json_object_close(wb); // contexts + buffer_json_object_close(wb); // host + last_host = NULL; + } + buffer_json_object_close(wb); // hosts +} + +void version_hashes_api_v2(BUFFER *wb, struct query_versions *versions) { + buffer_json_member_add_object(wb, "versions"); + buffer_json_member_add_uint64(wb, "routing_hard_hash", 1); + buffer_json_member_add_uint64(wb, "nodes_hard_hash", dictionary_version(rrdhost_root_index)); + buffer_json_member_add_uint64(wb, "contexts_hard_hash", versions->contexts_hard_hash); + buffer_json_member_add_uint64(wb, "contexts_soft_hash", versions->contexts_soft_hash); + buffer_json_member_add_uint64(wb, "alerts_hard_hash", versions->alerts_hard_hash); + buffer_json_member_add_uint64(wb, "alerts_soft_hash", versions->alerts_soft_hash); + buffer_json_object_close(wb); +} + +void rrdr_json_wrapper_begin2(RRDR *r, BUFFER *wb) { + QUERY_TARGET *qt = r->internal.qt; + RRDR_OPTIONS options = qt->window.options; + + char kq[2] = "\"", // key quote + sq[2] = "\""; // string quote + + if(unlikely(options & RRDR_OPTION_GOOGLE_JSON)) { + kq[0] = '\0'; + sq[0] = '\''; + } + + buffer_json_initialize( + wb, kq, sq, 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_uint64(wb, "api", 2); + + if(options & RRDR_OPTION_DEBUG) { + buffer_json_member_add_string(wb, "id", qt->id); + buffer_json_member_add_object(wb, "request"); + { + buffer_json_member_add_string(wb, "format", rrdr_format_to_string(qt->request.format)); + rrdr_options_to_buffer_json_array(wb, "options", qt->request.options); + + buffer_json_member_add_object(wb, "scope"); + buffer_json_member_add_string(wb, "scope_nodes", qt->request.scope_nodes); + buffer_json_member_add_string(wb, "scope_contexts", qt->request.scope_contexts); + buffer_json_object_close(wb); // scope + + buffer_json_member_add_object(wb, "selectors"); + if (qt->request.host) + buffer_json_member_add_string(wb, "nodes", rrdhost_hostname(qt->request.host)); + else + buffer_json_member_add_string(wb, "nodes", qt->request.nodes); + buffer_json_member_add_string(wb, "contexts", qt->request.contexts); + buffer_json_member_add_string(wb, "instances", qt->request.instances); + buffer_json_member_add_string(wb, "dimensions", qt->request.dimensions); + buffer_json_member_add_string(wb, "labels", qt->request.labels); + buffer_json_member_add_string(wb, "alerts", qt->request.alerts); + buffer_json_object_close(wb); // selectors + + buffer_json_member_add_object(wb, "window"); + buffer_json_member_add_time_t(wb, "after", qt->request.after); + buffer_json_member_add_time_t(wb, "before", qt->request.before); + buffer_json_member_add_uint64(wb, "points", qt->request.points); + if (qt->request.options & RRDR_OPTION_SELECTED_TIER) + buffer_json_member_add_uint64(wb, "tier", qt->request.tier); + else + buffer_json_member_add_string(wb, "tier", NULL); + buffer_json_object_close(wb); // window + + buffer_json_member_add_object(wb, "aggregations"); + { + buffer_json_member_add_object(wb, "time"); + buffer_json_member_add_string(wb, "time_group", time_grouping_tostring(qt->request.time_group_method)); + buffer_json_member_add_string(wb, "time_group_options", qt->request.time_group_options); + if (qt->request.resampling_time > 0) + buffer_json_member_add_time_t(wb, "time_resampling", qt->request.resampling_time); + else + buffer_json_member_add_string(wb, "time_resampling", NULL); + buffer_json_object_close(wb); // time + + buffer_json_member_add_array(wb, "metrics"); + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if(qt->request.group_by[g].group_by == RRDR_GROUP_BY_NONE) + break; + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_array(wb, "group_by"); + buffer_json_group_by_to_array(wb, qt->request.group_by[g].group_by); + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "group_by_label"); + for (size_t l = 0; l < qt->group_by[g].used; l++) + buffer_json_add_array_item_string(wb, qt->group_by[g].label_keys[l]); + buffer_json_array_close(wb); + + buffer_json_member_add_string( + wb, "aggregation",group_by_aggregate_function_to_string(qt->request.group_by[g].aggregation)); + } + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); // group_by + } + buffer_json_object_close(wb); // aggregations + + buffer_json_member_add_uint64(wb, "timeout", qt->request.timeout_ms); + } + buffer_json_object_close(wb); // request + } + + version_hashes_api_v2(wb, &qt->versions); + + buffer_json_member_add_object(wb, "summary"); + struct summary_total_counts + nodes_totals = { 0 }, + contexts_totals = { 0 }, + instances_totals = { 0 }, + metrics_totals = { 0 }, + label_key_totals = { 0 }, + label_key_value_totals = { 0 }; + { + query_target_summary_nodes_v2(wb, qt, "nodes", &nodes_totals); + r->internal.contexts = query_target_summary_contexts_v2(wb, qt, "contexts", &contexts_totals); + query_target_summary_instances_v2(wb, qt, "instances", &instances_totals); + query_target_summary_dimensions_v12(wb, qt, "dimensions", true, &metrics_totals); + query_target_summary_labels_v12(wb, qt, "labels", true, &label_key_totals, &label_key_value_totals); + query_target_summary_alerts_v2(wb, qt, "alerts"); + } + if(query_target_aggregatable(qt)) { + buffer_json_member_add_object(wb, "globals"); + query_target_points_statistics(wb, qt, &qt->query_points); + buffer_json_object_close(wb); // globals + } + buffer_json_object_close(wb); // summary + + buffer_json_member_add_object(wb, "totals"); + query_target_total_counts(wb, "nodes", &nodes_totals); + query_target_total_counts(wb, "contexts", &contexts_totals); + query_target_total_counts(wb, "instances", &instances_totals); + query_target_total_counts(wb, "dimensions", &metrics_totals); + query_target_total_counts(wb, "label_keys", &label_key_totals); + query_target_total_counts(wb, "label_key_values", &label_key_value_totals); + buffer_json_object_close(wb); // totals + + if(options & RRDR_OPTION_SHOW_DETAILS) { + buffer_json_member_add_object(wb, "detailed"); + query_target_detailed_objects_tree(wb, r, options); + buffer_json_object_close(wb); // detailed + } + + query_target_functions(wb, "functions", r); +} + +//static void annotations_range_for_value_flags(RRDR *r, BUFFER *wb, DATASOURCE_FORMAT format __maybe_unused, RRDR_OPTIONS options, RRDR_VALUE_FLAGS flags, const char *type) { +// const size_t dims = r->d, rows = r->rows; +// size_t next_d_idx = 0; +// for(size_t d = 0; d < dims ; d++) { +// if(!rrdr_dimension_should_be_exposed(r->od[d], options)) +// continue; +// +// size_t d_idx = next_d_idx++; +// +// size_t t = 0; +// while(t < rows) { +// +// // find the beginning +// time_t started = 0; +// for(; t < rows ;t++) { +// RRDR_VALUE_FLAGS o = r->o[t * r->d + d]; +// if(o & flags) { +// started = r->t[t]; +// break; +// } +// } +// +// if(started) { +// time_t ended = 0; +// for(; t < rows ;t++) { +// RRDR_VALUE_FLAGS o = r->o[t * r->d + d]; +// if(!(o & flags)) { +// ended = r->t[t]; +// break; +// } +// } +// +// if(!ended) +// ended = r->t[rows - 1]; +// +// buffer_json_add_array_item_object(wb); +// buffer_json_member_add_string(wb, "t", type); +// // buffer_json_member_add_string(wb, "d", string2str(r->dn[d])); +// buffer_json_member_add_uint64(wb, "d", d_idx); +// if(started == ended) { +// if(options & RRDR_OPTION_MILLISECONDS) +// buffer_json_member_add_time_t2ms(wb, "x", started); +// else +// buffer_json_member_add_time_t(wb, "x", started); +// } +// else { +// buffer_json_member_add_array(wb, "x"); +// if(options & RRDR_OPTION_MILLISECONDS) { +// buffer_json_add_array_item_time_t2ms(wb, started); +// buffer_json_add_array_item_time_t2ms(wb, ended); +// } +// else { +// buffer_json_add_array_item_time_t(wb, started); +// buffer_json_add_array_item_time_t(wb, ended); +// } +// buffer_json_array_close(wb); +// } +// buffer_json_object_close(wb); +// } +// } +// } +//} +// +//void rrdr_json_wrapper_annotations(RRDR *r, BUFFER *wb, DATASOURCE_FORMAT format __maybe_unused, RRDR_OPTIONS options) { +// buffer_json_member_add_array(wb, "annotations"); +// +// annotations_range_for_value_flags(r, wb, format, options, RRDR_VALUE_EMPTY, "G"); // Gap +// annotations_range_for_value_flags(r, wb, format, options, RRDR_VALUE_RESET, "O"); // Overflow +// annotations_range_for_value_flags(r, wb, format, options, RRDR_VALUE_PARTIAL, "P"); // Partial +// +// buffer_json_array_close(wb); // annotations +//} + +void rrdr_json_wrapper_end(RRDR *r, BUFFER *wb) { + buffer_json_member_add_double(wb, "min", r->view.min); + buffer_json_member_add_double(wb, "max", r->view.max); + + buffer_json_query_timings(wb, "timings", &r->internal.qt->timings); + buffer_json_finalize(wb); +} + +void rrdr_json_wrapper_end2(RRDR *r, BUFFER *wb) { + QUERY_TARGET *qt = r->internal.qt; + DATASOURCE_FORMAT format = qt->request.format; + RRDR_OPTIONS options = qt->window.options; + + buffer_json_member_add_object(wb, "db"); + { + buffer_json_member_add_uint64(wb, "tiers", storage_tiers); + buffer_json_member_add_time_t(wb, "update_every", qt->db.minimum_latest_update_every_s); + buffer_json_member_add_time_t(wb, "first_entry", qt->db.first_time_s); + buffer_json_member_add_time_t(wb, "last_entry", qt->db.last_time_s); + + query_target_combined_units_v2(wb, qt, r->internal.contexts, true); + buffer_json_member_add_object(wb, "dimensions"); + { + rrdr_dimension_ids(wb, "ids", r, options); + rrdr_dimension_units_array_v2(wb, "units", r, options, true); + rrdr_dimension_query_points_statistics(wb, "sts", r, options, false); + } + buffer_json_object_close(wb); // dimensions + + buffer_json_member_add_array(wb, "per_tier"); + for(size_t tier = 0; tier < storage_tiers ; tier++) { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_uint64(wb, "tier", tier); + buffer_json_member_add_uint64(wb, "queries", qt->db.tiers[tier].queries); + buffer_json_member_add_uint64(wb, "points", qt->db.tiers[tier].points); + buffer_json_member_add_time_t(wb, "update_every", qt->db.tiers[tier].update_every); + buffer_json_member_add_time_t(wb, "first_entry", qt->db.tiers[tier].retention.first_time_s); + buffer_json_member_add_time_t(wb, "last_entry", qt->db.tiers[tier].retention.last_time_s); + buffer_json_object_close(wb); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "view"); + { + query_target_title(wb, qt, r->internal.contexts); + buffer_json_member_add_time_t(wb, "update_every", r->view.update_every); + buffer_json_member_add_time_t(wb, "after", r->view.after); + buffer_json_member_add_time_t(wb, "before", r->view.before); + + if(options & RRDR_OPTION_DEBUG) { + buffer_json_member_add_string(wb, "format", rrdr_format_to_string(format)); + rrdr_options_to_buffer_json_array(wb, "options", options); + buffer_json_member_add_string(wb, "time_group", time_grouping_tostring(qt->request.time_group_method)); + } + + if(options & RRDR_OPTION_DEBUG) { + buffer_json_member_add_object(wb, "partial_data_trimming"); + buffer_json_member_add_time_t(wb, "max_update_every", r->partial_data_trimming.max_update_every); + buffer_json_member_add_time_t(wb, "expected_after", r->partial_data_trimming.expected_after); + buffer_json_member_add_time_t(wb, "trimmed_after", r->partial_data_trimming.trimmed_after); + buffer_json_object_close(wb); + } + + if(options & RRDR_OPTION_RETURN_RAW) + buffer_json_member_add_uint64(wb, "points", rrdr_rows(r)); + + query_target_combined_units_v2(wb, qt, r->internal.contexts, false); + query_target_combined_chart_type(wb, qt, r->internal.contexts); + buffer_json_member_add_object(wb, "dimensions"); + { + rrdr_grouped_by_array_v2(wb, "grouped_by", r, options); + rrdr_dimension_ids(wb, "ids", r, options); + rrdr_dimension_names(wb, "names", r, options); + rrdr_dimension_units_array_v2(wb, "units", r, options, false); + rrdr_dimension_priority_array_v2(wb, "priorities", r, options); + rrdr_dimension_aggregated_array_v2(wb, "aggregated", r, options); + rrdr_dimension_query_points_statistics(wb, "sts", r, options, true); + rrdr_json_group_by_labels(wb, "labels", r, options); + } + buffer_json_object_close(wb); // dimensions + buffer_json_member_add_double(wb, "min", r->view.min); + buffer_json_member_add_double(wb, "max", r->view.max); + } + buffer_json_object_close(wb); // view + + buffer_json_agents_v2(wb, &r->internal.qt->timings, 0, false, true); + buffer_json_cloud_timings(wb, "timings", &r->internal.qt->timings); + buffer_json_finalize(wb); +} diff --git a/src/web/api/formatters/json_wrapper.h b/src/web/api/formatters/json_wrapper.h new file mode 100644 index 000000000..a702f3a5c --- /dev/null +++ b/src/web/api/formatters/json_wrapper.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_JSON_WRAPPER_H +#define NETDATA_API_FORMATTER_JSON_WRAPPER_H + +#include "rrd2json.h" +#include "web/api/queries/query.h" + +typedef void (*wrapper_begin_t)(RRDR *r, BUFFER *wb); +typedef void (*wrapper_end_t)(RRDR *r, BUFFER *wb); + +void rrdr_json_wrapper_begin(RRDR *r, BUFFER *wb); +void rrdr_json_wrapper_end(RRDR *r, BUFFER *wb); + +void rrdr_json_wrapper_begin2(RRDR *r, BUFFER *wb); +void rrdr_json_wrapper_end2(RRDR *r, BUFFER *wb); + +struct query_versions; +void version_hashes_api_v2(BUFFER *wb, struct query_versions *versions); + +#endif //NETDATA_API_FORMATTER_JSON_WRAPPER_H diff --git a/src/web/api/formatters/rrd2json.c b/src/web/api/formatters/rrd2json.c new file mode 100644 index 000000000..81c9ad5c7 --- /dev/null +++ b/src/web/api/formatters/rrd2json.c @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web/api/web_api_v1.h" +#include "database/storage_engine.h" + +void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb) +{ + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + rrdset2json(st, wb, NULL, NULL); + buffer_json_finalize(wb); +} + +const char *rrdr_format_to_string(DATASOURCE_FORMAT format) { + switch(format) { + case DATASOURCE_JSON: + return DATASOURCE_FORMAT_JSON; + + case DATASOURCE_JSON2: + return DATASOURCE_FORMAT_JSON2; + + case DATASOURCE_DATATABLE_JSON: + return DATASOURCE_FORMAT_DATATABLE_JSON; + + case DATASOURCE_DATATABLE_JSONP: + return DATASOURCE_FORMAT_DATATABLE_JSONP; + + case DATASOURCE_JSONP: + return DATASOURCE_FORMAT_JSONP; + + case DATASOURCE_SSV: + return DATASOURCE_FORMAT_SSV; + + case DATASOURCE_CSV: + return DATASOURCE_FORMAT_CSV; + + case DATASOURCE_TSV: + return DATASOURCE_FORMAT_TSV; + + case DATASOURCE_HTML: + return DATASOURCE_FORMAT_HTML; + + case DATASOURCE_JS_ARRAY: + return DATASOURCE_FORMAT_JS_ARRAY; + + case DATASOURCE_SSV_COMMA: + return DATASOURCE_FORMAT_SSV_COMMA; + + default: + return "unknown"; + } +} + +int rrdset2value_api_v1( + RRDSET *st + , BUFFER *wb + , NETDATA_DOUBLE *n + , const char *dimensions + , size_t points + , time_t after + , time_t before + , RRDR_TIME_GROUPING group_method + , const char *group_options + , time_t resampling_time + , uint32_t options + , time_t *db_after + , time_t *db_before + , size_t *db_points_read + , size_t *db_points_per_tier + , size_t *result_points_generated + , int *value_is_null + , NETDATA_DOUBLE *anomaly_rate + , time_t timeout + , size_t tier + , QUERY_SOURCE query_source + , STORAGE_PRIORITY priority +) { + int ret = HTTP_RESP_INTERNAL_SERVER_ERROR; + + ONEWAYALLOC *owa = onewayalloc_create(0); + RRDR *r = rrd2rrdr_legacy( + owa, + st, + points, + after, + before, + group_method, + resampling_time, + options, + dimensions, + group_options, + timeout, + tier, + query_source, + priority); + + if(!r) { + if(value_is_null) *value_is_null = 1; + ret = HTTP_RESP_INTERNAL_SERVER_ERROR; + goto cleanup; + } + + if(db_points_read) + *db_points_read += r->stats.db_points_read; + + if(db_points_per_tier) { + for(size_t t = 0; t < storage_tiers ;t++) + db_points_per_tier[t] += r->internal.qt->db.tiers[t].points; + } + + if(result_points_generated) + *result_points_generated += r->stats.result_points_generated; + + if(rrdr_rows(r) == 0) { + if(db_after) *db_after = 0; + if(db_before) *db_before = 0; + if(value_is_null) *value_is_null = 1; + + ret = HTTP_RESP_BAD_REQUEST; + goto cleanup; + } + + if(wb) { + if (r->view.flags & RRDR_RESULT_FLAG_RELATIVE) + buffer_no_cacheable(wb); + else if (r->view.flags & RRDR_RESULT_FLAG_ABSOLUTE) + buffer_cacheable(wb); + } + + if(db_after) *db_after = r->view.after; + if(db_before) *db_before = r->view.before; + + long i = (!(options & RRDR_OPTION_REVERSED))?(long)rrdr_rows(r) - 1:0; + *n = rrdr2value(r, i, options, value_is_null, anomaly_rate); + ret = HTTP_RESP_OK; + +cleanup: + rrdr_free(owa, r); + onewayalloc_destroy(owa); + return ret; +} + +static inline void buffer_json_member_add_key_only(BUFFER *wb, const char *key) { + buffer_print_json_comma_newline_spacing(wb); + buffer_print_json_key(wb, key); + buffer_fast_strcat(wb, ":", 1); + wb->json.stack[wb->json.depth].count++; +} + +static inline void buffer_json_member_add_string_open(BUFFER *wb, const char *key) { + buffer_json_member_add_key_only(wb, key); + buffer_strcat(wb, wb->json.value_quote); +} + +static inline void buffer_json_member_add_string_close(BUFFER *wb) { + buffer_strcat(wb, wb->json.value_quote); +} + +int data_query_execute(ONEWAYALLOC *owa, BUFFER *wb, QUERY_TARGET *qt, time_t *latest_timestamp) { + wrapper_begin_t wrapper_begin = rrdr_json_wrapper_begin; + wrapper_end_t wrapper_end = rrdr_json_wrapper_end; + + if(qt->request.version == 2) { + wrapper_begin = rrdr_json_wrapper_begin2; + wrapper_end = rrdr_json_wrapper_end2; + } + + RRDR *r = rrd2rrdr(owa, qt); + + if(!r) { + buffer_strcat(wb, "Cannot generate output with these parameters on this chart."); + return HTTP_RESP_INTERNAL_SERVER_ERROR; + } + + if (r->view.flags & RRDR_RESULT_FLAG_CANCEL) { + rrdr_free(owa, r); + return HTTP_RESP_CLIENT_CLOSED_REQUEST; + } + + if(r->view.flags & RRDR_RESULT_FLAG_RELATIVE) + buffer_no_cacheable(wb); + else if(r->view.flags & RRDR_RESULT_FLAG_ABSOLUTE) + buffer_cacheable(wb); + + if(latest_timestamp && rrdr_rows(r) > 0) + *latest_timestamp = r->view.before; + + DATASOURCE_FORMAT format = qt->request.format; + RRDR_OPTIONS options = qt->window.options; + + switch(format) { + case DATASOURCE_SSV: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + rrdr2ssv(r, wb, options, "", " ", ""); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_PLAIN; + rrdr2ssv(r, wb, options, "", " ", ""); + } + break; + + case DATASOURCE_SSV_COMMA: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + rrdr2ssv(r, wb, options, "", ",", ""); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_PLAIN; + rrdr2ssv(r, wb, options, "", ",", ""); + } + break; + + case DATASOURCE_JS_ARRAY: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_array(wb, "result"); + rrdr2ssv(r, wb, options, "", ",", ""); + buffer_json_array_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_APPLICATION_JSON; + rrdr2ssv(r, wb, options, "[", ",", "]"); + } + break; + + case DATASOURCE_CSV: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + rrdr2csv(r, wb, format, options, "", ",", "\\n", ""); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_PLAIN; + rrdr2csv(r, wb, format, options, "", ",", "\r\n", ""); + } + break; + + case DATASOURCE_CSV_MARKDOWN: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + rrdr2csv(r, wb, format, options, "", "|", "\\n", ""); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_PLAIN; + rrdr2csv(r, wb, format, options, "", "|", "\r\n", ""); + } + break; + + case DATASOURCE_CSV_JSON_ARRAY: + wb->content_type = CT_APPLICATION_JSON; + if(options & RRDR_OPTION_JSON_WRAP) { + wrapper_begin(r, wb); + buffer_json_member_add_array(wb, "result"); + rrdr2csv(r, wb, format, options + RRDR_OPTION_LABEL_QUOTES, "[", ",", "]", ",\n"); + buffer_json_array_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_APPLICATION_JSON; + buffer_strcat(wb, "[\n"); + rrdr2csv(r, wb, format, options + RRDR_OPTION_LABEL_QUOTES, "[", ",", "]", ",\n"); + buffer_strcat(wb, "\n]"); + } + break; + + case DATASOURCE_TSV: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + rrdr2csv(r, wb, format, options, "", "\t", "\\n", ""); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_PLAIN; + rrdr2csv(r, wb, format, options, "", "\t", "\r\n", ""); + } + break; + + case DATASOURCE_HTML: + if(options & RRDR_OPTION_JSON_WRAP) { + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + buffer_json_member_add_string_open(wb, "result"); + buffer_strcat(wb, "<html>\\n<center>\\n<table border=\\\"0\\\" cellpadding=\\\"5\\\" cellspacing=\\\"5\\\">\\n"); + rrdr2csv(r, wb, format, options, "<tr><td>", "</td><td>", "</td></tr>\\n", ""); + buffer_strcat(wb, "</table>\\n</center>\\n</html>\\n"); + buffer_json_member_add_string_close(wb); + wrapper_end(r, wb); + } + else { + wb->content_type = CT_TEXT_HTML; + buffer_strcat(wb, "<html>\n<center>\n<table border=\"0\" cellpadding=\"5\" cellspacing=\"5\">\n"); + rrdr2csv(r, wb, format, options, "<tr><td>", "</td><td>", "</td></tr>\n", ""); + buffer_strcat(wb, "</table>\n</center>\n</html>\n"); + } + break; + + case DATASOURCE_DATATABLE_JSONP: + wb->content_type = CT_APPLICATION_X_JAVASCRIPT; + + if(options & RRDR_OPTION_JSON_WRAP) { + wrapper_begin(r, wb); + buffer_json_member_add_key_only(wb, "result"); + } + + rrdr2json(r, wb, options, 1); + + if(options & RRDR_OPTION_JSON_WRAP) + wrapper_end(r, wb); + + break; + + case DATASOURCE_DATATABLE_JSON: + wb->content_type = CT_APPLICATION_JSON; + + if(options & RRDR_OPTION_JSON_WRAP) { + wrapper_begin(r, wb); + buffer_json_member_add_key_only(wb, "result"); + } + + rrdr2json(r, wb, options, 1); + + if(options & RRDR_OPTION_JSON_WRAP) + wrapper_end(r, wb); + + break; + + case DATASOURCE_JSONP: + wb->content_type = CT_APPLICATION_X_JAVASCRIPT; + if(options & RRDR_OPTION_JSON_WRAP) { + wrapper_begin(r, wb); + buffer_json_member_add_key_only(wb, "result"); + } + + rrdr2json(r, wb, options, 0); + + if(options & RRDR_OPTION_JSON_WRAP) + wrapper_end(r, wb); + + break; + + case DATASOURCE_JSON: + default: + wb->content_type = CT_APPLICATION_JSON; + + if(options & RRDR_OPTION_JSON_WRAP) { + wrapper_begin(r, wb); + buffer_json_member_add_key_only(wb, "result"); + } + + rrdr2json(r, wb, options, 0); + + if(options & RRDR_OPTION_JSON_WRAP) { + if (options & RRDR_OPTION_RETURN_JWAR) { + buffer_json_member_add_key_only(wb, "anomaly_rates"); + rrdr2json(r, wb, options | RRDR_OPTION_INTERNAL_AR, false); + } + wrapper_end(r, wb); + } + break; + + case DATASOURCE_JSON2: + wb->content_type = CT_APPLICATION_JSON; + wrapper_begin(r, wb); + rrdr2json_v2(r, wb); + wrapper_end(r, wb); + break; + } + + rrdr_free(owa, r); + return HTTP_RESP_OK; +} diff --git a/src/web/api/formatters/rrd2json.h b/src/web/api/formatters/rrd2json.h new file mode 100644 index 000000000..f0c0c39ba --- /dev/null +++ b/src/web/api/formatters/rrd2json.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_RRD2JSON_H +#define NETDATA_RRD2JSON_H 1 + +// type of JSON generations +typedef enum { + DATASOURCE_JSON = 0, + DATASOURCE_DATATABLE_JSON = 1, + DATASOURCE_DATATABLE_JSONP = 2, + DATASOURCE_SSV = 3, + DATASOURCE_CSV = 4, + DATASOURCE_JSONP = 5, + DATASOURCE_TSV = 6, + DATASOURCE_HTML = 7, + DATASOURCE_JS_ARRAY = 8, + DATASOURCE_SSV_COMMA = 9, + DATASOURCE_CSV_JSON_ARRAY = 10, + DATASOURCE_CSV_MARKDOWN = 11, + DATASOURCE_JSON2 = 12, +} DATASOURCE_FORMAT; + +#include "web/api/web_api_v1.h" + +#include "web/api/exporters/allmetrics.h" +#include "web/api/queries/rrdr.h" + +#include "web/api/formatters/csv/csv.h" +#include "web/api/formatters/ssv/ssv.h" +#include "web/api/formatters/json/json.h" +#include "web/api/formatters/value/value.h" + +#include "web/api/formatters/rrdset2json.h" +#include "web/api/formatters/charts2json.h" +#include "web/api/formatters/json_wrapper.h" + +#include "web/server/web_client.h" + +#define HOSTNAME_MAX 1024 + +#define DATASOURCE_FORMAT_JSON "json" +#define DATASOURCE_FORMAT_JSON2 "json2" +#define DATASOURCE_FORMAT_DATATABLE_JSON "datatable" +#define DATASOURCE_FORMAT_DATATABLE_JSONP "datasource" +#define DATASOURCE_FORMAT_JSONP "jsonp" +#define DATASOURCE_FORMAT_SSV "ssv" +#define DATASOURCE_FORMAT_CSV "csv" +#define DATASOURCE_FORMAT_TSV "tsv" +#define DATASOURCE_FORMAT_HTML "html" +#define DATASOURCE_FORMAT_JS_ARRAY "array" +#define DATASOURCE_FORMAT_SSV_COMMA "ssvcomma" +#define DATASOURCE_FORMAT_CSV_JSON_ARRAY "csvjsonarray" +#define DATASOURCE_FORMAT_CSV_MARKDOWN "markdown" + +void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb); +const char *rrdr_format_to_string(DATASOURCE_FORMAT format); + +int data_query_execute(ONEWAYALLOC *owa, BUFFER *wb, struct query_target *qt, time_t *latest_timestamp); + +void rrdr_json_group_by_labels(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options); + +int rrdset2value_api_v1( + RRDSET *st + , BUFFER *wb + , NETDATA_DOUBLE *n + , const char *dimensions + , size_t points + , time_t after + , time_t before + , RRDR_TIME_GROUPING group_method + , const char *group_options + , time_t resampling_time + , uint32_t options + , time_t *db_after + , time_t *db_before + , size_t *db_points_read + , size_t *db_points_per_tier + , size_t *result_points_generated + , int *value_is_null + , NETDATA_DOUBLE *anomaly_rate + , time_t timeout + , size_t tier + , QUERY_SOURCE query_source + , STORAGE_PRIORITY priority +); + +static inline bool rrdr_dimension_should_be_exposed(RRDR_DIMENSION_FLAGS rrdr_dim_flags, RRDR_OPTIONS options) { + if(unlikely((options & RRDR_OPTION_RETURN_RAW) && (rrdr_dim_flags & RRDR_DIMENSION_QUERIED))) + return true; + + if(unlikely(rrdr_dim_flags & RRDR_DIMENSION_HIDDEN)) return false; + if(unlikely(!(rrdr_dim_flags & RRDR_DIMENSION_QUERIED))) return false; + if(unlikely((options & RRDR_OPTION_NONZERO) && !(rrdr_dim_flags & RRDR_DIMENSION_NONZERO))) return false; + + return true; +} + +#endif /* NETDATA_RRD2JSON_H */ diff --git a/src/web/api/formatters/rrdset2json.c b/src/web/api/formatters/rrdset2json.c new file mode 100644 index 000000000..542178b25 --- /dev/null +++ b/src/web/api/formatters/rrdset2json.c @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rrdset2json.h" + +static int process_label_callback(const char *name, const char *value, RRDLABEL_SRC ls __maybe_unused, void *data) { + BUFFER *wb = data; + buffer_json_member_add_string_or_empty(wb, name, value); + return 1; +} + +void chart_labels2json(RRDSET *st, BUFFER *wb) +{ + if(unlikely(!st->rrdlabels)) + return; + + rrdlabels_walkthrough_read(st->rrdlabels, process_label_callback, wb); +} + +// generate JSON for the /api/v1/chart API call +void rrdset2json(RRDSET *st, BUFFER *wb, size_t *dimensions_count, size_t *memory_used) +{ + time_t first_entry_t = rrdset_first_entry_s(st); + time_t last_entry_t = rrdset_last_entry_s(st); + char buf[RRD_ID_LENGTH_MAX + 16]; + + buffer_json_member_add_string(wb, "id", rrdset_id(st)); + buffer_json_member_add_string(wb, "name", rrdset_name(st)); + buffer_json_member_add_string(wb, "type", rrdset_parts_type(st)); + buffer_json_member_add_string(wb, "family", rrdset_family(st)); + buffer_json_member_add_string(wb, "context", rrdset_context(st)); + snprintfz(buf, RRD_ID_LENGTH_MAX + 15, "%s (%s)", rrdset_title(st), rrdset_name(st)); + buffer_json_member_add_string(wb, "title", buf); + buffer_json_member_add_int64(wb, "priority", st->priority); + buffer_json_member_add_string(wb, "plugin", rrdset_plugin_name(st)); + buffer_json_member_add_string(wb, "module", rrdset_module_name(st)); + buffer_json_member_add_string(wb, "units", rrdset_units(st)); + + snprintfz(buf, RRD_ID_LENGTH_MAX + 15, "/api/v1/data?chart=%s", rrdset_name(st)); + buffer_json_member_add_string(wb, "data_url", buf); + + buffer_json_member_add_string(wb, "chart_type", rrdset_type_name(st->chart_type)); + buffer_json_member_add_int64(wb, "duration", (int64_t)(last_entry_t - first_entry_t + st->update_every)); + buffer_json_member_add_int64(wb, "first_entry", (int64_t)first_entry_t); + buffer_json_member_add_int64(wb, "last_entry", (int64_t)last_entry_t); + buffer_json_member_add_int64(wb, "update_every", (int64_t)st->update_every); + + unsigned long memory = sizeof(RRDSET); + + size_t dimensions = 0; + buffer_json_member_add_object(wb, "dimensions"); + { + RRDDIM *rd; + rrddim_foreach_read(rd, st) + { + if (rrddim_option_check(rd, RRDDIM_OPTION_HIDDEN) || rrddim_flag_check(rd, RRDDIM_FLAG_OBSOLETE)) + continue; + + memory += rrddim_size() + rd->db.memsize; + + buffer_json_member_add_object(wb, rrddim_id(rd)); + buffer_json_member_add_string(wb, "name", rrddim_name(rd)); + buffer_json_object_close(wb); + + dimensions++; + } + rrddim_foreach_done(rd); + } + buffer_json_object_close(wb); + + if(dimensions_count) *dimensions_count += dimensions; + if(memory_used) *memory_used += memory; + + buffer_json_member_add_object(wb, "chart_variables"); + health_api_v1_chart_custom_variables2json(st, wb); + buffer_json_object_close(wb); + + buffer_json_member_add_double(wb, "green", st->green); + buffer_json_member_add_double(wb, "red", st->red); + + { + buffer_json_member_add_object(wb, "alarms"); + RRDCALC *rc; + rw_spinlock_read_lock(&st->alerts.spinlock); + DOUBLE_LINKED_LIST_FOREACH_FORWARD(st->alerts.base, rc, prev, next) + { + { + buffer_json_member_add_object(wb, rrdcalc_name(rc)); + buffer_json_member_add_string_or_empty(wb, "id", rrdcalc_name(rc)); + buffer_json_member_add_string_or_empty(wb, "status", rrdcalc_status2string(rc->status)); + buffer_json_member_add_string_or_empty(wb, "units", rrdcalc_units(rc)); + buffer_json_member_add_int64(wb, "duration", (int64_t)rc->config.update_every); + buffer_json_object_close(wb); + } + } + rw_spinlock_read_unlock(&st->alerts.spinlock); + buffer_json_object_close(wb); + } + + buffer_json_member_add_object(wb, "chart_labels"); + chart_labels2json(st, wb); + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "functions"); + chart_functions2json(st, wb); + buffer_json_object_close(wb); +} diff --git a/src/web/api/formatters/rrdset2json.h b/src/web/api/formatters/rrdset2json.h new file mode 100644 index 000000000..8b325c65d --- /dev/null +++ b/src/web/api/formatters/rrdset2json.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_RRDSET2JSON_H +#define NETDATA_API_FORMATTER_RRDSET2JSON_H + +#include "rrd2json.h" + +void rrdset2json(RRDSET *st, BUFFER *wb, size_t *dimensions_count, size_t *memory_used); + +#endif //NETDATA_API_FORMATTER_RRDSET2JSON_H diff --git a/src/web/api/formatters/ssv/README.md b/src/web/api/formatters/ssv/README.md new file mode 100644 index 000000000..2e9dd3886 --- /dev/null +++ b/src/web/api/formatters/ssv/README.md @@ -0,0 +1,63 @@ +<!-- +title: "SSV formatter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/ssv/README.md +sidebar_label: "SSV formatter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Formatters" +--> + +# SSV formatter + +The SSV formatter sums all dimensions in [results of database queries](https://github.com/netdata/netdata/blob/master/src/web/api/queries/README.md) +to a single value and returns a list of such values showing how it changes through time. + +It supports the following formats: + +| format | content type | description | +|:----:|:----------:|:----------| +| `ssv` | text/plain | a space separated list of values | +| `ssvcomma` | text/plain | a comma separated list of values | +| `array` | application/json | a JSON array | + +The SSV formatter respects the following API `&options=`: + +| option | supported | description | +| :----:|:-------:|:----------| +| `nonzero` | yes | to return only the dimensions that have at least a non-zero value | +| `flip` | yes | to return the numbers older to newer (the default is newer to older) | +| `percent` | yes | to replace all values with their percentage over the row total | +| `abs` | yes | to turn all values positive, before using them | +| `min2max` | yes | to return the delta from the minimum value to the maximum value (across dimensions) | + +## Examples + +Get the average system CPU utilization of the last hour, in 6 values (one every 10 minutes): + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=system.cpu&format=ssv&after=-3600&points=6&group=average' +1.741352 1.6800467 1.769411 1.6761112 1.629862 1.6807968 +``` + +--- + +Get the total mysql bandwidth (in + out) for the last hour, in 6 values (one every 10 minutes): + +Netdata returns bandwidth in `kilobits`. + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=mysql_local.net&format=ssvcomma&after=-3600&points=6&group=sum&options=abs' +72618.7936215,72618.778889,72618.788084,72618.9195918,72618.7760612,72618.6712421 +``` + +--- + +Get the web server max connections for the last hour, in 12 values (one every 5 minutes) +in a JSON array: + +```bash +# curl -Ss 'https://registry.my-netdata.io/api/v1/data?chart=nginx_local.connections&format=array&after=-3600&points=12&group=max' +[278,258,268,239,259,260,243,266,278,318,264,258] +``` + + diff --git a/src/web/api/formatters/ssv/ssv.c b/src/web/api/formatters/ssv/ssv.c new file mode 100644 index 000000000..2eb26b459 --- /dev/null +++ b/src/web/api/formatters/ssv/ssv.c @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ssv.h" + +void rrdr2ssv(RRDR *r, BUFFER *wb, RRDR_OPTIONS options, const char *prefix, const char *separator, const char *suffix) { + //netdata_log_info("RRD2SSV(): %s: BEGIN", r->st->id); + long i; + + buffer_strcat(wb, prefix); + long start = 0, end = rrdr_rows(r), step = 1; + if(!(options & RRDR_OPTION_REVERSED)) { + start = rrdr_rows(r) - 1; + end = -1; + step = -1; + } + + // for each line in the array + for(i = start; i != end ;i += step) { + int all_values_are_null = 0; + NETDATA_DOUBLE v = rrdr2value(r, i, options, &all_values_are_null, NULL); + + if(likely(i != start)) { + if(r->view.min > v) r->view.min = v; + if(r->view.max < v) r->view.max = v; + } + else { + r->view.min = v; + r->view.max = v; + } + + if(likely(i != start)) + buffer_strcat(wb, separator); + + if(all_values_are_null) { + if(options & RRDR_OPTION_NULL2ZERO) + buffer_strcat(wb, "0"); + else + buffer_strcat(wb, "null"); + } + else + buffer_print_netdata_double(wb, v); + } + buffer_strcat(wb, suffix); + //netdata_log_info("RRD2SSV(): %s: END", r->st->id); +} diff --git a/src/web/api/formatters/ssv/ssv.h b/src/web/api/formatters/ssv/ssv.h new file mode 100644 index 000000000..f7d4a9548 --- /dev/null +++ b/src/web/api/formatters/ssv/ssv.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_SSV_H +#define NETDATA_API_FORMATTER_SSV_H + +#include "../rrd2json.h" + +void rrdr2ssv(RRDR *r, BUFFER *wb, RRDR_OPTIONS options, const char *prefix, const char *separator, const char *suffix); + +#endif //NETDATA_API_FORMATTER_SSV_H diff --git a/src/web/api/formatters/value/README.md b/src/web/api/formatters/value/README.md new file mode 100644 index 000000000..3599a836e --- /dev/null +++ b/src/web/api/formatters/value/README.md @@ -0,0 +1,28 @@ +<!-- +title: "Value formatter" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/value/README.md +sidebar_label: "Value formatter" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Formatters" +--> + +# Value formatter + +The Value formatter presents [results of database queries](https://github.com/netdata/netdata/blob/master/src/web/api/queries/README.md) as a single value. + +To calculate the single value to be returned, it sums the values of all dimensions. + +The Value formatter respects the following API `&options=`: + +| option | supported | description | +|:----: |:-------: |:---------- | +| `percent` | yes | to replace all values with their percentage over the row total| +| `abs` | yes | to turn all values positive, before using them | +| `min2max` | yes | to return the delta from the minimum value to the maximum value (across dimensions)| + +The Value formatter is not exposed by the API by itself. +Instead it is used by the [`ssv`](https://github.com/netdata/netdata/blob/master/src/web/api/formatters/ssv/README.md) formatter +and [health monitoring queries](https://github.com/netdata/netdata/blob/master/src/health/README.md). + + diff --git a/src/web/api/formatters/value/value.c b/src/web/api/formatters/value/value.c new file mode 100644 index 000000000..0ec1b1265 --- /dev/null +++ b/src/web/api/formatters/value/value.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "value.h" + +inline NETDATA_DOUBLE rrdr2value(RRDR *r, long i, RRDR_OPTIONS options, int *all_values_are_null, NETDATA_DOUBLE *anomaly_rate) { + size_t c; + + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + RRDR_VALUE_FLAGS *co = &r->o[ i * r->d ]; + NETDATA_DOUBLE *ar = &r->ar[ i * r->d ]; + + NETDATA_DOUBLE sum = 0, min = NAN, max = NAN, v = NAN; + size_t dims = 0; + + NETDATA_DOUBLE total_anomaly_rate = 0; + + // for each dimension + for (c = 0; c < r->d ; c++) { + if(unlikely(!rrdr_dimension_should_be_exposed(r->od[c], options))) + continue; + + if(unlikely((co[c] & RRDR_VALUE_EMPTY))) + continue; + + NETDATA_DOUBLE n = cn[c]; + + if(unlikely(!dims)) + min = max = n; + + sum += n; + + if (n < min) min = n; + if (n > max) max = n; + + total_anomaly_rate += ar[c]; + + dims++; + } + + if(!dims) { + if(anomaly_rate) + *anomaly_rate = 0; + + if(all_values_are_null) + *all_values_are_null = 1; + + return (options & RRDR_OPTION_NULL2ZERO) ? 0 : NAN; + } + + if(anomaly_rate) + *anomaly_rate = total_anomaly_rate / (NETDATA_DOUBLE)dims; + + if(all_values_are_null) + *all_values_are_null = 0; + + if(options & RRDR_OPTION_DIMS_MIN2MAX) + v = max - min; + else if(options & RRDR_OPTION_DIMS_AVERAGE) + v = sum / (NETDATA_DOUBLE)dims; + else if(options & RRDR_OPTION_DIMS_MIN) + v = min; + else if(options & RRDR_OPTION_DIMS_MAX) + v = max; + else + v = sum; + + if((options & RRDR_OPTION_NULL2ZERO) && (isnan(v) || isinf(v))) + v = 0; + + return v; +} + +QUERY_VALUE rrdmetric2value(RRDHOST *host, + struct rrdcontext_acquired *rca, struct rrdinstance_acquired *ria, struct rrdmetric_acquired *rma, + time_t after, time_t before, + RRDR_OPTIONS options, RRDR_TIME_GROUPING time_group_method, const char *time_group_options, + size_t tier, time_t timeout, QUERY_SOURCE query_source, STORAGE_PRIORITY priority +) { + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .host = host, + .rca = rca, + .ria = ria, + .rma = rma, + .after = after, + .before = before, + .points = 1, + .options = options, + .time_group_method = time_group_method, + .time_group_options = time_group_options, + .tier = tier, + .timeout_ms = timeout, + .query_source = query_source, + .priority = priority, + }; + + ONEWAYALLOC *owa = onewayalloc_create(16 * 1024); + QUERY_TARGET *qt = query_target_create(&qtr); + RRDR *r = rrd2rrdr(owa, qt); + + QUERY_VALUE qv; + + if(!r || rrdr_rows(r) == 0) { + qv = (QUERY_VALUE) { + .value = NAN, + .anomaly_rate = NAN, + .sp = { + .count = 0, + .min = NAN, + .max = NAN, + .sum = NAN, + .anomaly_count = 0, + }, + .duration_ut = (r) ? r->internal.qt->timings.executed_ut - r->internal.qt->timings.received_ut : 0, + }; + } + else { + qv = (QUERY_VALUE) { + .after = r->view.after, + .before = r->view.before, + .points_read = r->stats.db_points_read, + .result_points = r->stats.result_points_generated, + .sp = { + .count = 0, + }, + .duration_ut = r->internal.qt->timings.executed_ut - r->internal.qt->timings.received_ut, + }; + + for(size_t d = 0; d < r->internal.qt->query.used ;d++) { + if(!rrdr_dimension_should_be_exposed(r->internal.qt->query.array[d].status, options)) + continue; + + storage_point_merge_to(qv.sp, r->internal.qt->query.array[d].query_points); + } + + for(size_t t = 0; t < storage_tiers ;t++) + qv.storage_points_per_tier[t] = r->internal.qt->db.tiers[t].points; + + long i = (!(options & RRDR_OPTION_REVERSED))?(long)rrdr_rows(r) - 1:0; + int all_values_are_null = 0; + qv.value = rrdr2value(r, i, options, &all_values_are_null, &qv.anomaly_rate); + if(all_values_are_null) { + qv.value = NAN; + qv.anomaly_rate = NAN; + } + } + + rrdr_free(owa, r); + query_target_release(qt); + onewayalloc_destroy(owa); + + return qv; +} diff --git a/src/web/api/formatters/value/value.h b/src/web/api/formatters/value/value.h new file mode 100644 index 000000000..072ca14f8 --- /dev/null +++ b/src/web/api/formatters/value/value.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_FORMATTER_VALUE_H +#define NETDATA_API_FORMATTER_VALUE_H + +#include "../rrd2json.h" + +typedef struct storage_value { + NETDATA_DOUBLE value; + NETDATA_DOUBLE anomaly_rate; + time_t after; + time_t before; + size_t points_read; + size_t storage_points_per_tier[RRD_STORAGE_TIERS]; + size_t result_points; + STORAGE_POINT sp; + usec_t duration_ut; +} QUERY_VALUE; + +struct rrdmetric_acquired; +struct rrdinstance_acquired; +struct rrdcontext_acquired; + +QUERY_VALUE rrdmetric2value(RRDHOST *host, + struct rrdcontext_acquired *rca, struct rrdinstance_acquired *ria, struct rrdmetric_acquired *rma, + time_t after, time_t before, + RRDR_OPTIONS options, RRDR_TIME_GROUPING time_group_method, const char *time_group_options, + size_t tier, time_t timeout, QUERY_SOURCE query_source, STORAGE_PRIORITY priority +); + +NETDATA_DOUBLE rrdr2value(RRDR *r, long i, RRDR_OPTIONS options, int *all_values_are_null, NETDATA_DOUBLE *anomaly_rate); + +#endif //NETDATA_API_FORMATTER_VALUE_H diff --git a/src/web/api/health/README.md b/src/web/api/health/README.md new file mode 100644 index 000000000..7e818c68f --- /dev/null +++ b/src/web/api/health/README.md @@ -0,0 +1,222 @@ +<!-- +title: "Health API Calls" +date: 2020-04-27 +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/health/README.md +sidebar_label: "Health API Calls" +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api" +--> + +# Health API Calls + +## Health Read API + +### Enabled Alerts + +Netdata enables alerts on demand, i.e. when the chart they should be linked to starts collecting data. So, although many +more alerts are configured, only the useful ones are enabled. + +To get the list of all enabled alerts, open your browser and navigate to `http://NODE:19999/api/v1/alarms?all`, +replacing `NODE` with the IP address or hostname for your Agent dashboard. + +### Raised Alerts + +This API call will return the alerts currently in WARNING or CRITICAL state. + +`http://NODE:19999/api/v1/alarms` + +### Event Log + +The size of the alert log is configured in `netdata.conf`. There are 2 settings: the event history kept in the DB (in seconds), and the in memory size of the alert log. + +``` +[health] + in memory max health log entries = 1000 + health log history = 432000 +``` + +The API call retrieves all entries of the alert log: + +`http://NODE:19999/api/v1/alarm_log` + +### Alert Log Incremental Updates + +`http://NODE:19999/api/v1/alarm_log?after=UNIQUEID` + +The above returns all the events in the alert log that occurred after UNIQUEID (you poll it once without `after=`, remember the last UNIQUEID of the returned set, which you give back to get incrementally the next events). + +### Alert badges + +The following will return an SVG badge of the alert named `NAME`, attached to the chart named `CHART`. + +`http://NODE:19999/api/v1/badge.svg?alarm=NAME&chart=CHART` + +## Health Management API + +Netdata v1.12 and beyond provides a command API to control health checks and notifications at runtime. The feature is especially useful for maintenance periods, during which you receive meaningless alerts. +From Netdata v1.16.0 and beyond, the configuration controlled via the API commands is [persisted across Netdata restarts](#persistence). + +Specifically, the API allows you to: + +- Disable health checks completely. Alert conditions will not be evaluated at all and no entries will be added to the alert log. +- Silence alert notifications. Alert conditions will be evaluated, the alerts will appear in the log and the Netdata UI will show the alerts as active, but no notifications will be sent. +- Disable or Silence specific alerts that match selectors on alert/template name, chart, context, and host. + +The API is available by default, but it is protected by an `api authorization token` that is stored in the file you will see in the following entry of `http://NODE:19999/netdata.conf`: + +``` +[registry] + # netdata management api key file = /var/lib/netdata/netdata.api.key +``` + +You can access the API via GET requests, by adding the bearer token to an `Authorization` http header, like this: + +``` +curl "http://NODE:19999/api/v1/manage/health?cmd=RESET" -H "X-Auth-Token: Mytoken" +``` + +By default access to the health management API is only allowed from `localhost`. Accessing the API from anything else will return a 403 error with the message `You are not allowed to access this resource.`. You can change permissions by editing the `allow management from` variable in `netdata.conf` within the [web] section. See [web server access lists](https://github.com/netdata/netdata/blob/master/src/web/server/README.md#access-lists) for more information. + +The command `RESET` just returns Netdata to the default operation, with all health checks and notifications enabled. +If you've configured and entered your token correctly, you should see the plain text response `All health checks and notifications are enabled`. + +### Disable or silence all alerts + +If all you need is temporarily disable all health checks, then you issue the following before your maintenance period starts: + +```sh +curl "http://NODE:19999/api/v1/manage/health?cmd=DISABLE%20ALL" -H "X-Auth-Token: Mytoken" +``` + +The effect of disabling health checks is that the alert criteria are not evaluated at all and nothing is written in the alert log. +If you want the health checks to be running but to not receive any notifications during your maintenance period, you can instead use this: + +```sh +curl "http://NODE:19999/api/v1/manage/health?cmd=SILENCE%20ALL" -H "X-Auth-Token: Mytoken" +``` + +Alerts may then still be raised and logged in Netdata, so you'll be able to see them via the UI. + +Regardless of the option you choose, at the end of your maintenance period you revert to the normal state via the RESET command. + +```sh + curl "http://NODE:19999/api/v1/manage/health?cmd=RESET" -H "X-Auth-Token: Mytoken" +``` + +### Disable or silence specific alerts + +If you do not wish to disable/silence all alerts, then the `DISABLE ALL` and `SILENCE ALL` commands can't be used. +Instead, the following commands expect that one or more alert selectors will be added, so that only alerts that match the selectors are disabled or silenced. + +- `DISABLE` : Set the mode to disable health checks. +- `SILENCE` : Set the mode to silence notifications. + +You will normally put one of these commands in the same request with your first alert selector, but it's possible to issue them separately as well. +You will get a warning in the response, if a selector was added without a SILENCE/DISABLE command, or vice versa. + +Each request can specify a single alert `selector`, with one or more `selection criteria`. +A single alert will match a `selector` if all selection criteria match the alert. +You can add as many selectors as you like. +In essence, the rule is: IF (alert matches all the criteria in selector1 OR all the criteria in selector2 OR ...) THEN apply the DISABLE or SILENCE command. + +To clear all selectors and reset the mode to default, use the `RESET` command. + +The following example silences notifications for all the alerts with context=load: + +``` +curl "http://NODE:19999/api/v1/manage/health?cmd=SILENCE&context=load" -H "X-Auth-Token: Mytoken" +``` + +#### Selection criteria + +The `selection criteria` are key/value pairs, in the format `key : value`, where value is a Netdata [simple pattern](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md). This means that you can create very powerful selectors (you will rarely need more than one or two). + +The accepted keys for the `selection criteria` are the following: + +- `alarm` : The expression provided will match both `alarm` and `template` names. +- `chart` : Chart ids/names, as shown on the dashboard. These will match the `on` entry of a configured `alarm`. +- `context` : Chart context, as shown on the dashboard. These will match the `on` entry of a configured `template`. +- `hosts` : The hostnames that will need to match. + +You can add any of the selection criteria you need on the request, to ensure that only the alerts you are interested in are matched and disabled/silenced. e.g. there is no reason to add `hosts: *`, if you want the criteria to be applied to alerts for all hosts. + +Example 1: Disable all health checks for context = `random` + +``` +http://NODE:19999/api/v1/manage/health?cmd=DISABLE&context=random +``` + +Example 2: Silence all alerts and templates with name starting with `out_of` on host `myhost` + +``` +http://NODE:19999/api/v1/manage/health?cmd=SILENCE&alarm=out_of*&hosts=myhost +``` + +### List silencers + +The command `LIST` was added in Netdata v1.16.0 and returns a JSON with the current status of the silencers. + +``` + curl "http://NODE:19999/api/v1/manage/health?cmd=LIST" -H "X-Auth-Token: Mytoken" +``` + +As an example, the following response shows that we have two silencers configured, one for an alert called `samplealert` and one for alerts with context `random` on host `myhost` + +``` +json +{ + "all": false, + "type": "SILENCE", + "silencers": [ + { + "alarm": "samplealert" + }, + { + "context": "random", + "hosts": "myhost" + } + ] +} +``` + +The response below shows that we have disabled all health checks. + +``` +json +{ + "all": true, + "type": "DISABLE", + "silencers": [] +} +``` + +### Responses + +- "Auth Error" : Token authentication failed +- "All alarm notifications are silenced" : Successful response to cmd=SILENCE ALL +- "All health checks are disabled" : Successful response to cmd=DISABLE ALL +- "All health checks and notifications are enabled" : Successful response to cmd=RESET +- "Health checks disabled for alarms matching the selectors" : Added to the response for a cmd=DISABLE +- "Alarm notifications silenced for alarms matching the selectors" : Added to the response for a cmd=SILENCE +- "Alarm selector added" : Added to the response when a new selector is added +- "Invalid key. Ignoring it." : Wrong name of a parameter. Added to the response and ignored. +- "WARNING: Added alarm selector to silence/disable alarms without a SILENCE or DISABLE command." : Added to the response if a selector is added without a selector-specific command. +- "WARNING: SILENCE or DISABLE command is ineffective without defining any alarm selectors." : Added to the response if a selector-specific command is issued without a selector. + +### Persistence + +From Netdata v1.16.0 and beyond, the silencers configuration is persisted to disk and loaded when Netdata starts. +The JSON string returned by the [LIST command](#list-silencers) is automatically saved to the `silencers file`, every time a command alters the silencers configuration. +The file's location is configurable in `netdata.conf`. The default is shown below: + +``` +[health] + # silencers file = /var/lib/netdata/health.silencers.json +``` + +### Further reading + +The test script under [tests/health_mgmtapi](https://github.com/netdata/netdata/blob/master/tests/health_mgmtapi/README.md) contains a series of tests that you can either run or read through to understand the various calls and responses better. + + diff --git a/src/web/api/http_auth.c b/src/web/api/http_auth.c new file mode 100644 index 000000000..3afab12e9 --- /dev/null +++ b/src/web/api/http_auth.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "http_auth.h" + +#define BEARER_TOKEN_EXPIRATION 86400 + +bool netdata_is_protected_by_bearer = false; // this is controlled by cloud, at the point the agent logs in - this should also be saved to /var/lib/netdata +static DICTIONARY *netdata_authorized_bearers = NULL; + +struct bearer_token { + uuid_t cloud_account_id; + char cloud_user_name[CLOUD_USER_NAME_LENGTH]; + HTTP_ACCESS access; + HTTP_USER_ROLE user_role; + time_t created_s; + time_t expires_s; +}; + +bool web_client_bearer_token_auth(struct web_client *w, const char *v) { + if(!uuid_parse_flexi(v, w->auth.bearer_token)) { + char uuid_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str); + + struct bearer_token *z = dictionary_get(netdata_authorized_bearers, uuid_str); + if (z && z->expires_s > now_monotonic_sec()) { + strncpyz(w->auth.client_name, z->cloud_user_name, sizeof(w->auth.client_name) - 1); + uuid_copy(w->auth.cloud_account_id, z->cloud_account_id); + web_client_set_permissions(w, z->access, z->user_role, WEB_CLIENT_FLAG_AUTH_BEARER); + return true; + } + } + else + nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v); + + return false; +} + +static void bearer_token_cleanup(void) { + static time_t attempts = 0; + + if(++attempts % 1000 != 0) + return; + + time_t now_s = now_monotonic_sec(); + + struct bearer_token *z; + dfe_start_read(netdata_authorized_bearers, z) { + if(z->expires_s < now_s) + dictionary_del(netdata_authorized_bearers, z_dfe.name); + } + dfe_done(z); + + dictionary_garbage_collect(netdata_authorized_bearers); +} + +void bearer_tokens_init(void) { + netdata_authorized_bearers = dictionary_create_advanced( + DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, + NULL, sizeof(struct bearer_token)); +} + +time_t bearer_create_token(uuid_t *uuid, struct web_client *w) { + char uuid_str[UUID_COMPACT_STR_LEN]; + + uuid_generate_random(*uuid); + uuid_unparse_lower_compact(*uuid, uuid_str); + + struct bearer_token t = { 0 }, *z; + z = dictionary_set(netdata_authorized_bearers, uuid_str, &t, sizeof(t)); + if(!z->created_s) { + z->created_s = now_monotonic_sec(); + z->expires_s = z->created_s + BEARER_TOKEN_EXPIRATION; + z->user_role = w->user_role; + z->access = w->access; + uuid_copy(z->cloud_account_id, w->auth.cloud_account_id); + strncpyz(z->cloud_user_name, w->auth.client_name, sizeof(z->cloud_account_id) - 1); + } + + bearer_token_cleanup(); + + return now_realtime_sec() + BEARER_TOKEN_EXPIRATION; +} + +bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) { + if(!web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_BEARER) || dst_len != UUID_STR_LEN) + return false; + + uuid_unparse_lower(w->auth.bearer_token, dst); + return true; +} diff --git a/src/web/api/http_auth.h b/src/web/api/http_auth.h new file mode 100644 index 000000000..ef06728f2 --- /dev/null +++ b/src/web/api/http_auth.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_HTTP_AUTH_H +#define NETDATA_HTTP_AUTH_H + +#include "web_api.h" + +struct web_client; + +extern bool netdata_is_protected_by_bearer; + +bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len); + +time_t bearer_create_token(uuid_t *uuid, struct web_client *w); +bool web_client_bearer_token_auth(struct web_client *w, const char *v); + +static inline bool http_access_user_has_enough_access_level_for_endpoint(HTTP_ACCESS user, HTTP_ACCESS endpoint) { + return ((user & endpoint) == endpoint); +} + +#endif //NETDATA_HTTP_AUTH_H diff --git a/src/web/api/http_header.c b/src/web/api/http_header.c new file mode 100644 index 000000000..5b0b1b651 --- /dev/null +++ b/src/web/api/http_header.c @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "http_header.h" + +static void web_client_enable_deflate(struct web_client *w, bool gzip) { + if(gzip) + web_client_flag_set(w, WEB_CLIENT_ENCODING_GZIP); + else + web_client_flag_set(w, WEB_CLIENT_ENCODING_DEFLATE); + + if(!web_client_check_conn_unix(w) && !web_client_check_conn_tcp(w) && !web_client_check_conn_cloud(w)) + return; + + if(unlikely(w->response.zinitialized)) { + // compression has already been initialized for this client. + return; + } + + if(unlikely(w->response.sent)) { + netdata_log_error("%llu: Cannot enable compression in the middle of a conversation.", w->id); + return; + } + + w->response.zstream.zalloc = Z_NULL; + w->response.zstream.zfree = Z_NULL; + w->response.zstream.opaque = Z_NULL; + + w->response.zstream.next_in = (Bytef *)w->response.data->buffer; + w->response.zstream.avail_in = 0; + w->response.zstream.total_in = 0; + + w->response.zstream.next_out = w->response.zbuffer; + w->response.zstream.avail_out = 0; + w->response.zstream.total_out = 0; + + w->response.zstream.zalloc = Z_NULL; + w->response.zstream.zfree = Z_NULL; + w->response.zstream.opaque = Z_NULL; + + // Select GZIP compression: windowbits = 15 + 16 = 31 + if(deflateInit2(&w->response.zstream, web_gzip_level, Z_DEFLATED, 15 + ((gzip)?16:0), 8, web_gzip_strategy) != Z_OK) { + netdata_log_error("%llu: Failed to initialize zlib. Proceeding without compression.", w->id); + return; + } + + w->response.zsent = 0; + w->response.zoutput = true; + w->response.zinitialized = true; + + if(!web_client_check_conn_cloud(w)) + // cloud sends the entire response at once, not in chunks + web_client_flag_set(w, WEB_CLIENT_CHUNKED_TRANSFER); + + netdata_log_debug(D_DEFLATE, "%llu: Initialized compression.", w->id); +} + +static void http_header_origin(struct web_client *w, const char *v, size_t len __maybe_unused) { + freez(w->origin); + w->origin = strdupz(v); +} + +static void http_header_connection(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(strcasestr(v, "keep-alive")) + web_client_enable_keepalive(w); +} + +static void http_header_dnt(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(respect_web_browser_do_not_track_policy) { + if (*v == '0') web_client_disable_donottrack(w); + else if (*v == '1') web_client_enable_donottrack(w); + } +} + +static void http_header_user_agent(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(w->mode == HTTP_REQUEST_MODE_STREAM) { + freez(w->user_agent); + w->user_agent = strdupz(v); + } +} + +static void http_header_x_auth_token(struct web_client *w, const char *v, size_t len __maybe_unused) { + freez(w->auth_bearer_token); + w->auth_bearer_token = strdupz(v); +} + +static void http_header_host(struct web_client *w, const char *v, size_t len) { + char buffer[NI_MAXHOST]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + freez(w->server_host); + w->server_host = strdupz(buffer); +} + +static void http_header_accept_encoding(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(web_enable_gzip) { + if(strcasestr(v, "gzip")) + web_client_enable_deflate(w, true); + + // does not seem to work + // else if(strcasestr(v, "deflate")) + // web_client_enable_deflate(w, 0); + } +} + +static void http_header_x_forwarded_host(struct web_client *w, const char *v, size_t len) { + char buffer[NI_MAXHOST]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + freez(w->forwarded_host); + w->forwarded_host = strdupz(buffer); +} + +static void http_header_x_forwarded_for(struct web_client *w, const char *v, size_t len) { + char buffer[NI_MAXHOST]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + freez(w->forwarded_for); + w->forwarded_for = strdupz(buffer); +} + +static void http_header_x_transaction_id(struct web_client *w, const char *v, size_t len) { + char buffer[UUID_STR_LEN * 2]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + uuid_parse_flexi(buffer, w->transaction); // will not alter w->transaction if it fails +} + +static void http_header_x_netdata_account_id(struct web_client *w, const char *v, size_t len) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD) && w->acl & HTTP_ACL_ACLK) { + char buffer[UUID_STR_LEN * 2]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + uuid_parse_flexi(buffer, w->auth.cloud_account_id); // will not alter w->cloud_account_id if it fails + } +} + +static void http_header_x_netdata_role(struct web_client *w, const char *v, size_t len) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD) && w->acl & HTTP_ACL_ACLK) { + char buffer[100]; + strncpyz(buffer, v, (len < sizeof(buffer) - 1 ? len : sizeof(buffer) - 1)); + if (strcasecmp(buffer, "admin") == 0) + w->user_role = HTTP_USER_ROLE_ADMIN; + else if(strcasecmp(buffer, "manager") == 0) + w->user_role = HTTP_USER_ROLE_MANAGER; + else if(strcasecmp(buffer, "troubleshooter") == 0) + w->user_role = HTTP_USER_ROLE_TROUBLESHOOTER; + else if(strcasecmp(buffer, "observer") == 0) + w->user_role = HTTP_USER_ROLE_OBSERVER; + else if(strcasecmp(buffer, "member") == 0) + w->user_role = HTTP_USER_ROLE_MEMBER; + else if(strcasecmp(buffer, "billing") == 0) + w->user_role = HTTP_USER_ROLE_BILLING; + else + w->user_role = HTTP_USER_ROLE_MEMBER; + } +} + +static void http_header_x_netdata_permissions(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD) && w->acl & HTTP_ACL_ACLK) { + HTTP_ACCESS access = http_access_from_hex(v); + web_client_set_permissions(w, access, w->user_role, WEB_CLIENT_FLAG_AUTH_CLOUD); + } +} + +static void http_header_x_netdata_user_name(struct web_client *w, const char *v, size_t len) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD) && w->acl & HTTP_ACL_ACLK) { + strncpyz(w->auth.client_name, v, (len < sizeof(w->auth.client_name) - 1 ? len : sizeof(w->auth.client_name) - 1)); + } +} + +static void http_header_x_netdata_auth(struct web_client *w, const char *v, size_t len __maybe_unused) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD) && w->acl & HTTP_ACL_ACLK) + // we don't need authorization bearer when the request comes from netdata cloud + return; + + if(strncasecmp(v, "Bearer ", 7) == 0) { + v = &v[7]; + while(*v && isspace(*v)) v++; + web_client_bearer_token_auth(w, v); + } +} + +struct { + uint32_t hash; + const char *key; + void (*cb)(struct web_client *w, const char *value, size_t value_len); +} supported_headers[] = { + { .hash = 0, .key = "Origin", .cb = http_header_origin }, + { .hash = 0, .key = "Connection", .cb = http_header_connection }, + { .hash = 0, .key = "DNT", .cb = http_header_dnt }, + { .hash = 0, .key = "User-Agent", .cb = http_header_user_agent}, + { .hash = 0, .key = "X-Auth-Token", .cb = http_header_x_auth_token }, + { .hash = 0, .key = "Host", .cb = http_header_host }, + { .hash = 0, .key = "Accept-Encoding", .cb = http_header_accept_encoding }, + { .hash = 0, .key = "X-Forwarded-Host", .cb = http_header_x_forwarded_host }, + { .hash = 0, .key = "X-Forwarded-For", .cb = http_header_x_forwarded_for }, + { .hash = 0, .key = "X-Transaction-Id", .cb = http_header_x_transaction_id }, + { .hash = 0, .key = "X-Netdata-Account-Id", .cb = http_header_x_netdata_account_id }, + { .hash = 0, .key = "X-Netdata-Role", .cb = http_header_x_netdata_role }, + { .hash = 0, .key = "X-Netdata-Permissions", .cb = http_header_x_netdata_permissions }, + { .hash = 0, .key = "X-Netdata-User-Name", .cb = http_header_x_netdata_user_name }, + { .hash = 0, .key = "X-Netdata-Auth", .cb = http_header_x_netdata_auth }, + + // for historical reasons. + // there are a few nightly versions of netdata UI that incorrectly use this instead of X-Netdata-Auth + { .hash = 0, .key = "Authorization", .cb = http_header_x_netdata_auth }, + + // terminator + { .hash = 0, .key = NULL, .cb = NULL } +}; + +char *http_header_parse_line(struct web_client *w, char *s) { + if(unlikely(!supported_headers[0].hash)) { + // initialize the hashes, the first time it runs + + for(size_t i = 0; supported_headers[i].key ;i++) + supported_headers[i].hash = simple_uhash(supported_headers[i].key); + } + + char *e = s; + + // find the colon + while(*e && *e != ':') e++; + if(!*e) return e; + + // get the name + *e = '\0'; + + // find the value + char *v = e + 1, *ve; + + // skip leading spaces from value + while(*v == ' ') v++; + ve = v; + + // find the \r + while(*ve && *ve != '\r') ve++; + if(!*ve || ve[1] != '\n') { + *e = ':'; + return ve; + } + + // terminate the value + *ve = '\0'; + + uint32_t hash = simple_uhash(s); + + for(size_t i = 0; supported_headers[i].key ;i++) { + if(likely(hash != supported_headers[i].hash || strcasecmp(s, supported_headers[i].key) != 0)) + continue; + + supported_headers[i].cb(w, v, ve - v); + break; + } + + *e = ':'; + *ve = '\r'; + return ve; +} diff --git a/src/web/api/http_header.h b/src/web/api/http_header.h new file mode 100644 index 000000000..15d14d7f1 --- /dev/null +++ b/src/web/api/http_header.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_HTTP_HEADER_H +#define NETDATA_HTTP_HEADER_H + +#include "web_api.h" + +struct web_client; +char *http_header_parse_line(struct web_client *w, char *s); + +#endif //NETDATA_HTTP_HEADER_H diff --git a/src/web/api/ilove/README.md b/src/web/api/ilove/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/web/api/ilove/README.md diff --git a/src/web/api/ilove/ilove.c b/src/web/api/ilove/ilove.c new file mode 100644 index 000000000..67489ec42 --- /dev/null +++ b/src/web/api/ilove/ilove.c @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ilove.h" + +static const unsigned short int ibm_plex_sans_bold_250[128][128] = { + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, + {5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* */, 5900 /* ! */, 5900 /* " */, 5900 /* # */, 5900 /* $ */, 5900 /* % */, 5900 /* & */, 5900 /* ' */, 5900 /* ( */, 5900 /* ) */, 5900 /* * */, 5900 /* + */, 5900 /* , */, 5900 /* - */, 5900 /* . */, 5900 /* / */, 5900 /* 0 */, 5900 /* 1 */, 5900 /* 2 */, 5900 /* 3 */, 5900 /* 4 */, 5900 /* 5 */, 5900 /* 6 */, 5900 /* 7 */, 5900 /* 8 */, 5900 /* 9 */, 5900 /* : */, 5900 /* ; */, 5900 /* < */, 5900 /* = */, 5900 /* > */, 5900 /* ? */, 5900 /* @ */, 5900 /* A */, 5900 /* B */, 5900 /* C */, 5900 /* D */, 5900 /* E */, 5900 /* F */, 5900 /* G */, 5900 /* H */, 5900 /* I */, 5900 /* J */, 5900 /* K */, 5900 /* L */, 5900 /* M */, 5900 /* N */, 5900 /* O */, 5900 /* P */, 5900 /* Q */, 5900 /* R */, 5900 /* S */, 5900 /* T */, 5900 /* U */, 5900 /* V */, 5900 /* W */, 5900 /* X */, 5900 /* Y */, 5900 /* Z */, 5900 /* [ */, 5900 /* \ */, 5900 /* ] */, 5900 /* ^ */, 5900 /* _ */, 5900 /* ` */, 5900 /* a */, 5900 /* b */, 5900 /* c */, 5900 /* d */, 5900 /* e */, 5900 /* f */, 5900 /* g */, 5900 /* h */, 5900 /* i */, 5900 /* j */, 5900 /* k */, 5900 /* l */, 5900 /* m */, 5900 /* n */, 5900 /* o */, 5900 /* p */, 5900 /* q */, 5900 /* r */, 5900 /* s */, 5900 /* t */, 5900 /* u */, 5900 /* v */, 5900 /* w */, 5900 /* x */, 5900 /* y */, 5900 /* z */, 5900 /* { */, 5900 /* | */, 5900 /* } */, 5900 /* ~ */}, + {8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* ! */, 8000 /* !! */, 8000 /* !" */, 8000 /* !# */, 8000 /* !$ */, 8000 /* !% */, 8000 /* !& */, 8000 /* !' */, 8000 /* !( */, 8000 /* !) */, 8000 /* !* */, 8000 /* !+ */, 8000 /* !, */, 8000 /* !- */, 8000 /* !. */, 8000 /* !/ */, 8000 /* !0 */, 8000 /* !1 */, 8000 /* !2 */, 8000 /* !3 */, 8000 /* !4 */, 8000 /* !5 */, 8000 /* !6 */, 8000 /* !7 */, 8000 /* !8 */, 8000 /* !9 */, 8000 /* !: */, 8000 /* !; */, 8000 /* !< */, 8000 /* != */, 8000 /* !> */, 8000 /* !? */, 8000 /* !@ */, 8000 /* !A */, 8000 /* !B */, 8000 /* !C */, 8000 /* !D */, 8000 /* !E */, 8000 /* !F */, 8000 /* !G */, 8000 /* !H */, 8000 /* !I */, 8000 /* !J */, 8000 /* !K */, 8000 /* !L */, 8000 /* !M */, 8000 /* !N */, 8000 /* !O */, 8000 /* !P */, 8000 /* !Q */, 8000 /* !R */, 8000 /* !S */, 8000 /* !T */, 8000 /* !U */, 8000 /* !V */, 8000 /* !W */, 8000 /* !X */, 8000 /* !Y */, 8000 /* !Z */, 8000 /* ![ */, 8000 /* !\ */, 8000 /* !] */, 8000 /* !^ */, 8000 /* !_ */, 8000 /* !` */, 8000 /* !a */, 8000 /* !b */, 8000 /* !c */, 8000 /* !d */, 8000 /* !e */, 8000 /* !f */, 8000 /* !g */, 8000 /* !h */, 8000 /* !i */, 8000 /* !j */, 8000 /* !k */, 8000 /* !l */, 8000 /* !m */, 8000 /* !n */, 8000 /* !o */, 8000 /* !p */, 8000 /* !q */, 8000 /* !r */, 8000 /* !s */, 8000 /* !t */, 8000 /* !u */, 8000 /* !v */, 8000 /* !w */, 8000 /* !x */, 8000 /* !y */, 8000 /* !z */, 8000 /* !{ */, 8000 /* !| */, 8000 /* !} */, 8000 /* !~ */}, + {12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* " */, 12325 /* "! */, 12325 /* "" */, 12325 /* "# */, 12325 /* "$ */, 12325 /* "% */, 12325 /* "& */, 12325 /* "' */, 12325 /* "( */, 12325 /* ") */, 12325 /* "* */, 12325 /* "+ */, 10075 /* ", */, 12325 /* "- */, 10075 /* ". */, 12325 /* "/ */, 12325 /* "0 */, 12325 /* "1 */, 12325 /* "2 */, 12325 /* "3 */, 12325 /* "4 */, 12325 /* "5 */, 12325 /* "6 */, 12325 /* "7 */, 12325 /* "8 */, 12325 /* "9 */, 12325 /* ": */, 12325 /* "; */, 12325 /* "< */, 12325 /* "= */, 12325 /* "> */, 12325 /* "? */, 12325 /* "@ */, 11075 /* "A */, 12325 /* "B */, 12075 /* "C */, 12325 /* "D */, 12325 /* "E */, 12325 /* "F */, 12075 /* "G */, 12325 /* "H */, 12325 /* "I */, 11325 /* "J */, 12325 /* "K */, 12325 /* "L */, 12325 /* "M */, 12325 /* "N */, 12075 /* "O */, 12325 /* "P */, 12075 /* "Q */, 12325 /* "R */, 12325 /* "S */, 12700 /* "T */, 12325 /* "U */, 12575 /* "V */, 12700 /* "W */, 12325 /* "X */, 12450 /* "Y */, 12325 /* "Z */, 12325 /* "[ */, 12325 /* "\ */, 12325 /* "] */, 12325 /* "^ */, 12325 /* "_ */, 12325 /* "` */, 12325 /* "a */, 12325 /* "b */, 11825 /* "c */, 11825 /* "d */, 11825 /* "e */, 12325 /* "f */, 12325 /* "g */, 12325 /* "h */, 12325 /* "i */, 12325 /* "j */, 12325 /* "k */, 12325 /* "l */, 12325 /* "m */, 12325 /* "n */, 11825 /* "o */, 12325 /* "p */, 11825 /* "q */, 12325 /* "r */, 12325 /* "s */, 12325 /* "t */, 12325 /* "u */, 12950 /* "v */, 12825 /* "w */, 12325 /* "x */, 12825 /* "y */, 12450 /* "z */, 12325 /* "{ */, 12325 /* "| */, 12325 /* "} */, 12325 /* "~ */}, + {15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* # */, 15800 /* #! */, 15800 /* #" */, 15800 /* ## */, 15800 /* #$ */, 15800 /* #% */, 15800 /* #& */, 15800 /* #' */, 15800 /* #( */, 15800 /* #) */, 15800 /* #* */, 15800 /* #+ */, 15800 /* #, */, 15800 /* #- */, 15800 /* #. */, 15800 /* #/ */, 15800 /* #0 */, 15800 /* #1 */, 15800 /* #2 */, 15800 /* #3 */, 15800 /* #4 */, 15800 /* #5 */, 15800 /* #6 */, 15800 /* #7 */, 15800 /* #8 */, 15800 /* #9 */, 15800 /* #: */, 15800 /* #; */, 15800 /* #< */, 15800 /* #= */, 15800 /* #> */, 15800 /* #? */, 15800 /* #@ */, 15800 /* #A */, 15800 /* #B */, 15800 /* #C */, 15800 /* #D */, 15800 /* #E */, 15800 /* #F */, 15800 /* #G */, 15800 /* #H */, 15800 /* #I */, 15800 /* #J */, 15800 /* #K */, 15800 /* #L */, 15800 /* #M */, 15800 /* #N */, 15800 /* #O */, 15800 /* #P */, 15800 /* #Q */, 15800 /* #R */, 15800 /* #S */, 15800 /* #T */, 15800 /* #U */, 15800 /* #V */, 15800 /* #W */, 15800 /* #X */, 15800 /* #Y */, 15800 /* #Z */, 15800 /* #[ */, 15800 /* #\ */, 15800 /* #] */, 15800 /* #^ */, 15800 /* #_ */, 15800 /* #` */, 15800 /* #a */, 15800 /* #b */, 15800 /* #c */, 15800 /* #d */, 15800 /* #e */, 15800 /* #f */, 15800 /* #g */, 15800 /* #h */, 15800 /* #i */, 15800 /* #j */, 15800 /* #k */, 15800 /* #l */, 15800 /* #m */, 15800 /* #n */, 15800 /* #o */, 15800 /* #p */, 15800 /* #q */, 15800 /* #r */, 15800 /* #s */, 15800 /* #t */, 15800 /* #u */, 15800 /* #v */, 15800 /* #w */, 15800 /* #x */, 15800 /* #y */, 15800 /* #z */, 15800 /* #{ */, 15800 /* #| */, 15800 /* #} */, 15800 /* #~ */}, + {15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $ */, 15025 /* $! */, 15025 /* $" */, 15025 /* $# */, 15025 /* $$ */, 15025 /* $% */, 15025 /* $& */, 15025 /* $' */, 15025 /* $( */, 15025 /* $) */, 15025 /* $* */, 15025 /* $+ */, 15025 /* $, */, 15025 /* $- */, 15025 /* $. */, 15025 /* $/ */, 15025 /* $0 */, 15025 /* $1 */, 15025 /* $2 */, 15025 /* $3 */, 15025 /* $4 */, 15025 /* $5 */, 15025 /* $6 */, 15025 /* $7 */, 15025 /* $8 */, 15025 /* $9 */, 15025 /* $: */, 15025 /* $; */, 15025 /* $< */, 15025 /* $= */, 15025 /* $> */, 15025 /* $? */, 15025 /* $@ */, 15025 /* $A */, 15025 /* $B */, 15025 /* $C */, 15025 /* $D */, 15025 /* $E */, 15025 /* $F */, 15025 /* $G */, 15025 /* $H */, 15025 /* $I */, 15025 /* $J */, 15025 /* $K */, 15025 /* $L */, 15025 /* $M */, 15025 /* $N */, 15025 /* $O */, 15025 /* $P */, 15025 /* $Q */, 15025 /* $R */, 15025 /* $S */, 15025 /* $T */, 15025 /* $U */, 15025 /* $V */, 15025 /* $W */, 15025 /* $X */, 15025 /* $Y */, 15025 /* $Z */, 15025 /* $[ */, 15025 /* $\ */, 15025 /* $] */, 15025 /* $^ */, 15025 /* $_ */, 15025 /* $` */, 15025 /* $a */, 15025 /* $b */, 15025 /* $c */, 15025 /* $d */, 15025 /* $e */, 15025 /* $f */, 15025 /* $g */, 15025 /* $h */, 15025 /* $i */, 15025 /* $j */, 15025 /* $k */, 15025 /* $l */, 15025 /* $m */, 15025 /* $n */, 15025 /* $o */, 15025 /* $p */, 15025 /* $q */, 15025 /* $r */, 15025 /* $s */, 15025 /* $t */, 15025 /* $u */, 15025 /* $v */, 15025 /* $w */, 15025 /* $x */, 15025 /* $y */, 15025 /* $z */, 15025 /* ${ */, 15025 /* $| */, 15025 /* $} */, 15025 /* $~ */}, + {24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* % */, 24350 /* %! */, 22850 /* %" */, 24350 /* %# */, 24350 /* %$ */, 24350 /* %% */, 24350 /* %& */, 22850 /* %' */, 24350 /* %( */, 24350 /* %) */, 22850 /* %* */, 24350 /* %+ */, 24350 /* %, */, 24350 /* %- */, 24350 /* %. */, 24350 /* %/ */, 24350 /* %0 */, 24350 /* %1 */, 24350 /* %2 */, 24350 /* %3 */, 24350 /* %4 */, 24350 /* %5 */, 24350 /* %6 */, 24350 /* %7 */, 24350 /* %8 */, 24350 /* %9 */, 24350 /* %: */, 24350 /* %; */, 24350 /* %< */, 24350 /* %= */, 24350 /* %> */, 24350 /* %? */, 24350 /* %@ */, 24350 /* %A */, 24350 /* %B */, 24350 /* %C */, 24350 /* %D */, 24350 /* %E */, 24350 /* %F */, 24350 /* %G */, 24350 /* %H */, 24350 /* %I */, 24350 /* %J */, 24350 /* %K */, 24350 /* %L */, 24350 /* %M */, 24350 /* %N */, 24350 /* %O */, 24350 /* %P */, 24350 /* %Q */, 24350 /* %R */, 24350 /* %S */, 24350 /* %T */, 24350 /* %U */, 24350 /* %V */, 24350 /* %W */, 24350 /* %X */, 24350 /* %Y */, 24350 /* %Z */, 24350 /* %[ */, 24350 /* %\ */, 24350 /* %] */, 24350 /* %^ */, 24350 /* %_ */, 24350 /* %` */, 24350 /* %a */, 24350 /* %b */, 24350 /* %c */, 24350 /* %d */, 24350 /* %e */, 24350 /* %f */, 24350 /* %g */, 24350 /* %h */, 24350 /* %i */, 24350 /* %j */, 24350 /* %k */, 24350 /* %l */, 24350 /* %m */, 24350 /* %n */, 24350 /* %o */, 24350 /* %p */, 24350 /* %q */, 24350 /* %r */, 24350 /* %s */, 24350 /* %t */, 24350 /* %u */, 24350 /* %v */, 24350 /* %w */, 24350 /* %x */, 24350 /* %y */, 24350 /* %z */, 24350 /* %{ */, 24350 /* %| */, 24350 /* %} */, 24350 /* %~ */}, + {18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* & */, 18025 /* &! */, 18025 /* &" */, 18025 /* &# */, 18025 /* &$ */, 18025 /* &% */, 18025 /* && */, 18025 /* &' */, 18025 /* &( */, 18025 /* &) */, 18025 /* &* */, 18025 /* &+ */, 18025 /* &, */, 18025 /* &- */, 18025 /* &. */, 18025 /* &/ */, 18025 /* &0 */, 18025 /* &1 */, 18025 /* &2 */, 18025 /* &3 */, 18025 /* &4 */, 18025 /* &5 */, 18025 /* &6 */, 18025 /* &7 */, 18025 /* &8 */, 18025 /* &9 */, 18025 /* &: */, 18025 /* &; */, 18025 /* &< */, 18025 /* &= */, 18025 /* &> */, 18025 /* &? */, 18025 /* &@ */, 18150 /* &A */, 18025 /* &B */, 18025 /* &C */, 18025 /* &D */, 18025 /* &E */, 18025 /* &F */, 18025 /* &G */, 18025 /* &H */, 18025 /* &I */, 18025 /* &J */, 18025 /* &K */, 18025 /* &L */, 18025 /* &M */, 18025 /* &N */, 18025 /* &O */, 18025 /* &P */, 18025 /* &Q */, 18025 /* &R */, 17900 /* &S */, 17025 /* &T */, 18025 /* &U */, 17150 /* &V */, 17525 /* &W */, 18025 /* &X */, 16525 /* &Y */, 17775 /* &Z */, 18025 /* &[ */, 18025 /* &\ */, 18025 /* &] */, 18025 /* &^ */, 18025 /* &_ */, 18025 /* &` */, 18275 /* &a */, 18025 /* &b */, 18275 /* &c */, 18275 /* &d */, 18275 /* &e */, 18025 /* &f */, 18025 /* &g */, 18025 /* &h */, 18025 /* &i */, 18025 /* &j */, 18025 /* &k */, 18025 /* &l */, 18025 /* &m */, 18025 /* &n */, 18275 /* &o */, 18025 /* &p */, 18275 /* &q */, 18025 /* &r */, 18025 /* &s */, 18025 /* &t */, 18025 /* &u */, 17775 /* &v */, 17775 /* &w */, 18025 /* &x */, 17775 /* &y */, 18025 /* &z */, 18025 /* &{ */, 18025 /* &| */, 18025 /* &} */, 18025 /* &~ */}, + {6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* ' */, 6700 /* '! */, 6700 /* '" */, 6700 /* '# */, 6700 /* '$ */, 6700 /* '% */, 6700 /* '& */, 6700 /* '' */, 6700 /* '( */, 6700 /* ') */, 6700 /* '* */, 6700 /* '+ */, 4450 /* ', */, 6700 /* '- */, 4450 /* '. */, 6700 /* '/ */, 6700 /* '0 */, 6700 /* '1 */, 6700 /* '2 */, 6700 /* '3 */, 6700 /* '4 */, 6700 /* '5 */, 6700 /* '6 */, 6700 /* '7 */, 6700 /* '8 */, 6700 /* '9 */, 6700 /* ': */, 6700 /* '; */, 6700 /* '< */, 6700 /* '= */, 6700 /* '> */, 6700 /* '? */, 6700 /* '@ */, 5450 /* 'A */, 6700 /* 'B */, 6450 /* 'C */, 6700 /* 'D */, 6700 /* 'E */, 6700 /* 'F */, 6450 /* 'G */, 6700 /* 'H */, 6700 /* 'I */, 5700 /* 'J */, 6700 /* 'K */, 6700 /* 'L */, 6700 /* 'M */, 6700 /* 'N */, 6450 /* 'O */, 6700 /* 'P */, 6450 /* 'Q */, 6700 /* 'R */, 6700 /* 'S */, 7075 /* 'T */, 6700 /* 'U */, 6950 /* 'V */, 7075 /* 'W */, 6700 /* 'X */, 6825 /* 'Y */, 6700 /* 'Z */, 6700 /* '[ */, 6700 /* '\ */, 6700 /* '] */, 6700 /* '^ */, 6700 /* '_ */, 6700 /* '` */, 6700 /* 'a */, 6700 /* 'b */, 6200 /* 'c */, 6200 /* 'd */, 6200 /* 'e */, 6700 /* 'f */, 6700 /* 'g */, 6700 /* 'h */, 6700 /* 'i */, 6700 /* 'j */, 6700 /* 'k */, 6700 /* 'l */, 6700 /* 'm */, 6700 /* 'n */, 6200 /* 'o */, 6700 /* 'p */, 6200 /* 'q */, 6700 /* 'r */, 6700 /* 's */, 6700 /* 't */, 6700 /* 'u */, 7325 /* 'v */, 7200 /* 'w */, 6700 /* 'x */, 7200 /* 'y */, 6825 /* 'z */, 6700 /* '{ */, 6700 /* '| */, 6700 /* '} */, 6700 /* '~ */}, + {8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* ( */, 8450 /* (! */, 8450 /* (" */, 8450 /* (# */, 8450 /* ($ */, 8450 /* (% */, 8450 /* (& */, 8450 /* (' */, 8450 /* (( */, 8450 /* () */, 8450 /* (* */, 8450 /* (+ */, 8450 /* (, */, 8450 /* (- */, 8450 /* (. */, 8450 /* (/ */, 8450 /* (0 */, 8450 /* (1 */, 8450 /* (2 */, 8450 /* (3 */, 8450 /* (4 */, 8450 /* (5 */, 8450 /* (6 */, 8450 /* (7 */, 8450 /* (8 */, 8450 /* (9 */, 8450 /* (: */, 8450 /* (; */, 8450 /* (< */, 8450 /* (= */, 8450 /* (> */, 8450 /* (? */, 8450 /* (@ */, 8450 /* (A */, 8450 /* (B */, 8450 /* (C */, 8450 /* (D */, 8450 /* (E */, 8450 /* (F */, 8450 /* (G */, 8450 /* (H */, 8450 /* (I */, 8450 /* (J */, 8450 /* (K */, 8450 /* (L */, 8450 /* (M */, 8450 /* (N */, 8450 /* (O */, 8450 /* (P */, 8450 /* (Q */, 8450 /* (R */, 8450 /* (S */, 8950 /* (T */, 8450 /* (U */, 8950 /* (V */, 8950 /* (W */, 8700 /* (X */, 8950 /* (Y */, 8450 /* (Z */, 8450 /* ([ */, 8450 /* (\ */, 8450 /* (] */, 8450 /* (^ */, 8450 /* (_ */, 8450 /* (` */, 8450 /* (a */, 8450 /* (b */, 8450 /* (c */, 8450 /* (d */, 8450 /* (e */, 8450 /* (f */, 8700 /* (g */, 8450 /* (h */, 8450 /* (i */, 10200 /* (j */, 8450 /* (k */, 8450 /* (l */, 8450 /* (m */, 8450 /* (n */, 8450 /* (o */, 8450 /* (p */, 8450 /* (q */, 8450 /* (r */, 8450 /* (s */, 8450 /* (t */, 8450 /* (u */, 8450 /* (v */, 8450 /* (w */, 8450 /* (x */, 8450 /* (y */, 8450 /* (z */, 8450 /* ({ */, 8450 /* (| */, 8450 /* (} */, 8450 /* (~ */}, + {8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* ) */, 8450 /* )! */, 8450 /* )" */, 8450 /* )# */, 8450 /* )$ */, 8450 /* )% */, 8450 /* )& */, 8450 /* )' */, 8450 /* )( */, 8450 /* )) */, 8450 /* )* */, 8450 /* )+ */, 8450 /* ), */, 8450 /* )- */, 8450 /* ). */, 8450 /* )/ */, 8450 /* )0 */, 8450 /* )1 */, 8450 /* )2 */, 8450 /* )3 */, 8450 /* )4 */, 8450 /* )5 */, 8450 /* )6 */, 8450 /* )7 */, 8450 /* )8 */, 8450 /* )9 */, 8450 /* ): */, 8450 /* ); */, 8450 /* )< */, 8450 /* )= */, 8450 /* )> */, 8450 /* )? */, 8450 /* )@ */, 8450 /* )A */, 8450 /* )B */, 8450 /* )C */, 8450 /* )D */, 8450 /* )E */, 8450 /* )F */, 8450 /* )G */, 8450 /* )H */, 8450 /* )I */, 8450 /* )J */, 8450 /* )K */, 8450 /* )L */, 8450 /* )M */, 8450 /* )N */, 8450 /* )O */, 8450 /* )P */, 8450 /* )Q */, 8450 /* )R */, 8450 /* )S */, 8450 /* )T */, 8450 /* )U */, 8450 /* )V */, 8450 /* )W */, 8450 /* )X */, 8450 /* )Y */, 8450 /* )Z */, 8450 /* )[ */, 8450 /* )\ */, 8450 /* )] */, 8450 /* )^ */, 8450 /* )_ */, 8450 /* )` */, 8450 /* )a */, 8450 /* )b */, 8450 /* )c */, 8450 /* )d */, 8450 /* )e */, 8450 /* )f */, 8450 /* )g */, 8450 /* )h */, 8450 /* )i */, 8450 /* )j */, 8450 /* )k */, 8450 /* )l */, 8450 /* )m */, 8450 /* )n */, 8450 /* )o */, 8450 /* )p */, 8450 /* )q */, 8450 /* )r */, 8450 /* )s */, 8450 /* )t */, 8450 /* )u */, 8450 /* )v */, 8450 /* )w */, 8450 /* )x */, 8450 /* )y */, 8450 /* )z */, 8450 /* ){ */, 8450 /* )| */, 8450 /* )} */, 8450 /* )~ */}, + {15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* * */, 15025 /* *! */, 15025 /* *" */, 15025 /* *# */, 15025 /* *$ */, 15025 /* *% */, 15025 /* *& */, 15025 /* *' */, 15025 /* *( */, 15025 /* *) */, 15025 /* ** */, 15025 /* *+ */, 12775 /* *, */, 15025 /* *- */, 12775 /* *. */, 15025 /* *\/ */, 15025 /* *0 */, 15025 /* *1 */, 15025 /* *2 */, 15025 /* *3 */, 15025 /* *4 */, 15025 /* *5 */, 15025 /* *6 */, 15025 /* *7 */, 15025 /* *8 */, 15025 /* *9 */, 15025 /* *: */, 15025 /* *; */, 15025 /* *< */, 15025 /* *= */, 15025 /* *> */, 15025 /* *? */, 15025 /* *@ */, 13775 /* *A */, 15025 /* *B */, 14775 /* *C */, 15025 /* *D */, 15025 /* *E */, 15025 /* *F */, 14775 /* *G */, 15025 /* *H */, 15025 /* *I */, 14025 /* *J */, 15025 /* *K */, 15025 /* *L */, 15025 /* *M */, 15025 /* *N */, 14775 /* *O */, 15025 /* *P */, 14775 /* *Q */, 15025 /* *R */, 15025 /* *S */, 15400 /* *T */, 15025 /* *U */, 15275 /* *V */, 15400 /* *W */, 15025 /* *X */, 15150 /* *Y */, 15025 /* *Z */, 15025 /* *[ */, 15025 /* *\ */, 15025 /* *] */, 15025 /* *^ */, 15025 /* *_ */, 15025 /* *` */, 15025 /* *a */, 15025 /* *b */, 14525 /* *c */, 14525 /* *d */, 14525 /* *e */, 15025 /* *f */, 15025 /* *g */, 15025 /* *h */, 15025 /* *i */, 15025 /* *j */, 15025 /* *k */, 15025 /* *l */, 15025 /* *m */, 15025 /* *n */, 14525 /* *o */, 15025 /* *p */, 14525 /* *q */, 15025 /* *r */, 15025 /* *s */, 15025 /* *t */, 15025 /* *u */, 15650 /* *v */, 15525 /* *w */, 15025 /* *x */, 15525 /* *y */, 15150 /* *z */, 15025 /* *{ */, 15025 /* *| */, 15025 /* *} */, 15025 /* *~ */}, + {15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* + */, 15000 /* +! */, 15000 /* +" */, 15000 /* +# */, 15000 /* +$ */, 15000 /* +% */, 15000 /* +& */, 15000 /* +' */, 15000 /* +( */, 15000 /* +) */, 15000 /* +* */, 15000 /* ++ */, 15000 /* +, */, 15000 /* +- */, 15000 /* +. */, 15000 /* +/ */, 15000 /* +0 */, 15000 /* +1 */, 15000 /* +2 */, 15000 /* +3 */, 15000 /* +4 */, 15000 /* +5 */, 15000 /* +6 */, 15000 /* +7 */, 15000 /* +8 */, 15000 /* +9 */, 15000 /* +: */, 15000 /* +; */, 15000 /* +< */, 15000 /* += */, 15000 /* +> */, 15000 /* +? */, 15000 /* +@ */, 15000 /* +A */, 15000 /* +B */, 15000 /* +C */, 15000 /* +D */, 15000 /* +E */, 15000 /* +F */, 15000 /* +G */, 15000 /* +H */, 15000 /* +I */, 15000 /* +J */, 15000 /* +K */, 15000 /* +L */, 15000 /* +M */, 15000 /* +N */, 15000 /* +O */, 15000 /* +P */, 15000 /* +Q */, 15000 /* +R */, 15000 /* +S */, 15000 /* +T */, 15000 /* +U */, 15000 /* +V */, 15000 /* +W */, 15000 /* +X */, 15000 /* +Y */, 15000 /* +Z */, 15000 /* +[ */, 15000 /* +\ */, 15000 /* +] */, 15000 /* +^ */, 15000 /* +_ */, 15000 /* +` */, 15000 /* +a */, 15000 /* +b */, 15000 /* +c */, 15000 /* +d */, 15000 /* +e */, 15000 /* +f */, 15000 /* +g */, 15000 /* +h */, 15000 /* +i */, 15000 /* +j */, 15000 /* +k */, 15000 /* +l */, 15000 /* +m */, 15000 /* +n */, 15000 /* +o */, 15000 /* +p */, 15000 /* +q */, 15000 /* +r */, 15000 /* +s */, 15000 /* +t */, 15000 /* +u */, 15000 /* +v */, 15000 /* +w */, 15000 /* +x */, 15000 /* +y */, 15000 /* +z */, 15000 /* +{ */, 15000 /* +| */, 15000 /* +} */, 15000 /* +~ */}, + {7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* , */, 7750 /* ,! */, 5500 /* ," */, 7750 /* ,# */, 7750 /* ,$ */, 7750 /* ,% */, 7750 /* ,& */, 5500 /* ,' */, 7750 /* ,( */, 7750 /* ,) */, 5500 /* ,* */, 7750 /* ,+ */, 7750 /* ,, */, 7750 /* ,- */, 7750 /* ,. */, 7750 /* ,/ */, 7750 /* ,0 */, 7750 /* ,1 */, 7750 /* ,2 */, 7750 /* ,3 */, 7750 /* ,4 */, 7750 /* ,5 */, 7750 /* ,6 */, 7750 /* ,7 */, 7750 /* ,8 */, 7750 /* ,9 */, 7750 /* ,: */, 7750 /* ,; */, 7750 /* ,< */, 7750 /* ,= */, 7750 /* ,> */, 5750 /* ,? */, 7750 /* ,@ */, 8250 /* ,A */, 7750 /* ,B */, 6750 /* ,C */, 7750 /* ,D */, 7750 /* ,E */, 7750 /* ,F */, 6750 /* ,G */, 7750 /* ,H */, 7750 /* ,I */, 7750 /* ,J */, 7750 /* ,K */, 7750 /* ,L */, 7750 /* ,M */, 7750 /* ,N */, 6750 /* ,O */, 7750 /* ,P */, 6750 /* ,Q */, 7750 /* ,R */, 7750 /* ,S */, 6000 /* ,T */, 6875 /* ,U */, 6250 /* ,V */, 6750 /* ,W */, 8000 /* ,X */, 6000 /* ,Y */, 8500 /* ,Z */, 7750 /* ,[ */, 7750 /* ,\ */, 7750 /* ,] */, 7750 /* ,^ */, 7750 /* ,_ */, 7750 /* ,` */, 7875 /* ,a */, 7750 /* ,b */, 7375 /* ,c */, 7375 /* ,d */, 7375 /* ,e */, 7375 /* ,f */, 7750 /* ,g */, 7750 /* ,h */, 7750 /* ,i */, 7750 /* ,j */, 7750 /* ,k */, 7750 /* ,l */, 7750 /* ,m */, 7750 /* ,n */, 7375 /* ,o */, 7750 /* ,p */, 7375 /* ,q */, 7750 /* ,r */, 7750 /* ,s */, 7150 /* ,t */, 7550 /* ,u */, 6550 /* ,v */, 6750 /* ,w */, 7750 /* ,x */, 6625 /* ,y */, 8000 /* ,z */, 7750 /* ,{ */, 7750 /* ,| */, 7750 /* ,} */, 7750 /* ,~ */}, + {10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* - */, 10075 /* -! */, 10075 /* -" */, 10075 /* -# */, 10075 /* -$ */, 10075 /* -% */, 10075 /* -& */, 10075 /* -' */, 10075 /* -( */, 10075 /* -) */, 10075 /* -* */, 10075 /* -+ */, 10075 /* -, */, 10075 /* -- */, 10075 /* -. */, 10075 /* -/ */, 10075 /* -0 */, 10075 /* -1 */, 10075 /* -2 */, 10075 /* -3 */, 10075 /* -4 */, 10075 /* -5 */, 10075 /* -6 */, 10075 /* -7 */, 10075 /* -8 */, 10075 /* -9 */, 10075 /* -: */, 10075 /* -; */, 10075 /* -< */, 10075 /* -= */, 10075 /* -> */, 10075 /* -? */, 10075 /* -@ */, 9700 /* -A */, 10075 /* -B */, 10450 /* -C */, 10075 /* -D */, 10075 /* -E */, 10075 /* -F */, 10450 /* -G */, 10075 /* -H */, 9325 /* -I */, 9450 /* -J */, 10075 /* -K */, 10075 /* -L */, 10075 /* -M */, 10075 /* -N */, 10450 /* -O */, 10075 /* -P */, 10450 /* -Q */, 10075 /* -R */, 9575 /* -S */, 8950 /* -T */, 10075 /* -U */, 9450 /* -V */, 9825 /* -W */, 9325 /* -X */, 8825 /* -Y */, 9325 /* -Z */, 10075 /* -[ */, 10075 /* -\ */, 10075 /* -] */, 10075 /* -^ */, 10075 /* -_ */, 10075 /* -` */, 10075 /* -a */, 10075 /* -b */, 10325 /* -c */, 10325 /* -d */, 10325 /* -e */, 10075 /* -f */, 10075 /* -g */, 10075 /* -h */, 10075 /* -i */, 10075 /* -j */, 10075 /* -k */, 10075 /* -l */, 10075 /* -m */, 10075 /* -n */, 10325 /* -o */, 10075 /* -p */, 10325 /* -q */, 10075 /* -r */, 10200 /* -s */, 10075 /* -t */, 10075 /* -u */, 9950 /* -v */, 9975 /* -w */, 9200 /* -x */, 9950 /* -y */, 9700 /* -z */, 10075 /* -{ */, 10075 /* -| */, 10075 /* -} */, 10075 /* -~ */}, + {7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* . */, 7750 /* .! */, 5500 /* ." */, 7750 /* .# */, 7750 /* .$ */, 7750 /* .% */, 7750 /* .& */, 5500 /* .' */, 7750 /* .( */, 7750 /* .) */, 5500 /* .* */, 7750 /* .+ */, 7750 /* ., */, 7750 /* .- */, 7750 /* .. */, 7750 /* ./ */, 7750 /* .0 */, 7750 /* .1 */, 7750 /* .2 */, 7750 /* .3 */, 7750 /* .4 */, 7750 /* .5 */, 7750 /* .6 */, 7750 /* .7 */, 7750 /* .8 */, 7750 /* .9 */, 7750 /* .: */, 7750 /* .; */, 7750 /* .< */, 7750 /* .= */, 7750 /* .> */, 5750 /* .? */, 7750 /* .@ */, 8250 /* .A */, 7750 /* .B */, 6750 /* .C */, 7750 /* .D */, 7750 /* .E */, 7750 /* .F */, 6750 /* .G */, 7750 /* .H */, 7750 /* .I */, 7750 /* .J */, 7750 /* .K */, 7750 /* .L */, 7750 /* .M */, 7750 /* .N */, 6750 /* .O */, 7750 /* .P */, 6750 /* .Q */, 7750 /* .R */, 7750 /* .S */, 6000 /* .T */, 6875 /* .U */, 6250 /* .V */, 6750 /* .W */, 8000 /* .X */, 6000 /* .Y */, 8500 /* .Z */, 7750 /* .[ */, 7750 /* .\ */, 7750 /* .] */, 7750 /* .^ */, 7750 /* ._ */, 7750 /* .` */, 7875 /* .a */, 7750 /* .b */, 7375 /* .c */, 7375 /* .d */, 7375 /* .e */, 7375 /* .f */, 7750 /* .g */, 7750 /* .h */, 7750 /* .i */, 7750 /* .j */, 7750 /* .k */, 7750 /* .l */, 7750 /* .m */, 7750 /* .n */, 7375 /* .o */, 7750 /* .p */, 7375 /* .q */, 7750 /* .r */, 7750 /* .s */, 7150 /* .t */, 7550 /* .u */, 6550 /* .v */, 6750 /* .w */, 7750 /* .x */, 6625 /* .y */, 8000 /* .z */, 7750 /* .{ */, 7750 /* .| */, 7750 /* .} */, 7750 /* .~ */}, + {11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* / */, 11500 /* /! */, 11500 /* /" */, 11500 /* /# */, 11500 /* /$ */, 11500 /* /% */, 11500 /* /& */, 11500 /* /' */, 11500 /* /( */, 11500 /* /) */, 11500 /* /\* */, 11500 /* /+ */, 11500 /* /, */, 11500 /* /- */, 11500 /* /. */, 6750 /* // */, 11500 /* /0 */, 11500 /* /1 */, 11500 /* /2 */, 11500 /* /3 */, 11500 /* /4 */, 11500 /* /5 */, 11500 /* /6 */, 11500 /* /7 */, 11500 /* /8 */, 11500 /* /9 */, 11500 /* /: */, 11500 /* /; */, 11500 /* /< */, 11500 /* /= */, 11500 /* /> */, 11500 /* /? */, 11500 /* /@ */, 10375 /* /A */, 11500 /* /B */, 10950 /* /C */, 11500 /* /D */, 11500 /* /E */, 11500 /* /F */, 10950 /* /G */, 11500 /* /H */, 11500 /* /I */, 11500 /* /J */, 11500 /* /K */, 11500 /* /L */, 11500 /* /M */, 11500 /* /N */, 10950 /* /O */, 11500 /* /P */, 10950 /* /Q */, 11500 /* /R */, 11000 /* /S */, 12000 /* /T */, 11500 /* /U */, 11750 /* /V */, 11625 /* /W */, 11500 /* /X */, 11750 /* /Y */, 11500 /* /Z */, 11500 /* /[ */, 11500 /* /\ */, 11500 /* /] */, 11500 /* /^ */, 11500 /* /_ */, 11500 /* /` */, 10500 /* /a */, 11500 /* /b */, 10625 /* /c */, 10625 /* /d */, 10625 /* /e */, 11500 /* /f */, 10875 /* /g */, 11500 /* /h */, 11500 /* /i */, 11500 /* /j */, 11500 /* /k */, 11500 /* /l */, 11250 /* /m */, 11250 /* /n */, 10625 /* /o */, 11500 /* /p */, 10625 /* /q */, 11250 /* /r */, 11500 /* /s */, 11500 /* /t */, 11500 /* /u */, 11500 /* /v */, 11500 /* /w */, 11500 /* /x */, 11500 /* /y */, 11500 /* /z */, 11500 /* /{ */, 11500 /* /| */, 11500 /* /} */, 11500 /* /~ */}, + {15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0 */, 15000 /* 0! */, 15000 /* 0" */, 15000 /* 0# */, 15000 /* 0$ */, 15000 /* 0% */, 15000 /* 0& */, 15000 /* 0' */, 15000 /* 0( */, 15000 /* 0) */, 15000 /* 0* */, 15000 /* 0+ */, 15000 /* 0, */, 15000 /* 0- */, 15000 /* 0. */, 15000 /* 0/ */, 15000 /* 00 */, 15000 /* 01 */, 15000 /* 02 */, 15000 /* 03 */, 15000 /* 04 */, 15000 /* 05 */, 15000 /* 06 */, 15000 /* 07 */, 15000 /* 08 */, 15000 /* 09 */, 15000 /* 0: */, 15000 /* 0; */, 15000 /* 0< */, 15000 /* 0= */, 15000 /* 0> */, 15000 /* 0? */, 15000 /* 0@ */, 15000 /* 0A */, 15000 /* 0B */, 15000 /* 0C */, 15000 /* 0D */, 15000 /* 0E */, 15000 /* 0F */, 15000 /* 0G */, 15000 /* 0H */, 15000 /* 0I */, 15000 /* 0J */, 15000 /* 0K */, 15000 /* 0L */, 15000 /* 0M */, 15000 /* 0N */, 15000 /* 0O */, 15000 /* 0P */, 15000 /* 0Q */, 15000 /* 0R */, 15000 /* 0S */, 15000 /* 0T */, 15000 /* 0U */, 15000 /* 0V */, 15000 /* 0W */, 15000 /* 0X */, 15000 /* 0Y */, 15000 /* 0Z */, 15000 /* 0[ */, 15000 /* 0\ */, 15000 /* 0] */, 15000 /* 0^ */, 15000 /* 0_ */, 15000 /* 0` */, 15000 /* 0a */, 15000 /* 0b */, 15000 /* 0c */, 15000 /* 0d */, 15000 /* 0e */, 15000 /* 0f */, 15000 /* 0g */, 15000 /* 0h */, 15000 /* 0i */, 15000 /* 0j */, 15000 /* 0k */, 15000 /* 0l */, 15000 /* 0m */, 15000 /* 0n */, 15000 /* 0o */, 15000 /* 0p */, 15000 /* 0q */, 15000 /* 0r */, 15000 /* 0s */, 15000 /* 0t */, 15000 /* 0u */, 15000 /* 0v */, 15000 /* 0w */, 15000 /* 0x */, 15000 /* 0y */, 15000 /* 0z */, 15000 /* 0{ */, 15000 /* 0| */, 15000 /* 0} */, 15000 /* 0~ */}, + {15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1 */, 15000 /* 1! */, 15000 /* 1" */, 15000 /* 1# */, 15000 /* 1$ */, 15000 /* 1% */, 15000 /* 1& */, 15000 /* 1' */, 15000 /* 1( */, 15000 /* 1) */, 15000 /* 1* */, 15000 /* 1+ */, 15000 /* 1, */, 15000 /* 1- */, 15000 /* 1. */, 15000 /* 1/ */, 15000 /* 10 */, 15000 /* 11 */, 15000 /* 12 */, 15000 /* 13 */, 15000 /* 14 */, 15000 /* 15 */, 15000 /* 16 */, 15000 /* 17 */, 15000 /* 18 */, 15000 /* 19 */, 15000 /* 1: */, 15000 /* 1; */, 15000 /* 1< */, 15000 /* 1= */, 15000 /* 1> */, 15000 /* 1? */, 15000 /* 1@ */, 15000 /* 1A */, 15000 /* 1B */, 15000 /* 1C */, 15000 /* 1D */, 15000 /* 1E */, 15000 /* 1F */, 15000 /* 1G */, 15000 /* 1H */, 15000 /* 1I */, 15000 /* 1J */, 15000 /* 1K */, 15000 /* 1L */, 15000 /* 1M */, 15000 /* 1N */, 15000 /* 1O */, 15000 /* 1P */, 15000 /* 1Q */, 15000 /* 1R */, 15000 /* 1S */, 15000 /* 1T */, 15000 /* 1U */, 15000 /* 1V */, 15000 /* 1W */, 15000 /* 1X */, 15000 /* 1Y */, 15000 /* 1Z */, 15000 /* 1[ */, 15000 /* 1\ */, 15000 /* 1] */, 15000 /* 1^ */, 15000 /* 1_ */, 15000 /* 1` */, 15000 /* 1a */, 15000 /* 1b */, 15000 /* 1c */, 15000 /* 1d */, 15000 /* 1e */, 15000 /* 1f */, 15000 /* 1g */, 15000 /* 1h */, 15000 /* 1i */, 15000 /* 1j */, 15000 /* 1k */, 15000 /* 1l */, 15000 /* 1m */, 15000 /* 1n */, 15000 /* 1o */, 15000 /* 1p */, 15000 /* 1q */, 15000 /* 1r */, 15000 /* 1s */, 15000 /* 1t */, 15000 /* 1u */, 15000 /* 1v */, 15000 /* 1w */, 15000 /* 1x */, 15000 /* 1y */, 15000 /* 1z */, 15000 /* 1{ */, 15000 /* 1| */, 15000 /* 1} */, 15000 /* 1~ */}, + {15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2 */, 15000 /* 2! */, 15000 /* 2" */, 15000 /* 2# */, 15000 /* 2$ */, 15000 /* 2% */, 15000 /* 2& */, 15000 /* 2' */, 15000 /* 2( */, 15000 /* 2) */, 15000 /* 2* */, 15000 /* 2+ */, 15000 /* 2, */, 15000 /* 2- */, 15000 /* 2. */, 15000 /* 2/ */, 15000 /* 20 */, 15000 /* 21 */, 15000 /* 22 */, 15000 /* 23 */, 15000 /* 24 */, 15000 /* 25 */, 15000 /* 26 */, 15000 /* 27 */, 15000 /* 28 */, 15000 /* 29 */, 15000 /* 2: */, 15000 /* 2; */, 15000 /* 2< */, 15000 /* 2= */, 15000 /* 2> */, 15000 /* 2? */, 15000 /* 2@ */, 15000 /* 2A */, 15000 /* 2B */, 15000 /* 2C */, 15000 /* 2D */, 15000 /* 2E */, 15000 /* 2F */, 15000 /* 2G */, 15000 /* 2H */, 15000 /* 2I */, 15000 /* 2J */, 15000 /* 2K */, 15000 /* 2L */, 15000 /* 2M */, 15000 /* 2N */, 15000 /* 2O */, 15000 /* 2P */, 15000 /* 2Q */, 15000 /* 2R */, 15000 /* 2S */, 15000 /* 2T */, 15000 /* 2U */, 15000 /* 2V */, 15000 /* 2W */, 15000 /* 2X */, 15000 /* 2Y */, 15000 /* 2Z */, 15000 /* 2[ */, 15000 /* 2\ */, 15000 /* 2] */, 15000 /* 2^ */, 15000 /* 2_ */, 15000 /* 2` */, 15000 /* 2a */, 15000 /* 2b */, 15000 /* 2c */, 15000 /* 2d */, 15000 /* 2e */, 15000 /* 2f */, 15000 /* 2g */, 15000 /* 2h */, 15000 /* 2i */, 15000 /* 2j */, 15000 /* 2k */, 15000 /* 2l */, 15000 /* 2m */, 15000 /* 2n */, 15000 /* 2o */, 15000 /* 2p */, 15000 /* 2q */, 15000 /* 2r */, 15000 /* 2s */, 15000 /* 2t */, 15000 /* 2u */, 15000 /* 2v */, 15000 /* 2w */, 15000 /* 2x */, 15000 /* 2y */, 15000 /* 2z */, 15000 /* 2{ */, 15000 /* 2| */, 15000 /* 2} */, 15000 /* 2~ */}, + {15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3 */, 15000 /* 3! */, 15000 /* 3" */, 15000 /* 3# */, 15000 /* 3$ */, 15000 /* 3% */, 15000 /* 3& */, 15000 /* 3' */, 15000 /* 3( */, 15000 /* 3) */, 15000 /* 3* */, 15000 /* 3+ */, 15000 /* 3, */, 15000 /* 3- */, 15000 /* 3. */, 15000 /* 3/ */, 15000 /* 30 */, 15000 /* 31 */, 15000 /* 32 */, 15000 /* 33 */, 15000 /* 34 */, 15000 /* 35 */, 15000 /* 36 */, 15000 /* 37 */, 15000 /* 38 */, 15000 /* 39 */, 15000 /* 3: */, 15000 /* 3; */, 15000 /* 3< */, 15000 /* 3= */, 15000 /* 3> */, 15000 /* 3? */, 15000 /* 3@ */, 15000 /* 3A */, 15000 /* 3B */, 15000 /* 3C */, 15000 /* 3D */, 15000 /* 3E */, 15000 /* 3F */, 15000 /* 3G */, 15000 /* 3H */, 15000 /* 3I */, 15000 /* 3J */, 15000 /* 3K */, 15000 /* 3L */, 15000 /* 3M */, 15000 /* 3N */, 15000 /* 3O */, 15000 /* 3P */, 15000 /* 3Q */, 15000 /* 3R */, 15000 /* 3S */, 15000 /* 3T */, 15000 /* 3U */, 15000 /* 3V */, 15000 /* 3W */, 15000 /* 3X */, 15000 /* 3Y */, 15000 /* 3Z */, 15000 /* 3[ */, 15000 /* 3\ */, 15000 /* 3] */, 15000 /* 3^ */, 15000 /* 3_ */, 15000 /* 3` */, 15000 /* 3a */, 15000 /* 3b */, 15000 /* 3c */, 15000 /* 3d */, 15000 /* 3e */, 15000 /* 3f */, 15000 /* 3g */, 15000 /* 3h */, 15000 /* 3i */, 15000 /* 3j */, 15000 /* 3k */, 15000 /* 3l */, 15000 /* 3m */, 15000 /* 3n */, 15000 /* 3o */, 15000 /* 3p */, 15000 /* 3q */, 15000 /* 3r */, 15000 /* 3s */, 15000 /* 3t */, 15000 /* 3u */, 15000 /* 3v */, 15000 /* 3w */, 15000 /* 3x */, 15000 /* 3y */, 15000 /* 3z */, 15000 /* 3{ */, 15000 /* 3| */, 15000 /* 3} */, 15000 /* 3~ */}, + {15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4 */, 15000 /* 4! */, 15000 /* 4" */, 15000 /* 4# */, 15000 /* 4$ */, 15000 /* 4% */, 15000 /* 4& */, 15000 /* 4' */, 15000 /* 4( */, 15000 /* 4) */, 15000 /* 4* */, 15000 /* 4+ */, 15000 /* 4, */, 15000 /* 4- */, 15000 /* 4. */, 15000 /* 4/ */, 15000 /* 40 */, 15000 /* 41 */, 15000 /* 42 */, 15000 /* 43 */, 15000 /* 44 */, 15000 /* 45 */, 15000 /* 46 */, 15000 /* 47 */, 15000 /* 48 */, 15000 /* 49 */, 15000 /* 4: */, 15000 /* 4; */, 15000 /* 4< */, 15000 /* 4= */, 15000 /* 4> */, 15000 /* 4? */, 15000 /* 4@ */, 15000 /* 4A */, 15000 /* 4B */, 15000 /* 4C */, 15000 /* 4D */, 15000 /* 4E */, 15000 /* 4F */, 15000 /* 4G */, 15000 /* 4H */, 15000 /* 4I */, 15000 /* 4J */, 15000 /* 4K */, 15000 /* 4L */, 15000 /* 4M */, 15000 /* 4N */, 15000 /* 4O */, 15000 /* 4P */, 15000 /* 4Q */, 15000 /* 4R */, 15000 /* 4S */, 15000 /* 4T */, 15000 /* 4U */, 15000 /* 4V */, 15000 /* 4W */, 15000 /* 4X */, 15000 /* 4Y */, 15000 /* 4Z */, 15000 /* 4[ */, 15000 /* 4\ */, 15000 /* 4] */, 15000 /* 4^ */, 15000 /* 4_ */, 15000 /* 4` */, 15000 /* 4a */, 15000 /* 4b */, 15000 /* 4c */, 15000 /* 4d */, 15000 /* 4e */, 15000 /* 4f */, 15000 /* 4g */, 15000 /* 4h */, 15000 /* 4i */, 15000 /* 4j */, 15000 /* 4k */, 15000 /* 4l */, 15000 /* 4m */, 15000 /* 4n */, 15000 /* 4o */, 15000 /* 4p */, 15000 /* 4q */, 15000 /* 4r */, 15000 /* 4s */, 15000 /* 4t */, 15000 /* 4u */, 15000 /* 4v */, 15000 /* 4w */, 15000 /* 4x */, 15000 /* 4y */, 15000 /* 4z */, 15000 /* 4{ */, 15000 /* 4| */, 15000 /* 4} */, 15000 /* 4~ */}, + {15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5 */, 15000 /* 5! */, 15000 /* 5" */, 15000 /* 5# */, 15000 /* 5$ */, 15000 /* 5% */, 15000 /* 5& */, 15000 /* 5' */, 15000 /* 5( */, 15000 /* 5) */, 15000 /* 5* */, 15000 /* 5+ */, 15000 /* 5, */, 15000 /* 5- */, 15000 /* 5. */, 15000 /* 5/ */, 15000 /* 50 */, 15000 /* 51 */, 15000 /* 52 */, 15000 /* 53 */, 15000 /* 54 */, 15000 /* 55 */, 15000 /* 56 */, 15000 /* 57 */, 15000 /* 58 */, 15000 /* 59 */, 15000 /* 5: */, 15000 /* 5; */, 15000 /* 5< */, 15000 /* 5= */, 15000 /* 5> */, 15000 /* 5? */, 15000 /* 5@ */, 15000 /* 5A */, 15000 /* 5B */, 15000 /* 5C */, 15000 /* 5D */, 15000 /* 5E */, 15000 /* 5F */, 15000 /* 5G */, 15000 /* 5H */, 15000 /* 5I */, 15000 /* 5J */, 15000 /* 5K */, 15000 /* 5L */, 15000 /* 5M */, 15000 /* 5N */, 15000 /* 5O */, 15000 /* 5P */, 15000 /* 5Q */, 15000 /* 5R */, 15000 /* 5S */, 15000 /* 5T */, 15000 /* 5U */, 15000 /* 5V */, 15000 /* 5W */, 15000 /* 5X */, 15000 /* 5Y */, 15000 /* 5Z */, 15000 /* 5[ */, 15000 /* 5\ */, 15000 /* 5] */, 15000 /* 5^ */, 15000 /* 5_ */, 15000 /* 5` */, 15000 /* 5a */, 15000 /* 5b */, 15000 /* 5c */, 15000 /* 5d */, 15000 /* 5e */, 15000 /* 5f */, 15000 /* 5g */, 15000 /* 5h */, 15000 /* 5i */, 15000 /* 5j */, 15000 /* 5k */, 15000 /* 5l */, 15000 /* 5m */, 15000 /* 5n */, 15000 /* 5o */, 15000 /* 5p */, 15000 /* 5q */, 15000 /* 5r */, 15000 /* 5s */, 15000 /* 5t */, 15000 /* 5u */, 15000 /* 5v */, 15000 /* 5w */, 15000 /* 5x */, 15000 /* 5y */, 15000 /* 5z */, 15000 /* 5{ */, 15000 /* 5| */, 15000 /* 5} */, 15000 /* 5~ */}, + {15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6 */, 15000 /* 6! */, 15000 /* 6" */, 15000 /* 6# */, 15000 /* 6$ */, 15000 /* 6% */, 15000 /* 6& */, 15000 /* 6' */, 15000 /* 6( */, 15000 /* 6) */, 15000 /* 6* */, 15000 /* 6+ */, 15000 /* 6, */, 15000 /* 6- */, 15000 /* 6. */, 15000 /* 6/ */, 15000 /* 60 */, 15000 /* 61 */, 15000 /* 62 */, 15000 /* 63 */, 15000 /* 64 */, 15000 /* 65 */, 15000 /* 66 */, 15000 /* 67 */, 15000 /* 68 */, 15000 /* 69 */, 15000 /* 6: */, 15000 /* 6; */, 15000 /* 6< */, 15000 /* 6= */, 15000 /* 6> */, 15000 /* 6? */, 15000 /* 6@ */, 15000 /* 6A */, 15000 /* 6B */, 15000 /* 6C */, 15000 /* 6D */, 15000 /* 6E */, 15000 /* 6F */, 15000 /* 6G */, 15000 /* 6H */, 15000 /* 6I */, 15000 /* 6J */, 15000 /* 6K */, 15000 /* 6L */, 15000 /* 6M */, 15000 /* 6N */, 15000 /* 6O */, 15000 /* 6P */, 15000 /* 6Q */, 15000 /* 6R */, 15000 /* 6S */, 15000 /* 6T */, 15000 /* 6U */, 15000 /* 6V */, 15000 /* 6W */, 15000 /* 6X */, 15000 /* 6Y */, 15000 /* 6Z */, 15000 /* 6[ */, 15000 /* 6\ */, 15000 /* 6] */, 15000 /* 6^ */, 15000 /* 6_ */, 15000 /* 6` */, 15000 /* 6a */, 15000 /* 6b */, 15000 /* 6c */, 15000 /* 6d */, 15000 /* 6e */, 15000 /* 6f */, 15000 /* 6g */, 15000 /* 6h */, 15000 /* 6i */, 15000 /* 6j */, 15000 /* 6k */, 15000 /* 6l */, 15000 /* 6m */, 15000 /* 6n */, 15000 /* 6o */, 15000 /* 6p */, 15000 /* 6q */, 15000 /* 6r */, 15000 /* 6s */, 15000 /* 6t */, 15000 /* 6u */, 15000 /* 6v */, 15000 /* 6w */, 15000 /* 6x */, 15000 /* 6y */, 15000 /* 6z */, 15000 /* 6{ */, 15000 /* 6| */, 15000 /* 6} */, 15000 /* 6~ */}, + {15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7 */, 15000 /* 7! */, 15000 /* 7" */, 15000 /* 7# */, 15000 /* 7$ */, 15000 /* 7% */, 15000 /* 7& */, 15000 /* 7' */, 15000 /* 7( */, 15000 /* 7) */, 15000 /* 7* */, 15000 /* 7+ */, 15000 /* 7, */, 15000 /* 7- */, 15000 /* 7. */, 15000 /* 7/ */, 15000 /* 70 */, 15000 /* 71 */, 15000 /* 72 */, 15000 /* 73 */, 15000 /* 74 */, 15000 /* 75 */, 15000 /* 76 */, 15000 /* 77 */, 15000 /* 78 */, 15000 /* 79 */, 15000 /* 7: */, 15000 /* 7; */, 15000 /* 7< */, 15000 /* 7= */, 15000 /* 7> */, 15000 /* 7? */, 15000 /* 7@ */, 15000 /* 7A */, 15000 /* 7B */, 15000 /* 7C */, 15000 /* 7D */, 15000 /* 7E */, 15000 /* 7F */, 15000 /* 7G */, 15000 /* 7H */, 15000 /* 7I */, 15000 /* 7J */, 15000 /* 7K */, 15000 /* 7L */, 15000 /* 7M */, 15000 /* 7N */, 15000 /* 7O */, 15000 /* 7P */, 15000 /* 7Q */, 15000 /* 7R */, 15000 /* 7S */, 15000 /* 7T */, 15000 /* 7U */, 15000 /* 7V */, 15000 /* 7W */, 15000 /* 7X */, 15000 /* 7Y */, 15000 /* 7Z */, 15000 /* 7[ */, 15000 /* 7\ */, 15000 /* 7] */, 15000 /* 7^ */, 15000 /* 7_ */, 15000 /* 7` */, 15000 /* 7a */, 15000 /* 7b */, 15000 /* 7c */, 15000 /* 7d */, 15000 /* 7e */, 15000 /* 7f */, 15000 /* 7g */, 15000 /* 7h */, 15000 /* 7i */, 15000 /* 7j */, 15000 /* 7k */, 15000 /* 7l */, 15000 /* 7m */, 15000 /* 7n */, 15000 /* 7o */, 15000 /* 7p */, 15000 /* 7q */, 15000 /* 7r */, 15000 /* 7s */, 15000 /* 7t */, 15000 /* 7u */, 15000 /* 7v */, 15000 /* 7w */, 15000 /* 7x */, 15000 /* 7y */, 15000 /* 7z */, 15000 /* 7{ */, 15000 /* 7| */, 15000 /* 7} */, 15000 /* 7~ */}, + {15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8 */, 15000 /* 8! */, 15000 /* 8" */, 15000 /* 8# */, 15000 /* 8$ */, 15000 /* 8% */, 15000 /* 8& */, 15000 /* 8' */, 15000 /* 8( */, 15000 /* 8) */, 15000 /* 8* */, 15000 /* 8+ */, 15000 /* 8, */, 15000 /* 8- */, 15000 /* 8. */, 15000 /* 8/ */, 15000 /* 80 */, 15000 /* 81 */, 15000 /* 82 */, 15000 /* 83 */, 15000 /* 84 */, 15000 /* 85 */, 15000 /* 86 */, 15000 /* 87 */, 15000 /* 88 */, 15000 /* 89 */, 15000 /* 8: */, 15000 /* 8; */, 15000 /* 8< */, 15000 /* 8= */, 15000 /* 8> */, 15000 /* 8? */, 15000 /* 8@ */, 15000 /* 8A */, 15000 /* 8B */, 15000 /* 8C */, 15000 /* 8D */, 15000 /* 8E */, 15000 /* 8F */, 15000 /* 8G */, 15000 /* 8H */, 15000 /* 8I */, 15000 /* 8J */, 15000 /* 8K */, 15000 /* 8L */, 15000 /* 8M */, 15000 /* 8N */, 15000 /* 8O */, 15000 /* 8P */, 15000 /* 8Q */, 15000 /* 8R */, 15000 /* 8S */, 15000 /* 8T */, 15000 /* 8U */, 15000 /* 8V */, 15000 /* 8W */, 15000 /* 8X */, 15000 /* 8Y */, 15000 /* 8Z */, 15000 /* 8[ */, 15000 /* 8\ */, 15000 /* 8] */, 15000 /* 8^ */, 15000 /* 8_ */, 15000 /* 8` */, 15000 /* 8a */, 15000 /* 8b */, 15000 /* 8c */, 15000 /* 8d */, 15000 /* 8e */, 15000 /* 8f */, 15000 /* 8g */, 15000 /* 8h */, 15000 /* 8i */, 15000 /* 8j */, 15000 /* 8k */, 15000 /* 8l */, 15000 /* 8m */, 15000 /* 8n */, 15000 /* 8o */, 15000 /* 8p */, 15000 /* 8q */, 15000 /* 8r */, 15000 /* 8s */, 15000 /* 8t */, 15000 /* 8u */, 15000 /* 8v */, 15000 /* 8w */, 15000 /* 8x */, 15000 /* 8y */, 15000 /* 8z */, 15000 /* 8{ */, 15000 /* 8| */, 15000 /* 8} */, 15000 /* 8~ */}, + {15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9 */, 15000 /* 9! */, 15000 /* 9" */, 15000 /* 9# */, 15000 /* 9$ */, 15000 /* 9% */, 15000 /* 9& */, 15000 /* 9' */, 15000 /* 9( */, 15000 /* 9) */, 15000 /* 9* */, 15000 /* 9+ */, 15000 /* 9, */, 15000 /* 9- */, 15000 /* 9. */, 15000 /* 9/ */, 15000 /* 90 */, 15000 /* 91 */, 15000 /* 92 */, 15000 /* 93 */, 15000 /* 94 */, 15000 /* 95 */, 15000 /* 96 */, 15000 /* 97 */, 15000 /* 98 */, 15000 /* 99 */, 15000 /* 9: */, 15000 /* 9; */, 15000 /* 9< */, 15000 /* 9= */, 15000 /* 9> */, 15000 /* 9? */, 15000 /* 9@ */, 15000 /* 9A */, 15000 /* 9B */, 15000 /* 9C */, 15000 /* 9D */, 15000 /* 9E */, 15000 /* 9F */, 15000 /* 9G */, 15000 /* 9H */, 15000 /* 9I */, 15000 /* 9J */, 15000 /* 9K */, 15000 /* 9L */, 15000 /* 9M */, 15000 /* 9N */, 15000 /* 9O */, 15000 /* 9P */, 15000 /* 9Q */, 15000 /* 9R */, 15000 /* 9S */, 15000 /* 9T */, 15000 /* 9U */, 15000 /* 9V */, 15000 /* 9W */, 15000 /* 9X */, 15000 /* 9Y */, 15000 /* 9Z */, 15000 /* 9[ */, 15000 /* 9\ */, 15000 /* 9] */, 15000 /* 9^ */, 15000 /* 9_ */, 15000 /* 9` */, 15000 /* 9a */, 15000 /* 9b */, 15000 /* 9c */, 15000 /* 9d */, 15000 /* 9e */, 15000 /* 9f */, 15000 /* 9g */, 15000 /* 9h */, 15000 /* 9i */, 15000 /* 9j */, 15000 /* 9k */, 15000 /* 9l */, 15000 /* 9m */, 15000 /* 9n */, 15000 /* 9o */, 15000 /* 9p */, 15000 /* 9q */, 15000 /* 9r */, 15000 /* 9s */, 15000 /* 9t */, 15000 /* 9u */, 15000 /* 9v */, 15000 /* 9w */, 15000 /* 9x */, 15000 /* 9y */, 15000 /* 9z */, 15000 /* 9{ */, 15000 /* 9| */, 15000 /* 9} */, 15000 /* 9~ */}, + {8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* : */, 8250 /* :! */, 8250 /* :" */, 8250 /* :# */, 8250 /* :$ */, 8250 /* :% */, 8250 /* :& */, 8250 /* :' */, 8250 /* :( */, 8250 /* :) */, 8250 /* :* */, 8250 /* :+ */, 8250 /* :, */, 8250 /* :- */, 8250 /* :. */, 8250 /* :/ */, 8250 /* :0 */, 8250 /* :1 */, 8250 /* :2 */, 8250 /* :3 */, 8250 /* :4 */, 8250 /* :5 */, 8250 /* :6 */, 8250 /* :7 */, 8250 /* :8 */, 8250 /* :9 */, 8250 /* :: */, 8250 /* :; */, 8250 /* :< */, 8250 /* := */, 8250 /* :> */, 8250 /* :? */, 8250 /* :@ */, 8250 /* :A */, 8250 /* :B */, 8250 /* :C */, 8250 /* :D */, 8250 /* :E */, 8250 /* :F */, 8250 /* :G */, 8250 /* :H */, 8250 /* :I */, 8250 /* :J */, 8250 /* :K */, 8250 /* :L */, 8250 /* :M */, 8250 /* :N */, 8250 /* :O */, 8250 /* :P */, 8250 /* :Q */, 8250 /* :R */, 8250 /* :S */, 7250 /* :T */, 8250 /* :U */, 7500 /* :V */, 8250 /* :W */, 7875 /* :X */, 7000 /* :Y */, 8250 /* :Z */, 8250 /* :[ */, 8250 /* :\ */, 8250 /* :] */, 8250 /* :^ */, 8250 /* :_ */, 8250 /* :` */, 8250 /* :a */, 8250 /* :b */, 8250 /* :c */, 8250 /* :d */, 8250 /* :e */, 8250 /* :f */, 8250 /* :g */, 8250 /* :h */, 8250 /* :i */, 8250 /* :j */, 8250 /* :k */, 8250 /* :l */, 8250 /* :m */, 8250 /* :n */, 8250 /* :o */, 8250 /* :p */, 8250 /* :q */, 8250 /* :r */, 8250 /* :s */, 8250 /* :t */, 8250 /* :u */, 8250 /* :v */, 8250 /* :w */, 8250 /* :x */, 8250 /* :y */, 8250 /* :z */, 8250 /* :{ */, 8250 /* :| */, 8250 /* :} */, 8250 /* :~ */}, + {8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ; */, 8250 /* ;! */, 8250 /* ;" */, 8250 /* ;# */, 8250 /* ;$ */, 8250 /* ;% */, 8250 /* ;& */, 8250 /* ;' */, 8250 /* ;( */, 8250 /* ;) */, 8250 /* ;* */, 8250 /* ;+ */, 8250 /* ;, */, 8250 /* ;- */, 8250 /* ;. */, 8250 /* ;/ */, 8250 /* ;0 */, 8250 /* ;1 */, 8250 /* ;2 */, 8250 /* ;3 */, 8250 /* ;4 */, 8250 /* ;5 */, 8250 /* ;6 */, 8250 /* ;7 */, 8250 /* ;8 */, 8250 /* ;9 */, 8250 /* ;: */, 8250 /* ;; */, 8250 /* ;< */, 8250 /* ;= */, 8250 /* ;> */, 8250 /* ;? */, 8250 /* ;@ */, 8250 /* ;A */, 8250 /* ;B */, 8250 /* ;C */, 8250 /* ;D */, 8250 /* ;E */, 8250 /* ;F */, 8250 /* ;G */, 8250 /* ;H */, 8250 /* ;I */, 8250 /* ;J */, 8250 /* ;K */, 8250 /* ;L */, 8250 /* ;M */, 8250 /* ;N */, 8250 /* ;O */, 8250 /* ;P */, 8250 /* ;Q */, 8250 /* ;R */, 8250 /* ;S */, 7250 /* ;T */, 8250 /* ;U */, 7500 /* ;V */, 8250 /* ;W */, 7875 /* ;X */, 7000 /* ;Y */, 8250 /* ;Z */, 8250 /* ;[ */, 8250 /* ;\ */, 8250 /* ;] */, 8250 /* ;^ */, 8250 /* ;_ */, 8250 /* ;` */, 8250 /* ;a */, 8250 /* ;b */, 8250 /* ;c */, 8250 /* ;d */, 8250 /* ;e */, 8250 /* ;f */, 8250 /* ;g */, 8250 /* ;h */, 8250 /* ;i */, 8250 /* ;j */, 8250 /* ;k */, 8250 /* ;l */, 8250 /* ;m */, 8250 /* ;n */, 8250 /* ;o */, 8250 /* ;p */, 8250 /* ;q */, 8250 /* ;r */, 8250 /* ;s */, 8250 /* ;t */, 8250 /* ;u */, 8250 /* ;v */, 8250 /* ;w */, 8250 /* ;x */, 8250 /* ;y */, 8250 /* ;z */, 8250 /* ;{ */, 8250 /* ;| */, 8250 /* ;} */, 8250 /* ;~ */}, + {15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* < */, 15000 /* <! */, 15000 /* <" */, 15000 /* <# */, 15000 /* <$ */, 15000 /* <% */, 15000 /* <& */, 15000 /* <' */, 15000 /* <( */, 15000 /* <) */, 15000 /* <* */, 15000 /* <+ */, 15000 /* <, */, 15000 /* <- */, 15000 /* <. */, 15000 /* </ */, 15000 /* <0 */, 15000 /* <1 */, 15000 /* <2 */, 15000 /* <3 */, 15000 /* <4 */, 15000 /* <5 */, 15000 /* <6 */, 15000 /* <7 */, 15000 /* <8 */, 15000 /* <9 */, 15000 /* <: */, 15000 /* <; */, 15000 /* << */, 15000 /* <= */, 15000 /* <> */, 15000 /* <? */, 15000 /* <@ */, 15000 /* <A */, 15000 /* <B */, 15000 /* <C */, 15000 /* <D */, 15000 /* <E */, 15000 /* <F */, 15000 /* <G */, 15000 /* <H */, 15000 /* <I */, 15000 /* <J */, 15000 /* <K */, 15000 /* <L */, 15000 /* <M */, 15000 /* <N */, 15000 /* <O */, 15000 /* <P */, 15000 /* <Q */, 15000 /* <R */, 15000 /* <S */, 15000 /* <T */, 15000 /* <U */, 15000 /* <V */, 15000 /* <W */, 15000 /* <X */, 15000 /* <Y */, 15000 /* <Z */, 15000 /* <[ */, 15000 /* <\ */, 15000 /* <] */, 15000 /* <^ */, 15000 /* <_ */, 15000 /* <` */, 15000 /* <a */, 15000 /* <b */, 15000 /* <c */, 15000 /* <d */, 15000 /* <e */, 15000 /* <f */, 15000 /* <g */, 15000 /* <h */, 15000 /* <i */, 15000 /* <j */, 15000 /* <k */, 15000 /* <l */, 15000 /* <m */, 15000 /* <n */, 15000 /* <o */, 15000 /* <p */, 15000 /* <q */, 15000 /* <r */, 15000 /* <s */, 15000 /* <t */, 15000 /* <u */, 15000 /* <v */, 15000 /* <w */, 15000 /* <x */, 15000 /* <y */, 15000 /* <z */, 15000 /* <{ */, 15000 /* <| */, 15000 /* <} */, 15000 /* <~ */}, + {15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* = */, 15000 /* =! */, 15000 /* =" */, 15000 /* =# */, 15000 /* =$ */, 15000 /* =% */, 15000 /* =& */, 15000 /* =' */, 15000 /* =( */, 15000 /* =) */, 15000 /* =* */, 15000 /* =+ */, 15000 /* =, */, 15000 /* =- */, 15000 /* =. */, 15000 /* =/ */, 15000 /* =0 */, 15000 /* =1 */, 15000 /* =2 */, 15000 /* =3 */, 15000 /* =4 */, 15000 /* =5 */, 15000 /* =6 */, 15000 /* =7 */, 15000 /* =8 */, 15000 /* =9 */, 15000 /* =: */, 15000 /* =; */, 15000 /* =< */, 15000 /* == */, 15000 /* => */, 15000 /* =? */, 15000 /* =@ */, 15000 /* =A */, 15000 /* =B */, 15000 /* =C */, 15000 /* =D */, 15000 /* =E */, 15000 /* =F */, 15000 /* =G */, 15000 /* =H */, 15000 /* =I */, 15000 /* =J */, 15000 /* =K */, 15000 /* =L */, 15000 /* =M */, 15000 /* =N */, 15000 /* =O */, 15000 /* =P */, 15000 /* =Q */, 15000 /* =R */, 15000 /* =S */, 15000 /* =T */, 15000 /* =U */, 15000 /* =V */, 15000 /* =W */, 15000 /* =X */, 15000 /* =Y */, 15000 /* =Z */, 15000 /* =[ */, 15000 /* =\ */, 15000 /* =] */, 15000 /* =^ */, 15000 /* =_ */, 15000 /* =` */, 15000 /* =a */, 15000 /* =b */, 15000 /* =c */, 15000 /* =d */, 15000 /* =e */, 15000 /* =f */, 15000 /* =g */, 15000 /* =h */, 15000 /* =i */, 15000 /* =j */, 15000 /* =k */, 15000 /* =l */, 15000 /* =m */, 15000 /* =n */, 15000 /* =o */, 15000 /* =p */, 15000 /* =q */, 15000 /* =r */, 15000 /* =s */, 15000 /* =t */, 15000 /* =u */, 15000 /* =v */, 15000 /* =w */, 15000 /* =x */, 15000 /* =y */, 15000 /* =z */, 15000 /* ={ */, 15000 /* =| */, 15000 /* =} */, 15000 /* =~ */}, + {15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* > */, 15000 /* >! */, 15000 /* >" */, 15000 /* ># */, 15000 /* >$ */, 15000 /* >% */, 15000 /* >& */, 15000 /* >' */, 15000 /* >( */, 15000 /* >) */, 15000 /* >* */, 15000 /* >+ */, 15000 /* >, */, 15000 /* >- */, 15000 /* >. */, 15000 /* >/ */, 15000 /* >0 */, 15000 /* >1 */, 15000 /* >2 */, 15000 /* >3 */, 15000 /* >4 */, 15000 /* >5 */, 15000 /* >6 */, 15000 /* >7 */, 15000 /* >8 */, 15000 /* >9 */, 15000 /* >: */, 15000 /* >; */, 15000 /* >< */, 15000 /* >= */, 15000 /* >> */, 15000 /* >? */, 15000 /* >@ */, 15000 /* >A */, 15000 /* >B */, 15000 /* >C */, 15000 /* >D */, 15000 /* >E */, 15000 /* >F */, 15000 /* >G */, 15000 /* >H */, 15000 /* >I */, 15000 /* >J */, 15000 /* >K */, 15000 /* >L */, 15000 /* >M */, 15000 /* >N */, 15000 /* >O */, 15000 /* >P */, 15000 /* >Q */, 15000 /* >R */, 15000 /* >S */, 15000 /* >T */, 15000 /* >U */, 15000 /* >V */, 15000 /* >W */, 15000 /* >X */, 15000 /* >Y */, 15000 /* >Z */, 15000 /* >[ */, 15000 /* >\ */, 15000 /* >] */, 15000 /* >^ */, 15000 /* >_ */, 15000 /* >` */, 15000 /* >a */, 15000 /* >b */, 15000 /* >c */, 15000 /* >d */, 15000 /* >e */, 15000 /* >f */, 15000 /* >g */, 15000 /* >h */, 15000 /* >i */, 15000 /* >j */, 15000 /* >k */, 15000 /* >l */, 15000 /* >m */, 15000 /* >n */, 15000 /* >o */, 15000 /* >p */, 15000 /* >q */, 15000 /* >r */, 15000 /* >s */, 15000 /* >t */, 15000 /* >u */, 15000 /* >v */, 15000 /* >w */, 15000 /* >x */, 15000 /* >y */, 15000 /* >z */, 15000 /* >{ */, 15000 /* >| */, 15000 /* >} */, 15000 /* >~ */}, + {12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ? */, 12500 /* ?! */, 12500 /* ?" */, 12500 /* ?# */, 12500 /* ?$ */, 12500 /* ?% */, 12500 /* ?& */, 12500 /* ?' */, 12500 /* ?( */, 12500 /* ?) */, 12500 /* ?* */, 12500 /* ?+ */, 9500 /* ?, */, 12500 /* ?- */, 9500 /* ?. */, 12500 /* ?/ */, 12500 /* ?0 */, 12500 /* ?1 */, 12500 /* ?2 */, 12500 /* ?3 */, 12500 /* ?4 */, 12500 /* ?5 */, 12500 /* ?6 */, 12500 /* ?7 */, 12500 /* ?8 */, 12500 /* ?9 */, 12500 /* ?: */, 12500 /* ?; */, 12500 /* ?< */, 12500 /* ?= */, 12500 /* ?> */, 12500 /* ?? */, 12500 /* ?@ */, 11750 /* ?A */, 12500 /* ?B */, 12500 /* ?C */, 12500 /* ?D */, 12500 /* ?E */, 12500 /* ?F */, 12500 /* ?G */, 12500 /* ?H */, 12500 /* ?I */, 12500 /* ?J */, 12500 /* ?K */, 12500 /* ?L */, 12500 /* ?M */, 12500 /* ?N */, 12500 /* ?O */, 12500 /* ?P */, 12500 /* ?Q */, 12500 /* ?R */, 12500 /* ?S */, 12500 /* ?T */, 12500 /* ?U */, 12750 /* ?V */, 12875 /* ?W */, 12500 /* ?X */, 12500 /* ?Y */, 12500 /* ?Z */, 12500 /* ?[ */, 12500 /* ?\ */, 12500 /* ?] */, 12500 /* ?^ */, 12500 /* ?_ */, 12500 /* ?` */, 12500 /* ?a */, 12500 /* ?b */, 12500 /* ?c */, 12500 /* ?d */, 12500 /* ?e */, 12500 /* ?f */, 12500 /* ?g */, 12500 /* ?h */, 12500 /* ?i */, 12500 /* ?j */, 12500 /* ?k */, 12500 /* ?l */, 12500 /* ?m */, 12500 /* ?n */, 12500 /* ?o */, 12500 /* ?p */, 12500 /* ?q */, 12500 /* ?r */, 12500 /* ?s */, 12500 /* ?t */, 12500 /* ?u */, 12500 /* ?v */, 12500 /* ?w */, 12500 /* ?x */, 12500 /* ?y */, 12500 /* ?z */, 12500 /* ?{ */, 12500 /* ?| */, 12500 /* ?} */, 12500 /* ?~ */}, + {22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @ */, 22575 /* @! */, 22575 /* @" */, 22575 /* @# */, 22575 /* @$ */, 22575 /* @% */, 22575 /* @& */, 22575 /* @' */, 22575 /* @( */, 22575 /* @) */, 22575 /* @* */, 22575 /* @+ */, 22575 /* @, */, 22575 /* @- */, 22575 /* @. */, 22575 /* @/ */, 22575 /* @0 */, 22575 /* @1 */, 22575 /* @2 */, 22575 /* @3 */, 22575 /* @4 */, 22575 /* @5 */, 22575 /* @6 */, 22575 /* @7 */, 22575 /* @8 */, 22575 /* @9 */, 22575 /* @: */, 22575 /* @; */, 22575 /* @< */, 22575 /* @= */, 22575 /* @> */, 22575 /* @? */, 22575 /* @@ */, 22075 /* @A */, 22575 /* @B */, 22575 /* @C */, 22575 /* @D */, 22575 /* @E */, 22575 /* @F */, 22575 /* @G */, 22575 /* @H */, 22075 /* @I */, 21825 /* @J */, 22575 /* @K */, 22575 /* @L */, 22575 /* @M */, 22575 /* @N */, 22575 /* @O */, 22575 /* @P */, 22575 /* @Q */, 22575 /* @R */, 22325 /* @S */, 21825 /* @T */, 22575 /* @U */, 21825 /* @V */, 22075 /* @W */, 21575 /* @X */, 21575 /* @Y */, 22200 /* @Z */, 22575 /* @[ */, 22575 /* @\ */, 22575 /* @] */, 22575 /* @^ */, 22575 /* @_ */, 22575 /* @` */, 22575 /* @a */, 22575 /* @b */, 22575 /* @c */, 22575 /* @d */, 22575 /* @e */, 22575 /* @f */, 22450 /* @g */, 22575 /* @h */, 22575 /* @i */, 22575 /* @j */, 22575 /* @k */, 22575 /* @l */, 22575 /* @m */, 22575 /* @n */, 22575 /* @o */, 22575 /* @p */, 22575 /* @q */, 22575 /* @r */, 22575 /* @s */, 22575 /* @t */, 22575 /* @u */, 22575 /* @v */, 22325 /* @w */, 22075 /* @x */, 22325 /* @y */, 22325 /* @z */, 22575 /* @{ */, 22575 /* @| */, 22575 /* @} */, 22575 /* @~ */}, + {17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A */, 17125 /* A! */, 15875 /* A" */, 17125 /* A# */, 17125 /* A$ */, 17125 /* A% */, 16875 /* A& */, 15875 /* A' */, 17125 /* A( */, 17125 /* A) */, 15875 /* A* */, 17125 /* A+ */, 17625 /* A, */, 16750 /* A- */, 17625 /* A. */, 17125 /* A/ */, 17125 /* A0 */, 17125 /* A1 */, 17125 /* A2 */, 17125 /* A3 */, 17125 /* A4 */, 17125 /* A5 */, 17125 /* A6 */, 17125 /* A7 */, 17125 /* A8 */, 17125 /* A9 */, 17125 /* A: */, 17125 /* A; */, 17125 /* A< */, 17125 /* A= */, 17125 /* A> */, 15875 /* A? */, 16625 /* A@ */, 17375 /* AA */, 17125 /* AB */, 16375 /* AC */, 17125 /* AD */, 17125 /* AE */, 17125 /* AF */, 16375 /* AG */, 17125 /* AH */, 17125 /* AI */, 16775 /* AJ */, 17125 /* AK */, 17125 /* AL */, 17125 /* AM */, 17125 /* AN */, 16375 /* AO */, 17125 /* AP */, 16375 /* AQ */, 17125 /* AR */, 17125 /* AS */, 15500 /* AT */, 16525 /* AU */, 15775 /* AV */, 16450 /* AW */, 17250 /* AX */, 15375 /* AY */, 17125 /* AZ */, 17125 /* A[ */, 17125 /* A\ */, 17125 /* A] */, 17125 /* A^ */, 17500 /* A_ */, 17125 /* A` */, 17200 /* Aa */, 17125 /* Ab */, 17125 /* Ac */, 17125 /* Ad */, 17125 /* Ae */, 17125 /* Af */, 17125 /* Ag */, 17125 /* Ah */, 17125 /* Ai */, 17125 /* Aj */, 17125 /* Ak */, 17125 /* Al */, 17125 /* Am */, 17125 /* An */, 17125 /* Ao */, 17125 /* Ap */, 17125 /* Aq */, 17125 /* Ar */, 17125 /* As */, 16650 /* At */, 17125 /* Au */, 16375 /* Av */, 16625 /* Aw */, 17125 /* Ax */, 16750 /* Ay */, 17375 /* Az */, 17125 /* A{ */, 17125 /* A| */, 17125 /* A} */, 17125 /* A~ */}, + {16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B */, 16675 /* B! */, 16675 /* B" */, 16675 /* B# */, 16675 /* B$ */, 16675 /* B% */, 16675 /* B& */, 16675 /* B' */, 16675 /* B( */, 16675 /* B) */, 16675 /* B* */, 16675 /* B+ */, 16300 /* B, */, 16675 /* B- */, 16300 /* B. */, 16050 /* B/ */, 16675 /* B0 */, 16675 /* B1 */, 16675 /* B2 */, 16675 /* B3 */, 16675 /* B4 */, 16675 /* B5 */, 16675 /* B6 */, 16675 /* B7 */, 16675 /* B8 */, 16675 /* B9 */, 16675 /* B: */, 16675 /* B; */, 16675 /* B< */, 16675 /* B= */, 16675 /* B> */, 16675 /* B? */, 16675 /* B@ */, 16300 /* BA */, 16675 /* BB */, 16675 /* BC */, 16675 /* BD */, 16675 /* BE */, 16675 /* BF */, 16675 /* BG */, 16675 /* BH */, 16675 /* BI */, 16425 /* BJ */, 16675 /* BK */, 16675 /* BL */, 16675 /* BM */, 16675 /* BN */, 16675 /* BO */, 16675 /* BP */, 16675 /* BQ */, 16675 /* BR */, 16675 /* BS */, 16025 /* BT */, 16675 /* BU */, 15975 /* BV */, 16475 /* BW */, 16150 /* BX */, 15600 /* BY */, 16675 /* BZ */, 16675 /* B[ */, 16675 /* B\ */, 16675 /* B] */, 16675 /* B^ */, 15075 /* B_ */, 16675 /* B` */, 16675 /* Ba */, 16675 /* Bb */, 16675 /* Bc */, 16675 /* Bd */, 16675 /* Be */, 16625 /* Bf */, 16600 /* Bg */, 16675 /* Bh */, 16675 /* Bi */, 16675 /* Bj */, 16675 /* Bk */, 16675 /* Bl */, 16675 /* Bm */, 16675 /* Bn */, 16675 /* Bo */, 16675 /* Bp */, 16675 /* Bq */, 16675 /* Br */, 16675 /* Bs */, 16550 /* Bt */, 16675 /* Bu */, 16475 /* Bv */, 16500 /* Bw */, 16375 /* Bx */, 16475 /* By */, 16675 /* Bz */, 16675 /* B{ */, 16675 /* B| */, 16675 /* B} */, 16675 /* B~ */}, + {16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C */, 16275 /* C! */, 16275 /* C" */, 16275 /* C# */, 16275 /* C$ */, 16275 /* C% */, 16025 /* C& */, 16275 /* C' */, 16275 /* C( */, 16275 /* C) */, 16275 /* C* */, 16275 /* C+ */, 16275 /* C, */, 16025 /* C- */, 16275 /* C. */, 15775 /* C/ */, 16275 /* C0 */, 16275 /* C1 */, 16275 /* C2 */, 16275 /* C3 */, 16275 /* C4 */, 16275 /* C5 */, 16275 /* C6 */, 16275 /* C7 */, 16275 /* C8 */, 16275 /* C9 */, 16275 /* C: */, 16275 /* C; */, 16275 /* C< */, 16275 /* C= */, 16275 /* C> */, 16275 /* C? */, 16025 /* C@ */, 16075 /* CA */, 16275 /* CB */, 15950 /* CC */, 16275 /* CD */, 16275 /* CE */, 16275 /* CF */, 15950 /* CG */, 16275 /* CH */, 16275 /* CI */, 16275 /* CJ */, 16275 /* CK */, 16275 /* CL */, 16275 /* CM */, 16275 /* CN */, 15950 /* CO */, 16275 /* CP */, 15950 /* CQ */, 16275 /* CR */, 16275 /* CS */, 16275 /* CT */, 16275 /* CU */, 16275 /* CV */, 16275 /* CW */, 15775 /* CX */, 16025 /* CY */, 16275 /* CZ */, 16275 /* C[ */, 16275 /* C\ */, 16275 /* C] */, 16275 /* C^ */, 15175 /* C_ */, 16275 /* C` */, 16275 /* Ca */, 16275 /* Cb */, 16275 /* Cc */, 16275 /* Cd */, 16275 /* Ce */, 16275 /* Cf */, 16275 /* Cg */, 16275 /* Ch */, 16275 /* Ci */, 16275 /* Cj */, 16275 /* Ck */, 16275 /* Cl */, 16275 /* Cm */, 16275 /* Cn */, 16275 /* Co */, 16275 /* Cp */, 16275 /* Cq */, 16275 /* Cr */, 16275 /* Cs */, 16275 /* Ct */, 16275 /* Cu */, 16275 /* Cv */, 16275 /* Cw */, 16275 /* Cx */, 16275 /* Cy */, 16275 /* Cz */, 16275 /* C{ */, 16275 /* C| */, 16275 /* C} */, 16275 /* C~ */}, + {17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D */, 17425 /* D! */, 17175 /* D" */, 17425 /* D# */, 17425 /* D$ */, 17425 /* D% */, 17425 /* D& */, 17175 /* D' */, 17425 /* D( */, 17425 /* D) */, 17175 /* D* */, 17425 /* D+ */, 16425 /* D, */, 17800 /* D- */, 16425 /* D. */, 16300 /* D/ */, 17425 /* D0 */, 17425 /* D1 */, 17425 /* D2 */, 17425 /* D3 */, 17425 /* D4 */, 17425 /* D5 */, 17425 /* D6 */, 17425 /* D7 */, 17425 /* D8 */, 17425 /* D9 */, 17425 /* D: */, 17425 /* D; */, 17425 /* D< */, 17425 /* D= */, 17425 /* D> */, 17425 /* D? */, 17425 /* D@ */, 16500 /* DA */, 17425 /* DB */, 17425 /* DC */, 17425 /* DD */, 17425 /* DE */, 17425 /* DF */, 17425 /* DG */, 17425 /* DH */, 17425 /* DI */, 16675 /* DJ */, 17425 /* DK */, 17425 /* DL */, 17425 /* DM */, 17425 /* DN */, 17425 /* DO */, 17425 /* DP */, 17425 /* DQ */, 17425 /* DR */, 17425 /* DS */, 16800 /* DT */, 17425 /* DU */, 16650 /* DV */, 16850 /* DW */, 16675 /* DX */, 16275 /* DY */, 16800 /* DZ */, 17425 /* D[ */, 17425 /* D\ */, 17425 /* D] */, 17425 /* D^ */, 17425 /* D_ */, 17425 /* D` */, 17425 /* Da */, 17425 /* Db */, 17425 /* Dc */, 17425 /* Dd */, 17425 /* De */, 17425 /* Df */, 17425 /* Dg */, 17425 /* Dh */, 17425 /* Di */, 17425 /* Dj */, 17425 /* Dk */, 17425 /* Dl */, 17425 /* Dm */, 17425 /* Dn */, 17425 /* Do */, 17425 /* Dp */, 17425 /* Dq */, 17425 /* Dr */, 17425 /* Ds */, 17425 /* Dt */, 17425 /* Du */, 17425 /* Dv */, 17425 /* Dw */, 17425 /* Dx */, 17425 /* Dy */, 17425 /* Dz */, 17425 /* D{ */, 17425 /* D| */, 17425 /* D} */, 17425 /* D~ */}, + {15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E */, 15175 /* E! */, 15175 /* E" */, 15175 /* E# */, 15175 /* E$ */, 15175 /* E% */, 14800 /* E& */, 15175 /* E' */, 15175 /* E( */, 15175 /* E) */, 15175 /* E* */, 15175 /* E+ */, 15175 /* E, */, 15175 /* E- */, 15175 /* E. */, 15175 /* E/ */, 15175 /* E0 */, 15175 /* E1 */, 15175 /* E2 */, 15175 /* E3 */, 15175 /* E4 */, 15175 /* E5 */, 15175 /* E6 */, 15175 /* E7 */, 15175 /* E8 */, 15175 /* E9 */, 15175 /* E: */, 15175 /* E; */, 15175 /* E< */, 15175 /* E= */, 15175 /* E> */, 15175 /* E? */, 14925 /* E@ */, 15175 /* EA */, 15175 /* EB */, 15175 /* EC */, 15175 /* ED */, 15175 /* EE */, 15175 /* EF */, 15175 /* EG */, 15175 /* EH */, 15175 /* EI */, 14675 /* EJ */, 15175 /* EK */, 15175 /* EL */, 15175 /* EM */, 15175 /* EN */, 15175 /* EO */, 15175 /* EP */, 15175 /* EQ */, 15175 /* ER */, 15175 /* ES */, 15175 /* ET */, 15175 /* EU */, 15175 /* EV */, 15175 /* EW */, 15175 /* EX */, 15175 /* EY */, 15175 /* EZ */, 15175 /* E[ */, 15175 /* E\ */, 15175 /* E] */, 15175 /* E^ */, 15175 /* E_ */, 15175 /* E` */, 15175 /* Ea */, 15175 /* Eb */, 14925 /* Ec */, 14925 /* Ed */, 14925 /* Ee */, 15175 /* Ef */, 15050 /* Eg */, 15175 /* Eh */, 15175 /* Ei */, 15175 /* Ej */, 15175 /* Ek */, 15175 /* El */, 15175 /* Em */, 15175 /* En */, 14925 /* Eo */, 15175 /* Ep */, 14925 /* Eq */, 15175 /* Er */, 15175 /* Es */, 15175 /* Et */, 15175 /* Eu */, 15175 /* Ev */, 15175 /* Ew */, 15175 /* Ex */, 15175 /* Ey */, 15175 /* Ez */, 15175 /* E{ */, 15175 /* E| */, 15175 /* E} */, 15175 /* E~ */}, + {14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F */, 14625 /* F! */, 14750 /* F" */, 14625 /* F# */, 14625 /* F$ */, 14625 /* F% */, 13625 /* F& */, 14750 /* F' */, 14625 /* F( */, 15000 /* F) */, 14750 /* F* */, 14625 /* F+ */, 12125 /* F, */, 14625 /* F- */, 12125 /* F. */, 12500 /* F/ */, 14625 /* F0 */, 14625 /* F1 */, 14625 /* F2 */, 14625 /* F3 */, 14625 /* F4 */, 14625 /* F5 */, 14625 /* F6 */, 14625 /* F7 */, 14625 /* F8 */, 14625 /* F9 */, 14625 /* F: */, 14625 /* F; */, 14625 /* F< */, 14625 /* F= */, 14625 /* F> */, 14625 /* F? */, 13875 /* F@ */, 12450 /* FA */, 14625 /* FB */, 13875 /* FC */, 14625 /* FD */, 14625 /* FE */, 14625 /* FF */, 13875 /* FG */, 14625 /* FH */, 14625 /* FI */, 12750 /* FJ */, 14625 /* FK */, 14625 /* FL */, 14625 /* FM */, 14625 /* FN */, 13875 /* FO */, 14625 /* FP */, 13875 /* FQ */, 14625 /* FR */, 14625 /* FS */, 14625 /* FT */, 14625 /* FU */, 14625 /* FV */, 14625 /* FW */, 14625 /* FX */, 14625 /* FY */, 14625 /* FZ */, 14625 /* F[ */, 14625 /* F\ */, 14625 /* F] */, 14625 /* F^ */, 11125 /* F_ */, 14625 /* F` */, 13625 /* Fa */, 14625 /* Fb */, 13825 /* Fc */, 13775 /* Fd */, 13825 /* Fe */, 14450 /* Ff */, 13525 /* Fg */, 14625 /* Fh */, 14625 /* Fi */, 14375 /* Fj */, 14625 /* Fk */, 14625 /* Fl */, 13875 /* Fm */, 13875 /* Fn */, 13825 /* Fo */, 13875 /* Fp */, 13775 /* Fq */, 13875 /* Fr */, 13625 /* Fs */, 14250 /* Ft */, 14000 /* Fu */, 14125 /* Fv */, 14250 /* Fw */, 13625 /* Fx */, 14125 /* Fy */, 14000 /* Fz */, 14625 /* F{ */, 14625 /* F| */, 14625 /* F} */, 14625 /* F~ */}, + {17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G */, 17975 /* G! */, 17725 /* G" */, 17975 /* G# */, 17975 /* G$ */, 17975 /* G% */, 17975 /* G& */, 17725 /* G' */, 17975 /* G( */, 17975 /* G) */, 17725 /* G* */, 17975 /* G+ */, 17975 /* G, */, 18225 /* G- */, 17975 /* G. */, 17975 /* G/ */, 17975 /* G0 */, 17975 /* G1 */, 17975 /* G2 */, 17975 /* G3 */, 17975 /* G4 */, 17975 /* G5 */, 17975 /* G6 */, 17975 /* G7 */, 17975 /* G8 */, 17975 /* G9 */, 17975 /* G: */, 17975 /* G; */, 17975 /* G< */, 17975 /* G= */, 17975 /* G> */, 17975 /* G? */, 17975 /* G@ */, 17975 /* GA */, 17975 /* GB */, 17975 /* GC */, 17975 /* GD */, 17975 /* GE */, 17975 /* GF */, 17975 /* GG */, 17975 /* GH */, 17975 /* GI */, 17675 /* GJ */, 17975 /* GK */, 17975 /* GL */, 17975 /* GM */, 17975 /* GN */, 17975 /* GO */, 17975 /* GP */, 17975 /* GQ */, 17975 /* GR */, 17975 /* GS */, 17025 /* GT */, 17975 /* GU */, 17225 /* GV */, 17650 /* GW */, 17725 /* GX */, 16875 /* GY */, 17975 /* GZ */, 17975 /* G[ */, 17975 /* G\ */, 17975 /* G] */, 17975 /* G^ */, 17975 /* G_ */, 17975 /* G` */, 17975 /* Ga */, 17975 /* Gb */, 17975 /* Gc */, 17975 /* Gd */, 17975 /* Ge */, 17975 /* Gf */, 17975 /* Gg */, 17975 /* Gh */, 17975 /* Gi */, 17975 /* Gj */, 17975 /* Gk */, 17975 /* Gl */, 17975 /* Gm */, 17975 /* Gn */, 17975 /* Go */, 17975 /* Gp */, 17975 /* Gq */, 17975 /* Gr */, 17975 /* Gs */, 17975 /* Gt */, 17975 /* Gu */, 17975 /* Gv */, 17975 /* Gw */, 17975 /* Gx */, 17975 /* Gy */, 17975 /* Gz */, 17975 /* G{ */, 17975 /* G| */, 17975 /* G} */, 17975 /* G~ */}, + {18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H */, 18100 /* H! */, 18100 /* H" */, 18100 /* H# */, 18100 /* H$ */, 18100 /* H% */, 18100 /* H& */, 18100 /* H' */, 18100 /* H( */, 18100 /* H) */, 18100 /* H* */, 18100 /* H+ */, 18100 /* H, */, 18100 /* H- */, 18100 /* H. */, 18100 /* H/ */, 18100 /* H0 */, 18100 /* H1 */, 18100 /* H2 */, 18100 /* H3 */, 18100 /* H4 */, 18100 /* H5 */, 18100 /* H6 */, 18100 /* H7 */, 18100 /* H8 */, 18100 /* H9 */, 18100 /* H: */, 18100 /* H; */, 18100 /* H< */, 18100 /* H= */, 18100 /* H> */, 18100 /* H? */, 18100 /* H@ */, 18100 /* HA */, 18100 /* HB */, 18100 /* HC */, 18100 /* HD */, 18100 /* HE */, 18100 /* HF */, 18100 /* HG */, 18100 /* HH */, 18100 /* HI */, 18100 /* HJ */, 18100 /* HK */, 18100 /* HL */, 18100 /* HM */, 18100 /* HN */, 18100 /* HO */, 18100 /* HP */, 18100 /* HQ */, 18100 /* HR */, 18100 /* HS */, 18100 /* HT */, 18100 /* HU */, 18100 /* HV */, 18100 /* HW */, 18100 /* HX */, 18100 /* HY */, 18100 /* HZ */, 18100 /* H[ */, 18100 /* H\ */, 18100 /* H] */, 18100 /* H^ */, 18100 /* H_ */, 18100 /* H` */, 18100 /* Ha */, 18100 /* Hb */, 18100 /* Hc */, 18100 /* Hd */, 18100 /* He */, 18100 /* Hf */, 18100 /* Hg */, 18100 /* Hh */, 18100 /* Hi */, 18100 /* Hj */, 18100 /* Hk */, 18100 /* Hl */, 18100 /* Hm */, 18100 /* Hn */, 18100 /* Ho */, 18100 /* Hp */, 18100 /* Hq */, 18100 /* Hr */, 18100 /* Hs */, 18100 /* Ht */, 18100 /* Hu */, 18100 /* Hv */, 18100 /* Hw */, 18100 /* Hx */, 18100 /* Hy */, 18100 /* Hz */, 18100 /* H{ */, 18100 /* H| */, 18100 /* H} */, 18100 /* H~ */}, + {10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I */, 10800 /* I! */, 10800 /* I" */, 10800 /* I# */, 10800 /* I$ */, 10800 /* I% */, 10800 /* I& */, 10800 /* I' */, 10800 /* I( */, 10800 /* I) */, 10800 /* I* */, 10800 /* I+ */, 10800 /* I, */, 10050 /* I- */, 10800 /* I. */, 10800 /* I/ */, 10800 /* I0 */, 10800 /* I1 */, 10800 /* I2 */, 10800 /* I3 */, 10800 /* I4 */, 10800 /* I5 */, 10800 /* I6 */, 10800 /* I7 */, 10800 /* I8 */, 10800 /* I9 */, 10800 /* I: */, 10800 /* I; */, 10800 /* I< */, 10800 /* I= */, 10800 /* I> */, 10800 /* I? */, 10300 /* I@ */, 10800 /* IA */, 10800 /* IB */, 10800 /* IC */, 10800 /* ID */, 10800 /* IE */, 10800 /* IF */, 10800 /* IG */, 10800 /* IH */, 10800 /* II */, 10800 /* IJ */, 10800 /* IK */, 10800 /* IL */, 10800 /* IM */, 10800 /* IN */, 10800 /* IO */, 10800 /* IP */, 10800 /* IQ */, 10800 /* IR */, 10800 /* IS */, 10700 /* IT */, 10800 /* IU */, 10800 /* IV */, 10800 /* IW */, 10800 /* IX */, 10800 /* IY */, 10800 /* IZ */, 10800 /* I[ */, 10800 /* I\ */, 10800 /* I] */, 10800 /* I^ */, 10800 /* I_ */, 10800 /* I` */, 10800 /* Ia */, 10800 /* Ib */, 10675 /* Ic */, 10675 /* Id */, 10675 /* Ie */, 10800 /* If */, 10800 /* Ig */, 10800 /* Ih */, 10800 /* Ii */, 10800 /* Ij */, 10800 /* Ik */, 10800 /* Il */, 10800 /* Im */, 10800 /* In */, 10675 /* Io */, 10800 /* Ip */, 10675 /* Iq */, 10800 /* Ir */, 10675 /* Is */, 10800 /* It */, 10800 /* Iu */, 10675 /* Iv */, 10800 /* Iw */, 10800 /* Ix */, 10675 /* Iy */, 10800 /* Iz */, 10800 /* I{ */, 10800 /* I| */, 10800 /* I} */, 10800 /* I~ */}, + {13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J */, 13975 /* J! */, 13975 /* J" */, 13975 /* J# */, 13975 /* J$ */, 13975 /* J% */, 13975 /* J& */, 13975 /* J' */, 13975 /* J( */, 13975 /* J) */, 13975 /* J* */, 13975 /* J+ */, 13350 /* J, */, 13975 /* J- */, 13350 /* J. */, 13100 /* J/ */, 13975 /* J0 */, 13975 /* J1 */, 13975 /* J2 */, 13975 /* J3 */, 13975 /* J4 */, 13975 /* J5 */, 13975 /* J6 */, 13975 /* J7 */, 13975 /* J8 */, 13975 /* J9 */, 13975 /* J: */, 13975 /* J; */, 13975 /* J< */, 13975 /* J= */, 13975 /* J> */, 13975 /* J? */, 13975 /* J@ */, 13450 /* JA */, 13975 /* JB */, 13975 /* JC */, 13975 /* JD */, 13975 /* JE */, 13975 /* JF */, 13975 /* JG */, 13975 /* JH */, 13975 /* JI */, 13675 /* JJ */, 13975 /* JK */, 13975 /* JL */, 13975 /* JM */, 13975 /* JN */, 13975 /* JO */, 13975 /* JP */, 13975 /* JQ */, 13975 /* JR */, 13975 /* JS */, 13975 /* JT */, 13975 /* JU */, 13975 /* JV */, 13975 /* JW */, 13600 /* JX */, 13975 /* JY */, 13975 /* JZ */, 13975 /* J[ */, 13975 /* J\ */, 13975 /* J] */, 13975 /* J^ */, 11850 /* J_ */, 13975 /* J` */, 13975 /* Ja */, 13975 /* Jb */, 13975 /* Jc */, 13975 /* Jd */, 13975 /* Je */, 13975 /* Jf */, 13975 /* Jg */, 13975 /* Jh */, 13975 /* Ji */, 13975 /* Jj */, 13975 /* Jk */, 13975 /* Jl */, 13975 /* Jm */, 13975 /* Jn */, 13975 /* Jo */, 13975 /* Jp */, 13975 /* Jq */, 13975 /* Jr */, 13975 /* Js */, 13975 /* Jt */, 13975 /* Ju */, 13975 /* Jv */, 13975 /* Jw */, 13975 /* Jx */, 13975 /* Jy */, 13975 /* Jz */, 13975 /* J{ */, 13975 /* J| */, 13975 /* J} */, 13975 /* J~ */}, + {17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K */, 17400 /* K! */, 17150 /* K" */, 17400 /* K# */, 17400 /* K$ */, 17400 /* K% */, 16650 /* K& */, 17150 /* K' */, 17400 /* K( */, 17650 /* K) */, 17150 /* K* */, 17400 /* K+ */, 17650 /* K, */, 16275 /* K- */, 17650 /* K. */, 17400 /* K/ */, 17400 /* K0 */, 17400 /* K1 */, 17400 /* K2 */, 17400 /* K3 */, 17400 /* K4 */, 17400 /* K5 */, 17400 /* K6 */, 17400 /* K7 */, 17400 /* K8 */, 17400 /* K9 */, 17400 /* K: */, 17400 /* K; */, 17400 /* K< */, 17400 /* K= */, 17400 /* K> */, 17400 /* K? */, 16400 /* K@ */, 17400 /* KA */, 17400 /* KB */, 16400 /* KC */, 17400 /* KD */, 17400 /* KE */, 17400 /* KF */, 16400 /* KG */, 17400 /* KH */, 17400 /* KI */, 16900 /* KJ */, 17400 /* KK */, 17400 /* KL */, 17400 /* KM */, 17400 /* KN */, 16400 /* KO */, 17400 /* KP */, 16400 /* KQ */, 17400 /* KR */, 17150 /* KS */, 16775 /* KT */, 17400 /* KU */, 16775 /* KV */, 17150 /* KW */, 17400 /* KX */, 16900 /* KY */, 17400 /* KZ */, 17400 /* K[ */, 17400 /* K\ */, 17400 /* K] */, 17400 /* K^ */, 17400 /* K_ */, 17400 /* K` */, 17150 /* Ka */, 17400 /* Kb */, 16775 /* Kc */, 16775 /* Kd */, 16775 /* Ke */, 16900 /* Kf */, 17150 /* Kg */, 17400 /* Kh */, 17400 /* Ki */, 17400 /* Kj */, 17400 /* Kk */, 17150 /* Kl */, 17150 /* Km */, 17150 /* Kn */, 16775 /* Ko */, 17150 /* Kp */, 16775 /* Kq */, 17150 /* Kr */, 17400 /* Ks */, 16650 /* Kt */, 16900 /* Ku */, 16900 /* Kv */, 16900 /* Kw */, 17275 /* Kx */, 16900 /* Ky */, 17650 /* Kz */, 17400 /* K{ */, 17400 /* K| */, 17400 /* K} */, 17400 /* K~ */}, + {13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L */, 13250 /* L! */, 11000 /* L" */, 13250 /* L# */, 13250 /* L$ */, 13250 /* L% */, 13125 /* L& */, 11000 /* L' */, 13250 /* L( */, 13250 /* L) */, 11000 /* L* */, 13250 /* L+ */, 13750 /* L, */, 12375 /* L- */, 13750 /* L. */, 13500 /* L/ */, 13250 /* L0 */, 13250 /* L1 */, 13250 /* L2 */, 13250 /* L3 */, 13250 /* L4 */, 13250 /* L5 */, 13250 /* L6 */, 13250 /* L7 */, 13250 /* L8 */, 13250 /* L9 */, 13250 /* L: */, 13250 /* L; */, 13250 /* L< */, 13250 /* L= */, 13250 /* L> */, 11750 /* L? */, 13000 /* L@ */, 13675 /* LA */, 13250 /* LB */, 12250 /* LC */, 13250 /* LD */, 13250 /* LE */, 13250 /* LF */, 12250 /* LG */, 13250 /* LH */, 13250 /* LI */, 13250 /* LJ */, 13250 /* LK */, 13250 /* LL */, 13250 /* LM */, 13250 /* LN */, 12250 /* LO */, 13250 /* LP */, 12250 /* LQ */, 13250 /* LR */, 13250 /* LS */, 11400 /* LT */, 12425 /* LU */, 11450 /* LV */, 12225 /* LW */, 13250 /* LX */, 11025 /* LY */, 13250 /* LZ */, 13250 /* L[ */, 13250 /* L\ */, 13250 /* L] */, 13250 /* L^ */, 13750 /* L_ */, 13250 /* L` */, 13350 /* La */, 13250 /* Lb */, 13000 /* Lc */, 13025 /* Ld */, 13000 /* Le */, 13250 /* Lf */, 13250 /* Lg */, 13250 /* Lh */, 13250 /* Li */, 13250 /* Lj */, 13250 /* Lk */, 13250 /* Ll */, 13250 /* Lm */, 13250 /* Ln */, 13000 /* Lo */, 13250 /* Lp */, 13025 /* Lq */, 13250 /* Lr */, 13250 /* Ls */, 12750 /* Lt */, 13125 /* Lu */, 12000 /* Lv */, 12500 /* Lw */, 13500 /* Lx */, 12375 /* Ly */, 13525 /* Lz */, 13250 /* L{ */, 13250 /* L| */, 13250 /* L} */, 13250 /* L~ */}, + {20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M */, 20475 /* M! */, 20475 /* M" */, 20475 /* M# */, 20475 /* M$ */, 20475 /* M% */, 20475 /* M& */, 20475 /* M' */, 20475 /* M( */, 20475 /* M) */, 20475 /* M* */, 20475 /* M+ */, 20475 /* M, */, 20475 /* M- */, 20475 /* M. */, 20475 /* M/ */, 20475 /* M0 */, 20475 /* M1 */, 20475 /* M2 */, 20475 /* M3 */, 20475 /* M4 */, 20475 /* M5 */, 20475 /* M6 */, 20475 /* M7 */, 20475 /* M8 */, 20475 /* M9 */, 20475 /* M: */, 20475 /* M; */, 20475 /* M< */, 20475 /* M= */, 20475 /* M> */, 20475 /* M? */, 20475 /* M@ */, 20475 /* MA */, 20475 /* MB */, 20475 /* MC */, 20475 /* MD */, 20475 /* ME */, 20475 /* MF */, 20475 /* MG */, 20475 /* MH */, 20475 /* MI */, 20475 /* MJ */, 20475 /* MK */, 20475 /* ML */, 20475 /* MM */, 20475 /* MN */, 20475 /* MO */, 20475 /* MP */, 20475 /* MQ */, 20475 /* MR */, 20475 /* MS */, 20475 /* MT */, 20475 /* MU */, 20475 /* MV */, 20475 /* MW */, 20475 /* MX */, 20475 /* MY */, 20475 /* MZ */, 20475 /* M[ */, 20475 /* M\ */, 20475 /* M] */, 20475 /* M^ */, 20475 /* M_ */, 20475 /* M` */, 20475 /* Ma */, 20475 /* Mb */, 20475 /* Mc */, 20475 /* Md */, 20475 /* Me */, 20475 /* Mf */, 20475 /* Mg */, 20475 /* Mh */, 20475 /* Mi */, 20475 /* Mj */, 20475 /* Mk */, 20475 /* Ml */, 20475 /* Mm */, 20475 /* Mn */, 20475 /* Mo */, 20475 /* Mp */, 20475 /* Mq */, 20475 /* Mr */, 20475 /* Ms */, 20475 /* Mt */, 20475 /* Mu */, 20475 /* Mv */, 20475 /* Mw */, 20475 /* Mx */, 20475 /* My */, 20475 /* Mz */, 20475 /* M{ */, 20475 /* M| */, 20475 /* M} */, 20475 /* M~ */}, + {18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N */, 18100 /* N! */, 18100 /* N" */, 18100 /* N# */, 18100 /* N$ */, 18100 /* N% */, 18100 /* N& */, 18100 /* N' */, 18100 /* N( */, 18100 /* N) */, 18100 /* N* */, 18100 /* N+ */, 18100 /* N, */, 18100 /* N- */, 18100 /* N. */, 18100 /* N/ */, 18100 /* N0 */, 18100 /* N1 */, 18100 /* N2 */, 18100 /* N3 */, 18100 /* N4 */, 18100 /* N5 */, 18100 /* N6 */, 18100 /* N7 */, 18100 /* N8 */, 18100 /* N9 */, 18100 /* N: */, 18100 /* N; */, 18100 /* N< */, 18100 /* N= */, 18100 /* N> */, 18100 /* N? */, 18100 /* N@ */, 18100 /* NA */, 18100 /* NB */, 18100 /* NC */, 18100 /* ND */, 18100 /* NE */, 18100 /* NF */, 18100 /* NG */, 18100 /* NH */, 18100 /* NI */, 18100 /* NJ */, 18100 /* NK */, 18100 /* NL */, 18100 /* NM */, 18100 /* NN */, 18100 /* NO */, 18100 /* NP */, 18100 /* NQ */, 18100 /* NR */, 18100 /* NS */, 18100 /* NT */, 18100 /* NU */, 18100 /* NV */, 18100 /* NW */, 18100 /* NX */, 18100 /* NY */, 18100 /* NZ */, 18100 /* N[ */, 18100 /* N\ */, 18100 /* N] */, 18100 /* N^ */, 18100 /* N_ */, 18100 /* N` */, 18100 /* Na */, 18100 /* Nb */, 18100 /* Nc */, 18100 /* Nd */, 18100 /* Ne */, 18100 /* Nf */, 18100 /* Ng */, 18100 /* Nh */, 18100 /* Ni */, 18100 /* Nj */, 18100 /* Nk */, 18100 /* Nl */, 18100 /* Nm */, 18100 /* Nn */, 18100 /* No */, 18100 /* Np */, 18100 /* Nq */, 18100 /* Nr */, 18100 /* Ns */, 18100 /* Nt */, 18100 /* Nu */, 18100 /* Nv */, 18100 /* Nw */, 18100 /* Nx */, 18100 /* Ny */, 18100 /* Nz */, 18100 /* N{ */, 18100 /* N| */, 18100 /* N} */, 18100 /* N~ */}, + {17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O */, 17850 /* O! */, 17600 /* O" */, 17850 /* O# */, 17850 /* O$ */, 17850 /* O% */, 17725 /* O& */, 17600 /* O' */, 17850 /* O( */, 17850 /* O) */, 17600 /* O* */, 17850 /* O+ */, 16850 /* O, */, 18225 /* O- */, 16850 /* O. */, 16725 /* O/ */, 17850 /* O0 */, 17850 /* O1 */, 17850 /* O2 */, 17850 /* O3 */, 17850 /* O4 */, 17850 /* O5 */, 17850 /* O6 */, 17850 /* O7 */, 17850 /* O8 */, 17850 /* O9 */, 17850 /* O: */, 17850 /* O; */, 17850 /* O< */, 17850 /* O= */, 17850 /* O> */, 17850 /* O? */, 17850 /* O@ */, 17100 /* OA */, 17850 /* OB */, 17850 /* OC */, 17850 /* OD */, 17850 /* OE */, 17850 /* OF */, 17850 /* OG */, 17850 /* OH */, 17850 /* OI */, 17100 /* OJ */, 17850 /* OK */, 17850 /* OL */, 17850 /* OM */, 17850 /* ON */, 17850 /* OO */, 17850 /* OP */, 17850 /* OQ */, 17850 /* OR */, 17750 /* OS */, 17225 /* OT */, 17850 /* OU */, 17075 /* OV */, 17275 /* OW */, 17100 /* OX */, 16600 /* OY */, 17400 /* OZ */, 17850 /* O[ */, 17850 /* O\ */, 17850 /* O] */, 17850 /* O^ */, 15350 /* O_ */, 17850 /* O` */, 17850 /* Oa */, 17850 /* Ob */, 17850 /* Oc */, 17850 /* Od */, 17850 /* Oe */, 17850 /* Of */, 17850 /* Og */, 17850 /* Oh */, 17850 /* Oi */, 17850 /* Oj */, 17850 /* Ok */, 17850 /* Ol */, 17850 /* Om */, 17850 /* On */, 17850 /* Oo */, 17850 /* Op */, 17850 /* Oq */, 17850 /* Or */, 17850 /* Os */, 17850 /* Ot */, 17850 /* Ou */, 17850 /* Ov */, 17850 /* Ow */, 17850 /* Ox */, 17850 /* Oy */, 17850 /* Oz */, 17850 /* O{ */, 17850 /* O| */, 17850 /* O} */, 17850 /* O~ */}, + {16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P */, 16400 /* P! */, 16525 /* P" */, 16400 /* P# */, 16400 /* P$ */, 16400 /* P% */, 15650 /* P& */, 16525 /* P' */, 16400 /* P( */, 16400 /* P) */, 16525 /* P* */, 16400 /* P+ */, 13900 /* P, */, 16400 /* P- */, 13900 /* P. */, 14150 /* P/ */, 16400 /* P0 */, 16400 /* P1 */, 16400 /* P2 */, 16400 /* P3 */, 16400 /* P4 */, 16400 /* P5 */, 16400 /* P6 */, 16400 /* P7 */, 16400 /* P8 */, 16400 /* P9 */, 16400 /* P: */, 16400 /* P; */, 16400 /* P< */, 16400 /* P= */, 16400 /* P> */, 16400 /* P? */, 16150 /* P@ */, 14575 /* PA */, 16400 /* PB */, 16400 /* PC */, 16400 /* PD */, 16400 /* PE */, 16400 /* PF */, 16400 /* PG */, 16400 /* PH */, 16400 /* PI */, 14650 /* PJ */, 16400 /* PK */, 16400 /* PL */, 16400 /* PM */, 16400 /* PN */, 16400 /* PO */, 16400 /* PP */, 16400 /* PQ */, 16400 /* PR */, 16150 /* PS */, 16400 /* PT */, 16400 /* PU */, 16275 /* PV */, 16150 /* PW */, 15525 /* PX */, 15775 /* PY */, 15775 /* PZ */, 16400 /* P[ */, 16400 /* P\ */, 16400 /* P] */, 16400 /* P^ */, 12650 /* P_ */, 16400 /* P` */, 16025 /* Pa */, 16400 /* Pb */, 15900 /* Pc */, 15900 /* Pd */, 15900 /* Pe */, 16400 /* Pf */, 15650 /* Pg */, 16400 /* Ph */, 16400 /* Pi */, 16400 /* Pj */, 16400 /* Pk */, 16400 /* Pl */, 16150 /* Pm */, 16150 /* Pn */, 15900 /* Po */, 16025 /* Pp */, 15900 /* Pq */, 16150 /* Pr */, 16025 /* Ps */, 16400 /* Pt */, 16400 /* Pu */, 16400 /* Pv */, 16525 /* Pw */, 16025 /* Px */, 16400 /* Py */, 16400 /* Pz */, 16400 /* P{ */, 16400 /* P| */, 16400 /* P} */, 16400 /* P~ */}, + {17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q */, 17850 /* Q! */, 17600 /* Q" */, 17850 /* Q# */, 17850 /* Q$ */, 17850 /* Q% */, 17725 /* Q& */, 17600 /* Q' */, 17850 /* Q( */, 17850 /* Q) */, 17600 /* Q* */, 17850 /* Q+ */, 16850 /* Q, */, 18225 /* Q- */, 16850 /* Q. */, 16725 /* Q/ */, 17850 /* Q0 */, 17850 /* Q1 */, 17850 /* Q2 */, 17850 /* Q3 */, 17850 /* Q4 */, 17850 /* Q5 */, 17850 /* Q6 */, 17850 /* Q7 */, 17850 /* Q8 */, 17850 /* Q9 */, 17850 /* Q: */, 17850 /* Q; */, 17850 /* Q< */, 17850 /* Q= */, 17850 /* Q> */, 17850 /* Q? */, 17850 /* Q@ */, 17100 /* QA */, 17850 /* QB */, 17850 /* QC */, 17850 /* QD */, 17850 /* QE */, 17850 /* QF */, 17850 /* QG */, 17850 /* QH */, 17850 /* QI */, 17100 /* QJ */, 17850 /* QK */, 17850 /* QL */, 17850 /* QM */, 17850 /* QN */, 17850 /* QO */, 17850 /* QP */, 17850 /* QQ */, 17850 /* QR */, 17750 /* QS */, 17225 /* QT */, 17850 /* QU */, 17075 /* QV */, 17275 /* QW */, 17100 /* QX */, 16600 /* QY */, 17400 /* QZ */, 17850 /* Q[ */, 17850 /* Q\ */, 17850 /* Q] */, 17850 /* Q^ */, 15350 /* Q_ */, 17850 /* Q` */, 17850 /* Qa */, 17850 /* Qb */, 17850 /* Qc */, 17850 /* Qd */, 17850 /* Qe */, 17850 /* Qf */, 17850 /* Qg */, 17850 /* Qh */, 17850 /* Qi */, 17850 /* Qj */, 17850 /* Qk */, 17850 /* Ql */, 17850 /* Qm */, 17850 /* Qn */, 17850 /* Qo */, 17850 /* Qp */, 17850 /* Qq */, 17850 /* Qr */, 17850 /* Qs */, 17850 /* Qt */, 17850 /* Qu */, 17850 /* Qv */, 17850 /* Qw */, 17850 /* Qx */, 17850 /* Qy */, 17850 /* Qz */, 17850 /* Q{ */, 17850 /* Q| */, 17850 /* Q} */, 17850 /* Q~ */}, + {16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R */, 16850 /* R! */, 16850 /* R" */, 16850 /* R# */, 16850 /* R$ */, 16850 /* R% */, 16225 /* R& */, 16850 /* R' */, 16850 /* R( */, 16850 /* R) */, 16850 /* R* */, 16850 /* R+ */, 16850 /* R, */, 16475 /* R- */, 16850 /* R. */, 16350 /* R/ */, 16850 /* R0 */, 16850 /* R1 */, 16850 /* R2 */, 16850 /* R3 */, 16850 /* R4 */, 16850 /* R5 */, 16850 /* R6 */, 16850 /* R7 */, 16850 /* R8 */, 16850 /* R9 */, 16850 /* R: */, 16850 /* R; */, 16850 /* R< */, 16850 /* R= */, 16850 /* R> */, 16850 /* R? */, 16350 /* R@ */, 16850 /* RA */, 16850 /* RB */, 16600 /* RC */, 16850 /* RD */, 16850 /* RE */, 16850 /* RF */, 16600 /* RG */, 16850 /* RH */, 16850 /* RI */, 15975 /* RJ */, 16850 /* RK */, 16850 /* RL */, 16850 /* RM */, 16850 /* RN */, 16600 /* RO */, 16850 /* RP */, 16600 /* RQ */, 16850 /* RR */, 16850 /* RS */, 16250 /* RT */, 16500 /* RU */, 16100 /* RV */, 16500 /* RW */, 16850 /* RX */, 15850 /* RY */, 16850 /* RZ */, 16850 /* R[ */, 16850 /* R\ */, 16850 /* R] */, 16850 /* R^ */, 16850 /* R_ */, 16850 /* R` */, 16600 /* Ra */, 16850 /* Rb */, 16475 /* Rc */, 16475 /* Rd */, 16475 /* Re */, 16850 /* Rf */, 16225 /* Rg */, 16850 /* Rh */, 16850 /* Ri */, 16850 /* Rj */, 16850 /* Rk */, 16850 /* Rl */, 16850 /* Rm */, 16850 /* Rn */, 16475 /* Ro */, 16850 /* Rp */, 16475 /* Rq */, 16850 /* Rr */, 16850 /* Rs */, 16850 /* Rt */, 16850 /* Ru */, 16850 /* Rv */, 16850 /* Rw */, 16850 /* Rx */, 16750 /* Ry */, 16850 /* Rz */, 16850 /* R{ */, 16850 /* R| */, 16850 /* R} */, 16850 /* R~ */}, + {15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S */, 15600 /* S! */, 15600 /* S" */, 15600 /* S# */, 15600 /* S$ */, 15600 /* S% */, 15350 /* S& */, 15600 /* S' */, 15600 /* S( */, 15600 /* S) */, 15600 /* S* */, 15600 /* S+ */, 15600 /* S, */, 15975 /* S- */, 15600 /* S. */, 14725 /* S/ */, 15600 /* S0 */, 15600 /* S1 */, 15600 /* S2 */, 15600 /* S3 */, 15600 /* S4 */, 15600 /* S5 */, 15600 /* S6 */, 15600 /* S7 */, 15600 /* S8 */, 15600 /* S9 */, 15600 /* S: */, 15600 /* S; */, 15600 /* S< */, 15600 /* S= */, 15600 /* S> */, 15600 /* S? */, 15350 /* S@ */, 15600 /* SA */, 15600 /* SB */, 15600 /* SC */, 15600 /* SD */, 15600 /* SE */, 15600 /* SF */, 15600 /* SG */, 15600 /* SH */, 15600 /* SI */, 15600 /* SJ */, 15600 /* SK */, 15600 /* SL */, 15600 /* SM */, 15600 /* SN */, 15600 /* SO */, 15600 /* SP */, 15600 /* SQ */, 15600 /* SR */, 15250 /* SS */, 15025 /* ST */, 15600 /* SU */, 15150 /* SV */, 15150 /* SW */, 15600 /* SX */, 14600 /* SY */, 15600 /* SZ */, 15600 /* S[ */, 15600 /* S\ */, 15600 /* S] */, 15600 /* S^ */, 14100 /* S_ */, 15600 /* S` */, 15600 /* Sa */, 15600 /* Sb */, 15600 /* Sc */, 15600 /* Sd */, 15600 /* Se */, 15600 /* Sf */, 15600 /* Sg */, 15600 /* Sh */, 15600 /* Si */, 15600 /* Sj */, 15600 /* Sk */, 15600 /* Sl */, 15600 /* Sm */, 15600 /* Sn */, 15600 /* So */, 15600 /* Sp */, 15600 /* Sq */, 15600 /* Sr */, 15600 /* Ss */, 15475 /* St */, 15600 /* Su */, 15600 /* Sv */, 15600 /* Sw */, 15600 /* Sx */, 15600 /* Sy */, 15600 /* Sz */, 15600 /* S{ */, 15600 /* S| */, 15600 /* S} */, 15600 /* S~ */}, + {14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T */, 14600 /* T! */, 14975 /* T" */, 14600 /* T# */, 14600 /* T$ */, 14600 /* T% */, 13850 /* T& */, 14975 /* T' */, 14600 /* T( */, 15100 /* T) */, 14975 /* T* */, 14600 /* T+ */, 12850 /* T, */, 13475 /* T- */, 12850 /* T. */, 12100 /* T/ */, 14600 /* T0 */, 14600 /* T1 */, 14600 /* T2 */, 14600 /* T3 */, 14600 /* T4 */, 14600 /* T5 */, 14600 /* T6 */, 14600 /* T7 */, 14600 /* T8 */, 14600 /* T9 */, 13600 /* T: */, 13600 /* T; */, 14600 /* T< */, 14600 /* T= */, 14600 /* T> */, 14600 /* T? */, 13850 /* T@ */, 12975 /* TA */, 14600 /* TB */, 13975 /* TC */, 14600 /* TD */, 14600 /* TE */, 14600 /* TF */, 13975 /* TG */, 14600 /* TH */, 14500 /* TI */, 13225 /* TJ */, 14600 /* TK */, 14600 /* TL */, 14600 /* TM */, 14600 /* TN */, 13975 /* TO */, 14600 /* TP */, 13975 /* TQ */, 14600 /* TR */, 14300 /* TS */, 14975 /* TT */, 14600 /* TU */, 14675 /* TV */, 14675 /* TW */, 14600 /* TX */, 14675 /* TY */, 14600 /* TZ */, 14600 /* T[ */, 14600 /* T\ */, 14600 /* T] */, 14600 /* T^ */, 12600 /* T_ */, 14600 /* T` */, 12850 /* Ta */, 14600 /* Tb */, 12975 /* Tc */, 12975 /* Td */, 12975 /* Te */, 14225 /* Tf */, 12600 /* Tg */, 14600 /* Th */, 14600 /* Ti */, 14600 /* Tj */, 14600 /* Tk */, 14600 /* Tl */, 13475 /* Tm */, 13475 /* Tn */, 12975 /* To */, 13475 /* Tp */, 12975 /* Tq */, 13475 /* Tr */, 12850 /* Ts */, 14600 /* Tt */, 13475 /* Tu */, 13975 /* Tv */, 13725 /* Tw */, 13725 /* Tx */, 13850 /* Ty */, 13350 /* Tz */, 14600 /* T{ */, 14600 /* T| */, 14600 /* T} */, 14600 /* T~ */}, + {17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U */, 17350 /* U! */, 17350 /* U" */, 17350 /* U# */, 17350 /* U$ */, 17350 /* U% */, 17100 /* U& */, 17350 /* U' */, 17350 /* U( */, 17350 /* U) */, 17350 /* U* */, 17350 /* U+ */, 16475 /* U, */, 17350 /* U- */, 16475 /* U. */, 16350 /* U/ */, 17350 /* U0 */, 17350 /* U1 */, 17350 /* U2 */, 17350 /* U3 */, 17350 /* U4 */, 17350 /* U5 */, 17350 /* U6 */, 17350 /* U7 */, 17350 /* U8 */, 17350 /* U9 */, 17350 /* U: */, 17350 /* U; */, 17350 /* U< */, 17350 /* U= */, 17350 /* U> */, 17350 /* U? */, 17350 /* U@ */, 16725 /* UA */, 17350 /* UB */, 17350 /* UC */, 17350 /* UD */, 17350 /* UE */, 17350 /* UF */, 17350 /* UG */, 17350 /* UH */, 17350 /* UI */, 16975 /* UJ */, 17350 /* UK */, 17350 /* UL */, 17350 /* UM */, 17350 /* UN */, 17350 /* UO */, 17350 /* UP */, 17350 /* UQ */, 17350 /* UR */, 17350 /* US */, 17350 /* UT */, 17350 /* UU */, 17350 /* UV */, 17350 /* UW */, 17350 /* UX */, 17350 /* UY */, 17350 /* UZ */, 17350 /* U[ */, 17350 /* U\ */, 17350 /* U] */, 17350 /* U^ */, 15600 /* U_ */, 17350 /* U` */, 17350 /* Ua */, 17350 /* Ub */, 17350 /* Uc */, 17350 /* Ud */, 17350 /* Ue */, 17350 /* Uf */, 17100 /* Ug */, 17350 /* Uh */, 17350 /* Ui */, 17350 /* Uj */, 17350 /* Uk */, 17350 /* Ul */, 17350 /* Um */, 17350 /* Un */, 17350 /* Uo */, 17350 /* Up */, 17350 /* Uq */, 17350 /* Ur */, 17350 /* Us */, 17350 /* Ut */, 17350 /* Uu */, 17350 /* Uv */, 17350 /* Uw */, 17350 /* Ux */, 17350 /* Uy */, 17350 /* Uz */, 17350 /* U{ */, 17350 /* U| */, 17350 /* U} */, 17350 /* U~ */}, + {16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V */, 16250 /* V! */, 16500 /* V" */, 16250 /* V# */, 16250 /* V$ */, 16250 /* V% */, 15375 /* V& */, 16500 /* V' */, 16250 /* V( */, 16750 /* V) */, 16500 /* V* */, 16250 /* V+ */, 14750 /* V, */, 15625 /* V- */, 14750 /* V. */, 14625 /* V/ */, 16250 /* V0 */, 16250 /* V1 */, 16250 /* V2 */, 16250 /* V3 */, 16250 /* V4 */, 16250 /* V5 */, 16250 /* V6 */, 16250 /* V7 */, 16250 /* V8 */, 16250 /* V9 */, 15500 /* V: */, 15500 /* V; */, 16250 /* V< */, 16250 /* V= */, 16250 /* V> */, 16625 /* V? */, 15500 /* V@ */, 14900 /* VA */, 16250 /* VB */, 15475 /* VC */, 16250 /* VD */, 16250 /* VE */, 16250 /* VF */, 15475 /* VG */, 16250 /* VH */, 16250 /* VI */, 14875 /* VJ */, 16250 /* VK */, 16250 /* VL */, 16250 /* VM */, 16250 /* VN */, 15475 /* VO */, 16250 /* VP */, 15475 /* VQ */, 16250 /* VR */, 15500 /* VS */, 16325 /* VT */, 16250 /* VU */, 16375 /* VV */, 16325 /* VW */, 16250 /* VX */, 16250 /* VY */, 16125 /* VZ */, 16250 /* V[ */, 16250 /* V\ */, 16250 /* V] */, 16250 /* V^ */, 14250 /* V_ */, 16250 /* V` */, 15250 /* Va */, 16250 /* Vb */, 15375 /* Vc */, 15375 /* Vd */, 15375 /* Ve */, 16250 /* Vf */, 15250 /* Vg */, 16250 /* Vh */, 16250 /* Vi */, 16125 /* Vj */, 16250 /* Vk */, 16250 /* Vl */, 15750 /* Vm */, 15750 /* Vn */, 15375 /* Vo */, 15750 /* Vp */, 15375 /* Vq */, 15750 /* Vr */, 15625 /* Vs */, 16250 /* Vt */, 15625 /* Vu */, 16000 /* Vv */, 16000 /* Vw */, 16000 /* Vx */, 16125 /* Vy */, 15875 /* Vz */, 16250 /* V{ */, 16250 /* V| */, 16250 /* V} */, 16250 /* V~ */}, + {24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W */, 24325 /* W! */, 24700 /* W" */, 24325 /* W# */, 24325 /* W$ */, 24325 /* W% */, 23450 /* W& */, 24700 /* W' */, 24325 /* W( */, 24575 /* W) */, 24700 /* W* */, 24325 /* W+ */, 23325 /* W, */, 24075 /* W- */, 23325 /* W. */, 23075 /* W/ */, 24325 /* W0 */, 24325 /* W1 */, 24325 /* W2 */, 24325 /* W3 */, 24325 /* W4 */, 24325 /* W5 */, 24325 /* W6 */, 24325 /* W7 */, 24325 /* W8 */, 24325 /* W9 */, 24325 /* W: */, 24325 /* W; */, 24325 /* W< */, 24325 /* W= */, 24325 /* W> */, 24325 /* W? */, 23825 /* W@ */, 23625 /* WA */, 24325 /* WB */, 23750 /* WC */, 24325 /* WD */, 24325 /* WE */, 24325 /* WF */, 23750 /* WG */, 24325 /* WH */, 24325 /* WI */, 23075 /* WJ */, 24325 /* WK */, 24325 /* WL */, 24325 /* WM */, 24325 /* WN */, 23750 /* WO */, 24325 /* WP */, 23750 /* WQ */, 24325 /* WR */, 24325 /* WS */, 24400 /* WT */, 24325 /* WU */, 24400 /* WV */, 24375 /* WW */, 24325 /* WX */, 24325 /* WY */, 24200 /* WZ */, 24325 /* W[ */, 24325 /* W\ */, 24325 /* W] */, 24325 /* W^ */, 23075 /* W_ */, 24325 /* W` */, 23575 /* Wa */, 24325 /* Wb */, 23700 /* Wc */, 23700 /* Wd */, 23700 /* We */, 24325 /* Wf */, 23825 /* Wg */, 24325 /* Wh */, 24325 /* Wi */, 24200 /* Wj */, 24325 /* Wk */, 24325 /* Wl */, 24075 /* Wm */, 24075 /* Wn */, 23700 /* Wo */, 24200 /* Wp */, 23700 /* Wq */, 24075 /* Wr */, 24075 /* Ws */, 24325 /* Wt */, 24200 /* Wu */, 24325 /* Wv */, 24075 /* Ww */, 24075 /* Wx */, 24325 /* Wy */, 24075 /* Wz */, 24325 /* W{ */, 24325 /* W| */, 24325 /* W} */, 24325 /* W~ */}, + {16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X */, 16825 /* X! */, 16825 /* X" */, 16825 /* X# */, 16825 /* X$ */, 16825 /* X% */, 16200 /* X& */, 16825 /* X' */, 16825 /* X( */, 16825 /* X) */, 16825 /* X* */, 16825 /* X+ */, 17075 /* X, */, 16075 /* X- */, 17075 /* X. */, 16825 /* X/ */, 16825 /* X0 */, 16825 /* X1 */, 16825 /* X2 */, 16825 /* X3 */, 16825 /* X4 */, 16825 /* X5 */, 16825 /* X6 */, 16825 /* X7 */, 16825 /* X8 */, 16825 /* X9 */, 16450 /* X: */, 16450 /* X; */, 16825 /* X< */, 16825 /* X= */, 16825 /* X> */, 16575 /* X? */, 16075 /* X@ */, 16950 /* XA */, 16825 /* XB */, 16075 /* XC */, 16825 /* XD */, 16825 /* XE */, 16825 /* XF */, 16075 /* XG */, 16825 /* XH */, 16825 /* XI */, 16325 /* XJ */, 16825 /* XK */, 16825 /* XL */, 16825 /* XM */, 16825 /* XN */, 16075 /* XO */, 16825 /* XP */, 16075 /* XQ */, 16825 /* XR */, 16825 /* XS */, 16825 /* XT */, 16825 /* XU */, 16825 /* XV */, 16825 /* XW */, 16825 /* XX */, 16825 /* XY */, 16825 /* XZ */, 16825 /* X[ */, 16825 /* X\ */, 16825 /* X] */, 16825 /* X^ */, 17325 /* X_ */, 16825 /* X` */, 16700 /* Xa */, 16825 /* Xb */, 16325 /* Xc */, 16325 /* Xd */, 16325 /* Xe */, 16825 /* Xf */, 16825 /* Xg */, 16825 /* Xh */, 16825 /* Xi */, 16825 /* Xj */, 16825 /* Xk */, 16825 /* Xl */, 16825 /* Xm */, 16825 /* Xn */, 16325 /* Xo */, 16825 /* Xp */, 16325 /* Xq */, 16825 /* Xr */, 16575 /* Xs */, 16325 /* Xt */, 16325 /* Xu */, 16450 /* Xv */, 16325 /* Xw */, 16825 /* Xx */, 16575 /* Xy */, 17075 /* Xz */, 16825 /* X{ */, 16825 /* X| */, 16825 /* X} */, 16825 /* X~ */}, + {16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y */, 16225 /* Y! */, 16350 /* Y" */, 16225 /* Y# */, 16225 /* Y$ */, 16225 /* Y% */, 14850 /* Y& */, 16350 /* Y' */, 16225 /* Y( */, 16725 /* Y) */, 16350 /* Y* */, 16225 /* Y+ */, 14475 /* Y, */, 14975 /* Y- */, 14475 /* Y. */, 13975 /* Y/ */, 16225 /* Y0 */, 16225 /* Y1 */, 16225 /* Y2 */, 16225 /* Y3 */, 16225 /* Y4 */, 16225 /* Y5 */, 16225 /* Y6 */, 16225 /* Y7 */, 16225 /* Y8 */, 16225 /* Y9 */, 14975 /* Y: */, 14975 /* Y; */, 16225 /* Y< */, 16225 /* Y= */, 16225 /* Y> */, 16225 /* Y? */, 15225 /* Y@ */, 14475 /* YA */, 16225 /* YB */, 14975 /* YC */, 16225 /* YD */, 16225 /* YE */, 16225 /* YF */, 14975 /* YG */, 16225 /* YH */, 16225 /* YI */, 14475 /* YJ */, 16225 /* YK */, 16225 /* YL */, 16225 /* YM */, 16225 /* YN */, 14975 /* YO */, 16225 /* YP */, 14975 /* YQ */, 16225 /* YR */, 15125 /* YS */, 16300 /* YT */, 16225 /* YU */, 16225 /* YV */, 16225 /* YW */, 16225 /* YX */, 16225 /* YY */, 16225 /* YZ */, 16225 /* Y[ */, 16225 /* Y\ */, 16225 /* Y] */, 16225 /* Y^ */, 13975 /* Y_ */, 16225 /* Y` */, 14600 /* Ya */, 15975 /* Yb */, 14725 /* Yc */, 14725 /* Yd */, 14725 /* Ye */, 15600 /* Yf */, 14700 /* Yg */, 15975 /* Yh */, 15975 /* Yi */, 15475 /* Yj */, 15975 /* Yk */, 16225 /* Yl */, 15100 /* Ym */, 15100 /* Yn */, 14725 /* Yo */, 14975 /* Yp */, 14725 /* Yq */, 15100 /* Yr */, 14725 /* Ys */, 15600 /* Yt */, 15225 /* Yu */, 15350 /* Yv */, 15225 /* Yw */, 15225 /* Yx */, 15350 /* Yy */, 14975 /* Yz */, 16225 /* Y{ */, 16225 /* Y| */, 16225 /* Y} */, 16225 /* Y~ */}, + {15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z */, 15175 /* Z! */, 15300 /* Z" */, 15175 /* Z# */, 15175 /* Z$ */, 15175 /* Z% */, 14925 /* Z& */, 15300 /* Z' */, 15175 /* Z( */, 15425 /* Z) */, 15300 /* Z* */, 15175 /* Z+ */, 15925 /* Z, */, 14175 /* Z- */, 15925 /* Z. */, 15175 /* Z/ */, 15175 /* Z0 */, 15175 /* Z1 */, 15175 /* Z2 */, 15175 /* Z3 */, 15175 /* Z4 */, 15175 /* Z5 */, 15175 /* Z6 */, 15175 /* Z7 */, 15175 /* Z8 */, 15175 /* Z9 */, 15175 /* Z: */, 15175 /* Z; */, 15175 /* Z< */, 15175 /* Z= */, 15175 /* Z> */, 15175 /* Z? */, 14800 /* Z@ */, 15175 /* ZA */, 15175 /* ZB */, 14725 /* ZC */, 15175 /* ZD */, 15175 /* ZE */, 15175 /* ZF */, 14725 /* ZG */, 15175 /* ZH */, 15175 /* ZI */, 15000 /* ZJ */, 15175 /* ZK */, 15175 /* ZL */, 15175 /* ZM */, 15175 /* ZN */, 14725 /* ZO */, 15175 /* ZP */, 14725 /* ZQ */, 15175 /* ZR */, 15175 /* ZS */, 15175 /* ZT */, 15175 /* ZU */, 14975 /* ZV */, 15050 /* ZW */, 15175 /* ZX */, 14925 /* ZY */, 15175 /* ZZ */, 15175 /* Z[ */, 15175 /* Z\ */, 15175 /* Z] */, 15175 /* Z^ */, 15550 /* Z_ */, 15175 /* Z` */, 15175 /* Za */, 15175 /* Zb */, 15050 /* Zc */, 15050 /* Zd */, 15050 /* Ze */, 15175 /* Zf */, 15175 /* Zg */, 15175 /* Zh */, 15175 /* Zi */, 15175 /* Zj */, 15175 /* Zk */, 15175 /* Zl */, 15175 /* Zm */, 15175 /* Zn */, 15050 /* Zo */, 15175 /* Zp */, 15050 /* Zq */, 15175 /* Zr */, 15175 /* Zs */, 14925 /* Zt */, 15050 /* Zu */, 15175 /* Zv */, 15175 /* Zw */, 15175 /* Zx */, 15050 /* Zy */, 15175 /* Zz */, 15175 /* Z{ */, 15175 /* Z| */, 15175 /* Z} */, 15175 /* Z~ */}, + {8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [ */, 8350 /* [! */, 8350 /* [" */, 8350 /* [# */, 8350 /* [$ */, 8350 /* [% */, 8350 /* [& */, 8350 /* [' */, 8350 /* [( */, 8350 /* [) */, 8350 /* [* */, 8350 /* [+ */, 8350 /* [, */, 8350 /* [- */, 8350 /* [. */, 8350 /* [/ */, 8350 /* [0 */, 8350 /* [1 */, 8350 /* [2 */, 8350 /* [3 */, 8350 /* [4 */, 8350 /* [5 */, 8350 /* [6 */, 8350 /* [7 */, 8350 /* [8 */, 8350 /* [9 */, 8350 /* [: */, 8350 /* [; */, 8350 /* [< */, 8350 /* [= */, 8350 /* [> */, 8350 /* [? */, 8350 /* [@ */, 8350 /* [A */, 8350 /* [B */, 8350 /* [C */, 8350 /* [D */, 8350 /* [E */, 8350 /* [F */, 8350 /* [G */, 8350 /* [H */, 8350 /* [I */, 8350 /* [J */, 8350 /* [K */, 8350 /* [L */, 8350 /* [M */, 8350 /* [N */, 8350 /* [O */, 8350 /* [P */, 8350 /* [Q */, 8350 /* [R */, 8350 /* [S */, 8350 /* [T */, 8350 /* [U */, 8350 /* [V */, 8350 /* [W */, 8350 /* [X */, 8350 /* [Y */, 8350 /* [Z */, 8350 /* [[ */, 8350 /* [\ */, 8350 /* [] */, 8350 /* [^ */, 8350 /* [_ */, 8350 /* [` */, 8350 /* [a */, 8350 /* [b */, 8350 /* [c */, 8350 /* [d */, 8350 /* [e */, 8350 /* [f */, 8350 /* [g */, 8350 /* [h */, 8350 /* [i */, 8600 /* [j */, 8350 /* [k */, 8350 /* [l */, 8350 /* [m */, 8350 /* [n */, 8350 /* [o */, 8350 /* [p */, 8350 /* [q */, 8350 /* [r */, 8350 /* [s */, 8350 /* [t */, 8350 /* [u */, 8350 /* [v */, 8350 /* [w */, 8350 /* [x */, 8350 /* [y */, 8350 /* [z */, 8350 /* [{ */, 8350 /* [| */, 8350 /* [} */, 8350 /* [~ */}, + {11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \ */, 11500 /* \! */, 11500 /* \" */, 11500 /* \# */, 11500 /* \$ */, 11500 /* \% */, 11500 /* \& */, 11500 /* \' */, 11500 /* \( */, 11500 /* \) */, 11500 /* \* */, 11500 /* \+ */, 11500 /* \, */, 11500 /* \- */, 11500 /* \. */, 11500 /* \/ */, 11500 /* \0 */, 11500 /* \1 */, 11500 /* \2 */, 11500 /* \3 */, 11500 /* \4 */, 11500 /* \5 */, 11500 /* \6 */, 11500 /* \7 */, 11500 /* \8 */, 11500 /* \9 */, 11500 /* \: */, 11500 /* \; */, 11500 /* \< */, 11500 /* \= */, 11500 /* \> */, 11500 /* \? */, 11500 /* \@ */, 11500 /* \A */, 11500 /* \B */, 11500 /* \C */, 11500 /* \D */, 11500 /* \E */, 11500 /* \F */, 11500 /* \G */, 11500 /* \H */, 11500 /* \I */, 11500 /* \J */, 11500 /* \K */, 11500 /* \L */, 11500 /* \M */, 11500 /* \N */, 11500 /* \O */, 11500 /* \P */, 11500 /* \Q */, 11500 /* \R */, 11500 /* \S */, 11500 /* \T */, 11500 /* \U */, 11500 /* \V */, 11500 /* \W */, 11500 /* \X */, 11500 /* \Y */, 11500 /* \Z */, 11500 /* \[ */, 11500 /* \\ */, 11500 /* \] */, 11500 /* \^ */, 11500 /* \_ */, 11500 /* \` */, 11500 /* \a */, 11500 /* \b */, 11500 /* \c */, 11500 /* \d */, 11500 /* \e */, 11500 /* \f */, 11500 /* \g */, 11500 /* \h */, 11500 /* \i */, 11500 /* \j */, 11500 /* \k */, 11500 /* \l */, 11500 /* \m */, 11500 /* \n */, 11500 /* \o */, 11500 /* \p */, 11500 /* \q */, 11500 /* \r */, 11500 /* \s */, 11500 /* \t */, 11500 /* \u */, 11500 /* \v */, 11500 /* \w */, 11500 /* \x */, 11500 /* \y */, 11500 /* \z */, 11500 /* \{ */, 11500 /* \| */, 11500 /* \} */, 11500 /* \~ */}, + {8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ] */, 8350 /* ]! */, 8350 /* ]" */, 8350 /* ]# */, 8350 /* ]$ */, 8350 /* ]% */, 8350 /* ]& */, 8350 /* ]' */, 8350 /* ]( */, 8350 /* ]) */, 8350 /* ]* */, 8350 /* ]+ */, 8350 /* ], */, 8350 /* ]- */, 8350 /* ]. */, 8350 /* ]/ */, 8350 /* ]0 */, 8350 /* ]1 */, 8350 /* ]2 */, 8350 /* ]3 */, 8350 /* ]4 */, 8350 /* ]5 */, 8350 /* ]6 */, 8350 /* ]7 */, 8350 /* ]8 */, 8350 /* ]9 */, 8350 /* ]: */, 8350 /* ]; */, 8350 /* ]< */, 8350 /* ]= */, 8350 /* ]> */, 8350 /* ]? */, 8350 /* ]@ */, 8350 /* ]A */, 8350 /* ]B */, 8350 /* ]C */, 8350 /* ]D */, 8350 /* ]E */, 8350 /* ]F */, 8350 /* ]G */, 8350 /* ]H */, 8350 /* ]I */, 8350 /* ]J */, 8350 /* ]K */, 8350 /* ]L */, 8350 /* ]M */, 8350 /* ]N */, 8350 /* ]O */, 8350 /* ]P */, 8350 /* ]Q */, 8350 /* ]R */, 8350 /* ]S */, 8350 /* ]T */, 8350 /* ]U */, 8350 /* ]V */, 8350 /* ]W */, 8350 /* ]X */, 8350 /* ]Y */, 8350 /* ]Z */, 8350 /* ][ */, 8350 /* ]\ */, 8350 /* ]] */, 8350 /* ]^ */, 8350 /* ]_ */, 8350 /* ]` */, 8350 /* ]a */, 8350 /* ]b */, 8350 /* ]c */, 8350 /* ]d */, 8350 /* ]e */, 8350 /* ]f */, 8350 /* ]g */, 8350 /* ]h */, 8350 /* ]i */, 8350 /* ]j */, 8350 /* ]k */, 8350 /* ]l */, 8350 /* ]m */, 8350 /* ]n */, 8350 /* ]o */, 8350 /* ]p */, 8350 /* ]q */, 8350 /* ]r */, 8350 /* ]s */, 8350 /* ]t */, 8350 /* ]u */, 8350 /* ]v */, 8350 /* ]w */, 8350 /* ]x */, 8350 /* ]y */, 8350 /* ]z */, 8350 /* ]{ */, 8350 /* ]| */, 8350 /* ]} */, 8350 /* ]~ */}, + {15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^ */, 15000 /* ^! */, 15000 /* ^" */, 15000 /* ^# */, 15000 /* ^$ */, 15000 /* ^% */, 15000 /* ^& */, 15000 /* ^' */, 15000 /* ^( */, 15000 /* ^) */, 15000 /* ^* */, 15000 /* ^+ */, 15000 /* ^, */, 15000 /* ^- */, 15000 /* ^. */, 15000 /* ^/ */, 15000 /* ^0 */, 15000 /* ^1 */, 15000 /* ^2 */, 15000 /* ^3 */, 15000 /* ^4 */, 15000 /* ^5 */, 15000 /* ^6 */, 15000 /* ^7 */, 15000 /* ^8 */, 15000 /* ^9 */, 15000 /* ^: */, 15000 /* ^; */, 15000 /* ^< */, 15000 /* ^= */, 15000 /* ^> */, 15000 /* ^? */, 15000 /* ^@ */, 15000 /* ^A */, 15000 /* ^B */, 15000 /* ^C */, 15000 /* ^D */, 15000 /* ^E */, 15000 /* ^F */, 15000 /* ^G */, 15000 /* ^H */, 15000 /* ^I */, 15000 /* ^J */, 15000 /* ^K */, 15000 /* ^L */, 15000 /* ^M */, 15000 /* ^N */, 15000 /* ^O */, 15000 /* ^P */, 15000 /* ^Q */, 15000 /* ^R */, 15000 /* ^S */, 15000 /* ^T */, 15000 /* ^U */, 15000 /* ^V */, 15000 /* ^W */, 15000 /* ^X */, 15000 /* ^Y */, 15000 /* ^Z */, 15000 /* ^[ */, 15000 /* ^\ */, 15000 /* ^] */, 15000 /* ^^ */, 15000 /* ^_ */, 15000 /* ^` */, 15000 /* ^a */, 15000 /* ^b */, 15000 /* ^c */, 15000 /* ^d */, 15000 /* ^e */, 15000 /* ^f */, 15000 /* ^g */, 15000 /* ^h */, 15000 /* ^i */, 15000 /* ^j */, 15000 /* ^k */, 15000 /* ^l */, 15000 /* ^m */, 15000 /* ^n */, 15000 /* ^o */, 15000 /* ^p */, 15000 /* ^q */, 15000 /* ^r */, 15000 /* ^s */, 15000 /* ^t */, 15000 /* ^u */, 15000 /* ^v */, 15000 /* ^w */, 15000 /* ^x */, 15000 /* ^y */, 15000 /* ^z */, 15000 /* ^{ */, 15000 /* ^| */, 15000 /* ^} */, 15000 /* ^~ */}, + {13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _ */, 13900 /* _! */, 13900 /* _" */, 13900 /* _# */, 13900 /* _$ */, 13900 /* _% */, 13900 /* _& */, 13900 /* _' */, 13900 /* _( */, 13900 /* _) */, 13900 /* _* */, 13900 /* _+ */, 13900 /* _, */, 13900 /* _- */, 13900 /* _. */, 13900 /* _/ */, 13900 /* _0 */, 13900 /* _1 */, 13900 /* _2 */, 13900 /* _3 */, 13900 /* _4 */, 13900 /* _5 */, 13900 /* _6 */, 13900 /* _7 */, 13900 /* _8 */, 13900 /* _9 */, 13900 /* _: */, 13900 /* _; */, 13900 /* _< */, 13900 /* _= */, 13900 /* _> */, 13900 /* _? */, 13900 /* _@ */, 14275 /* _A */, 13900 /* _B */, 11400 /* _C */, 13900 /* _D */, 13900 /* _E */, 13900 /* _F */, 11400 /* _G */, 13900 /* _H */, 13900 /* _I */, 12650 /* _J */, 13900 /* _K */, 13900 /* _L */, 13900 /* _M */, 13900 /* _N */, 11400 /* _O */, 13900 /* _P */, 11400 /* _Q */, 13900 /* _R */, 12400 /* _S */, 11900 /* _T */, 12150 /* _U */, 11900 /* _V */, 12650 /* _W */, 14400 /* _X */, 11650 /* _Y */, 14525 /* _Z */, 13900 /* _[ */, 13900 /* _\ */, 13900 /* _] */, 13900 /* _^ */, 13900 /* __ */, 13900 /* _` */, 13150 /* _a */, 13900 /* _b */, 12400 /* _c */, 12650 /* _d */, 12400 /* _e */, 13275 /* _f */, 14525 /* _g */, 13900 /* _h */, 13900 /* _i */, 15275 /* _j */, 13900 /* _k */, 12525 /* _l */, 13900 /* _m */, 13900 /* _n */, 12400 /* _o */, 14150 /* _p */, 12650 /* _q */, 13900 /* _r */, 12900 /* _s */, 12275 /* _t */, 13025 /* _u */, 12150 /* _v */, 12650 /* _w */, 14400 /* _x */, 13525 /* _y */, 14025 /* _z */, 13900 /* _{ */, 13900 /* _| */, 13900 /* _} */, 13900 /* _~ */}, + {15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* ` */, 15000 /* `! */, 15000 /* `" */, 15000 /* `# */, 15000 /* `$ */, 15000 /* `% */, 15000 /* `& */, 15000 /* `' */, 15000 /* `( */, 15000 /* `) */, 15000 /* `* */, 15000 /* `+ */, 15000 /* `, */, 15000 /* `- */, 15000 /* `. */, 15000 /* `/ */, 15000 /* `0 */, 15000 /* `1 */, 15000 /* `2 */, 15000 /* `3 */, 15000 /* `4 */, 15000 /* `5 */, 15000 /* `6 */, 15000 /* `7 */, 15000 /* `8 */, 15000 /* `9 */, 15000 /* `: */, 15000 /* `; */, 15000 /* `< */, 15000 /* `= */, 15000 /* `> */, 15000 /* `? */, 15000 /* `@ */, 15000 /* `A */, 15000 /* `B */, 15000 /* `C */, 15000 /* `D */, 15000 /* `E */, 15000 /* `F */, 15000 /* `G */, 15000 /* `H */, 15000 /* `I */, 15000 /* `J */, 15000 /* `K */, 15000 /* `L */, 15000 /* `M */, 15000 /* `N */, 15000 /* `O */, 15000 /* `P */, 15000 /* `Q */, 15000 /* `R */, 15000 /* `S */, 15000 /* `T */, 15000 /* `U */, 15000 /* `V */, 15000 /* `W */, 15000 /* `X */, 15000 /* `Y */, 15000 /* `Z */, 15000 /* `[ */, 15000 /* `\ */, 15000 /* `] */, 15000 /* `^ */, 15000 /* `_ */, 15000 /* `` */, 15000 /* `a */, 15000 /* `b */, 15000 /* `c */, 15000 /* `d */, 15000 /* `e */, 15000 /* `f */, 15000 /* `g */, 15000 /* `h */, 15000 /* `i */, 15000 /* `j */, 15000 /* `k */, 15000 /* `l */, 15000 /* `m */, 15000 /* `n */, 15000 /* `o */, 15000 /* `p */, 15000 /* `q */, 15000 /* `r */, 15000 /* `s */, 15000 /* `t */, 15000 /* `u */, 15000 /* `v */, 15000 /* `w */, 15000 /* `x */, 15000 /* `y */, 15000 /* `z */, 15000 /* `{ */, 15000 /* `| */, 15000 /* `} */, 15000 /* `~ */}, + {14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a */, 14225 /* a! */, 13850 /* a" */, 14225 /* a# */, 14225 /* a$ */, 14225 /* a% */, 14225 /* a& */, 13850 /* a' */, 14225 /* a( */, 14225 /* a) */, 13850 /* a* */, 14225 /* a+ */, 14475 /* a, */, 14100 /* a- */, 14475 /* a. */, 14725 /* a/ */, 14225 /* a0 */, 14225 /* a1 */, 14225 /* a2 */, 14225 /* a3 */, 14225 /* a4 */, 14225 /* a5 */, 14225 /* a6 */, 14225 /* a7 */, 14225 /* a8 */, 14225 /* a9 */, 14225 /* a: */, 14225 /* a; */, 14225 /* a< */, 14225 /* a= */, 14225 /* a> */, 14225 /* a? */, 14225 /* a@ */, 14425 /* aA */, 14225 /* aB */, 14225 /* aC */, 14225 /* aD */, 14225 /* aE */, 14225 /* aF */, 14225 /* aG */, 14225 /* aH */, 14225 /* aI */, 14225 /* aJ */, 14225 /* aK */, 14225 /* aL */, 14225 /* aM */, 14225 /* aN */, 14225 /* aO */, 14225 /* aP */, 14225 /* aQ */, 14225 /* aR */, 14225 /* aS */, 12475 /* aT */, 14225 /* aU */, 13475 /* aV */, 13975 /* aW */, 14225 /* aX */, 13100 /* aY */, 14375 /* aZ */, 14225 /* a[ */, 14225 /* a\ */, 14225 /* a] */, 14225 /* a^ */, 14475 /* a_ */, 14225 /* a` */, 14225 /* aa */, 14225 /* ab */, 14225 /* ac */, 14225 /* ad */, 14225 /* ae */, 14225 /* af */, 14225 /* ag */, 14225 /* ah */, 14225 /* ai */, 14225 /* aj */, 14225 /* ak */, 14225 /* al */, 14225 /* am */, 14225 /* an */, 14225 /* ao */, 14225 /* ap */, 14225 /* aq */, 14225 /* ar */, 14225 /* as */, 14075 /* at */, 14225 /* au */, 13975 /* av */, 14075 /* aw */, 14225 /* ax */, 14000 /* ay */, 14225 /* az */, 14225 /* a{ */, 14225 /* a| */, 14225 /* a} */, 14225 /* a~ */}, + {15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b */, 15200 /* b! */, 14700 /* b" */, 15200 /* b# */, 15200 /* b$ */, 15200 /* b% */, 15200 /* b& */, 14700 /* b' */, 15200 /* b( */, 15200 /* b) */, 14700 /* b* */, 15200 /* b+ */, 14825 /* b, */, 15450 /* b- */, 14825 /* b. */, 15200 /* b/ */, 15200 /* b0 */, 15200 /* b1 */, 15200 /* b2 */, 15200 /* b3 */, 15200 /* b4 */, 15200 /* b5 */, 15200 /* b6 */, 15200 /* b7 */, 15200 /* b8 */, 15200 /* b9 */, 15200 /* b: */, 15200 /* b; */, 15200 /* b< */, 15200 /* b= */, 15200 /* b> */, 15200 /* b? */, 15200 /* b@ */, 15200 /* bA */, 15200 /* bB */, 15200 /* bC */, 15200 /* bD */, 15200 /* bE */, 15200 /* bF */, 15200 /* bG */, 15200 /* bH */, 15075 /* bI */, 15200 /* bJ */, 15200 /* bK */, 15200 /* bL */, 15200 /* bM */, 15200 /* bN */, 15200 /* bO */, 15200 /* bP */, 15200 /* bQ */, 15200 /* bR */, 15200 /* bS */, 13575 /* bT */, 15200 /* bU */, 14325 /* bV */, 14575 /* bW */, 14700 /* bX */, 13700 /* bY */, 15075 /* bZ */, 15200 /* b[ */, 15200 /* b\ */, 15200 /* b] */, 15200 /* b^ */, 13950 /* b_ */, 15200 /* b` */, 15200 /* ba */, 15200 /* bb */, 15200 /* bc */, 15200 /* bd */, 15200 /* be */, 15050 /* bf */, 15200 /* bg */, 15200 /* bh */, 15200 /* bi */, 15200 /* bj */, 15200 /* bk */, 15200 /* bl */, 15200 /* bm */, 15200 /* bn */, 15200 /* bo */, 15200 /* bp */, 15200 /* bq */, 15200 /* br */, 15200 /* bs */, 15050 /* bt */, 15200 /* bu */, 15000 /* bv */, 14950 /* bw */, 14800 /* bx */, 14950 /* by */, 15000 /* bz */, 15200 /* b{ */, 15200 /* b| */, 15200 /* b} */, 15200 /* b~ */}, + {12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c */, 12925 /* c! */, 12925 /* c" */, 12925 /* c# */, 12925 /* c$ */, 12925 /* c% */, 12925 /* c& */, 12925 /* c' */, 12925 /* c( */, 12925 /* c) */, 12925 /* c* */, 12925 /* c+ */, 12925 /* c, */, 12925 /* c- */, 12925 /* c. */, 12800 /* c/ */, 12925 /* c0 */, 12925 /* c1 */, 12925 /* c2 */, 12925 /* c3 */, 12925 /* c4 */, 12925 /* c5 */, 12925 /* c6 */, 12925 /* c7 */, 12925 /* c8 */, 12925 /* c9 */, 12925 /* c: */, 12925 /* c; */, 12925 /* c< */, 12925 /* c= */, 12925 /* c> */, 12925 /* c? */, 12925 /* c@ */, 13125 /* cA */, 12925 /* cB */, 12925 /* cC */, 12925 /* cD */, 12925 /* cE */, 12925 /* cF */, 12925 /* cG */, 12925 /* cH */, 12925 /* cI */, 13050 /* cJ */, 12925 /* cK */, 12925 /* cL */, 12925 /* cM */, 12925 /* cN */, 12925 /* cO */, 12925 /* cP */, 12925 /* cQ */, 12925 /* cR */, 12925 /* cS */, 11775 /* cT */, 12925 /* cU */, 12250 /* cV */, 12675 /* cW */, 12925 /* cX */, 11625 /* cY */, 13075 /* cZ */, 12925 /* c[ */, 12925 /* c\ */, 12925 /* c] */, 12925 /* c^ */, 11925 /* c_ */, 12925 /* c` */, 12925 /* ca */, 12925 /* cb */, 12800 /* cc */, 12800 /* cd */, 12800 /* ce */, 12925 /* cf */, 12925 /* cg */, 12925 /* ch */, 12925 /* ci */, 12925 /* cj */, 12925 /* ck */, 12925 /* cl */, 12925 /* cm */, 12925 /* cn */, 12800 /* co */, 12925 /* cp */, 12800 /* cq */, 12925 /* cr */, 12925 /* cs */, 12925 /* ct */, 12925 /* cu */, 12925 /* cv */, 12925 /* cw */, 12925 /* cx */, 12925 /* cy */, 12925 /* cz */, 12925 /* c{ */, 12925 /* c| */, 12925 /* c} */, 12925 /* c~ */}, + {15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d */, 15200 /* d! */, 15200 /* d" */, 15200 /* d# */, 15200 /* d$ */, 15200 /* d% */, 15200 /* d& */, 15200 /* d' */, 15200 /* d( */, 15200 /* d) */, 15200 /* d* */, 15200 /* d+ */, 15200 /* d, */, 15200 /* d- */, 15200 /* d. */, 15200 /* d/ */, 15200 /* d0 */, 15200 /* d1 */, 15200 /* d2 */, 15200 /* d3 */, 15200 /* d4 */, 15200 /* d5 */, 15200 /* d6 */, 15200 /* d7 */, 15200 /* d8 */, 15200 /* d9 */, 15200 /* d: */, 15200 /* d; */, 15200 /* d< */, 15200 /* d= */, 15200 /* d> */, 15200 /* d? */, 15200 /* d@ */, 15200 /* dA */, 15200 /* dB */, 15200 /* dC */, 15200 /* dD */, 15200 /* dE */, 15200 /* dF */, 15200 /* dG */, 15200 /* dH */, 15200 /* dI */, 15200 /* dJ */, 15200 /* dK */, 15200 /* dL */, 15200 /* dM */, 15200 /* dN */, 15200 /* dO */, 15200 /* dP */, 15200 /* dQ */, 15200 /* dR */, 15200 /* dS */, 15200 /* dT */, 15200 /* dU */, 15200 /* dV */, 15200 /* dW */, 15200 /* dX */, 15200 /* dY */, 15200 /* dZ */, 15200 /* d[ */, 15200 /* d\ */, 15200 /* d] */, 15200 /* d^ */, 15200 /* d_ */, 15200 /* d` */, 15200 /* da */, 15200 /* db */, 15200 /* dc */, 15200 /* dd */, 15200 /* de */, 15200 /* df */, 15200 /* dg */, 15200 /* dh */, 15200 /* di */, 15200 /* dj */, 15200 /* dk */, 15200 /* dl */, 15200 /* dm */, 15200 /* dn */, 15200 /* do */, 15200 /* dp */, 15200 /* dq */, 15200 /* dr */, 15200 /* ds */, 15200 /* dt */, 15200 /* du */, 15200 /* dv */, 15200 /* dw */, 15200 /* dx */, 15200 /* dy */, 15200 /* dz */, 15200 /* d{ */, 15200 /* d| */, 15200 /* d} */, 15200 /* d~ */}, + {14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e */, 14050 /* e! */, 13550 /* e" */, 14050 /* e# */, 14050 /* e$ */, 14050 /* e% */, 14050 /* e& */, 13550 /* e' */, 14050 /* e( */, 14050 /* e) */, 13550 /* e* */, 14050 /* e+ */, 13675 /* e, */, 14300 /* e- */, 13675 /* e. */, 14050 /* e/ */, 14050 /* e0 */, 14050 /* e1 */, 14050 /* e2 */, 14050 /* e3 */, 14050 /* e4 */, 14050 /* e5 */, 14050 /* e6 */, 14050 /* e7 */, 14050 /* e8 */, 14050 /* e9 */, 14050 /* e: */, 14050 /* e; */, 14050 /* e< */, 14050 /* e= */, 14050 /* e> */, 14050 /* e? */, 14050 /* e@ */, 14050 /* eA */, 14050 /* eB */, 14050 /* eC */, 14050 /* eD */, 14050 /* eE */, 14050 /* eF */, 14050 /* eG */, 14050 /* eH */, 13925 /* eI */, 14050 /* eJ */, 14050 /* eK */, 14050 /* eL */, 14050 /* eM */, 14050 /* eN */, 14050 /* eO */, 14050 /* eP */, 14050 /* eQ */, 14050 /* eR */, 13800 /* eS */, 12425 /* eT */, 14050 /* eU */, 13175 /* eV */, 13550 /* eW */, 13550 /* eX */, 12550 /* eY */, 13925 /* eZ */, 14050 /* e[ */, 14050 /* e\ */, 14050 /* e] */, 14050 /* e^ */, 12550 /* e_ */, 14050 /* e` */, 14050 /* ea */, 14050 /* eb */, 14050 /* ec */, 14050 /* ed */, 14050 /* ee */, 14050 /* ef */, 14050 /* eg */, 14050 /* eh */, 14050 /* ei */, 14050 /* ej */, 14050 /* ek */, 14050 /* el */, 14050 /* em */, 14050 /* en */, 14050 /* eo */, 14050 /* ep */, 14050 /* eq */, 14050 /* er */, 14050 /* es */, 14050 /* et */, 14050 /* eu */, 13800 /* ev */, 13925 /* ew */, 13650 /* ex */, 13875 /* ey */, 13925 /* ez */, 14050 /* e{ */, 14050 /* e| */, 14050 /* e} */, 14050 /* e~ */}, + {9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9025 /* f */, 9525 /* f! */, 9775 /* f" */, 9025 /* f# */, 9025 /* f$ */, 9025 /* f% */, 8650 /* f& */, 9775 /* f' */, 9025 /* f( */, 9650 /* f) */, 9775 /* f* */, 9025 /* f+ */, 8275 /* f, */, 9025 /* f- */, 8275 /* f. */, 8525 /* f/ */, 9025 /* f0 */, 9025 /* f1 */, 9025 /* f2 */, 9025 /* f3 */, 9025 /* f4 */, 9025 /* f5 */, 9025 /* f6 */, 9025 /* f7 */, 9025 /* f8 */, 9025 /* f9 */, 9025 /* f: */, 9025 /* f; */, 9025 /* f< */, 9025 /* f= */, 9025 /* f> */, 9775 /* f? */, 9025 /* f@ */, 8875 /* fA */, 9025 /* fB */, 9400 /* fC */, 9025 /* fD */, 9025 /* fE */, 9025 /* fF */, 9400 /* fG */, 9025 /* fH */, 9025 /* fI */, 8450 /* fJ */, 9025 /* fK */, 9025 /* fL */, 9025 /* fM */, 9025 /* fN */, 9400 /* fO */, 9025 /* fP */, 9400 /* fQ */, 9025 /* fR */, 9025 /* fS */, 9650 /* fT */, 9025 /* fU */, 9900 /* fV */, 9650 /* fW */, 9375 /* fX */, 9775 /* fY */, 9075 /* fZ */, 9025 /* f[ */, 9025 /* f\ */, 9400 /* f] */, 9025 /* f^ */, 7475 /* f_ */, 9025 /* f` */, 9025 /* fa */, 9025 /* fb */, 8925 /* fc */, 8925 /* fd */, 8925 /* fe */, 9125 /* ff */, 8925 /* fg */, 9025 /* fh */, 9025 /* fi */, 9025 /* fj */, 9025 /* fk */, 9025 /* fl */, 9025 /* fm */, 9025 /* fn */, 8925 /* fo */, 9025 /* fp */, 8925 /* fq */, 9025 /* fr */, 9025 /* fs */, 9025 /* ft */, 9025 /* fu */, 9275 /* fv */, 9025 /* fw */, 9025 /* fx */, 9275 /* fy */, 9025 /* fz */, 9025 /* f{ */, 9025 /* f| */, 9400 /* f} */, 9025 /* f~ */}, + {13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g */, 13800 /* g! */, 13925 /* g" */, 13800 /* g# */, 13800 /* g$ */, 13800 /* g% */, 13675 /* g& */, 13925 /* g' */, 13800 /* g( */, 14475 /* g) */, 13925 /* g* */, 13800 /* g+ */, 13950 /* g, */, 13800 /* g- */, 13950 /* g. */, 14375 /* g/ */, 13800 /* g0 */, 13800 /* g1 */, 13800 /* g2 */, 13800 /* g3 */, 13800 /* g4 */, 13800 /* g5 */, 13800 /* g6 */, 13800 /* g7 */, 13800 /* g8 */, 13800 /* g9 */, 13800 /* g: */, 13800 /* g; */, 13800 /* g< */, 13800 /* g= */, 13800 /* g> */, 13800 /* g? */, 13800 /* g@ */, 14425 /* gA */, 13800 /* gB */, 13800 /* gC */, 13800 /* gD */, 13800 /* gE */, 13800 /* gF */, 13800 /* gG */, 13800 /* gH */, 13800 /* gI */, 13575 /* gJ */, 13800 /* gK */, 13800 /* gL */, 13800 /* gM */, 13800 /* gN */, 13800 /* gO */, 13800 /* gP */, 13800 /* gQ */, 13800 /* gR */, 13800 /* gS */, 13575 /* gT */, 13800 /* gU */, 13625 /* gV */, 13800 /* gW */, 14075 /* gX */, 13600 /* gY */, 14050 /* gZ */, 13800 /* g[ */, 13800 /* g\ */, 14100 /* g] */, 13800 /* g^ */, 15200 /* g_ */, 13800 /* g` */, 13750 /* ga */, 13800 /* gb */, 13650 /* gc */, 13650 /* gd */, 13650 /* ge */, 13800 /* gf */, 13975 /* gg */, 13800 /* gh */, 13800 /* gi */, 14500 /* gj */, 13800 /* gk */, 13800 /* gl */, 13800 /* gm */, 13800 /* gn */, 13650 /* go */, 13800 /* gp */, 13650 /* gq */, 13800 /* gr */, 13800 /* gs */, 13800 /* gt */, 13700 /* gu */, 13800 /* gv */, 13800 /* gw */, 13800 /* gx */, 13925 /* gy */, 13800 /* gz */, 13800 /* g{ */, 13800 /* g| */, 14100 /* g} */, 13800 /* g~ */}, + {14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h */, 14900 /* h! */, 14525 /* h" */, 14900 /* h# */, 14900 /* h$ */, 14900 /* h% */, 14900 /* h& */, 14525 /* h' */, 14900 /* h( */, 14900 /* h) */, 14525 /* h* */, 14900 /* h+ */, 14900 /* h, */, 14900 /* h- */, 14900 /* h. */, 14900 /* h/ */, 14900 /* h0 */, 14900 /* h1 */, 14900 /* h2 */, 14900 /* h3 */, 14900 /* h4 */, 14900 /* h5 */, 14900 /* h6 */, 14900 /* h7 */, 14900 /* h8 */, 14900 /* h9 */, 14900 /* h: */, 14900 /* h; */, 14900 /* h< */, 14900 /* h= */, 14900 /* h> */, 14900 /* h? */, 14900 /* h@ */, 14900 /* hA */, 14900 /* hB */, 14900 /* hC */, 14900 /* hD */, 14900 /* hE */, 14900 /* hF */, 14900 /* hG */, 14900 /* hH */, 14900 /* hI */, 14900 /* hJ */, 14900 /* hK */, 14900 /* hL */, 14900 /* hM */, 14900 /* hN */, 14900 /* hO */, 14900 /* hP */, 14900 /* hQ */, 14900 /* hR */, 14900 /* hS */, 13525 /* hT */, 14900 /* hU */, 14025 /* hV */, 14275 /* hW */, 14900 /* hX */, 13525 /* hY */, 14900 /* hZ */, 14900 /* h[ */, 14900 /* h\ */, 14900 /* h] */, 14900 /* h^ */, 14900 /* h_ */, 14900 /* h` */, 14900 /* ha */, 14900 /* hb */, 14900 /* hc */, 14900 /* hd */, 14900 /* he */, 14900 /* hf */, 14900 /* hg */, 14900 /* hh */, 14900 /* hi */, 14900 /* hj */, 14900 /* hk */, 14900 /* hl */, 14900 /* hm */, 14900 /* hn */, 14900 /* ho */, 14900 /* hp */, 14900 /* hq */, 14900 /* hr */, 14900 /* hs */, 14900 /* ht */, 14900 /* hu */, 14700 /* hv */, 14750 /* hw */, 14900 /* hx */, 14700 /* hy */, 14900 /* hz */, 14900 /* h{ */, 14900 /* h| */, 14900 /* h} */, 14900 /* h~ */}, + {7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i */, 7150 /* i! */, 7150 /* i" */, 7150 /* i# */, 7150 /* i$ */, 7150 /* i% */, 7150 /* i& */, 7150 /* i' */, 7150 /* i( */, 7150 /* i) */, 7150 /* i* */, 7150 /* i+ */, 7150 /* i, */, 7150 /* i- */, 7150 /* i. */, 7150 /* i/ */, 7150 /* i0 */, 7150 /* i1 */, 7150 /* i2 */, 7150 /* i3 */, 7150 /* i4 */, 7150 /* i5 */, 7150 /* i6 */, 7150 /* i7 */, 7150 /* i8 */, 7150 /* i9 */, 7150 /* i: */, 7150 /* i; */, 7150 /* i< */, 7150 /* i= */, 7150 /* i> */, 7150 /* i? */, 7150 /* i@ */, 7150 /* iA */, 7150 /* iB */, 7150 /* iC */, 7150 /* iD */, 7150 /* iE */, 7150 /* iF */, 7150 /* iG */, 7150 /* iH */, 7150 /* iI */, 6900 /* iJ */, 7150 /* iK */, 7150 /* iL */, 7150 /* iM */, 7150 /* iN */, 7150 /* iO */, 7150 /* iP */, 7150 /* iQ */, 7150 /* iR */, 7150 /* iS */, 7150 /* iT */, 7150 /* iU */, 7150 /* iV */, 7150 /* iW */, 7150 /* iX */, 6900 /* iY */, 7150 /* iZ */, 7150 /* i[ */, 7150 /* i\ */, 7150 /* i] */, 7150 /* i^ */, 7150 /* i_ */, 7150 /* i` */, 7150 /* ia */, 7150 /* ib */, 7150 /* ic */, 7150 /* id */, 7150 /* ie */, 7150 /* if */, 7150 /* ig */, 7150 /* ih */, 7150 /* ii */, 7150 /* ij */, 7150 /* ik */, 7150 /* il */, 7150 /* im */, 7150 /* in */, 7150 /* io */, 7150 /* ip */, 7150 /* iq */, 7150 /* ir */, 7150 /* is */, 7150 /* it */, 7150 /* iu */, 7150 /* iv */, 7150 /* iw */, 7150 /* ix */, 7150 /* iy */, 7150 /* iz */, 7150 /* i{ */, 7150 /* i| */, 7150 /* i} */, 7150 /* i~ */}, + {7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j */, 7150 /* j! */, 7150 /* j" */, 7150 /* j# */, 7150 /* j$ */, 7150 /* j% */, 7150 /* j& */, 7150 /* j' */, 7150 /* j( */, 7150 /* j) */, 7150 /* j* */, 7150 /* j+ */, 7150 /* j, */, 7150 /* j- */, 7150 /* j. */, 7150 /* j/ */, 7150 /* j0 */, 7150 /* j1 */, 7150 /* j2 */, 7150 /* j3 */, 7150 /* j4 */, 7150 /* j5 */, 7150 /* j6 */, 7150 /* j7 */, 7150 /* j8 */, 7150 /* j9 */, 7150 /* j: */, 7150 /* j; */, 7150 /* j< */, 7150 /* j= */, 7150 /* j> */, 7150 /* j? */, 7150 /* j@ */, 7150 /* jA */, 7150 /* jB */, 7150 /* jC */, 7150 /* jD */, 7150 /* jE */, 7150 /* jF */, 7150 /* jG */, 7150 /* jH */, 7150 /* jI */, 7150 /* jJ */, 7150 /* jK */, 7150 /* jL */, 7150 /* jM */, 7150 /* jN */, 7150 /* jO */, 7150 /* jP */, 7150 /* jQ */, 7150 /* jR */, 7150 /* jS */, 6150 /* jT */, 7150 /* jU */, 6525 /* jV */, 6900 /* jW */, 7150 /* jX */, 7150 /* jY */, 7150 /* jZ */, 7150 /* j[ */, 7150 /* j\ */, 7150 /* j] */, 7150 /* j^ */, 7150 /* j_ */, 7150 /* j` */, 7150 /* ja */, 7150 /* jb */, 7150 /* jc */, 7150 /* jd */, 7150 /* je */, 7150 /* jf */, 7150 /* jg */, 7150 /* jh */, 7150 /* ji */, 7150 /* jj */, 7150 /* jk */, 7150 /* jl */, 7150 /* jm */, 7150 /* jn */, 7150 /* jo */, 7150 /* jp */, 7150 /* jq */, 7150 /* jr */, 7150 /* js */, 7150 /* jt */, 7150 /* ju */, 7150 /* jv */, 7150 /* jw */, 7150 /* jx */, 7150 /* jy */, 7150 /* jz */, 7150 /* j{ */, 7150 /* j| */, 7150 /* j} */, 7150 /* j~ */}, + {14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k */, 14425 /* k! */, 14425 /* k" */, 14425 /* k# */, 14425 /* k$ */, 14425 /* k% */, 14175 /* k& */, 14425 /* k' */, 14425 /* k( */, 14425 /* k) */, 14425 /* k* */, 14425 /* k+ */, 14425 /* k, */, 13550 /* k- */, 14425 /* k. */, 14425 /* k/ */, 14425 /* k0 */, 14425 /* k1 */, 14425 /* k2 */, 14425 /* k3 */, 14425 /* k4 */, 14425 /* k5 */, 14425 /* k6 */, 14425 /* k7 */, 14425 /* k8 */, 14425 /* k9 */, 14425 /* k: */, 14425 /* k; */, 14425 /* k< */, 14425 /* k= */, 14425 /* k> */, 14425 /* k? */, 14175 /* k@ */, 14800 /* kA */, 14425 /* kB */, 14100 /* kC */, 14425 /* kD */, 14425 /* kE */, 14425 /* kF */, 14100 /* kG */, 14425 /* kH */, 14425 /* kI */, 14300 /* kJ */, 14425 /* kK */, 14425 /* kL */, 14425 /* kM */, 14425 /* kN */, 14100 /* kO */, 14425 /* kP */, 14100 /* kQ */, 14425 /* kR */, 14250 /* kS */, 13625 /* kT */, 14425 /* kU */, 13975 /* kV */, 14250 /* kW */, 14425 /* kX */, 13275 /* kY */, 14825 /* kZ */, 14425 /* k[ */, 14425 /* k\ */, 14425 /* k] */, 14425 /* k^ */, 14925 /* k_ */, 14425 /* k` */, 14425 /* ka */, 14425 /* kb */, 13750 /* kc */, 13925 /* kd */, 13750 /* ke */, 14425 /* kf */, 14425 /* kg */, 14425 /* kh */, 14425 /* ki */, 14425 /* kj */, 14425 /* kk */, 14425 /* kl */, 14425 /* km */, 14425 /* kn */, 13750 /* ko */, 14425 /* kp */, 13925 /* kq */, 14425 /* kr */, 14175 /* ks */, 14175 /* kt */, 14175 /* ku */, 14125 /* kv */, 14300 /* kw */, 14425 /* kx */, 14300 /* ky */, 14425 /* kz */, 14425 /* k{ */, 14425 /* k| */, 14425 /* k} */, 14425 /* k~ */}, + {7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l */, 7575 /* l! */, 7575 /* l" */, 7575 /* l# */, 7575 /* l$ */, 7575 /* l% */, 7575 /* l& */, 7575 /* l' */, 7575 /* l( */, 7575 /* l) */, 7575 /* l* */, 7575 /* l+ */, 7825 /* l, */, 7325 /* l- */, 7825 /* l. */, 7575 /* l/ */, 7575 /* l0 */, 7575 /* l1 */, 7575 /* l2 */, 7575 /* l3 */, 7575 /* l4 */, 7575 /* l5 */, 7575 /* l6 */, 7575 /* l7 */, 7575 /* l8 */, 7575 /* l9 */, 7575 /* l: */, 7575 /* l; */, 7575 /* l< */, 7575 /* l= */, 7575 /* l> */, 7575 /* l? */, 7575 /* l@ */, 8175 /* lA */, 7575 /* lB */, 7575 /* lC */, 7575 /* lD */, 7575 /* lE */, 7575 /* lF */, 7575 /* lG */, 7575 /* lH */, 7575 /* lI */, 7825 /* lJ */, 7575 /* lK */, 7575 /* lL */, 7575 /* lM */, 7575 /* lN */, 7575 /* lO */, 7575 /* lP */, 7575 /* lQ */, 7575 /* lR */, 7575 /* lS */, 7250 /* lT */, 7575 /* lU */, 7450 /* lV */, 7575 /* lW */, 7950 /* lX */, 7200 /* lY */, 7950 /* lZ */, 7575 /* l[ */, 7575 /* l\ */, 7575 /* l] */, 7575 /* l^ */, 8200 /* l_ */, 7575 /* l` */, 7575 /* la */, 7575 /* lb */, 7575 /* lc */, 7575 /* ld */, 7575 /* le */, 7575 /* lf */, 7575 /* lg */, 7575 /* lh */, 7575 /* li */, 7575 /* lj */, 7575 /* lk */, 7375 /* ll */, 7575 /* lm */, 7575 /* ln */, 7575 /* lo */, 7575 /* lp */, 7575 /* lq */, 7575 /* lr */, 7575 /* ls */, 7450 /* lt */, 7575 /* lu */, 7425 /* lv */, 7475 /* lw */, 7575 /* lx */, 7425 /* ly */, 7725 /* lz */, 7575 /* l{ */, 7575 /* l| */, 7575 /* l} */, 7575 /* l~ */}, + {22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m */, 22350 /* m! */, 21975 /* m" */, 22350 /* m# */, 22350 /* m$ */, 22350 /* m% */, 22350 /* m& */, 21975 /* m' */, 22350 /* m( */, 22350 /* m) */, 21975 /* m* */, 22350 /* m+ */, 22350 /* m, */, 22350 /* m- */, 22350 /* m. */, 22350 /* m/ */, 22350 /* m0 */, 22350 /* m1 */, 22350 /* m2 */, 22350 /* m3 */, 22350 /* m4 */, 22350 /* m5 */, 22350 /* m6 */, 22350 /* m7 */, 22350 /* m8 */, 22350 /* m9 */, 22350 /* m: */, 22350 /* m; */, 22350 /* m< */, 22350 /* m= */, 22350 /* m> */, 22350 /* m? */, 22350 /* m@ */, 22350 /* mA */, 22350 /* mB */, 22350 /* mC */, 22350 /* mD */, 22350 /* mE */, 22350 /* mF */, 22350 /* mG */, 22350 /* mH */, 22350 /* mI */, 22350 /* mJ */, 22350 /* mK */, 22350 /* mL */, 22350 /* mM */, 22350 /* mN */, 22350 /* mO */, 22350 /* mP */, 22350 /* mQ */, 22350 /* mR */, 22350 /* mS */, 20975 /* mT */, 22350 /* mU */, 21475 /* mV */, 21725 /* mW */, 22350 /* mX */, 20975 /* mY */, 22350 /* mZ */, 22350 /* m[ */, 22350 /* m\ */, 22350 /* m] */, 22350 /* m^ */, 22350 /* m_ */, 22350 /* m` */, 22350 /* ma */, 22350 /* mb */, 22350 /* mc */, 22350 /* md */, 22350 /* me */, 22350 /* mf */, 22350 /* mg */, 22350 /* mh */, 22350 /* mi */, 22350 /* mj */, 22350 /* mk */, 22350 /* ml */, 22350 /* mm */, 22350 /* mn */, 22350 /* mo */, 22350 /* mp */, 22350 /* mq */, 22350 /* mr */, 22350 /* ms */, 22350 /* mt */, 22350 /* mu */, 22150 /* mv */, 22200 /* mw */, 22350 /* mx */, 22150 /* my */, 22350 /* mz */, 22350 /* m{ */, 22350 /* m| */, 22350 /* m} */, 22350 /* m~ */}, + {14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n */, 14900 /* n! */, 14525 /* n" */, 14900 /* n# */, 14900 /* n$ */, 14900 /* n% */, 14900 /* n& */, 14525 /* n' */, 14900 /* n( */, 14900 /* n) */, 14525 /* n* */, 14900 /* n+ */, 14900 /* n, */, 14900 /* n- */, 14900 /* n. */, 14900 /* n/ */, 14900 /* n0 */, 14900 /* n1 */, 14900 /* n2 */, 14900 /* n3 */, 14900 /* n4 */, 14900 /* n5 */, 14900 /* n6 */, 14900 /* n7 */, 14900 /* n8 */, 14900 /* n9 */, 14900 /* n: */, 14900 /* n; */, 14900 /* n< */, 14900 /* n= */, 14900 /* n> */, 14900 /* n? */, 14900 /* n@ */, 14900 /* nA */, 14900 /* nB */, 14900 /* nC */, 14900 /* nD */, 14900 /* nE */, 14900 /* nF */, 14900 /* nG */, 14900 /* nH */, 14900 /* nI */, 14900 /* nJ */, 14900 /* nK */, 14900 /* nL */, 14900 /* nM */, 14900 /* nN */, 14900 /* nO */, 14900 /* nP */, 14900 /* nQ */, 14900 /* nR */, 14900 /* nS */, 13525 /* nT */, 14900 /* nU */, 14025 /* nV */, 14275 /* nW */, 14900 /* nX */, 13525 /* nY */, 14900 /* nZ */, 14900 /* n[ */, 14900 /* n\ */, 14900 /* n] */, 14900 /* n^ */, 14900 /* n_ */, 14900 /* n` */, 14900 /* na */, 14900 /* nb */, 14900 /* nc */, 14900 /* nd */, 14900 /* ne */, 14900 /* nf */, 14900 /* ng */, 14900 /* nh */, 14900 /* ni */, 14900 /* nj */, 14900 /* nk */, 14900 /* nl */, 14900 /* nm */, 14900 /* nn */, 14900 /* no */, 14900 /* np */, 14900 /* nq */, 14900 /* nr */, 14900 /* ns */, 14900 /* nt */, 14900 /* nu */, 14700 /* nv */, 14750 /* nw */, 14900 /* nx */, 14700 /* ny */, 14900 /* nz */, 14900 /* n{ */, 14900 /* n| */, 14900 /* n} */, 14900 /* n~ */}, + {14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o */, 14100 /* o! */, 13600 /* o" */, 14100 /* o# */, 14100 /* o$ */, 14100 /* o% */, 14100 /* o& */, 13600 /* o' */, 14100 /* o( */, 14100 /* o) */, 13600 /* o* */, 14100 /* o+ */, 13725 /* o, */, 14350 /* o- */, 13725 /* o. */, 14100 /* o/ */, 14100 /* o0 */, 14100 /* o1 */, 14100 /* o2 */, 14100 /* o3 */, 14100 /* o4 */, 14100 /* o5 */, 14100 /* o6 */, 14100 /* o7 */, 14100 /* o8 */, 14100 /* o9 */, 14100 /* o: */, 14100 /* o; */, 14100 /* o< */, 14100 /* o= */, 14100 /* o> */, 14100 /* o? */, 14100 /* o@ */, 14100 /* oA */, 14100 /* oB */, 14100 /* oC */, 14100 /* oD */, 14100 /* oE */, 14100 /* oF */, 14100 /* oG */, 14100 /* oH */, 13975 /* oI */, 14100 /* oJ */, 14100 /* oK */, 14100 /* oL */, 14100 /* oM */, 14100 /* oN */, 14100 /* oO */, 14100 /* oP */, 14100 /* oQ */, 14100 /* oR */, 14100 /* oS */, 12475 /* oT */, 14100 /* oU */, 13225 /* oV */, 13475 /* oW */, 13600 /* oX */, 12600 /* oY */, 13975 /* oZ */, 14100 /* o[ */, 14100 /* o\ */, 14100 /* o] */, 14100 /* o^ */, 12600 /* o_ */, 14100 /* o` */, 14100 /* oa */, 14100 /* ob */, 14100 /* oc */, 14100 /* od */, 14100 /* oe */, 13950 /* of */, 14100 /* og */, 14100 /* oh */, 14100 /* oi */, 14100 /* oj */, 14100 /* ok */, 14100 /* ol */, 14100 /* om */, 14100 /* on */, 14100 /* oo */, 14100 /* op */, 14100 /* oq */, 14100 /* or */, 14100 /* os */, 13950 /* ot */, 14100 /* ou */, 13900 /* ov */, 13850 /* ow */, 13700 /* ox */, 13850 /* oy */, 13900 /* oz */, 14100 /* o{ */, 14100 /* o| */, 14100 /* o} */, 14100 /* o~ */}, + {15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p */, 15200 /* p! */, 14700 /* p" */, 15200 /* p# */, 15200 /* p$ */, 15200 /* p% */, 15200 /* p& */, 14700 /* p' */, 15200 /* p( */, 15200 /* p) */, 14700 /* p* */, 15200 /* p+ */, 14825 /* p, */, 15450 /* p- */, 14825 /* p. */, 15200 /* p/ */, 15200 /* p0 */, 15200 /* p1 */, 15200 /* p2 */, 15200 /* p3 */, 15200 /* p4 */, 15200 /* p5 */, 15200 /* p6 */, 15200 /* p7 */, 15200 /* p8 */, 15200 /* p9 */, 15200 /* p: */, 15200 /* p; */, 15200 /* p< */, 15200 /* p= */, 15200 /* p> */, 15200 /* p? */, 15200 /* p@ */, 15200 /* pA */, 15200 /* pB */, 15200 /* pC */, 15200 /* pD */, 15200 /* pE */, 15200 /* pF */, 15200 /* pG */, 15200 /* pH */, 15075 /* pI */, 15200 /* pJ */, 15200 /* pK */, 15200 /* pL */, 15200 /* pM */, 15200 /* pN */, 15200 /* pO */, 15200 /* pP */, 15200 /* pQ */, 15200 /* pR */, 15200 /* pS */, 13575 /* pT */, 15200 /* pU */, 14325 /* pV */, 14575 /* pW */, 14700 /* pX */, 13700 /* pY */, 15075 /* pZ */, 15200 /* p[ */, 15200 /* p\ */, 15200 /* p] */, 15200 /* p^ */, 13950 /* p_ */, 15200 /* p` */, 15200 /* pa */, 15200 /* pb */, 15200 /* pc */, 15200 /* pd */, 15200 /* pe */, 15050 /* pf */, 15200 /* pg */, 15200 /* ph */, 15200 /* pi */, 15200 /* pj */, 15200 /* pk */, 15200 /* pl */, 15200 /* pm */, 15200 /* pn */, 15200 /* po */, 15200 /* pp */, 15200 /* pq */, 15200 /* pr */, 15200 /* ps */, 15050 /* pt */, 15200 /* pu */, 15000 /* pv */, 14950 /* pw */, 14800 /* px */, 14950 /* py */, 15000 /* pz */, 15200 /* p{ */, 15200 /* p| */, 15200 /* p} */, 15200 /* p~ */}, + {15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q */, 15200 /* q! */, 15200 /* q" */, 15200 /* q# */, 15200 /* q$ */, 15200 /* q% */, 15200 /* q& */, 15200 /* q' */, 15200 /* q( */, 15200 /* q) */, 15200 /* q* */, 15200 /* q+ */, 15200 /* q, */, 15200 /* q- */, 15200 /* q. */, 15200 /* q/ */, 15200 /* q0 */, 15200 /* q1 */, 15200 /* q2 */, 15200 /* q3 */, 15200 /* q4 */, 15200 /* q5 */, 15200 /* q6 */, 15200 /* q7 */, 15200 /* q8 */, 15200 /* q9 */, 15200 /* q: */, 15200 /* q; */, 15200 /* q< */, 15200 /* q= */, 15200 /* q> */, 15200 /* q? */, 15200 /* q@ */, 15200 /* qA */, 15200 /* qB */, 15200 /* qC */, 15200 /* qD */, 15200 /* qE */, 15200 /* qF */, 15200 /* qG */, 15200 /* qH */, 15200 /* qI */, 15200 /* qJ */, 15200 /* qK */, 15200 /* qL */, 15200 /* qM */, 15200 /* qN */, 15200 /* qO */, 15200 /* qP */, 15200 /* qQ */, 15200 /* qR */, 15200 /* qS */, 14200 /* qT */, 15200 /* qU */, 14575 /* qV */, 14950 /* qW */, 15200 /* qX */, 14450 /* qY */, 15200 /* qZ */, 15200 /* q[ */, 15200 /* q\ */, 15200 /* q] */, 15200 /* q^ */, 15200 /* q_ */, 15200 /* q` */, 15200 /* qa */, 15200 /* qb */, 15200 /* qc */, 15200 /* qd */, 15200 /* qe */, 15200 /* qf */, 15200 /* qg */, 15200 /* qh */, 15200 /* qi */, 15200 /* qj */, 15200 /* qk */, 15200 /* ql */, 15200 /* qm */, 15200 /* qn */, 15200 /* qo */, 15200 /* qp */, 15200 /* qq */, 15200 /* qr */, 15200 /* qs */, 15200 /* qt */, 15200 /* qu */, 15200 /* qv */, 15200 /* qw */, 15200 /* qx */, 15200 /* qy */, 15200 /* qz */, 15200 /* q{ */, 15200 /* q| */, 15200 /* q} */, 15200 /* q~ */}, + {10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r */, 10100 /* r! */, 10850 /* r" */, 10100 /* r# */, 10100 /* r$ */, 10100 /* r% */, 9600 /* r& */, 10850 /* r' */, 10100 /* r( */, 10100 /* r) */, 10850 /* r* */, 10100 /* r+ */, 8100 /* r, */, 10100 /* r- */, 8100 /* r. */, 9550 /* r/ */, 10100 /* r0 */, 10100 /* r1 */, 10100 /* r2 */, 10100 /* r3 */, 10100 /* r4 */, 10100 /* r5 */, 10100 /* r6 */, 10100 /* r7 */, 10100 /* r8 */, 10100 /* r9 */, 10100 /* r: */, 10100 /* r; */, 10100 /* r< */, 10100 /* r= */, 10100 /* r> */, 10100 /* r? */, 10100 /* r@ */, 9100 /* rA */, 10100 /* rB */, 10350 /* rC */, 10100 /* rD */, 10100 /* rE */, 10100 /* rF */, 10350 /* rG */, 10100 /* rH */, 10100 /* rI */, 9500 /* rJ */, 10100 /* rK */, 10100 /* rL */, 10100 /* rM */, 10100 /* rN */, 10350 /* rO */, 10100 /* rP */, 10350 /* rQ */, 10100 /* rR */, 10275 /* rS */, 9850 /* rT */, 10100 /* rU */, 10100 /* rV */, 10100 /* rW */, 9600 /* rX */, 9600 /* rY */, 9825 /* rZ */, 10100 /* r[ */, 10100 /* r\ */, 10100 /* r] */, 10100 /* r^ */, 8100 /* r_ */, 10100 /* r` */, 9750 /* ra */, 10100 /* rb */, 9975 /* rc */, 10000 /* rd */, 9975 /* re */, 10325 /* rf */, 9750 /* rg */, 10100 /* rh */, 10100 /* ri */, 10100 /* rj */, 10100 /* rk */, 10100 /* rl */, 10100 /* rm */, 10100 /* rn */, 9975 /* ro */, 10100 /* rp */, 10000 /* rq */, 10100 /* rr */, 10100 /* rs */, 10200 /* rt */, 10100 /* ru */, 10275 /* rv */, 10225 /* rw */, 10100 /* rx */, 10275 /* ry */, 10100 /* rz */, 10100 /* r{ */, 10100 /* r| */, 10100 /* r} */, 10100 /* r~ */}, + {12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s */, 12600 /* s! */, 12350 /* s" */, 12600 /* s# */, 12600 /* s$ */, 12600 /* s% */, 12600 /* s& */, 12350 /* s' */, 12600 /* s( */, 12600 /* s) */, 12350 /* s* */, 12600 /* s+ */, 12400 /* s, */, 12600 /* s- */, 12400 /* s. */, 12600 /* s/ */, 12600 /* s0 */, 12600 /* s1 */, 12600 /* s2 */, 12600 /* s3 */, 12600 /* s4 */, 12600 /* s5 */, 12600 /* s6 */, 12600 /* s7 */, 12600 /* s8 */, 12600 /* s9 */, 12600 /* s: */, 12600 /* s; */, 12600 /* s< */, 12600 /* s= */, 12600 /* s> */, 12600 /* s? */, 12600 /* s@ */, 12650 /* sA */, 12600 /* sB */, 12600 /* sC */, 12600 /* sD */, 12600 /* sE */, 12600 /* sF */, 12600 /* sG */, 12600 /* sH */, 12600 /* sI */, 12550 /* sJ */, 12600 /* sK */, 12600 /* sL */, 12600 /* sM */, 12600 /* sN */, 12600 /* sO */, 12600 /* sP */, 12600 /* sQ */, 12600 /* sR */, 12600 /* sS */, 10975 /* sT */, 12600 /* sU */, 11775 /* sV */, 12600 /* sW */, 12025 /* sX */, 11250 /* sY */, 12600 /* sZ */, 12600 /* s[ */, 12600 /* s\ */, 12600 /* s] */, 12600 /* s^ */, 11350 /* s_ */, 12600 /* s` */, 12600 /* sa */, 12600 /* sb */, 12600 /* sc */, 12600 /* sd */, 12600 /* se */, 12525 /* sf */, 12600 /* sg */, 12600 /* sh */, 12600 /* si */, 12600 /* sj */, 12600 /* sk */, 12600 /* sl */, 12600 /* sm */, 12600 /* sn */, 12600 /* so */, 12600 /* sp */, 12600 /* sq */, 12600 /* sr */, 12425 /* ss */, 12400 /* st */, 12600 /* su */, 12300 /* sv */, 12350 /* sw */, 12175 /* sx */, 12375 /* sy */, 12350 /* sz */, 12600 /* s{ */, 12600 /* s| */, 12600 /* s} */, 12600 /* s~ */}, + {9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t */, 9575 /* t! */, 9575 /* t" */, 9575 /* t# */, 9575 /* t$ */, 9575 /* t% */, 9325 /* t& */, 9575 /* t' */, 9575 /* t( */, 9575 /* t) */, 9575 /* t* */, 9575 /* t+ */, 9575 /* t, */, 9325 /* t- */, 9575 /* t. */, 9575 /* t/ */, 9575 /* t0 */, 9575 /* t1 */, 9575 /* t2 */, 9575 /* t3 */, 9575 /* t4 */, 9575 /* t5 */, 9575 /* t6 */, 9575 /* t7 */, 9575 /* t8 */, 9575 /* t9 */, 9575 /* t: */, 9575 /* t; */, 9575 /* t< */, 9575 /* t= */, 9575 /* t> */, 9575 /* t? */, 9575 /* t@ */, 9725 /* tA */, 9575 /* tB */, 9575 /* tC */, 9575 /* tD */, 9575 /* tE */, 9575 /* tF */, 9575 /* tG */, 9575 /* tH */, 9575 /* tI */, 9575 /* tJ */, 9575 /* tK */, 9575 /* tL */, 9575 /* tM */, 9575 /* tN */, 9575 /* tO */, 9575 /* tP */, 9575 /* tQ */, 9575 /* tR */, 9575 /* tS */, 8975 /* tT */, 9575 /* tU */, 9575 /* tV */, 9575 /* tW */, 9575 /* tX */, 9075 /* tY */, 9575 /* tZ */, 9575 /* t[ */, 9575 /* t\ */, 9575 /* t] */, 9575 /* t^ */, 10075 /* t_ */, 9575 /* t` */, 9575 /* ta */, 9575 /* tb */, 9475 /* tc */, 9475 /* td */, 9475 /* te */, 9575 /* tf */, 9575 /* tg */, 9575 /* th */, 9575 /* ti */, 9575 /* tj */, 9575 /* tk */, 9575 /* tl */, 9575 /* tm */, 9575 /* tn */, 9475 /* to */, 9575 /* tp */, 9475 /* tq */, 9575 /* tr */, 9575 /* ts */, 9450 /* tt */, 9575 /* tu */, 9575 /* tv */, 9575 /* tw */, 9575 /* tx */, 9575 /* ty */, 9575 /* tz */, 9575 /* t{ */, 9575 /* t| */, 9575 /* t} */, 9575 /* t~ */}, + {14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u */, 14900 /* u! */, 14900 /* u" */, 14900 /* u# */, 14900 /* u$ */, 14900 /* u% */, 14900 /* u& */, 14900 /* u' */, 14900 /* u( */, 14900 /* u) */, 14900 /* u* */, 14900 /* u+ */, 14900 /* u, */, 14900 /* u- */, 14900 /* u. */, 14900 /* u/ */, 14900 /* u0 */, 14900 /* u1 */, 14900 /* u2 */, 14900 /* u3 */, 14900 /* u4 */, 14900 /* u5 */, 14900 /* u6 */, 14900 /* u7 */, 14900 /* u8 */, 14900 /* u9 */, 14900 /* u: */, 14900 /* u; */, 14900 /* u< */, 14900 /* u= */, 14900 /* u> */, 14900 /* u? */, 14900 /* u@ */, 14900 /* uA */, 14900 /* uB */, 14900 /* uC */, 14900 /* uD */, 14900 /* uE */, 14900 /* uF */, 14900 /* uG */, 14900 /* uH */, 14900 /* uI */, 14900 /* uJ */, 14900 /* uK */, 14900 /* uL */, 14900 /* uM */, 14900 /* uN */, 14900 /* uO */, 14900 /* uP */, 14900 /* uQ */, 14900 /* uR */, 14900 /* uS */, 13775 /* uT */, 14900 /* uU */, 14275 /* uV */, 14650 /* uW */, 14900 /* uX */, 13650 /* uY */, 14900 /* uZ */, 14900 /* u[ */, 14900 /* u\ */, 14900 /* u] */, 14900 /* u^ */, 14900 /* u_ */, 14900 /* u` */, 14900 /* ua */, 14900 /* ub */, 14900 /* uc */, 14900 /* ud */, 14900 /* ue */, 14900 /* uf */, 14900 /* ug */, 14900 /* uh */, 14900 /* ui */, 14900 /* uj */, 14900 /* uk */, 14900 /* ul */, 14900 /* um */, 14900 /* un */, 14900 /* uo */, 14900 /* up */, 14900 /* uq */, 14900 /* ur */, 14900 /* us */, 14900 /* ut */, 14900 /* uu */, 14900 /* uv */, 14900 /* uw */, 14900 /* ux */, 14900 /* uy */, 14900 /* uz */, 14900 /* u{ */, 14900 /* u| */, 14900 /* u} */, 14900 /* u~ */}, + {13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v */, 13450 /* v! */, 14075 /* v" */, 13450 /* v# */, 13450 /* v$ */, 13450 /* v% */, 12950 /* v& */, 14075 /* v' */, 13450 /* v( */, 13450 /* v) */, 14075 /* v* */, 13450 /* v+ */, 12250 /* v, */, 13325 /* v- */, 12250 /* v. */, 13075 /* v/ */, 13450 /* v0 */, 13450 /* v1 */, 13450 /* v2 */, 13450 /* v3 */, 13450 /* v4 */, 13450 /* v5 */, 13450 /* v6 */, 13450 /* v7 */, 13450 /* v8 */, 13450 /* v9 */, 13450 /* v: */, 13450 /* v; */, 13450 /* v< */, 13450 /* v= */, 13450 /* v> */, 13450 /* v? */, 13450 /* v@ */, 12700 /* vA */, 13450 /* vB */, 13450 /* vC */, 13450 /* vD */, 13450 /* vE */, 13450 /* vF */, 13450 /* vG */, 13450 /* vH */, 13325 /* vI */, 12700 /* vJ */, 13450 /* vK */, 13450 /* vL */, 13450 /* vM */, 13450 /* vN */, 13450 /* vO */, 13450 /* vP */, 13450 /* vQ */, 13450 /* vR */, 13450 /* vS */, 12825 /* vT */, 13450 /* vU */, 13200 /* vV */, 13450 /* vW */, 13450 /* vX */, 12575 /* vY */, 13450 /* vZ */, 13450 /* v[ */, 13450 /* v\ */, 13450 /* v] */, 13450 /* v^ */, 11700 /* v_ */, 13450 /* v` */, 13225 /* va */, 13450 /* vb */, 13250 /* vc */, 13250 /* vd */, 13250 /* ve */, 13600 /* vf */, 13125 /* vg */, 13450 /* vh */, 13450 /* vi */, 13450 /* vj */, 13450 /* vk */, 13450 /* vl */, 13450 /* vm */, 13450 /* vn */, 13250 /* vo */, 13450 /* vp */, 13250 /* vq */, 13450 /* vr */, 13400 /* vs */, 13450 /* vt */, 13450 /* vu */, 13450 /* vv */, 13450 /* vw */, 13450 /* vx */, 13450 /* vy */, 13350 /* vz */, 13450 /* v{ */, 13450 /* v| */, 13450 /* v} */, 13450 /* v~ */}, + {21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w */, 21025 /* w! */, 21525 /* w" */, 21025 /* w# */, 21025 /* w$ */, 21025 /* w% */, 20525 /* w& */, 21525 /* w' */, 21025 /* w( */, 21025 /* w) */, 21525 /* w* */, 21025 /* w+ */, 20025 /* w, */, 20925 /* w- */, 20025 /* w. */, 21025 /* w/ */, 21025 /* w0 */, 21025 /* w1 */, 21025 /* w2 */, 21025 /* w3 */, 21025 /* w4 */, 21025 /* w5 */, 21025 /* w6 */, 21025 /* w7 */, 21025 /* w8 */, 21025 /* w9 */, 21025 /* w: */, 21025 /* w; */, 21025 /* w< */, 21025 /* w= */, 21025 /* w> */, 21025 /* w? */, 20775 /* w@ */, 20525 /* wA */, 21025 /* wB */, 21025 /* wC */, 21025 /* wD */, 21025 /* wE */, 21025 /* wF */, 21025 /* wG */, 21025 /* wH */, 21025 /* wI */, 20275 /* wJ */, 21025 /* wK */, 21025 /* wL */, 21025 /* wM */, 21025 /* wN */, 21025 /* wO */, 21025 /* wP */, 21025 /* wQ */, 21025 /* wR */, 21025 /* wS */, 20150 /* wT */, 21025 /* wU */, 20775 /* wV */, 20775 /* wW */, 21025 /* wX */, 20025 /* wY */, 21025 /* wZ */, 21025 /* w[ */, 21025 /* w\ */, 21025 /* w] */, 21025 /* w^ */, 19775 /* w_ */, 21025 /* w` */, 20675 /* wa */, 21025 /* wb */, 20775 /* wc */, 20775 /* wd */, 20775 /* we */, 21025 /* wf */, 20600 /* wg */, 21025 /* wh */, 21025 /* wi */, 21025 /* wj */, 21025 /* wk */, 21025 /* wl */, 21025 /* wm */, 21025 /* wn */, 20775 /* wo */, 21025 /* wp */, 20775 /* wq */, 21025 /* wr */, 20925 /* ws */, 21025 /* wt */, 21025 /* wu */, 21025 /* wv */, 21025 /* ww */, 21025 /* wx */, 21025 /* wy */, 21025 /* wz */, 21025 /* w{ */, 21025 /* w| */, 21025 /* w} */, 21025 /* w~ */}, + {14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x */, 14000 /* x! */, 14000 /* x" */, 14000 /* x# */, 14000 /* x$ */, 14000 /* x% */, 13250 /* x& */, 14000 /* x' */, 14000 /* x( */, 14000 /* x) */, 14000 /* x* */, 14000 /* x+ */, 14000 /* x, */, 13125 /* x- */, 14000 /* x. */, 14000 /* x/ */, 14000 /* x0 */, 14000 /* x1 */, 14000 /* x2 */, 14000 /* x3 */, 14000 /* x4 */, 14000 /* x5 */, 14000 /* x6 */, 14000 /* x7 */, 14000 /* x8 */, 14000 /* x9 */, 14000 /* x: */, 14000 /* x; */, 14000 /* x< */, 14000 /* x= */, 14000 /* x> */, 14000 /* x? */, 13500 /* x@ */, 14000 /* xA */, 14000 /* xB */, 14000 /* xC */, 14000 /* xD */, 14000 /* xE */, 14000 /* xF */, 14000 /* xG */, 14000 /* xH */, 14000 /* xI */, 13625 /* xJ */, 14000 /* xK */, 14000 /* xL */, 14000 /* xM */, 14000 /* xN */, 14000 /* xO */, 14000 /* xP */, 14000 /* xQ */, 14000 /* xR */, 14000 /* xS */, 13125 /* xT */, 14000 /* xU */, 13750 /* xV */, 13750 /* xW */, 14000 /* xX */, 13000 /* xY */, 14000 /* xZ */, 14000 /* x[ */, 14000 /* x\ */, 14000 /* x] */, 14000 /* x^ */, 14500 /* x_ */, 14000 /* x` */, 14000 /* xa */, 14000 /* xb */, 13600 /* xc */, 13600 /* xd */, 13600 /* xe */, 14000 /* xf */, 14000 /* xg */, 14000 /* xh */, 14000 /* xi */, 14000 /* xj */, 14000 /* xk */, 14000 /* xl */, 14000 /* xm */, 14000 /* xn */, 13600 /* xo */, 14000 /* xp */, 13600 /* xq */, 14000 /* xr */, 13900 /* xs */, 14000 /* xt */, 14000 /* xu */, 14000 /* xv */, 14000 /* xw */, 14000 /* xx */, 14000 /* xy */, 14000 /* xz */, 14000 /* x{ */, 14000 /* x| */, 14000 /* x} */, 14000 /* x~ */}, + {13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y */, 13350 /* y! */, 13850 /* y" */, 13350 /* y# */, 13350 /* y$ */, 13350 /* y% */, 12600 /* y& */, 13850 /* y' */, 13350 /* y( */, 13350 /* y) */, 13850 /* y* */, 13350 /* y+ */, 12225 /* y, */, 13225 /* y- */, 12225 /* y. */, 13225 /* y/ */, 13350 /* y0 */, 13350 /* y1 */, 13350 /* y2 */, 13350 /* y3 */, 13350 /* y4 */, 13350 /* y5 */, 13350 /* y6 */, 13350 /* y7 */, 13350 /* y8 */, 13350 /* y9 */, 13350 /* y: */, 13350 /* y; */, 13350 /* y< */, 13350 /* y= */, 13350 /* y> */, 13350 /* y? */, 13100 /* y@ */, 12975 /* yA */, 13350 /* yB */, 13350 /* yC */, 13350 /* yD */, 13350 /* yE */, 13350 /* yF */, 13350 /* yG */, 13350 /* yH */, 13225 /* yI */, 12600 /* yJ */, 13350 /* yK */, 13350 /* yL */, 13350 /* yM */, 13350 /* yN */, 13350 /* yO */, 13350 /* yP */, 13350 /* yQ */, 13350 /* yR */, 13350 /* yS */, 12600 /* yT */, 13350 /* yU */, 13100 /* yV */, 13350 /* yW */, 13350 /* yX */, 12475 /* yY */, 13350 /* yZ */, 13350 /* y[ */, 13350 /* y\ */, 13350 /* y] */, 13350 /* y^ */, 11350 /* y_ */, 13350 /* y` */, 13000 /* ya */, 13350 /* yb */, 13150 /* yc */, 13150 /* yd */, 13150 /* ye */, 13525 /* yf */, 12825 /* yg */, 13350 /* yh */, 13350 /* yi */, 13350 /* yj */, 13350 /* yk */, 13350 /* yl */, 13350 /* ym */, 13350 /* yn */, 13150 /* yo */, 13350 /* yp */, 13150 /* yq */, 13350 /* yr */, 13125 /* ys */, 13350 /* yt */, 13350 /* yu */, 13350 /* yv */, 13350 /* yw */, 13350 /* yx */, 13350 /* yy */, 13250 /* yz */, 13350 /* y{ */, 13350 /* y| */, 13350 /* y} */, 13350 /* y~ */}, + {12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z */, 12950 /* z! */, 13075 /* z" */, 12950 /* z# */, 12950 /* z$ */, 12950 /* z% */, 12700 /* z& */, 13075 /* z' */, 12950 /* z( */, 12950 /* z) */, 13075 /* z* */, 12950 /* z+ */, 13200 /* z, */, 12450 /* z- */, 13200 /* z. */, 12950 /* z/ */, 12950 /* z0 */, 12950 /* z1 */, 12950 /* z2 */, 12950 /* z3 */, 12950 /* z4 */, 12950 /* z5 */, 12950 /* z6 */, 12950 /* z7 */, 12950 /* z8 */, 12950 /* z9 */, 12950 /* z: */, 12950 /* z; */, 12950 /* z< */, 12950 /* z= */, 12950 /* z> */, 12950 /* z? */, 12700 /* z@ */, 13200 /* zA */, 12950 /* zB */, 12950 /* zC */, 12950 /* zD */, 12950 /* zE */, 12950 /* zF */, 12950 /* zG */, 12950 /* zH */, 12950 /* zI */, 12950 /* zJ */, 12950 /* zK */, 12950 /* zL */, 12950 /* zM */, 12950 /* zN */, 12950 /* zO */, 12950 /* zP */, 12950 /* zQ */, 12950 /* zR */, 12950 /* zS */, 12075 /* zT */, 12950 /* zU */, 12700 /* zV */, 12825 /* zW */, 13200 /* zX */, 12200 /* zY */, 12950 /* zZ */, 12950 /* z[ */, 12950 /* z\ */, 12950 /* z] */, 12950 /* z^ */, 13075 /* z_ */, 12950 /* z` */, 12950 /* za */, 12950 /* zb */, 12650 /* zc */, 12700 /* zd */, 12650 /* ze */, 12950 /* zf */, 12950 /* zg */, 12950 /* zh */, 12950 /* zi */, 12950 /* zj */, 12950 /* zk */, 12950 /* zl */, 12950 /* zm */, 12950 /* zn */, 12650 /* zo */, 12950 /* zp */, 12700 /* zq */, 12950 /* zr */, 12950 /* zs */, 12950 /* zt */, 12950 /* zu */, 12950 /* zv */, 12950 /* zw */, 12950 /* zx */, 12950 /* zy */, 12950 /* zz */, 12950 /* z{ */, 12950 /* z| */, 12950 /* z} */, 12950 /* z~ */}, + {9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* { */, 9300 /* {! */, 9300 /* {" */, 9300 /* {# */, 9300 /* {$ */, 9300 /* {% */, 9300 /* {& */, 9300 /* {' */, 9300 /* {( */, 9300 /* {) */, 9300 /* {* */, 9300 /* {+ */, 9300 /* {, */, 9300 /* {- */, 9300 /* {. */, 9300 /* {/ */, 9300 /* {0 */, 9300 /* {1 */, 9300 /* {2 */, 9300 /* {3 */, 9300 /* {4 */, 9300 /* {5 */, 9300 /* {6 */, 9300 /* {7 */, 9300 /* {8 */, 9300 /* {9 */, 9300 /* {: */, 9300 /* {; */, 9300 /* {< */, 9300 /* {= */, 9300 /* {> */, 9300 /* {? */, 9300 /* {@ */, 9300 /* {A */, 9300 /* {B */, 9300 /* {C */, 9300 /* {D */, 9300 /* {E */, 9300 /* {F */, 9300 /* {G */, 9300 /* {H */, 9300 /* {I */, 9300 /* {J */, 9300 /* {K */, 9300 /* {L */, 9300 /* {M */, 9300 /* {N */, 9300 /* {O */, 9300 /* {P */, 9300 /* {Q */, 9300 /* {R */, 9300 /* {S */, 9300 /* {T */, 9300 /* {U */, 9300 /* {V */, 9300 /* {W */, 9300 /* {X */, 9300 /* {Y */, 9300 /* {Z */, 9300 /* {[ */, 9300 /* {\ */, 9300 /* {] */, 9300 /* {^ */, 9300 /* {_ */, 9300 /* {` */, 9300 /* {a */, 9300 /* {b */, 9300 /* {c */, 9300 /* {d */, 9300 /* {e */, 9300 /* {f */, 9300 /* {g */, 9300 /* {h */, 9300 /* {i */, 9550 /* {j */, 9300 /* {k */, 9300 /* {l */, 9300 /* {m */, 9300 /* {n */, 9300 /* {o */, 9300 /* {p */, 9300 /* {q */, 9300 /* {r */, 9300 /* {s */, 9300 /* {t */, 9300 /* {u */, 9300 /* {v */, 9300 /* {w */, 9300 /* {x */, 9300 /* {y */, 9300 /* {z */, 9300 /* {{ */, 9300 /* {| */, 9300 /* {} */, 9300 /* {~ */}, + {10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* | */, 10050 /* |! */, 10050 /* |" */, 10050 /* |# */, 10050 /* |$ */, 10050 /* |% */, 10050 /* |& */, 10050 /* |' */, 10050 /* |( */, 10050 /* |) */, 10050 /* |* */, 10050 /* |+ */, 10050 /* |, */, 10050 /* |- */, 10050 /* |. */, 10050 /* |/ */, 10050 /* |0 */, 10050 /* |1 */, 10050 /* |2 */, 10050 /* |3 */, 10050 /* |4 */, 10050 /* |5 */, 10050 /* |6 */, 10050 /* |7 */, 10050 /* |8 */, 10050 /* |9 */, 10050 /* |: */, 10050 /* |; */, 10050 /* |< */, 10050 /* |= */, 10050 /* |> */, 10050 /* |? */, 10050 /* |@ */, 10050 /* |A */, 10050 /* |B */, 10050 /* |C */, 10050 /* |D */, 10050 /* |E */, 10050 /* |F */, 10050 /* |G */, 10050 /* |H */, 10050 /* |I */, 10050 /* |J */, 10050 /* |K */, 10050 /* |L */, 10050 /* |M */, 10050 /* |N */, 10050 /* |O */, 10050 /* |P */, 10050 /* |Q */, 10050 /* |R */, 10050 /* |S */, 10050 /* |T */, 10050 /* |U */, 10050 /* |V */, 10050 /* |W */, 10050 /* |X */, 10050 /* |Y */, 10050 /* |Z */, 10050 /* |[ */, 10050 /* |\ */, 10050 /* |] */, 10050 /* |^ */, 10050 /* |_ */, 10050 /* |` */, 10050 /* |a */, 10050 /* |b */, 10050 /* |c */, 10050 /* |d */, 10050 /* |e */, 10050 /* |f */, 10050 /* |g */, 10050 /* |h */, 10050 /* |i */, 10050 /* |j */, 10050 /* |k */, 10050 /* |l */, 10050 /* |m */, 10050 /* |n */, 10050 /* |o */, 10050 /* |p */, 10050 /* |q */, 10050 /* |r */, 10050 /* |s */, 10050 /* |t */, 10050 /* |u */, 10050 /* |v */, 10050 /* |w */, 10050 /* |x */, 10050 /* |y */, 10050 /* |z */, 10050 /* |{ */, 10050 /* || */, 10050 /* |} */, 10050 /* |~ */}, + {9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* } */, 9300 /* }! */, 9300 /* }" */, 9300 /* }# */, 9300 /* }$ */, 9300 /* }% */, 9300 /* }& */, 9300 /* }' */, 9300 /* }( */, 9300 /* }) */, 9300 /* }* */, 9300 /* }+ */, 9300 /* }, */, 9300 /* }- */, 9300 /* }. */, 9300 /* }/ */, 9300 /* }0 */, 9300 /* }1 */, 9300 /* }2 */, 9300 /* }3 */, 9300 /* }4 */, 9300 /* }5 */, 9300 /* }6 */, 9300 /* }7 */, 9300 /* }8 */, 9300 /* }9 */, 9300 /* }: */, 9300 /* }; */, 9300 /* }< */, 9300 /* }= */, 9300 /* }> */, 9300 /* }? */, 9300 /* }@ */, 9300 /* }A */, 9300 /* }B */, 9300 /* }C */, 9300 /* }D */, 9300 /* }E */, 9300 /* }F */, 9300 /* }G */, 9300 /* }H */, 9300 /* }I */, 9300 /* }J */, 9300 /* }K */, 9300 /* }L */, 9300 /* }M */, 9300 /* }N */, 9300 /* }O */, 9300 /* }P */, 9300 /* }Q */, 9300 /* }R */, 9300 /* }S */, 9300 /* }T */, 9300 /* }U */, 9300 /* }V */, 9300 /* }W */, 9300 /* }X */, 9300 /* }Y */, 9300 /* }Z */, 9300 /* }[ */, 9300 /* }\ */, 9300 /* }] */, 9300 /* }^ */, 9300 /* }_ */, 9300 /* }` */, 9300 /* }a */, 9300 /* }b */, 9300 /* }c */, 9300 /* }d */, 9300 /* }e */, 9300 /* }f */, 9300 /* }g */, 9300 /* }h */, 9300 /* }i */, 9300 /* }j */, 9300 /* }k */, 9300 /* }l */, 9300 /* }m */, 9300 /* }n */, 9300 /* }o */, 9300 /* }p */, 9300 /* }q */, 9300 /* }r */, 9300 /* }s */, 9300 /* }t */, 9300 /* }u */, 9300 /* }v */, 9300 /* }w */, 9300 /* }x */, 9300 /* }y */, 9300 /* }z */, 9300 /* }{ */, 9300 /* }| */, 9300 /* }} */, 9300 /* }~ */}, + {15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~ */, 15000 /* ~! */, 15000 /* ~" */, 15000 /* ~# */, 15000 /* ~$ */, 15000 /* ~% */, 15000 /* ~& */, 15000 /* ~' */, 15000 /* ~( */, 15000 /* ~) */, 15000 /* ~* */, 15000 /* ~+ */, 15000 /* ~, */, 15000 /* ~- */, 15000 /* ~. */, 15000 /* ~/ */, 15000 /* ~0 */, 15000 /* ~1 */, 15000 /* ~2 */, 15000 /* ~3 */, 15000 /* ~4 */, 15000 /* ~5 */, 15000 /* ~6 */, 15000 /* ~7 */, 15000 /* ~8 */, 15000 /* ~9 */, 15000 /* ~: */, 15000 /* ~; */, 15000 /* ~< */, 15000 /* ~= */, 15000 /* ~> */, 15000 /* ~? */, 15000 /* ~@ */, 15000 /* ~A */, 15000 /* ~B */, 15000 /* ~C */, 15000 /* ~D */, 15000 /* ~E */, 15000 /* ~F */, 15000 /* ~G */, 15000 /* ~H */, 15000 /* ~I */, 15000 /* ~J */, 15000 /* ~K */, 15000 /* ~L */, 15000 /* ~M */, 15000 /* ~N */, 15000 /* ~O */, 15000 /* ~P */, 15000 /* ~Q */, 15000 /* ~R */, 15000 /* ~S */, 15000 /* ~T */, 15000 /* ~U */, 15000 /* ~V */, 15000 /* ~W */, 15000 /* ~X */, 15000 /* ~Y */, 15000 /* ~Z */, 15000 /* ~[ */, 15000 /* ~\ */, 15000 /* ~] */, 15000 /* ~^ */, 15000 /* ~_ */, 15000 /* ~` */, 15000 /* ~a */, 15000 /* ~b */, 15000 /* ~c */, 15000 /* ~d */, 15000 /* ~e */, 15000 /* ~f */, 15000 /* ~g */, 15000 /* ~h */, 15000 /* ~i */, 15000 /* ~j */, 15000 /* ~k */, 15000 /* ~l */, 15000 /* ~m */, 15000 /* ~n */, 15000 /* ~o */, 15000 /* ~p */, 15000 /* ~q */, 15000 /* ~r */, 15000 /* ~s */, 15000 /* ~t */, 15000 /* ~u */, 15000 /* ~v */, 15000 /* ~w */, 15000 /* ~x */, 15000 /* ~y */, 15000 /* ~z */, 15000 /* ~{ */, 15000 /* ~| */, 15000 /* ~} */, 15000 /* ~~ */}, +}; + +static const unsigned short int ibm_plex_sans_bold_250_em_size = 20475; + +static double ibm_plex_sans_bold_word_width(const char *s, double fontSize) { + unsigned long int totalWidth = 0; + + while(*s) { + if (IS_UTF8_STARTBYTE(*s)) { + s++; + + while(IS_UTF8_BYTE(*s) && !IS_UTF8_STARTBYTE(*s)) + s++; + + totalWidth += ibm_plex_sans_bold_250_em_size; + } + else { + if (*s >= 0 && *s <= 126) // Check if it's a valid ASCII character (including '\0') + totalWidth += ibm_plex_sans_bold_250[(unsigned char)*s][(unsigned char)s[1]]; + + s++; + } + } + + // Convert the width from the encoded value to the actual float value + double actualWidth = (double)totalWidth / 100.0; + + // Scale the width proportionally based on the desired font size + double scaledWidth = actualWidth * (fontSize / 250.0); + + return scaledWidth; +} + +/* + + <svg viewBox="0 0 1000 500" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"> + <!-- White bounding box with rounded corners --> + <rect x="0" y="0" width="1000" height="500" fill="blue" rx="50" ry="50"/> + + <!-- Grouped content scaled down by 10% and centered --> + <!-- Black background --> + <rect x="25" y="25" width="950" height="450" fill="black" rx="35" ry="35"/> + + <!-- Netdata logo (scaled down and moved up) --> + <path transform="translate(450,50) scale(0.215)" + d="M 615.38 819.2 H 413.58 L 0.0 0.0 H 587.01 C 815.3 0.41 1000.24 190.3 1000.35 424.49 C 1000.0 642.76 827.81 819.25 615.33 819.25 L 615.38 819.2 Z " + fill="#00AB44"/> + + <!-- First line: 'I' --> + <text x="350" y="225" font-family="IBM Plex Sans" font-weight="bold" font-size="250" fill="white">I</text> + + <!-- Second line: 'TROUBLE' or any other word --> + <text x="50" y="435" font-family="IBM Plex Sans" font-weight="bold" font-size="250" fill="white" textLength="900" lengthAdjust="spacingAndGlyphs">TROUBLE</text> + + </svg> + + */ + +static bool word_goes_below_baseline(const char *love) { + const char *s = love; + while(*s) { + switch(*s) { + case 'g': + case 'j': + case 'p': + case 'q': + case 'y': + case 'Q': + return true; + } + + s++; + } + + return false; +} + +static void generate_ilove_svg(BUFFER *wb, const char *love) { + const char *i = "I"; + const char *stretch = "spacing"; + + double font_size = 250.0; + double border_width = 25.0; + double logo_scale = 0.215; + double logo_width = 1000.0 * logo_scale; + double i_width = ibm_plex_sans_bold_word_width(i, font_size); + double first_line_width = i_width + logo_width; + double second_line_font_size = font_size; + double second_line_width = ibm_plex_sans_bold_word_width(love, second_line_font_size); + bool second_line_needs_height = word_goes_below_baseline(love); + + if(second_line_width <= first_line_width) { + second_line_width = first_line_width; + stretch = "spacingAndGlyphs"; + + if(!second_line_needs_height) + second_line_font_size *= 1.10; + } + else if(second_line_width > first_line_width * 4) { + second_line_width *= 0.80; + stretch = "spacingAndGlyphs"; + second_line_font_size *= 0.90; + } + else if(second_line_width > first_line_width * 2) { + second_line_width *= 0.93; + stretch = "spacing"; + } + + double width = second_line_width + border_width * 4.0; + + buffer_flush(wb); + + buffer_sprintf(wb, "<svg viewBox=\"0 0 %.0f 500\" preserveAspectRatio=\"xMidYMid meet\" xmlns=\"http://www.w3.org/2000/svg\">\n", + width); + + // White bounding box with rounded corners + buffer_sprintf(wb, " <rect x=\"0\" y=\"0\" width=\"%.0f\" height=\"500\" fill=\"white\" rx=\"%.0f\" ry=\"%.0f\"/>\n", + width, border_width * 2, border_width * 2); + + // Black background + buffer_sprintf(wb, " <rect x=\"%.0f\" y=\"%.0f\" width=\"%.0f\" height=\"450\" fill=\"black\" rx=\"%.0f\" ry=\"%.0f\"/>\n", + border_width, border_width, width - border_width * 2, border_width * 1.5, border_width * 1.5); + + // Netdata logo + buffer_sprintf(wb, " <path transform=\"translate(%.0f,%.0f) scale(%.3f)\"\n" + " d=\"M 615.38 819.2 H 413.58 L 0.0 0.0 H 587.01 C 815.3 0.41 1000.24 190.3 1000.35 424.49 C 1000.0 642.76 827.81 819.25 615.33 819.25 L 615.38 819.2 Z \"\n" + " fill=\"#00AB44\"/>\n", + (width - first_line_width) / 2 + i_width, border_width * 2, logo_scale); + + // first line + double first_line_baseline = font_size * 0.70 + border_width * 2; + buffer_sprintf(wb, " <text x=\"%.0f\" y=\"%.0f\" font-family=\"IBM Plex Sans\" font-weight=\"bold\" font-size=\"%.0f\" fill=\"white\">%s</text>\n", + (width - first_line_width) / 2, first_line_baseline, font_size, i); + + // second line + double second_line_baseline = first_line_baseline + font_size * 0.85; + if(second_line_needs_height) + second_line_baseline = first_line_baseline + font_size * 0.78; + + buffer_sprintf(wb, " <text x=\"%.0f\" y=\"%.0f\" font-family=\"IBM Plex Sans\" font-weight=\"bold\" font-size=\"%.0f\" fill=\"white\" textLength=\"%.0f\" lengthAdjust=\"%s\">%s</text>\n", + border_width * 2, second_line_baseline, second_line_font_size, second_line_width, stretch, love); + + buffer_sprintf(wb, "</svg>"); + + wb->content_type = CT_IMAGE_SVG_XML; +} + +int web_client_api_request_v2_ilove(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + char *love = "TROUBLE"; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "love")) love = value; + } + +// char *s = love; +// while(*s) { +// *s = toupper(*s); +// s++; +// } + + generate_ilove_svg(w->response.data, love); + + return HTTP_RESP_OK; +} diff --git a/src/web/api/ilove/ilove.h b/src/web/api/ilove/ilove.h new file mode 100644 index 000000000..010c19c6b --- /dev/null +++ b/src/web/api/ilove/ilove.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_API_ILOVE_H +#define NETDATA_WEB_API_ILOVE_H 1 + +#include "libnetdata/libnetdata.h" +#include "web/server/web_client.h" + +int web_client_api_request_v2_ilove(RRDHOST *host, struct web_client *w, char *url); + +#include "web/api/web_api_v1.h" + +#endif /* NETDATA_WEB_API_ILOVE_H */ diff --git a/src/web/api/ilove/measure-text.js b/src/web/api/ilove/measure-text.js new file mode 100644 index 000000000..e2a2a6e94 --- /dev/null +++ b/src/web/api/ilove/measure-text.js @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict'; + +var path = require('path'); +var fs = require('fs'); +var PDFDocument = require('pdfkit'); +var doc = new PDFDocument({size:'A4', layout:'landscape'}); + +function loadFont(fontPaths, callback) { + for (let fontPath of fontPaths) { + try { + doc = doc.font(fontPath); + if (callback) { callback(null); } + return; // Exit once a font is loaded successfully + } catch(err) { + // Log error but continue to next font path + console.error(`Failed to load font from path: ${fontPath}. Error: ${err.message}`); + } + } + + // If we reached here, none of the fonts were loaded successfully. + console.error('All font paths failed. Stopping execution.'); + process.exit(1); // Exit with an error code +} + +loadFont(['IBMPlexSans-Bold.ttf'], function(err) { + if (err) { + console.error('Could not load any of the specified fonts.'); + } +}); + +doc = doc.fontSize(250); + +function measureCombination(charA, charB) { + return doc.widthOfString(charA + charB); +} + +function getCharRepresentation(charCode) { + return (charCode >= 32 && charCode <= 126) ? String.fromCharCode(charCode) : ''; +} + +function generateCombinationArray() { + let output = "static const unsigned short int ibm_plex_sans_bold_250[128][128] = {\n"; + + for (let i = 0; i <= 126; i++) { + output += " {"; // Start of inner array + for (let j = 0; j <= 126; j++) { + let charA = getCharRepresentation(i); + let charB = getCharRepresentation(j); + let width = measureCombination(charA, charB) - doc.widthOfString(charB); + let encodedWidth = Math.round(width * 100); // Multiply by 100 and round + + if(charA === '*' && charB == '/') + charB = '\\/'; + + if(charA === '/' && charB == '*') + charB = '\\*'; + + output += `${encodedWidth} /* ${charA}${charB} */`; + if (j < 126) { + output += ", "; + } + } + output += "},\n"; // End of inner array + } + output += "};\n"; // End of 2D array + + return output; +} + +console.log(generateCombinationArray()); +console.log('static const unsigned short int ibm_plex_sans_bold_250_em_size = ' + Math.round(doc.widthOfString('M') * 100) + ';'); diff --git a/src/web/api/netdata-swagger.json b/src/web/api/netdata-swagger.json new file mode 100644 index 000000000..3017f5d5c --- /dev/null +++ b/src/web/api/netdata-swagger.json @@ -0,0 +1,4764 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Netdata API", + "description": "Real-time performance and health monitoring.", + "version": "v1-rolling", + "contact": { + "name": "Netdata Agent API", + "email": "info@netdata.cloud", + "url": "https://netdata.cloud" + }, + "license": { + "name": "GPL v3+", + "url": "https://github.com/netdata/netdata/blob/master/LICENSE" + } + }, + "servers": [ + { + "url": "https://registry.my-netdata.io" + }, + { + "url": "http://registry.my-netdata.io" + }, + { + "url": "http://localhost:19999" + } + ], + "tags": [ + { + "name": "nodes", + "description": "Everything related to monitored nodes" + }, + { + "name": "charts", + "description": "Everything related to chart instances - DO NOT USE IN NEW CODE - use contexts instead" + }, + { + "name": "contexts", + "description": "Everything related contexts - in new code, use this instead of charts" + }, + { + "name": "data", + "description": "Everything related to data queries" + }, + { + "name": "badges", + "description": "Everything related to dynamic badges based on metric data" + }, + { + "name": "weights", + "description": "Everything related to scoring / weighting metrics" + }, + { + "name": "functions", + "description": "Everything related to functions" + }, + { + "name": "alerts", + "description": "Everything related to alerts" + }, + { + "name": "management", + "description": "Everything related to managing netdata agents" + } + ], + "paths": { + "/api/v2/nodes": { + "get": { + "operationId": "getNodes2", + "tags": [ + "nodes" + ], + "summary": "Nodes Info v2", + "description": "Get a list of all nodes hosted by this Netdata agent.\n", + "parameters": [ + { + "$ref": "#/components/parameters/scopeNodes" + }, + { + "$ref": "#/components/parameters/scopeContexts" + }, + { + "$ref": "#/components/parameters/filterNodes" + }, + { + "$ref": "#/components/parameters/filterContexts" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "description": "`/api/v2/nodes` response for all nodes hosted by a Netdata agent.\n", + "type": "object", + "properties": { + "api": { + "$ref": "#/components/schemas/api" + }, + "agents": { + "$ref": "#/components/schemas/agents" + }, + "versions": { + "$ref": "#/components/schemas/versions" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nodeFull" + } + } + } + } + } + } + } + } + } + }, + "/api/v2/contexts": { + "get": { + "operationId": "getContexts2", + "tags": [ + "contexts" + ], + "summary": "Contexts Info v2", + "description": "Get a list of all contexts, across all nodes, hosted by this Netdata agent.\n", + "parameters": [ + { + "$ref": "#/components/parameters/scopeNodes" + }, + { + "$ref": "#/components/parameters/scopeContexts" + }, + { + "$ref": "#/components/parameters/filterNodes" + }, + { + "$ref": "#/components/parameters/filterContexts" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contexts2" + } + } + } + } + } + } + }, + "/api/v2/q": { + "get": { + "operationId": "q2", + "tags": [ + "contexts" + ], + "summary": "Full Text Search v2", + "description": "Get a list of contexts, across all nodes, hosted by this Netdata agent, matching a string expression\n", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "The strings to search for, formatted as a simple pattern", + "required": true, + "schema": { + "type": "string", + "format": "simple pattern" + } + }, + { + "$ref": "#/components/parameters/scopeNodes" + }, + { + "$ref": "#/components/parameters/scopeContexts" + }, + { + "$ref": "#/components/parameters/filterNodes" + }, + { + "$ref": "#/components/parameters/filterContexts" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/contexts2" + } + } + } + } + } + } + }, + "/api/v1/info": { + "get": { + "operationId": "getNodeInfo1", + "tags": [ + "nodes" + ], + "summary": "Node Info v1", + "description": "The info endpoint returns basic information about netdata. It provides:\n* netdata version\n* netdata unique id\n* list of hosts mirrored (includes itself)\n* Operating System, Virtualization, K8s nodes and Container technology information\n* List of active collector plugins and modules\n* Streaming information\n* number of alarms in the host\n * number of alarms in normal state\n * number of alarms in warning state\n * number of alarms in critical state\n", + "responses": { + "200": { + "description": "netdata basic information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/info" + } + } + } + }, + "503": { + "description": "netdata daemon not ready (used for health checks)." + } + } + } + }, + "/api/v1/charts": { + "get": { + "operationId": "getNodeCharts1", + "tags": [ + "charts" + ], + "summary": "List all charts v1 - EOL", + "description": "The charts endpoint returns a summary about all charts stored in the netdata server.", + "responses": { + "200": { + "description": "An array of charts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/chart_summary" + } + } + } + } + } + } + }, + "/api/v1/chart": { + "get": { + "operationId": "getNodeChart1", + "tags": [ + "charts" + ], + "summary": "Get one chart v1 - EOL", + "description": "The chart endpoint returns detailed information about a chart.", + "parameters": [ + { + "$ref": "#/components/parameters/chart" + } + ], + "responses": { + "200": { + "description": "A javascript object with detailed information about the chart.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/chart" + } + } + } + }, + "400": { + "description": "No chart id was supplied in the request." + }, + "404": { + "description": "No chart with the given id is found." + } + } + } + }, + "/api/v1/contexts": { + "get": { + "operationId": "getNodeContexts1", + "tags": [ + "contexts" + ], + "summary": "Get a list of all node contexts available v1", + "description": "The contexts endpoint returns a summary about all contexts stored in the netdata server.", + "parameters": [ + { + "$ref": "#/components/parameters/dimensions" + }, + { + "$ref": "#/components/parameters/chart_label_key" + }, + { + "$ref": "#/components/parameters/chart_labels_filter" + }, + { + "$ref": "#/components/parameters/contextOptions1" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + } + ], + "responses": { + "200": { + "description": "An array of contexts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/context_summary" + } + } + } + } + } + } + }, + "/api/v1/context": { + "get": { + "operationId": "getNodeContext1", + "tags": [ + "contexts" + ], + "summary": "Get info about a specific context", + "description": "The context endpoint returns detailed information about a given context.\nThe `context` parameter is required for this call.\n", + "parameters": [ + { + "$ref": "#/components/parameters/context" + }, + { + "$ref": "#/components/parameters/dimensions" + }, + { + "$ref": "#/components/parameters/chart_label_key" + }, + { + "$ref": "#/components/parameters/chart_labels_filter" + }, + { + "$ref": "#/components/parameters/contextOptions1" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + } + ], + "responses": { + "200": { + "description": "A javascript object with detailed information about the context.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/context" + } + } + } + }, + "400": { + "description": "No context id was supplied in the request." + }, + "404": { + "description": "No context with the given id is found." + } + } + } + }, + "/api/v1/config": { + "get": { + "operationId": "getConfig", + "tags": [ + "dyncfg" + ], + "description": "Get dynamic configuration information.\n", + "parameters": [ + { + "name": "action", + "in": "query", + "description": "The type of information required", + "schema": { + "type": "string", + "enum": [ + "tree", + "schema", + "get", + "enable", + "disable", + "restart" + ], + "default": "tree" + } + }, + { + "name": "id", + "in": "query", + "description": "The ID of the dynamic configuration entity", + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "query", + "description": "Top level path of the configuration entities, used with action 'tree'", + "schema": { + "type": "string", + "default": "/" + } + }, + { + "name": "timeout", + "in": "query", + "description": "The timeout in seconds", + "schema": { + "type": "number", + "default": 120 + } + } + ], + "responses": { + "200": { + "description": "The call was successful.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/config_default_response" + }, + { + "$ref": "#/components/schemas/config_tree" + }, + { + "$ref": "#/components/schemas/config_schema" + } + ] + } + } + } + }, + "400": { + "description": "Something is wrong with the request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + }, + "404": { + "description": "The configurable entity requests is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + } + } + }, + "post": { + "operationId": "postConfig", + "tags": [ + "dyncfg" + ], + "description": "Post dynamic configuration to Netdata.\n", + "parameters": [ + { + "name": "action", + "in": "query", + "description": "The type of action required.", + "schema": { + "type": "string", + "enum": [ + "add", + "test", + "update" + ] + } + }, + { + "name": "id", + "in": "query", + "description": "The ID of the dynamic configuration entity to configure.", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of the dynamic configuration entity, used with action 'add'", + "schema": { + "type": "string" + } + }, + { + "name": "timeout", + "in": "query", + "description": "The timeout in seconds", + "schema": { + "type": "number", + "default": 120 + } + } + ], + "responses": { + "200": { + "description": "The call was successful. This also means the configuration is currently running.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + }, + "202": { + "description": "The call was successful. The configuration has been accepted, but its status is not yet known.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + }, + "299": { + "description": "The call was successful. The configuration has been accepted, but a restart is required to apply it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + }, + "400": { + "description": "Something is wrong with the request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + }, + "404": { + "description": "The configurable entity requests is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/config_default_response" + } + } + } + } + } + } + }, + "/api/v2/data": { + "get": { + "operationId": "dataQuery2", + "tags": [ + "data" + ], + "summary": "Data Query v2", + "description": "Multi-node, multi-context, multi-instance, multi-dimension data queries, with time and metric aggregation.\n", + "parameters": [ + { + "name": "group_by", + "in": "query", + "description": "A comma separated list of the groupings required.\nAll possible values can be combined together, except `selected`. If `selected` is given in the list, all others are ignored.\nThe order they are placed in the list is currently ignored.\nThis parameter is also accepted as `group_by[0]` and `group_by[1]` when multiple grouping passes are required.\n", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "dimension", + "instance", + "percentage-of-instance", + "label", + "node", + "context", + "units", + "selected" + ] + }, + "default": [ + "dimension" + ] + } + }, + { + "name": "group_by_label", + "in": "query", + "description": "A comma separated list of the label keys to group by their values. The order of the labels in the list is respected.\nThis parameter is also accepted as `group_by_label[0]` and `group_by_label[1]` when multiple grouping passes are required.\n", + "required": false, + "schema": { + "type": "string", + "format": "comma separated list of label keys to group by", + "default": "" + } + }, + { + "name": "aggregation", + "in": "query", + "description": "The aggregation function to apply when grouping metrics together.\nWhen option `raw` is given, `average` and `avg` behave like `sum` and the caller is expected to calculate the average.\nThis parameter is also accepted as `aggregation[0]` and `aggregation[1]` when multiple grouping passes are required.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "min", + "max", + "avg", + "average", + "sum", + "percentage" + ], + "default": "average" + } + }, + { + "$ref": "#/components/parameters/scopeNodes" + }, + { + "$ref": "#/components/parameters/scopeContexts" + }, + { + "$ref": "#/components/parameters/filterNodes" + }, + { + "$ref": "#/components/parameters/filterContexts" + }, + { + "$ref": "#/components/parameters/filterInstances" + }, + { + "$ref": "#/components/parameters/filterLabels" + }, + { + "$ref": "#/components/parameters/filterAlerts" + }, + { + "$ref": "#/components/parameters/filterDimensions" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/points" + }, + { + "$ref": "#/components/parameters/tier" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "$ref": "#/components/parameters/dataTimeGroup2" + }, + { + "$ref": "#/components/parameters/dataTimeGroupOptions2" + }, + { + "$ref": "#/components/parameters/dataTimeResampling2" + }, + { + "$ref": "#/components/parameters/dataFormat2" + }, + { + "$ref": "#/components/parameters/timeoutMS" + }, + { + "$ref": "#/components/parameters/callback" + }, + { + "$ref": "#/components/parameters/filename" + }, + { + "$ref": "#/components/parameters/tqx" + } + ], + "responses": { + "200": { + "description": "The call was successful. The response includes the data in the format requested.\n", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2" + }, + { + "$ref": "#/components/schemas/data_json_formats2" + } + ] + } + }, + "text/plain": { + "schema": { + "type": "string", + "format": "according to the format requested." + } + }, + "text/html": { + "schema": { + "type": "string", + "format": "html" + } + }, + "application/x-javascript": { + "schema": { + "type": "string", + "format": "javascript" + } + } + } + }, + "400": { + "description": "Bad request - the body will include a message stating what is wrong.\n" + }, + "500": { + "description": "Internal server error. This usually means the server is out of memory.\n" + } + } + } + }, + "/api/v1/data": { + "get": { + "operationId": "dataQuery1", + "tags": [ + "data" + ], + "summary": "Data Query v1 - Single node, single chart or context queries. without group-by.", + "description": "Query metric data of a chart or context of a node and return a dataset having time-series data for all dimensions available.\nFor group-by functionality, use `/api/v2/data`.\nAt least a `chart` or a `context` have to be given for the data query to be executed.\n", + "parameters": [ + { + "$ref": "#/components/parameters/chart" + }, + { + "$ref": "#/components/parameters/context" + }, + { + "$ref": "#/components/parameters/dimension" + }, + { + "$ref": "#/components/parameters/chart_label_key" + }, + { + "$ref": "#/components/parameters/chart_labels_filter" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/points" + }, + { + "$ref": "#/components/parameters/tier" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "$ref": "#/components/parameters/dataFormat1" + }, + { + "$ref": "#/components/parameters/dataTimeGroup1" + }, + { + "$ref": "#/components/parameters/dataTimeGroupOptions1" + }, + { + "$ref": "#/components/parameters/dataTimeResampling1" + }, + { + "$ref": "#/components/parameters/timeoutMS" + }, + { + "$ref": "#/components/parameters/callback" + }, + { + "$ref": "#/components/parameters/filename" + }, + { + "$ref": "#/components/parameters/tqx" + } + ], + "responses": { + "200": { + "description": "The call was successful. The response includes the data in the format requested.\n", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap1" + }, + { + "$ref": "#/components/schemas/data_json_formats1" + } + ] + } + }, + "text/plain": { + "schema": { + "type": "string", + "format": "according to the format requested." + } + }, + "text/html": { + "schema": { + "type": "string", + "format": "html" + } + }, + "application/x-javascript": { + "schema": { + "type": "string", + "format": "javascript" + } + } + } + }, + "400": { + "description": "Bad request - the body will include a message stating what is wrong." + }, + "404": { + "description": "Chart or context is not found. The supplied chart or context will be reported." + }, + "500": { + "description": "Internal server error. This usually means the server is out of memory." + } + } + } + }, + "/api/v1/allmetrics": { + "get": { + "operationId": "allMetrics1", + "tags": [ + "data" + ], + "summary": "All Metrics v1 - Fetch latest value for all metrics", + "description": "The `allmetrics` endpoint returns the latest value of all metrics maintained for a netdata node.\n", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "The format of the response to be returned.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "shell", + "prometheus", + "prometheus_all_hosts", + "json" + ], + "default": "shell" + } + }, + { + "name": "filter", + "in": "query", + "description": "Allows to filter charts out using simple patterns.", + "required": false, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "variables", + "in": "query", + "description": "When enabled, netdata will expose various system configuration variables.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "no" + } + }, + { + "name": "help", + "in": "query", + "description": "Enable or disable HELP lines in prometheus output.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "no" + } + }, + { + "name": "types", + "in": "query", + "description": "Enable or disable TYPE lines in prometheus output.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "no" + } + }, + { + "name": "timestamps", + "in": "query", + "description": "Enable or disable timestamps in prometheus output.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "yes" + } + }, + { + "name": "names", + "in": "query", + "description": "When enabled netdata will report dimension names. When disabled netdata will report dimension IDs. The default is controlled in netdata.conf.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "yes" + } + }, + { + "name": "oldunits", + "in": "query", + "description": "When enabled, netdata will show metric names for the default `source=average` as they appeared before 1.12, by using the legacy unit naming conventions.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "yes" + } + }, + { + "name": "hideunits", + "in": "query", + "description": "When enabled, netdata will not include the units in the metric names, for the default `source=average`.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "yes", + "no" + ], + "default": "yes" + } + }, + { + "name": "server", + "in": "query", + "description": "Set a distinct name of the client querying prometheus metrics. Netdata will use the client IP if this is not set.\n", + "required": false, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "prefix", + "in": "query", + "description": "Prefix all prometheus metrics with this string.\n", + "required": false, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "data", + "in": "query", + "description": "Select the prometheus response data source. There is a setting in netdata.conf for the default.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "as-collected", + "average", + "sum" + ], + "default": "average" + } + } + ], + "responses": { + "200": { + "description": "All the metrics returned in the format requested." + }, + "400": { + "description": "The format requested is not supported." + } + } + } + }, + "/api/v1/badge.svg": { + "get": { + "operationId": "badge1", + "tags": [ + "badges" + ], + "summary": "Generate a badge in form of SVG image for a chart (or dimension)", + "description": "Successful responses are SVG images.", + "parameters": [ + { + "$ref": "#/components/parameters/chart" + }, + { + "$ref": "#/components/parameters/dimension" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/dataTimeGroup1" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "name": "alarm", + "in": "query", + "description": "The name of an alarm linked to the chart.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "label", + "in": "query", + "description": "A text to be used as the label.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "units", + "in": "query", + "description": "A text to be used as the units.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "label_color", + "in": "query", + "description": "A color to be used for the background of the label side(left side) of the badge. One of predefined colors or specific color in hex `RGB` or `RRGGBB` format (without preceding `#` character). If value wrong or not given default color will be used.\n", + "required": false, + "allowEmptyValue": true, + "schema": { + "oneOf": [ + { + "type": "string", + "enum": [ + "green", + "brightgreen", + "yellow", + "yellowgreen", + "orange", + "red", + "blue", + "grey", + "gray", + "lightgrey", + "lightgray" + ] + }, + { + "type": "string", + "format": "^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + } + ] + } + }, + { + "name": "value_color", + "in": "query", + "description": "A color to be used for the background of the value *(right)* part of badge. You can set multiple using a pipe with a condition each, like this: `color<value|color:null` The following operators are supported: >, <, >=, <=, =, :null (to check if no value exists). Each color can be specified in same manner as for `label_color` parameter. Currently only integers are supported as values.\n", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string", + "format": "any text" + } + }, + { + "name": "text_color_lbl", + "in": "query", + "description": "Font color for label *(left)* part of the badge. One of predefined colors or as HTML hexadecimal color without preceding `#` character. Formats allowed `RGB` or `RRGGBB`. If no or wrong value given default color will be used.\n", + "required": false, + "allowEmptyValue": true, + "schema": { + "oneOf": [ + { + "type": "string", + "enum": [ + "green", + "brightgreen", + "yellow", + "yellowgreen", + "orange", + "red", + "blue", + "grey", + "gray", + "lightgrey", + "lightgray" + ] + }, + { + "type": "string", + "format": "^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + } + ] + } + }, + { + "name": "text_color_val", + "in": "query", + "description": "Font color for value *(right)* part of the badge. One of predefined colors or as HTML hexadecimal color without preceding `#` character. Formats allowed `RGB` or `RRGGBB`. If no or wrong value given default color will be used.\n", + "required": false, + "allowEmptyValue": true, + "schema": { + "oneOf": [ + { + "type": "string", + "enum": [ + "green", + "brightgreen", + "yellow", + "yellowgreen", + "orange", + "red", + "blue", + "grey", + "gray", + "lightgrey", + "lightgray" + ] + }, + { + "type": "string", + "format": "^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + } + ] + } + }, + { + "name": "multiply", + "in": "query", + "description": "Multiply the value with this number for rendering it at the image (integer value required).", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "number", + "format": "integer" + } + }, + { + "name": "divide", + "in": "query", + "description": "Divide the value with this number for rendering it at the image (integer value required).", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "number", + "format": "integer" + } + }, + { + "name": "scale", + "in": "query", + "description": "Set the scale of the badge (greater or equal to 100).", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "number", + "format": "integer" + } + }, + { + "name": "fixed_width_lbl", + "in": "query", + "description": "This parameter overrides auto-sizing of badge and creates it with fixed width. This parameter determines the size of the label's left side *(label/name)*. You must set this parameter together with `fixed_width_val` otherwise it will be ignored. You should set the label/value widths wide enough to provide space for all the possible values/contents of the badge you're requesting. In case the text cannot fit the space given it will be clipped. The `scale` parameter still applies on the values you give to `fixed_width_lbl` and `fixed_width_val`.\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "number", + "format": "integer" + } + }, + { + "name": "fixed_width_val", + "in": "query", + "description": "This parameter overrides auto-sizing of badge and creates it with fixed width. This parameter determines the size of the label's right side *(value)*. You must set this parameter together with `fixed_width_lbl` otherwise it will be ignored. You should set the label/value widths wide enough to provide space for all the possible values/contents of the badge you're requesting. In case the text cannot fit the space given it will be clipped. The `scale` parameter still applies on the values you give to `fixed_width_lbl` and `fixed_width_val`.\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "number", + "format": "integer" + } + } + ], + "responses": { + "200": { + "description": "The call was successful. The response should be an SVG image." + }, + "400": { + "description": "Bad request - the body will include a message stating what is wrong." + }, + "404": { + "description": "No chart with the given id is found." + }, + "500": { + "description": "Internal server error. This usually means the server is out of memory." + } + } + } + }, + "/api/v2/weights": { + "get": { + "operationId": "weights2", + "tags": [ + "weights" + ], + "summary": "Score or weight all or some of the metrics, across all nodes, according to various algorithms.", + "description": "This endpoint goes through all metrics and scores them according to an algorithm.\n", + "parameters": [ + { + "$ref": "#/components/parameters/weightMethods" + }, + { + "$ref": "#/components/parameters/scopeNodes" + }, + { + "$ref": "#/components/parameters/scopeContexts" + }, + { + "$ref": "#/components/parameters/filterNodes" + }, + { + "$ref": "#/components/parameters/filterContexts" + }, + { + "$ref": "#/components/parameters/filterInstances" + }, + { + "$ref": "#/components/parameters/filterLabels" + }, + { + "$ref": "#/components/parameters/filterAlerts" + }, + { + "$ref": "#/components/parameters/filterDimensions" + }, + { + "$ref": "#/components/parameters/baselineAfter" + }, + { + "$ref": "#/components/parameters/baselineBefore" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/tier" + }, + { + "$ref": "#/components/parameters/points" + }, + { + "$ref": "#/components/parameters/timeoutMS" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "$ref": "#/components/parameters/dataTimeGroup2" + }, + { + "$ref": "#/components/parameters/dataTimeGroupOptions2" + } + ], + "responses": { + "200": { + "description": "JSON object with weights for each context, chart and dimension.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/weights2" + } + } + } + }, + "400": { + "description": "The given parameters are invalid." + }, + "403": { + "description": "metrics correlations are not enabled on this Netdata Agent." + }, + "404": { + "description": "No charts could be found, or the method that correlated the metrics did not produce any result.\n" + }, + "504": { + "description": "Timeout - the query took too long and has been cancelled." + } + } + } + }, + "/api/v1/weights": { + "get": { + "operationId": "weights1", + "tags": [ + "weights" + ], + "summary": "Score or weight all or some of the metrics of a single node, according to various algorithms.", + "description": "This endpoint goes through all metrics and scores them according to an algorithm.\n", + "parameters": [ + { + "$ref": "#/components/parameters/weightMethods" + }, + { + "$ref": "#/components/parameters/context" + }, + { + "$ref": "#/components/parameters/baselineAfter" + }, + { + "$ref": "#/components/parameters/baselineBefore" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/tier" + }, + { + "$ref": "#/components/parameters/points" + }, + { + "$ref": "#/components/parameters/timeoutMS" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "$ref": "#/components/parameters/dataTimeGroup1" + }, + { + "$ref": "#/components/parameters/dataTimeGroupOptions1" + } + ], + "responses": { + "200": { + "description": "JSON object with weights for each context, chart and dimension.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/weights" + } + } + } + }, + "400": { + "description": "The given parameters are invalid." + }, + "403": { + "description": "metrics correlations are not enabled on this Netdata Agent." + }, + "404": { + "description": "No charts could be found, or the method that correlated the metrics did not produce any result." + }, + "504": { + "description": "Timeout - the query took too long and has been cancelled." + } + } + } + }, + "/api/v1/metric_correlations": { + "get": { + "operationId": "metricCorrelations1", + "tags": [ + "weights" + ], + "summary": "Analyze all the metrics to find their correlations - EOL", + "description": "THIS ENDPOINT IS OBSOLETE. Use the /weights endpoint. Given two time-windows (baseline, highlight), it goes through all the available metrics, querying both windows and tries to find how these two windows relate to each other. It supports multiple algorithms to do so. The result is a list of all metrics evaluated, weighted for 0.0 (the two windows are more different) to 1.0 (the two windows are similar). The algorithm adjusts automatically the baseline window to be a power of two multiple of the highlighted (1, 2, 4, 8, etc).\n", + "parameters": [ + { + "$ref": "#/components/parameters/weightMethods" + }, + { + "$ref": "#/components/parameters/baselineAfter" + }, + { + "$ref": "#/components/parameters/baselineBefore" + }, + { + "$ref": "#/components/parameters/after" + }, + { + "$ref": "#/components/parameters/before" + }, + { + "$ref": "#/components/parameters/points" + }, + { + "$ref": "#/components/parameters/tier" + }, + { + "$ref": "#/components/parameters/timeoutMS" + }, + { + "$ref": "#/components/parameters/dataQueryOptions" + }, + { + "$ref": "#/components/parameters/dataTimeGroup1" + }, + { + "$ref": "#/components/parameters/dataTimeGroupOptions1" + } + ], + "responses": { + "200": { + "description": "JSON object with weights for each chart and dimension.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/metric_correlations" + } + } + } + }, + "400": { + "description": "The given parameters are invalid." + }, + "403": { + "description": "metrics correlations are not enabled on this Netdata Agent." + }, + "404": { + "description": "No charts could be found, or the method that correlated the metrics did not produce any result." + }, + "504": { + "description": "Timeout - the query took too long and has been cancelled." + } + } + } + }, + "/api/v1/function": { + "get": { + "operationId": "function1", + "tags": [ + "functions" + ], + "description": "Execute a collector function.", + "parameters": [ + { + "name": "function", + "in": "query", + "description": "The name of the function, as returned by the collector.", + "required": true, + "allowEmptyValue": false, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/timeoutSecs" + } + ], + "responses": { + "200": { + "description": "The collector function has been executed successfully. Each collector may return a different type of content." + }, + "400": { + "description": "The request was rejected by the collector." + }, + "404": { + "description": "The requested function is not found." + }, + "500": { + "description": "Other internal error, getting this error means there is a bug in Netdata." + }, + "503": { + "description": "The collector to execute the function is not currently available." + }, + "504": { + "description": "Timeout while waiting for the collector to execute the function." + }, + "591": { + "description": "The collector sent a response, but it was invalid or corrupted." + } + } + } + }, + "/api/v1/functions": { + "get": { + "operationId": "functions1", + "tags": [ + "functions" + ], + "summary": "Get a list of all registered collector functions.", + "description": "Collector functions are programs that can be executed on demand.", + "responses": { + "200": { + "description": "A JSON object containing one object per supported function." + } + } + } + }, + "/api/v1/alarms": { + "get": { + "operationId": "alerts1", + "tags": [ + "alerts" + ], + "summary": "Get a list of active or raised alarms on the server", + "description": "The alarms endpoint returns the list of all raised or enabled alarms on the netdata server. Called without any parameters, the raised alarms in state WARNING or CRITICAL are returned. By passing \"?all\", all the enabled alarms are returned.\n", + "parameters": [ + { + "name": "all", + "in": "query", + "description": "If passed, all enabled alarms are returned.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "active", + "in": "query", + "description": "If passed, the raised alarms in state WARNING or CRITICAL are returned.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "An object containing general info and a linked list of alarms.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alarms" + } + } + } + } + } + } + }, + "/api/v1/alarms_values": { + "get": { + "operationId": "alertValues1", + "tags": [ + "alerts" + ], + "summary": "Get a list of active or raised alarms on the server", + "description": "The alarms_values endpoint returns the list of all raised or enabled alarms on the netdata server. Called without any parameters, the raised alarms in state WARNING or CRITICAL are returned. By passing '?all', all the enabled alarms are returned. This option output differs from `/alarms` in the number of variables delivered. This endpoint gives to user `id`, `value`, `last_updated` time, and alarm `status`.\n", + "parameters": [ + { + "name": "all", + "in": "query", + "description": "If passed, all enabled alarms are returned.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "active", + "in": "query", + "description": "If passed, the raised alarms in state WARNING or CRITICAL are returned.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "An object containing general info and a linked list of alarms.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alarms_values" + } + } + } + } + } + } + }, + "/api/v1/alarm_log": { + "get": { + "operationId": "alertsLog1", + "tags": [ + "alerts" + ], + "summary": "Retrieves the entries of the alarm log", + "description": "Returns an array of alarm_log entries, with historical information on raised and cleared alarms.\n", + "parameters": [ + { + "name": "after", + "in": "query", + "description": "Passing the parameter after=UNIQUEID returns all the events in the alarm log that occurred after UNIQUEID. An automated series of calls would call the interface once without after=, store the last UNIQUEID of the returned set, and give it back to get incrementally the next events.\n", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of alarm log entries.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/alarm_log_entry" + } + } + } + } + } + } + } + }, + "/api/v1/alarm_count": { + "get": { + "operationId": "alertsCount1", + "tags": [ + "alerts" + ], + "summary": "Get an overall status of the chart", + "description": "Checks multiple charts with the same context and counts number of alarms with given status.\n", + "parameters": [ + { + "$ref": "#/components/parameters/context" + }, + { + "name": "status", + "in": "query", + "description": "Specify alarm status to count.", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string", + "enum": [ + "REMOVED", + "UNDEFINED", + "UNINITIALIZED", + "CLEAR", + "RAISED", + "WARNING", + "CRITICAL" + ], + "default": "RAISED" + } + } + ], + "responses": { + "200": { + "description": "An object containing a count of alarms with given status for given contexts.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "number" + } + } + } + } + }, + "500": { + "description": "Internal server error. This usually means the server is out of memory." + } + } + } + }, + "/api/v1/alarm_variables": { + "get": { + "operationId": "getNodeAlertVariables1", + "tags": [ + "alerts" + ], + "summary": "List variables available to configure alarms for a chart", + "description": "Returns the basic information of a chart and all the variables that can be used in alarm and template health configurations for the particular chart or family.\n", + "parameters": [ + { + "name": "chart", + "in": "query", + "description": "The id of the chart as returned by the /charts call.", + "required": true, + "schema": { + "type": "string", + "format": "as returned by /charts", + "default": "system.cpu" + } + } + ], + "responses": { + "200": { + "description": "A javascript object with information about the chart and the available variables.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alarm_variables" + } + } + } + }, + "400": { + "description": "Bad request - the body will include a message stating what is wrong." + }, + "404": { + "description": "No chart with the given id is found." + }, + "500": { + "description": "Internal server error. This usually means the server is out of memory." + } + } + } + }, + "/api/v1/manage/health": { + "get": { + "operationId": "health1", + "tags": [ + "management" + ], + "summary": "Accesses the health management API to control health checks and notifications at runtime.\n", + "description": "Available from Netdata v1.12 and above, protected via bearer authorization. Especially useful for maintenance periods, the API allows you to disable health checks completely, silence alarm notifications, or Disable/Silence specific alarms that match selectors on alarm/template name, chart, context, host and family. For the simple disable/silence all scenarios, only the cmd parameter is required. The other parameters are used to define alarm selectors. For more information and examples, refer to the netdata documentation.\n", + "parameters": [ + { + "name": "cmd", + "in": "query", + "description": "DISABLE ALL: No alarm criteria are evaluated, nothing is written in the alarm log. SILENCE ALL: No notifications are sent. RESET: Return to the default state. DISABLE/SILENCE: Set the mode to be used for the alarms matching the criteria of the alarm selectors. LIST: Show active configuration.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "DISABLE ALL", + "SILENCE ALL", + "DISABLE", + "SILENCE", + "RESET", + "LIST" + ] + } + }, + { + "name": "alarm", + "in": "query", + "description": "The expression provided will match both `alarm` and `template` names.", + "schema": { + "type": "string" + } + }, + { + "name": "chart", + "in": "query", + "description": "Chart ids/names, as shown on the dashboard. These will match the `on` entry of a configured `alarm`.", + "schema": { + "type": "string" + } + }, + { + "name": "context", + "in": "query", + "description": "Chart context, as shown on the dashboard. These will match the `on` entry of a configured `template`.", + "schema": { + "type": "string" + } + }, + { + "name": "hosts", + "in": "query", + "description": "The hostnames that will need to match.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A plain text response based on the result of the command." + }, + "403": { + "description": "Bearer authentication error." + } + } + } + }, + "/api/v1/aclk": { + "get": { + "operationId": "aclk1", + "tags": [ + "management" + ], + "summary": "Get information about current ACLK state", + "description": "ACLK endpoint returns detailed information about current state of ACLK (Agent to Cloud communication).\n", + "responses": { + "200": { + "description": "JSON object with ACLK information.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/aclk_state" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "scopeNodes": { + "name": "scope_nodes", + "in": "query", + "description": "A simple pattern limiting the nodes scope of the query. The scope controls both data and metadata response. The simple pattern is checked against the nodes' machine guid, node id and hostname. The default nodes scope is all nodes for which this agent has data for. Usually the nodes scope is used to slice the entire dashboard (e.g. the Global Nodes Selector at the Netdata Cloud overview dashboard). Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "scopeContexts": { + "name": "scope_contexts", + "in": "query", + "description": "A simple pattern limiting the contexts scope of the query. The scope controls both data and metadata response. The default contexts scope is all contexts for which this agent has data for. Usually the contexts scope is used to slice data on the dashboard (e.g. each context based chart has its own contexts scope, limiting the chart to all the instances of the selected context). Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterNodes": { + "name": "nodes", + "in": "query", + "description": "A simple pattern matching the nodes to be queried. This only controls the data response, not the metadata. The simple pattern is checked against the nodes' machine guid, node id, hostname. The default nodes selector is all the nodes matched by the nodes scope. Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterContexts": { + "name": "contexts", + "in": "query", + "description": "A simple pattern matching the contexts to be queried. This only controls the data response, not the metadata. Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterInstances": { + "name": "instances", + "in": "query", + "description": "A simple pattern matching the instances to be queried. The simple pattern is checked against the instance `id`, the instance `name`, the fully qualified name of the instance `id` and `name`, like `instance@machine_guid`, where `instance` is either its `id` or `name`. Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterLabels": { + "name": "labels", + "in": "query", + "description": "A simple pattern matching the labels to be queried. The simple pattern is checked against `name:value` of all the labels of all the eligible instances (as filtered by all the above: scope nodes, scope contexts, nodes, contexts and instances). Negative simple patterns should not be used in this filter.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterAlerts": { + "name": "alerts", + "in": "query", + "description": "A simple pattern matching the alerts to be queried. The simple pattern is checked against the `name` of alerts and the combination of `name:status`, when status is one of `CLEAR`, `WARNING`, `CRITICAL`, `REMOVED`, `UNDEFINED`, `UNINITIALIZED`, of all the alerts of all the eligible instances (as filtered by all the above). A negative simple pattern will exclude the instances having the labels matched.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "filterDimensions": { + "name": "dimensions", + "in": "query", + "description": "A simple patterns matching the dimensions to be queried. The simple pattern is checked against and `id` and the `name` of the dimensions of the eligible instances (as filtered by all the above). Both positive and negative simple pattern expressions are supported.\n", + "required": false, + "schema": { + "type": "string", + "format": "simple pattern", + "default": "*" + } + }, + "dataFormat1": { + "name": "format", + "in": "query", + "description": "The format of the data to be returned.", + "allowEmptyValue": false, + "schema": { + "type": "string", + "enum": [ + "json", + "jsonp", + "csv", + "tsv", + "tsv-excel", + "ssv", + "ssvcomma", + "datatable", + "datasource", + "html", + "markdown", + "array", + "csvjsonarray" + ], + "default": "json" + } + }, + "dataFormat2": { + "name": "format", + "in": "query", + "description": "The format of the data to be returned.", + "allowEmptyValue": false, + "schema": { + "type": "string", + "enum": [ + "json", + "json2", + "jsonp", + "csv", + "tsv", + "tsv-excel", + "ssv", + "ssvcomma", + "datatable", + "datasource", + "html", + "markdown", + "array", + "csvjsonarray" + ], + "default": "json2" + } + }, + "dataQueryOptions": { + "name": "options", + "in": "query", + "description": "Options that affect data generation.\n* `jsonwrap` - Wrap the output in a JSON object with metadata about the query.\n* `raw` - change the output so that it is aggregatable across multiple such queries. Supported by `/api/v2` data queries and `json2` format.\n* `minify` - Remove unnecessary spaces and newlines from the output.\n* `debug` - Provide additional information in `jsonwrap` output to help tracing issues.\n* `nonzero` - Do not return dimensions that all their values are zero, to improve the visual appearance of charts. They will still be returned if all the dimensions are entirely zero.\n* `null2zero` - Replace `null` values with `0`.\n* `absolute` or `abs` - Traditionally Netdata returns select dimensions negative to improve visual appearance. This option turns this feature off.\n* `display-absolute` - Only used by badges, to do color calculation using the signed value, but render the value without a sign.\n* `flip` or `reversed` - Order the timestamps array in reverse order (newest to oldest).\n* `min2max` - When flattening multi-dimensional data into a single metric format, use `max - min` instead of `sum`. This is EOL - use `/api/v2` to control aggregation across dimensions.\n* `percentage` - Convert all values into a percentage vs the row total. When enabled, Netdata will query all dimensions, even the ones that have not been selected or are hidden, to find the row total, in order to calculate the percentage of each dimension selected.\n* `seconds` - Output timestamps in seconds instead of dates.\n* `milliseconds` or `ms` - Output timestamps in milliseconds instead of dates.\n* `unaligned` - by default queries are aligned to the the view, so that as time passes past data returned do not change. When a data query will not be used for visualization, `unaligned` can be given to avoid aligning the query time-frame for visual precision.\n* `match-ids`, `match-names`. By default filters match both IDs and names when they are available. Setting either of the two options will disable the other.\n* `anomaly-bit` - query the anomaly information instead of metric values. This is EOL, use `/api/v2` and `json2` format which always returns this information and many more.\n* `jw-anomaly-rates` - return anomaly rates as a separate result set in the same `json` format response. This is EOL, use `/api/v2` and `json2` format which always returns information and many more. \n* `details` - `/api/v2/data` returns in `jsonwrap` the full tree of dimensions that have been matched by the query.\n* `group-by-labels` - `/api/v2/data` returns in `jsonwrap` flattened labels per output dimension. These are used to identify the instances that have been aggregated into each dimension, making it possible to provide a map, like Netdata does for Kubernetes.\n* `natural-points` - return timestamps as found in the database. The result is again fixed-step, but the query engine attempts to align them with the timestamps found in the database.\n* `virtual-points` - return timestamps independent of the database alignment. This is needed aggregating data across multiple Netdata agents, to ensure that their outputs do not need to be interpolated to be merged.\n* `selected-tier` - use data exclusively from the selected tier given with the `tier` parameter. This option is set automatically when the `tier` parameter is set.\n* `all-dimensions` - In `/api/v1` `jsonwrap` include metadata for all candidate metrics examined. In `/api/v2` this is standard behavior and no option is needed.\n* `label-quotes` - In `csv` output format, enclose each header label in quotes.\n* `objectrows` - Each row of value should be an object, not an array (only for `json` format).\n* `google_json` - Comply with google JSON/JSONP specs (only for `json` format).\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jsonwrap", + "raw", + "minify", + "debug", + "nonzero", + "null2zero", + "abs", + "absolute", + "display-absolute", + "flip", + "reversed", + "min2max", + "percentage", + "seconds", + "ms", + "milliseconds", + "unaligned", + "match-ids", + "match-names", + "anomaly-bit", + "jw-anomaly-rates", + "details", + "group-by-labels", + "natural-points", + "virtual-points", + "selected-tier", + "all-dimensions", + "label-quotes", + "objectrows", + "google_json" + ] + }, + "default": [ + "seconds", + "jsonwrap" + ] + } + }, + "dataTimeGroup1": { + "name": "group", + "in": "query", + "description": "Time aggregation function. If multiple collected values are to be grouped in order to return fewer points, this parameters defines the method of grouping. If the `absolute` option is set, the values are turned positive before applying this calculation.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "min", + "max", + "avg", + "average", + "median", + "stddev", + "sum", + "incremental-sum", + "ses", + "des", + "cv", + "countif", + "percentile", + "percentile25", + "percentile50", + "percentile75", + "percentile80", + "percentile90", + "percentile95", + "percentile97", + "percentile98", + "percentile99", + "trimmed-mean", + "trimmed-mean1", + "trimmed-mean2", + "trimmed-mean3", + "trimmed-mean5", + "trimmed-mean10", + "trimmed-mean15", + "trimmed-mean20", + "trimmed-mean25", + "trimmed-median", + "trimmed-median1", + "trimmed-median2", + "trimmed-median3", + "trimmed-median5", + "trimmed-median10", + "trimmed-median15", + "trimmed-median20", + "trimmed-median25" + ], + "default": "average" + } + }, + "dataTimeGroup2": { + "name": "time_group", + "in": "query", + "description": "Time aggregation function. If multiple collected values are to be grouped in order to return fewer points, this parameters defines the method of grouping. If the `absolute` option is set, the values are turned positive before applying this calculation.\n", + "required": false, + "schema": { + "type": "string", + "enum": [ + "min", + "max", + "avg", + "average", + "median", + "stddev", + "sum", + "incremental-sum", + "ses", + "des", + "cv", + "countif", + "percentile", + "percentile25", + "percentile50", + "percentile75", + "percentile80", + "percentile90", + "percentile95", + "percentile97", + "percentile98", + "percentile99", + "trimmed-mean", + "trimmed-mean1", + "trimmed-mean2", + "trimmed-mean3", + "trimmed-mean5", + "trimmed-mean10", + "trimmed-mean15", + "trimmed-mean20", + "trimmed-mean25", + "trimmed-median", + "trimmed-median1", + "trimmed-median2", + "trimmed-median3", + "trimmed-median5", + "trimmed-median10", + "trimmed-median15", + "trimmed-median20", + "trimmed-median25" + ], + "default": "average" + } + }, + "dataTimeGroupOptions1": { + "name": "group_options", + "in": "query", + "description": "When the time grouping function supports additional parameters, this field can be used to pass them to it. Currently `countif`, `trimmed-mean`, `trimmed-median` and `percentile` support this. For `countif` the string may start with `<`, `<=`, `<:`, `<>`, `!=`, `>`, `>=`, `>:`. For all others just a number is expected.\n", + "required": false, + "schema": { + "type": "string" + } + }, + "dataTimeGroupOptions2": { + "name": "time_group_options", + "in": "query", + "description": "When the time grouping function supports additional parameters, this field can be used to pass them to it. Currently `countif`, `trimmed-mean`, `trimmed-median` and `percentile` support this. For `countif` the string may start with `<`, `<=`, `<:`, `<>`, `!=`, `>`, `>=`, `>:`. For all others just a number is expected.\n", + "required": false, + "schema": { + "type": "string" + } + }, + "dataTimeResampling1": { + "name": "gtime", + "in": "query", + "description": "The grouping number of seconds. This is used in conjunction with group=average to change the units of metrics (ie when the data is per-second, setting gtime=60 will turn them to per-minute).\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "number", + "format": "integer", + "default": 0 + } + }, + "dataTimeResampling2": { + "name": "time_resampling", + "in": "query", + "description": "For incremental values that are \"per second\", this value is used to resample them to \"per minute` (60) or \"per hour\" (3600). It can only be used in conjunction with group=average.\n", + "required": false, + "schema": { + "type": "number", + "format": "integer", + "default": 0 + } + }, + "timeoutMS": { + "name": "timeout", + "in": "query", + "description": "Specify a timeout value in milliseconds after which the agent will abort the query and return a 503 error. A value of 0 indicates no timeout.\n", + "required": false, + "schema": { + "type": "number", + "format": "integer", + "default": 0 + } + }, + "timeoutSecs": { + "name": "timeout", + "in": "query", + "description": "Specify a timeout value in seconds after which the agent will abort the query and return a 504 error. A value of 0 indicates no timeout, but some endpoints, like `weights`, do not accept infinite timeouts (they have a predefined default), so to disable the timeout it must be set to a really high value.\n", + "required": false, + "schema": { + "type": "number", + "format": "integer", + "default": 0 + } + }, + "before": { + "name": "before", + "in": "query", + "description": "`after` and `before` define the time-frame of a query. `before` can be a negative number of seconds, up to 3 years (-94608000), relative to current clock. If not set, it is assumed to be the current clock time. When `before` is positive, it is assumed to be a unix epoch timestamp. When non-data endpoints support the `after` and `before`, they use the time-frame to limit their response for objects having data retention within the time-frame given.\n", + "required": false, + "schema": { + "type": "integer", + "default": 0 + } + }, + "after": { + "name": "after", + "in": "query", + "description": "`after` and `before` define the time-frame of a query. `after` can be a negative number of seconds, up to 3 years (-94608000), relative to `before`. If not set, it is usually assumed to be -600. When non-data endpoints support the `after` and `before`, they use the time-frame to limit their response for objects having data retention within the time-frame given.\n", + "required": false, + "schema": { + "type": "integer", + "default": -600 + } + }, + "baselineBefore": { + "name": "baseline_before", + "in": "query", + "description": "`baseline_after` and `baseline_before` define the baseline time-frame of a comparative query. `baseline_before` can be a negative number of seconds, up to 3 years (-94608000), relative to current clock. If not set, it is assumed to be the current clock time. When `baseline_before` is positive, it is assumed to be a unix epoch timestamp.\n", + "required": false, + "schema": { + "type": "integer", + "default": 0 + } + }, + "baselineAfter": { + "name": "baseline_after", + "in": "query", + "description": "`baseline_after` and `baseline_before` define the baseline time-frame of a comparative query. `baseline_after` can be a negative number of seconds, up to 3 years (-94608000), relative to `baseline_before`. If not set, it is usually assumed to be -300.\n", + "required": false, + "schema": { + "type": "integer", + "default": -600 + } + }, + "points": { + "name": "points", + "in": "query", + "description": "The number of points to be returned. If not given, or it is <= 0, or it is bigger than the points stored in the database for the given duration, all the available collected values for the given duration will be returned. For `weights` endpoints that do statistical analysis, the `points` define the detail of this analysis (the default is 500).\n", + "required": false, + "schema": { + "type": "number", + "format": "integer", + "default": 0 + } + }, + "tier": { + "name": "tier", + "in": "query", + "description": "Use only the given dbengine tier for executing the query. Setting this parameters automatically sets the option `selected-tier` for the query.\n", + "required": false, + "schema": { + "type": "number", + "format": "integer" + } + }, + "callback": { + "name": "callback", + "in": "query", + "description": "For JSONP responses, the callback function name.\n", + "required": false, + "schema": { + "type": "string" + } + }, + "filename": { + "name": "filename", + "in": "query", + "description": "Add `Content-Disposition: attachment; filename=` header to the response, that will instruct the browser to save the response with the given filename.\"\n", + "required": false, + "schema": { + "type": "string" + } + }, + "tqx": { + "name": "tqx", + "in": "query", + "description": "[Google Visualization API](https://developers.google.com/chart/interactive/docs/dev/implementing_data_source?hl=en) formatted parameter.\n", + "required": false, + "schema": { + "type": "string" + } + }, + "contextOptions1": { + "name": "options", + "in": "query", + "description": "Options that affect data generation.", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "full", + "all", + "charts", + "dimensions", + "labels", + "uuids", + "queue", + "flags", + "deleted", + "deepscan" + ] + } + } + }, + "chart": { + "name": "chart", + "in": "query", + "description": "The id of the chart as returned by the `/api/v1/charts` call.", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "string", + "format": "as returned by `/api/v1/charts`" + } + }, + "context": { + "name": "context", + "in": "query", + "description": "The context of the chart as returned by the /charts call.", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "string", + "format": "as returned by /charts" + } + }, + "dimension": { + "name": "dimension", + "in": "query", + "description": "Zero, one or more dimension ids or names, as returned by the /chart call, separated with comma or pipe. Netdata simple patterns are supported.", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "as returned by /charts" + } + } + }, + "dimensions": { + "name": "dimensions", + "in": "query", + "description": "a simple pattern matching dimensions (use comma or pipe as separator)", + "required": false, + "allowEmptyValue": true, + "schema": { + "type": "string" + } + }, + "chart_label_key": { + "name": "chart_label_key", + "in": "query", + "description": "Specify the chart label keys that need to match for context queries as comma separated values. At least one matching key is needed to match the corresponding chart.\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "string", + "format": "key1,key2,key3" + } + }, + "chart_labels_filter": { + "name": "chart_labels_filter", + "in": "query", + "description": "Specify the chart label keys and values to match for context queries. All keys/values need to match for the chart to be included in the query. The labels are specified as key1:value1,key2:value2\n", + "required": false, + "allowEmptyValue": false, + "schema": { + "type": "string", + "format": "key1:value1,key2:value2,key3:value3" + } + }, + "weightMethods": { + "name": "method", + "in": "query", + "description": "The weighting / scoring algorithm.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "ks2", + "volume", + "anomaly-rate", + "value" + ] + } + } + }, + "schemas": { + "info": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "netdata version of the server.", + "example": "1.11.1_rolling" + }, + "uid": { + "type": "string", + "description": "netdata unique id of the server.", + "example": "24e9fe3c-f2ac-11e8-bafc-0242ac110002" + }, + "mirrored_hosts": { + "type": "array", + "description": "List of hosts mirrored of the server (include itself).", + "items": { + "type": "string" + }, + "example": [ + "host1.example.com", + "host2.example.com" + ] + }, + "mirrored_hosts_status": { + "type": "array", + "description": "List of details of hosts mirrored to this served (including self). Indexes correspond to indexes in \"mirrored_hosts\".", + "items": { + "type": "object", + "description": "Host data", + "properties": { + "guid": { + "type": "string", + "format": "uuid", + "nullable": false, + "description": "Host unique GUID from `netdata.public.unique.id`.", + "example": "245e4bff-3b34-47c1-a6e5-5c535a9abfb2" + }, + "reachable": { + "type": "boolean", + "nullable": false, + "description": "Current state of streaming. Always true for localhost/self." + }, + "claim_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Cloud GUID/identifier in case the host is claimed. If child status unknown or unclaimed this field is set to `null`", + "example": "c3b2a66a-3052-498c-ac52-7fe9e8cccb0c" + } + } + } + }, + "os_name": { + "type": "string", + "description": "Operating System Name.", + "example": "Manjaro Linux" + }, + "os_id": { + "type": "string", + "description": "Operating System ID.", + "example": "manjaro" + }, + "os_id_like": { + "type": "string", + "description": "Known OS similar to this OS.", + "example": "arch" + }, + "os_version": { + "type": "string", + "description": "Operating System Version.", + "example": "18.0.4" + }, + "os_version_id": { + "type": "string", + "description": "Operating System Version ID.", + "example": "unknown" + }, + "os_detection": { + "type": "string", + "description": "OS parameters detection method.", + "example": "Mixed" + }, + "kernel_name": { + "type": "string", + "description": "Kernel Name.", + "example": "Linux" + }, + "kernel_version": { + "type": "string", + "description": "Kernel Version.", + "example": "4.19.32-1-MANJARO" + }, + "is_k8s_node": { + "type": "boolean", + "description": "Netdata is running on a K8s node.", + "example": false + }, + "architecture": { + "type": "string", + "description": "Kernel architecture.", + "example": "x86_64" + }, + "virtualization": { + "type": "string", + "description": "Virtualization Type.", + "example": "kvm" + }, + "virt_detection": { + "type": "string", + "description": "Virtualization detection method.", + "example": "systemd-detect-virt" + }, + "container": { + "type": "string", + "description": "Container technology.", + "example": "docker" + }, + "container_detection": { + "type": "string", + "description": "Container technology detection method.", + "example": "dockerenv" + }, + "stream_compression": { + "type": "boolean", + "description": "Stream transmission compression method.", + "example": true + }, + "labels": { + "type": "object", + "description": "List of host labels.", + "properties": { + "app": { + "type": "string", + "description": "Host label.", + "example": "netdata" + } + } + }, + "collectors": { + "type": "array", + "items": { + "type": "object", + "description": "Array of collector plugins and modules.", + "properties": { + "plugin": { + "type": "string", + "description": "Collector plugin.", + "example": "python.d.plugin" + }, + "module": { + "type": "string", + "description": "Module of the collector plugin.", + "example": "dockerd" + } + } + } + }, + "alarms": { + "type": "object", + "description": "Number of alarms in the server.", + "properties": { + "normal": { + "type": "integer", + "description": "Number of alarms in normal state." + }, + "warning": { + "type": "integer", + "description": "Number of alarms in warning state." + }, + "critical": { + "type": "integer", + "description": "Number of alarms in critical state." + } + } + } + } + }, + "chart_summary": { + "type": "object", + "properties": { + "hostname": { + "type": "string", + "description": "The hostname of the netdata server." + }, + "version": { + "type": "string", + "description": "netdata version of the server." + }, + "release_channel": { + "type": "string", + "description": "The release channel of the build on the server.", + "example": "nightly" + }, + "timezone": { + "type": "string", + "description": "The current timezone on the server." + }, + "os": { + "type": "string", + "description": "The netdata server host operating system.", + "enum": [ + "macos", + "linux", + "freebsd" + ] + }, + "history": { + "type": "number", + "description": "The duration, in seconds, of the round robin database maintained by netdata." + }, + "memory_mode": { + "type": "string", + "description": "The name of the database memory mode on the server." + }, + "update_every": { + "type": "number", + "description": "The default update frequency of the netdata server. All charts have an update frequency equal or bigger than this." + }, + "charts": { + "type": "object", + "description": "An object containing all the chart objects available at the netdata server. This is used as an indexed array. The key of each chart object is the id of the chart.", + "additionalProperties": { + "$ref": "#/components/schemas/chart" + } + }, + "charts_count": { + "type": "number", + "description": "The number of charts." + }, + "dimensions_count": { + "type": "number", + "description": "The total number of dimensions." + }, + "alarms_count": { + "type": "number", + "description": "The number of alarms." + }, + "rrd_memory_bytes": { + "type": "number", + "description": "The size of the round robin database in bytes." + } + } + }, + "chart": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique id of the chart." + }, + "name": { + "type": "string", + "description": "The name of the chart." + }, + "type": { + "type": "string", + "description": "The type of the chart. Types are not handled by netdata. You can use this field for anything you like." + }, + "family": { + "type": "string", + "description": "The family of the chart. Families are not handled by netdata. You can use this field for anything you like." + }, + "title": { + "type": "string", + "description": "The title of the chart." + }, + "priority": { + "type": "number", + "description": "The relative priority of the chart. Netdata does not care about priorities. This is just an indication of importance for the chart viewers to sort charts of higher priority (lower number) closer to the top. Priority sorting should only be used among charts of the same type or family." + }, + "enabled": { + "type": "boolean", + "description": "True when the chart is enabled. Disabled charts do not currently collect values, but they may have historical values available." + }, + "units": { + "type": "string", + "description": "The unit of measurement for the values of all dimensions of the chart." + }, + "data_url": { + "type": "string", + "description": "The absolute path to get data values for this chart. You are expected to use this path as the base when constructing the URL to fetch data values for this chart." + }, + "chart_type": { + "type": "string", + "description": "The chart type.", + "enum": [ + "line", + "area", + "stacked" + ] + }, + "duration": { + "type": "number", + "description": "The duration, in seconds, of the round robin database maintained by netdata." + }, + "first_entry": { + "type": "number", + "description": "The UNIX timestamp of the first entry (the oldest) in the round robin database." + }, + "last_entry": { + "type": "number", + "description": "The UNIX timestamp of the latest entry in the round robin database." + }, + "update_every": { + "type": "number", + "description": "The update frequency of this chart, in seconds. One value every this amount of time is kept in the round robin database." + }, + "dimensions": { + "type": "object", + "description": "An object containing all the chart dimensions available for the chart. This is used as an indexed array. For each pair in the dictionary: the key is the id of the dimension and the value is a dictionary containing the name.\"\n", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the dimension" + } + } + } + }, + "chart_variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/chart_variables" + } + }, + "green": { + "type": "number", + "nullable": true, + "description": "Chart health green threshold." + }, + "red": { + "type": "number", + "nullable": true, + "description": "Chart health red threshold." + } + } + }, + "context_summary": { + "type": "object", + "properties": { + "hostname": { + "type": "string", + "description": "The hostname of the netdata server." + }, + "machine_guid": { + "type": "string", + "description": "The unique installation id of this netdata server." + }, + "node_id": { + "type": "string", + "description": "The unique node id of this netdata server at the hub.", + "example": "nightly" + }, + "claim_id": { + "type": "string", + "description": "The unique handshake id of this netdata server and the hub." + }, + "host_labels": { + "type": "object", + "description": "The host labels associated with this netdata server." + }, + "context": { + "type": "object", + "description": "An object containing all the context objects available at the netdata server. This is used as an indexed array. The key of each context object is the id of the context.", + "additionalProperties": { + "$ref": "#/components/schemas/context" + } + } + } + }, + "context": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The version of this context. The number are not sequential, but bigger numbers depict a newer object." + }, + "hub_version": { + "type": "string", + "description": "The version of this context, as known by hub." + }, + "family": { + "type": "string", + "description": "The family of the context. When multiple charts of a context have different families, the netdata server replaces the different parts with [x], so that the context can have only one family." + }, + "title": { + "type": "string", + "description": "The title of the context. When multiple charts of a context have different titles, the netdata server replaces the different parts with [x], so that the context can have only one title." + }, + "priority": { + "type": "number", + "description": "The relative priority of the context. When multiple contexts have different priorities, the minimum among them is selected as the priority of the context." + }, + "units": { + "type": "string", + "description": "The unit of measurement for the values of all dimensions of the context. If multiple charts of context have different units, the latest collected is selected." + }, + "chart_type": { + "type": "string", + "description": "The chart type.", + "enum": [ + "line", + "area", + "stacked" + ] + }, + "first_time_t": { + "type": "number", + "description": "The UNIX timestamp of the first entry (the oldest) in the database." + }, + "last_time_t": { + "type": "number", + "description": "The UNIX timestamp of the latest entry in the database." + }, + "charts": { + "type": "object", + "description": "An object containing all the charts available for the chart. This is used as an indexed array. For each pair in the dictionary, the key is the id of the chart and the value provides all details about the chart." + } + } + }, + "alarm_variables": { + "type": "object", + "properties": { + "chart": { + "type": "string", + "description": "The unique id of the chart." + }, + "chart_name": { + "type": "string", + "description": "The name of the chart." + }, + "cnart_context": { + "type": "string", + "description": "The context of the chart. It is shared across multiple monitored software or hardware instances and used in alarm templates." + }, + "family": { + "type": "string", + "description": "The family of the chart." + }, + "host": { + "type": "string", + "description": "The host containing the chart." + }, + "chart_variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/chart_variables" + } + }, + "family_variables": { + "type": "object", + "properties": { + "varname1": { + "type": "number", + "format": "float" + }, + "varname2": { + "type": "number", + "format": "float" + } + } + }, + "host_variables": { + "type": "object", + "properties": { + "varname1": { + "type": "number", + "format": "float" + }, + "varname2": { + "type": "number", + "format": "float" + } + } + } + } + }, + "chart_variables": { + "type": "object", + "properties": { + "varname1": { + "type": "number", + "format": "float" + }, + "varname2": { + "type": "number", + "format": "float" + } + } + }, + "jsonwrap2": { + "description": "Data response with `format=json2`\n", + "type": "object", + "properties": { + "api": { + "$ref": "#/components/schemas/api" + }, + "agents": { + "$ref": "#/components/schemas/agents" + }, + "versions": { + "$ref": "#/components/schemas/versions" + }, + "summary": { + "description": "Summarized information about nodes, contexts, instances, labels, alerts, and dimensions. The items returned are determined by the scope of the query only, however the statistical data in them are influenced by the filters of the query. Using this information the dashboard allows users to slice and dice the data by filtering and grouping.\n", + "type": "object", + "properties": { + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/nodeWithDataStatistics" + } + }, + "contexts": { + "type": "array", + "items": { + "type": "object", + "description": "An object describing a unique context. `is` stands for instances, `ds` for dimensions, `al` for alerts, `sts` for statistics.\n", + "properties": { + "id": { + "description": "the context id.", + "type": "string" + }, + "is": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "al": { + "$ref": "#/components/schemas/jsonwrap2_alerts_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "description": "An object describing an instance. `ds` stands for dimensions, `al` for alerts, `sts` for statistics.\n", + "properties": { + "id": { + "description": "the id of the instance.", + "type": "string" + }, + "nm": { + "description": "the name of the instance (may be absent when it is the same with the id)", + "type": "string" + }, + "ni": { + "description": "the node index id this instance belongs to. The UI uses this to compone the fully qualified name of the instance, using the node hostname to present it to users and its machine guid to add it to filters." + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "al": { + "$ref": "#/components/schemas/jsonwrap2_alerts_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + }, + "dimensions": { + "type": "array", + "items": { + "type": "object", + "description": "An object describing a unique dimension. `ds` stands for `dimensions`, `sts` for statistics.\n", + "properties": { + "id": { + "description": "the id of the dimension.", + "type": "string" + }, + "nm": { + "description": "the name of the dimension (may be absent when it is the same with the id)", + "type": "string" + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "description": "An object describing a label key. `ds` stands for `dimensions`, `sts` for statistics.\n", + "properties": { + "id": { + "description": "the key of the label.", + "type": "string" + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + }, + "vl": { + "description": "An array of values for this key.\n", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "The value string", + "type": "string" + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + } + } + } + }, + "alerts": { + "description": "An array of all the unique alerts running, grouped by alert name (`nm` is available here)\n", + "type": "array", + "items": { + "$ref": "#/components/schemas/jsonwrap2_alerts_count" + } + } + } + }, + "totals": { + "type": "object", + "properties": { + "nodes": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "contexts": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "instances": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "dimensions": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "label_keys": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "label_key_values": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + } + } + }, + "functions": { + "type": "array", + "items": { + "type": "string" + } + }, + "db": { + "type": "object", + "properties": { + "tiers": { + "description": "The number of tiers this server is using.\n", + "type": "integer" + }, + "update_every": { + "description": "The minimum update every, in seconds, for all tiers and all metrics aggregated into this query.\n", + "type": "integer" + }, + "first_entry": { + "description": "The minimum unix epoch timestamp of the retention across all tiers for all metrics aggregated into this query.\n", + "type": "integer" + }, + "last_entry": { + "description": "The maximum unix epoch timestamp of the retention across all tier for all metrics aggregated into this query.\n", + "type": "integer" + }, + "per_tier": { + "description": "An array with information for each of the tiers available, related to this query.\n", + "type": "array", + "items": { + "type": "object", + "properties": { + "tier": { + "description": "The tier number of this tier, starting at 0.\n", + "type": "integer" + }, + "queries": { + "description": "The number of queries executed on this tier. Usually one query per metric is made, but the query may cross multiple tier, in which case more than one query per metric is made.\n", + "type": "integer" + }, + "points": { + "description": "The number of points read from this tier.\n", + "type": "integer" + }, + "update_every": { + "description": "The minimum resolution of all metrics queried on this tier.\n", + "type": "integer" + }, + "first_entry": { + "description": "The minimum unix epoch timestamp available across all metrics that used this tier. This reflects the oldest timestamp of the tier's retention.\n", + "type": "integer" + }, + "last_entry": { + "description": "The maximum unix epoch timestamp available across all metrics that used this tier. This reflects the newest timestamp of the tier's retention.\n" + } + } + } + }, + "units": { + "description": "The units of the database data\n", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "dimensions": { + "type": "object", + "properties": { + "ids": { + "description": "An array with the dimension ids that uniquely identify the dimensions for this query. It is the same with `view.dimensions.ids`.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "units": { + "description": "An array with the units each dimension has in the database (independent of group-by aggregation that may override the units).\n", + "type": "array", + "items": { + "type": "string" + } + }, + "sts": { + "description": "Statistics about the data collection points used for each dimension.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + } + }, + "view": { + "type": "object", + "properties": { + "title": { + "description": "The title the chart should have.\n", + "type": "string" + }, + "format": { + "description": "The format the `result` top level member has. Available on when `debug` flag is set.\n", + "type": "string" + }, + "options": { + "description": "An array presenting all the options given to the query. Available on when `debug` flag is set.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "time_group": { + "description": "The same as the parameter `time_group`. Available on when `debug` flag is set.\n", + "type": "string" + }, + "after": { + "description": "The oldest unix epoch timestamp of the data returned in the `result`.\n", + "type": "integer" + }, + "before": { + "description": "The newest unix epoch timestamp of the data returned in the `result`.\n", + "type": "integer" + }, + "partial_data_trimming": { + "description": "Information related to trimming of the last few points of the `result`, that was required to remove (increasing) partial data.\nTrimming is disabled when the `raw` option is given to the query.\nThis object is available only when the `debug` flag is set.\n", + "type": "object", + "properties": { + "max_update_every": { + "description": "The maximum `update_every` for all metrics aggregated into the query.\nTrimming is by default enabled at `view.before - max_update_every`, but only when `view.before >= now - max_update_every`.\n", + "type": "integer" + }, + "expected_after": { + "description": "The timestamp at which trimming can be enabled.\nIf this timestamp is greater or equal to `view.before`, there is no trimming.\n", + "type": "integer" + }, + "trimmed_after": { + "description": "The timestamp at which trimming has been applied.\nIf this timestamp is greater or equal to `view.before`, there is no trimming.\n" + } + } + }, + "points": { + "description": "The number of points in `result`. Available only when `raw` is given.\n", + "type": "integer" + }, + "units": { + "description": "The units of the query.\n", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "chart_type": { + "description": "The default chart type of the query.\n", + "type": "string", + "enum": [ + "line", + "area", + "stacked" + ] + }, + "dimensions": { + "description": "Detailed information about the chart dimensions included in the `result`.\n", + "type": "object", + "properties": { + "grouped_by": { + "description": "An array with the order of the groupings performed.\n", + "type": "array", + "items": { + "type": "string", + "enum": [ + "selected", + "dimension", + "instance", + "node", + "context", + "units", + "label:key1", + "label:key2", + "label:keyN" + ] + } + }, + "ids": { + "description": "An array with the dimension ids that uniquely identify the dimensions for this query.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "names": { + "description": "An array with the dimension names to be presented to users. Names may be overlapping, but IDs are not.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "priorities": { + "description": "An array with the relative priorities of the dimensions.\nNumbers may not be sequential or unique. The application is expected to order by this and then by name.\n", + "type": "array", + "items": { + "type": "integer" + } + }, + "aggregated": { + "description": "An array with the number of source metrics aggregated into each dimension.\n", + "type": "array", + "items": { + "type": "integer" + } + }, + "units": { + "description": "An array with the units each dimension has.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "sts": { + "description": "Statistics about the view points for each dimension.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + }, + "labels": { + "description": "The labels associated with each dimension in the query.\nThis object is only available when the `group-by-labels` option is given to the query.\n", + "type": "object", + "properties": { + "label_key1": { + "description": "An array having one entry for each of the dimensions of the query.\n", + "type": "array", + "items": { + "description": "An array having one entry for each of the values this label key has for the given dimension.\n", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "min": { + "description": "The minimum value of all points included in the `result`.\n", + "type": "number" + }, + "max": { + "description": "The maximum value of all points included in the `result`.\n", + "type": "number" + } + } + }, + "result": { + "$ref": "#/components/schemas/data_json_formats2" + }, + "timings": { + "type": "object" + } + } + }, + "jsonwrap2_sts": { + "description": "Statistical values\n", + "type": "object", + "properties": { + "min": { + "description": "The minimum value of all metrics aggregated", + "type": "number" + }, + "max": { + "description": "The maximum value of all metrics aggregated", + "type": "number" + }, + "avg": { + "description": "The average value of all metrics aggregated", + "type": "number" + }, + "arp": { + "description": "The average anomaly rate of all metrics aggregated", + "type": "number" + }, + "con": { + "description": "The contribution percentage of all the metrics aggregated", + "type": "number" + } + } + }, + "jsonwrap2_sts_raw": { + "description": "Statistical values when `raw` option is given.\n", + "type": "object", + "properties": { + "min": { + "description": "The minimum value of all metrics aggregated", + "type": "number" + }, + "max": { + "description": "The maximum value of all metrics aggregated", + "type": "number" + }, + "sum": { + "description": "The sum value of all metrics aggregated", + "type": "number" + }, + "ars": { + "description": "The sum anomaly rate of all metrics aggregated", + "type": "number" + }, + "vol": { + "description": "The volume of all the metrics aggregated", + "type": "number" + }, + "cnt": { + "description": "The count of all metrics aggregated", + "type": "integer" + } + } + }, + "jsonwrap2_items_count": { + "description": "Depending on the placement of this object, `items` may be `nodes`, `contexts`, `instances`, `dimensions`, `label keys`, `label key-value pairs`. Furthermore, if the whole object is missing it should be assumed that all its members are zero.\n", + "type": "object", + "properties": { + "sl": { + "description": "The number of items `selected` to query. If absent it is zero.", + "type": "integer" + }, + "ex": { + "description": "The number of items `excluded` from querying. If absent it is zero.", + "type": "integer" + }, + "qr": { + "description": "The number of items (out of `selected`) the query successfully `queried`. If absent it is zero.", + "type": "integer" + }, + "fl": { + "description": "The number of items (from `selected`) that `failed` to be queried. If absent it is zero.", + "type": "integer" + } + } + }, + "jsonwrap2_alerts_count": { + "description": "Counters about alert statuses. If this object is missing, it is assumed that all its members are zero.\n", + "type": "object", + "properties": { + "nm": { + "description": "The name of the alert. Can be absent when the counters refer to more than one alert instances.", + "type": "string" + }, + "cl": { + "description": "The number of CLEAR alerts. If absent, it is zero.", + "type": "integer" + }, + "wr": { + "description": "The number of WARNING alerts. If absent, it is zero.", + "type": "integer" + }, + "cr": { + "description": "The number of CRITICAL alerts. If absent, it is zero.", + "type": "integer" + }, + "ot": { + "description": "The number of alerts that are not CLEAR, WARNING, CRITICAL (so, they are \"other\"). If absent, it is zero.\n", + "type": "integer" + } + } + }, + "api": { + "description": "The version of the API used.", + "type": "integer" + }, + "agents": { + "description": "An array of agent definitions consulted to compose this response.\n", + "type": "array", + "items": { + "type": "object", + "properties": { + "mg": { + "description": "The agent machine GUID.", + "type": "string", + "format": "uuid" + }, + "nd": { + "description": "The agent cloud node ID.", + "type": "string", + "format": "uuid" + }, + "nm": { + "description": "The agent hostname.", + "type": "string" + }, + "ai": { + "description": "The agent index ID for this agent, in this response.", + "type": "integer" + }, + "now": { + "description": "The current unix epoch timestamp of this agent.", + "type": "integer" + } + } + } + }, + "versions": { + "description": "Hashes that allow the caller to detect important database changes of Netdata agents.\n", + "type": "object", + "properties": { + "nodes_hard_hash": { + "description": "An auto-increment value that reflects the number of changes to the number of nodes maintained by the server. Everytime a node is added or removed, this number gets incremented.\n", + "type": "integer" + }, + "contexts_hard_hash": { + "description": "An auto-increment value that reflects the number of changes to the number of contexts maintained by the server. Everytime a context is added or removed, this number gets incremented.\n", + "type": "integer" + }, + "contexts_soft_hash": { + "description": "An auto-increment value that reflects the number of changes to the queue that sends contexts updates to Netdata Cloud. Everytime the contents of a context are updated, this number gets incremented.\n", + "type": "integer" + }, + "alerts_hard_hash": { + "description": "An auto-increment value that reflects the number of changes to the number of alerts. Everytime an alert is added or removed, this number gets incremented.\n", + "type": "integer" + }, + "alerts_soft_hash": { + "description": "An auto-increment value that reflects the number of alerts transitions. Everytime an alert transitions to a new state, this number gets incremented.\n", + "type": "integer" + } + } + }, + "nodeBasic": { + "type": "object", + "description": "Basic information about a node.", + "required": [ + "ni", + "st" + ], + "properties": { + "mg": { + "description": "The machine guid of the node. May not be available if the request is served by the Netdata Cloud.", + "type": "string", + "format": "UUID" + }, + "nd": { + "description": "The node id of the node. May not be available if the node is not registered to Netdata Cloud.", + "type": "string", + "format": "UUID" + }, + "nm": { + "description": "The name (hostname) of the node.", + "type": "string" + }, + "ni": { + "description": "The node index id, a number that uniquely identifies this node for this query.", + "type": "integer" + }, + "st": { + "description": "Status information about the communication with this node.", + "type": "object", + "properties": { + "ai": { + "description": "The agent index id that has been contacted for this node.", + "type": "integer" + }, + "code": { + "description": "The HTTP response code of the response for this node. When working directly with an agent, this is always 200. If the `code` is missing, it should be assumed to be 200.", + "type": "integer" + }, + "msg": { + "description": "A human readable description of the error, if any. If `msg` is missing, or is the empty string `\"\"` or is `null`, there is no description associated with the current status.", + "type": "string" + }, + "ms": { + "description": "The time in milliseconds this node took to respond, or if the local agent responded for this node, the time it needed to execute the query. If `ms` is missing, the time that was required to query this node is unknown.", + "type": "number" + } + } + } + } + }, + "nodeWithDataStatistics": { + "allOf": [ + { + "$ref": "#/components/schemas/nodeBasic" + }, + { + "type": "object", + "description": "`is` stands for instances, `ds` for dimensions, `al` for alerts, `sts` for statistics.\n", + "properties": { + "is": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "ds": { + "$ref": "#/components/schemas/jsonwrap2_items_count" + }, + "al": { + "$ref": "#/components/schemas/jsonwrap2_alerts_count" + }, + "sts": { + "oneOf": [ + { + "$ref": "#/components/schemas/jsonwrap2_sts" + }, + { + "$ref": "#/components/schemas/jsonwrap2_sts_raw" + } + ] + } + } + } + ] + }, + "nodeFull": { + "allOf": [ + { + "$ref": "#/components/schemas/nodeBasic" + }, + { + "type": "object", + "properties": { + "version": { + "description": "The version of the Netdata Agent the node runs.", + "type": "string" + }, + "hops": { + "description": "How many hops away from the origin node, the queried one is. 0 means the agent itself is the origin node.", + "type": "integer" + }, + "state": { + "description": "The current state of the node on this agent.", + "type": "string", + "enum": [ + "reachable", + "stale", + "offline" + ] + } + } + } + ] + }, + "context2Basic": { + "type": "object", + "properties": { + "family": { + "type": "string" + }, + "priority": { + "type": "integer" + }, + "first_entry": { + "type": "integer" + }, + "last_entry": { + "type": "integer" + }, + "live": { + "type": "boolean" + } + } + }, + "contexts2": { + "description": "`/api/v2/contexts` and `/api/v2/q` response about multi-node contexts hosted by a Netdata agent.\n", + "type": "object", + "properties": { + "api": { + "$ref": "#/components/schemas/api" + }, + "agents": { + "$ref": "#/components/schemas/agents" + }, + "versions": { + "$ref": "#/components/schemas/versions" + }, + "contexts": { + "additionalProperties": { + "$ref": "#/components/schemas/context2Basic" + } + } + } + }, + "jsonwrap1": { + "type": "object", + "discriminator": { + "propertyName": "format" + }, + "description": "Response will contain the appropriate subtype, e.g. data_json depending on the requested format.", + "properties": { + "api": { + "type": "number", + "description": "The API version this conforms to." + }, + "id": { + "type": "string", + "description": "The unique id of the chart." + }, + "name": { + "type": "string", + "description": "The name of the chart." + }, + "update_every": { + "type": "number", + "description": "The update frequency of this chart, in seconds. One value every this amount of time is kept in the round robin database (independently of the current view)." + }, + "view_update_every": { + "type": "number", + "description": "The current view appropriate update frequency of this chart, in seconds. There is no point to request chart refreshes, using the same settings, more frequently than this." + }, + "first_entry": { + "type": "number", + "description": "The UNIX timestamp of the first entry (the oldest) in the round robin database (independently of the current view)." + }, + "last_entry": { + "type": "number", + "description": "The UNIX timestamp of the latest entry in the round robin database (independently of the current view)." + }, + "after": { + "type": "number", + "description": "The UNIX timestamp of the first entry (the oldest) returned in this response." + }, + "before": { + "type": "number", + "description": "The UNIX timestamp of the latest entry returned in this response." + }, + "min": { + "type": "number", + "description": "The minimum value returned in the current view. This can be used to size the y-series of the chart." + }, + "max": { + "type": "number", + "description": "The maximum value returned in the current view. This can be used to size the y-series of the chart." + }, + "dimension_names": { + "description": "The dimension names of the chart as returned in the current view.", + "type": "array", + "items": { + "type": "string" + } + }, + "dimension_ids": { + "description": "The dimension IDs of the chart as returned in the current view.", + "type": "array", + "items": { + "type": "string" + } + }, + "latest_values": { + "description": "The latest values collected for the chart (independently of the current view).", + "type": "array", + "items": { + "type": "string" + } + }, + "view_latest_values": { + "description": "The latest values returned with this response.", + "type": "array", + "items": { + "type": "string" + } + }, + "dimensions": { + "type": "number", + "description": "The number of dimensions returned." + }, + "points": { + "type": "number", + "description": "The number of rows / points returned." + }, + "format": { + "type": "string", + "description": "The format of the result returned." + }, + "chart_variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/chart_variables" + } + }, + "result": { + "$ref": "#/components/schemas/data_json_formats1" + } + } + }, + "data_json_formats1": { + "description": "Depending on the `format` given to a data query, any of the following may be returned.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/data_json" + }, + { + "$ref": "#/components/schemas/data_datatable" + }, + { + "$ref": "#/components/schemas/data_csvjsonarray" + }, + { + "$ref": "#/components/schemas/data_array" + }, + { + "$ref": "#/components/schemas/data_txt" + } + ] + }, + "data_json_formats2": { + "description": "Depending on the `format` given to a data query, any of the following may be returned.\n", + "oneOf": [ + { + "$ref": "#/components/schemas/data_json2" + }, + { + "$ref": "#/components/schemas/data_json_formats1" + } + ] + }, + "data_json2": { + "type": "object", + "properties": { + "labels": { + "description": "The IDs of the dimensions returned. The first is always `time`.\n", + "type": "array", + "items": { + "type": "string" + } + }, + "point": { + "description": "The format of each point returned.\n", + "type": "object", + "properties": { + "value": { + "description": "The index of the value in each point.\n", + "type": "integer" + }, + "arp": { + "description": "The index of the anomaly rate in each point.\n", + "type": "integer" + }, + "pa": { + "description": "The index of the point annotations in each point.\nThis is a bitmap. `EMPTY = 1`, `RESET = 2`, `PARTIAL = 4`.\n`EMPTY` means the point has no value.\n`RESET` means that at least one metric aggregated experienced an overflow (a counter that wrapped).\n`PARTIAL` means that this point should have more metrics aggregated into it, but not all metrics had data.\n", + "type": "integer" + }, + "count": { + "description": "The number of metrics aggregated into this point.\nThis exists only when the option `raw` is given to the query and the final aggregation point is NOT `percentage`.\n", + "type": "integer" + }, + "hidden": { + "description": "The sum of the non-selected dimensions aggregated for this group item point.\nThis exists only when the option `raw` is given to the query and the final aggregation method is `percentage`.\n" + } + } + }, + "data": { + "type": "array", + "items": { + "allOf": [ + { + "type": "integer" + }, + { + "type": "array" + } + ] + } + } + } + }, + "data_json": { + "description": "Data response in `json` format.", + "type": "object", + "properties": { + "labels": { + "description": "The dimensions retrieved from the chart.", + "type": "array", + "items": { + "type": "string" + } + }, + "data": { + "description": "The data requested, one element per sample with each element containing the values of the dimensions described in the labels value.\n", + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "data_txt": { + "description": "Data response in `csv`, `tsv`, `tsv-excel`, `ssv`, `ssv-comma`, `markdown`, `html` formats.\n", + "type": "string" + }, + "data_array": { + "description": "Data response in `array` format.", + "type": "array", + "items": { + "type": "number" + } + }, + "data_csvjsonarray": { + "description": "The first inner array contains strings showing the labels of each column, each subsequent array contains the values for each point in time.\n", + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "data_datatable": { + "description": "Data response in datatable / datasource formats (suitable for Google Charts).\n", + "type": "object", + "properties": { + "cols": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "Always empty - for future use." + }, + "label": { + "description": "The dimension returned from the chart." + }, + "pattern": { + "description": "Always empty - for future use." + }, + "type": { + "description": "The type of data in the column / chart-dimension." + }, + "p": { + "description": "Contains any annotations for the column." + } + }, + "required": [ + "id", + "label", + "pattern", + "type" + ] + } + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "c": { + "type": "array", + "items": { + "properties": { + "v": { + "description": "Each value in the row is represented by an object named `c` with five v fields: data, null, null, 0, the value. This format is fixed by the Google Charts API.\"\n" + } + } + } + } + } + } + } + } + }, + "alarms": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "latest_alarm_log_unique_id": { + "type": "integer", + "format": "int32" + }, + "status": { + "type": "boolean" + }, + "now": { + "type": "integer", + "format": "int32" + }, + "alarms": { + "type": "object", + "properties": { + "chart-name.alarm-name": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "description": "Full alarm name." + }, + "chart": { + "type": "string" + }, + "family": { + "type": "string" + }, + "active": { + "type": "boolean", + "description": "Will be false only if the alarm is disabled in the configuration." + }, + "disabled": { + "type": "boolean", + "description": "Whether the health check for this alarm has been disabled via a health command API DISABLE command." + }, + "silenced": { + "type": "boolean", + "description": "Whether notifications for this alarm have been silenced via a health command API SILENCE command." + }, + "exec": { + "type": "string" + }, + "recipient": { + "type": "string" + }, + "source": { + "type": "string" + }, + "units": { + "type": "string" + }, + "info": { + "type": "string" + }, + "status": { + "type": "string" + }, + "last_status_change": { + "type": "integer", + "format": "int32" + }, + "last_updated": { + "type": "integer", + "format": "int32" + }, + "next_update": { + "type": "integer", + "format": "int32" + }, + "update_every": { + "type": "integer", + "format": "int32" + }, + "delay_up_duration": { + "type": "integer", + "format": "int32" + }, + "delay_down_duration": { + "type": "integer", + "format": "int32" + }, + "delay_max_duration": { + "type": "integer", + "format": "int32" + }, + "delay_multiplier": { + "type": "integer", + "format": "int32" + }, + "delay": { + "type": "integer", + "format": "int32" + }, + "delay_up_to_timestamp": { + "type": "integer", + "format": "int32" + }, + "value_string": { + "type": "string" + }, + "no_clear_notification": { + "type": "boolean" + }, + "lookup_dimensions": { + "type": "string" + }, + "db_after": { + "type": "integer", + "format": "int32" + }, + "db_before": { + "type": "integer", + "format": "int32" + }, + "lookup_method": { + "type": "string" + }, + "lookup_after": { + "type": "integer", + "format": "int32" + }, + "lookup_before": { + "type": "integer", + "format": "int32" + }, + "lookup_options": { + "type": "string" + }, + "calc": { + "type": "string" + }, + "calc_parsed": { + "type": "string" + }, + "warn": { + "type": "string" + }, + "warn_parsed": { + "type": "string" + }, + "crit": { + "type": "string" + }, + "crit_parsed": { + "type": "string" + }, + "warn_repeat_every": { + "type": "integer", + "format": "int32" + }, + "crit_repeat_every": { + "type": "integer", + "format": "int32" + }, + "green": { + "type": "string", + "format": "nullable" + }, + "red": { + "type": "string", + "format": "nullable" + }, + "value": { + "type": "number" + } + } + } + } + } + } + }, + "alarm_log_entry": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "unique_id": { + "type": "integer", + "format": "int32" + }, + "alarm_id": { + "type": "integer", + "format": "int32" + }, + "alarm_event_id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "chart": { + "type": "string" + }, + "family": { + "type": "string" + }, + "processed": { + "type": "boolean" + }, + "updated": { + "type": "boolean" + }, + "exec_run": { + "type": "integer", + "format": "int32" + }, + "exec_failed": { + "type": "boolean" + }, + "exec": { + "type": "string" + }, + "recipient": { + "type": "string" + }, + "exec_code": { + "type": "integer", + "format": "int32" + }, + "source": { + "type": "string" + }, + "units": { + "type": "string" + }, + "when": { + "type": "integer", + "format": "int32" + }, + "duration": { + "type": "integer", + "format": "int32" + }, + "non_clear_duration": { + "type": "integer", + "format": "int32" + }, + "status": { + "type": "string" + }, + "old_status": { + "type": "string" + }, + "delay": { + "type": "integer", + "format": "int32" + }, + "delay_up_to_timestamp": { + "type": "integer", + "format": "int32" + }, + "updated_by_id": { + "type": "integer", + "format": "int32" + }, + "updates_id": { + "type": "integer", + "format": "int32" + }, + "value_string": { + "type": "string" + }, + "old_value_string": { + "type": "string" + }, + "silenced": { + "type": "string" + }, + "info": { + "type": "string" + }, + "value": { + "type": "number", + "nullable": true + }, + "old_value": { + "type": "number", + "nullable": true + } + } + }, + "alarms_values": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "alarms": { + "type": "object", + "description": "HashMap with keys being alarm names", + "additionalProperties": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "value": { + "type": "integer" + }, + "last_updated": { + "type": "integer", + "format": "int32" + }, + "status": { + "type": "string", + "enum": [ + "REMOVED", + "UNDEFINED", + "UNINITIALIZED", + "CLEAR", + "RAISED", + "WARNING", + "CRITICAL", + "UNKNOWN" + ] + } + } + } + } + } + }, + "aclk_state": { + "type": "object", + "properties": { + "aclk-available": { + "type": "string", + "description": "Describes whether this agent is capable of connection to the Cloud. False means agent has been built without ACLK component either on purpose (user choice) or due to missing dependency.\n" + }, + "aclk-version": { + "type": "integer", + "description": "Describes which ACLK version is currently used." + }, + "protocols-supported": { + "type": "array", + "description": "List of supported protocols for communication with Cloud.", + "items": { + "type": "string" + } + }, + "agent-claimed": { + "type": "boolean", + "description": "Informs whether this agent has been added to a space in the cloud (User has to perform claiming). If false (user didn't perform claiming) agent will never attempt any cloud connection." + }, + "claimed_id": { + "type": "string", + "format": "uuid", + "description": "Unique ID this agent uses to identify when connecting to cloud" + }, + "online": { + "type": "boolean", + "description": "Informs if this agent was connected to the cloud at the time this request has been processed." + }, + "used-cloud-protocol": { + "type": "string", + "description": "Informs which protocol is used to communicate with cloud", + "enum": [ + "Old", + "New" + ] + } + } + }, + "metric_correlations": { + "type": "object", + "properties": { + "after": { + "description": "the start time of the highlighted window", + "type": "integer" + }, + "before": { + "description": "the end time of the highlighted window", + "type": "integer" + }, + "duration": { + "description": "the duration of the highlighted window", + "type": "integer" + }, + "points": { + "description": "the points of the highlighted window", + "type": "integer" + }, + "baseline_after": { + "description": "the start time of the baseline window", + "type": "integer" + }, + "baseline_before": { + "description": "the end time of the baseline window", + "type": "integer" + }, + "baseline_duration": { + "description": "the duration of the baseline window", + "type": "integer" + }, + "baseline_points": { + "description": "the points of the baseline window", + "type": "integer" + }, + "group": { + "description": "the grouping method across time", + "type": "string" + }, + "method": { + "description": "the correlation method used", + "type": "string" + }, + "options": { + "description": "a comma separated list of the query options set", + "type": "string" + }, + "correlated_dimensions": { + "description": "the number of dimensions returned in the result" + }, + "total_dimensions_count": { + "description": "the total number of dimensions evaluated", + "type": "integer" + }, + "statistics": { + "type": "object", + "properties": { + "query_time_ms": { + "type": "number" + }, + "db_queries": { + "type": "integer" + }, + "db_points_read": { + "type": "integer" + }, + "query_result_points": { + "type": "integer" + }, + "binary_searches": { + "type": "integer" + } + } + }, + "correlated_charts": { + "type": "object", + "description": "An object containing chart objects with their metrics correlations.", + "properties": { + "chart-id1": { + "type": "object", + "properties": { + "context": { + "type": "string" + }, + "dimensions": { + "type": "object", + "properties": { + "dimension1-name": { + "type": "number" + }, + "dimension2-name": { + "type": "number" + } + } + } + } + }, + "chart-id2": { + "type": "object", + "properties": { + "context": { + "type": "string" + }, + "dimensions": { + "type": "object", + "properties": { + "dimension1-name": { + "type": "number" + }, + "dimension2-name": { + "type": "number" + } + } + } + } + } + } + } + } + }, + "weights2": { + "type": "object" + }, + "weights": { + "type": "object", + "properties": { + "after": { + "description": "the start time of the highlighted window", + "type": "integer" + }, + "before": { + "description": "the end time of the highlighted window", + "type": "integer" + }, + "duration": { + "description": "the duration of the highlighted window", + "type": "integer" + }, + "points": { + "description": "the points of the highlighted window", + "type": "integer" + }, + "baseline_after": { + "description": "the start time of the baseline window", + "type": "integer" + }, + "baseline_before": { + "description": "the end time of the baseline window", + "type": "integer" + }, + "baseline_duration": { + "description": "the duration of the baseline window", + "type": "integer" + }, + "baseline_points": { + "description": "the points of the baseline window", + "type": "integer" + }, + "group": { + "description": "the grouping method across time", + "type": "string" + }, + "method": { + "description": "the correlation method used", + "type": "string" + }, + "options": { + "description": "a comma separated list of the query options set", + "type": "string" + }, + "correlated_dimensions": { + "description": "the number of dimensions returned in the result" + }, + "total_dimensions_count": { + "description": "the total number of dimensions evaluated", + "type": "integer" + }, + "statistics": { + "type": "object", + "properties": { + "query_time_ms": { + "type": "number" + }, + "db_queries": { + "type": "integer" + }, + "db_points_read": { + "type": "integer" + }, + "query_result_points": { + "type": "integer" + }, + "binary_searches": { + "type": "integer" + } + } + }, + "contexts": { + "description": "A dictionary of weighted context objects.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/weighted_context" + } + } + } + }, + "weighted_context": { + "type": "object", + "properties": { + "weight": { + "description": "The average weight of the context.", + "type": "number" + }, + "charts": { + "description": "A dictionary of weighted chart objects.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/weighted_chart" + } + } + } + }, + "weighted_chart": { + "type": "object", + "properties": { + "weight": { + "description": "The average weight of the context.", + "type": "number" + }, + "dimensions": { + "description": "A dictionary of weighted dimensions.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/weighted_dimension" + } + } + } + }, + "weighted_dimension": { + "type": "number" + }, + "config_schema": { + "type": "object", + "properties": { + "jsonSchema": { + "type": "object", + "description": "Standard JSON Schema object describing the schema of each configurable entity." + }, + "uiSchema": { + "type": "object", + "description": "Schema for react-json-schema-form to drive the UI. Provides additional UI-specific configuration." + } + } + }, + "config_tree": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "description": "The version of dynamic configuration supported by the Netdata agent." + }, + "tree": { + "type": "object", + "description": "A map of configuration entity paths, each containing one or more configurable entities.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/config_entity" + } + } + }, + "attention": { + "$ref": "#/components/schemas/config_attention" + } + } + }, + "config_entity": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Can be 'single' for entities appearing once, 'template' for entities supporting multiple instances, or 'job' for jobs belonging to a template." + }, + "status": { + "type": "string", + "description": "The current status of the entity. Values include 'accepted', 'running', 'failed', 'disabled', 'incomplete', or 'orphan'." + }, + "cmds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "An array of the possible actions supported by this entity." + }, + "source_type": { + "type": "string", + "description": "The source type of the configuration (e.g., 'internal', 'stock', 'user', 'discovered', 'dyncfg')." + }, + "source": { + "type": "string", + "description": "Additional information about the source, formatted as comma-separated name-value pairs." + }, + "sync": { + "type": "boolean", + "description": "Indicates if this is an internal module (true) or an external plugin (false)." + }, + "user_disabled": { + "type": "boolean", + "description": "True if the entity is disabled by the user." + }, + "restart_required": { + "type": "boolean", + "description": "True if the entity requires a restart after addition or update." + }, + "plugin_rejected": { + "type": "boolean", + "description": "True if a previously saved configuration failed to apply after a restart." + }, + "payload": { + "type": "object", + "description": "Object containing at least an 'available' boolean indicating if there's a saved configuration for this entity.", + "properties": { + "available": { + "type": "boolean" + } + } + }, + "saves": { + "type": "integer", + "description": "The number of times this configuration has been saved to disk by the dynamic configuration manager." + }, + "created_ut": { + "type": "integer", + "format": "int64", + "description": "The timestamp in microseconds when this dynamic configuration was first created." + }, + "modified_ut": { + "type": "integer", + "format": "int64", + "description": "The timestamp in microseconds when this dynamic configuration was last modified." + }, + "template": { + "type": "string", + "description": "Shows the template the job belongs to, applicable when type is 'job'." + } + } + }, + "config_attention": { + "type": "object", + "properties": { + "degraded": { + "type": "boolean" + }, + "restart_required": { + "type": "integer" + }, + "plugin_rejected": { + "type": "integer" + }, + "status_failed": { + "type": "integer" + }, + "status_incomplete": { + "type": "integer" + } + } + }, + "config_default_response": { + "type": "object", + "properties": { + "status": { + "type": "integer", + "description": "The HTTP status code of the response." + }, + "message": { + "type": "string", + "description": "A descriptive message about the response or the action taken." + }, + "data": { + "type": "object", + "description": "The data payload of the response, contents vary depending on the specific request and action.", + "additionalProperties": true + } + } + } + } + } +}
\ No newline at end of file diff --git a/src/web/api/netdata-swagger.yaml b/src/web/api/netdata-swagger.yaml new file mode 100644 index 000000000..ad007ba47 --- /dev/null +++ b/src/web/api/netdata-swagger.yaml @@ -0,0 +1,3486 @@ +openapi: 3.0.0 +info: + title: Netdata API + description: Real-time performance and health monitoring. + version: "v1-rolling" + contact: + name: Netdata Agent API + email: info@netdata.cloud + url: https://netdata.cloud + license: + name: GPL v3+ + url: https://github.com/netdata/netdata/blob/master/LICENSE +servers: + - url: https://registry.my-netdata.io + - url: http://registry.my-netdata.io + - url: http://localhost:19999 +tags: + - name: nodes + description: Everything related to monitored nodes + - name: charts + description: Everything related to chart instances - DO NOT USE IN NEW CODE - use contexts instead + - name: contexts + description: Everything related contexts - in new code, use this instead of charts + - name: data + description: Everything related to data queries + - name: badges + description: Everything related to dynamic badges based on metric data + - name: weights + description: Everything related to scoring / weighting metrics + - name: functions + description: Everything related to functions + - name: alerts + description: Everything related to alerts + - name: management + description: Everything related to managing netdata agents +paths: + /api/v2/nodes: + get: + operationId: getNodes2 + tags: + - nodes + summary: Nodes Info v2 + description: | + Get a list of all nodes hosted by this Netdata agent. + parameters: + - $ref: '#/components/parameters/scopeNodes' + - $ref: '#/components/parameters/scopeContexts' + - $ref: '#/components/parameters/filterNodes' + - $ref: '#/components/parameters/filterContexts' + responses: + "200": + description: OK + content: + application/json: + schema: + description: | + `/api/v2/nodes` response for all nodes hosted by a Netdata agent. + type: object + properties: + api: + $ref: '#/components/schemas/api' + agents: + $ref: '#/components/schemas/agents' + versions: + $ref: '#/components/schemas/versions' + nodes: + type: array + items: + $ref: '#/components/schemas/nodeFull' + /api/v2/contexts: + get: + operationId: getContexts2 + tags: + - contexts + summary: Contexts Info v2 + description: | + Get a list of all contexts, across all nodes, hosted by this Netdata agent. + parameters: + - $ref: '#/components/parameters/scopeNodes' + - $ref: '#/components/parameters/scopeContexts' + - $ref: '#/components/parameters/filterNodes' + - $ref: '#/components/parameters/filterContexts' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/contexts2' + /api/v2/q: + get: + operationId: q2 + tags: + - contexts + summary: Full Text Search v2 + description: | + Get a list of contexts, across all nodes, hosted by this Netdata agent, matching a string expression + parameters: + - name: q + in: query + description: The strings to search for, formatted as a simple pattern + required: true + schema: + type: string + format: simple pattern + - $ref: '#/components/parameters/scopeNodes' + - $ref: '#/components/parameters/scopeContexts' + - $ref: '#/components/parameters/filterNodes' + - $ref: '#/components/parameters/filterContexts' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/contexts2' + /api/v1/info: + get: + operationId: getNodeInfo1 + tags: + - nodes + summary: Node Info v1 + description: | + The info endpoint returns basic information about netdata. It provides: + * netdata version + * netdata unique id + * list of hosts mirrored (includes itself) + * Operating System, Virtualization, K8s nodes and Container technology information + * List of active collector plugins and modules + * Streaming information + * number of alarms in the host + * number of alarms in normal state + * number of alarms in warning state + * number of alarms in critical state + responses: + "200": + description: netdata basic information. + content: + application/json: + schema: + $ref: "#/components/schemas/info" + "503": + description: netdata daemon not ready (used for health checks). + /api/v1/charts: + get: + operationId: getNodeCharts1 + tags: + - charts + summary: List all charts v1 - EOL + description: The charts endpoint returns a summary about all charts stored in the + netdata server. + responses: + "200": + description: An array of charts. + content: + application/json: + schema: + $ref: "#/components/schemas/chart_summary" + /api/v1/chart: + get: + operationId: getNodeChart1 + tags: + - charts + summary: Get one chart v1 - EOL + description: The chart endpoint returns detailed information about a chart. + parameters: + - $ref: '#/components/parameters/chart' + responses: + "200": + description: A javascript object with detailed information about the chart. + content: + application/json: + schema: + $ref: "#/components/schemas/chart" + "400": + description: No chart id was supplied in the request. + "404": + description: No chart with the given id is found. + /api/v1/contexts: + get: + operationId: getNodeContexts1 + tags: + - contexts + summary: Get a list of all node contexts available v1 + description: The contexts endpoint returns a summary about all contexts stored in the + netdata server. + parameters: + - $ref: '#/components/parameters/dimensions' + - $ref: '#/components/parameters/chart_label_key' + - $ref: '#/components/parameters/chart_labels_filter' + - $ref: '#/components/parameters/contextOptions1' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + responses: + "200": + description: An array of contexts. + content: + application/json: + schema: + $ref: "#/components/schemas/context_summary" + /api/v1/context: + get: + operationId: getNodeContext1 + tags: + - contexts + summary: Get info about a specific context + description: | + The context endpoint returns detailed information about a given context. + The `context` parameter is required for this call. + parameters: + - $ref: '#/components/parameters/context' + - $ref: '#/components/parameters/dimensions' + - $ref: '#/components/parameters/chart_label_key' + - $ref: '#/components/parameters/chart_labels_filter' + - $ref: '#/components/parameters/contextOptions1' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + responses: + "200": + description: A javascript object with detailed information about the context. + content: + application/json: + schema: + $ref: "#/components/schemas/context" + "400": + description: No context id was supplied in the request. + "404": + description: No context with the given id is found. + /api/v1/config: + get: + operationId: getConfig + tags: + - dyncfg + description: | + Get dynamic configuration information. + parameters: + - name: action + in: query + description: The type of information required + schema: + type: string + enum: + - tree + - schema + - get + - enable + - disable + - restart + default: tree + - name: id + in: query + description: The ID of the dynamic configuration entity + schema: + type: string + - name: path + in: query + description: Top level path of the configuration entities, used with action 'tree' + schema: + type: string + default: '/' + - name: timeout + in: query + description: The timeout in seconds + schema: + type: number + default: 120 + responses: + "200": + description: The call was successful. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/config_default_response' + - $ref: '#/components/schemas/config_tree' + - $ref: "#/components/schemas/config_schema" + "400": + description: Something is wrong with the request. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + "404": + description: The configurable entity requests is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + post: + operationId: postConfig + tags: + - dyncfg + description: | + Post dynamic configuration to Netdata. + parameters: + - name: action + in: query + description: The type of action required. + schema: + type: string + enum: + - add + - test + - update + - name: id + in: query + description: The ID of the dynamic configuration entity to configure. + schema: + type: string + - name: name + in: query + description: Name of the dynamic configuration entity, used with action 'add' + schema: + type: string + - name: timeout + in: query + description: The timeout in seconds + schema: + type: number + default: 120 + responses: + "200": + description: The call was successful. This also means the configuration is currently running. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + "202": + description: The call was successful. The configuration has been accepted, but its status is not yet known. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + "299": + description: The call was successful. The configuration has been accepted, but a restart is required to apply it. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + "400": + description: Something is wrong with the request. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + "404": + description: The configurable entity requests is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/config_default_response' + /api/v2/data: + get: + operationId: dataQuery2 + tags: + - data + summary: Data Query v2 + description: | + Multi-node, multi-context, multi-instance, multi-dimension data queries, with time and metric aggregation. + parameters: + - name: group_by + in: query + description: | + A comma separated list of the groupings required. + All possible values can be combined together, except `selected`. If `selected` is given in the list, all others are ignored. + The order they are placed in the list is currently ignored. + This parameter is also accepted as `group_by[0]` and `group_by[1]` when multiple grouping passes are required. + required: false + schema: + type: array + items: + type: string + enum: + - dimension + - instance + - percentage-of-instance + - label + - node + - context + - units + - selected + default: + - dimension + - name: group_by_label + in: query + description: | + A comma separated list of the label keys to group by their values. The order of the labels in the list is respected. + This parameter is also accepted as `group_by_label[0]` and `group_by_label[1]` when multiple grouping passes are required. + required: false + schema: + type: string + format: comma separated list of label keys to group by + default: "" + - name: aggregation + in: query + description: | + The aggregation function to apply when grouping metrics together. + When option `raw` is given, `average` and `avg` behave like `sum` and the caller is expected to calculate the average. + This parameter is also accepted as `aggregation[0]` and `aggregation[1]` when multiple grouping passes are required. + required: false + schema: + type: string + enum: + - min + - max + - avg + - average + - sum + - percentage + default: average + - $ref: '#/components/parameters/scopeNodes' + - $ref: '#/components/parameters/scopeContexts' + - $ref: '#/components/parameters/filterNodes' + - $ref: '#/components/parameters/filterContexts' + - $ref: '#/components/parameters/filterInstances' + - $ref: '#/components/parameters/filterLabels' + - $ref: '#/components/parameters/filterAlerts' + - $ref: '#/components/parameters/filterDimensions' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/points' + - $ref: '#/components/parameters/tier' + - $ref: '#/components/parameters/dataQueryOptions' + - $ref: '#/components/parameters/dataTimeGroup2' + - $ref: '#/components/parameters/dataTimeGroupOptions2' + - $ref: '#/components/parameters/dataTimeResampling2' + - $ref: '#/components/parameters/dataFormat2' + - $ref: '#/components/parameters/timeoutMS' + - $ref: '#/components/parameters/callback' + - $ref: '#/components/parameters/filename' + - $ref: '#/components/parameters/tqx' + responses: + "200": + description: | + The call was successful. The response includes the data in the format requested. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/jsonwrap2' + - $ref: '#/components/schemas/data_json_formats2' + text/plain: + schema: + type: string + format: according to the format requested. + text/html: + schema: + type: string + format: html + application/x-javascript: + schema: + type: string + format: javascript + "400": + description: | + Bad request - the body will include a message stating what is wrong. + "500": + description: | + Internal server error. This usually means the server is out of memory. + /api/v1/data: + get: + operationId: dataQuery1 + tags: + - data + summary: Data Query v1 - Single node, single chart or context queries. without group-by. + description: | + Query metric data of a chart or context of a node and return a dataset having time-series data for all dimensions available. + For group-by functionality, use `/api/v2/data`. + At least a `chart` or a `context` have to be given for the data query to be executed. + parameters: + - $ref: '#/components/parameters/chart' + - $ref: '#/components/parameters/context' + - $ref: '#/components/parameters/dimension' + - $ref: '#/components/parameters/chart_label_key' + - $ref: '#/components/parameters/chart_labels_filter' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/points' + - $ref: '#/components/parameters/tier' + - $ref: '#/components/parameters/dataQueryOptions' + - $ref: '#/components/parameters/dataFormat1' + - $ref: '#/components/parameters/dataTimeGroup1' + - $ref: '#/components/parameters/dataTimeGroupOptions1' + - $ref: '#/components/parameters/dataTimeResampling1' + - $ref: '#/components/parameters/timeoutMS' + - $ref: '#/components/parameters/callback' + - $ref: '#/components/parameters/filename' + - $ref: '#/components/parameters/tqx' + responses: + "200": + description: | + The call was successful. The response includes the data in the format requested. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/jsonwrap1' + - $ref: '#/components/schemas/data_json_formats1' + text/plain: + schema: + type: string + format: according to the format requested. + text/html: + schema: + type: string + format: html + application/x-javascript: + schema: + type: string + format: javascript + "400": + description: Bad request - the body will include a message stating what is wrong. + "404": + description: Chart or context is not found. The supplied chart or context will be reported. + "500": + description: Internal server error. This usually means the server is out of + memory. + /api/v1/allmetrics: + get: + operationId: allMetrics1 + tags: + - data + summary: All Metrics v1 - Fetch latest value for all metrics + description: | + The `allmetrics` endpoint returns the latest value of all metrics maintained for a netdata node. + parameters: + - name: format + in: query + description: The format of the response to be returned. + required: true + schema: + type: string + enum: + - shell + - prometheus + - prometheus_all_hosts + - json + default: shell + - name: filter + in: query + description: Allows to filter charts out using simple patterns. + required: false + schema: + type: string + format: any text + - name: variables + in: query + description: | + When enabled, netdata will expose various system configuration variables. + required: false + schema: + type: string + enum: + - yes + - no + default: no + - name: help + in: query + description: | + Enable or disable HELP lines in prometheus output. + required: false + schema: + type: string + enum: + - yes + - no + default: no + - name: types + in: query + description: | + Enable or disable TYPE lines in prometheus output. + required: false + schema: + type: string + enum: + - yes + - no + default: no + - name: timestamps + in: query + description: | + Enable or disable timestamps in prometheus output. + required: false + schema: + type: string + enum: + - yes + - no + default: yes + - name: names + in: query + description: | + When enabled netdata will report dimension names. When disabled netdata will report dimension IDs. The default is controlled in netdata.conf. + required: false + schema: + type: string + enum: + - yes + - no + default: yes + - name: oldunits + in: query + description: | + When enabled, netdata will show metric names for the default `source=average` as they appeared before 1.12, by using the legacy unit naming conventions. + required: false + schema: + type: string + enum: + - yes + - no + default: yes + - name: hideunits + in: query + description: | + When enabled, netdata will not include the units in the metric names, for the default `source=average`. + required: false + schema: + type: string + enum: + - yes + - no + default: yes + - name: server + in: query + description: | + Set a distinct name of the client querying prometheus metrics. Netdata will use the client IP if this is not set. + required: false + schema: + type: string + format: any text + - name: prefix + in: query + description: | + Prefix all prometheus metrics with this string. + required: false + schema: + type: string + format: any text + - name: data + in: query + description: | + Select the prometheus response data source. There is a setting in netdata.conf for the default. + required: false + schema: + type: string + enum: + - as-collected + - average + - sum + default: average + responses: + "200": + description: All the metrics returned in the format requested. + "400": + description: The format requested is not supported. + /api/v1/badge.svg: + get: + operationId: badge1 + tags: + - badges + summary: Generate a badge in form of SVG image for a chart (or dimension) + description: Successful responses are SVG images. + parameters: + - $ref: '#/components/parameters/chart' + - $ref: '#/components/parameters/dimension' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/dataTimeGroup1' + - $ref: '#/components/parameters/dataQueryOptions' + - name: alarm + in: query + description: The name of an alarm linked to the chart. + required: false + allowEmptyValue: true + schema: + type: string + format: any text + - name: label + in: query + description: A text to be used as the label. + required: false + allowEmptyValue: true + schema: + type: string + format: any text + - name: units + in: query + description: A text to be used as the units. + required: false + allowEmptyValue: true + schema: + type: string + format: any text + - name: label_color + in: query + description: | + A color to be used for the background of the label side(left side) of the badge. One of predefined colors or specific color in hex `RGB` or `RRGGBB` format (without preceding `#` character). If value wrong or not given default color will be used. + required: false + allowEmptyValue: true + schema: + oneOf: + - type: string + enum: + - green + - brightgreen + - yellow + - yellowgreen + - orange + - red + - blue + - grey + - gray + - lightgrey + - lightgray + - type: string + format: ^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - name: value_color + in: query + description: | + A color to be used for the background of the value *(right)* part of badge. You can set multiple using a pipe with a condition each, like this: `color<value|color:null` The following operators are supported: >, <, >=, <=, =, :null (to check if no value exists). Each color can be specified in same manner as for `label_color` parameter. Currently only integers are supported as values. + required: false + allowEmptyValue: true + schema: + type: string + format: any text + - name: text_color_lbl + in: query + description: | + Font color for label *(left)* part of the badge. One of predefined colors or as HTML hexadecimal color without preceding `#` character. Formats allowed `RGB` or `RRGGBB`. If no or wrong value given default color will be used. + required: false + allowEmptyValue: true + schema: + oneOf: + - type: string + enum: + - green + - brightgreen + - yellow + - yellowgreen + - orange + - red + - blue + - grey + - gray + - lightgrey + - lightgray + - type: string + format: ^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - name: text_color_val + in: query + description: | + Font color for value *(right)* part of the badge. One of predefined colors or as HTML hexadecimal color without preceding `#` character. Formats allowed `RGB` or `RRGGBB`. If no or wrong value given default color will be used. + required: false + allowEmptyValue: true + schema: + oneOf: + - type: string + enum: + - green + - brightgreen + - yellow + - yellowgreen + - orange + - red + - blue + - grey + - gray + - lightgrey + - lightgray + - type: string + format: ^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - name: multiply + in: query + description: Multiply the value with this number for rendering it at the image + (integer value required). + required: false + allowEmptyValue: true + schema: + type: number + format: integer + - name: divide + in: query + description: Divide the value with this number for rendering it at the image + (integer value required). + required: false + allowEmptyValue: true + schema: + type: number + format: integer + - name: scale + in: query + description: Set the scale of the badge (greater or equal to 100). + required: false + allowEmptyValue: true + schema: + type: number + format: integer + - name: fixed_width_lbl + in: query + description: | + This parameter overrides auto-sizing of badge and creates it with fixed width. This parameter determines the size of the label's left side *(label/name)*. You must set this parameter together with `fixed_width_val` otherwise it will be ignored. You should set the label/value widths wide enough to provide space for all the possible values/contents of the badge you're requesting. In case the text cannot fit the space given it will be clipped. The `scale` parameter still applies on the values you give to `fixed_width_lbl` and `fixed_width_val`. + required: false + allowEmptyValue: false + schema: + type: number + format: integer + - name: fixed_width_val + in: query + description: | + This parameter overrides auto-sizing of badge and creates it with fixed width. This parameter determines the size of the label's right side *(value)*. You must set this parameter together with `fixed_width_lbl` otherwise it will be ignored. You should set the label/value widths wide enough to provide space for all the possible values/contents of the badge you're requesting. In case the text cannot fit the space given it will be clipped. The `scale` parameter still applies on the values you give to `fixed_width_lbl` and `fixed_width_val`. + required: false + allowEmptyValue: false + schema: + type: number + format: integer + responses: + "200": + description: The call was successful. The response should be an SVG image. + "400": + description: Bad request - the body will include a message stating what is wrong. + "404": + description: No chart with the given id is found. + "500": + description: Internal server error. This usually means the server is out of + memory. + /api/v2/weights: + get: + operationId: weights2 + tags: + - weights + summary: Score or weight all or some of the metrics, across all nodes, according to various algorithms. + description: | + This endpoint goes through all metrics and scores them according to an algorithm. + parameters: + - $ref: '#/components/parameters/weightMethods' + - $ref: '#/components/parameters/scopeNodes' + - $ref: '#/components/parameters/scopeContexts' + - $ref: '#/components/parameters/filterNodes' + - $ref: '#/components/parameters/filterContexts' + - $ref: '#/components/parameters/filterInstances' + - $ref: '#/components/parameters/filterLabels' + - $ref: '#/components/parameters/filterAlerts' + - $ref: '#/components/parameters/filterDimensions' + - $ref: '#/components/parameters/baselineAfter' + - $ref: '#/components/parameters/baselineBefore' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/tier' + - $ref: '#/components/parameters/points' + - $ref: '#/components/parameters/timeoutMS' + - $ref: '#/components/parameters/dataQueryOptions' + - $ref: '#/components/parameters/dataTimeGroup2' + - $ref: '#/components/parameters/dataTimeGroupOptions2' + responses: + "200": + description: JSON object with weights for each context, chart and dimension. + content: + application/json: + schema: + $ref: "#/components/schemas/weights2" + "400": + description: The given parameters are invalid. + "403": + description: metrics correlations are not enabled on this Netdata Agent. + "404": + description: | + No charts could be found, or the method that correlated the metrics did not produce any result. + "504": + description: Timeout - the query took too long and has been cancelled. + /api/v1/weights: + get: + operationId: weights1 + tags: + - weights + summary: Score or weight all or some of the metrics of a single node, according to various algorithms. + description: | + This endpoint goes through all metrics and scores them according to an algorithm. + parameters: + - $ref: '#/components/parameters/weightMethods' + - $ref: '#/components/parameters/context' + - $ref: '#/components/parameters/baselineAfter' + - $ref: '#/components/parameters/baselineBefore' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/tier' + - $ref: '#/components/parameters/points' + - $ref: '#/components/parameters/timeoutMS' + - $ref: '#/components/parameters/dataQueryOptions' + - $ref: '#/components/parameters/dataTimeGroup1' + - $ref: '#/components/parameters/dataTimeGroupOptions1' + responses: + "200": + description: JSON object with weights for each context, chart and dimension. + content: + application/json: + schema: + $ref: "#/components/schemas/weights" + "400": + description: The given parameters are invalid. + "403": + description: metrics correlations are not enabled on this Netdata Agent. + "404": + description: No charts could be found, or the method + that correlated the metrics did not produce any result. + "504": + description: Timeout - the query took too long and has been cancelled. + /api/v1/metric_correlations: + get: + operationId: metricCorrelations1 + tags: + - weights + summary: Analyze all the metrics to find their correlations - EOL + description: | + THIS ENDPOINT IS OBSOLETE. Use the /weights endpoint. Given two time-windows (baseline, highlight), it goes through all the available metrics, querying both windows and tries to find how these two windows relate to each other. It supports multiple algorithms to do so. The result is a list of all metrics evaluated, weighted for 0.0 (the two windows are more different) to 1.0 (the two windows are similar). The algorithm adjusts automatically the baseline window to be a power of two multiple of the highlighted (1, 2, 4, 8, etc). + parameters: + - $ref: '#/components/parameters/weightMethods' + - $ref: '#/components/parameters/baselineAfter' + - $ref: '#/components/parameters/baselineBefore' + - $ref: '#/components/parameters/after' + - $ref: '#/components/parameters/before' + - $ref: '#/components/parameters/points' + - $ref: '#/components/parameters/tier' + - $ref: '#/components/parameters/timeoutMS' + - $ref: '#/components/parameters/dataQueryOptions' + - $ref: '#/components/parameters/dataTimeGroup1' + - $ref: '#/components/parameters/dataTimeGroupOptions1' + responses: + "200": + description: JSON object with weights for each chart and dimension. + content: + application/json: + schema: + $ref: "#/components/schemas/metric_correlations" + "400": + description: The given parameters are invalid. + "403": + description: metrics correlations are not enabled on this Netdata Agent. + "404": + description: No charts could be found, or the method + that correlated the metrics did not produce any result. + "504": + description: Timeout - the query took too long and has been cancelled. + /api/v1/function: + get: + operationId: function1 + tags: + - functions + description: "Execute a collector function." + parameters: + - name: function + in: query + description: The name of the function, as returned by the collector. + required: true + allowEmptyValue: false + schema: + type: string + - $ref: '#/components/parameters/timeoutSecs' + responses: + "200": + description: The collector function has been executed successfully. Each collector may return a different type of content. + "400": + description: The request was rejected by the collector. + "404": + description: The requested function is not found. + "500": + description: Other internal error, getting this error means there is a bug in Netdata. + "503": + description: The collector to execute the function is not currently available. + "504": + description: Timeout while waiting for the collector to execute the function. + "591": + description: The collector sent a response, but it was invalid or corrupted. + /api/v1/functions: + get: + operationId: functions1 + tags: + - functions + summary: Get a list of all registered collector functions. + description: Collector functions are programs that can be executed on demand. + responses: + "200": + description: A JSON object containing one object per supported function. + /api/v1/alarms: + get: + operationId: alerts1 + tags: + - alerts + summary: Get a list of active or raised alarms on the server + description: | + The alarms endpoint returns the list of all raised or enabled alarms on the netdata server. Called without any parameters, the raised alarms in state WARNING or CRITICAL are returned. By passing "?all", all the enabled alarms are returned. + parameters: + - name: all + in: query + description: If passed, all enabled alarms are returned. + required: false + allowEmptyValue: true + schema: + type: boolean + - name: active + in: query + description: If passed, the raised alarms in state WARNING or CRITICAL are returned. + required: false + allowEmptyValue: true + schema: + type: boolean + responses: + "200": + description: An object containing general info and a linked list of alarms. + content: + application/json: + schema: + $ref: "#/components/schemas/alarms" + /api/v1/alarms_values: + get: + operationId: alertValues1 + tags: + - alerts + summary: Get a list of active or raised alarms on the server + description: | + The alarms_values endpoint returns the list of all raised or enabled alarms on the netdata server. Called without any parameters, the raised alarms in state WARNING or CRITICAL are returned. By passing '?all', all the enabled alarms are returned. This option output differs from `/alarms` in the number of variables delivered. This endpoint gives to user `id`, `value`, `last_updated` time, and alarm `status`. + parameters: + - name: all + in: query + description: If passed, all enabled alarms are returned. + required: false + allowEmptyValue: true + schema: + type: boolean + - name: active + in: query + description: If passed, the raised alarms in state WARNING or CRITICAL are returned. + required: false + allowEmptyValue: true + schema: + type: boolean + responses: + "200": + description: An object containing general info and a linked list of alarms. + content: + application/json: + schema: + $ref: "#/components/schemas/alarms_values" + /api/v1/alarm_log: + get: + operationId: alertsLog1 + tags: + - alerts + summary: Retrieves the entries of the alarm log + description: | + Returns an array of alarm_log entries, with historical information on raised and cleared alarms. + parameters: + - name: after + in: query + description: | + Passing the parameter after=UNIQUEID returns all the events in the alarm log that occurred after UNIQUEID. An automated series of calls would call the interface once without after=, store the last UNIQUEID of the returned set, and give it back to get incrementally the next events. + required: false + schema: + type: integer + responses: + "200": + description: An array of alarm log entries. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/alarm_log_entry" + /api/v1/alarm_count: + get: + operationId: alertsCount1 + tags: + - alerts + summary: Get an overall status of the chart + description: | + Checks multiple charts with the same context and counts number of alarms with given status. + parameters: + - $ref: '#/components/parameters/context' + - name: status + in: query + description: Specify alarm status to count. + required: false + allowEmptyValue: true + schema: + type: string + enum: + - REMOVED + - UNDEFINED + - UNINITIALIZED + - CLEAR + - RAISED + - WARNING + - CRITICAL + default: RAISED + responses: + "200": + description: An object containing a count of alarms with given status for given + contexts. + content: + application/json: + schema: + type: array + items: + type: number + "500": + description: Internal server error. This usually means the server is out of + memory. + /api/v1/alarm_variables: + get: + operationId: getNodeAlertVariables1 + tags: + - alerts + summary: List variables available to configure alarms for a chart + description: | + Returns the basic information of a chart and all the variables that can be used in alarm and template health configurations for the particular chart or family. + parameters: + - name: chart + in: query + description: The id of the chart as returned by the /charts call. + required: true + schema: + type: string + format: as returned by /charts + default: system.cpu + responses: + "200": + description: A javascript object with information about the chart and the + available variables. + content: + application/json: + schema: + $ref: "#/components/schemas/alarm_variables" + "400": + description: Bad request - the body will include a message stating what is wrong. + "404": + description: No chart with the given id is found. + "500": + description: Internal server error. This usually means the server is out of + memory. + /api/v1/manage/health: + get: + operationId: health1 + tags: + - management + summary: | + Accesses the health management API to control health checks and notifications at runtime. + description: | + Available from Netdata v1.12 and above, protected via bearer authorization. Especially useful for maintenance periods, the API allows you to disable health checks completely, silence alarm notifications, or Disable/Silence specific alarms that match selectors on alarm/template name, chart, context, host and family. For the simple disable/silence all scenarios, only the cmd parameter is required. The other parameters are used to define alarm selectors. For more information and examples, refer to the netdata documentation. + parameters: + - name: cmd + in: query + description: | + DISABLE ALL: No alarm criteria are evaluated, nothing is written in the alarm log. SILENCE ALL: No notifications are sent. RESET: Return to the default state. DISABLE/SILENCE: Set the mode to be used for the alarms matching the criteria of the alarm selectors. LIST: Show active configuration. + required: false + schema: + type: string + enum: + - DISABLE ALL + - SILENCE ALL + - DISABLE + - SILENCE + - RESET + - LIST + - name: alarm + in: query + description: The expression provided will match both `alarm` and `template` names. + schema: + type: string + - name: chart + in: query + description: Chart ids/names, as shown on the dashboard. These will match the + `on` entry of a configured `alarm`. + schema: + type: string + - name: context + in: query + description: Chart context, as shown on the dashboard. These will match the `on` + entry of a configured `template`. + schema: + type: string + - name: hosts + in: query + description: The hostnames that will need to match. + schema: + type: string + responses: + "200": + description: A plain text response based on the result of the command. + "403": + description: Bearer authentication error. + /api/v1/aclk: + get: + operationId: aclk1 + tags: + - management + summary: Get information about current ACLK state + description: | + ACLK endpoint returns detailed information about current state of ACLK (Agent to Cloud communication). + responses: + "200": + description: JSON object with ACLK information. + content: + application/json: + schema: + $ref: "#/components/schemas/aclk_state" +components: + parameters: + scopeNodes: + name: scope_nodes + in: query + description: | + A simple pattern limiting the nodes scope of the query. The scope controls both data and metadata response. The simple pattern is checked against the nodes' machine guid, node id and hostname. The default nodes scope is all nodes for which this agent has data for. Usually the nodes scope is used to slice the entire dashboard (e.g. the Global Nodes Selector at the Netdata Cloud overview dashboard). Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + scopeContexts: + name: scope_contexts + in: query + description: | + A simple pattern limiting the contexts scope of the query. The scope controls both data and metadata response. The default contexts scope is all contexts for which this agent has data for. Usually the contexts scope is used to slice data on the dashboard (e.g. each context based chart has its own contexts scope, limiting the chart to all the instances of the selected context). Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterNodes: + name: nodes + in: query + description: | + A simple pattern matching the nodes to be queried. This only controls the data response, not the metadata. The simple pattern is checked against the nodes' machine guid, node id, hostname. The default nodes selector is all the nodes matched by the nodes scope. Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterContexts: + name: contexts + in: query + description: | + A simple pattern matching the contexts to be queried. This only controls the data response, not the metadata. Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterInstances: + name: instances + in: query + description: | + A simple pattern matching the instances to be queried. The simple pattern is checked against the instance `id`, the instance `name`, the fully qualified name of the instance `id` and `name`, like `instance@machine_guid`, where `instance` is either its `id` or `name`. Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterLabels: + name: labels + in: query + description: | + A simple pattern matching the labels to be queried. The simple pattern is checked against `name:value` of all the labels of all the eligible instances (as filtered by all the above: scope nodes, scope contexts, nodes, contexts and instances). Negative simple patterns should not be used in this filter. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterAlerts: + name: alerts + in: query + description: | + A simple pattern matching the alerts to be queried. The simple pattern is checked against the `name` of alerts and the combination of `name:status`, when status is one of `CLEAR`, `WARNING`, `CRITICAL`, `REMOVED`, `UNDEFINED`, `UNINITIALIZED`, of all the alerts of all the eligible instances (as filtered by all the above). A negative simple pattern will exclude the instances having the labels matched. + required: false + schema: + type: string + format: simple pattern + default: "*" + filterDimensions: + name: dimensions + in: query + description: | + A simple patterns matching the dimensions to be queried. The simple pattern is checked against and `id` and the `name` of the dimensions of the eligible instances (as filtered by all the above). Both positive and negative simple pattern expressions are supported. + required: false + schema: + type: string + format: simple pattern + default: "*" + + dataFormat1: + name: format + in: query + description: The format of the data to be returned. + allowEmptyValue: false + schema: + type: string + enum: + - json + - jsonp + - csv + - tsv + - tsv-excel + - ssv + - ssvcomma + - datatable + - datasource + - html + - markdown + - array + - csvjsonarray + default: json + dataFormat2: + name: format + in: query + description: The format of the data to be returned. + allowEmptyValue: false + schema: + type: string + enum: + - json + - json2 + - jsonp + - csv + - tsv + - tsv-excel + - ssv + - ssvcomma + - datatable + - datasource + - html + - markdown + - array + - csvjsonarray + default: json2 + dataQueryOptions: + name: options + in: query + description: | + Options that affect data generation. + * `jsonwrap` - Wrap the output in a JSON object with metadata about the query. + * `raw` - change the output so that it is aggregatable across multiple such queries. Supported by `/api/v2` data queries and `json2` format. + * `minify` - Remove unnecessary spaces and newlines from the output. + * `debug` - Provide additional information in `jsonwrap` output to help tracing issues. + * `nonzero` - Do not return dimensions that all their values are zero, to improve the visual appearance of charts. They will still be returned if all the dimensions are entirely zero. + * `null2zero` - Replace `null` values with `0`. + * `absolute` or `abs` - Traditionally Netdata returns select dimensions negative to improve visual appearance. This option turns this feature off. + * `display-absolute` - Only used by badges, to do color calculation using the signed value, but render the value without a sign. + * `flip` or `reversed` - Order the timestamps array in reverse order (newest to oldest). + * `min2max` - When flattening multi-dimensional data into a single metric format, use `max - min` instead of `sum`. This is EOL - use `/api/v2` to control aggregation across dimensions. + * `percentage` - Convert all values into a percentage vs the row total. When enabled, Netdata will query all dimensions, even the ones that have not been selected or are hidden, to find the row total, in order to calculate the percentage of each dimension selected. + * `seconds` - Output timestamps in seconds instead of dates. + * `milliseconds` or `ms` - Output timestamps in milliseconds instead of dates. + * `unaligned` - by default queries are aligned to the the view, so that as time passes past data returned do not change. When a data query will not be used for visualization, `unaligned` can be given to avoid aligning the query time-frame for visual precision. + * `match-ids`, `match-names`. By default filters match both IDs and names when they are available. Setting either of the two options will disable the other. + * `anomaly-bit` - query the anomaly information instead of metric values. This is EOL, use `/api/v2` and `json2` format which always returns this information and many more. + * `jw-anomaly-rates` - return anomaly rates as a separate result set in the same `json` format response. This is EOL, use `/api/v2` and `json2` format which always returns information and many more. + * `details` - `/api/v2/data` returns in `jsonwrap` the full tree of dimensions that have been matched by the query. + * `group-by-labels` - `/api/v2/data` returns in `jsonwrap` flattened labels per output dimension. These are used to identify the instances that have been aggregated into each dimension, making it possible to provide a map, like Netdata does for Kubernetes. + * `natural-points` - return timestamps as found in the database. The result is again fixed-step, but the query engine attempts to align them with the timestamps found in the database. + * `virtual-points` - return timestamps independent of the database alignment. This is needed aggregating data across multiple Netdata agents, to ensure that their outputs do not need to be interpolated to be merged. + * `selected-tier` - use data exclusively from the selected tier given with the `tier` parameter. This option is set automatically when the `tier` parameter is set. + * `all-dimensions` - In `/api/v1` `jsonwrap` include metadata for all candidate metrics examined. In `/api/v2` this is standard behavior and no option is needed. + * `label-quotes` - In `csv` output format, enclose each header label in quotes. + * `objectrows` - Each row of value should be an object, not an array (only for `json` format). + * `google_json` - Comply with google JSON/JSONP specs (only for `json` format). + required: false + allowEmptyValue: false + schema: + type: array + items: + type: string + enum: + - jsonwrap + - raw + - minify + - debug + - nonzero + - null2zero + - abs + - absolute + - display-absolute + - flip + - reversed + - min2max + - percentage + - seconds + - ms + - milliseconds + - unaligned + - match-ids + - match-names + - anomaly-bit + - jw-anomaly-rates + - details + - group-by-labels + - natural-points + - virtual-points + - selected-tier + - all-dimensions + - label-quotes + - objectrows + - google_json + default: + - seconds + - jsonwrap + dataTimeGroup1: + name: group + in: query + description: | + Time aggregation function. If multiple collected values are to be grouped in order to return fewer points, this parameters defines the method of grouping. If the `absolute` option is set, the values are turned positive before applying this calculation. + required: false + schema: + type: string + enum: + - min + - max + - avg + - average + - median + - stddev + - sum + - incremental-sum + - ses + - des + - cv + - countif + - percentile + - percentile25 + - percentile50 + - percentile75 + - percentile80 + - percentile90 + - percentile95 + - percentile97 + - percentile98 + - percentile99 + - trimmed-mean + - trimmed-mean1 + - trimmed-mean2 + - trimmed-mean3 + - trimmed-mean5 + - trimmed-mean10 + - trimmed-mean15 + - trimmed-mean20 + - trimmed-mean25 + - trimmed-median + - trimmed-median1 + - trimmed-median2 + - trimmed-median3 + - trimmed-median5 + - trimmed-median10 + - trimmed-median15 + - trimmed-median20 + - trimmed-median25 + default: average + dataTimeGroup2: + name: time_group + in: query + description: | + Time aggregation function. If multiple collected values are to be grouped in order to return fewer points, this parameters defines the method of grouping. If the `absolute` option is set, the values are turned positive before applying this calculation. + required: false + schema: + type: string + enum: + - min + - max + - avg + - average + - median + - stddev + - sum + - incremental-sum + - ses + - des + - cv + - countif + - percentile + - percentile25 + - percentile50 + - percentile75 + - percentile80 + - percentile90 + - percentile95 + - percentile97 + - percentile98 + - percentile99 + - trimmed-mean + - trimmed-mean1 + - trimmed-mean2 + - trimmed-mean3 + - trimmed-mean5 + - trimmed-mean10 + - trimmed-mean15 + - trimmed-mean20 + - trimmed-mean25 + - trimmed-median + - trimmed-median1 + - trimmed-median2 + - trimmed-median3 + - trimmed-median5 + - trimmed-median10 + - trimmed-median15 + - trimmed-median20 + - trimmed-median25 + default: average + dataTimeGroupOptions1: + name: group_options + in: query + description: | + When the time grouping function supports additional parameters, this field can be used to pass them to it. Currently `countif`, `trimmed-mean`, `trimmed-median` and `percentile` support this. For `countif` the string may start with `<`, `<=`, `<:`, `<>`, `!=`, `>`, `>=`, `>:`. For all others just a number is expected. + required: false + schema: + type: string + dataTimeGroupOptions2: + name: time_group_options + in: query + description: | + When the time grouping function supports additional parameters, this field can be used to pass them to it. Currently `countif`, `trimmed-mean`, `trimmed-median` and `percentile` support this. For `countif` the string may start with `<`, `<=`, `<:`, `<>`, `!=`, `>`, `>=`, `>:`. For all others just a number is expected. + required: false + schema: + type: string + dataTimeResampling1: + name: gtime + in: query + description: | + The grouping number of seconds. This is used in conjunction with group=average to change the units of metrics (ie when the data is per-second, setting gtime=60 will turn them to per-minute). + required: false + allowEmptyValue: false + schema: + type: number + format: integer + default: 0 + dataTimeResampling2: + name: time_resampling + in: query + description: | + For incremental values that are "per second", this value is used to resample them to "per minute` (60) or "per hour" (3600). It can only be used in conjunction with group=average. + required: false + schema: + type: number + format: integer + default: 0 + timeoutMS: + name: timeout + in: query + description: | + Specify a timeout value in milliseconds after which the agent will abort the query and return a 503 error. A value of 0 indicates no timeout. + required: false + schema: + type: number + format: integer + default: 0 + timeoutSecs: + name: timeout + in: query + description: | + Specify a timeout value in seconds after which the agent will abort the query and return a 504 error. A value of 0 indicates no timeout, but some endpoints, like `weights`, do not accept infinite timeouts (they have a predefined default), so to disable the timeout it must be set to a really high value. + required: false + schema: + type: number + format: integer + default: 0 + before: + name: before + in: query + description: | + `after` and `before` define the time-frame of a query. `before` can be a negative number of seconds, up to 3 years (-94608000), relative to current clock. If not set, it is assumed to be the current clock time. When `before` is positive, it is assumed to be a unix epoch timestamp. When non-data endpoints support the `after` and `before`, they use the time-frame to limit their response for objects having data retention within the time-frame given. + required: false + schema: + type: integer + default: 0 + after: + name: after + in: query + description: | + `after` and `before` define the time-frame of a query. `after` can be a negative number of seconds, up to 3 years (-94608000), relative to `before`. If not set, it is usually assumed to be -600. When non-data endpoints support the `after` and `before`, they use the time-frame to limit their response for objects having data retention within the time-frame given. + required: false + schema: + type: integer + default: -600 + baselineBefore: + name: baseline_before + in: query + description: | + `baseline_after` and `baseline_before` define the baseline time-frame of a comparative query. `baseline_before` can be a negative number of seconds, up to 3 years (-94608000), relative to current clock. If not set, it is assumed to be the current clock time. When `baseline_before` is positive, it is assumed to be a unix epoch timestamp. + required: false + schema: + type: integer + default: 0 + baselineAfter: + name: baseline_after + in: query + description: | + `baseline_after` and `baseline_before` define the baseline time-frame of a comparative query. `baseline_after` can be a negative number of seconds, up to 3 years (-94608000), relative to `baseline_before`. If not set, it is usually assumed to be -300. + required: false + schema: + type: integer + default: -600 + points: + name: points + in: query + description: | + The number of points to be returned. If not given, or it is <= 0, or it is bigger than the points stored in the database for the given duration, all the available collected values for the given duration will be returned. For `weights` endpoints that do statistical analysis, the `points` define the detail of this analysis (the default is 500). + required: false + schema: + type: number + format: integer + default: 0 + tier: + name: tier + in: query + description: | + Use only the given dbengine tier for executing the query. Setting this parameters automatically sets the option `selected-tier` for the query. + required: false + schema: + type: number + format: integer + callback: + name: callback + in: query + description: | + For JSONP responses, the callback function name. + required: false + schema: + type: string + filename: + name: filename + in: query + description: | + Add `Content-Disposition: attachment; filename=` header to the response, that will instruct the browser to save the response with the given filename." + required: false + schema: + type: string + tqx: + name: tqx + in: query + description: | + [Google Visualization API](https://developers.google.com/chart/interactive/docs/dev/implementing_data_source?hl=en) formatted parameter. + required: false + schema: + type: string + contextOptions1: + name: options + in: query + description: Options that affect data generation. + required: false + schema: + type: array + items: + type: string + enum: + - full + - all + - charts + - dimensions + - labels + - uuids + - queue + - flags + - deleted + - deepscan + chart: + name: chart + in: query + description: The id of the chart as returned by the `/api/v1/charts` call. + required: false + allowEmptyValue: false + schema: + type: string + format: as returned by `/api/v1/charts` + context: + name: context + in: query + description: The context of the chart as returned by the /charts call. + required: false + allowEmptyValue: false + schema: + type: string + format: as returned by /charts + dimension: + name: dimension + in: query + description: Zero, one or more dimension ids or names, as returned by the /chart + call, separated with comma or pipe. Netdata simple patterns are + supported. + required: false + allowEmptyValue: false + schema: + type: array + items: + type: string + format: as returned by /charts + dimensions: + name: dimensions + in: query + description: a simple pattern matching dimensions (use comma or pipe as separator) + required: false + allowEmptyValue: true + schema: + type: string + chart_label_key: + name: chart_label_key + in: query + description: | + Specify the chart label keys that need to match for context queries as comma separated values. At least one matching key is needed to match the corresponding chart. + required: false + allowEmptyValue: false + schema: + type: string + format: key1,key2,key3 + chart_labels_filter: + name: chart_labels_filter + in: query + description: | + Specify the chart label keys and values to match for context queries. All keys/values need to match for the chart to be included in the query. The labels are specified as key1:value1,key2:value2 + required: false + allowEmptyValue: false + schema: + type: string + format: key1:value1,key2:value2,key3:value3 + weightMethods: + name: method + in: query + description: The weighting / scoring algorithm. + required: false + schema: + type: string + enum: + - ks2 + - volume + - anomaly-rate + - value + schemas: + info: + type: object + properties: + version: + type: string + description: netdata version of the server. + example: 1.11.1_rolling + uid: + type: string + description: netdata unique id of the server. + example: 24e9fe3c-f2ac-11e8-bafc-0242ac110002 + mirrored_hosts: + type: array + description: List of hosts mirrored of the server (include itself). + items: + type: string + example: + - host1.example.com + - host2.example.com + mirrored_hosts_status: + type: array + description: >- + List of details of hosts mirrored to this served (including self). + Indexes correspond to indexes in "mirrored_hosts". + items: + type: object + description: Host data + properties: + guid: + type: string + format: uuid + nullable: false + description: Host unique GUID from `netdata.public.unique.id`. + example: 245e4bff-3b34-47c1-a6e5-5c535a9abfb2 + reachable: + type: boolean + nullable: false + description: Current state of streaming. Always true for localhost/self. + claim_id: + type: string + format: uuid + nullable: true + description: >- + Cloud GUID/identifier in case the host is claimed. + If child status unknown or unclaimed this field is set to `null` + example: c3b2a66a-3052-498c-ac52-7fe9e8cccb0c + os_name: + type: string + description: Operating System Name. + example: Manjaro Linux + os_id: + type: string + description: Operating System ID. + example: manjaro + os_id_like: + type: string + description: Known OS similar to this OS. + example: arch + os_version: + type: string + description: Operating System Version. + example: 18.0.4 + os_version_id: + type: string + description: Operating System Version ID. + example: unknown + os_detection: + type: string + description: OS parameters detection method. + example: Mixed + kernel_name: + type: string + description: Kernel Name. + example: Linux + kernel_version: + type: string + description: Kernel Version. + example: 4.19.32-1-MANJARO + is_k8s_node: + type: boolean + description: Netdata is running on a K8s node. + example: false + architecture: + type: string + description: Kernel architecture. + example: x86_64 + virtualization: + type: string + description: Virtualization Type. + example: kvm + virt_detection: + type: string + description: Virtualization detection method. + example: systemd-detect-virt + container: + type: string + description: Container technology. + example: docker + container_detection: + type: string + description: Container technology detection method. + example: dockerenv + stream_compression: + type: boolean + description: Stream transmission compression method. + example: true + labels: + type: object + description: List of host labels. + properties: + app: + type: string + description: Host label. + example: netdata + collectors: + type: array + items: + type: object + description: Array of collector plugins and modules. + properties: + plugin: + type: string + description: Collector plugin. + example: python.d.plugin + module: + type: string + description: Module of the collector plugin. + example: dockerd + alarms: + type: object + description: Number of alarms in the server. + properties: + normal: + type: integer + description: Number of alarms in normal state. + warning: + type: integer + description: Number of alarms in warning state. + critical: + type: integer + description: Number of alarms in critical state. + chart_summary: + type: object + properties: + hostname: + type: string + description: The hostname of the netdata server. + version: + type: string + description: netdata version of the server. + release_channel: + type: string + description: The release channel of the build on the server. + example: nightly + timezone: + type: string + description: The current timezone on the server. + os: + type: string + description: The netdata server host operating system. + enum: + - macos + - linux + - freebsd + history: + type: number + description: The duration, in seconds, of the round robin database maintained by + netdata. + memory_mode: + type: string + description: The name of the database memory mode on the server. + update_every: + type: number + description: The default update frequency of the netdata server. All charts have + an update frequency equal or bigger than this. + charts: + type: object + description: An object containing all the chart objects available at the netdata + server. This is used as an indexed array. The key of each chart + object is the id of the chart. + additionalProperties: + $ref: "#/components/schemas/chart" + charts_count: + type: number + description: The number of charts. + dimensions_count: + type: number + description: The total number of dimensions. + alarms_count: + type: number + description: The number of alarms. + rrd_memory_bytes: + type: number + description: The size of the round robin database in bytes. + chart: + type: object + properties: + id: + type: string + description: The unique id of the chart. + name: + type: string + description: The name of the chart. + type: + type: string + description: The type of the chart. Types are not handled by netdata. You can use + this field for anything you like. + family: + type: string + description: The family of the chart. Families are not handled by netdata. You + can use this field for anything you like. + title: + type: string + description: The title of the chart. + priority: + type: number + description: The relative priority of the chart. Netdata does not care about + priorities. This is just an indication of importance for the chart + viewers to sort charts of higher priority (lower number) closer to + the top. Priority sorting should only be used among charts of the + same type or family. + enabled: + type: boolean + description: True when the chart is enabled. Disabled charts do not currently + collect values, but they may have historical values available. + units: + type: string + description: The unit of measurement for the values of all dimensions of the + chart. + data_url: + type: string + description: The absolute path to get data values for this chart. You are + expected to use this path as the base when constructing the URL to + fetch data values for this chart. + chart_type: + type: string + description: The chart type. + enum: + - line + - area + - stacked + duration: + type: number + description: The duration, in seconds, of the round robin database maintained by + netdata. + first_entry: + type: number + description: The UNIX timestamp of the first entry (the oldest) in the round + robin database. + last_entry: + type: number + description: The UNIX timestamp of the latest entry in the round robin database. + update_every: + type: number + description: The update frequency of this chart, in seconds. One value every this + amount of time is kept in the round robin database. + dimensions: + type: object + description: | + An object containing all the chart dimensions available for the chart. This is used as an indexed array. For each pair in the dictionary: the key is the id of the dimension and the value is a dictionary containing the name." + additionalProperties: + type: object + properties: + name: + type: string + description: The name of the dimension + chart_variables: + type: object + additionalProperties: + $ref: "#/components/schemas/chart_variables" + green: + type: number + nullable: true + description: Chart health green threshold. + red: + type: number + nullable: true + description: Chart health red threshold. + context_summary: + type: object + properties: + hostname: + type: string + description: The hostname of the netdata server. + machine_guid: + type: string + description: The unique installation id of this netdata server. + node_id: + type: string + description: The unique node id of this netdata server at the hub. + example: nightly + claim_id: + type: string + description: The unique handshake id of this netdata server and the hub. + host_labels: + type: object + description: The host labels associated with this netdata server. + context: + type: object + description: "An object containing all the context objects available at the netdata server. + This is used as an indexed array. The key of each context object is the id of the context." + additionalProperties: + $ref: "#/components/schemas/context" + context: + type: object + properties: + version: + type: string + description: "The version of this context. + The number are not sequential, but bigger numbers depict a newer object." + hub_version: + type: string + description: The version of this context, as known by hub. + family: + type: string + description: "The family of the context. When multiple charts of a context have different families, + the netdata server replaces the different parts with [x], so that the context can have only one family." + title: + type: string + description: "The title of the context. When multiple charts of a context have different titles, + the netdata server replaces the different parts with [x], so that the context can have only one title." + priority: + type: number + description: "The relative priority of the context. When multiple contexts have different priorities, + the minimum among them is selected as the priority of the context." + units: + type: string + description: "The unit of measurement for the values of all dimensions of the context. If multiple charts + of context have different units, the latest collected is selected." + chart_type: + type: string + description: The chart type. + enum: + - line + - area + - stacked + first_time_t: + type: number + description: The UNIX timestamp of the first entry (the oldest) in the database. + last_time_t: + type: number + description: The UNIX timestamp of the latest entry in the database. + charts: + type: object + description: "An object containing all the charts available for the chart. This is used as an indexed array. + For each pair in the dictionary, the key is the id of the chart and the value provides all details about + the chart." + alarm_variables: + type: object + properties: + chart: + type: string + description: The unique id of the chart. + chart_name: + type: string + description: The name of the chart. + cnart_context: + type: string + description: The context of the chart. It is shared across multiple monitored + software or hardware instances and used in alarm templates. + family: + type: string + description: The family of the chart. + host: + type: string + description: The host containing the chart. + chart_variables: + type: object + additionalProperties: + $ref: "#/components/schemas/chart_variables" + family_variables: + type: object + properties: + varname1: + type: number + format: float + varname2: + type: number + format: float + host_variables: + type: object + properties: + varname1: + type: number + format: float + varname2: + type: number + format: float + chart_variables: + type: object + properties: + varname1: + type: number + format: float + varname2: + type: number + format: float + jsonwrap2: + description: | + Data response with `format=json2` + type: object + properties: + api: + $ref: '#/components/schemas/api' + agents: + $ref: '#/components/schemas/agents' + versions: + $ref: '#/components/schemas/versions' + summary: + description: | + Summarized information about nodes, contexts, instances, labels, alerts, and dimensions. The items returned are determined by the scope of the query only, however the statistical data in them are influenced by the filters of the query. Using this information the dashboard allows users to slice and dice the data by filtering and grouping. + type: object + properties: + nodes: + type: array + items: + $ref: '#/components/schemas/nodeWithDataStatistics' + contexts: + type: array + items: + type: object + description: | + An object describing a unique context. `is` stands for instances, `ds` for dimensions, `al` for alerts, `sts` for statistics. + properties: + id: + description: the context id. + type: string + is: + $ref: "#/components/schemas/jsonwrap2_items_count" + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + al: + $ref: "#/components/schemas/jsonwrap2_alerts_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + instances: + type: array + items: + type: object + description: | + An object describing an instance. `ds` stands for dimensions, `al` for alerts, `sts` for statistics. + properties: + id: + description: the id of the instance. + type: string + nm: + description: the name of the instance (may be absent when it is the same with the id) + type: string + ni: + description: the node index id this instance belongs to. The UI uses this to compone the fully qualified name of the instance, using the node hostname to present it to users and its machine guid to add it to filters. + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + al: + $ref: "#/components/schemas/jsonwrap2_alerts_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + dimensions: + type: array + items: + type: object + description: | + An object describing a unique dimension. `ds` stands for `dimensions`, `sts` for statistics. + properties: + id: + description: the id of the dimension. + type: string + nm: + description: the name of the dimension (may be absent when it is the same with the id) + type: string + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + labels: + type: array + items: + type: object + description: | + An object describing a label key. `ds` stands for `dimensions`, `sts` for statistics. + properties: + id: + description: the key of the label. + type: string + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + vl: + description: | + An array of values for this key. + type: array + items: + type: object + properties: + id: + description: The value string + type: string + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + alerts: + description: | + An array of all the unique alerts running, grouped by alert name (`nm` is available here) + type: array + items: + $ref: "#/components/schemas/jsonwrap2_alerts_count" + totals: + type: object + properties: + nodes: + $ref: "#/components/schemas/jsonwrap2_items_count" + contexts: + $ref: "#/components/schemas/jsonwrap2_items_count" + instances: + $ref: "#/components/schemas/jsonwrap2_items_count" + dimensions: + $ref: "#/components/schemas/jsonwrap2_items_count" + label_keys: + $ref: "#/components/schemas/jsonwrap2_items_count" + label_key_values: + $ref: "#/components/schemas/jsonwrap2_items_count" + functions: + type: array + items: + type: string + db: + type: object + properties: + tiers: + description: | + The number of tiers this server is using. + type: integer + update_every: + description: | + The minimum update every, in seconds, for all tiers and all metrics aggregated into this query. + type: integer + first_entry: + description: | + The minimum unix epoch timestamp of the retention across all tiers for all metrics aggregated into this query. + type: integer + last_entry: + description: | + The maximum unix epoch timestamp of the retention across all tier for all metrics aggregated into this query. + type: integer + per_tier: + description: | + An array with information for each of the tiers available, related to this query. + type: array + items: + type: object + properties: + tier: + description: | + The tier number of this tier, starting at 0. + type: integer + queries: + description: | + The number of queries executed on this tier. Usually one query per metric is made, but the query may cross multiple tier, in which case more than one query per metric is made. + type: integer + points: + description: | + The number of points read from this tier. + type: integer + update_every: + description: | + The minimum resolution of all metrics queried on this tier. + type: integer + first_entry: + description: | + The minimum unix epoch timestamp available across all metrics that used this tier. This reflects the oldest timestamp of the tier's retention. + type: integer + last_entry: + description: | + The maximum unix epoch timestamp available across all metrics that used this tier. This reflects the newest timestamp of the tier's retention. + units: + description: | + The units of the database data + oneOf: + - type: string + - type: array + items: + type: string + dimensions: + type: object + properties: + ids: + description: | + An array with the dimension ids that uniquely identify the dimensions for this query. It is the same with `view.dimensions.ids`. + type: array + items: + type: string + units: + description: | + An array with the units each dimension has in the database (independent of group-by aggregation that may override the units). + type: array + items: + type: string + sts: + description: | + Statistics about the data collection points used for each dimension. + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + view: + type: object + properties: + title: + description: | + The title the chart should have. + type: string + format: + description: | + The format the `result` top level member has. Available on when `debug` flag is set. + type: string + options: + description: | + An array presenting all the options given to the query. Available on when `debug` flag is set. + type: array + items: + type: string + time_group: + description: | + The same as the parameter `time_group`. Available on when `debug` flag is set. + type: string + after: + description: | + The oldest unix epoch timestamp of the data returned in the `result`. + type: integer + before: + description: | + The newest unix epoch timestamp of the data returned in the `result`. + type: integer + partial_data_trimming: + description: | + Information related to trimming of the last few points of the `result`, that was required to remove (increasing) partial data. + Trimming is disabled when the `raw` option is given to the query. + This object is available only when the `debug` flag is set. + type: object + properties: + max_update_every: + description: | + The maximum `update_every` for all metrics aggregated into the query. + Trimming is by default enabled at `view.before - max_update_every`, but only when `view.before >= now - max_update_every`. + type: integer + expected_after: + description: | + The timestamp at which trimming can be enabled. + If this timestamp is greater or equal to `view.before`, there is no trimming. + type: integer + trimmed_after: + description: | + The timestamp at which trimming has been applied. + If this timestamp is greater or equal to `view.before`, there is no trimming. + points: + description: | + The number of points in `result`. Available only when `raw` is given. + type: integer + units: + description: | + The units of the query. + oneOf: + - type: string + - type: array + items: + type: string + chart_type: + description: | + The default chart type of the query. + type: string + enum: + - line + - area + - stacked + dimensions: + description: | + Detailed information about the chart dimensions included in the `result`. + type: object + properties: + grouped_by: + description: | + An array with the order of the groupings performed. + type: array + items: + type: string + enum: + - selected + - dimension + - instance + - node + - context + - units + - "label:key1" + - "label:key2" + - "label:keyN" + ids: + description: | + An array with the dimension ids that uniquely identify the dimensions for this query. + type: array + items: + type: string + names: + description: | + An array with the dimension names to be presented to users. Names may be overlapping, but IDs are not. + type: array + items: + type: string + priorities: + description: | + An array with the relative priorities of the dimensions. + Numbers may not be sequential or unique. The application is expected to order by this and then by name. + type: array + items: + type: integer + aggregated: + description: | + An array with the number of source metrics aggregated into each dimension. + type: array + items: + type: integer + units: + description: | + An array with the units each dimension has. + type: array + items: + type: string + sts: + description: | + Statistics about the view points for each dimension. + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + labels: + description: | + The labels associated with each dimension in the query. + This object is only available when the `group-by-labels` option is given to the query. + type: object + properties: + label_key1: + description: | + An array having one entry for each of the dimensions of the query. + type: array + items: + description: | + An array having one entry for each of the values this label key has for the given dimension. + type: array + items: + type: string + min: + description: | + The minimum value of all points included in the `result`. + type: number + max: + description: | + The maximum value of all points included in the `result`. + type: number + result: + $ref: '#/components/schemas/data_json_formats2' + timings: + type: object + jsonwrap2_sts: + description: | + Statistical values + type: object + properties: + min: + description: The minimum value of all metrics aggregated + type: number + max: + description: The maximum value of all metrics aggregated + type: number + avg: + description: The average value of all metrics aggregated + type: number + arp: + description: The average anomaly rate of all metrics aggregated + type: number + con: + description: The contribution percentage of all the metrics aggregated + type: number + jsonwrap2_sts_raw: + description: | + Statistical values when `raw` option is given. + type: object + properties: + min: + description: The minimum value of all metrics aggregated + type: number + max: + description: The maximum value of all metrics aggregated + type: number + sum: + description: The sum value of all metrics aggregated + type: number + ars: + description: The sum anomaly rate of all metrics aggregated + type: number + vol: + description: The volume of all the metrics aggregated + type: number + cnt: + description: The count of all metrics aggregated + type: integer + jsonwrap2_items_count: + description: | + Depending on the placement of this object, `items` may be `nodes`, `contexts`, `instances`, `dimensions`, `label keys`, `label key-value pairs`. Furthermore, if the whole object is missing it should be assumed that all its members are zero. + type: object + properties: + sl: + description: The number of items `selected` to query. If absent it is zero. + type: integer + ex: + description: The number of items `excluded` from querying. If absent it is zero. + type: integer + qr: + description: The number of items (out of `selected`) the query successfully `queried`. If absent it is zero. + type: integer + fl: + description: The number of items (from `selected`) that `failed` to be queried. If absent it is zero. + type: integer + jsonwrap2_alerts_count: + description: | + Counters about alert statuses. If this object is missing, it is assumed that all its members are zero. + type: object + properties: + nm: + description: The name of the alert. Can be absent when the counters refer to more than one alert instances. + type: string + cl: + description: The number of CLEAR alerts. If absent, it is zero. + type: integer + wr: + description: The number of WARNING alerts. If absent, it is zero. + type: integer + cr: + description: The number of CRITICAL alerts. If absent, it is zero. + type: integer + ot: + description: | + The number of alerts that are not CLEAR, WARNING, CRITICAL (so, they are "other"). If absent, it is zero. + type: integer + api: + description: The version of the API used. + type: integer + agents: + description: | + An array of agent definitions consulted to compose this response. + type: array + items: + type: object + properties: + mg: + description: The agent machine GUID. + type: string + format: uuid + nd: + description: The agent cloud node ID. + type: string + format: uuid + nm: + description: The agent hostname. + type: string + ai: + description: The agent index ID for this agent, in this response. + type: integer + now: + description: The current unix epoch timestamp of this agent. + type: integer + versions: + description: | + Hashes that allow the caller to detect important database changes of Netdata agents. + type: object + properties: + nodes_hard_hash: + description: | + An auto-increment value that reflects the number of changes to the number of nodes maintained by the server. Everytime a node is added or removed, this number gets incremented. + type: integer + contexts_hard_hash: + description: | + An auto-increment value that reflects the number of changes to the number of contexts maintained by the server. Everytime a context is added or removed, this number gets incremented. + type: integer + contexts_soft_hash: + description: | + An auto-increment value that reflects the number of changes to the queue that sends contexts updates to Netdata Cloud. Everytime the contents of a context are updated, this number gets incremented. + type: integer + alerts_hard_hash: + description: | + An auto-increment value that reflects the number of changes to the number of alerts. Everytime an alert is added or removed, this number gets incremented. + type: integer + alerts_soft_hash: + description: | + An auto-increment value that reflects the number of alerts transitions. Everytime an alert transitions to a new state, this number gets incremented. + type: integer + nodeBasic: + type: object + description: Basic information about a node. + required: + - ni + - st + properties: + mg: + description: The machine guid of the node. May not be available if the request is served by the Netdata Cloud. + type: string + format: UUID + nd: + description: The node id of the node. May not be available if the node is not registered to Netdata Cloud. + type: string + format: UUID + nm: + description: The name (hostname) of the node. + type: string + ni: + description: The node index id, a number that uniquely identifies this node for this query. + type: integer + st: + description: Status information about the communication with this node. + type: object + properties: + ai: + description: The agent index id that has been contacted for this node. + type: integer + code: + description: The HTTP response code of the response for this node. When working directly with an agent, this is always 200. If the `code` is missing, it should be assumed to be 200. + type: integer + msg: + description: A human readable description of the error, if any. If `msg` is missing, or is the empty string `""` or is `null`, there is no description associated with the current status. + type: string + ms: + description: The time in milliseconds this node took to respond, or if the local agent responded for this node, the time it needed to execute the query. If `ms` is missing, the time that was required to query this node is unknown. + type: number + nodeWithDataStatistics: + allOf: + - $ref: '#/components/schemas/nodeBasic' + - type: object + description: | + `is` stands for instances, `ds` for dimensions, `al` for alerts, `sts` for statistics. + properties: + is: + $ref: "#/components/schemas/jsonwrap2_items_count" + ds: + $ref: "#/components/schemas/jsonwrap2_items_count" + al: + $ref: "#/components/schemas/jsonwrap2_alerts_count" + sts: + oneOf: + - $ref: "#/components/schemas/jsonwrap2_sts" + - $ref: "#/components/schemas/jsonwrap2_sts_raw" + nodeFull: + allOf: + - $ref: '#/components/schemas/nodeBasic' + - type: object + properties: + version: + description: The version of the Netdata Agent the node runs. + type: string + hops: + description: How many hops away from the origin node, the queried one is. 0 means the agent itself is the origin node. + type: integer + state: + description: The current state of the node on this agent. + type: string + enum: + - reachable + - stale + - offline + context2Basic: + type: object + properties: + family: + type: string + priority: + type: integer + first_entry: + type: integer + last_entry: + type: integer + live: + type: boolean + contexts2: + description: | + `/api/v2/contexts` and `/api/v2/q` response about multi-node contexts hosted by a Netdata agent. + type: object + properties: + api: + $ref: '#/components/schemas/api' + agents: + $ref: '#/components/schemas/agents' + versions: + $ref: '#/components/schemas/versions' + contexts: + additionalProperties: + $ref: '#/components/schemas/context2Basic' + jsonwrap1: + type: object + discriminator: + propertyName: format + description: Response will contain the appropriate subtype, e.g. data_json depending + on the requested format. + properties: + api: + type: number + description: The API version this conforms to. + id: + type: string + description: The unique id of the chart. + name: + type: string + description: The name of the chart. + update_every: + type: number + description: The update frequency of this chart, in seconds. One value every this + amount of time is kept in the round robin database (independently of + the current view). + view_update_every: + type: number + description: The current view appropriate update frequency of this chart, in + seconds. There is no point to request chart refreshes, using the + same settings, more frequently than this. + first_entry: + type: number + description: The UNIX timestamp of the first entry (the oldest) in the round + robin database (independently of the current view). + last_entry: + type: number + description: The UNIX timestamp of the latest entry in the round robin database + (independently of the current view). + after: + type: number + description: The UNIX timestamp of the first entry (the oldest) returned in this + response. + before: + type: number + description: The UNIX timestamp of the latest entry returned in this response. + min: + type: number + description: The minimum value returned in the current view. This can be used to + size the y-series of the chart. + max: + type: number + description: The maximum value returned in the current view. This can be used to + size the y-series of the chart. + dimension_names: + description: The dimension names of the chart as returned in the current view. + type: array + items: + type: string + dimension_ids: + description: The dimension IDs of the chart as returned in the current view. + type: array + items: + type: string + latest_values: + description: The latest values collected for the chart (independently of the + current view). + type: array + items: + type: string + view_latest_values: + description: The latest values returned with this response. + type: array + items: + type: string + dimensions: + type: number + description: The number of dimensions returned. + points: + type: number + description: The number of rows / points returned. + format: + type: string + description: The format of the result returned. + chart_variables: + type: object + additionalProperties: + $ref: '#/components/schemas/chart_variables' + result: + $ref: '#/components/schemas/data_json_formats1' + data_json_formats1: + description: | + Depending on the `format` given to a data query, any of the following may be returned. + oneOf: + - $ref: '#/components/schemas/data_json' + - $ref: '#/components/schemas/data_datatable' + - $ref: '#/components/schemas/data_csvjsonarray' + - $ref: '#/components/schemas/data_array' + - $ref: '#/components/schemas/data_txt' + data_json_formats2: + description: | + Depending on the `format` given to a data query, any of the following may be returned. + oneOf: + - $ref: '#/components/schemas/data_json2' + - $ref: '#/components/schemas/data_json_formats1' + data_json2: + type: object + properties: + labels: + description: | + The IDs of the dimensions returned. The first is always `time`. + type: array + items: + type: string + point: + description: | + The format of each point returned. + type: object + properties: + value: + description: | + The index of the value in each point. + type: integer + arp: + description: | + The index of the anomaly rate in each point. + type: integer + pa: + description: | + The index of the point annotations in each point. + This is a bitmap. `EMPTY = 1`, `RESET = 2`, `PARTIAL = 4`. + `EMPTY` means the point has no value. + `RESET` means that at least one metric aggregated experienced an overflow (a counter that wrapped). + `PARTIAL` means that this point should have more metrics aggregated into it, but not all metrics had data. + type: integer + count: + description: | + The number of metrics aggregated into this point. + This exists only when the option `raw` is given to the query and the final aggregation point is NOT `percentage`. + type: integer + hidden: + description: | + The sum of the non-selected dimensions aggregated for this group item point. + This exists only when the option `raw` is given to the query and the final aggregation method is `percentage`. + data: + type: array + items: + allOf: + - type: integer + - type: array + data_json: + description: Data response in `json` format. + type: object + properties: + labels: + description: The dimensions retrieved from the chart. + type: array + items: + type: string + data: + description: | + The data requested, one element per sample with each element containing the values of the dimensions described in the labels value. + type: array + items: + type: number + data_txt: + description: | + Data response in `csv`, `tsv`, `tsv-excel`, `ssv`, `ssv-comma`, `markdown`, `html` formats. + type: string + data_array: + description: Data response in `array` format. + type: array + items: + type: number + data_csvjsonarray: + description: | + The first inner array contains strings showing the labels of each column, each subsequent array contains the values for each point in time. + type: array + items: + type: array + items: {} + data_datatable: + description: | + Data response in datatable / datasource formats (suitable for Google Charts). + type: object + properties: + cols: + type: array + items: + type: object + properties: + id: + description: Always empty - for future use. + label: + description: The dimension returned from the chart. + pattern: + description: Always empty - for future use. + type: + description: The type of data in the column / chart-dimension. + p: + description: Contains any annotations for the column. + required: + - id + - label + - pattern + - type + rows: + type: array + items: + type: object + properties: + c: + type: array + items: + properties: + v: + description: | + Each value in the row is represented by an object named `c` with five v fields: data, null, null, 0, the value. This format is fixed by the Google Charts API." + alarms: + type: object + properties: + hostname: + type: string + latest_alarm_log_unique_id: + type: integer + format: int32 + status: + type: boolean + now: + type: integer + format: int32 + alarms: + type: object + properties: + chart-name.alarm-name: + type: object + properties: + id: + type: integer + format: int32 + name: + type: string + description: Full alarm name. + chart: + type: string + family: + type: string + active: + type: boolean + description: Will be false only if the alarm is disabled in the + configuration. + disabled: + type: boolean + description: Whether the health check for this alarm has been disabled + via a health command API DISABLE command. + silenced: + type: boolean + description: Whether notifications for this alarm have been silenced via + a health command API SILENCE command. + exec: + type: string + recipient: + type: string + source: + type: string + units: + type: string + info: + type: string + status: + type: string + last_status_change: + type: integer + format: int32 + last_updated: + type: integer + format: int32 + next_update: + type: integer + format: int32 + update_every: + type: integer + format: int32 + delay_up_duration: + type: integer + format: int32 + delay_down_duration: + type: integer + format: int32 + delay_max_duration: + type: integer + format: int32 + delay_multiplier: + type: integer + format: int32 + delay: + type: integer + format: int32 + delay_up_to_timestamp: + type: integer + format: int32 + value_string: + type: string + no_clear_notification: + type: boolean + lookup_dimensions: + type: string + db_after: + type: integer + format: int32 + db_before: + type: integer + format: int32 + lookup_method: + type: string + lookup_after: + type: integer + format: int32 + lookup_before: + type: integer + format: int32 + lookup_options: + type: string + calc: + type: string + calc_parsed: + type: string + warn: + type: string + warn_parsed: + type: string + crit: + type: string + crit_parsed: + type: string + warn_repeat_every: + type: integer + format: int32 + crit_repeat_every: + type: integer + format: int32 + green: + type: string + format: nullable + red: + type: string + format: nullable + value: + type: number + alarm_log_entry: + type: object + properties: + hostname: + type: string + unique_id: + type: integer + format: int32 + alarm_id: + type: integer + format: int32 + alarm_event_id: + type: integer + format: int32 + name: + type: string + chart: + type: string + family: + type: string + processed: + type: boolean + updated: + type: boolean + exec_run: + type: integer + format: int32 + exec_failed: + type: boolean + exec: + type: string + recipient: + type: string + exec_code: + type: integer + format: int32 + source: + type: string + units: + type: string + when: + type: integer + format: int32 + duration: + type: integer + format: int32 + non_clear_duration: + type: integer + format: int32 + status: + type: string + old_status: + type: string + delay: + type: integer + format: int32 + delay_up_to_timestamp: + type: integer + format: int32 + updated_by_id: + type: integer + format: int32 + updates_id: + type: integer + format: int32 + value_string: + type: string + old_value_string: + type: string + silenced: + type: string + info: + type: string + value: + type: number + nullable: true + old_value: + type: number + nullable: true + alarms_values: + type: object + properties: + hostname: + type: string + alarms: + type: object + description: HashMap with keys being alarm names + additionalProperties: + type: object + properties: + id: + type: integer + value: + type: integer + last_updated: + type: integer + format: int32 + status: + type: string + enum: + - REMOVED + - UNDEFINED + - UNINITIALIZED + - CLEAR + - RAISED + - WARNING + - CRITICAL + - UNKNOWN + aclk_state: + type: object + properties: + aclk-available: + type: string + description: | + Describes whether this agent is capable of connection to the Cloud. False means agent has been built without ACLK component either on purpose (user choice) or due to missing dependency. + aclk-version: + type: integer + description: Describes which ACLK version is currently used. + protocols-supported: + type: array + description: List of supported protocols for communication with Cloud. + items: + type: string + agent-claimed: + type: boolean + description: Informs whether this agent has been added to a space in the cloud (User has to perform claiming). + If false (user didn't perform claiming) agent will never attempt any cloud connection. + claimed_id: + type: string + format: uuid + description: Unique ID this agent uses to identify when connecting to cloud + online: + type: boolean + description: Informs if this agent was connected to the cloud at the time this request has been processed. + used-cloud-protocol: + type: string + description: Informs which protocol is used to communicate with cloud + enum: + - Old + - New + metric_correlations: + type: object + properties: + after: + description: the start time of the highlighted window + type: integer + before: + description: the end time of the highlighted window + type: integer + duration: + description: the duration of the highlighted window + type: integer + points: + description: the points of the highlighted window + type: integer + baseline_after: + description: the start time of the baseline window + type: integer + baseline_before: + description: the end time of the baseline window + type: integer + baseline_duration: + description: the duration of the baseline window + type: integer + baseline_points: + description: the points of the baseline window + type: integer + group: + description: the grouping method across time + type: string + method: + description: the correlation method used + type: string + options: + description: a comma separated list of the query options set + type: string + correlated_dimensions: + description: the number of dimensions returned in the result + total_dimensions_count: + description: the total number of dimensions evaluated + type: integer + statistics: + type: object + properties: + query_time_ms: + type: number + db_queries: + type: integer + db_points_read: + type: integer + query_result_points: + type: integer + binary_searches: + type: integer + correlated_charts: + type: object + description: An object containing chart objects with their metrics correlations. + properties: + chart-id1: + type: object + properties: + context: + type: string + dimensions: + type: object + properties: + dimension1-name: + type: number + dimension2-name: + type: number + chart-id2: + type: object + properties: + context: + type: string + dimensions: + type: object + properties: + dimension1-name: + type: number + dimension2-name: + type: number + weights2: + type: object + weights: + type: object + properties: + after: + description: the start time of the highlighted window + type: integer + before: + description: the end time of the highlighted window + type: integer + duration: + description: the duration of the highlighted window + type: integer + points: + description: the points of the highlighted window + type: integer + baseline_after: + description: the start time of the baseline window + type: integer + baseline_before: + description: the end time of the baseline window + type: integer + baseline_duration: + description: the duration of the baseline window + type: integer + baseline_points: + description: the points of the baseline window + type: integer + group: + description: the grouping method across time + type: string + method: + description: the correlation method used + type: string + options: + description: a comma separated list of the query options set + type: string + correlated_dimensions: + description: the number of dimensions returned in the result + total_dimensions_count: + description: the total number of dimensions evaluated + type: integer + statistics: + type: object + properties: + query_time_ms: + type: number + db_queries: + type: integer + db_points_read: + type: integer + query_result_points: + type: integer + binary_searches: + type: integer + contexts: + description: A dictionary of weighted context objects. + type: object + additionalProperties: + $ref: '#/components/schemas/weighted_context' + weighted_context: + type: object + properties: + weight: + description: The average weight of the context. + type: number + charts: + description: A dictionary of weighted chart objects. + type: object + additionalProperties: + $ref: '#/components/schemas/weighted_chart' + weighted_chart: + type: object + properties: + weight: + description: The average weight of the context. + type: number + dimensions: + description: A dictionary of weighted dimensions. + type: object + additionalProperties: + $ref: '#/components/schemas/weighted_dimension' + weighted_dimension: + type: number + config_schema: + type: object + properties: + jsonSchema: + type: object + description: Standard JSON Schema object describing the schema of each configurable entity. + uiSchema: + type: object + description: Schema for react-json-schema-form to drive the UI. Provides additional UI-specific configuration. + config_tree: + type: object + properties: + version: + type: integer + description: The version of dynamic configuration supported by the Netdata agent. + tree: + type: object + description: A map of configuration entity paths, each containing one or more configurable entities. + additionalProperties: + type: object + additionalProperties: + $ref: '#/components/schemas/config_entity' + attention: + $ref: '#/components/schemas/config_attention' + config_entity: + type: object + properties: + type: + type: string + description: Can be 'single' for entities appearing once, 'template' for entities supporting multiple instances, or 'job' for jobs belonging to a template. + status: + type: string + description: The current status of the entity. Values include 'accepted', 'running', 'failed', 'disabled', 'incomplete', or 'orphan'. + cmds: + type: array + items: + type: string + description: An array of the possible actions supported by this entity. + source_type: + type: string + description: The source type of the configuration (e.g., 'internal', 'stock', 'user', 'discovered', 'dyncfg'). + source: + type: string + description: Additional information about the source, formatted as comma-separated name-value pairs. + sync: + type: boolean + description: Indicates if this is an internal module (true) or an external plugin (false). + user_disabled: + type: boolean + description: True if the entity is disabled by the user. + restart_required: + type: boolean + description: True if the entity requires a restart after addition or update. + plugin_rejected: + type: boolean + description: True if a previously saved configuration failed to apply after a restart. + payload: + type: object + description: Object containing at least an 'available' boolean indicating if there's a saved configuration for this entity. + properties: + available: + type: boolean + saves: + type: integer + description: The number of times this configuration has been saved to disk by the dynamic configuration manager. + created_ut: + type: integer + format: int64 + description: The timestamp in microseconds when this dynamic configuration was first created. + modified_ut: + type: integer + format: int64 + description: The timestamp in microseconds when this dynamic configuration was last modified. + template: + type: string + description: Shows the template the job belongs to, applicable when type is 'job'. + config_attention: + type: object + properties: + degraded: + type: boolean + restart_required: + type: integer + plugin_rejected: + type: integer + status_failed: + type: integer + status_incomplete: + type: integer + config_default_response: + type: object + properties: + status: + type: integer + description: The HTTP status code of the response. + message: + type: string + description: A descriptive message about the response or the action taken. + data: + type: object + description: The data payload of the response, contents vary depending on the specific request and action. + additionalProperties: true diff --git a/src/web/api/queries/README.md b/src/web/api/queries/README.md new file mode 100644 index 000000000..e95dfbbc3 --- /dev/null +++ b/src/web/api/queries/README.md @@ -0,0 +1,181 @@ +# Database queries/lookup + +This document explains in detail the options available to retrieve data from the Netdata timeseries database in order to configure alerts, create badges or +create custom charts. + +The Netdata database can be queried with the `/api/v1/data` and `/api/v1/badge.svg` REST API methods. The database is also queried from the `lookup` line +in an [alert configuration](https://github.com/netdata/netdata/blob/master/src/health/REFERENCE.md). + +Every data query accepts the following parameters: + +|name|required|description| +|:--:|:------:|:----------| +|`chart`|yes|The chart to be queried.| +|`points`|no|The number of points to be returned. Netdata can reduce number of points by applying query grouping methods. If not given, the result will have the same granularity as the database (although this relates to `gtime`).| +|`before`|no|The absolute timestamp or the relative (to now) time the query should finish evaluating data. If not given, it defaults to the timestamp of the latest point in the database.| +|`after`|no|The absolute timestamp or the relative (to `before`) time the query should start evaluating data. if not given, it defaults to the timestamp of the oldest point in the database.| +|`group`|no|The grouping method to use when reducing the points the database has. If not given, it defaults to `average`.| +|`gtime`|no|A resampling period to change the units of the metrics (i.e. setting this to `60` will convert `per second` metrics to `per minute`. If not given it defaults to granularity of the database.| +|`options`|no|A bitmap of options that can affect the operation of the query. Only 2 options are used by the query engine: `unaligned` and `percentage`. All the other options are used by the output formatters. The default is to return aligned data.| +|`dimensions`|no|A simple pattern to filter the dimensions to be queried. The default is to return all the dimensions of the chart.| + +## Operation + +The query engine works as follows (in this order): + +#### Time-frame + +`after` and `before` define a time-frame, accepting: + +- **absolute timestamps** (unix timestamps, i.e. seconds since epoch). + +- **relative timestamps**: + + `before` is relative to now and `after` is relative to `before`. + + Example: `before=-60&after=-60` evaluates to the time-frame from -120 up to -60 seconds in + the past, relative to the latest entry of the database of the chart. + +The engine verifies that the time-frame requested is available at the database: + +- If the requested time-frame overlaps with the database, the excess requested + will be truncated. + +- If the requested time-frame does not overlap with the database, the engine will + return an empty data set. + +At the end of this operation, `after` and `before` are absolute timestamps. + +#### Data grouping + +Database points grouping is applied when the caller requests a time-frame to be +expressed with fewer points, compared to what is available at the database. + +There are 2 uses that enable this feature: + +- The caller requests a specific number of `points` to be returned. + + For example, for a time-frame of 10 minutes, the database has 600 points (1/sec), + while the caller requested these 10 minutes to be expressed in 200 points. + + This feature is used by Netdata dashboards when you zoom-out the charts. + The dashboard is requesting the number of points the user's screen has. + This saves bandwidth and speeds up the browser (fewer points to evaluate for drawing the charts). +- The caller requests a **re-sampling** of the database, by setting `gtime` to any value + above the granularity of the chart. + + For example, the chart's units is `requests/sec` and caller wants `requests/min`. + +Using `points` and `gtime` the query engine tries to find a best fit for **database-points** +vs **result-points** (we call this ratio `group points`). It always tries to keep `group points` +an integer. Keep in mind the query engine may shift `after` if required. See also the [example](#example). + +#### Time-frame Alignment + +Alignment is a very important aspect of Netdata queries. Without it, the animated +charts on the dashboards would constantly [change shape](#example) during incremental updates. + +To provide consistent grouping through time, the query engine (by default) aligns +`after` and `before` to be a multiple of `group points`. + +For example, if `group points` is 60 and alignment is enabled, the engine will return +each point with durations XX:XX:00 - XX:XX:59, matching whole minutes. + +To disable alignment, pass `&options=unaligned` to the query. + +#### Query Execution + +To execute the query, the engine evaluates all dimensions of the chart, one after another. + +The engine does not evaluate dimensions that do not match the [simple pattern](https://github.com/netdata/netdata/blob/master/src/libnetdata/simple_pattern/README.md) +given at the `dimensions` parameter, except when `options=percentage` is given (this option +requires all the dimensions to be evaluated to find the percentage of each dimension vs to chart +total). + +For each dimension, it starts evaluating values starting at `after` (not inclusive) towards +`before` (inclusive). + +For each value it calls the **grouping method** given with the `&group=` query parameter +(the default is `average`). + +## Grouping methods + +The following grouping methods are supported. These are given all the values in the time-frame +and they group the values every `group points`. + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=min&after=-60&label=min&value_color=blue) finds the minimum value +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=max&after=-60&label=max&value_color=lightblue) finds the maximum value +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=average&after=-60&label=average&value_color=yellow) finds the average value +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=sum&units=kilobits&after=-60&label=sum&value_color=orange) adds all the values and returns the sum +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=median&after=-60&label=median&value_color=red) sorts the values and returns the value in the middle of the list +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=stddev&after=-60&label=stddev&value_color=green) finds the standard deviation of the values +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=cv&after=-60&label=cv&units=pcent&value_color=yellow) finds the relative standard deviation (coefficient of variation) of the values +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=ses&after=-60&label=ses&value_color=brown) finds the exponential weighted moving average of the values +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=des&after=-60&label=des&value_color=blue) applies Holt-Winters double exponential smoothing +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=net.eth0&options=unaligned&dimensions=received&group=incremental_sum&after=-60&label=incremental_sum&value_color=red) finds the difference of the last vs the first value + +The examples shown above show live information from the `received` traffic on the `eth0` interface of the global Netdata registry. +Inspect any of the badges to see the parameters provided. You can directly issue the request to the registry server's API yourself, e.g. by +passing the following to get the value shown on the badge for the sum of the values within the period: + +``` +https://registry.my-netdata.io/api/v1/data?chart=net.eth0&options=unaligned&dimensions=received&group=sum&units=kilobits&after=-60&label=sum&points=1 +``` + +## Further processing + +The result of the query engine is always a structure that has dimensions and values +for each dimension. + +Formatting modules are then used to convert this result in many different formats and return it +to the caller. + +## Performance + +The query engine is highly optimized for speed. Most of its modules implement "online" +versions of the algorithms, requiring just one pass on the database values to produce +the result. + +## Example + +When Netdata is reducing metrics, it tries to return always the same boundaries. So, if we want 10s averages, it will always return points starting at a `unix timestamp % 10 = 0`. + +Let's see why this is needed, by looking at the error case. + +Assume we have 5 points: + +|time|value| +|:--:|:---:| +|00:01|1| +|00:02|2| +|00:03|3| +|00:04|4| +|00:05|5| + +At 00:04 you ask for 2 points for 4 seconds in the past. So `group = 2`. Netdata would return: + +|point|time|value| +|:---:|:--:|:---:| +|1|00:01 - 00:02|1.5| +|2|00:03 - 00:04|3.5| + +A second later the chart is to be refreshed, and makes again the same request at 00:05. These are the points that would have been returned: + +|point|time|value| +|:---:|:--:|:---:| +|1|00:02 - 00:03|2.5| +|2|00:04 - 00:05|4.5| + +**Wait a moment!** The chart was shifted just one point and it changed value! Point 2 was 3.5 and when shifted to point 1 is 2.5! If you see this in a chart, it's a mess. The charts change shape constantly. + +For this reason, Netdata always aligns the data it returns to the `group`. + +When you request `points=1`, Netdata understands that you need 1 point for the whole database, so `group = 3600`. Then it tries to find the starting point which would be `timestamp % 3600 = 0` Within a database of 3600 seconds, there is one such point for sure. Then it tries to find the average of 3600 points. But, most probably it will not find 3600 of them (for just 1 out of 3600 seconds this query will return something). + +So, the proper way to query the database is to also set at least `after`. The following call will returns 1 point for the last complete 10-second duration (it starts at `timestamp % 10 = 0`): + +<http://netdata.firehol.org/api/v1/data?chart=system.cpu&points=1&after=-10&options=seconds> + +When you keep calling this URL, you will see that it returns one new value every 10 seconds, and the timestamp always ends with zero. Similarly, if you say `points=1&after=-5` it will always return timestamps ending with 0 or 5. + + diff --git a/src/web/api/queries/average/README.md b/src/web/api/queries/average/README.md new file mode 100644 index 000000000..1ad78bee5 --- /dev/null +++ b/src/web/api/queries/average/README.md @@ -0,0 +1,50 @@ +<!-- +title: "Average or Mean" +sidebar_label: "Average or Mean" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/average/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Average or Mean + +> This query is available as `average` and `mean`. + +An average is a single number taken as representative of a list of numbers. + +It is calculated as: + +``` +average = sum(numbers) / count(numbers) +``` + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: average -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`average` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=average` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Average>. + + diff --git a/src/web/api/queries/average/average.c b/src/web/api/queries/average/average.c new file mode 100644 index 000000000..f54dcb243 --- /dev/null +++ b/src/web/api/queries/average/average.c @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "average.h" + diff --git a/src/web/api/queries/average/average.h b/src/web/api/queries/average/average.h new file mode 100644 index 000000000..2d77cc571 --- /dev/null +++ b/src/web/api/queries/average/average.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_AVERAGE_H +#define NETDATA_API_QUERY_AVERAGE_H + +#include "../query.h" +#include "../rrdr.h" + +// ---------------------------------------------------------------------------- +// average + +struct tg_average { + NETDATA_DOUBLE sum; + size_t count; +}; + +static inline void tg_average_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_average)); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_average_reset(RRDR *r) { + struct tg_average *g = (struct tg_average *)r->time_grouping.data; + g->sum = 0; + g->count = 0; +} + +static inline void tg_average_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_average_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_average *g = (struct tg_average *)r->time_grouping.data; + g->sum += value; + g->count++; +} + +static inline NETDATA_DOUBLE tg_average_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_average *g = (struct tg_average *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + if(unlikely(r->time_grouping.resampling_group != 1)) + value = g->sum / r->time_grouping.resampling_divisor; + else + value = g->sum / g->count; + } + + g->sum = 0.0; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_AVERAGE_H diff --git a/src/web/api/queries/countif/README.md b/src/web/api/queries/countif/README.md new file mode 100644 index 000000000..a40535395 --- /dev/null +++ b/src/web/api/queries/countif/README.md @@ -0,0 +1,40 @@ +<!-- +title: "CountIf" +sidebar_label: "CountIf" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/countif/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# CountIf + +> This query is available as `countif`. + +CountIf returns the percentage of points in the database that satisfy the condition supplied. + +The following conditions are available: + +- `!` or `!=` or `<>`, different than +- `=` or `:`, equal to +- `>`, greater than +- `<`, less than +- `>=`, greater or equal to +- `<=`, less or equal to + +The target number and the desired condition can be set using the `group_options` query parameter, as a string, like in these examples: + +- `!0`, to match any number except zero. +- `>=-3` to match any number bigger or equal to -3. + +. When an invalid condition is given, the web server can deliver a not accurate response. + +## how to use + +This query cannot be used in alerts. + +`countif` changes the units of charts. The result of the calculation is always from zero to 1, expressing the percentage of database points that matched the condition. + +In APIs and badges can be used like this: `&group=countif&group_options=>10` in the URL. + + diff --git a/src/web/api/queries/countif/countif.c b/src/web/api/queries/countif/countif.c new file mode 100644 index 000000000..8a3a1f50b --- /dev/null +++ b/src/web/api/queries/countif/countif.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "countif.h" + +// ---------------------------------------------------------------------------- +// countif + diff --git a/src/web/api/queries/countif/countif.h b/src/web/api/queries/countif/countif.h new file mode 100644 index 000000000..896b9d873 --- /dev/null +++ b/src/web/api/queries/countif/countif.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_COUNTIF_H +#define NETDATA_API_QUERY_COUNTIF_H + +#include "../query.h" +#include "../rrdr.h" + +enum tg_countif_cmp { + TG_COUNTIF_EQUAL, + TG_COUNTIF_NOTEQUAL, + TG_COUNTIF_LESS, + TG_COUNTIF_LESSEQUAL, + TG_COUNTIF_GREATER, + TG_COUNTIF_GREATEREQUAL, +}; + +struct tg_countif { + enum tg_countif_cmp comparison; + NETDATA_DOUBLE target; + size_t count; + size_t matched; +}; + +static inline void tg_countif_create(RRDR *r, const char *options __maybe_unused) { + struct tg_countif *g = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_countif)); + r->time_grouping.data = g; + + if(options && *options) { + // skip any leading spaces + while(isspace(*options)) options++; + + // find the comparison function + switch(*options) { + case '!': + options++; + if(*options != '=' && *options != ':') + options--; + g->comparison = TG_COUNTIF_NOTEQUAL; + break; + + case '>': + options++; + if(*options == '=' || *options == ':') { + g->comparison = TG_COUNTIF_GREATEREQUAL; + } + else { + options--; + g->comparison = TG_COUNTIF_GREATER; + } + break; + + case '<': + options++; + if(*options == '>') { + g->comparison = TG_COUNTIF_NOTEQUAL; + } + else if(*options == '=' || *options == ':') { + g->comparison = TG_COUNTIF_LESSEQUAL; + } + else { + options--; + g->comparison = TG_COUNTIF_LESS; + } + break; + + default: + case '=': + case ':': + g->comparison = TG_COUNTIF_EQUAL; + break; + } + if(*options) options++; + + // skip everything up to the first digit + while(isspace(*options)) options++; + + g->target = str2ndd(options, NULL); + } + else { + g->target = 0.0; + g->comparison = TG_COUNTIF_EQUAL; + } +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_countif_reset(RRDR *r) { + struct tg_countif *g = (struct tg_countif *)r->time_grouping.data; + g->matched = 0; + g->count = 0; +} + +static inline void tg_countif_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_countif_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_countif *g = (struct tg_countif *)r->time_grouping.data; + switch(g->comparison) { + case TG_COUNTIF_GREATER: + if(value > g->target) g->matched++; + break; + + case TG_COUNTIF_GREATEREQUAL: + if(value >= g->target) g->matched++; + break; + + case TG_COUNTIF_LESS: + if(value < g->target) g->matched++; + break; + + case TG_COUNTIF_LESSEQUAL: + if(value <= g->target) g->matched++; + break; + + case TG_COUNTIF_EQUAL: + if(value == g->target) g->matched++; + break; + + case TG_COUNTIF_NOTEQUAL: + if(value != g->target) g->matched++; + break; + } + g->count++; +} + +static inline NETDATA_DOUBLE tg_countif_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_countif *g = (struct tg_countif *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = (NETDATA_DOUBLE)g->matched * 100 / (NETDATA_DOUBLE)g->count; + } + + g->matched = 0; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_COUNTIF_H diff --git a/src/web/api/queries/des/README.md b/src/web/api/queries/des/README.md new file mode 100644 index 000000000..6dc19e732 --- /dev/null +++ b/src/web/api/queries/des/README.md @@ -0,0 +1,77 @@ +<!-- +title: "double exponential smoothing" +sidebar_label: "double exponential smoothing" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/des/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# double exponential smoothing + +Exponential smoothing is one of many window functions commonly applied to smooth data in signal +processing, acting as low-pass filters to remove high frequency noise. + +Simple exponential smoothing does not do well when there is a trend in the data. +In such situations, several methods were devised under the name "double exponential smoothing" +or "second-order exponential smoothing.", which is the recursive application of an exponential +filter twice, thus being termed "double exponential smoothing". + +In simple terms, this is like an average value, but more recent values are given more weight +and the trend of the values influences significantly the result. + +> **IMPORTANT** +> +> It is common for `des` to provide "average" values that far beyond the minimum or the maximum +> values found in the time-series. +> `des` estimates these values because of it takes into account the trend. + +This module implements the "Holt-Winters double exponential smoothing". + +Netdata automatically adjusts the weight (`alpha`) and the trend (`beta`) based on the number +of values processed, using the formula: + +``` +window = max(number of values, 15) +alpha = 2 / (window + 1) +beta = 2 / (window + 1) +``` + +You can change the fixed value `15` by setting in `netdata.conf`: + +``` +[web] + des max window = 15 +``` + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: des -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`des` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=des` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average&value_color=yellow) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=ses&after=-60&label=single+exponential+smoothing&value_color=yellow) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=des&after=-60&label=double+exponential+smoothing&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Exponential_smoothing>. + + diff --git a/src/web/api/queries/des/des.c b/src/web/api/queries/des/des.c new file mode 100644 index 000000000..d0e234e23 --- /dev/null +++ b/src/web/api/queries/des/des.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <web/api/queries/rrdr.h> +#include "des.h" + + +// ---------------------------------------------------------------------------- +// single exponential smoothing diff --git a/src/web/api/queries/des/des.h b/src/web/api/queries/des/des.h new file mode 100644 index 000000000..3153d497c --- /dev/null +++ b/src/web/api/queries/des/des.h @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_DES_H +#define NETDATA_API_QUERIES_DES_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_des { + NETDATA_DOUBLE alpha; + NETDATA_DOUBLE alpha_other; + NETDATA_DOUBLE beta; + NETDATA_DOUBLE beta_other; + + NETDATA_DOUBLE level; + NETDATA_DOUBLE trend; + + size_t count; +}; + +static size_t tg_des_max_window_size = 15; + +static inline void tg_des_init(void) { + long long ret = config_get_number(CONFIG_SECTION_WEB, "des max tg_des_window", (long long)tg_des_max_window_size); + if(ret <= 1) { + config_set_number(CONFIG_SECTION_WEB, "des max tg_des_window", (long long)tg_des_max_window_size); + } + else { + tg_des_max_window_size = (size_t) ret; + } +} + +static inline NETDATA_DOUBLE tg_des_window(RRDR *r, struct tg_des *g) { + (void)g; + + NETDATA_DOUBLE points; + if(r->view.group == 1) { + // provide a running DES + points = (NETDATA_DOUBLE)r->time_grouping.points_wanted; + } + else { + // provide a SES with flush points + points = (NETDATA_DOUBLE)r->view.group; + } + + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // A commonly used value for alpha is 2 / (N + 1) + return (points > (NETDATA_DOUBLE)tg_des_max_window_size) ? (NETDATA_DOUBLE)tg_des_max_window_size : points; +} + +static inline void tg_des_set_alpha(RRDR *r, struct tg_des *g) { + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // A commonly used value for alpha is 2 / (N + 1) + + g->alpha = 2.0 / (tg_des_window(r, g) + 1.0); + g->alpha_other = 1.0 - g->alpha; + + //info("alpha for chart '%s' is " CALCULATED_NUMBER_FORMAT, r->st->name, g->alpha); +} + +static inline void tg_des_set_beta(RRDR *r, struct tg_des *g) { + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // A commonly used value for alpha is 2 / (N + 1) + + g->beta = 2.0 / (tg_des_window(r, g) + 1.0); + g->beta_other = 1.0 - g->beta; + + //info("beta for chart '%s' is " CALCULATED_NUMBER_FORMAT, r->st->name, g->beta); +} + +static inline void tg_des_create(RRDR *r, const char *options __maybe_unused) { + struct tg_des *g = (struct tg_des *)onewayalloc_mallocz(r->internal.owa, sizeof(struct tg_des)); + tg_des_set_alpha(r, g); + tg_des_set_beta(r, g); + g->level = 0.0; + g->trend = 0.0; + g->count = 0; + r->time_grouping.data = g; +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_des_reset(RRDR *r) { + struct tg_des *g = (struct tg_des *)r->time_grouping.data; + g->level = 0.0; + g->trend = 0.0; + g->count = 0; + + // fprintf(stderr, "\nDES: "); + +} + +static inline void tg_des_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_des_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_des *g = (struct tg_des *)r->time_grouping.data; + + if(likely(g->count > 0)) { + // we have at least a number so far + + if(unlikely(g->count == 1)) { + // the second value we got + g->trend = value - g->trend; + g->level = value; + } + + // for the values, except the first + NETDATA_DOUBLE last_level = g->level; + g->level = (g->alpha * value) + (g->alpha_other * (g->level + g->trend)); + g->trend = (g->beta * (g->level - last_level)) + (g->beta_other * g->trend); + } + else { + // the first value we got + g->level = g->trend = value; + } + + g->count++; + + //fprintf(stderr, "value: " CALCULATED_NUMBER_FORMAT ", level: " CALCULATED_NUMBER_FORMAT ", trend: " CALCULATED_NUMBER_FORMAT "\n", value, g->level, g->trend); +} + +static inline NETDATA_DOUBLE tg_des_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_des *g = (struct tg_des *)r->time_grouping.data; + + if(unlikely(!g->count || !netdata_double_isnumber(g->level))) { + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + return 0.0; + } + + //fprintf(stderr, " RESULT for %zu values = " CALCULATED_NUMBER_FORMAT " \n", g->count, g->level); + + return g->level; +} + +#endif //NETDATA_API_QUERIES_DES_H diff --git a/src/web/api/queries/incremental_sum/README.md b/src/web/api/queries/incremental_sum/README.md new file mode 100644 index 000000000..6f02abe7d --- /dev/null +++ b/src/web/api/queries/incremental_sum/README.md @@ -0,0 +1,45 @@ +<!-- +title: "Incremental Sum (`incremental_sum`)" +sidebar_label: "Incremental Sum (`incremental_sum`)" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/incremental_sum/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Incremental Sum (`incremental_sum`) + +This modules finds the incremental sum of a period, which `last value - first value`. + +The result may be positive (rising) or negative (falling) depending on the first and last values. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: incremental_sum -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`incremental_sum` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=incremental_sum` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=incremental_sum&after=-60&label=incremental+sum&value_color=orange) + +## References + +- none + + diff --git a/src/web/api/queries/incremental_sum/incremental_sum.c b/src/web/api/queries/incremental_sum/incremental_sum.c new file mode 100644 index 000000000..88072f297 --- /dev/null +++ b/src/web/api/queries/incremental_sum/incremental_sum.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "incremental_sum.h" + +// ---------------------------------------------------------------------------- +// incremental sum + diff --git a/src/web/api/queries/incremental_sum/incremental_sum.h b/src/web/api/queries/incremental_sum/incremental_sum.h new file mode 100644 index 000000000..f110c5861 --- /dev/null +++ b/src/web/api/queries/incremental_sum/incremental_sum.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_INCREMENTAL_SUM_H +#define NETDATA_API_QUERY_INCREMENTAL_SUM_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_incremental_sum { + NETDATA_DOUBLE first; + NETDATA_DOUBLE last; + size_t count; +}; + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_incremental_sum_reset(RRDR *r) { + struct tg_incremental_sum *g = (struct tg_incremental_sum *)r->time_grouping.data; + g->first = NAN; + g->last = NAN; + g->count = 0; +} + +static inline void tg_incremental_sum_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_mallocz(r->internal.owa, sizeof(struct tg_incremental_sum)); + tg_incremental_sum_reset(r); +} + +static inline void tg_incremental_sum_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_incremental_sum_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_incremental_sum *g = (struct tg_incremental_sum *)r->time_grouping.data; + + if(unlikely(!g->count)) { + if(isnan(g->first)) + g->first = value; + else + g->last = value; + + g->count++; + } + else { + g->last = value; + g->count++; + } +} + +static inline NETDATA_DOUBLE tg_incremental_sum_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_incremental_sum *g = (struct tg_incremental_sum *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count || isnan(g->first) || isnan(g->last))) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = g->last - g->first; + } + + g->first = g->last; + g->last = NAN; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_INCREMENTAL_SUM_H diff --git a/src/web/api/queries/max/README.md b/src/web/api/queries/max/README.md new file mode 100644 index 000000000..ae634e05e --- /dev/null +++ b/src/web/api/queries/max/README.md @@ -0,0 +1,42 @@ +<!-- +title: "Max" +sidebar_label: "Max" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/max/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Max + +This module finds the max value in the time-frame given. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: max -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`max` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=max` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max&value_color=orange) + +## References + +- <https://en.wikipedia.org/wiki/Sample_maximum_and_minimum>. + + diff --git a/src/web/api/queries/max/max.c b/src/web/api/queries/max/max.c new file mode 100644 index 000000000..cc5999a29 --- /dev/null +++ b/src/web/api/queries/max/max.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "max.h" + +// ---------------------------------------------------------------------------- +// max + diff --git a/src/web/api/queries/max/max.h b/src/web/api/queries/max/max.h new file mode 100644 index 000000000..c26bb79ad --- /dev/null +++ b/src/web/api/queries/max/max.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_MAX_H +#define NETDATA_API_QUERY_MAX_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_max { + NETDATA_DOUBLE max; + size_t count; +}; + +static inline void tg_max_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_max)); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_max_reset(RRDR *r) { + struct tg_max *g = (struct tg_max *)r->time_grouping.data; + g->max = 0; + g->count = 0; +} + +static inline void tg_max_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_max_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_max *g = (struct tg_max *)r->time_grouping.data; + + if(!g->count || fabsndd(value) > fabsndd(g->max)) { + g->max = value; + g->count++; + } +} + +static inline NETDATA_DOUBLE tg_max_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_max *g = (struct tg_max *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = g->max; + } + + g->max = 0.0; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_MAX_H diff --git a/src/web/api/queries/median/README.md b/src/web/api/queries/median/README.md new file mode 100644 index 000000000..e6f6c04e7 --- /dev/null +++ b/src/web/api/queries/median/README.md @@ -0,0 +1,64 @@ +<!-- +title: "Median" +sidebar_label: "Median" +description: "Use median in API queries and health entities to find the 'middle' value from a sample, eliminating any unwanted spikes in the returned metrics." +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/median/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Median + +The median is the value separating the higher half from the lower half of a data sample +(a population or a probability distribution). For a data set, it may be thought of as the +"middle" value. + +`median` is not an accurate average. However, it eliminates all spikes, by sorting +all the values in a period, and selecting the value in the middle of the sorted array. + +Netdata also supports `trimmed-median`, which trims a percentage of the smaller and bigger values prior to finding the +median. The following `trimmed-median` functions are defined: + +- `trimmed-median1` +- `trimmed-median2` +- `trimmed-median3` +- `trimmed-median5` +- `trimmed-median10` +- `trimmed-median15` +- `trimmed-median20` +- `trimmed-median25` + +The function `trimmed-median` is an alias for `trimmed-median5`. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: median -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`median` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=median` in the URL. Additionally, a percentage may be given with +`&group_options=` to trim all small and big values before finding the median. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=median&after=-60&label=median&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Median>. + + diff --git a/src/web/api/queries/median/median.c b/src/web/api/queries/median/median.c new file mode 100644 index 000000000..9865b485c --- /dev/null +++ b/src/web/api/queries/median/median.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "median.h" + +// ---------------------------------------------------------------------------- +// median diff --git a/src/web/api/queries/median/median.h b/src/web/api/queries/median/median.h new file mode 100644 index 000000000..3d6d35925 --- /dev/null +++ b/src/web/api/queries/median/median.h @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_MEDIAN_H +#define NETDATA_API_QUERIES_MEDIAN_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_median { + size_t series_size; + size_t next_pos; + NETDATA_DOUBLE percent; + + NETDATA_DOUBLE *series; +}; + +static inline void tg_median_create_internal(RRDR *r, const char *options, NETDATA_DOUBLE def) { + long entries = r->view.group; + if(entries < 10) entries = 10; + + struct tg_median *g = (struct tg_median *)onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_median)); + g->series = onewayalloc_mallocz(r->internal.owa, entries * sizeof(NETDATA_DOUBLE)); + g->series_size = (size_t)entries; + + g->percent = def; + if(options && *options) { + g->percent = str2ndd(options, NULL); + if(!netdata_double_isnumber(g->percent)) g->percent = 0.0; + if(g->percent < 0.0) g->percent = 0.0; + if(g->percent > 50.0) g->percent = 50.0; + } + + g->percent = g->percent / 100.0; + r->time_grouping.data = g; +} + +static inline void tg_median_create(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 0.0); +} +static inline void tg_median_create_trimmed_1(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 1.0); +} +static inline void tg_median_create_trimmed_2(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 2.0); +} +static inline void tg_median_create_trimmed_3(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 3.0); +} +static inline void tg_median_create_trimmed_5(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 5.0); +} +static inline void tg_median_create_trimmed_10(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 10.0); +} +static inline void tg_median_create_trimmed_15(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 15.0); +} +static inline void tg_median_create_trimmed_20(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 20.0); +} +static inline void tg_median_create_trimmed_25(RRDR *r, const char *options) { + tg_median_create_internal(r, options, 25.0); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_median_reset(RRDR *r) { + struct tg_median *g = (struct tg_median *)r->time_grouping.data; + g->next_pos = 0; +} + +static inline void tg_median_free(RRDR *r) { + struct tg_median *g = (struct tg_median *)r->time_grouping.data; + if(g) onewayalloc_freez(r->internal.owa, g->series); + + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_median_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_median *g = (struct tg_median *)r->time_grouping.data; + + if(unlikely(g->next_pos >= g->series_size)) { + g->series = onewayalloc_doublesize( r->internal.owa, g->series, g->series_size * sizeof(NETDATA_DOUBLE)); + g->series_size *= 2; + } + + g->series[g->next_pos++] = value; +} + +static inline NETDATA_DOUBLE tg_median_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_median *g = (struct tg_median *)r->time_grouping.data; + + size_t available_slots = g->next_pos; + NETDATA_DOUBLE value; + + if(unlikely(!available_slots)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else if(available_slots == 1) { + value = g->series[0]; + } + else { + sort_series(g->series, available_slots); + + size_t start_slot = 0; + size_t end_slot = available_slots - 1; + + if(g->percent > 0.0) { + NETDATA_DOUBLE min = g->series[0]; + NETDATA_DOUBLE max = g->series[available_slots - 1]; + NETDATA_DOUBLE delta = (max - min) * g->percent; + + NETDATA_DOUBLE wanted_min = min + delta; + NETDATA_DOUBLE wanted_max = max - delta; + + for (start_slot = 0; start_slot < available_slots; start_slot++) + if (g->series[start_slot] >= wanted_min) break; + + for (end_slot = available_slots - 1; end_slot > start_slot; end_slot--) + if (g->series[end_slot] <= wanted_max) break; + } + + if(start_slot == end_slot) + value = g->series[start_slot]; + else + value = median_on_sorted_series(&g->series[start_slot], end_slot - start_slot + 1); + } + + if(unlikely(!netdata_double_isnumber(value))) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + + //log_series_to_stderr(g->series, g->next_pos, value, "median"); + + g->next_pos = 0; + + return value; +} + +#endif //NETDATA_API_QUERIES_MEDIAN_H diff --git a/src/web/api/queries/min/README.md b/src/web/api/queries/min/README.md new file mode 100644 index 000000000..35acb8c9e --- /dev/null +++ b/src/web/api/queries/min/README.md @@ -0,0 +1,42 @@ +<!-- +title: "Min" +sidebar_label: "Min" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/min/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Min + +This module finds the min value in the time-frame given. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: min -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`min` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=min` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Sample_maximum_and_minimum>. + + diff --git a/src/web/api/queries/min/min.c b/src/web/api/queries/min/min.c new file mode 100644 index 000000000..cefa7cf31 --- /dev/null +++ b/src/web/api/queries/min/min.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "min.h" + +// ---------------------------------------------------------------------------- +// min + diff --git a/src/web/api/queries/min/min.h b/src/web/api/queries/min/min.h new file mode 100644 index 000000000..3c53dfd1d --- /dev/null +++ b/src/web/api/queries/min/min.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_MIN_H +#define NETDATA_API_QUERY_MIN_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_min { + NETDATA_DOUBLE min; + size_t count; +}; + +static inline void tg_min_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_min)); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_min_reset(RRDR *r) { + struct tg_min *g = (struct tg_min *)r->time_grouping.data; + g->min = 0; + g->count = 0; +} + +static inline void tg_min_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_min_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_min *g = (struct tg_min *)r->time_grouping.data; + + if(!g->count || fabsndd(value) < fabsndd(g->min)) { + g->min = value; + g->count++; + } +} + +static inline NETDATA_DOUBLE tg_min_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_min *g = (struct tg_min *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = g->min; + } + + g->min = 0.0; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_MIN_H diff --git a/src/web/api/queries/percentile/README.md b/src/web/api/queries/percentile/README.md new file mode 100644 index 000000000..88abf8d5c --- /dev/null +++ b/src/web/api/queries/percentile/README.md @@ -0,0 +1,62 @@ +<!-- +title: "Percentile" +sidebar_label: "Percentile" +description: "Use percentile in API queries and health entities to find the 'percentile' value from a sample, eliminating any unwanted spikes in the returned metrics." +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/percentile/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Percentile + +The percentile is the average value of a series using only the smaller N percentile of the values. +(a population or a probability distribution). + +Netdata applies linear interpolation on the last point, if the percentile requested does not give a round number of +points. + +The following percentile aliases are defined: + +- `percentile25` +- `percentile50` +- `percentile75` +- `percentile80` +- `percentile90` +- `percentile95` +- `percentile97` +- `percentile98` +- `percentile99` + +The default `percentile` is an alias for `percentile95`. +Any percentile may be requested using the `group_options` query parameter. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: percentile95 -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`percentile` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=percentile` in the URL and the additional parameter `group_options` +may be used to request any percentile (e.g. `&group=percentile&group_options=96`). + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=percentile95&after=-60&label=percentile95&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Percentile>. diff --git a/src/web/api/queries/percentile/percentile.c b/src/web/api/queries/percentile/percentile.c new file mode 100644 index 000000000..da3b32696 --- /dev/null +++ b/src/web/api/queries/percentile/percentile.c @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "percentile.h" + +// ---------------------------------------------------------------------------- +// median diff --git a/src/web/api/queries/percentile/percentile.h b/src/web/api/queries/percentile/percentile.h new file mode 100644 index 000000000..0532f9d3f --- /dev/null +++ b/src/web/api/queries/percentile/percentile.h @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_PERCENTILE_H +#define NETDATA_API_QUERIES_PERCENTILE_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_percentile { + size_t series_size; + size_t next_pos; + NETDATA_DOUBLE percent; + + NETDATA_DOUBLE *series; +}; + +static inline void tg_percentile_create_internal(RRDR *r, const char *options, NETDATA_DOUBLE def) { + long entries = r->view.group; + if(entries < 10) entries = 10; + + struct tg_percentile *g = (struct tg_percentile *)onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_percentile)); + g->series = onewayalloc_mallocz(r->internal.owa, entries * sizeof(NETDATA_DOUBLE)); + g->series_size = (size_t)entries; + + g->percent = def; + if(options && *options) { + g->percent = str2ndd(options, NULL); + if(!netdata_double_isnumber(g->percent)) g->percent = 0.0; + if(g->percent < 0.0) g->percent = 0.0; + if(g->percent > 100.0) g->percent = 100.0; + } + + g->percent = g->percent / 100.0; + r->time_grouping.data = g; +} + +static inline void tg_percentile_create_25(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 25.0); +} +static inline void tg_percentile_create_50(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 50.0); +} +static inline void tg_percentile_create_75(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 75.0); +} +static inline void tg_percentile_create_80(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 80.0); +} +static inline void tg_percentile_create_90(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 90.0); +} +static inline void tg_percentile_create_95(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 95.0); +} +static inline void tg_percentile_create_97(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 97.0); +} +static inline void tg_percentile_create_98(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 98.0); +} +static inline void tg_percentile_create_99(RRDR *r, const char *options) { + tg_percentile_create_internal(r, options, 99.0); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_percentile_reset(RRDR *r) { + struct tg_percentile *g = (struct tg_percentile *)r->time_grouping.data; + g->next_pos = 0; +} + +static inline void tg_percentile_free(RRDR *r) { + struct tg_percentile *g = (struct tg_percentile *)r->time_grouping.data; + if(g) onewayalloc_freez(r->internal.owa, g->series); + + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_percentile_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_percentile *g = (struct tg_percentile *)r->time_grouping.data; + + if(unlikely(g->next_pos >= g->series_size)) { + g->series = onewayalloc_doublesize( r->internal.owa, g->series, g->series_size * sizeof(NETDATA_DOUBLE)); + g->series_size *= 2; + } + + g->series[g->next_pos++] = value; +} + +static inline NETDATA_DOUBLE tg_percentile_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_percentile *g = (struct tg_percentile *)r->time_grouping.data; + + NETDATA_DOUBLE value; + size_t available_slots = g->next_pos; + + if(unlikely(!available_slots)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else if(available_slots == 1) { + value = g->series[0]; + } + else { + sort_series(g->series, available_slots); + + NETDATA_DOUBLE min = g->series[0]; + NETDATA_DOUBLE max = g->series[available_slots - 1]; + + if (min != max) { + size_t slots_to_use = (size_t)((NETDATA_DOUBLE)available_slots * g->percent); + if(!slots_to_use) slots_to_use = 1; + + NETDATA_DOUBLE percent_to_use = (NETDATA_DOUBLE)slots_to_use / (NETDATA_DOUBLE)available_slots; + NETDATA_DOUBLE percent_delta = g->percent - percent_to_use; + + NETDATA_DOUBLE percent_interpolation_slot = 0.0; + NETDATA_DOUBLE percent_last_slot = 0.0; + if(percent_delta > 0.0) { + NETDATA_DOUBLE percent_to_use_plus_1_slot = (NETDATA_DOUBLE)(slots_to_use + 1) / (NETDATA_DOUBLE)available_slots; + NETDATA_DOUBLE percent_1slot = percent_to_use_plus_1_slot - percent_to_use; + + percent_interpolation_slot = percent_delta / percent_1slot; + percent_last_slot = 1 - percent_interpolation_slot; + } + + int start_slot, stop_slot, step, last_slot, interpolation_slot; + if(min >= 0.0 && max >= 0.0) { + start_slot = 0; + stop_slot = start_slot + (int)slots_to_use; + last_slot = stop_slot - 1; + interpolation_slot = stop_slot; + step = 1; + } + else { + start_slot = (int)available_slots - 1; + stop_slot = start_slot - (int)slots_to_use; + last_slot = stop_slot + 1; + interpolation_slot = stop_slot; + step = -1; + } + + value = 0.0; + for(int slot = start_slot; slot != stop_slot ; slot += step) + value += g->series[slot]; + + size_t counted = slots_to_use; + if(percent_interpolation_slot > 0.0 && interpolation_slot >= 0 && interpolation_slot < (int)available_slots) { + value += g->series[interpolation_slot] * percent_interpolation_slot; + value += g->series[last_slot] * percent_last_slot; + counted++; + } + + value = value / (NETDATA_DOUBLE)counted; + } + else + value = min; + } + + if(unlikely(!netdata_double_isnumber(value))) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + + //log_series_to_stderr(g->series, g->next_pos, value, "percentile"); + + g->next_pos = 0; + + return value; +} + +#endif //NETDATA_API_QUERIES_PERCENTILE_H diff --git a/src/web/api/queries/query.c b/src/web/api/queries/query.c new file mode 100644 index 000000000..fe1664068 --- /dev/null +++ b/src/web/api/queries/query.c @@ -0,0 +1,3728 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "query.h" +#include "web/api/formatters/rrd2json.h" +#include "rrdr.h" + +#include "average/average.h" +#include "countif/countif.h" +#include "incremental_sum/incremental_sum.h" +#include "max/max.h" +#include "median/median.h" +#include "min/min.h" +#include "sum/sum.h" +#include "stddev/stddev.h" +#include "ses/ses.h" +#include "des/des.h" +#include "percentile/percentile.h" +#include "trimmed_mean/trimmed_mean.h" + +#define QUERY_PLAN_MIN_POINTS 10 +#define POINTS_TO_EXPAND_QUERY 5 + +// ---------------------------------------------------------------------------- + +static struct { + const char *name; + uint32_t hash; + RRDR_TIME_GROUPING value; + RRDR_TIME_GROUPING add_flush; + + // One time initialization for the module. + // This is called once, when netdata starts. + void (*init)(void); + + // Allocate all required structures for a query. + // This is called once for each netdata query. + void (*create)(struct rrdresult *r, const char *options); + + // Cleanup collected values, but don't destroy the structures. + // This is called when the query engine switches dimensions, + // as part of the same query (so same chart, switching metric). + void (*reset)(struct rrdresult *r); + + // Free all resources allocated for the query. + void (*free)(struct rrdresult *r); + + // Add a single value into the calculation. + // The module may decide to cache it, or use it in the fly. + void (*add)(struct rrdresult *r, NETDATA_DOUBLE value); + + // Generate a single result for the values added so far. + // More values and points may be requested later. + // It is up to the module to reset its internal structures + // when flushing it (so for a few modules it may be better to + // continue after a flush as if nothing changed, for others a + // cleanup of the internal structures may be required). + NETDATA_DOUBLE (*flush)(struct rrdresult *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr); + + TIER_QUERY_FETCH tier_query_fetch; +} api_v1_data_groups[] = { + {.name = "average", + .hash = 0, + .value = RRDR_GROUPING_AVERAGE, + .add_flush = RRDR_GROUPING_AVERAGE, + .init = NULL, + .create= tg_average_create, + .reset = tg_average_reset, + .free = tg_average_free, + .add = tg_average_add, + .flush = tg_average_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "avg", // alias on 'average' + .hash = 0, + .value = RRDR_GROUPING_AVERAGE, + .add_flush = RRDR_GROUPING_AVERAGE, + .init = NULL, + .create= tg_average_create, + .reset = tg_average_reset, + .free = tg_average_free, + .add = tg_average_add, + .flush = tg_average_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "mean", // alias on 'average' + .hash = 0, + .value = RRDR_GROUPING_AVERAGE, + .add_flush = RRDR_GROUPING_AVERAGE, + .init = NULL, + .create= tg_average_create, + .reset = tg_average_reset, + .free = tg_average_free, + .add = tg_average_add, + .flush = tg_average_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean1", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN1, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_1, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean2", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN2, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_2, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean3", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN3, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_3, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean5", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_5, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean10", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN10, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_10, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean15", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN15, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_15, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean20", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN20, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_20, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean25", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN25, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_25, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-mean", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEAN, + .add_flush = RRDR_GROUPING_TRIMMED_MEAN, + .init = NULL, + .create= tg_trimmed_mean_create_5, + .reset = tg_trimmed_mean_reset, + .free = tg_trimmed_mean_free, + .add = tg_trimmed_mean_add, + .flush = tg_trimmed_mean_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "incremental_sum", + .hash = 0, + .value = RRDR_GROUPING_INCREMENTAL_SUM, + .add_flush = RRDR_GROUPING_INCREMENTAL_SUM, + .init = NULL, + .create= tg_incremental_sum_create, + .reset = tg_incremental_sum_reset, + .free = tg_incremental_sum_free, + .add = tg_incremental_sum_add, + .flush = tg_incremental_sum_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "incremental-sum", + .hash = 0, + .value = RRDR_GROUPING_INCREMENTAL_SUM, + .add_flush = RRDR_GROUPING_INCREMENTAL_SUM, + .init = NULL, + .create= tg_incremental_sum_create, + .reset = tg_incremental_sum_reset, + .free = tg_incremental_sum_free, + .add = tg_incremental_sum_add, + .flush = tg_incremental_sum_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "median", + .hash = 0, + .value = RRDR_GROUPING_MEDIAN, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median1", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN1, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_1, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median2", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN2, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_2, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median3", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN3, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_3, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median5", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_5, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median10", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN10, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_10, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median15", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN15, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_15, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median20", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN20, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_20, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median25", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN25, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_25, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "trimmed-median", + .hash = 0, + .value = RRDR_GROUPING_TRIMMED_MEDIAN, + .add_flush = RRDR_GROUPING_MEDIAN, + .init = NULL, + .create= tg_median_create_trimmed_5, + .reset = tg_median_reset, + .free = tg_median_free, + .add = tg_median_add, + .flush = tg_median_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile25", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE25, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_25, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile50", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE50, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_50, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile75", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE75, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_75, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile80", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE80, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_80, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile90", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE90, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_90, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile95", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_95, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile97", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE97, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_97, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile98", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE98, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_98, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile99", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE99, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_99, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "percentile", + .hash = 0, + .value = RRDR_GROUPING_PERCENTILE, + .add_flush = RRDR_GROUPING_PERCENTILE, + .init = NULL, + .create= tg_percentile_create_95, + .reset = tg_percentile_reset, + .free = tg_percentile_free, + .add = tg_percentile_add, + .flush = tg_percentile_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "min", + .hash = 0, + .value = RRDR_GROUPING_MIN, + .add_flush = RRDR_GROUPING_MIN, + .init = NULL, + .create= tg_min_create, + .reset = tg_min_reset, + .free = tg_min_free, + .add = tg_min_add, + .flush = tg_min_flush, + .tier_query_fetch = TIER_QUERY_FETCH_MIN + }, + {.name = "max", + .hash = 0, + .value = RRDR_GROUPING_MAX, + .add_flush = RRDR_GROUPING_MAX, + .init = NULL, + .create= tg_max_create, + .reset = tg_max_reset, + .free = tg_max_free, + .add = tg_max_add, + .flush = tg_max_flush, + .tier_query_fetch = TIER_QUERY_FETCH_MAX + }, + {.name = "sum", + .hash = 0, + .value = RRDR_GROUPING_SUM, + .add_flush = RRDR_GROUPING_SUM, + .init = NULL, + .create= tg_sum_create, + .reset = tg_sum_reset, + .free = tg_sum_free, + .add = tg_sum_add, + .flush = tg_sum_flush, + .tier_query_fetch = TIER_QUERY_FETCH_SUM + }, + + // standard deviation + {.name = "stddev", + .hash = 0, + .value = RRDR_GROUPING_STDDEV, + .add_flush = RRDR_GROUPING_STDDEV, + .init = NULL, + .create= tg_stddev_create, + .reset = tg_stddev_reset, + .free = tg_stddev_free, + .add = tg_stddev_add, + .flush = tg_stddev_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "cv", // coefficient variation is calculated by stddev + .hash = 0, + .value = RRDR_GROUPING_CV, + .add_flush = RRDR_GROUPING_CV, + .init = NULL, + .create= tg_stddev_create, // not an error, stddev calculates this too + .reset = tg_stddev_reset, // not an error, stddev calculates this too + .free = tg_stddev_free, // not an error, stddev calculates this too + .add = tg_stddev_add, // not an error, stddev calculates this too + .flush = tg_stddev_coefficient_of_variation_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "rsd", // alias of 'cv' + .hash = 0, + .value = RRDR_GROUPING_CV, + .add_flush = RRDR_GROUPING_CV, + .init = NULL, + .create= tg_stddev_create, // not an error, stddev calculates this too + .reset = tg_stddev_reset, // not an error, stddev calculates this too + .free = tg_stddev_free, // not an error, stddev calculates this too + .add = tg_stddev_add, // not an error, stddev calculates this too + .flush = tg_stddev_coefficient_of_variation_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + + // single exponential smoothing + {.name = "ses", + .hash = 0, + .value = RRDR_GROUPING_SES, + .add_flush = RRDR_GROUPING_SES, + .init = tg_ses_init, + .create= tg_ses_create, + .reset = tg_ses_reset, + .free = tg_ses_free, + .add = tg_ses_add, + .flush = tg_ses_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "ema", // alias for 'ses' + .hash = 0, + .value = RRDR_GROUPING_SES, + .add_flush = RRDR_GROUPING_SES, + .init = NULL, + .create= tg_ses_create, + .reset = tg_ses_reset, + .free = tg_ses_free, + .add = tg_ses_add, + .flush = tg_ses_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + {.name = "ewma", // alias for ses + .hash = 0, + .value = RRDR_GROUPING_SES, + .add_flush = RRDR_GROUPING_SES, + .init = NULL, + .create= tg_ses_create, + .reset = tg_ses_reset, + .free = tg_ses_free, + .add = tg_ses_add, + .flush = tg_ses_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + + // double exponential smoothing + {.name = "des", + .hash = 0, + .value = RRDR_GROUPING_DES, + .add_flush = RRDR_GROUPING_DES, + .init = tg_des_init, + .create= tg_des_create, + .reset = tg_des_reset, + .free = tg_des_free, + .add = tg_des_add, + .flush = tg_des_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + + {.name = "countif", + .hash = 0, + .value = RRDR_GROUPING_COUNTIF, + .add_flush = RRDR_GROUPING_COUNTIF, + .init = NULL, + .create= tg_countif_create, + .reset = tg_countif_reset, + .free = tg_countif_free, + .add = tg_countif_add, + .flush = tg_countif_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + }, + + // terminator + {.name = NULL, + .hash = 0, + .value = RRDR_GROUPING_UNDEFINED, + .add_flush = RRDR_GROUPING_AVERAGE, + .init = NULL, + .create= tg_average_create, + .reset = tg_average_reset, + .free = tg_average_free, + .add = tg_average_add, + .flush = tg_average_flush, + .tier_query_fetch = TIER_QUERY_FETCH_AVERAGE + } +}; + +void time_grouping_init(void) { + int i; + + for(i = 0; api_v1_data_groups[i].name ; i++) { + api_v1_data_groups[i].hash = simple_hash(api_v1_data_groups[i].name); + + if(api_v1_data_groups[i].init) + api_v1_data_groups[i].init(); + } +} + +const char *time_grouping_id2txt(RRDR_TIME_GROUPING group) { + int i; + + for(i = 0; api_v1_data_groups[i].name ; i++) { + if(api_v1_data_groups[i].value == group) { + return api_v1_data_groups[i].name; + } + } + + return "average"; +} + +RRDR_TIME_GROUPING time_grouping_txt2id(const char *name) { + int i; + + uint32_t hash = simple_hash(name); + for(i = 0; api_v1_data_groups[i].name ; i++) + if(unlikely(hash == api_v1_data_groups[i].hash && !strcmp(name, api_v1_data_groups[i].name))) + return api_v1_data_groups[i].value; + + return RRDR_GROUPING_AVERAGE; +} + +RRDR_TIME_GROUPING time_grouping_parse(const char *name, RRDR_TIME_GROUPING def) { + int i; + + uint32_t hash = simple_hash(name); + for(i = 0; api_v1_data_groups[i].name ; i++) + if(unlikely(hash == api_v1_data_groups[i].hash && !strcmp(name, api_v1_data_groups[i].name))) + return api_v1_data_groups[i].value; + + return def; +} + +const char *time_grouping_tostring(RRDR_TIME_GROUPING group) { + int i; + + for(i = 0; api_v1_data_groups[i].name ; i++) + if(unlikely(group == api_v1_data_groups[i].value)) + return api_v1_data_groups[i].name; + + return "unknown"; +} + +static void rrdr_set_grouping_function(RRDR *r, RRDR_TIME_GROUPING group_method) { + int i, found = 0; + for(i = 0; !found && api_v1_data_groups[i].name ;i++) { + if(api_v1_data_groups[i].value == group_method) { + r->time_grouping.create = api_v1_data_groups[i].create; + r->time_grouping.reset = api_v1_data_groups[i].reset; + r->time_grouping.free = api_v1_data_groups[i].free; + r->time_grouping.add = api_v1_data_groups[i].add; + r->time_grouping.flush = api_v1_data_groups[i].flush; + r->time_grouping.tier_query_fetch = api_v1_data_groups[i].tier_query_fetch; + r->time_grouping.add_flush = api_v1_data_groups[i].add_flush; + found = 1; + } + } + if(!found) { + errno = 0; + internal_error(true, "QUERY: grouping method %u not found. Using 'average'", (unsigned int)group_method); + r->time_grouping.create = tg_average_create; + r->time_grouping.reset = tg_average_reset; + r->time_grouping.free = tg_average_free; + r->time_grouping.add = tg_average_add; + r->time_grouping.flush = tg_average_flush; + r->time_grouping.tier_query_fetch = TIER_QUERY_FETCH_AVERAGE; + r->time_grouping.add_flush = RRDR_GROUPING_AVERAGE; + } +} + +static inline void time_grouping_add(RRDR *r, NETDATA_DOUBLE value, const RRDR_TIME_GROUPING add_flush) { + switch(add_flush) { + case RRDR_GROUPING_AVERAGE: + tg_average_add(r, value); + break; + + case RRDR_GROUPING_MAX: + tg_max_add(r, value); + break; + + case RRDR_GROUPING_MIN: + tg_min_add(r, value); + break; + + case RRDR_GROUPING_MEDIAN: + tg_median_add(r, value); + break; + + case RRDR_GROUPING_STDDEV: + case RRDR_GROUPING_CV: + tg_stddev_add(r, value); + break; + + case RRDR_GROUPING_SUM: + tg_sum_add(r, value); + break; + + case RRDR_GROUPING_COUNTIF: + tg_countif_add(r, value); + break; + + case RRDR_GROUPING_TRIMMED_MEAN: + tg_trimmed_mean_add(r, value); + break; + + case RRDR_GROUPING_PERCENTILE: + tg_percentile_add(r, value); + break; + + case RRDR_GROUPING_SES: + tg_ses_add(r, value); + break; + + case RRDR_GROUPING_DES: + tg_des_add(r, value); + break; + + case RRDR_GROUPING_INCREMENTAL_SUM: + tg_incremental_sum_add(r, value); + break; + + default: + r->time_grouping.add(r, value); + break; + } +} + +static inline NETDATA_DOUBLE time_grouping_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr, const RRDR_TIME_GROUPING add_flush) { + switch(add_flush) { + case RRDR_GROUPING_AVERAGE: + return tg_average_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_MAX: + return tg_max_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_MIN: + return tg_min_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_MEDIAN: + return tg_median_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_STDDEV: + return tg_stddev_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_CV: + return tg_stddev_coefficient_of_variation_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_SUM: + return tg_sum_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_COUNTIF: + return tg_countif_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_TRIMMED_MEAN: + return tg_trimmed_mean_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_PERCENTILE: + return tg_percentile_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_SES: + return tg_ses_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_DES: + return tg_des_flush(r, rrdr_value_options_ptr); + + case RRDR_GROUPING_INCREMENTAL_SUM: + return tg_incremental_sum_flush(r, rrdr_value_options_ptr); + + default: + return r->time_grouping.flush(r, rrdr_value_options_ptr); + } +} + +RRDR_GROUP_BY group_by_parse(char *s) { + RRDR_GROUP_BY group_by = RRDR_GROUP_BY_NONE; + + while(s) { + char *key = strsep_skip_consecutive_separators(&s, ",| "); + if (!key || !*key) continue; + + if (strcmp(key, "selected") == 0) + group_by |= RRDR_GROUP_BY_SELECTED; + + if (strcmp(key, "dimension") == 0) + group_by |= RRDR_GROUP_BY_DIMENSION; + + if (strcmp(key, "instance") == 0) + group_by |= RRDR_GROUP_BY_INSTANCE; + + if (strcmp(key, "percentage-of-instance") == 0) + group_by |= RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE; + + if (strcmp(key, "label") == 0) + group_by |= RRDR_GROUP_BY_LABEL; + + if (strcmp(key, "node") == 0) + group_by |= RRDR_GROUP_BY_NODE; + + if (strcmp(key, "context") == 0) + group_by |= RRDR_GROUP_BY_CONTEXT; + + if (strcmp(key, "units") == 0) + group_by |= RRDR_GROUP_BY_UNITS; + } + + if((group_by & RRDR_GROUP_BY_SELECTED) && (group_by & ~RRDR_GROUP_BY_SELECTED)) { + internal_error(true, "group-by given by query has 'selected' together with more groupings"); + group_by = RRDR_GROUP_BY_SELECTED; // remove all other groupings + } + + if(group_by & RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE) + group_by = RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE; // remove all other groupings + + return group_by; +} + +void buffer_json_group_by_to_array(BUFFER *wb, RRDR_GROUP_BY group_by) { + if(group_by == RRDR_GROUP_BY_NONE) + buffer_json_add_array_item_string(wb, "none"); + else { + if (group_by & RRDR_GROUP_BY_DIMENSION) + buffer_json_add_array_item_string(wb, "dimension"); + + if (group_by & RRDR_GROUP_BY_INSTANCE) + buffer_json_add_array_item_string(wb, "instance"); + + if (group_by & RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE) + buffer_json_add_array_item_string(wb, "percentage-of-instance"); + + if (group_by & RRDR_GROUP_BY_LABEL) + buffer_json_add_array_item_string(wb, "label"); + + if (group_by & RRDR_GROUP_BY_NODE) + buffer_json_add_array_item_string(wb, "node"); + + if (group_by & RRDR_GROUP_BY_CONTEXT) + buffer_json_add_array_item_string(wb, "context"); + + if (group_by & RRDR_GROUP_BY_UNITS) + buffer_json_add_array_item_string(wb, "units"); + + if (group_by & RRDR_GROUP_BY_SELECTED) + buffer_json_add_array_item_string(wb, "selected"); + } +} + +RRDR_GROUP_BY_FUNCTION group_by_aggregate_function_parse(const char *s) { + if(strcmp(s, "average") == 0) + return RRDR_GROUP_BY_FUNCTION_AVERAGE; + + if(strcmp(s, "avg") == 0) + return RRDR_GROUP_BY_FUNCTION_AVERAGE; + + if(strcmp(s, "min") == 0) + return RRDR_GROUP_BY_FUNCTION_MIN; + + if(strcmp(s, "max") == 0) + return RRDR_GROUP_BY_FUNCTION_MAX; + + if(strcmp(s, "sum") == 0) + return RRDR_GROUP_BY_FUNCTION_SUM; + + if(strcmp(s, "percentage") == 0) + return RRDR_GROUP_BY_FUNCTION_PERCENTAGE; + + return RRDR_GROUP_BY_FUNCTION_AVERAGE; +} + +const char *group_by_aggregate_function_to_string(RRDR_GROUP_BY_FUNCTION group_by_function) { + switch(group_by_function) { + default: + case RRDR_GROUP_BY_FUNCTION_AVERAGE: + return "average"; + + case RRDR_GROUP_BY_FUNCTION_MIN: + return "min"; + + case RRDR_GROUP_BY_FUNCTION_MAX: + return "max"; + + case RRDR_GROUP_BY_FUNCTION_SUM: + return "sum"; + + case RRDR_GROUP_BY_FUNCTION_PERCENTAGE: + return "percentage"; + } +} + +// ---------------------------------------------------------------------------- +// helpers to find our way in RRDR + +static inline RRDR_VALUE_FLAGS *UNUSED_FUNCTION(rrdr_line_options)(RRDR *r, long rrdr_line) { + return &r->o[ rrdr_line * r->d ]; +} + +static inline NETDATA_DOUBLE *UNUSED_FUNCTION(rrdr_line_values)(RRDR *r, long rrdr_line) { + return &r->v[ rrdr_line * r->d ]; +} + +static inline long rrdr_line_init(RRDR *r __maybe_unused, time_t t __maybe_unused, long rrdr_line) { + rrdr_line++; + + internal_fatal(rrdr_line >= (long)r->n, + "QUERY: requested to step above RRDR size for query '%s'", + r->internal.qt->id); + + internal_fatal(r->t[rrdr_line] != t, + "QUERY: wrong timestamp at RRDR line %ld, expected %ld, got %ld, of query '%s'", + rrdr_line, r->t[rrdr_line], t, r->internal.qt->id); + + return rrdr_line; +} + +// ---------------------------------------------------------------------------- +// tier management + +static bool query_metric_is_valid_tier(QUERY_METRIC *qm, size_t tier) { + if(!qm->tiers[tier].smh || !qm->tiers[tier].db_first_time_s || !qm->tiers[tier].db_last_time_s || !qm->tiers[tier].db_update_every_s) + return false; + + return true; +} + +static size_t query_metric_first_working_tier(QUERY_METRIC *qm) { + for(size_t tier = 0; tier < storage_tiers ; tier++) { + + // find the db time-range for this tier for all metrics + STORAGE_METRIC_HANDLE *smh = qm->tiers[tier].smh; + time_t first_time_s = qm->tiers[tier].db_first_time_s; + time_t last_time_s = qm->tiers[tier].db_last_time_s; + time_t update_every_s = qm->tiers[tier].db_update_every_s; + + if(!smh || !first_time_s || !last_time_s || !update_every_s) + continue; + + return tier; + } + + return 0; +} + +static long query_plan_points_coverage_weight(time_t db_first_time_s, time_t db_last_time_s, time_t db_update_every_s, time_t after_wanted, time_t before_wanted, size_t points_wanted, size_t tier __maybe_unused) { + if(db_first_time_s == 0 || + db_last_time_s == 0 || + db_update_every_s == 0 || + db_first_time_s > before_wanted || + db_last_time_s < after_wanted) + return -LONG_MAX; + + long long common_first_t = MAX(db_first_time_s, after_wanted); + long long common_last_t = MIN(db_last_time_s, before_wanted); + + long long time_coverage = (common_last_t - common_first_t) * 1000000LL / (before_wanted - after_wanted); + long long points_wanted_in_coverage = (long long)points_wanted * time_coverage / 1000000LL; + + long long points_available = (common_last_t - common_first_t) / db_update_every_s; + long long points_delta = (long)(points_available - points_wanted_in_coverage); + long long points_coverage = (points_delta < 0) ? (long)(points_available * time_coverage / points_wanted_in_coverage) : time_coverage; + + // a way to benefit higher tiers + // points_coverage += (long)tier * 10000; + + if(points_available <= 0) + return -LONG_MAX; + + return (long)(points_coverage + (25000LL * tier)); // 2.5% benefit for each higher tier +} + +static size_t query_metric_best_tier_for_timeframe(QUERY_METRIC *qm, time_t after_wanted, time_t before_wanted, size_t points_wanted) { + if(unlikely(storage_tiers < 2)) + return 0; + + if(unlikely(after_wanted == before_wanted || points_wanted <= 0)) + return query_metric_first_working_tier(qm); + + if(points_wanted < QUERY_PLAN_MIN_POINTS) + // when selecting tiers, aim for a resolution of at least QUERY_PLAN_MIN_POINTS points + points_wanted = (before_wanted - after_wanted) > QUERY_PLAN_MIN_POINTS ? QUERY_PLAN_MIN_POINTS : before_wanted - after_wanted; + + time_t min_first_time_s = 0; + time_t max_last_time_s = 0; + + for(size_t tier = 0; tier < storage_tiers ; tier++) { + time_t first_time_s = qm->tiers[tier].db_first_time_s; + time_t last_time_s = qm->tiers[tier].db_last_time_s; + + if(!min_first_time_s || (first_time_s && first_time_s < min_first_time_s)) + min_first_time_s = first_time_s; + + if(!max_last_time_s || (last_time_s && last_time_s > max_last_time_s)) + max_last_time_s = last_time_s; + } + + for(size_t tier = 0; tier < storage_tiers ; tier++) { + + // find the db time-range for this tier for all metrics + STORAGE_METRIC_HANDLE *smh = qm->tiers[tier].smh; + time_t first_time_s = qm->tiers[tier].db_first_time_s; + time_t last_time_s = qm->tiers[tier].db_last_time_s; + time_t update_every_s = qm->tiers[tier].db_update_every_s; + + if( !smh || + !first_time_s || + !last_time_s || + !update_every_s || + first_time_s > before_wanted || + last_time_s < after_wanted + ) { + qm->tiers[tier].weight = -LONG_MAX; + continue; + } + + internal_fatal(first_time_s > before_wanted || last_time_s < after_wanted, "QUERY: invalid db durations"); + + qm->tiers[tier].weight = query_plan_points_coverage_weight( + min_first_time_s, max_last_time_s, update_every_s, + after_wanted, before_wanted, points_wanted, tier); + } + + size_t best_tier = 0; + for(size_t tier = 1; tier < storage_tiers ; tier++) { + if(qm->tiers[tier].weight >= qm->tiers[best_tier].weight) + best_tier = tier; + } + + return best_tier; +} + +static size_t rrddim_find_best_tier_for_timeframe(QUERY_TARGET *qt, time_t after_wanted, time_t before_wanted, size_t points_wanted) { + if(unlikely(storage_tiers < 2)) + return 0; + + if(unlikely(after_wanted == before_wanted || points_wanted <= 0)) { + internal_error(true, "QUERY: '%s' has invalid params to tier calculation", qt->id); + return 0; + } + + long weight[storage_tiers]; + + for(size_t tier = 0; tier < storage_tiers ; tier++) { + + time_t common_first_time_s = 0; + time_t common_last_time_s = 0; + time_t common_update_every_s = 0; + + // find the db time-range for this tier for all metrics + for(size_t i = 0, used = qt->query.used; i < used ; i++) { + QUERY_METRIC *qm = query_metric(qt, i); + + time_t first_time_s = qm->tiers[tier].db_first_time_s; + time_t last_time_s = qm->tiers[tier].db_last_time_s; + time_t update_every_s = qm->tiers[tier].db_update_every_s; + + if(!first_time_s || !last_time_s || !update_every_s) + continue; + + if(!common_first_time_s) + common_first_time_s = first_time_s; + else + common_first_time_s = MIN(first_time_s, common_first_time_s); + + if(!common_last_time_s) + common_last_time_s = last_time_s; + else + common_last_time_s = MAX(last_time_s, common_last_time_s); + + if(!common_update_every_s) + common_update_every_s = update_every_s; + else + common_update_every_s = MIN(update_every_s, common_update_every_s); + } + + weight[tier] = query_plan_points_coverage_weight(common_first_time_s, common_last_time_s, common_update_every_s, after_wanted, before_wanted, points_wanted, tier); + } + + size_t best_tier = 0; + for(size_t tier = 1; tier < storage_tiers ; tier++) { + if(weight[tier] >= weight[best_tier]) + best_tier = tier; + } + + if(weight[best_tier] == -LONG_MAX) + best_tier = 0; + + return best_tier; +} + +static time_t rrdset_find_natural_update_every_for_timeframe(QUERY_TARGET *qt, time_t after_wanted, time_t before_wanted, size_t points_wanted, RRDR_OPTIONS options, size_t tier) { + size_t best_tier; + if((options & RRDR_OPTION_SELECTED_TIER) && tier < storage_tiers) + best_tier = tier; + else + best_tier = rrddim_find_best_tier_for_timeframe(qt, after_wanted, before_wanted, points_wanted); + + // find the db minimum update every for this tier for all metrics + time_t common_update_every_s = default_rrd_update_every; + for(size_t i = 0, used = qt->query.used; i < used ; i++) { + QUERY_METRIC *qm = query_metric(qt, i); + + time_t update_every_s = qm->tiers[best_tier].db_update_every_s; + + if(!i) + common_update_every_s = update_every_s; + else + common_update_every_s = MIN(update_every_s, common_update_every_s); + } + + return common_update_every_s; +} + +// ---------------------------------------------------------------------------- +// query ops + +typedef struct query_point { + STORAGE_POINT sp; + NETDATA_DOUBLE value; + bool added; +#ifdef NETDATA_INTERNAL_CHECKS + size_t id; +#endif +} QUERY_POINT; + +QUERY_POINT QUERY_POINT_EMPTY = { + .sp = STORAGE_POINT_UNSET, + .value = NAN, + .added = false, +#ifdef NETDATA_INTERNAL_CHECKS + .id = 0, +#endif +}; + +#ifdef NETDATA_INTERNAL_CHECKS +#define query_point_set_id(point, point_id) (point).id = point_id +#else +#define query_point_set_id(point, point_id) debug_dummy() +#endif + +typedef struct query_engine_ops { + // configuration + RRDR *r; + QUERY_METRIC *qm; + time_t view_update_every; + time_t query_granularity; + TIER_QUERY_FETCH tier_query_fetch; + + // query planer + size_t current_plan; + time_t current_plan_expire_time; + time_t plan_expanded_after; + time_t plan_expanded_before; + + // storage queries + size_t tier; + struct query_metric_tier *tier_ptr; + struct storage_engine_query_handle *seqh; + + // aggregating points over time + size_t group_points_non_zero; + size_t group_points_added; + STORAGE_POINT group_point; // aggregates min, max, sum, count, anomaly count for each group point + STORAGE_POINT query_point; // aggregates min, max, sum, count, anomaly count across the whole query + RRDR_VALUE_FLAGS group_value_flags; + + // statistics + size_t db_total_points_read; + size_t db_points_read_per_tier[RRD_STORAGE_TIERS]; + + struct { + time_t expanded_after; + time_t expanded_before; + struct storage_engine_query_handle handle; + bool initialized; + bool finalized; + } plans[QUERY_PLANS_MAX]; + + struct query_engine_ops *next; +} QUERY_ENGINE_OPS; + + +// ---------------------------------------------------------------------------- +// query planer + +#define query_plan_should_switch_plan(ops, now) ((now) >= (ops)->current_plan_expire_time) + +static size_t query_planer_expand_duration_in_points(time_t this_update_every, time_t next_update_every) { + + time_t delta = this_update_every - next_update_every; + if(delta < 0) delta = -delta; + + size_t points; + if(delta < this_update_every * POINTS_TO_EXPAND_QUERY) + points = POINTS_TO_EXPAND_QUERY; + else + points = (delta + this_update_every - 1) / this_update_every; + + return points; +} + +static void query_planer_initialize_plans(QUERY_ENGINE_OPS *ops) { + QUERY_METRIC *qm = ops->qm; + + for(size_t p = 0; p < qm->plan.used ; p++) { + size_t tier = qm->plan.array[p].tier; + time_t update_every = qm->tiers[tier].db_update_every_s; + + size_t points_to_add_to_after; + if(p > 0) { + // there is another plan before to this + + size_t tier0 = qm->plan.array[p - 1].tier; + time_t update_every0 = qm->tiers[tier0].db_update_every_s; + + points_to_add_to_after = query_planer_expand_duration_in_points(update_every, update_every0); + } + else + points_to_add_to_after = (tier == 0) ? 0 : POINTS_TO_EXPAND_QUERY; + + size_t points_to_add_to_before; + if(p + 1 < qm->plan.used) { + // there is another plan after to this + + size_t tier1 = qm->plan.array[p+1].tier; + time_t update_every1 = qm->tiers[tier1].db_update_every_s; + + points_to_add_to_before = query_planer_expand_duration_in_points(update_every, update_every1); + } + else + points_to_add_to_before = POINTS_TO_EXPAND_QUERY; + + time_t after = qm->plan.array[p].after - (time_t)(update_every * points_to_add_to_after); + time_t before = qm->plan.array[p].before + (time_t)(update_every * points_to_add_to_before); + + ops->plans[p].expanded_after = after; + ops->plans[p].expanded_before = before; + + ops->r->internal.qt->db.tiers[tier].queries++; + + struct query_metric_tier *tier_ptr = &qm->tiers[tier]; + STORAGE_ENGINE *eng = query_metric_storage_engine(ops->r->internal.qt, qm, tier); + storage_engine_query_init(eng->seb, tier_ptr->smh, &ops->plans[p].handle, + after, before, ops->r->internal.qt->request.priority); + + ops->plans[p].initialized = true; + ops->plans[p].finalized = false; + } +} + +static void query_planer_finalize_plan(QUERY_ENGINE_OPS *ops, size_t plan_id) { + // QUERY_METRIC *qm = ops->qm; + + if(ops->plans[plan_id].initialized && !ops->plans[plan_id].finalized) { + storage_engine_query_finalize(&ops->plans[plan_id].handle); + ops->plans[plan_id].initialized = false; + ops->plans[plan_id].finalized = true; + } +} + +static void query_planer_finalize_remaining_plans(QUERY_ENGINE_OPS *ops) { + QUERY_METRIC *qm = ops->qm; + + for(size_t p = 0; p < qm->plan.used ; p++) + query_planer_finalize_plan(ops, p); +} + +static void query_planer_activate_plan(QUERY_ENGINE_OPS *ops, size_t plan_id, time_t overwrite_after __maybe_unused) { + QUERY_METRIC *qm = ops->qm; + + internal_fatal(plan_id >= qm->plan.used, "QUERY: invalid plan_id given"); + internal_fatal(!ops->plans[plan_id].initialized, "QUERY: plan has not been initialized"); + internal_fatal(ops->plans[plan_id].finalized, "QUERY: plan has been finalized"); + + internal_fatal(qm->plan.array[plan_id].after > qm->plan.array[plan_id].before, "QUERY: flipped after/before"); + + ops->tier = qm->plan.array[plan_id].tier; + ops->tier_ptr = &qm->tiers[ops->tier]; + ops->seqh = &ops->plans[plan_id].handle; + ops->current_plan = plan_id; + + if(plan_id + 1 < qm->plan.used && qm->plan.array[plan_id + 1].after < qm->plan.array[plan_id].before) + ops->current_plan_expire_time = qm->plan.array[plan_id + 1].after; + else + ops->current_plan_expire_time = qm->plan.array[plan_id].before; + + ops->plan_expanded_after = ops->plans[plan_id].expanded_after; + ops->plan_expanded_before = ops->plans[plan_id].expanded_before; +} + +static bool query_planer_next_plan(QUERY_ENGINE_OPS *ops, time_t now, time_t last_point_end_time) { + QUERY_METRIC *qm = ops->qm; + + size_t old_plan = ops->current_plan; + + time_t next_plan_before_time; + do { + ops->current_plan++; + + if (ops->current_plan >= qm->plan.used) { + ops->current_plan = old_plan; + ops->current_plan_expire_time = ops->r->internal.qt->window.before; + // let the query run with current plan + // we will not switch it + return false; + } + + next_plan_before_time = qm->plan.array[ops->current_plan].before; + } while(now >= next_plan_before_time || last_point_end_time >= next_plan_before_time); + + if(!query_metric_is_valid_tier(qm, qm->plan.array[ops->current_plan].tier)) { + ops->current_plan = old_plan; + ops->current_plan_expire_time = ops->r->internal.qt->window.before; + return false; + } + + query_planer_finalize_plan(ops, old_plan); + query_planer_activate_plan(ops, ops->current_plan, MIN(now, last_point_end_time)); + return true; +} + +static int compare_query_plan_entries_on_start_time(const void *a, const void *b) { + QUERY_PLAN_ENTRY *p1 = (QUERY_PLAN_ENTRY *)a; + QUERY_PLAN_ENTRY *p2 = (QUERY_PLAN_ENTRY *)b; + return (p1->after < p2->after)?-1:1; +} + +static bool query_plan(QUERY_ENGINE_OPS *ops, time_t after_wanted, time_t before_wanted, size_t points_wanted) { + QUERY_METRIC *qm = ops->qm; + + // put our selected tier as the first plan + size_t selected_tier; + bool switch_tiers = true; + + if((ops->r->internal.qt->window.options & RRDR_OPTION_SELECTED_TIER) + && ops->r->internal.qt->window.tier < storage_tiers + && query_metric_is_valid_tier(qm, ops->r->internal.qt->window.tier)) { + selected_tier = ops->r->internal.qt->window.tier; + switch_tiers = false; + } + else { + selected_tier = query_metric_best_tier_for_timeframe(qm, after_wanted, before_wanted, points_wanted); + + if(!query_metric_is_valid_tier(qm, selected_tier)) + return false; + } + + if(qm->tiers[selected_tier].db_first_time_s > before_wanted || + qm->tiers[selected_tier].db_last_time_s < after_wanted) { + // we don't have any data to satisfy this query + return false; + } + + qm->plan.used = 1; + qm->plan.array[0].tier = selected_tier; + qm->plan.array[0].after = (qm->tiers[selected_tier].db_first_time_s < after_wanted) ? after_wanted : qm->tiers[selected_tier].db_first_time_s; + qm->plan.array[0].before = (qm->tiers[selected_tier].db_last_time_s > before_wanted) ? before_wanted : qm->tiers[selected_tier].db_last_time_s; + + if(switch_tiers) { + // the selected tier + time_t selected_tier_first_time_s = qm->plan.array[0].after; + time_t selected_tier_last_time_s = qm->plan.array[0].before; + + // check if our selected tier can start the query + if (selected_tier_first_time_s > after_wanted) { + // we need some help from other tiers + for (size_t tr = (int)selected_tier + 1; tr < storage_tiers && qm->plan.used < QUERY_PLANS_MAX ; tr++) { + if(!query_metric_is_valid_tier(qm, tr)) + continue; + + // find the first time of this tier + time_t tier_first_time_s = qm->tiers[tr].db_first_time_s; + time_t tier_last_time_s = qm->tiers[tr].db_last_time_s; + + // can it help? + if (tier_first_time_s < selected_tier_first_time_s && tier_first_time_s <= before_wanted && tier_last_time_s >= after_wanted) { + // it can help us add detail at the beginning of the query + QUERY_PLAN_ENTRY t = { + .tier = tr, + .after = (tier_first_time_s < after_wanted) ? after_wanted : tier_first_time_s, + .before = selected_tier_first_time_s, + }; + ops->plans[qm->plan.used].initialized = false; + ops->plans[qm->plan.used].finalized = false; + qm->plan.array[qm->plan.used++] = t; + + internal_fatal(!t.after || !t.before, "QUERY: invalid plan selected"); + + // prepare for the tier + selected_tier_first_time_s = t.after; + + if (t.after <= after_wanted) + break; + } + } + } + + // check if our selected tier can finish the query + if (selected_tier_last_time_s < before_wanted) { + // we need some help from other tiers + for (int tr = (int)selected_tier - 1; tr >= 0 && qm->plan.used < QUERY_PLANS_MAX ; tr--) { + if(!query_metric_is_valid_tier(qm, tr)) + continue; + + // find the last time of this tier + time_t tier_first_time_s = qm->tiers[tr].db_first_time_s; + time_t tier_last_time_s = qm->tiers[tr].db_last_time_s; + + //buffer_sprintf(wb, ": EVAL BEFORE tier %d, %ld", tier, last_time_s); + + // can it help? + if (tier_last_time_s > selected_tier_last_time_s && tier_first_time_s <= before_wanted && tier_last_time_s >= after_wanted) { + // it can help us add detail at the end of the query + QUERY_PLAN_ENTRY t = { + .tier = tr, + .after = selected_tier_last_time_s, + .before = (tier_last_time_s > before_wanted) ? before_wanted : tier_last_time_s, + }; + ops->plans[qm->plan.used].initialized = false; + ops->plans[qm->plan.used].finalized = false; + qm->plan.array[qm->plan.used++] = t; + + // prepare for the tier + selected_tier_last_time_s = t.before; + + internal_fatal(!t.after || !t.before, "QUERY: invalid plan selected"); + + if (t.before >= before_wanted) + break; + } + } + } + } + + // sort the query plan + if(qm->plan.used > 1) + qsort(&qm->plan.array, qm->plan.used, sizeof(QUERY_PLAN_ENTRY), compare_query_plan_entries_on_start_time); + + if(!query_metric_is_valid_tier(qm, qm->plan.array[0].tier)) + return false; + +#ifdef NETDATA_INTERNAL_CHECKS + for(size_t p = 0; p < qm->plan.used ;p++) { + internal_fatal(qm->plan.array[p].after > qm->plan.array[p].before, "QUERY: flipped after/before"); + internal_fatal(qm->plan.array[p].after < after_wanted, "QUERY: too small plan first time"); + internal_fatal(qm->plan.array[p].before > before_wanted, "QUERY: too big plan last time"); + } +#endif + + query_planer_initialize_plans(ops); + query_planer_activate_plan(ops, 0, 0); + + return true; +} + + +// ---------------------------------------------------------------------------- +// dimension level query engine + +#define query_interpolate_point(this_point, last_point, now) do { \ + if(likely( \ + /* the point to interpolate is more than 1s wide */ \ + (this_point).sp.end_time_s - (this_point).sp.start_time_s > 1 \ + \ + /* the two points are exactly next to each other */ \ + && (last_point).sp.end_time_s == (this_point).sp.start_time_s \ + \ + /* both points are valid numbers */ \ + && netdata_double_isnumber((this_point).value) \ + && netdata_double_isnumber((last_point).value) \ + \ + )) { \ + (this_point).value = (last_point).value + ((this_point).value - (last_point).value) * (1.0 - (NETDATA_DOUBLE)((this_point).sp.end_time_s - (now)) / (NETDATA_DOUBLE)((this_point).sp.end_time_s - (this_point).sp.start_time_s)); \ + (this_point).sp.end_time_s = now; \ + } \ +} while(0) + +#define query_add_point_to_group(r, point, ops, add_flush) do { \ + if(likely(netdata_double_isnumber((point).value))) { \ + if(likely(fpclassify((point).value) != FP_ZERO)) \ + (ops)->group_points_non_zero++; \ + \ + if(unlikely((point).sp.flags & SN_FLAG_RESET)) \ + (ops)->group_value_flags |= RRDR_VALUE_RESET; \ + \ + time_grouping_add(r, (point).value, add_flush); \ + \ + storage_point_merge_to((ops)->group_point, (point).sp); \ + if(!(point).added) \ + storage_point_merge_to((ops)->query_point, (point).sp); \ + } \ + \ + (ops)->group_points_added++; \ +} while(0) + +static __thread QUERY_ENGINE_OPS *released_ops = NULL; + +static void rrd2rrdr_query_ops_freeall(RRDR *r __maybe_unused) { + while(released_ops) { + QUERY_ENGINE_OPS *ops = released_ops; + released_ops = ops->next; + + onewayalloc_freez(r->internal.owa, ops); + } +} + +static void rrd2rrdr_query_ops_release(QUERY_ENGINE_OPS *ops) { + if(!ops) return; + + ops->next = released_ops; + released_ops = ops; +} + +static QUERY_ENGINE_OPS *rrd2rrdr_query_ops_get(RRDR *r) { + QUERY_ENGINE_OPS *ops; + if(released_ops) { + ops = released_ops; + released_ops = ops->next; + } + else { + ops = onewayalloc_mallocz(r->internal.owa, sizeof(QUERY_ENGINE_OPS)); + } + + memset(ops, 0, sizeof(*ops)); + return ops; +} + +static QUERY_ENGINE_OPS *rrd2rrdr_query_ops_prep(RRDR *r, size_t query_metric_id) { + QUERY_TARGET *qt = r->internal.qt; + + QUERY_ENGINE_OPS *ops = rrd2rrdr_query_ops_get(r); + *ops = (QUERY_ENGINE_OPS) { + .r = r, + .qm = query_metric(qt, query_metric_id), + .tier_query_fetch = r->time_grouping.tier_query_fetch, + .view_update_every = r->view.update_every, + .query_granularity = (time_t)(r->view.update_every / r->view.group), + .group_value_flags = RRDR_VALUE_NOTHING, + }; + + if(!query_plan(ops, qt->window.after, qt->window.before, qt->window.points)) { + rrd2rrdr_query_ops_release(ops); + return NULL; + } + + return ops; +} + +static void rrd2rrdr_query_execute(RRDR *r, size_t dim_id_in_rrdr, QUERY_ENGINE_OPS *ops) { + QUERY_TARGET *qt = r->internal.qt; + QUERY_METRIC *qm = ops->qm; + + const RRDR_TIME_GROUPING add_flush = r->time_grouping.add_flush; + + ops->group_point = STORAGE_POINT_UNSET; + ops->query_point = STORAGE_POINT_UNSET; + + RRDR_OPTIONS options = qt->window.options; + size_t points_wanted = qt->window.points; + time_t after_wanted = qt->window.after; + time_t before_wanted = qt->window.before; (void)before_wanted; + +// bool debug_this = false; +// if(strcmp("user", string2str(rd->id)) == 0 && strcmp("system.cpu", string2str(rd->rrdset->id)) == 0) +// debug_this = true; + + size_t points_added = 0; + + long rrdr_line = -1; + bool use_anomaly_bit_as_value = (r->internal.qt->window.options & RRDR_OPTION_ANOMALY_BIT) ? true : false; + + NETDATA_DOUBLE min = r->view.min, max = r->view.max; + + QUERY_POINT last2_point = QUERY_POINT_EMPTY; + QUERY_POINT last1_point = QUERY_POINT_EMPTY; + QUERY_POINT new_point = QUERY_POINT_EMPTY; + + // ONE POINT READ-AHEAD + // when we switch plans, we read-ahead a point from the next plan + // to join them smoothly at the exact time the next plan begins + STORAGE_POINT next1_point = STORAGE_POINT_UNSET; + + time_t now_start_time = after_wanted - ops->query_granularity; + time_t now_end_time = after_wanted + ops->view_update_every - ops->query_granularity; + + size_t db_points_read_since_plan_switch = 0; (void)db_points_read_since_plan_switch; + size_t query_is_finished_counter = 0; + + // The main loop, based on the query granularity we need + for( ; points_added < points_wanted && query_is_finished_counter <= 10 ; + now_start_time = now_end_time, now_end_time += ops->view_update_every) { + + if(unlikely(query_plan_should_switch_plan(ops, now_end_time))) { + query_planer_next_plan(ops, now_end_time, new_point.sp.end_time_s); + db_points_read_since_plan_switch = 0; + } + + // read all the points of the db, prior to the time we need (now_end_time) + + size_t count_same_end_time = 0; + while(count_same_end_time < 100) { + if(likely(count_same_end_time == 0)) { + last2_point = last1_point; + last1_point = new_point; + } + + if(unlikely(storage_engine_query_is_finished(ops->seqh))) { + query_is_finished_counter++; + + if(count_same_end_time != 0) { + last2_point = last1_point; + last1_point = new_point; + } + new_point = QUERY_POINT_EMPTY; + new_point.sp.start_time_s = last1_point.sp.end_time_s; + new_point.sp.end_time_s = now_end_time; +// +// if(debug_this) netdata_log_info("QUERY: is finished() returned true"); +// + break; + } + else + query_is_finished_counter = 0; + + // fetch the new point + { + STORAGE_POINT sp; + if(likely(storage_point_is_unset(next1_point))) { + db_points_read_since_plan_switch++; + sp = storage_engine_query_next_metric(ops->seqh); + ops->db_points_read_per_tier[ops->tier]++; + ops->db_total_points_read++; + + if(unlikely(options & RRDR_OPTION_ABSOLUTE)) + storage_point_make_positive(sp); + } + else { + // ONE POINT READ-AHEAD + sp = next1_point; + storage_point_unset(next1_point); + db_points_read_since_plan_switch = 1; + } + + // ONE POINT READ-AHEAD + if(unlikely(query_plan_should_switch_plan(ops, sp.end_time_s) && + query_planer_next_plan(ops, now_end_time, new_point.sp.end_time_s))) { + + // The end time of the current point, crosses our plans (tiers) + // so, we switched plan (tier) + // + // There are 2 cases now: + // + // A. the entire point of the previous plan is to the future of point from the next plan + // B. part of the point of the previous plan overlaps with the point from the next plan + + STORAGE_POINT sp2 = storage_engine_query_next_metric(ops->seqh); + ops->db_points_read_per_tier[ops->tier]++; + ops->db_total_points_read++; + + if(unlikely(options & RRDR_OPTION_ABSOLUTE)) + storage_point_make_positive(sp); + + if(sp.start_time_s > sp2.start_time_s) + // the point from the previous plan is useless + sp = sp2; + else + // let the query run from the previous plan + // but setting this will also cut off the interpolation + // of the point from the previous plan + next1_point = sp2; + } + + new_point.sp = sp; + new_point.added = false; + query_point_set_id(new_point, ops->db_total_points_read); + +// if(debug_this) +// netdata_log_info("QUERY: got point %zu, from time %ld to %ld // now from %ld to %ld // query from %ld to %ld", +// new_point.id, new_point.start_time, new_point.end_time, now_start_time, now_end_time, after_wanted, before_wanted); +// + // get the right value from the point we got + if(likely(!storage_point_is_unset(sp) && !storage_point_is_gap(sp))) { + + if(unlikely(use_anomaly_bit_as_value)) + new_point.value = storage_point_anomaly_rate(new_point.sp); + + else { + switch (ops->tier_query_fetch) { + default: + case TIER_QUERY_FETCH_AVERAGE: + new_point.value = sp.sum / (NETDATA_DOUBLE)sp.count; + break; + + case TIER_QUERY_FETCH_MIN: + new_point.value = sp.min; + break; + + case TIER_QUERY_FETCH_MAX: + new_point.value = sp.max; + break; + + case TIER_QUERY_FETCH_SUM: + new_point.value = sp.sum; + break; + } + } + } + else + new_point.value = NAN; + } + + // check if the db is giving us zero duration points + if(unlikely(db_points_read_since_plan_switch > 1 && + new_point.sp.start_time_s == new_point.sp.end_time_s)) { + + internal_error(true, "QUERY: '%s', dimension '%s' next_metric() returned " + "point %zu from %ld to %ld, that are both equal", + qt->id, query_metric_id(qt, qm), + new_point.id, new_point.sp.start_time_s, new_point.sp.end_time_s); + + new_point.sp.start_time_s = new_point.sp.end_time_s - ops->tier_ptr->db_update_every_s; + } + + // check if the db is advancing the query + if(unlikely(db_points_read_since_plan_switch > 1 && + new_point.sp.end_time_s <= last1_point.sp.end_time_s)) { + + internal_error(true, + "QUERY: '%s', dimension '%s' next_metric() returned " + "point %zu from %ld to %ld, before the " + "last point %zu from %ld to %ld, " + "now is %ld to %ld", + qt->id, query_metric_id(qt, qm), + new_point.id, new_point.sp.start_time_s, new_point.sp.end_time_s, + last1_point.id, last1_point.sp.start_time_s, last1_point.sp.end_time_s, + now_start_time, now_end_time); + + count_same_end_time++; + continue; + } + count_same_end_time = 0; + + // decide how to use this point + if(likely(new_point.sp.end_time_s < now_end_time)) { // likely to favor tier0 + // this db point ends before our now_end_time + + if(likely(new_point.sp.end_time_s >= now_start_time)) { // likely to favor tier0 + // this db point ends after our now_start time + + query_add_point_to_group(r, new_point, ops, add_flush); + new_point.added = true; + } + else { + // we don't need this db point + // it is totally outside our current time-frame + + // this is desirable for the first point of the query + // because it allows us to interpolate the next point + // at exactly the time we will want + + // we only log if this is not point 1 + internal_error(new_point.sp.end_time_s < ops->plan_expanded_after && + db_points_read_since_plan_switch > 1, + "QUERY: '%s', dimension '%s' next_metric() " + "returned point %zu from %ld time %ld, " + "which is entirely before our current timeframe %ld to %ld " + "(and before the entire query, after %ld, before %ld)", + qt->id, query_metric_id(qt, qm), + new_point.id, new_point.sp.start_time_s, new_point.sp.end_time_s, + now_start_time, now_end_time, + ops->plan_expanded_after, ops->plan_expanded_before); + } + + } + else { + // the point ends in the future + // so, we will interpolate it below, at the inner loop + break; + } + } + + if(unlikely(count_same_end_time)) { + internal_error(true, + "QUERY: '%s', dimension '%s', the database does not advance the query," + " it returned an end time less or equal to the end time of the last " + "point we got %ld, %zu times", + qt->id, query_metric_id(qt, qm), + last1_point.sp.end_time_s, count_same_end_time); + + if(unlikely(new_point.sp.end_time_s <= last1_point.sp.end_time_s)) + new_point.sp.end_time_s = now_end_time; + } + + time_t stop_time = new_point.sp.end_time_s; + if(unlikely(!storage_point_is_unset(next1_point) && next1_point.start_time_s >= now_end_time)) { + // ONE POINT READ-AHEAD + // the point crosses the start time of the + // read ahead storage point we have read + stop_time = next1_point.start_time_s; + } + + // the inner loop + // we have 3 points in memory: last2, last1, new + // we select the one to use based on their timestamps + + internal_fatal(now_end_time > stop_time || points_added >= points_wanted, + "QUERY: first part of query provides invalid point to interpolate (now_end_time %ld, stop_time %ld", + now_end_time, stop_time); + + do { + // now_start_time is wrong in this loop + // but, we don't need it + + QUERY_POINT current_point; + + if(likely(now_end_time > new_point.sp.start_time_s)) { + // it is time for our NEW point to be used + current_point = new_point; + new_point.added = true; // first copy, then set it, so that new_point will not be added again + query_interpolate_point(current_point, last1_point, now_end_time); + +// internal_error(current_point.id > 0 +// && last1_point.id == 0 +// && current_point.end_time > after_wanted +// && current_point.end_time > now_end_time, +// "QUERY: '%s', dimension '%s', after %ld, before %ld, view update every %ld," +// " query granularity %ld, interpolating point %zu (from %ld to %ld) at %ld," +// " but we could really favor by having last_point1 in this query.", +// qt->id, string2str(qm->dimension.id), +// after_wanted, before_wanted, +// ops.view_update_every, ops.query_granularity, +// current_point.id, current_point.start_time, current_point.end_time, +// now_end_time); + } + else if(likely(now_end_time <= last1_point.sp.end_time_s)) { + // our LAST point is still valid + current_point = last1_point; + last1_point.added = true; // first copy, then set it, so that last1_point will not be added again + query_interpolate_point(current_point, last2_point, now_end_time); + +// internal_error(current_point.id > 0 +// && last2_point.id == 0 +// && current_point.end_time > after_wanted +// && current_point.end_time > now_end_time, +// "QUERY: '%s', dimension '%s', after %ld, before %ld, view update every %ld," +// " query granularity %ld, interpolating point %zu (from %ld to %ld) at %ld," +// " but we could really favor by having last_point2 in this query.", +// qt->id, string2str(qm->dimension.id), +// after_wanted, before_wanted, ops.view_update_every, ops.query_granularity, +// current_point.id, current_point.start_time, current_point.end_time, +// now_end_time); + } + else { + // a GAP, we don't have a value this time + current_point = QUERY_POINT_EMPTY; + } + + query_add_point_to_group(r, current_point, ops, add_flush); + + rrdr_line = rrdr_line_init(r, now_end_time, rrdr_line); + size_t rrdr_o_v_index = rrdr_line * r->d + dim_id_in_rrdr; + + // find the place to store our values + RRDR_VALUE_FLAGS *rrdr_value_options_ptr = &r->o[rrdr_o_v_index]; + + // update the dimension options + if(likely(ops->group_points_non_zero)) + r->od[dim_id_in_rrdr] |= RRDR_DIMENSION_NONZERO; + + // store the specific point options + *rrdr_value_options_ptr = ops->group_value_flags; + + // store the group value + NETDATA_DOUBLE group_value = time_grouping_flush(r, rrdr_value_options_ptr, add_flush); + r->v[rrdr_o_v_index] = group_value; + + r->ar[rrdr_o_v_index] = storage_point_anomaly_rate(ops->group_point); + + if(likely(points_added || r->internal.queries_count)) { + // find the min/max across all dimensions + + if(unlikely(group_value < min)) min = group_value; + if(unlikely(group_value > max)) max = group_value; + + } + else { + // runs only when r->internal.queries_count == 0 && points_added == 0 + // so, on the first point added for the query. + min = max = group_value; + } + + points_added++; + ops->group_points_added = 0; + ops->group_value_flags = RRDR_VALUE_NOTHING; + ops->group_points_non_zero = 0; + ops->group_point = STORAGE_POINT_UNSET; + + now_end_time += ops->view_update_every; + } while(now_end_time <= stop_time && points_added < points_wanted); + + // the loop above increased "now" by ops->view_update_every, + // but the main loop will increase it too, + // so, let's undo the last iteration of this loop + now_end_time -= ops->view_update_every; + } + query_planer_finalize_remaining_plans(ops); + + qm->query_points = ops->query_point; + + // fill the rest of the points with empty values + while (points_added < points_wanted) { + rrdr_line++; + size_t rrdr_o_v_index = rrdr_line * r->d + dim_id_in_rrdr; + r->o[rrdr_o_v_index] = RRDR_VALUE_EMPTY; + r->v[rrdr_o_v_index] = 0.0; + r->ar[rrdr_o_v_index] = 0.0; + points_added++; + } + + r->internal.queries_count++; + r->view.min = min; + r->view.max = max; + + r->stats.result_points_generated += points_added; + r->stats.db_points_read += ops->db_total_points_read; + for(size_t tr = 0; tr < storage_tiers ; tr++) + qt->db.tiers[tr].points += ops->db_points_read_per_tier[tr]; +} + +// ---------------------------------------------------------------------------- +// fill the gap of a tier + +void store_metric_at_tier(RRDDIM *rd, size_t tier, struct rrddim_tier *t, STORAGE_POINT sp, usec_t now_ut); + +void rrdr_fill_tier_gap_from_smaller_tiers(RRDDIM *rd, size_t tier, time_t now_s) { + if(unlikely(tier >= storage_tiers)) return; + if(storage_tiers_backfill[tier] == RRD_BACKFILL_NONE) return; + + struct rrddim_tier *t = &rd->tiers[tier]; + if(unlikely(!t)) return; + + time_t latest_time_s = storage_engine_latest_time_s(t->seb, t->smh); + time_t granularity = (time_t)t->tier_grouping * (time_t)rd->rrdset->update_every; + time_t time_diff = now_s - latest_time_s; + + // if the user wants only NEW backfilling, and we don't have any data + if(storage_tiers_backfill[tier] == RRD_BACKFILL_NEW && latest_time_s <= 0) return; + + // there is really nothing we can do + if(now_s <= latest_time_s || time_diff < granularity) return; + + struct storage_engine_query_handle seqh; + + // for each lower tier + for(int read_tier = (int)tier - 1; read_tier >= 0 ; read_tier--){ + time_t smaller_tier_first_time = storage_engine_oldest_time_s(rd->tiers[read_tier].seb, rd->tiers[read_tier].smh); + time_t smaller_tier_last_time = storage_engine_latest_time_s(rd->tiers[read_tier].seb, rd->tiers[read_tier].smh); + if(smaller_tier_last_time <= latest_time_s) continue; // it is as bad as we are + + long after_wanted = (latest_time_s < smaller_tier_first_time) ? smaller_tier_first_time : latest_time_s; + long before_wanted = smaller_tier_last_time; + + struct rrddim_tier *tmp = &rd->tiers[read_tier]; + storage_engine_query_init(tmp->seb, tmp->smh, &seqh, after_wanted, before_wanted, STORAGE_PRIORITY_HIGH); + + size_t points_read = 0; + + while(!storage_engine_query_is_finished(&seqh)) { + + STORAGE_POINT sp = storage_engine_query_next_metric(&seqh); + points_read++; + + if(sp.end_time_s > latest_time_s) { + latest_time_s = sp.end_time_s; + store_metric_at_tier(rd, tier, t, sp, sp.end_time_s * USEC_PER_SEC); + } + } + + storage_engine_query_finalize(&seqh); + store_metric_collection_completed(); + global_statistics_backfill_query_completed(points_read); + + //internal_error(true, "DBENGINE: backfilled chart '%s', dimension '%s', tier %d, from %ld to %ld, with %zu points from tier %d", + // rd->rrdset->name, rd->name, tier, after_wanted, before_wanted, points, tr); + } +} + +// ---------------------------------------------------------------------------- +// fill RRDR for the whole chart + +#ifdef NETDATA_INTERNAL_CHECKS +static void rrd2rrdr_log_request_response_metadata(RRDR *r + , RRDR_OPTIONS options __maybe_unused + , RRDR_TIME_GROUPING group_method + , bool aligned + , size_t group + , time_t resampling_time + , size_t resampling_group + , time_t after_wanted + , time_t after_requested + , time_t before_wanted + , time_t before_requested + , size_t points_requested + , size_t points_wanted + //, size_t after_slot + //, size_t before_slot + , const char *msg + ) { + + QUERY_TARGET *qt = r->internal.qt; + time_t first_entry_s = qt->db.first_time_s; + time_t last_entry_s = qt->db.last_time_s; + + internal_error( + true, + "rrd2rrdr() on %s update every %ld with %s grouping %s (group: %zu, resampling_time: %ld, resampling_group: %zu), " + "after (got: %ld, want: %ld, req: %ld, db: %ld), " + "before (got: %ld, want: %ld, req: %ld, db: %ld), " + "duration (got: %ld, want: %ld, req: %ld, db: %ld), " + "points (got: %zu, want: %zu, req: %zu), " + "%s" + , qt->id + , qt->window.query_granularity + + // grouping + , (aligned) ? "aligned" : "unaligned" + , time_grouping_id2txt(group_method) + , group + , resampling_time + , resampling_group + + // after + , r->view.after + , after_wanted + , after_requested + , first_entry_s + + // before + , r->view.before + , before_wanted + , before_requested + , last_entry_s + + // duration + , (long)(r->view.before - r->view.after + qt->window.query_granularity) + , (long)(before_wanted - after_wanted + qt->window.query_granularity) + , (long)before_requested - after_requested + , (long)((last_entry_s - first_entry_s) + qt->window.query_granularity) + + // points + , r->rows + , points_wanted + , points_requested + + // message + , msg + ); +} +#endif // NETDATA_INTERNAL_CHECKS + +// #define DEBUG_QUERY_LOGIC 1 + +#ifdef DEBUG_QUERY_LOGIC +#define query_debug_log_init() BUFFER *debug_log = buffer_create(1000) +#define query_debug_log(args...) buffer_sprintf(debug_log, ##args) +#define query_debug_log_fin() { \ + netdata_log_info("QUERY: '%s', after:%ld, before:%ld, duration:%ld, points:%zu, res:%ld - wanted => after:%ld, before:%ld, points:%zu, group:%zu, granularity:%ld, resgroup:%ld, resdiv:" NETDATA_DOUBLE_FORMAT_AUTO " %s", qt->id, after_requested, before_requested, before_requested - after_requested, points_requested, resampling_time_requested, after_wanted, before_wanted, points_wanted, group, query_granularity, resampling_group, resampling_divisor, buffer_tostring(debug_log)); \ + buffer_free(debug_log); \ + debug_log = NULL; \ + } +#define query_debug_log_free() do { buffer_free(debug_log); } while(0) +#else +#define query_debug_log_init() debug_dummy() +#define query_debug_log(args...) debug_dummy() +#define query_debug_log_fin() debug_dummy() +#define query_debug_log_free() debug_dummy() +#endif + +bool query_target_calculate_window(QUERY_TARGET *qt) { + if (unlikely(!qt)) return false; + + size_t points_requested = (long)qt->request.points; + time_t after_requested = qt->request.after; + time_t before_requested = qt->request.before; + RRDR_TIME_GROUPING group_method = qt->request.time_group_method; + time_t resampling_time_requested = qt->request.resampling_time; + RRDR_OPTIONS options = qt->window.options; + size_t tier = qt->request.tier; + time_t update_every = qt->db.minimum_latest_update_every_s ? qt->db.minimum_latest_update_every_s : 1; + + // RULES + // points_requested = 0 + // the user wants all the natural points the database has + // + // after_requested = 0 + // the user wants to start the query from the oldest point in our database + // + // before_requested = 0 + // the user wants the query to end to the latest point in our database + // + // when natural points are wanted, the query has to be aligned to the update_every + // of the database + + size_t points_wanted = points_requested; + time_t after_wanted = after_requested; + time_t before_wanted = before_requested; + + bool aligned = !(options & RRDR_OPTION_NOT_ALIGNED); + bool automatic_natural_points = (points_wanted == 0); + bool relative_period_requested = false; + bool natural_points = (options & RRDR_OPTION_NATURAL_POINTS) || automatic_natural_points; + bool before_is_aligned_to_db_end = false; + + query_debug_log_init(); + + if (ABS(before_requested) <= API_RELATIVE_TIME_MAX || ABS(after_requested) <= API_RELATIVE_TIME_MAX) { + relative_period_requested = true; + natural_points = true; + options |= RRDR_OPTION_NATURAL_POINTS; + query_debug_log(":relative+natural"); + } + + // if the user wants virtual points, make sure we do it + if (options & RRDR_OPTION_VIRTUAL_POINTS) + natural_points = false; + + // set the right flag about natural and virtual points + if (natural_points) { + options |= RRDR_OPTION_NATURAL_POINTS; + + if (options & RRDR_OPTION_VIRTUAL_POINTS) + options &= ~RRDR_OPTION_VIRTUAL_POINTS; + } + else { + options |= RRDR_OPTION_VIRTUAL_POINTS; + + if (options & RRDR_OPTION_NATURAL_POINTS) + options &= ~RRDR_OPTION_NATURAL_POINTS; + } + + if (after_wanted == 0 || before_wanted == 0) { + relative_period_requested = true; + + time_t first_entry_s = qt->db.first_time_s; + time_t last_entry_s = qt->db.last_time_s; + + if (first_entry_s == 0 || last_entry_s == 0) { + internal_error(true, "QUERY: no data detected on query '%s' (db first_entry_t = %ld, last_entry_t = %ld)", qt->id, first_entry_s, last_entry_s); + after_wanted = qt->window.after; + before_wanted = qt->window.before; + + if(after_wanted == before_wanted) + after_wanted = before_wanted - update_every; + + if (points_wanted == 0) { + points_wanted = (before_wanted - after_wanted) / update_every; + query_debug_log(":zero points_wanted %zu", points_wanted); + } + } + else { + query_debug_log(":first_entry_t %ld, last_entry_t %ld", first_entry_s, last_entry_s); + + if (after_wanted == 0) { + after_wanted = first_entry_s; + query_debug_log(":zero after_wanted %ld", after_wanted); + } + + if (before_wanted == 0) { + before_wanted = last_entry_s; + before_is_aligned_to_db_end = true; + query_debug_log(":zero before_wanted %ld", before_wanted); + } + + if (points_wanted == 0) { + points_wanted = (last_entry_s - first_entry_s) / update_every; + query_debug_log(":zero points_wanted %zu", points_wanted); + } + } + } + + if (points_wanted == 0) { + points_wanted = 600; + query_debug_log(":zero600 points_wanted %zu", points_wanted); + } + + // convert our before_wanted and after_wanted to absolute + rrdr_relative_window_to_absolute_query(&after_wanted, &before_wanted, NULL, unittest_running); + query_debug_log(":relative2absolute after %ld, before %ld", after_wanted, before_wanted); + + if (natural_points && (options & RRDR_OPTION_SELECTED_TIER) && tier > 0 && storage_tiers > 1) { + update_every = rrdset_find_natural_update_every_for_timeframe( + qt, after_wanted, before_wanted, points_wanted, options, tier); + + if (update_every <= 0) update_every = qt->db.minimum_latest_update_every_s; + query_debug_log(":natural update every %ld", update_every); + } + + // this is the update_every of the query + // it may be different to the update_every of the database + time_t query_granularity = (natural_points) ? update_every : 1; + if (query_granularity <= 0) query_granularity = 1; + query_debug_log(":query_granularity %ld", query_granularity); + + // align before_wanted and after_wanted to query_granularity + if (before_wanted % query_granularity) { + before_wanted -= before_wanted % query_granularity; + query_debug_log(":granularity align before_wanted %ld", before_wanted); + } + + if (after_wanted % query_granularity) { + after_wanted -= after_wanted % query_granularity; + query_debug_log(":granularity align after_wanted %ld", after_wanted); + } + + // automatic_natural_points is set when the user wants all the points available in the database + if (automatic_natural_points) { + points_wanted = (before_wanted - after_wanted + 1) / query_granularity; + if (unlikely(points_wanted <= 0)) points_wanted = 1; + query_debug_log(":auto natural points_wanted %zu", points_wanted); + } + + time_t duration = before_wanted - after_wanted; + + // if the resampling time is too big, extend the duration to the past + if (unlikely(resampling_time_requested > duration)) { + after_wanted = before_wanted - resampling_time_requested; + duration = before_wanted - after_wanted; + query_debug_log(":resampling after_wanted %ld", after_wanted); + } + + // if the duration is not aligned to resampling time + // extend the duration to the past, to avoid a gap at the chart + // only when the missing duration is above 1/10th of a point + if (resampling_time_requested > query_granularity && duration % resampling_time_requested) { + time_t delta = duration % resampling_time_requested; + if (delta > resampling_time_requested / 10) { + after_wanted -= resampling_time_requested - delta; + duration = before_wanted - after_wanted; + query_debug_log(":resampling2 after_wanted %ld", after_wanted); + } + } + + // the available points of the query + size_t points_available = (duration + 1) / query_granularity; + if (unlikely(points_available <= 0)) points_available = 1; + query_debug_log(":points_available %zu", points_available); + + if (points_wanted > points_available) { + points_wanted = points_available; + query_debug_log(":max points_wanted %zu", points_wanted); + } + + if(points_wanted > 86400 && !unittest_running) { + points_wanted = 86400; + query_debug_log(":absolute max points_wanted %zu", points_wanted); + } + + // calculate the desired grouping of source data points + size_t group = points_available / points_wanted; + if (group == 0) group = 1; + + // round "group" to the closest integer + if (points_available % points_wanted > points_wanted / 2) + group++; + + query_debug_log(":group %zu", group); + + if (points_wanted * group * query_granularity < (size_t)duration) { + // the grouping we are going to do, is not enough + // to cover the entire duration requested, so + // we have to change the number of points, to make sure we will + // respect the timeframe as closely as possibly + + // let's see how many points are the optimal + points_wanted = points_available / group; + + if (points_wanted * group < points_available) + points_wanted++; + + if (unlikely(points_wanted == 0)) + points_wanted = 1; + + query_debug_log(":optimal points %zu", points_wanted); + } + + // resampling_time_requested enforces a certain grouping multiple + NETDATA_DOUBLE resampling_divisor = 1.0; + size_t resampling_group = 1; + if (unlikely(resampling_time_requested > query_granularity)) { + // the points we should group to satisfy gtime + resampling_group = resampling_time_requested / query_granularity; + if (unlikely(resampling_time_requested % query_granularity)) + resampling_group++; + + query_debug_log(":resampling group %zu", resampling_group); + + // adapt group according to resampling_group + if (unlikely(group < resampling_group)) { + group = resampling_group; // do not allow grouping below the desired one + query_debug_log(":group less res %zu", group); + } + if (unlikely(group % resampling_group)) { + group += resampling_group - (group % resampling_group); // make sure group is multiple of resampling_group + query_debug_log(":group mod res %zu", group); + } + + // resampling_divisor = group / resampling_group; + resampling_divisor = (NETDATA_DOUBLE) (group * query_granularity) / (NETDATA_DOUBLE) resampling_time_requested; + query_debug_log(":resampling divisor " NETDATA_DOUBLE_FORMAT, resampling_divisor); + } + + // now that we have group, align the requested timeframe to fit it. + if (aligned && before_wanted % (group * query_granularity)) { + if (before_is_aligned_to_db_end) + before_wanted -= before_wanted % (time_t)(group * query_granularity); + else + before_wanted += (time_t)(group * query_granularity) - before_wanted % (time_t)(group * query_granularity); + query_debug_log(":align before_wanted %ld", before_wanted); + } + + after_wanted = before_wanted - (time_t)(points_wanted * group * query_granularity) + query_granularity; + query_debug_log(":final after_wanted %ld", after_wanted); + + duration = before_wanted - after_wanted; + query_debug_log(":final duration %ld", duration + 1); + + query_debug_log_fin(); + + internal_error(points_wanted != duration / (query_granularity * group) + 1, + "QUERY: points_wanted %zu is not points %zu", + points_wanted, (size_t)(duration / (query_granularity * group) + 1)); + + internal_error(group < resampling_group, + "QUERY: group %zu is less than the desired group points %zu", + group, resampling_group); + + internal_error(group > resampling_group && group % resampling_group, + "QUERY: group %zu is not a multiple of the desired group points %zu", + group, resampling_group); + + // ------------------------------------------------------------------------- + // update QUERY_TARGET with our calculations + + qt->window.after = after_wanted; + qt->window.before = before_wanted; + qt->window.relative = relative_period_requested; + qt->window.points = points_wanted; + qt->window.group = group; + qt->window.time_group_method = group_method; + qt->window.time_group_options = qt->request.time_group_options; + qt->window.query_granularity = query_granularity; + qt->window.resampling_group = resampling_group; + qt->window.resampling_divisor = resampling_divisor; + qt->window.options = options; + qt->window.tier = tier; + qt->window.aligned = aligned; + + return true; +} + +// ---------------------------------------------------------------------------- +// group by + +struct group_by_label_key { + DICTIONARY *values; +}; + +static void group_by_label_key_insert_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data) { + // add the key to our r->label_keys global keys dictionary + DICTIONARY *label_keys = data; + dictionary_set(label_keys, dictionary_acquired_item_name(item), NULL, 0); + + // create a dictionary for the values of this key + struct group_by_label_key *k = value; + k->values = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); +} + +static void group_by_label_key_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { + struct group_by_label_key *k = value; + dictionary_destroy(k->values); +} + +static int rrdlabels_traversal_cb_to_group_by_label_key(const char *name, const char *value, RRDLABEL_SRC ls __maybe_unused, void *data) { + DICTIONARY *dl = data; + struct group_by_label_key *k = dictionary_set(dl, name, NULL, sizeof(struct group_by_label_key)); + dictionary_set(k->values, value, NULL, 0); + return 1; +} + +void rrdr_json_group_by_labels(BUFFER *wb, const char *key, RRDR *r, RRDR_OPTIONS options) { + if(!r->label_keys || !r->dl) + return; + + buffer_json_member_add_object(wb, key); + + void *t; + dfe_start_read(r->label_keys, t) { + buffer_json_member_add_array(wb, t_dfe.name); + + for(size_t d = 0; d < r->d ;d++) { + if(!rrdr_dimension_should_be_exposed(r->od[d], options)) + continue; + + struct group_by_label_key *k = dictionary_get(r->dl[d], t_dfe.name); + if(k) { + buffer_json_add_array_item_array(wb); + void *tt; + dfe_start_read(k->values, tt) { + buffer_json_add_array_item_string(wb, tt_dfe.name); + } + dfe_done(tt); + buffer_json_array_close(wb); + } + else + buffer_json_add_array_item_string(wb, NULL); + } + + buffer_json_array_close(wb); + } + dfe_done(t); + + buffer_json_object_close(wb); // key +} + +static void rrd2rrdr_set_timestamps(RRDR *r) { + QUERY_TARGET *qt = r->internal.qt; + + internal_fatal(qt->window.points != r->n, "QUERY: mismatch to the number of points in qt and r"); + + r->view.group = qt->window.group; + r->view.update_every = (int) query_view_update_every(qt); + r->view.before = qt->window.before; + r->view.after = qt->window.after; + + r->time_grouping.points_wanted = qt->window.points; + r->time_grouping.resampling_group = qt->window.resampling_group; + r->time_grouping.resampling_divisor = qt->window.resampling_divisor; + + r->rows = qt->window.points; + + size_t points_wanted = qt->window.points; + time_t after_wanted = qt->window.after; + time_t before_wanted = qt->window.before; (void)before_wanted; + + time_t view_update_every = r->view.update_every; + time_t query_granularity = (time_t)(r->view.update_every / r->view.group); + + size_t rrdr_line = 0; + time_t first_point_end_time = after_wanted + view_update_every - query_granularity; + time_t now_end_time = first_point_end_time; + + while (rrdr_line < points_wanted) { + r->t[rrdr_line++] = now_end_time; + now_end_time += view_update_every; + } + + internal_fatal(r->t[0] != first_point_end_time, "QUERY: wrong first timestamp in the query"); + internal_error(r->t[points_wanted - 1] != before_wanted, + "QUERY: wrong last timestamp in the query, expected %ld, found %ld", + before_wanted, r->t[points_wanted - 1]); +} + +static void query_group_by_make_dimension_key(BUFFER *key, RRDR_GROUP_BY group_by, size_t group_by_id, QUERY_TARGET *qt, QUERY_NODE *qn, QUERY_CONTEXT *qc, QUERY_INSTANCE *qi, QUERY_DIMENSION *qd __maybe_unused, QUERY_METRIC *qm, bool query_has_percentage_of_group) { + buffer_flush(key); + if(unlikely(!query_has_percentage_of_group && qm->status & RRDR_DIMENSION_HIDDEN)) { + buffer_strcat(key, "__hidden_dimensions__"); + } + else if(unlikely(group_by & RRDR_GROUP_BY_SELECTED)) { + buffer_strcat(key, "selected"); + } + else { + if (group_by & RRDR_GROUP_BY_DIMENSION) { + buffer_fast_strcat(key, "|", 1); + buffer_strcat(key, query_metric_name(qt, qm)); + } + + if (group_by & (RRDR_GROUP_BY_INSTANCE|RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE)) { + buffer_fast_strcat(key, "|", 1); + buffer_strcat(key, string2str(query_instance_id_fqdn(qi, qt->request.version))); + } + + if (group_by & RRDR_GROUP_BY_LABEL) { + RRDLABELS *labels = rrdinstance_acquired_labels(qi->ria); + for (size_t l = 0; l < qt->group_by[group_by_id].used; l++) { + buffer_fast_strcat(key, "|", 1); + rrdlabels_get_value_to_buffer_or_unset(labels, key, qt->group_by[group_by_id].label_keys[l], "[unset]"); + } + } + + if (group_by & RRDR_GROUP_BY_NODE) { + buffer_fast_strcat(key, "|", 1); + buffer_strcat(key, qn->rrdhost->machine_guid); + } + + if (group_by & RRDR_GROUP_BY_CONTEXT) { + buffer_fast_strcat(key, "|", 1); + buffer_strcat(key, rrdcontext_acquired_id(qc->rca)); + } + + if (group_by & RRDR_GROUP_BY_UNITS) { + buffer_fast_strcat(key, "|", 1); + buffer_strcat(key, query_target_has_percentage_units(qt) ? "%" : rrdinstance_acquired_units(qi->ria)); + } + } +} + +static void query_group_by_make_dimension_id(BUFFER *key, RRDR_GROUP_BY group_by, size_t group_by_id, QUERY_TARGET *qt, QUERY_NODE *qn, QUERY_CONTEXT *qc, QUERY_INSTANCE *qi, QUERY_DIMENSION *qd __maybe_unused, QUERY_METRIC *qm, bool query_has_percentage_of_group) { + buffer_flush(key); + if(unlikely(!query_has_percentage_of_group && qm->status & RRDR_DIMENSION_HIDDEN)) { + buffer_strcat(key, "__hidden_dimensions__"); + } + else if(unlikely(group_by & RRDR_GROUP_BY_SELECTED)) { + buffer_strcat(key, "selected"); + } + else { + if (group_by & RRDR_GROUP_BY_DIMENSION) { + buffer_strcat(key, query_metric_name(qt, qm)); + } + + if (group_by & (RRDR_GROUP_BY_INSTANCE|RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE)) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + if (group_by & RRDR_GROUP_BY_NODE) + buffer_strcat(key, rrdinstance_acquired_id(qi->ria)); + else + buffer_strcat(key, string2str(query_instance_id_fqdn(qi, qt->request.version))); + } + + if (group_by & RRDR_GROUP_BY_LABEL) { + RRDLABELS *labels = rrdinstance_acquired_labels(qi->ria); + for (size_t l = 0; l < qt->group_by[group_by_id].used; l++) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + rrdlabels_get_value_to_buffer_or_unset(labels, key, qt->group_by[group_by_id].label_keys[l], "[unset]"); + } + } + + if (group_by & RRDR_GROUP_BY_NODE) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, qn->rrdhost->machine_guid); + } + + if (group_by & RRDR_GROUP_BY_CONTEXT) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, rrdcontext_acquired_id(qc->rca)); + } + + if (group_by & RRDR_GROUP_BY_UNITS) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, query_target_has_percentage_units(qt) ? "%" : rrdinstance_acquired_units(qi->ria)); + } + } +} + +static void query_group_by_make_dimension_name(BUFFER *key, RRDR_GROUP_BY group_by, size_t group_by_id, QUERY_TARGET *qt, QUERY_NODE *qn, QUERY_CONTEXT *qc, QUERY_INSTANCE *qi, QUERY_DIMENSION *qd __maybe_unused, QUERY_METRIC *qm, bool query_has_percentage_of_group) { + buffer_flush(key); + if(unlikely(!query_has_percentage_of_group && qm->status & RRDR_DIMENSION_HIDDEN)) { + buffer_strcat(key, "__hidden_dimensions__"); + } + else if(unlikely(group_by & RRDR_GROUP_BY_SELECTED)) { + buffer_strcat(key, "selected"); + } + else { + if (group_by & RRDR_GROUP_BY_DIMENSION) { + buffer_strcat(key, query_metric_name(qt, qm)); + } + + if (group_by & (RRDR_GROUP_BY_INSTANCE|RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE)) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + if (group_by & RRDR_GROUP_BY_NODE) + buffer_strcat(key, rrdinstance_acquired_name(qi->ria)); + else + buffer_strcat(key, string2str(query_instance_name_fqdn(qi, qt->request.version))); + } + + if (group_by & RRDR_GROUP_BY_LABEL) { + RRDLABELS *labels = rrdinstance_acquired_labels(qi->ria); + for (size_t l = 0; l < qt->group_by[group_by_id].used; l++) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + rrdlabels_get_value_to_buffer_or_unset(labels, key, qt->group_by[group_by_id].label_keys[l], "[unset]"); + } + } + + if (group_by & RRDR_GROUP_BY_NODE) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, rrdhost_hostname(qn->rrdhost)); + } + + if (group_by & RRDR_GROUP_BY_CONTEXT) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, rrdcontext_acquired_id(qc->rca)); + } + + if (group_by & RRDR_GROUP_BY_UNITS) { + if (buffer_strlen(key) != 0) + buffer_fast_strcat(key, ",", 1); + + buffer_strcat(key, query_target_has_percentage_units(qt) ? "%" : rrdinstance_acquired_units(qi->ria)); + } + } +} + +struct rrdr_group_by_entry { + size_t priority; + size_t count; + STRING *id; + STRING *name; + STRING *units; + RRDR_DIMENSION_FLAGS od; + DICTIONARY *dl; +}; + +static RRDR *rrd2rrdr_group_by_initialize(ONEWAYALLOC *owa, QUERY_TARGET *qt) { + RRDR *r_tmp = NULL; + RRDR_OPTIONS options = qt->window.options; + + if(qt->request.version < 2) { + // v1 query + RRDR *r = rrdr_create(owa, qt, qt->query.used, qt->window.points); + if(unlikely(!r)) { + internal_error(true, "QUERY: cannot create RRDR for %s, after=%ld, before=%ld, dimensions=%u, points=%zu", + qt->id, qt->window.after, qt->window.before, qt->query.used, qt->window.points); + return NULL; + } + r->group_by.r = NULL; + + for(size_t d = 0; d < qt->query.used ; d++) { + QUERY_METRIC *qm = query_metric(qt, d); + QUERY_DIMENSION *qd = query_dimension(qt, qm->link.query_dimension_id); + r->di[d] = rrdmetric_acquired_id_dup(qd->rma); + r->dn[d] = rrdmetric_acquired_name_dup(qd->rma); + } + + rrd2rrdr_set_timestamps(r); + return r; + } + // v2 query + + // parse all the group-by label keys + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if (qt->request.group_by[g].group_by & RRDR_GROUP_BY_LABEL && + qt->request.group_by[g].group_by_label && *qt->request.group_by[g].group_by_label) + qt->group_by[g].used = quoted_strings_splitter_query_group_by_label( + qt->request.group_by[g].group_by_label, qt->group_by[g].label_keys, + GROUP_BY_MAX_LABEL_KEYS); + + if (!qt->group_by[g].used) + qt->request.group_by[g].group_by &= ~RRDR_GROUP_BY_LABEL; + } + + // make sure there are valid group-by methods + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if(!(qt->request.group_by[g].group_by & SUPPORTED_GROUP_BY_METHODS)) + qt->request.group_by[g].group_by = (g == 0) ? RRDR_GROUP_BY_DIMENSION : RRDR_GROUP_BY_NONE; + } + + bool query_has_percentage_of_group = query_target_has_percentage_of_group(qt); + + // merge all group-by options to upper levels, + // so that the top level has all the groupings of the inner levels, + // and each subsequent level has all the groupings of its inner levels. + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES - 1 ;g++) { + if(qt->request.group_by[g].group_by == RRDR_GROUP_BY_NONE) + continue; + + if(qt->request.group_by[g].group_by == RRDR_GROUP_BY_SELECTED) { + for (size_t r = g + 1; r < MAX_QUERY_GROUP_BY_PASSES; r++) + qt->request.group_by[r].group_by = RRDR_GROUP_BY_NONE; + } + else { + for (size_t r = g + 1; r < MAX_QUERY_GROUP_BY_PASSES; r++) { + if (qt->request.group_by[r].group_by == RRDR_GROUP_BY_NONE) + continue; + + if (qt->request.group_by[r].group_by != RRDR_GROUP_BY_SELECTED) { + if(qt->request.group_by[r].group_by & RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE) + qt->request.group_by[g].group_by |= RRDR_GROUP_BY_INSTANCE; + else + qt->request.group_by[g].group_by |= qt->request.group_by[r].group_by; + + if(qt->request.group_by[r].group_by & RRDR_GROUP_BY_LABEL) { + for (size_t lr = 0; lr < qt->group_by[r].used; lr++) { + bool found = false; + for (size_t lg = 0; lg < qt->group_by[g].used; lg++) { + if (strcmp(qt->group_by[g].label_keys[lg], qt->group_by[r].label_keys[lr]) == 0) { + found = true; + break; + } + } + + if (!found && qt->group_by[g].used < GROUP_BY_MAX_LABEL_KEYS * MAX_QUERY_GROUP_BY_PASSES) + qt->group_by[g].label_keys[qt->group_by[g].used++] = qt->group_by[r].label_keys[lr]; + } + } + } + } + } + } + + int added = 0; + RRDR *first_r = NULL, *last_r = NULL; + BUFFER *key = buffer_create(0, NULL); + struct rrdr_group_by_entry *entries = onewayalloc_mallocz(owa, qt->query.used * sizeof(struct rrdr_group_by_entry)); + DICTIONARY *groups = dictionary_create(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE); + DICTIONARY *label_keys = NULL; + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + RRDR_GROUP_BY group_by = qt->request.group_by[g].group_by; + RRDR_GROUP_BY_FUNCTION aggregation_method = qt->request.group_by[g].aggregation; + + if(group_by == RRDR_GROUP_BY_NONE) + break; + + memset(entries, 0, qt->query.used * sizeof(struct rrdr_group_by_entry)); + dictionary_flush(groups); + added = 0; + + size_t hidden_dimensions = 0; + bool final_grouping = (g == MAX_QUERY_GROUP_BY_PASSES - 1 || qt->request.group_by[g + 1].group_by == RRDR_GROUP_BY_NONE) ? true : false; + + if (final_grouping && (options & RRDR_OPTION_GROUP_BY_LABELS)) + label_keys = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, 0); + + QUERY_INSTANCE *last_qi = NULL; + size_t priority = 0; + time_t update_every_max = 0; + for (size_t d = 0; d < qt->query.used; d++) { + QUERY_METRIC *qm = query_metric(qt, d); + QUERY_DIMENSION *qd = query_dimension(qt, qm->link.query_dimension_id); + QUERY_INSTANCE *qi = query_instance(qt, qm->link.query_instance_id); + QUERY_CONTEXT *qc = query_context(qt, qm->link.query_context_id); + QUERY_NODE *qn = query_node(qt, qm->link.query_node_id); + + if (qi != last_qi) { + last_qi = qi; + + time_t update_every = rrdinstance_acquired_update_every(qi->ria); + if (update_every > update_every_max) + update_every_max = update_every; + } + + priority = qd->priority; + + if(qm->status & RRDR_DIMENSION_HIDDEN) + hidden_dimensions++; + + // -------------------------------------------------------------------- + // generate the group by key + + query_group_by_make_dimension_key(key, group_by, g, qt, qn, qc, qi, qd, qm, query_has_percentage_of_group); + + // lookup the key in the dictionary + + int pos = -1; + int *set = dictionary_set(groups, buffer_tostring(key), &pos, sizeof(pos)); + if (*set == -1) { + // the key just added to the dictionary + + *set = pos = added++; + + // ---------------------------------------------------------------- + // generate the dimension id + + query_group_by_make_dimension_id(key, group_by, g, qt, qn, qc, qi, qd, qm, query_has_percentage_of_group); + entries[pos].id = string_strdupz(buffer_tostring(key)); + + // ---------------------------------------------------------------- + // generate the dimension name + + query_group_by_make_dimension_name(key, group_by, g, qt, qn, qc, qi, qd, qm, query_has_percentage_of_group); + entries[pos].name = string_strdupz(buffer_tostring(key)); + + // add the rest of the info + entries[pos].units = rrdinstance_acquired_units_dup(qi->ria); + entries[pos].priority = priority; + + if (label_keys) { + entries[pos].dl = dictionary_create_advanced( + DICT_OPTION_SINGLE_THREADED | DICT_OPTION_FIXED_SIZE | DICT_OPTION_DONT_OVERWRITE_VALUE, + NULL, sizeof(struct group_by_label_key)); + dictionary_register_insert_callback(entries[pos].dl, group_by_label_key_insert_cb, label_keys); + dictionary_register_delete_callback(entries[pos].dl, group_by_label_key_delete_cb, label_keys); + } + } else { + // the key found in the dictionary + pos = *set; + } + + entries[pos].count++; + + if (unlikely(priority < entries[pos].priority)) + entries[pos].priority = priority; + + if(g > 0) + last_r->dgbs[qm->grouped_as.slot] = pos; + else + qm->grouped_as.first_slot = pos; + + qm->grouped_as.slot = pos; + qm->grouped_as.id = entries[pos].id; + qm->grouped_as.name = entries[pos].name; + qm->grouped_as.units = entries[pos].units; + + // copy the dimension flags decided by the query target + // we need this, because if a dimension is explicitly selected + // the query target adds to it the non-zero flag + qm->status |= RRDR_DIMENSION_GROUPED; + + if(query_has_percentage_of_group) + // when the query has percentage of group + // there will be no hidden dimensions in the final query, + // so we have to remove the hidden flag from all dimensions + entries[pos].od |= qm->status & ~RRDR_DIMENSION_HIDDEN; + else + entries[pos].od |= qm->status; + + if (entries[pos].dl) + rrdlabels_walkthrough_read(rrdinstance_acquired_labels(qi->ria), + rrdlabels_traversal_cb_to_group_by_label_key, entries[pos].dl); + } + + RRDR *r = rrdr_create(owa, qt, added, qt->window.points); + if (!r) { + internal_error(true, + "QUERY: cannot create group by RRDR for %s, after=%ld, before=%ld, dimensions=%d, points=%zu", + qt->id, qt->window.after, qt->window.before, added, qt->window.points); + goto cleanup; + } + // prevent double free at cleanup in case of error + added = 0; + + // link this RRDR + if(!last_r) + first_r = last_r = r; + else + last_r->group_by.r = r; + + last_r = r; + + rrd2rrdr_set_timestamps(r); + r->dp = onewayalloc_callocz(owa, r->d, sizeof(*r->dp)); + r->dview = onewayalloc_callocz(owa, r->d, sizeof(*r->dview)); + r->dgbc = onewayalloc_callocz(owa, r->d, sizeof(*r->dgbc)); + r->gbc = onewayalloc_callocz(owa, r->n * r->d, sizeof(*r->gbc)); + r->dqp = onewayalloc_callocz(owa, r->d, sizeof(STORAGE_POINT)); + + if(hidden_dimensions && ((group_by & RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE) || (aggregation_method == RRDR_GROUP_BY_FUNCTION_PERCENTAGE))) + // this is where we are going to group the hidden dimensions + r->vh = onewayalloc_mallocz(owa, r->n * r->d * sizeof(*r->vh)); + + if(!final_grouping) + // this is where we are going to store the slot in the next RRDR + // that we are going to group by the dimension of this RRDR + r->dgbs = onewayalloc_callocz(owa, r->d, sizeof(*r->dgbs)); + + if (label_keys) { + r->dl = onewayalloc_callocz(owa, r->d, sizeof(DICTIONARY *)); + r->label_keys = label_keys; + label_keys = NULL; + } + + // zero r (dimension options, names, and ids) + // this is required, because group-by may lead to empty dimensions + for (size_t d = 0; d < r->d; d++) { + r->di[d] = entries[d].id; + r->dn[d] = entries[d].name; + + r->od[d] = entries[d].od; + r->du[d] = entries[d].units; + r->dp[d] = entries[d].priority; + r->dgbc[d] = entries[d].count; + + if (r->dl) + r->dl[d] = entries[d].dl; + } + + // initialize partial trimming + r->partial_data_trimming.max_update_every = update_every_max * 2; + r->partial_data_trimming.expected_after = + (!query_target_aggregatable(qt) && + qt->window.before >= qt->window.now - r->partial_data_trimming.max_update_every) ? + qt->window.before - r->partial_data_trimming.max_update_every : + qt->window.before; + r->partial_data_trimming.trimmed_after = qt->window.before; + + // make all values empty + for (size_t i = 0; i != r->n; i++) { + NETDATA_DOUBLE *cn = &r->v[i * r->d]; + RRDR_VALUE_FLAGS *co = &r->o[i * r->d]; + NETDATA_DOUBLE *ar = &r->ar[i * r->d]; + NETDATA_DOUBLE *vh = r->vh ? &r->vh[i * r->d] : NULL; + + for (size_t d = 0; d < r->d; d++) { + cn[d] = NAN; + ar[d] = 0.0; + co[d] = RRDR_VALUE_EMPTY; + + if(vh) + vh[d] = NAN; + } + } + } + + if(!first_r || !last_r) + goto cleanup; + + r_tmp = rrdr_create(owa, qt, 1, qt->window.points); + if (!r_tmp) { + internal_error(true, + "QUERY: cannot create group by temporary RRDR for %s, after=%ld, before=%ld, dimensions=%d, points=%zu", + qt->id, qt->window.after, qt->window.before, 1, qt->window.points); + goto cleanup; + } + rrd2rrdr_set_timestamps(r_tmp); + r_tmp->group_by.r = first_r; + +cleanup: + if(!first_r || !last_r || !r_tmp) { + if(r_tmp) { + r_tmp->group_by.r = NULL; + rrdr_free(owa, r_tmp); + } + + if(first_r) { + RRDR *r = first_r; + while (r) { + r_tmp = r->group_by.r; + r->group_by.r = NULL; + rrdr_free(owa, r); + r = r_tmp; + } + } + + if(entries && added) { + for (int d = 0; d < added; d++) { + string_freez(entries[d].id); + string_freez(entries[d].name); + string_freez(entries[d].units); + dictionary_destroy(entries[d].dl); + } + } + dictionary_destroy(label_keys); + + first_r = last_r = r_tmp = NULL; + } + + buffer_free(key); + onewayalloc_freez(owa, entries); + dictionary_destroy(groups); + + return r_tmp; +} + +static void rrd2rrdr_group_by_add_metric(RRDR *r_dst, size_t d_dst, RRDR *r_tmp, size_t d_tmp, + RRDR_GROUP_BY_FUNCTION group_by_aggregate_function, + STORAGE_POINT *query_points, size_t pass __maybe_unused) { + if(!r_tmp || r_dst == r_tmp || !(r_tmp->od[d_tmp] & RRDR_DIMENSION_QUERIED)) + return; + + internal_fatal(r_dst->n != r_tmp->n, "QUERY: group-by source and destination do not have the same number of rows"); + internal_fatal(d_dst >= r_dst->d, "QUERY: group-by destination dimension number exceeds destination RRDR size"); + internal_fatal(d_tmp >= r_tmp->d, "QUERY: group-by source dimension number exceeds source RRDR size"); + internal_fatal(!r_dst->dqp, "QUERY: group-by destination is not properly prepared (missing dqp array)"); + internal_fatal(!r_dst->gbc, "QUERY: group-by destination is not properly prepared (missing gbc array)"); + + bool hidden_dimension_on_percentage_of_group = (r_tmp->od[d_tmp] & RRDR_DIMENSION_HIDDEN) && r_dst->vh; + + if(!hidden_dimension_on_percentage_of_group) { + r_dst->od[d_dst] |= r_tmp->od[d_tmp]; + storage_point_merge_to(r_dst->dqp[d_dst], *query_points); + } + + // do the group_by + for(size_t i = 0; i != rrdr_rows(r_tmp) ; i++) { + + size_t idx_tmp = i * r_tmp->d + d_tmp; + NETDATA_DOUBLE n_tmp = r_tmp->v[ idx_tmp ]; + RRDR_VALUE_FLAGS o_tmp = r_tmp->o[ idx_tmp ]; + NETDATA_DOUBLE ar_tmp = r_tmp->ar[ idx_tmp ]; + + if(o_tmp & RRDR_VALUE_EMPTY) + continue; + + size_t idx_dst = i * r_dst->d + d_dst; + NETDATA_DOUBLE *cn = (hidden_dimension_on_percentage_of_group) ? &r_dst->vh[ idx_dst ] : &r_dst->v[ idx_dst ]; + RRDR_VALUE_FLAGS *co = &r_dst->o[ idx_dst ]; + NETDATA_DOUBLE *ar = &r_dst->ar[ idx_dst ]; + uint32_t *gbc = &r_dst->gbc[ idx_dst ]; + + switch(group_by_aggregate_function) { + default: + case RRDR_GROUP_BY_FUNCTION_AVERAGE: + case RRDR_GROUP_BY_FUNCTION_SUM: + case RRDR_GROUP_BY_FUNCTION_PERCENTAGE: + if(isnan(*cn)) + *cn = n_tmp; + else + *cn += n_tmp; + break; + + case RRDR_GROUP_BY_FUNCTION_MIN: + if(isnan(*cn) || n_tmp < *cn) + *cn = n_tmp; + break; + + case RRDR_GROUP_BY_FUNCTION_MAX: + if(isnan(*cn) || n_tmp > *cn) + *cn = n_tmp; + break; + } + + if(!hidden_dimension_on_percentage_of_group) { + *co &= ~RRDR_VALUE_EMPTY; + *co |= (o_tmp & (RRDR_VALUE_RESET | RRDR_VALUE_PARTIAL)); + *ar += ar_tmp; + (*gbc)++; + } + } +} + +static void rrdr2rrdr_group_by_partial_trimming(RRDR *r) { + time_t trimmable_after = r->partial_data_trimming.expected_after; + + // find the point just before the trimmable ones + ssize_t i = (ssize_t)r->n - 1; + for( ; i >= 0 ;i--) { + if (r->t[i] < trimmable_after) + break; + } + + if(unlikely(i < 0)) + return; + + // internal_error(true, "Found trimmable index %zd (from 0 to %zu)", i, r->n - 1); + + size_t last_row_gbc = 0; + for (; i < (ssize_t)r->n; i++) { + size_t row_gbc = 0; + for (size_t d = 0; d < r->d; d++) { + if (unlikely(!(r->od[d] & RRDR_DIMENSION_QUERIED))) + continue; + + row_gbc += r->gbc[ i * r->d + d ]; + } + + // internal_error(true, "GBC of index %zd is %zu", i, row_gbc); + + if (unlikely(r->t[i] >= trimmable_after && (row_gbc < last_row_gbc || !row_gbc))) { + // discard the rest of the points + // internal_error(true, "Discarding points %zd to %zu", i, r->n - 1); + r->partial_data_trimming.trimmed_after = r->t[i]; + r->rows = i; + break; + } + else + last_row_gbc = row_gbc; + } +} + +static void rrdr2rrdr_group_by_calculate_percentage_of_group(RRDR *r) { + if(!r->vh) + return; + + if(query_target_aggregatable(r->internal.qt) && query_has_group_by_aggregation_percentage(r->internal.qt)) + return; + + for(size_t i = 0; i < r->n ;i++) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + NETDATA_DOUBLE *ch = &r->vh[ i * r->d ]; + + for(size_t d = 0; d < r->d ;d++) { + NETDATA_DOUBLE n = cn[d]; + NETDATA_DOUBLE h = ch[d]; + + if(isnan(n)) + cn[d] = 0.0; + + else if(isnan(h)) + cn[d] = 100.0; + + else + cn[d] = n * 100.0 / (n + h); + } + } +} + +static void rrd2rrdr_convert_values_to_percentage_of_total(RRDR *r) { + if(!(r->internal.qt->window.options & RRDR_OPTION_PERCENTAGE) || query_target_aggregatable(r->internal.qt)) + return; + + size_t global_min_max_values = 0; + NETDATA_DOUBLE global_min = NAN, global_max = NAN; + + for(size_t i = 0; i != r->n ;i++) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + RRDR_VALUE_FLAGS *co = &r->o[ i * r->d ]; + + NETDATA_DOUBLE total = 0; + for (size_t d = 0; d < r->d; d++) { + if (unlikely(!(r->od[d] & RRDR_DIMENSION_QUERIED))) + continue; + + if(co[d] & RRDR_VALUE_EMPTY) + continue; + + total += cn[d]; + } + + if(total == 0.0) + total = 1.0; + + for (size_t d = 0; d < r->d; d++) { + if (unlikely(!(r->od[d] & RRDR_DIMENSION_QUERIED))) + continue; + + if(co[d] & RRDR_VALUE_EMPTY) + continue; + + NETDATA_DOUBLE n = cn[d]; + n = cn[d] = n * 100.0 / total; + + if(unlikely(!global_min_max_values++)) + global_min = global_max = n; + else { + if(n < global_min) + global_min = n; + if(n > global_max) + global_max = n; + } + } + } + + r->view.min = global_min; + r->view.max = global_max; + + if(!r->dview) + // v1 query + return; + + // v2 query + + for (size_t d = 0; d < r->d; d++) { + if (unlikely(!(r->od[d] & RRDR_DIMENSION_QUERIED))) + continue; + + size_t count = 0; + NETDATA_DOUBLE min = 0.0, max = 0.0, sum = 0.0, ars = 0.0; + for(size_t i = 0; i != r->rows ;i++) { // we use r->rows to respect trimming + size_t idx = i * r->d + d; + + RRDR_VALUE_FLAGS o = r->o[ idx ]; + + if (o & RRDR_VALUE_EMPTY) + continue; + + NETDATA_DOUBLE ar = r->ar[ idx ]; + ars += ar; + + NETDATA_DOUBLE n = r->v[ idx ]; + sum += n; + + if(!count++) + min = max = n; + else { + if(n < min) + min = n; + if(n > max) + max = n; + } + } + + r->dview[d] = (STORAGE_POINT) { + .sum = sum, + .count = count, + .min = min, + .max = max, + .anomaly_count = (size_t)(ars * (NETDATA_DOUBLE)count), + }; + } +} + +static RRDR *rrd2rrdr_group_by_finalize(RRDR *r_tmp) { + QUERY_TARGET *qt = r_tmp->internal.qt; + + if(!r_tmp->group_by.r) { + // v1 query + rrd2rrdr_convert_values_to_percentage_of_total(r_tmp); + return r_tmp; + } + // v2 query + + // do the additional passes on RRDRs + RRDR *last_r = r_tmp->group_by.r; + rrdr2rrdr_group_by_calculate_percentage_of_group(last_r); + + RRDR *r = last_r->group_by.r; + size_t pass = 0; + while(r) { + pass++; + for(size_t d = 0; d < last_r->d ;d++) { + rrd2rrdr_group_by_add_metric(r, last_r->dgbs[d], last_r, d, + qt->request.group_by[pass].aggregation, + &last_r->dqp[d], pass); + } + rrdr2rrdr_group_by_calculate_percentage_of_group(r); + + last_r = r; + r = last_r->group_by.r; + } + + // free all RRDRs except the last one + r = r_tmp; + while(r != last_r) { + r_tmp = r->group_by.r; + r->group_by.r = NULL; + rrdr_free(r->internal.owa, r); + r = r_tmp; + } + r = last_r; + + // find the final aggregation + RRDR_GROUP_BY_FUNCTION aggregation = qt->request.group_by[0].aggregation; + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) + if(qt->request.group_by[g].group_by != RRDR_GROUP_BY_NONE) + aggregation = qt->request.group_by[g].aggregation; + + if(!query_target_aggregatable(qt) && r->partial_data_trimming.expected_after < qt->window.before) + rrdr2rrdr_group_by_partial_trimming(r); + + // apply averaging, remove RRDR_VALUE_EMPTY, find the non-zero dimensions, min and max + size_t global_min_max_values = 0; + size_t dimensions_nonzero = 0; + NETDATA_DOUBLE global_min = NAN, global_max = NAN; + for (size_t d = 0; d < r->d; d++) { + if (unlikely(!(r->od[d] & RRDR_DIMENSION_QUERIED))) + continue; + + size_t points_nonzero = 0; + NETDATA_DOUBLE min = 0, max = 0, sum = 0, ars = 0; + size_t count = 0; + + for(size_t i = 0; i != r->n ;i++) { + size_t idx = i * r->d + d; + + NETDATA_DOUBLE *cn = &r->v[ idx ]; + RRDR_VALUE_FLAGS *co = &r->o[ idx ]; + NETDATA_DOUBLE *ar = &r->ar[ idx ]; + uint32_t gbc = r->gbc[ idx ]; + + if(likely(gbc)) { + *co &= ~RRDR_VALUE_EMPTY; + + if(gbc != r->dgbc[d]) + *co |= RRDR_VALUE_PARTIAL; + + NETDATA_DOUBLE n; + + sum += *cn; + ars += *ar; + + if(aggregation == RRDR_GROUP_BY_FUNCTION_AVERAGE && !query_target_aggregatable(qt)) + n = (*cn /= gbc); + else + n = *cn; + + if(!query_target_aggregatable(qt)) + *ar /= gbc; + + if(islessgreater(n, 0.0)) + points_nonzero++; + + if(unlikely(!count)) + min = max = n; + else { + if(n < min) + min = n; + + if(n > max) + max = n; + } + + if(unlikely(!global_min_max_values++)) + global_min = global_max = n; + else { + if(n < global_min) + global_min = n; + + if(n > global_max) + global_max = n; + } + + count += gbc; + } + } + + if(points_nonzero) { + r->od[d] |= RRDR_DIMENSION_NONZERO; + dimensions_nonzero++; + } + + r->dview[d] = (STORAGE_POINT) { + .sum = sum, + .count = count, + .min = min, + .max = max, + .anomaly_count = (size_t)(ars * RRDR_DVIEW_ANOMALY_COUNT_MULTIPLIER / 100.0), + }; + } + + r->view.min = global_min; + r->view.max = global_max; + + if(!dimensions_nonzero && (qt->window.options & RRDR_OPTION_NONZERO)) { + // all dimensions are zero + // remove the nonzero option + qt->window.options &= ~RRDR_OPTION_NONZERO; + } + + rrd2rrdr_convert_values_to_percentage_of_total(r); + + // update query instance counts in query host and query context + { + size_t h = 0, c = 0, i = 0; + for(; h < qt->nodes.used ; h++) { + QUERY_NODE *qn = &qt->nodes.array[h]; + + for(; c < qt->contexts.used ;c++) { + QUERY_CONTEXT *qc = &qt->contexts.array[c]; + + if(!rrdcontext_acquired_belongs_to_host(qc->rca, qn->rrdhost)) + break; + + for(; i < qt->instances.used ;i++) { + QUERY_INSTANCE *qi = &qt->instances.array[i]; + + if(!rrdinstance_acquired_belongs_to_context(qi->ria, qc->rca)) + break; + + if(qi->metrics.queried) { + qc->instances.queried++; + qn->instances.queried++; + } + else if(qi->metrics.failed) { + qc->instances.failed++; + qn->instances.failed++; + } + } + } + } + } + + return r; +} + +// ---------------------------------------------------------------------------- +// query entry point + +RRDR *rrd2rrdr_legacy( + ONEWAYALLOC *owa, + RRDSET *st, size_t points, time_t after, time_t before, + RRDR_TIME_GROUPING group_method, time_t resampling_time, RRDR_OPTIONS options, const char *dimensions, + const char *group_options, time_t timeout_ms, size_t tier, QUERY_SOURCE query_source, + STORAGE_PRIORITY priority) { + + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .st = st, + .points = points, + .after = after, + .before = before, + .time_group_method = group_method, + .resampling_time = resampling_time, + .options = options, + .dimensions = dimensions, + .time_group_options = group_options, + .timeout_ms = timeout_ms, + .tier = tier, + .query_source = query_source, + .priority = priority, + }; + + QUERY_TARGET *qt = query_target_create(&qtr); + RRDR *r = rrd2rrdr(owa, qt); + if(!r) { + query_target_release(qt); + return NULL; + } + + r->internal.release_with_rrdr_qt = qt; + return r; +} + +RRDR *rrd2rrdr(ONEWAYALLOC *owa, QUERY_TARGET *qt) { + if(!qt || !owa) + return NULL; + + // qt.window members are the WANTED ones. + // qt.request members are the REQUESTED ones. + + RRDR *r_tmp = rrd2rrdr_group_by_initialize(owa, qt); + if(!r_tmp) + return NULL; + + // the RRDR we group-by at + RRDR *r = (r_tmp->group_by.r) ? r_tmp->group_by.r : r_tmp; + + // the final RRDR to return to callers + RRDR *last_r = r_tmp; + while(last_r->group_by.r) + last_r = last_r->group_by.r; + + if(qt->window.relative) + last_r->view.flags |= RRDR_RESULT_FLAG_RELATIVE; + else + last_r->view.flags |= RRDR_RESULT_FLAG_ABSOLUTE; + + // ------------------------------------------------------------------------- + // assign the processor functions + rrdr_set_grouping_function(r_tmp, qt->window.time_group_method); + + // allocate any memory required by the grouping method + r_tmp->time_grouping.create(r_tmp, qt->window.time_group_options); + + // ------------------------------------------------------------------------- + // do the work for each dimension + + time_t max_after = 0, min_before = 0; + size_t max_rows = 0; + + long dimensions_used = 0, dimensions_nonzero = 0; + size_t last_db_points_read = 0; + size_t last_result_points_generated = 0; + + internal_fatal(released_ops, "QUERY: released_ops should be NULL when the query starts"); + + query_progress_set_finish_line(qt->request.transaction, qt->query.used); + + QUERY_ENGINE_OPS **ops = NULL; + if(qt->query.used) + ops = onewayalloc_callocz(owa, qt->query.used, sizeof(QUERY_ENGINE_OPS *)); + + size_t capacity = libuv_worker_threads * 10; + size_t max_queries_to_prepare = (qt->query.used > (capacity - 1)) ? (capacity - 1) : qt->query.used; + size_t queries_prepared = 0; + while(queries_prepared < max_queries_to_prepare) { + // preload another query + ops[queries_prepared] = rrd2rrdr_query_ops_prep(r_tmp, queries_prepared); + queries_prepared++; + } + + QUERY_NODE *last_qn = NULL; + usec_t last_ut = now_monotonic_usec(); + usec_t last_qn_ut = last_ut; + + for(size_t d = 0; d < qt->query.used ; d++) { + QUERY_METRIC *qm = query_metric(qt, d); + QUERY_DIMENSION *qd = query_dimension(qt, qm->link.query_dimension_id); + QUERY_INSTANCE *qi = query_instance(qt, qm->link.query_instance_id); + QUERY_CONTEXT *qc = query_context(qt, qm->link.query_context_id); + QUERY_NODE *qn = query_node(qt, qm->link.query_node_id); + + usec_t now_ut = last_ut; + if(qn != last_qn) { + if(last_qn) + last_qn->duration_ut = now_ut - last_qn_ut; + + last_qn = qn; + last_qn_ut = now_ut; + } + + if(queries_prepared < qt->query.used) { + // preload another query + ops[queries_prepared] = rrd2rrdr_query_ops_prep(r_tmp, queries_prepared); + queries_prepared++; + } + + size_t dim_in_rrdr_tmp = (r_tmp != r) ? 0 : d; + + // set the query target dimension options to rrdr + r_tmp->od[dim_in_rrdr_tmp] = qm->status; + + // reset the grouping for the new dimension + r_tmp->time_grouping.reset(r_tmp); + + if(ops[d]) { + rrd2rrdr_query_execute(r_tmp, dim_in_rrdr_tmp, ops[d]); + r_tmp->od[dim_in_rrdr_tmp] |= RRDR_DIMENSION_QUERIED; + + now_ut = now_monotonic_usec(); + qm->duration_ut = now_ut - last_ut; + last_ut = now_ut; + + if(r_tmp != r) { + // copy back whatever got updated from the temporary r + + // the query updates RRDR_DIMENSION_NONZERO + qm->status = r_tmp->od[dim_in_rrdr_tmp]; + + // the query updates these + r->view.min = r_tmp->view.min; + r->view.max = r_tmp->view.max; + r->view.after = r_tmp->view.after; + r->view.before = r_tmp->view.before; + r->rows = r_tmp->rows; + + rrd2rrdr_group_by_add_metric(r, qm->grouped_as.first_slot, r_tmp, dim_in_rrdr_tmp, + qt->request.group_by[0].aggregation, &qm->query_points, 0); + } + + rrd2rrdr_query_ops_release(ops[d]); // reuse this ops allocation + ops[d] = NULL; + + qi->metrics.queried++; + qc->metrics.queried++; + qn->metrics.queried++; + + qd->status |= QUERY_STATUS_QUERIED; + qm->status |= RRDR_DIMENSION_QUERIED; + + if(qt->request.version >= 2) { + // we need to make the query points positive now + // since we will aggregate it across multiple dimensions + storage_point_make_positive(qm->query_points); + storage_point_merge_to(qi->query_points, qm->query_points); + storage_point_merge_to(qc->query_points, qm->query_points); + storage_point_merge_to(qn->query_points, qm->query_points); + storage_point_merge_to(qt->query_points, qm->query_points); + } + } + else { + qi->metrics.failed++; + qc->metrics.failed++; + qn->metrics.failed++; + + qd->status |= QUERY_STATUS_FAILED; + qm->status |= RRDR_DIMENSION_FAILED; + + continue; + } + + global_statistics_rrdr_query_completed( + 1, + r_tmp->stats.db_points_read - last_db_points_read, + r_tmp->stats.result_points_generated - last_result_points_generated, + qt->request.query_source); + + last_db_points_read = r_tmp->stats.db_points_read; + last_result_points_generated = r_tmp->stats.result_points_generated; + + if(qm->status & RRDR_DIMENSION_NONZERO) + dimensions_nonzero++; + + // verify all dimensions are aligned + if(unlikely(!dimensions_used)) { + min_before = r->view.before; + max_after = r->view.after; + max_rows = r->rows; + } + else { + if(r->view.after != max_after) { + internal_error(true, "QUERY: 'after' mismatch between dimensions for chart '%s': max is %zu, dimension '%s' has %zu", + rrdinstance_acquired_id(qi->ria), (size_t)max_after, rrdmetric_acquired_id(qd->rma), (size_t)r->view.after); + + r->view.after = (r->view.after > max_after) ? r->view.after : max_after; + } + + if(r->view.before != min_before) { + internal_error(true, "QUERY: 'before' mismatch between dimensions for chart '%s': max is %zu, dimension '%s' has %zu", + rrdinstance_acquired_id(qi->ria), (size_t)min_before, rrdmetric_acquired_id(qd->rma), (size_t)r->view.before); + + r->view.before = (r->view.before < min_before) ? r->view.before : min_before; + } + + if(r->rows != max_rows) { + internal_error(true, "QUERY: 'rows' mismatch between dimensions for chart '%s': max is %zu, dimension '%s' has %zu", + rrdinstance_acquired_id(qi->ria), (size_t)max_rows, rrdmetric_acquired_id(qd->rma), (size_t)r->rows); + + r->rows = (r->rows > max_rows) ? r->rows : max_rows; + } + } + + dimensions_used++; + + bool cancel = false; + if (qt->request.interrupt_callback && qt->request.interrupt_callback(qt->request.interrupt_callback_data)) { + cancel = true; + nd_log(NDLS_ACCESS, NDLP_NOTICE, "QUERY INTERRUPTED"); + } + + if (qt->request.timeout_ms && ((NETDATA_DOUBLE)(now_ut - qt->timings.received_ut) / 1000.0) > (NETDATA_DOUBLE)qt->request.timeout_ms) { + cancel = true; + nd_log(NDLS_ACCESS, NDLP_WARNING, "QUERY CANCELED RUNTIME EXCEEDED %0.2f ms (LIMIT %lld ms)", + (NETDATA_DOUBLE)(now_ut - qt->timings.received_ut) / 1000.0, (long long)qt->request.timeout_ms); + } + + if(cancel) { + r->view.flags |= RRDR_RESULT_FLAG_CANCEL; + + for(size_t i = d + 1; i < queries_prepared ; i++) { + if(ops[i]) { + query_planer_finalize_remaining_plans(ops[i]); + rrd2rrdr_query_ops_release(ops[i]); + ops[i] = NULL; + } + } + + break; + } + else + query_progress_done_step(qt->request.transaction, 1); + } + + // free all resources used by the grouping method + r_tmp->time_grouping.free(r_tmp); + + // get the final RRDR to send to the caller + r = rrd2rrdr_group_by_finalize(r_tmp); + +#ifdef NETDATA_INTERNAL_CHECKS + if (dimensions_used && !(r->view.flags & RRDR_RESULT_FLAG_CANCEL)) { + if(r->internal.log) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + r->internal.log); + + if(r->rows != qt->window.points) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + "got 'points' is not wanted 'points'"); + + if(qt->window.aligned && (r->view.before % query_view_update_every(qt)) != 0) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + "'before' is not aligned but alignment is required"); + + // 'after' should not be aligned, since we start inside the first group + //if(qt->window.aligned && (r->after % group) != 0) + // rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, qt->window.after, after_requested, before_wanted, before_requested, points_requested, points_wanted, after_slot, before_slot, "'after' is not aligned but alignment is required"); + + if(r->view.before != qt->window.before) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + "chart is not aligned to requested 'before'"); + + if(r->view.before != qt->window.before) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + "got 'before' is not wanted 'before'"); + + // reported 'after' varies, depending on group + if(r->view.after != qt->window.after) + rrd2rrdr_log_request_response_metadata(r, qt->window.options, qt->window.time_group_method, qt->window.aligned, qt->window.group, qt->request.resampling_time, qt->window.resampling_group, + qt->window.after, qt->request.after, qt->window.before, qt->request.before, + qt->request.points, qt->window.points, /*after_slot, before_slot,*/ + "got 'after' is not wanted 'after'"); + + } +#endif + + // free the query pipelining ops + for(size_t d = 0; d < qt->query.used ; d++) { + rrd2rrdr_query_ops_release(ops[d]); + ops[d] = NULL; + } + rrd2rrdr_query_ops_freeall(r); + internal_fatal(released_ops, "QUERY: released_ops should be NULL when the query ends"); + + onewayalloc_freez(owa, ops); + + if(likely(dimensions_used && (qt->window.options & RRDR_OPTION_NONZERO) && !dimensions_nonzero)) + // when all the dimensions are zero, we should return all of them + qt->window.options &= ~RRDR_OPTION_NONZERO; + + qt->timings.executed_ut = now_monotonic_usec(); + + return r; +} diff --git a/src/web/api/queries/query.h b/src/web/api/queries/query.h new file mode 100644 index 000000000..37202a0ba --- /dev/null +++ b/src/web/api/queries/query.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_DATA_QUERY_H +#define NETDATA_API_DATA_QUERY_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum rrdr_time_grouping { + RRDR_GROUPING_UNDEFINED = 0, + RRDR_GROUPING_AVERAGE, + RRDR_GROUPING_MIN, + RRDR_GROUPING_MAX, + RRDR_GROUPING_SUM, + RRDR_GROUPING_INCREMENTAL_SUM, + RRDR_GROUPING_TRIMMED_MEAN1, + RRDR_GROUPING_TRIMMED_MEAN2, + RRDR_GROUPING_TRIMMED_MEAN3, + RRDR_GROUPING_TRIMMED_MEAN, + RRDR_GROUPING_TRIMMED_MEAN10, + RRDR_GROUPING_TRIMMED_MEAN15, + RRDR_GROUPING_TRIMMED_MEAN20, + RRDR_GROUPING_TRIMMED_MEAN25, + RRDR_GROUPING_MEDIAN, + RRDR_GROUPING_TRIMMED_MEDIAN1, + RRDR_GROUPING_TRIMMED_MEDIAN2, + RRDR_GROUPING_TRIMMED_MEDIAN3, + RRDR_GROUPING_TRIMMED_MEDIAN, + RRDR_GROUPING_TRIMMED_MEDIAN10, + RRDR_GROUPING_TRIMMED_MEDIAN15, + RRDR_GROUPING_TRIMMED_MEDIAN20, + RRDR_GROUPING_TRIMMED_MEDIAN25, + RRDR_GROUPING_PERCENTILE25, + RRDR_GROUPING_PERCENTILE50, + RRDR_GROUPING_PERCENTILE75, + RRDR_GROUPING_PERCENTILE80, + RRDR_GROUPING_PERCENTILE90, + RRDR_GROUPING_PERCENTILE, + RRDR_GROUPING_PERCENTILE97, + RRDR_GROUPING_PERCENTILE98, + RRDR_GROUPING_PERCENTILE99, + RRDR_GROUPING_STDDEV, + RRDR_GROUPING_CV, + RRDR_GROUPING_SES, + RRDR_GROUPING_DES, + RRDR_GROUPING_COUNTIF, +} RRDR_TIME_GROUPING; + +const char *time_grouping_id2txt(RRDR_TIME_GROUPING group); +RRDR_TIME_GROUPING time_grouping_txt2id(const char *name); + +void time_grouping_init(void); +RRDR_TIME_GROUPING time_grouping_parse(const char *name, RRDR_TIME_GROUPING def); +const char *time_grouping_tostring(RRDR_TIME_GROUPING group); + +typedef enum rrdr_group_by { + RRDR_GROUP_BY_NONE = 0, + RRDR_GROUP_BY_SELECTED = (1 << 0), + RRDR_GROUP_BY_DIMENSION = (1 << 1), + RRDR_GROUP_BY_INSTANCE = (1 << 2), + RRDR_GROUP_BY_LABEL = (1 << 3), + RRDR_GROUP_BY_NODE = (1 << 4), + RRDR_GROUP_BY_CONTEXT = (1 << 5), + RRDR_GROUP_BY_UNITS = (1 << 6), + RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE = (1 << 7), +} RRDR_GROUP_BY; + +#define SUPPORTED_GROUP_BY_METHODS (\ + RRDR_GROUP_BY_SELECTED |\ + RRDR_GROUP_BY_DIMENSION |\ + RRDR_GROUP_BY_INSTANCE |\ + RRDR_GROUP_BY_LABEL |\ + RRDR_GROUP_BY_NODE |\ + RRDR_GROUP_BY_CONTEXT |\ + RRDR_GROUP_BY_UNITS |\ + RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE \ +) + +struct web_buffer; + +RRDR_GROUP_BY group_by_parse(char *s); +void buffer_json_group_by_to_array(struct web_buffer *wb, RRDR_GROUP_BY group_by); + +typedef enum rrdr_group_by_function { + RRDR_GROUP_BY_FUNCTION_AVERAGE = 0, + RRDR_GROUP_BY_FUNCTION_MIN, + RRDR_GROUP_BY_FUNCTION_MAX, + RRDR_GROUP_BY_FUNCTION_SUM, + RRDR_GROUP_BY_FUNCTION_PERCENTAGE, +} RRDR_GROUP_BY_FUNCTION; + +RRDR_GROUP_BY_FUNCTION group_by_aggregate_function_parse(const char *s); +const char *group_by_aggregate_function_to_string(RRDR_GROUP_BY_FUNCTION group_by_function); + +#ifdef __cplusplus +} +#endif + +#endif //NETDATA_API_DATA_QUERY_H diff --git a/src/web/api/queries/rrdr.c b/src/web/api/queries/rrdr.c new file mode 100644 index 000000000..2a0016891 --- /dev/null +++ b/src/web/api/queries/rrdr.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rrdr.h" + +/* +static void rrdr_dump(RRDR *r) +{ + long c, i; + RRDDIM *d; + + fprintf(stderr, "\nCHART %s (%s)\n", r->st->id, r->st->name); + + for(c = 0, d = r->st->dimensions; d ;c++, d = d->next) { + fprintf(stderr, "DIMENSION %s (%s), %s%s%s%s\n" + , d->id + , d->name + , (r->od[c] & RRDR_EMPTY)?"EMPTY ":"" + , (r->od[c] & RRDR_RESET)?"RESET ":"" + , (r->od[c] & RRDR_DIMENSION_HIDDEN)?"HIDDEN ":"" + , (r->od[c] & RRDR_DIMENSION_NONZERO)?"NONZERO ":"" + ); + } + + if(r->rows <= 0) { + fprintf(stderr, "RRDR does not have any values in it.\n"); + return; + } + + fprintf(stderr, "RRDR includes %d values in it:\n", r->rows); + + // for each line in the array + for(i = 0; i < r->rows ;i++) { + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + RRDR_DIMENSION_FLAGS *co = &r->o[ i * r->d ]; + + // print the id and the timestamp of the line + fprintf(stderr, "%ld %ld ", i + 1, r->t[i]); + + // for each dimension + for(c = 0, d = r->st->dimensions; d ;c++, d = d->next) { + if(unlikely(r->od[c] & RRDR_DIMENSION_HIDDEN)) continue; + if(unlikely(!(r->od[c] & RRDR_DIMENSION_NONZERO))) continue; + + if(co[c] & RRDR_EMPTY) + fprintf(stderr, "null "); + else + fprintf(stderr, NETDATA_DOUBLE_FORMAT " %s%s%s%s " + , cn[c] + , (co[c] & RRDR_EMPTY)?"E":" " + , (co[c] & RRDR_RESET)?"R":" " + , (co[c] & RRDR_DIMENSION_HIDDEN)?"H":" " + , (co[c] & RRDR_DIMENSION_NONZERO)?"N":" " + ); + } + + fprintf(stderr, "\n"); + } +} +*/ + +inline void rrdr_free(ONEWAYALLOC *owa, RRDR *r) { + if(unlikely(!r)) return; + + for(size_t d = 0; d < r->d ;d++) { + string_freez(r->di[d]); + string_freez(r->dn[d]); + string_freez(r->du[d]); + } + + query_target_release(r->internal.release_with_rrdr_qt); + + onewayalloc_freez(owa, r->t); + onewayalloc_freez(owa, r->v); + onewayalloc_freez(owa, r->vh); + onewayalloc_freez(owa, r->o); + onewayalloc_freez(owa, r->od); + onewayalloc_freez(owa, r->di); + onewayalloc_freez(owa, r->dn); + onewayalloc_freez(owa, r->du); + onewayalloc_freez(owa, r->dp); + onewayalloc_freez(owa, r->dview); + onewayalloc_freez(owa, r->dqp); + onewayalloc_freez(owa, r->ar); + onewayalloc_freez(owa, r->gbc); + onewayalloc_freez(owa, r->dgbc); + onewayalloc_freez(owa, r->dgbs); + + if(r->dl) { + for(size_t d = 0; d < r->d ;d++) + dictionary_destroy(r->dl[d]); + + onewayalloc_freez(owa, r->dl); + } + + dictionary_destroy(r->label_keys); + + if(r->group_by.r) { + // prevent accidental infinite recursion + r->group_by.r->group_by.r = NULL; + + // do not release qt twice + r->group_by.r->internal.qt = NULL; + + rrdr_free(owa, r->group_by.r); + } + + onewayalloc_freez(owa, r); +} + +RRDR *rrdr_create(ONEWAYALLOC *owa, QUERY_TARGET *qt, size_t dimensions, size_t points) { + if(unlikely(!qt)) + return NULL; + + // create the rrdr + RRDR *r = onewayalloc_callocz(owa, 1, sizeof(RRDR)); + r->internal.owa = owa; + r->internal.qt = qt; + + r->view.before = qt->window.before; + r->view.after = qt->window.after; + r->time_grouping.points_wanted = points; + r->d = (int)dimensions; + r->n = (int)points; + + if(points && dimensions) { + r->v = onewayalloc_mallocz(owa, points * dimensions * sizeof(NETDATA_DOUBLE)); + r->o = onewayalloc_mallocz(owa, points * dimensions * sizeof(RRDR_VALUE_FLAGS)); + r->ar = onewayalloc_mallocz(owa, points * dimensions * sizeof(NETDATA_DOUBLE)); + } + + if(points) { + r->t = onewayalloc_callocz(owa, points, sizeof(time_t)); + } + + if(dimensions) { + r->od = onewayalloc_mallocz(owa, dimensions * sizeof(RRDR_DIMENSION_FLAGS)); + r->di = onewayalloc_callocz(owa, dimensions, sizeof(STRING *)); + r->dn = onewayalloc_callocz(owa, dimensions, sizeof(STRING *)); + r->du = onewayalloc_callocz(owa, dimensions, sizeof(STRING *)); + } + + r->view.group = 1; + r->view.update_every = 1; + + return r; +} diff --git a/src/web/api/queries/rrdr.h b/src/web/api/queries/rrdr.h new file mode 100644 index 000000000..d36d3f5b3 --- /dev/null +++ b/src/web/api/queries/rrdr.h @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_QUERIES_RRDR_H +#define NETDATA_QUERIES_RRDR_H + +#include "libnetdata/libnetdata.h" +#include "web/api/queries/query.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum tier_query_fetch { + TIER_QUERY_FETCH_SUM, + TIER_QUERY_FETCH_MIN, + TIER_QUERY_FETCH_MAX, + TIER_QUERY_FETCH_AVERAGE +} TIER_QUERY_FETCH; + +typedef enum rrdr_options { + RRDR_OPTION_NONZERO = (1 << 0), // don't output dimensions with just zero values + RRDR_OPTION_REVERSED = (1 << 1), // output the rows in reverse order (oldest to newest) + RRDR_OPTION_ABSOLUTE = (1 << 2), // values positive, for DATASOURCE_SSV before summing + RRDR_OPTION_DIMS_MIN2MAX = (1 << 3), // when adding dimensions, use max - min, instead of sum + RRDR_OPTION_DIMS_AVERAGE = (1 << 4), // when adding dimensions, use average, instead of sum + RRDR_OPTION_DIMS_MIN = (1 << 5), // when adding dimensions, use minimum, instead of sum + RRDR_OPTION_DIMS_MAX = (1 << 6), // when adding dimensions, use maximum, instead of sum + RRDR_OPTION_SECONDS = (1 << 7), // output seconds, instead of dates + RRDR_OPTION_MILLISECONDS = (1 << 8), // output milliseconds, instead of dates + RRDR_OPTION_NULL2ZERO = (1 << 9), // do not show nulls, convert them to zeros + RRDR_OPTION_OBJECTSROWS = (1 << 10), // each row of values should be an object, not an array + RRDR_OPTION_GOOGLE_JSON = (1 << 11), // comply with google JSON/JSONP specs + RRDR_OPTION_JSON_WRAP = (1 << 12), // wrap the response in a JSON header with info about the result + RRDR_OPTION_LABEL_QUOTES = (1 << 13), // in CSV output, wrap header labels in double quotes + RRDR_OPTION_PERCENTAGE = (1 << 14), // give values as percentage of total + RRDR_OPTION_NOT_ALIGNED = (1 << 15), // do not align charts for persistent timeframes + RRDR_OPTION_DISPLAY_ABS = (1 << 16), // for badges, display the absolute value, but calculate colors with sign + RRDR_OPTION_MATCH_IDS = (1 << 17), // when filtering dimensions, match only IDs + RRDR_OPTION_MATCH_NAMES = (1 << 18), // when filtering dimensions, match only names + RRDR_OPTION_NATURAL_POINTS = (1 << 19), // return the natural points of the database + RRDR_OPTION_VIRTUAL_POINTS = (1 << 20), // return virtual points + RRDR_OPTION_ANOMALY_BIT = (1 << 21), // Return the anomaly bit stored in each collected_number + RRDR_OPTION_RETURN_RAW = (1 << 22), // Return raw data for aggregating across multiple nodes + RRDR_OPTION_RETURN_JWAR = (1 << 23), // Return anomaly rates in jsonwrap + RRDR_OPTION_SELECTED_TIER = (1 << 24), // Use the selected tier for the query + RRDR_OPTION_ALL_DIMENSIONS = (1 << 25), // Return the full dimensions list + RRDR_OPTION_SHOW_DETAILS = (1 << 26), // v2 returns detailed object tree + RRDR_OPTION_DEBUG = (1 << 27), // v2 returns request description + RRDR_OPTION_MINIFY = (1 << 28), // remove JSON spaces and newlines from JSON output + RRDR_OPTION_GROUP_BY_LABELS = (1 << 29), // v2 returns flattened labels per dimension of the chart + + // internal ones - not to be exposed to the API + RRDR_OPTION_INTERNAL_AR = (1 << 31), // internal use only, to let the formatters know we want to render the anomaly rate +} RRDR_OPTIONS; + +typedef enum context_v2_options { + CONTEXT_V2_OPTION_MINIFY = (1 << 0), // remove JSON spaces and newlines from JSON output + CONTEXT_V2_OPTION_DEBUG = (1 << 1), // show the request + CONTEXT_V2_OPTION_ALERTS_WITH_CONFIGURATIONS = (1 << 2), // include alert configurations (used by /api/v2/alert_transitions) + CONTEXT_V2_OPTION_ALERTS_WITH_INSTANCES = (1 << 3), // include alert instances (used by /api/v2/alerts) + CONTEXT_V2_OPTION_ALERTS_WITH_VALUES = (1 << 4), // include alert latest values (used by /api/v2/alerts) + CONTEXT_V2_OPTION_ALERTS_WITH_SUMMARY = (1 << 5), // include alerts summary counters (used by /api/v2/alerts) +} CONTEXTS_V2_OPTIONS; + +typedef enum context_v2_alert_status { + CONTEXT_V2_ALERT_UNINITIALIZED = (1 << 5), // include UNINITIALIZED alerts + CONTEXT_V2_ALERT_UNDEFINED = (1 << 6), // include UNDEFINED alerts + CONTEXT_V2_ALERT_CLEAR = (1 << 7), // include CLEAR alerts + CONTEXT_V2_ALERT_RAISED = (1 << 8), // include WARNING & CRITICAL alerts + CONTEXT_V2_ALERT_WARNING = (1 << 9), // include WARNING alerts + CONTEXT_V2_ALERT_CRITICAL = (1 << 10), // include CRITICAL alerts +} CONTEXTS_V2_ALERT_STATUS; + +#define CONTEXTS_V2_ALERT_STATUSES (CONTEXT_V2_ALERT_UNINITIALIZED|CONTEXT_V2_ALERT_UNDEFINED|CONTEXT_V2_ALERT_CLEAR|CONTEXT_V2_ALERT_RAISED|CONTEXT_V2_ALERT_WARNING|CONTEXT_V2_ALERT_CRITICAL) + +typedef enum __attribute__ ((__packed__)) rrdr_value_flag { + + // IMPORTANT: + // THIS IS AN AGREED BIT MAP BETWEEN AGENT, CLOUD FRONT-END AND CLOUD BACK-END + // DO NOT CHANGE THE MAPPINGS ! + + RRDR_VALUE_NOTHING = 0, // no flag set (a good default) + RRDR_VALUE_EMPTY = (1 << 0), // the database value is empty + RRDR_VALUE_RESET = (1 << 1), // the database value is marked as reset (overflown) + RRDR_VALUE_PARTIAL = (1 << 2), // the database provides partial data about this point (used in group-by) +} RRDR_VALUE_FLAGS; + +typedef enum __attribute__ ((__packed__)) rrdr_dimension_flag { + RRDR_DIMENSION_DEFAULT = 0, + RRDR_DIMENSION_HIDDEN = (1 << 0), // the dimension is hidden (not to be presented to callers) + RRDR_DIMENSION_NONZERO = (1 << 1), // the dimension is non zero (contains non-zero values) + RRDR_DIMENSION_SELECTED = (1 << 2), // the dimension has been selected for query + RRDR_DIMENSION_QUERIED = (1 << 3), // the dimension has been queried + RRDR_DIMENSION_FAILED = (1 << 4), // the dimension failed to be queried + RRDR_DIMENSION_GROUPED = (1 << 5), // the dimension has been grouped in this RRDR +} RRDR_DIMENSION_FLAGS; + +// RRDR result options +typedef enum __attribute__ ((__packed__)) rrdr_result_flags { + RRDR_RESULT_FLAG_ABSOLUTE = (1 << 0), // the query uses absolute time-frames + // (can be cached by browsers and proxies) + RRDR_RESULT_FLAG_RELATIVE = (1 << 1), // the query uses relative time-frames + // (should not to be cached by browsers and proxies) + RRDR_RESULT_FLAG_CANCEL = (1 << 2), // the query needs to be cancelled +} RRDR_RESULT_FLAGS; + +#define RRDR_DVIEW_ANOMALY_COUNT_MULTIPLIER 1000.0 + +typedef struct rrdresult { + size_t d; // the number of dimensions + size_t n; // the number of values in the arrays (number of points per dimension) + size_t rows; // the number of actual rows used + + RRDR_DIMENSION_FLAGS *od; // the options for the dimensions + + STRING **di; // array of d dimension ids + STRING **dn; // array of d dimension names + STRING **du; // array of d dimension units + uint32_t *dgbs; // array of d dimension group by slots - NOT ALLOCATED when RRDR is created + uint32_t *dgbc; // array of d dimension group by counts - NOT ALLOCATED when RRDR is created + uint32_t *dp; // array of d dimension priority - NOT ALLOCATED when RRDR is created + DICTIONARY **dl; // array of d dimension labels - NOT ALLOCATED when RRDR is created + STORAGE_POINT *dqp; // array of d dimensions query points - NOT ALLOCATED when RRDR is created + STORAGE_POINT *dview; // array of d dimensions group by view - NOT ALLOCATED when RRDR is created + NETDATA_DOUBLE *vh; // array of n x d hidden values, while grouping - NOT ALLOCATED when RRDR is created + + DICTIONARY *label_keys; + + time_t *t; // array of n timestamps + NETDATA_DOUBLE *v; // array n x d values + RRDR_VALUE_FLAGS *o; // array n x d options for each value returned + NETDATA_DOUBLE *ar; // array n x d of anomaly rates (0 - 100) + uint32_t *gbc; // array n x d of group by count - NOT ALLOCATED when RRDR is created + + struct { + size_t group; // how many collected values were grouped for each row - NEEDED BY GROUPING FUNCTIONS + time_t after; + time_t before; + time_t update_every; // what is the suggested update frequency in seconds + NETDATA_DOUBLE min; + NETDATA_DOUBLE max; + RRDR_RESULT_FLAGS flags; // RRDR_RESULT_FLAG_* + } view; + + struct { + size_t db_points_read; + size_t result_points_generated; + } stats; + + struct { + void *data; // the internal data of the grouping function + + // grouping function pointers + RRDR_TIME_GROUPING add_flush; + void (*create)(struct rrdresult *r, const char *options); + void (*reset)(struct rrdresult *r); + void (*free)(struct rrdresult *r); + void (*add)(struct rrdresult *r, NETDATA_DOUBLE value); + NETDATA_DOUBLE (*flush)(struct rrdresult *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr); + + TIER_QUERY_FETCH tier_query_fetch; // which value to use from STORAGE_POINT + + size_t points_wanted; // used by SES and DES + size_t resampling_group; // used by AVERAGE + NETDATA_DOUBLE resampling_divisor; // used by AVERAGE + } time_grouping; + + struct { + struct rrdresult *r; + } group_by; + + struct { + time_t max_update_every; + time_t expected_after; + time_t trimmed_after; + } partial_data_trimming; + + struct { + ONEWAYALLOC *owa; // the allocator used + struct query_target *qt; // the QUERY_TARGET + size_t contexts; // temp needed between json_wrapper_begin2() and json_wrapper_end2() + size_t queries_count; // temp needed to know if a query is the first executed + +#ifdef NETDATA_INTERNAL_CHECKS + const char *log; +#endif + + struct query_target *release_with_rrdr_qt; + } internal; +} RRDR; + +#define rrdr_rows(r) ((r)->rows) + +#include "database/rrd.h" +void rrdr_free(ONEWAYALLOC *owa, RRDR *r); +RRDR *rrdr_create(ONEWAYALLOC *owa, struct query_target *qt, size_t dimensions, size_t points); + +#include "../web_api_v1.h" +#include "web/api/queries/query.h" + +RRDR *rrd2rrdr_legacy( + ONEWAYALLOC *owa, + RRDSET *st, size_t points, time_t after, time_t before, + RRDR_TIME_GROUPING group_method, time_t resampling_time, RRDR_OPTIONS options, const char *dimensions, + const char *group_options, time_t timeout_ms, size_t tier, QUERY_SOURCE query_source, + STORAGE_PRIORITY priority); + +RRDR *rrd2rrdr(ONEWAYALLOC *owa, struct query_target *qt); +bool query_target_calculate_window(struct query_target *qt); + +#ifdef __cplusplus +} +#endif + +#endif //NETDATA_QUERIES_RRDR_H diff --git a/src/web/api/queries/ses/README.md b/src/web/api/queries/ses/README.md new file mode 100644 index 000000000..e2fd65d7a --- /dev/null +++ b/src/web/api/queries/ses/README.md @@ -0,0 +1,65 @@ +<!-- +title: "Single (or Simple) Exponential Smoothing (`ses`)" +sidebar_label: "Single (or Simple) Exponential Smoothing (`ses`)" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/ses/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Single (or Simple) Exponential Smoothing (`ses`) + +> This query is also available as `ema` and `ewma`. + +An exponential moving average (`ema`), also known as an exponentially weighted moving average (`ewma`) +is a first-order infinite impulse response filter that applies weighting factors which decrease +exponentially. The weighting for each older datum decreases exponentially, never reaching zero. + +In simple terms, this is like an average value, but more recent values are given more weight. + +Netdata automatically adjusts the weight (`alpha`) based on the number of values processed, +using the formula: + +``` +window = max(number of values, 15) +alpha = 2 / (window + 1) +``` + +You can change the fixed value `15` by setting in `netdata.conf`: + +``` +[web] + ses max window = 15 +``` + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: ses -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`ses` does not change the units. For example, if the chart units is `requests/sec`, the exponential +moving average will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=ses` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average&value_color=yellow) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=ses&after=-60&label=single+exponential+smoothing&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Moving_average#exponential-moving-average> +- <https://en.wikipedia.org/wiki/Exponential_smoothing>. + + diff --git a/src/web/api/queries/ses/ses.c b/src/web/api/queries/ses/ses.c new file mode 100644 index 000000000..39eb445a0 --- /dev/null +++ b/src/web/api/queries/ses/ses.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ses.h" + + +// ---------------------------------------------------------------------------- +// single exponential smoothing + diff --git a/src/web/api/queries/ses/ses.h b/src/web/api/queries/ses/ses.h new file mode 100644 index 000000000..de8645ff0 --- /dev/null +++ b/src/web/api/queries/ses/ses.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_SES_H +#define NETDATA_API_QUERIES_SES_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_ses { + NETDATA_DOUBLE alpha; + NETDATA_DOUBLE alpha_other; + NETDATA_DOUBLE level; + size_t count; +}; + +static size_t tg_ses_max_window_size = 15; + +static inline void tg_ses_init(void) { + long long ret = config_get_number(CONFIG_SECTION_WEB, "ses max tg_des_window", (long long)tg_ses_max_window_size); + if(ret <= 1) { + config_set_number(CONFIG_SECTION_WEB, "ses max tg_des_window", (long long)tg_ses_max_window_size); + } + else { + tg_ses_max_window_size = (size_t) ret; + } +} + +static inline NETDATA_DOUBLE tg_ses_window(RRDR *r, struct tg_ses *g) { + (void)g; + + NETDATA_DOUBLE points; + if(r->view.group == 1) { + // provide a running DES + points = (NETDATA_DOUBLE)r->time_grouping.points_wanted; + } + else { + // provide a SES with flush points + points = (NETDATA_DOUBLE)r->view.group; + } + + return (points > (NETDATA_DOUBLE)tg_ses_max_window_size) ? (NETDATA_DOUBLE)tg_ses_max_window_size : points; +} + +static inline void tg_ses_set_alpha(RRDR *r, struct tg_ses *g) { + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // A commonly used value for alpha is 2 / (N + 1) + g->alpha = 2.0 / (tg_ses_window(r, g) + 1.0); + g->alpha_other = 1.0 - g->alpha; +} + +static inline void tg_ses_create(RRDR *r, const char *options __maybe_unused) { + struct tg_ses *g = (struct tg_ses *)onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_ses)); + tg_ses_set_alpha(r, g); + g->level = 0.0; + r->time_grouping.data = g; +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_ses_reset(RRDR *r) { + struct tg_ses *g = (struct tg_ses *)r->time_grouping.data; + g->level = 0.0; + g->count = 0; +} + +static inline void tg_ses_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_ses_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_ses *g = (struct tg_ses *)r->time_grouping.data; + + if(unlikely(!g->count)) + g->level = value; + + g->level = g->alpha * value + g->alpha_other * g->level; + g->count++; +} + +static inline NETDATA_DOUBLE tg_ses_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_ses *g = (struct tg_ses *)r->time_grouping.data; + + if(unlikely(!g->count || !netdata_double_isnumber(g->level))) { + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + return 0.0; + } + + return g->level; +} + +#endif //NETDATA_API_QUERIES_SES_H diff --git a/src/web/api/queries/stddev/README.md b/src/web/api/queries/stddev/README.md new file mode 100644 index 000000000..76cfee1f1 --- /dev/null +++ b/src/web/api/queries/stddev/README.md @@ -0,0 +1,97 @@ +<!-- +title: "standard deviation (`stddev`)" +sidebar_label: "standard deviation (`stddev`)" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/stddev/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# standard deviation (`stddev`) + +The standard deviation is a measure that is used to quantify the amount of variation or dispersion +of a set of data values. + +A low standard deviation indicates that the data points tend to be close to the mean (also called the +expected value) of the set, while a high standard deviation indicates that the data points are spread +out over a wider range of values. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: stddev -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`stdev` does not change the units. For example, if the chart units is `requests/sec`, the standard +deviation will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=stddev` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=average&after=-60&label=average&value_color=yellow) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=stddev&after=-60&label=standard+deviation&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=max&after=-60&label=max) + +## References + +Check <https://en.wikipedia.org/wiki/Standard_deviation>. + +--- + +# Coefficient of variation (`cv`) + +> This query is also available as `rsd`. + +The coefficient of variation (`cv`), also known as relative standard deviation (`rsd`), +is a standardized measure of dispersion of a probability distribution or frequency distribution. + +It is defined as the ratio of the **standard deviation** to the **mean**. + +In simple terms, it gives the percentage of change. So, if the average value of a metric is 1000 +and its standard deviation is 100 (meaning that it variates from 900 to 1100), then `cv` is 10%. + +This is an easy way to check the % variation, without using absolute values. + +For example, you may trigger an alert if your web server requests/sec `cv` is above 20 (`%`) +over the last minute. So if your web server was serving 1000 reqs/sec over the last minute, +it will trigger the alert if had spikes below 800/sec or above 1200/sec. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: cv -1m unaligned of my_dimension + units: % + warn: $this > 20 +``` + +The units reported by `cv` is always `%`. + +It can also be used in APIs and badges as `&group=cv` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=average&after=-60&label=average&value_color=yellow) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=cv&after=-60&label=coefficient+of+variation&value_color=orange&units=pcent) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&dimensions=success&group=max&after=-60&label=max) + +## References + +Check <https://en.wikipedia.org/wiki/Coefficient_of_variation>. + + diff --git a/src/web/api/queries/stddev/stddev.c b/src/web/api/queries/stddev/stddev.c new file mode 100644 index 000000000..8f5431194 --- /dev/null +++ b/src/web/api/queries/stddev/stddev.c @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "stddev.h" + + +// ---------------------------------------------------------------------------- +// stddev + +/* + * Mean = average + * +NETDATA_DOUBLE grouping_flush_mean(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct grouping_stddev *g = (struct grouping_stddev *)r->grouping.grouping_data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = mean(g); + + if(!isnormal(value)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + } + + grouping_reset_stddev(r); + + return value; +} + */ + +/* + * It is not advised to use this version of variance directly + * +NETDATA_DOUBLE grouping_flush_variance(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct grouping_stddev *g = (struct grouping_stddev *)r->grouping.grouping_data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = variance(g); + + if(!isnormal(value)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + } + + grouping_reset_stddev(r); + + return value; +} +*/
\ No newline at end of file diff --git a/src/web/api/queries/stddev/stddev.h b/src/web/api/queries/stddev/stddev.h new file mode 100644 index 000000000..f7a1a06c3 --- /dev/null +++ b/src/web/api/queries/stddev/stddev.h @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_STDDEV_H +#define NETDATA_API_QUERIES_STDDEV_H + +#include "../query.h" +#include "../rrdr.h" + +// this implementation comes from: +// https://www.johndcook.com/blog/standard_deviation/ + +struct tg_stddev { + long count; + NETDATA_DOUBLE m_oldM, m_newM, m_oldS, m_newS; +}; + +static inline void tg_stddev_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_stddev)); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_stddev_reset(RRDR *r) { + struct tg_stddev *g = (struct tg_stddev *)r->time_grouping.data; + g->count = 0; +} + +static inline void tg_stddev_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_stddev_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_stddev *g = (struct tg_stddev *)r->time_grouping.data; + + g->count++; + + // See Knuth TAOCP vol 2, 3rd edition, page 232 + if (g->count == 1) { + g->m_oldM = g->m_newM = value; + g->m_oldS = 0.0; + } + else { + g->m_newM = g->m_oldM + (value - g->m_oldM) / g->count; + g->m_newS = g->m_oldS + (value - g->m_oldM) * (value - g->m_newM); + + // set up for next iteration + g->m_oldM = g->m_newM; + g->m_oldS = g->m_newS; + } +} + +static inline NETDATA_DOUBLE tg_stddev_mean(struct tg_stddev *g) { + return (g->count > 0) ? g->m_newM : 0.0; +} + +static inline NETDATA_DOUBLE tg_stddev_variance(struct tg_stddev *g) { + return ( (g->count > 1) ? g->m_newS/(NETDATA_DOUBLE)(g->count - 1) : 0.0 ); +} +static inline NETDATA_DOUBLE tg_stddev_stddev(struct tg_stddev *g) { + return sqrtndd(tg_stddev_variance(g)); +} + +static inline NETDATA_DOUBLE tg_stddev_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_stddev *g = (struct tg_stddev *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(likely(g->count > 1)) { + value = tg_stddev_stddev(g); + + if(!netdata_double_isnumber(value)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + } + else if(g->count == 1) { + value = 0.0; + } + else { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + + tg_stddev_reset(r); + + return value; +} + +// https://en.wikipedia.org/wiki/Coefficient_of_variation +static inline NETDATA_DOUBLE tg_stddev_coefficient_of_variation_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_stddev *g = (struct tg_stddev *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(likely(g->count > 1)) { + NETDATA_DOUBLE m = tg_stddev_mean(g); + value = 100.0 * tg_stddev_stddev(g) / ((m < 0)? -m : m); + + if(unlikely(!netdata_double_isnumber(value))) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + } + else if(g->count == 1) { + // one value collected + value = 0.0; + } + else { + // no values collected + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + + tg_stddev_reset(r); + + return value; +} + +#endif //NETDATA_API_QUERIES_STDDEV_H diff --git a/src/web/api/queries/sum/README.md b/src/web/api/queries/sum/README.md new file mode 100644 index 000000000..dd29b9c5b --- /dev/null +++ b/src/web/api/queries/sum/README.md @@ -0,0 +1,45 @@ +<!-- +title: "Sum" +sidebar_label: "Sum" +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/sum/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Sum + +This module sums all the values in the time-frame requested. + +You can use `sum` to find the volume of something over a period. + +## how to use + +Use it in alarms like this: + +``` + alarm: my_alarm + on: my_chart +lookup: sum -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`sum` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=sum` in the URL. + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=sum&after=-60&label=1m+sum&value_color=orange&units=requests) + +## References + +- <https://en.wikipedia.org/wiki/Summation>. + + diff --git a/src/web/api/queries/sum/sum.c b/src/web/api/queries/sum/sum.c new file mode 100644 index 000000000..cf4484217 --- /dev/null +++ b/src/web/api/queries/sum/sum.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "sum.h" + +// ---------------------------------------------------------------------------- +// sum + + + diff --git a/src/web/api/queries/sum/sum.h b/src/web/api/queries/sum/sum.h new file mode 100644 index 000000000..5e07f45d6 --- /dev/null +++ b/src/web/api/queries/sum/sum.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERY_SUM_H +#define NETDATA_API_QUERY_SUM_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_sum { + NETDATA_DOUBLE sum; + size_t count; +}; + +static inline void tg_sum_create(RRDR *r, const char *options __maybe_unused) { + r->time_grouping.data = onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_sum)); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_sum_reset(RRDR *r) { + struct tg_sum *g = (struct tg_sum *)r->time_grouping.data; + g->sum = 0; + g->count = 0; +} + +static inline void tg_sum_free(RRDR *r) { + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_sum_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_sum *g = (struct tg_sum *)r->time_grouping.data; + g->sum += value; + g->count++; +} + +static inline NETDATA_DOUBLE tg_sum_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_sum *g = (struct tg_sum *)r->time_grouping.data; + + NETDATA_DOUBLE value; + + if(unlikely(!g->count)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else { + value = g->sum; + } + + g->sum = 0.0; + g->count = 0; + + return value; +} + +#endif //NETDATA_API_QUERY_SUM_H diff --git a/src/web/api/queries/trimmed_mean/README.md b/src/web/api/queries/trimmed_mean/README.md new file mode 100644 index 000000000..969023292 --- /dev/null +++ b/src/web/api/queries/trimmed_mean/README.md @@ -0,0 +1,60 @@ +<!-- +title: "Trimmed Mean" +sidebar_label: "Trimmed Mean" +description: "Use trimmed-mean in API queries and health entities to find the average value from a sample, eliminating any unwanted spikes in the returned metrics." +custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/trimmed_mean/README.md +learn_status: "Published" +learn_topic_type: "References" +learn_rel_path: "Developers/Web/Api/Queries" +--> + +# Trimmed Mean + +The trimmed mean is the average value of a series excluding the smallest and biggest points. + +Netdata applies linear interpolation on the last point, if the percentage requested to be excluded does not give a +round number of points. + +The following percentile aliases are defined: + +- `trimmed-mean1` +- `trimmed-mean2` +- `trimmed-mean3` +- `trimmed-mean5` +- `trimmed-mean10` +- `trimmed-mean15` +- `trimmed-mean20` +- `trimmed-mean25` + +The default `trimmed-mean` is an alias for `trimmed-mean5`. +Any percentage may be requested using the `group_options` query parameter. + +## how to use + +Use it in alerts like this: + +``` + alarm: my_alert + on: my_chart +lookup: trimmed-mean5 -1m unaligned of my_dimension + warn: $this > 1000 +``` + +`trimmed-mean` does not change the units. For example, if the chart units is `requests/sec`, the result +will be again expressed in the same units. + +It can also be used in APIs and badges as `&group=trimmed-mean` in the URL and the additional parameter `group_options` +may be used to request any percentage (e.g. `&group=trimmed-mean&group_options=29`). + +## Examples + +Examining last 1 minute `successful` web server responses: + +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=min&after=-60&label=min) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=average&after=-60&label=average) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=trimmed-mean5&after=-60&label=trimmed-mean5&value_color=orange) +- ![](https://registry.my-netdata.io/api/v1/badge.svg?chart=web_log_nginx.response_statuses&options=unaligned&dimensions=success&group=max&after=-60&label=max) + +## References + +- <https://en.wikipedia.org/wiki/Truncated_mean>. diff --git a/src/web/api/queries/trimmed_mean/trimmed_mean.c b/src/web/api/queries/trimmed_mean/trimmed_mean.c new file mode 100644 index 000000000..c50db7ed6 --- /dev/null +++ b/src/web/api/queries/trimmed_mean/trimmed_mean.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "trimmed_mean.h" + +// ---------------------------------------------------------------------------- +// median + diff --git a/src/web/api/queries/trimmed_mean/trimmed_mean.h b/src/web/api/queries/trimmed_mean/trimmed_mean.h new file mode 100644 index 000000000..3c09015bf --- /dev/null +++ b/src/web/api/queries/trimmed_mean/trimmed_mean.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_QUERIES_TRIMMED_MEAN_H +#define NETDATA_API_QUERIES_TRIMMED_MEAN_H + +#include "../query.h" +#include "../rrdr.h" + +struct tg_trimmed_mean { + size_t series_size; + size_t next_pos; + NETDATA_DOUBLE percent; + + NETDATA_DOUBLE *series; +}; + +static inline void tg_trimmed_mean_create_internal(RRDR *r, const char *options, NETDATA_DOUBLE def) { + long entries = r->view.group; + if(entries < 10) entries = 10; + + struct tg_trimmed_mean *g = (struct tg_trimmed_mean *)onewayalloc_callocz(r->internal.owa, 1, sizeof(struct tg_trimmed_mean)); + g->series = onewayalloc_mallocz(r->internal.owa, entries * sizeof(NETDATA_DOUBLE)); + g->series_size = (size_t)entries; + + g->percent = def; + if(options && *options) { + g->percent = str2ndd(options, NULL); + if(!netdata_double_isnumber(g->percent)) g->percent = 0.0; + if(g->percent < 0.0) g->percent = 0.0; + if(g->percent > 50.0) g->percent = 50.0; + } + + g->percent = 1.0 - ((g->percent / 100.0) * 2.0); + r->time_grouping.data = g; +} + +static inline void tg_trimmed_mean_create_1(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 1.0); +} +static inline void tg_trimmed_mean_create_2(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 2.0); +} +static inline void tg_trimmed_mean_create_3(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 3.0); +} +static inline void tg_trimmed_mean_create_5(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 5.0); +} +static inline void tg_trimmed_mean_create_10(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 10.0); +} +static inline void tg_trimmed_mean_create_15(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 15.0); +} +static inline void tg_trimmed_mean_create_20(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 20.0); +} +static inline void tg_trimmed_mean_create_25(RRDR *r, const char *options) { + tg_trimmed_mean_create_internal(r, options, 25.0); +} + +// resets when switches dimensions +// so, clear everything to restart +static inline void tg_trimmed_mean_reset(RRDR *r) { + struct tg_trimmed_mean *g = (struct tg_trimmed_mean *)r->time_grouping.data; + g->next_pos = 0; +} + +static inline void tg_trimmed_mean_free(RRDR *r) { + struct tg_trimmed_mean *g = (struct tg_trimmed_mean *)r->time_grouping.data; + if(g) onewayalloc_freez(r->internal.owa, g->series); + + onewayalloc_freez(r->internal.owa, r->time_grouping.data); + r->time_grouping.data = NULL; +} + +static inline void tg_trimmed_mean_add(RRDR *r, NETDATA_DOUBLE value) { + struct tg_trimmed_mean *g = (struct tg_trimmed_mean *)r->time_grouping.data; + + if(unlikely(g->next_pos >= g->series_size)) { + g->series = onewayalloc_doublesize( r->internal.owa, g->series, g->series_size * sizeof(NETDATA_DOUBLE)); + g->series_size *= 2; + } + + g->series[g->next_pos++] = value; +} + +static inline NETDATA_DOUBLE tg_trimmed_mean_flush(RRDR *r, RRDR_VALUE_FLAGS *rrdr_value_options_ptr) { + struct tg_trimmed_mean *g = (struct tg_trimmed_mean *)r->time_grouping.data; + + NETDATA_DOUBLE value; + size_t available_slots = g->next_pos; + + if(unlikely(!available_slots)) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + else if(available_slots == 1) { + value = g->series[0]; + } + else { + sort_series(g->series, available_slots); + + NETDATA_DOUBLE min = g->series[0]; + NETDATA_DOUBLE max = g->series[available_slots - 1]; + + if (min != max) { + size_t slots_to_use = (size_t)((NETDATA_DOUBLE)available_slots * g->percent); + if(!slots_to_use) slots_to_use = 1; + + NETDATA_DOUBLE percent_to_use = (NETDATA_DOUBLE)slots_to_use / (NETDATA_DOUBLE)available_slots; + NETDATA_DOUBLE percent_delta = g->percent - percent_to_use; + + NETDATA_DOUBLE percent_interpolation_slot = 0.0; + NETDATA_DOUBLE percent_last_slot = 0.0; + if(percent_delta > 0.0) { + NETDATA_DOUBLE percent_to_use_plus_1_slot = (NETDATA_DOUBLE)(slots_to_use + 1) / (NETDATA_DOUBLE)available_slots; + NETDATA_DOUBLE percent_1slot = percent_to_use_plus_1_slot - percent_to_use; + + percent_interpolation_slot = percent_delta / percent_1slot; + percent_last_slot = 1 - percent_interpolation_slot; + } + + int start_slot, stop_slot, step, last_slot, interpolation_slot; + if(min >= 0.0 && max >= 0.0) { + start_slot = (int)((available_slots - slots_to_use) / 2); + stop_slot = start_slot + (int)slots_to_use; + last_slot = stop_slot - 1; + interpolation_slot = stop_slot; + step = 1; + } + else { + start_slot = (int)available_slots - 1 - (int)((available_slots - slots_to_use) / 2); + stop_slot = start_slot - (int)slots_to_use; + last_slot = stop_slot + 1; + interpolation_slot = stop_slot; + step = -1; + } + + value = 0.0; + for(int slot = start_slot; slot != stop_slot ; slot += step) + value += g->series[slot]; + + size_t counted = slots_to_use; + if(percent_interpolation_slot > 0.0 && interpolation_slot >= 0 && interpolation_slot < (int)available_slots) { + value += g->series[interpolation_slot] * percent_interpolation_slot; + value += g->series[last_slot] * percent_last_slot; + counted++; + } + + value = value / (NETDATA_DOUBLE)counted; + } + else + value = min; + } + + if(unlikely(!netdata_double_isnumber(value))) { + value = 0.0; + *rrdr_value_options_ptr |= RRDR_VALUE_EMPTY; + } + + //log_series_to_stderr(g->series, g->next_pos, value, "trimmed_mean"); + + g->next_pos = 0; + + return value; +} + +#endif //NETDATA_API_QUERIES_TRIMMED_MEAN_H diff --git a/src/web/api/queries/weights.c b/src/web/api/queries/weights.c new file mode 100644 index 000000000..44928fea8 --- /dev/null +++ b/src/web/api/queries/weights.c @@ -0,0 +1,2105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "daemon/common.h" +#include "database/KolmogorovSmirnovDist.h" + +#define MAX_POINTS 10000 +int enable_metric_correlations = CONFIG_BOOLEAN_YES; +int metric_correlations_version = 1; +WEIGHTS_METHOD default_metric_correlations_method = WEIGHTS_METHOD_MC_KS2; + +typedef struct weights_stats { + NETDATA_DOUBLE max_base_high_ratio; + size_t db_points; + size_t result_points; + size_t db_queries; + size_t db_points_per_tier[RRD_STORAGE_TIERS]; + size_t binary_searches; +} WEIGHTS_STATS; + +// ---------------------------------------------------------------------------- +// parse and render metric correlations methods + +static struct { + const char *name; + WEIGHTS_METHOD value; +} weights_methods[] = { + { "ks2" , WEIGHTS_METHOD_MC_KS2} + , { "volume" , WEIGHTS_METHOD_MC_VOLUME} + , { "anomaly-rate" , WEIGHTS_METHOD_ANOMALY_RATE} + , { "value" , WEIGHTS_METHOD_VALUE} + , { NULL , 0 } +}; + +WEIGHTS_METHOD weights_string_to_method(const char *method) { + for(int i = 0; weights_methods[i].name ;i++) + if(strcmp(method, weights_methods[i].name) == 0) + return weights_methods[i].value; + + return default_metric_correlations_method; +} + +const char *weights_method_to_string(WEIGHTS_METHOD method) { + for(int i = 0; weights_methods[i].name ;i++) + if(weights_methods[i].value == method) + return weights_methods[i].name; + + return "unknown"; +} + +// ---------------------------------------------------------------------------- +// The results per dimension are aggregated into a dictionary + +typedef enum { + RESULT_IS_BASE_HIGH_RATIO = (1 << 0), + RESULT_IS_PERCENTAGE_OF_TIME = (1 << 1), +} RESULT_FLAGS; + +struct register_result { + RESULT_FLAGS flags; + RRDHOST *host; + RRDCONTEXT_ACQUIRED *rca; + RRDINSTANCE_ACQUIRED *ria; + RRDMETRIC_ACQUIRED *rma; + NETDATA_DOUBLE value; + STORAGE_POINT highlighted; + STORAGE_POINT baseline; + usec_t duration_ut; +}; + +static DICTIONARY *register_result_init() { + DICTIONARY *results = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct register_result)); + return results; +} + +static void register_result_destroy(DICTIONARY *results) { + dictionary_destroy(results); +} + +static void register_result(DICTIONARY *results, RRDHOST *host, RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, + RRDMETRIC_ACQUIRED *rma, NETDATA_DOUBLE value, RESULT_FLAGS flags, + STORAGE_POINT *highlighted, STORAGE_POINT *baseline, WEIGHTS_STATS *stats, + bool register_zero, usec_t duration_ut) { + + if(!netdata_double_isnumber(value)) return; + + // make it positive + NETDATA_DOUBLE v = fabsndd(value); + + // no need to store zero scored values + if(unlikely(fpclassify(v) == FP_ZERO && !register_zero)) + return; + + // keep track of the max of the baseline / highlight ratio + if((flags & RESULT_IS_BASE_HIGH_RATIO) && v > stats->max_base_high_ratio) + stats->max_base_high_ratio = v; + + struct register_result t = { + .flags = flags, + .host = host, + .rca = rca, + .ria = ria, + .rma = rma, + .value = v, + .duration_ut = duration_ut, + }; + + if(highlighted) + t.highlighted = *highlighted; + + if(baseline) + t.baseline = *baseline; + + // we can use the pointer address or RMA as a unique key for each metric + char buf[20 + 1]; + ssize_t len = snprintfz(buf, sizeof(buf) - 1, "%p", rma); + dictionary_set_advanced(results, buf, len, &t, sizeof(struct register_result), NULL); +} + +// ---------------------------------------------------------------------------- +// Generation of JSON output for the results + +static void results_header_to_json(DICTIONARY *results __maybe_unused, BUFFER *wb, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions __maybe_unused, usec_t duration, + WEIGHTS_STATS *stats) { + + buffer_json_member_add_time_t(wb, "after", after); + buffer_json_member_add_time_t(wb, "before", before); + buffer_json_member_add_time_t(wb, "duration", before - after); + buffer_json_member_add_uint64(wb, "points", points); + + if(method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME) { + buffer_json_member_add_time_t(wb, "baseline_after", baseline_after); + buffer_json_member_add_time_t(wb, "baseline_before", baseline_before); + buffer_json_member_add_time_t(wb, "baseline_duration", baseline_before - baseline_after); + buffer_json_member_add_uint64(wb, "baseline_points", points << shifts); + } + + buffer_json_member_add_object(wb, "statistics"); + { + buffer_json_member_add_double(wb, "query_time_ms", (double) duration / (double) USEC_PER_MS); + buffer_json_member_add_uint64(wb, "db_queries", stats->db_queries); + buffer_json_member_add_uint64(wb, "query_result_points", stats->result_points); + buffer_json_member_add_uint64(wb, "binary_searches", stats->binary_searches); + buffer_json_member_add_uint64(wb, "db_points_read", stats->db_points); + + buffer_json_member_add_array(wb, "db_points_per_tier"); + { + for (size_t tier = 0; tier < storage_tiers; tier++) + buffer_json_add_array_item_uint64(wb, stats->db_points_per_tier[tier]); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_string(wb, "group", time_grouping_tostring(group)); + buffer_json_member_add_string(wb, "method", weights_method_to_string(method)); + rrdr_options_to_buffer_json_array(wb, "options", options); +} + +static size_t registered_results_to_json_charts(DICTIONARY *results, BUFFER *wb, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions, usec_t duration, + WEIGHTS_STATS *stats) { + + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + + results_header_to_json(results, wb, after, before, baseline_after, baseline_before, + points, method, group, options, shifts, examined_dimensions, duration, stats); + + buffer_json_member_add_object(wb, "correlated_charts"); + + size_t charts = 0, total_dimensions = 0; + struct register_result *t; + RRDINSTANCE_ACQUIRED *last_ria = NULL; // never access this - we use it only for comparison + dfe_start_read(results, t) { + if(t->ria != last_ria) { + last_ria = t->ria; + + if(charts) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // chart:id + } + + buffer_json_member_add_object(wb, rrdinstance_acquired_id(t->ria)); + buffer_json_member_add_string(wb, "context", rrdcontext_acquired_id(t->rca)); + buffer_json_member_add_object(wb, "dimensions"); + charts++; + } + buffer_json_member_add_double(wb, rrdmetric_acquired_name(t->rma), t->value); + total_dimensions++; + } + dfe_done(t); + + // close dimensions and chart + if (total_dimensions) { + buffer_json_object_close(wb); // dimensions + buffer_json_object_close(wb); // chart:id + } + + buffer_json_object_close(wb); + + buffer_json_member_add_uint64(wb, "correlated_dimensions", total_dimensions); + buffer_json_member_add_uint64(wb, "total_dimensions_count", examined_dimensions); + buffer_json_finalize(wb); + + return total_dimensions; +} + +static size_t registered_results_to_json_contexts(DICTIONARY *results, BUFFER *wb, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions, usec_t duration, + WEIGHTS_STATS *stats) { + + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + + results_header_to_json(results, wb, after, before, baseline_after, baseline_before, + points, method, group, options, shifts, examined_dimensions, duration, stats); + + buffer_json_member_add_object(wb, "contexts"); + + size_t contexts = 0, charts = 0, total_dimensions = 0, context_dims = 0, chart_dims = 0; + NETDATA_DOUBLE contexts_total_weight = 0.0, charts_total_weight = 0.0; + struct register_result *t; + RRDCONTEXT_ACQUIRED *last_rca = NULL; + RRDINSTANCE_ACQUIRED *last_ria = NULL; + dfe_start_read(results, t) { + + if(t->rca != last_rca) { + last_rca = t->rca; + + if(contexts) { + buffer_json_object_close(wb); // dimensions + buffer_json_member_add_double(wb, "weight", charts_total_weight / (double) chart_dims); + buffer_json_object_close(wb); // chart:id + buffer_json_object_close(wb); // charts + buffer_json_member_add_double(wb, "weight", contexts_total_weight / (double) context_dims); + buffer_json_object_close(wb); // context + } + + buffer_json_member_add_object(wb, rrdcontext_acquired_id(t->rca)); + buffer_json_member_add_object(wb, "charts"); + + contexts++; + charts = 0; + context_dims = 0; + contexts_total_weight = 0.0; + + last_ria = NULL; + } + + if(t->ria != last_ria) { + last_ria = t->ria; + + if(charts) { + buffer_json_object_close(wb); // dimensions + buffer_json_member_add_double(wb, "weight", charts_total_weight / (double) chart_dims); + buffer_json_object_close(wb); // chart:id + } + + buffer_json_member_add_object(wb, rrdinstance_acquired_id(t->ria)); + buffer_json_member_add_object(wb, "dimensions"); + + charts++; + chart_dims = 0; + charts_total_weight = 0.0; + } + + buffer_json_member_add_double(wb, rrdmetric_acquired_name(t->rma), t->value); + charts_total_weight += t->value; + contexts_total_weight += t->value; + chart_dims++; + context_dims++; + total_dimensions++; + } + dfe_done(t); + + // close dimensions and chart + if (total_dimensions) { + buffer_json_object_close(wb); // dimensions + buffer_json_member_add_double(wb, "weight", charts_total_weight / (double) chart_dims); + buffer_json_object_close(wb); // chart:id + buffer_json_object_close(wb); // charts + buffer_json_member_add_double(wb, "weight", contexts_total_weight / (double) context_dims); + buffer_json_object_close(wb); // context + } + + buffer_json_object_close(wb); + + buffer_json_member_add_uint64(wb, "correlated_dimensions", total_dimensions); + buffer_json_member_add_uint64(wb, "total_dimensions_count", examined_dimensions); + buffer_json_finalize(wb); + + return total_dimensions; +} + +struct query_weights_data { + QUERY_WEIGHTS_REQUEST *qwr; + + SIMPLE_PATTERN *scope_nodes_sp; + SIMPLE_PATTERN *scope_contexts_sp; + SIMPLE_PATTERN *nodes_sp; + SIMPLE_PATTERN *contexts_sp; + SIMPLE_PATTERN *instances_sp; + SIMPLE_PATTERN *dimensions_sp; + SIMPLE_PATTERN *labels_sp; + SIMPLE_PATTERN *alerts_sp; + + usec_t timeout_us; + bool timed_out; + bool interrupted; + + struct query_timings timings; + + size_t examined_dimensions; + bool register_zero; + + DICTIONARY *results; + WEIGHTS_STATS stats; + + uint32_t shifts; + + struct query_versions versions; +}; + +#define AGGREGATED_WEIGHT_EMPTY (struct aggregated_weight) { \ + .min = NAN, \ + .max = NAN, \ + .sum = NAN, \ + .count = 0, \ + .hsp = STORAGE_POINT_UNSET, \ + .bsp = STORAGE_POINT_UNSET, \ +} + +#define merge_into_aw(aw, t) do { \ + if(!(aw).count) { \ + (aw).count = 1; \ + (aw).min = (aw).max = (aw).sum = (t)->value; \ + (aw).hsp = (t)->highlighted; \ + if(baseline) \ + (aw).bsp = (t)->baseline; \ + } \ + else { \ + (aw).count++; \ + (aw).sum += (t)->value; \ + if((t)->value < (aw).min) \ + (aw).min = (t)->value; \ + if((t)->value > (aw).max) \ + (aw).max = (t)->value; \ + storage_point_merge_to((aw).hsp, (t)->highlighted); \ + if(baseline) \ + storage_point_merge_to((aw).bsp, (t)->baseline); \ + } \ +} while(0) + +static void results_header_to_json_v2(DICTIONARY *results __maybe_unused, BUFFER *wb, struct query_weights_data *qwd, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions __maybe_unused, usec_t duration __maybe_unused, + WEIGHTS_STATS *stats, bool group_by) { + + buffer_json_member_add_object(wb, "request"); + buffer_json_member_add_string(wb, "method", weights_method_to_string(method)); + rrdr_options_to_buffer_json_array(wb, "options", options); + + buffer_json_member_add_object(wb, "scope"); + buffer_json_member_add_string(wb, "scope_nodes", qwd->qwr->scope_nodes ? qwd->qwr->scope_nodes : "*"); + buffer_json_member_add_string(wb, "scope_contexts", qwd->qwr->scope_contexts ? qwd->qwr->scope_contexts : "*"); + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "selectors"); + buffer_json_member_add_string(wb, "nodes", qwd->qwr->nodes ? qwd->qwr->nodes : "*"); + buffer_json_member_add_string(wb, "contexts", qwd->qwr->contexts ? qwd->qwr->contexts : "*"); + buffer_json_member_add_string(wb, "instances", qwd->qwr->instances ? qwd->qwr->instances : "*"); + buffer_json_member_add_string(wb, "dimensions", qwd->qwr->dimensions ? qwd->qwr->dimensions : "*"); + buffer_json_member_add_string(wb, "labels", qwd->qwr->labels ? qwd->qwr->labels : "*"); + buffer_json_member_add_string(wb, "alerts", qwd->qwr->alerts ? qwd->qwr->alerts : "*"); + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "window"); + buffer_json_member_add_time_t(wb, "after", qwd->qwr->after); + buffer_json_member_add_time_t(wb, "before", qwd->qwr->before); + buffer_json_member_add_uint64(wb, "points", qwd->qwr->points); + if(qwd->qwr->options & RRDR_OPTION_SELECTED_TIER) + buffer_json_member_add_uint64(wb, "tier", qwd->qwr->tier); + else + buffer_json_member_add_string(wb, "tier", NULL); + buffer_json_object_close(wb); + + if(method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME) { + buffer_json_member_add_object(wb, "baseline"); + buffer_json_member_add_time_t(wb, "baseline_after", qwd->qwr->baseline_after); + buffer_json_member_add_time_t(wb, "baseline_before", qwd->qwr->baseline_before); + buffer_json_object_close(wb); + } + + buffer_json_member_add_object(wb, "aggregations"); + buffer_json_member_add_object(wb, "time"); + buffer_json_member_add_string(wb, "time_group", time_grouping_tostring(qwd->qwr->time_group_method)); + buffer_json_member_add_string(wb, "time_group_options", qwd->qwr->time_group_options); + buffer_json_object_close(wb); // time + + buffer_json_member_add_array(wb, "metrics"); + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_array(wb, "group_by"); + buffer_json_group_by_to_array(wb, qwd->qwr->group_by.group_by); + buffer_json_array_close(wb); + +// buffer_json_member_add_array(wb, "group_by_label"); +// buffer_json_array_close(wb); + + buffer_json_member_add_string(wb, "aggregation", group_by_aggregate_function_to_string(qwd->qwr->group_by.aggregation)); + } + buffer_json_object_close(wb); // 1st group by + buffer_json_array_close(wb); // array + buffer_json_object_close(wb); // aggregations + + buffer_json_member_add_uint64(wb, "timeout", qwd->qwr->timeout_ms); + buffer_json_object_close(wb); // request + + buffer_json_member_add_object(wb, "view"); + buffer_json_member_add_string(wb, "format", (group_by)?"grouped":"full"); + buffer_json_member_add_string(wb, "time_group", time_grouping_tostring(group)); + + buffer_json_member_add_object(wb, "window"); + buffer_json_member_add_time_t(wb, "after", after); + buffer_json_member_add_time_t(wb, "before", before); + buffer_json_member_add_time_t(wb, "duration", before - after); + buffer_json_member_add_uint64(wb, "points", points); + buffer_json_object_close(wb); + + if(method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME) { + buffer_json_member_add_object(wb, "baseline"); + buffer_json_member_add_time_t(wb, "after", baseline_after); + buffer_json_member_add_time_t(wb, "before", baseline_before); + buffer_json_member_add_time_t(wb, "duration", baseline_before - baseline_after); + buffer_json_member_add_uint64(wb, "points", points << shifts); + buffer_json_object_close(wb); + } + + buffer_json_object_close(wb); // view + + buffer_json_member_add_object(wb, "db"); + { + buffer_json_member_add_uint64(wb, "db_queries", stats->db_queries); + buffer_json_member_add_uint64(wb, "query_result_points", stats->result_points); + buffer_json_member_add_uint64(wb, "binary_searches", stats->binary_searches); + buffer_json_member_add_uint64(wb, "db_points_read", stats->db_points); + + buffer_json_member_add_array(wb, "db_points_per_tier"); + { + for (size_t tier = 0; tier < storage_tiers; tier++) + buffer_json_add_array_item_uint64(wb, stats->db_points_per_tier[tier]); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); // db +} + +typedef enum { + WPT_DIMENSION = 0, + WPT_INSTANCE = 1, + WPT_CONTEXT = 2, + WPT_NODE = 3, + WPT_GROUP = 4, +} WEIGHTS_POINT_TYPE; + +struct aggregated_weight { + const char *name; + NETDATA_DOUBLE min; + NETDATA_DOUBLE max; + NETDATA_DOUBLE sum; + size_t count; + STORAGE_POINT hsp; + STORAGE_POINT bsp; +}; + +static inline void storage_point_to_json(BUFFER *wb, WEIGHTS_POINT_TYPE type, ssize_t di, ssize_t ii, ssize_t ci, ssize_t ni, struct aggregated_weight *aw, RRDR_OPTIONS options __maybe_unused, bool baseline) { + if(type != WPT_GROUP) { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_uint64(wb, type); // "type" + buffer_json_add_array_item_int64(wb, ni); + if (type != WPT_NODE) { + buffer_json_add_array_item_int64(wb, ci); + if (type != WPT_CONTEXT) { + buffer_json_add_array_item_int64(wb, ii); + if (type != WPT_INSTANCE) + buffer_json_add_array_item_int64(wb, di); + else + buffer_json_add_array_item_string(wb, NULL); + } + else { + buffer_json_add_array_item_string(wb, NULL); + buffer_json_add_array_item_string(wb, NULL); + } + } + else { + buffer_json_add_array_item_string(wb, NULL); + buffer_json_add_array_item_string(wb, NULL); + buffer_json_add_array_item_string(wb, NULL); + } + buffer_json_add_array_item_double(wb, (aw->count) ? aw->sum / (NETDATA_DOUBLE)aw->count : 0.0); // "weight" + } + else { + buffer_json_member_add_array(wb, "v"); + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_double(wb, aw->min); // "min" + buffer_json_add_array_item_double(wb, (aw->count) ? aw->sum / (NETDATA_DOUBLE)aw->count : 0.0); // "avg" + buffer_json_add_array_item_double(wb, aw->max); // "max" + buffer_json_add_array_item_double(wb, aw->sum); // "sum" + buffer_json_add_array_item_uint64(wb, aw->count); // "count" + buffer_json_array_close(wb); + } + + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_double(wb, aw->hsp.min); // "min" + buffer_json_add_array_item_double(wb, (aw->hsp.count) ? aw->hsp.sum / (NETDATA_DOUBLE) aw->hsp.count : 0.0); // "avg" + buffer_json_add_array_item_double(wb, aw->hsp.max); // "max" + buffer_json_add_array_item_double(wb, aw->hsp.sum); // "sum" + buffer_json_add_array_item_uint64(wb, aw->hsp.count); // "count" + buffer_json_add_array_item_uint64(wb, aw->hsp.anomaly_count); // "anomaly_count" + buffer_json_array_close(wb); + + if(baseline) { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_double(wb, aw->bsp.min); // "min" + buffer_json_add_array_item_double(wb, (aw->bsp.count) ? aw->bsp.sum / (NETDATA_DOUBLE) aw->bsp.count : 0.0); // "avg" + buffer_json_add_array_item_double(wb, aw->bsp.max); // "max" + buffer_json_add_array_item_double(wb, aw->bsp.sum); // "sum" + buffer_json_add_array_item_uint64(wb, aw->bsp.count); // "count" + buffer_json_add_array_item_uint64(wb, aw->bsp.anomaly_count); // "anomaly_count" + buffer_json_array_close(wb); + } + + buffer_json_array_close(wb); +} + +static void multinode_data_schema(BUFFER *wb, RRDR_OPTIONS options __maybe_unused, const char *key, bool baseline, bool group_by) { + buffer_json_member_add_object(wb, key); // schema + + buffer_json_member_add_string(wb, "type", "array"); + buffer_json_member_add_array(wb, "items"); + + if(group_by) { + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "weight"); + buffer_json_member_add_string(wb, "type", "array"); + buffer_json_member_add_array(wb, "labels"); + { + buffer_json_add_array_item_string(wb, "min"); + buffer_json_add_array_item_string(wb, "avg"); + buffer_json_add_array_item_string(wb, "max"); + buffer_json_add_array_item_string(wb, "sum"); + buffer_json_add_array_item_string(wb, "count"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + } + else { + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "name", "row_type"); + buffer_json_member_add_string(wb, "type", "integer"); + buffer_json_member_add_array(wb, "value"); + buffer_json_add_array_item_string(wb, "dimension"); + buffer_json_add_array_item_string(wb, "instance"); + buffer_json_add_array_item_string(wb, "context"); + buffer_json_add_array_item_string(wb, "node"); + buffer_json_array_close(wb); + buffer_json_object_close(wb); + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "ni"); + buffer_json_member_add_string(wb, "type", "integer"); + buffer_json_member_add_string(wb, "dictionary", "nodes"); + } + buffer_json_object_close(wb); + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "ci"); + buffer_json_member_add_string(wb, "type", "integer"); + buffer_json_member_add_string(wb, "dictionary", "contexts"); + } + buffer_json_object_close(wb); + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "ii"); + buffer_json_member_add_string(wb, "type", "integer"); + buffer_json_member_add_string(wb, "dictionary", "instances"); + } + buffer_json_object_close(wb); + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "di"); + buffer_json_member_add_string(wb, "type", "integer"); + buffer_json_member_add_string(wb, "dictionary", "dimensions"); + } + buffer_json_object_close(wb); + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "weight"); + buffer_json_member_add_string(wb, "type", "number"); + } + buffer_json_object_close(wb); + } + + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "timeframe"); + buffer_json_member_add_string(wb, "type", "array"); + buffer_json_member_add_array(wb, "labels"); + { + buffer_json_add_array_item_string(wb, "min"); + buffer_json_add_array_item_string(wb, "avg"); + buffer_json_add_array_item_string(wb, "max"); + buffer_json_add_array_item_string(wb, "sum"); + buffer_json_add_array_item_string(wb, "count"); + buffer_json_add_array_item_string(wb, "anomaly_count"); + } + buffer_json_array_close(wb); + buffer_json_member_add_object(wb, "calculations"); + buffer_json_member_add_string(wb, "anomaly rate", "anomaly_count * 100 / count"); + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); + + if(baseline) { + buffer_json_add_array_item_object(wb); + { + buffer_json_member_add_string(wb, "name", "baseline timeframe"); + buffer_json_member_add_string(wb, "type", "array"); + buffer_json_member_add_array(wb, "labels"); + { + buffer_json_add_array_item_string(wb, "min"); + buffer_json_add_array_item_string(wb, "avg"); + buffer_json_add_array_item_string(wb, "max"); + buffer_json_add_array_item_string(wb, "sum"); + buffer_json_add_array_item_string(wb, "count"); + buffer_json_add_array_item_string(wb, "anomaly_count"); + } + buffer_json_array_close(wb); + buffer_json_member_add_object(wb, "calculations"); + buffer_json_member_add_string(wb, "anomaly rate", "anomaly_count * 100 / count"); + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); + } + + buffer_json_array_close(wb); // items + buffer_json_object_close(wb); // schema +} + +struct dict_unique_node { + bool existing; + bool exposed; + uint32_t i; + RRDHOST *host; + usec_t duration_ut; +}; + +struct dict_unique_name_units { + bool existing; + bool exposed; + uint32_t i; + const char *units; +}; + +struct dict_unique_id_name { + bool existing; + bool exposed; + uint32_t i; + const char *id; + const char *name; +}; + +static inline struct dict_unique_node *dict_unique_node_add(DICTIONARY *dict, RRDHOST *host, ssize_t *max_id) { + struct dict_unique_node *dun = dictionary_set(dict, host->machine_guid, NULL, sizeof(struct dict_unique_node)); + if(!dun->existing) { + dun->existing = true; + dun->host = host; + dun->i = *max_id; + (*max_id)++; + } + + return dun; +} + +static inline struct dict_unique_name_units *dict_unique_name_units_add(DICTIONARY *dict, const char *name, const char *units, ssize_t *max_id) { + struct dict_unique_name_units *dun = dictionary_set(dict, name, NULL, sizeof(struct dict_unique_name_units)); + if(!dun->existing) { + dun->units = units; + dun->existing = true; + dun->i = *max_id; + (*max_id)++; + } + + return dun; +} + +static inline struct dict_unique_id_name *dict_unique_id_name_add(DICTIONARY *dict, const char *id, const char *name, ssize_t *max_id) { + char key[1024 + 1]; + snprintfz(key, sizeof(key) - 1, "%s:%s", id, name); + struct dict_unique_id_name *dun = dictionary_set(dict, key, NULL, sizeof(struct dict_unique_id_name)); + if(!dun->existing) { + dun->existing = true; + dun->i = *max_id; + (*max_id)++; + dun->id = id; + dun->name = name; + } + + return dun; +} + +static size_t registered_results_to_json_multinode_no_group_by( + DICTIONARY *results, BUFFER *wb, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions, struct query_weights_data *qwd, + WEIGHTS_STATS *stats, + struct query_versions *versions) { + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_uint64(wb, "api", 2); + + results_header_to_json_v2(results, wb, qwd, after, before, baseline_after, baseline_before, + points, method, group, options, shifts, examined_dimensions, + qwd->timings.executed_ut - qwd->timings.received_ut, stats, false); + + version_hashes_api_v2(wb, versions); + + bool baseline = method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME; + multinode_data_schema(wb, options, "schema", baseline, false); + + DICTIONARY *dict_nodes = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct dict_unique_node)); + DICTIONARY *dict_contexts = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct dict_unique_name_units)); + DICTIONARY *dict_instances = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct dict_unique_id_name)); + DICTIONARY *dict_dimensions = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, NULL, sizeof(struct dict_unique_id_name)); + + buffer_json_member_add_array(wb, "result"); + + struct aggregated_weight node_aw = AGGREGATED_WEIGHT_EMPTY, context_aw = AGGREGATED_WEIGHT_EMPTY, instance_aw = AGGREGATED_WEIGHT_EMPTY; + struct register_result *t; + RRDHOST *last_host = NULL; + RRDCONTEXT_ACQUIRED *last_rca = NULL; + RRDINSTANCE_ACQUIRED *last_ria = NULL; + struct dict_unique_name_units *context_dun = NULL; + struct dict_unique_node *node_dun = NULL; + struct dict_unique_id_name *instance_dun = NULL; + struct dict_unique_id_name *dimension_dun = NULL; + ssize_t di = -1, ii = -1, ci = -1, ni = -1; + ssize_t di_max = 0, ii_max = 0, ci_max = 0, ni_max = 0; + size_t total_dimensions = 0; + dfe_start_read(results, t) { + + // close instance + if(t->ria != last_ria && last_ria) { + storage_point_to_json(wb, WPT_INSTANCE, di, ii, ci, ni, &instance_aw, options, baseline); + instance_dun->exposed = true; + last_ria = NULL; + instance_aw = AGGREGATED_WEIGHT_EMPTY; + } + + // close context + if(t->rca != last_rca && last_rca) { + storage_point_to_json(wb, WPT_CONTEXT, di, ii, ci, ni, &context_aw, options, baseline); + context_dun->exposed = true; + last_rca = NULL; + context_aw = AGGREGATED_WEIGHT_EMPTY; + } + + // close node + if(t->host != last_host && last_host) { + storage_point_to_json(wb, WPT_NODE, di, ii, ci, ni, &node_aw, options, baseline); + node_dun->exposed = true; + last_host = NULL; + node_aw = AGGREGATED_WEIGHT_EMPTY; + } + + // open node + if(t->host != last_host) { + last_host = t->host; + node_dun = dict_unique_node_add(dict_nodes, t->host, &ni_max); + ni = node_dun->i; + } + + // open context + if(t->rca != last_rca) { + last_rca = t->rca; + context_dun = dict_unique_name_units_add(dict_contexts, rrdcontext_acquired_id(t->rca), + rrdcontext_acquired_units(t->rca), &ci_max); + ci = context_dun->i; + } + + // open instance + if(t->ria != last_ria) { + last_ria = t->ria; + instance_dun = dict_unique_id_name_add(dict_instances, rrdinstance_acquired_id(t->ria), rrdinstance_acquired_name(t->ria), &ii_max); + ii = instance_dun->i; + } + + dimension_dun = dict_unique_id_name_add(dict_dimensions, rrdmetric_acquired_id(t->rma), rrdmetric_acquired_name(t->rma), &di_max); + di = dimension_dun->i; + + struct aggregated_weight aw = { + .min = t->value, + .max = t->value, + .sum = t->value, + .count = 1, + .hsp = t->highlighted, + .bsp = t->baseline, + }; + + storage_point_to_json(wb, WPT_DIMENSION, di, ii, ci, ni, &aw, options, baseline); + node_dun->exposed = true; + context_dun->exposed = true; + instance_dun->exposed = true; + dimension_dun->exposed = true; + + merge_into_aw(instance_aw, t); + merge_into_aw(context_aw, t); + merge_into_aw(node_aw, t); + + node_dun->duration_ut += t->duration_ut; + total_dimensions++; + } + dfe_done(t); + + // close instance + if(last_ria) { + storage_point_to_json(wb, WPT_INSTANCE, di, ii, ci, ni, &instance_aw, options, baseline); + instance_dun->exposed = true; + } + + // close context + if(last_rca) { + storage_point_to_json(wb, WPT_CONTEXT, di, ii, ci, ni, &context_aw, options, baseline); + context_dun->exposed = true; + } + + // close node + if(last_host) { + storage_point_to_json(wb, WPT_NODE, di, ii, ci, ni, &node_aw, options, baseline); + node_dun->exposed = true; + } + + buffer_json_array_close(wb); // points + + buffer_json_member_add_object(wb, "dictionaries"); + buffer_json_member_add_array(wb, "nodes"); + { + struct dict_unique_node *dun; + dfe_start_read(dict_nodes, dun) { + if(!dun->exposed) + continue; + + buffer_json_add_array_item_object(wb); + buffer_json_node_add_v2(wb, dun->host, dun->i, dun->duration_ut, true); + buffer_json_object_close(wb); + } + dfe_done(dun); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "contexts"); + { + struct dict_unique_name_units *dun; + dfe_start_read(dict_contexts, dun) { + if(!dun->exposed) + continue; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", dun_dfe.name); + buffer_json_member_add_string(wb, "units", dun->units); + buffer_json_member_add_int64(wb, "ci", dun->i); + buffer_json_object_close(wb); + } + dfe_done(dun); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "instances"); + { + struct dict_unique_id_name *dun; + dfe_start_read(dict_instances, dun) { + if(!dun->exposed) + continue; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", dun->id); + if(dun->id != dun->name) + buffer_json_member_add_string(wb, "nm", dun->name); + buffer_json_member_add_int64(wb, "ii", dun->i); + buffer_json_object_close(wb); + } + dfe_done(dun); + } + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "dimensions"); + { + struct dict_unique_id_name *dun; + dfe_start_read(dict_dimensions, dun) { + if(!dun->exposed) + continue; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", dun->id); + if(dun->id != dun->name) + buffer_json_member_add_string(wb, "nm", dun->name); + buffer_json_member_add_int64(wb, "di", dun->i); + buffer_json_object_close(wb); + } + dfe_done(dun); + } + buffer_json_array_close(wb); + + buffer_json_object_close(wb); //dictionaries + + buffer_json_agents_v2(wb, &qwd->timings, 0, false, true); + buffer_json_member_add_uint64(wb, "correlated_dimensions", total_dimensions); + buffer_json_member_add_uint64(wb, "total_dimensions_count", examined_dimensions); + buffer_json_finalize(wb); + + dictionary_destroy(dict_nodes); + dictionary_destroy(dict_contexts); + dictionary_destroy(dict_instances); + dictionary_destroy(dict_dimensions); + + return total_dimensions; +} + +static size_t registered_results_to_json_multinode_group_by( + DICTIONARY *results, BUFFER *wb, + time_t after, time_t before, + time_t baseline_after, time_t baseline_before, + size_t points, WEIGHTS_METHOD method, + RRDR_TIME_GROUPING group, RRDR_OPTIONS options, uint32_t shifts, + size_t examined_dimensions, struct query_weights_data *qwd, + WEIGHTS_STATS *stats, + struct query_versions *versions) { + buffer_json_initialize(wb, "\"", "\"", 0, true, (options & RRDR_OPTION_MINIFY) ? BUFFER_JSON_OPTIONS_MINIFY : BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_uint64(wb, "api", 2); + + results_header_to_json_v2(results, wb, qwd, after, before, baseline_after, baseline_before, + points, method, group, options, shifts, examined_dimensions, + qwd->timings.executed_ut - qwd->timings.received_ut, stats, true); + + version_hashes_api_v2(wb, versions); + + bool baseline = method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME; + multinode_data_schema(wb, options, "v_schema", baseline, true); + + DICTIONARY *group_by = dictionary_create_advanced(DICT_OPTION_SINGLE_THREADED | DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, + NULL, sizeof(struct aggregated_weight)); + + struct register_result *t; + size_t total_dimensions = 0; + BUFFER *key = buffer_create(0, NULL); + BUFFER *name = buffer_create(0, NULL); + dfe_start_read(results, t) { + + buffer_flush(key); + buffer_flush(name); + + if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_DIMENSION) { + buffer_strcat(key, rrdmetric_acquired_name(t->rma)); + buffer_strcat(name, rrdmetric_acquired_name(t->rma)); + } + if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_INSTANCE) { + if(buffer_strlen(key)) { + buffer_fast_strcat(key, ",", 1); + buffer_fast_strcat(name, ",", 1); + } + + buffer_strcat(key, rrdinstance_acquired_id(t->ria)); + buffer_strcat(name, rrdinstance_acquired_name(t->ria)); + + if(!(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_NODE)) { + buffer_fast_strcat(key, "@", 1); + buffer_fast_strcat(name, "@", 1); + buffer_strcat(key, t->host->machine_guid); + buffer_strcat(name, rrdhost_hostname(t->host)); + } + } + if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_NODE) { + if(buffer_strlen(key)) { + buffer_fast_strcat(key, ",", 1); + buffer_fast_strcat(name, ",", 1); + } + + buffer_strcat(key, t->host->machine_guid); + buffer_strcat(name, rrdhost_hostname(t->host)); + } + if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_CONTEXT) { + if(buffer_strlen(key)) { + buffer_fast_strcat(key, ",", 1); + buffer_fast_strcat(name, ",", 1); + } + + buffer_strcat(key, rrdcontext_acquired_id(t->rca)); + buffer_strcat(name, rrdcontext_acquired_id(t->rca)); + } + if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_UNITS) { + if(buffer_strlen(key)) { + buffer_fast_strcat(key, ",", 1); + buffer_fast_strcat(name, ",", 1); + } + + buffer_strcat(key, rrdcontext_acquired_units(t->rca)); + buffer_strcat(name, rrdcontext_acquired_units(t->rca)); + } + + struct aggregated_weight *aw = dictionary_set(group_by, buffer_tostring(key), NULL, sizeof(struct aggregated_weight)); + if(!aw->name) { + aw->name = strdupz(buffer_tostring(name)); + aw->min = aw->max = aw->sum = t->value; + aw->count = 1; + aw->hsp = t->highlighted; + aw->bsp = t->baseline; + } + else + merge_into_aw(*aw, t); + + total_dimensions++; + } + dfe_done(t); + buffer_free(key); key = NULL; + buffer_free(name); name = NULL; + + struct aggregated_weight *aw; + buffer_json_member_add_array(wb, "result"); + dfe_start_read(group_by, aw) { + const char *k = aw_dfe.name; + const char *n = aw->name; + + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "id", k); + + if(strcmp(k, n) != 0) + buffer_json_member_add_string(wb, "nm", n); + + storage_point_to_json(wb, WPT_GROUP, 0, 0, 0, 0, aw, options, baseline); + buffer_json_object_close(wb); + + freez((void *)aw->name); + } + dfe_done(aw); + buffer_json_array_close(wb); // result + + buffer_json_agents_v2(wb, &qwd->timings, 0, false, true); + buffer_json_member_add_uint64(wb, "correlated_dimensions", total_dimensions); + buffer_json_member_add_uint64(wb, "total_dimensions_count", examined_dimensions); + buffer_json_finalize(wb); + + dictionary_destroy(group_by); + + return total_dimensions; +} + +// ---------------------------------------------------------------------------- +// KS2 algorithm functions + +typedef long int DIFFS_NUMBERS; +#define DOUBLE_TO_INT_MULTIPLIER 100000 + +static inline int binary_search_bigger_than(const DIFFS_NUMBERS arr[], int left, int size, DIFFS_NUMBERS K) { + // binary search to find the index the smallest index + // of the first value in the array that is greater than K + + int right = size; + while(left < right) { + int middle = (int)(((unsigned int)(left + right)) >> 1); + + if(arr[middle] > K) + right = middle; + + else + left = middle + 1; + } + + return left; +} + +int compare_diffs(const void *left, const void *right) { + DIFFS_NUMBERS lt = *(DIFFS_NUMBERS *)left; + DIFFS_NUMBERS rt = *(DIFFS_NUMBERS *)right; + + // https://stackoverflow.com/a/3886497/1114110 + return (lt > rt) - (lt < rt); +} + +static size_t calculate_pairs_diff(DIFFS_NUMBERS *diffs, NETDATA_DOUBLE *arr, size_t size) { + NETDATA_DOUBLE *last = &arr[size - 1]; + size_t added = 0; + + while(last > arr) { + NETDATA_DOUBLE second = *last--; + NETDATA_DOUBLE first = *last; + *diffs++ = (DIFFS_NUMBERS)((first - second) * (NETDATA_DOUBLE)DOUBLE_TO_INT_MULTIPLIER); + added++; + } + + return added; +} + +static double ks_2samp( + DIFFS_NUMBERS baseline_diffs[], int base_size, + DIFFS_NUMBERS highlight_diffs[], int high_size, + uint32_t base_shifts) { + + qsort(baseline_diffs, base_size, sizeof(DIFFS_NUMBERS), compare_diffs); + qsort(highlight_diffs, high_size, sizeof(DIFFS_NUMBERS), compare_diffs); + + // Now we should be calculating this: + // + // For each number in the diffs arrays, we should find the index of the + // number bigger than them in both arrays and calculate the % of this index + // vs the total array size. Once we have the 2 percentages, we should find + // the min and max across the delta of all of them. + // + // It should look like this: + // + // base_pcent = binary_search_bigger_than(...) / base_size; + // high_pcent = binary_search_bigger_than(...) / high_size; + // delta = base_pcent - high_pcent; + // if(delta < min) min = delta; + // if(delta > max) max = delta; + // + // This would require a lot of multiplications and divisions. + // + // To speed it up, we do the binary search to find the index of each number + // but, then we divide the base index by the power of two number (shifts) it + // is bigger than high index. So the 2 indexes are now comparable. + // We also keep track of the original indexes with min and max, to properly + // calculate their percentages once the loops finish. + + + // initialize min and max using the first number of baseline_diffs + DIFFS_NUMBERS K = baseline_diffs[0]; + int base_idx = binary_search_bigger_than(baseline_diffs, 1, base_size, K); + int high_idx = binary_search_bigger_than(highlight_diffs, 0, high_size, K); + int delta = base_idx - (high_idx << base_shifts); + int min = delta, max = delta; + int base_min_idx = base_idx; + int base_max_idx = base_idx; + int high_min_idx = high_idx; + int high_max_idx = high_idx; + + // do the baseline_diffs starting from 1 (we did position 0 above) + for(int i = 1; i < base_size; i++) { + K = baseline_diffs[i]; + base_idx = binary_search_bigger_than(baseline_diffs, i + 1, base_size, K); // starting from i, since data1 is sorted + high_idx = binary_search_bigger_than(highlight_diffs, 0, high_size, K); + + delta = base_idx - (high_idx << base_shifts); + if(delta < min) { + min = delta; + base_min_idx = base_idx; + high_min_idx = high_idx; + } + else if(delta > max) { + max = delta; + base_max_idx = base_idx; + high_max_idx = high_idx; + } + } + + // do the highlight_diffs starting from 0 + for(int i = 0; i < high_size; i++) { + K = highlight_diffs[i]; + base_idx = binary_search_bigger_than(baseline_diffs, 0, base_size, K); + high_idx = binary_search_bigger_than(highlight_diffs, i + 1, high_size, K); // starting from i, since data2 is sorted + + delta = base_idx - (high_idx << base_shifts); + if(delta < min) { + min = delta; + base_min_idx = base_idx; + high_min_idx = high_idx; + } + else if(delta > max) { + max = delta; + base_max_idx = base_idx; + high_max_idx = high_idx; + } + } + + // now we have the min, max and their indexes + // properly calculate min and max as dmin and dmax + double dbase_size = (double)base_size; + double dhigh_size = (double)high_size; + double dmin = ((double)base_min_idx / dbase_size) - ((double)high_min_idx / dhigh_size); + double dmax = ((double)base_max_idx / dbase_size) - ((double)high_max_idx / dhigh_size); + + dmin = -dmin; + if(islessequal(dmin, 0.0)) dmin = 0.0; + else if(isgreaterequal(dmin, 1.0)) dmin = 1.0; + + double d; + if(isgreaterequal(dmin, dmax)) d = dmin; + else d = dmax; + + double en = round(dbase_size * dhigh_size / (dbase_size + dhigh_size)); + + // under these conditions, KSfbar() crashes + if(unlikely(isnan(en) || isinf(en) || en == 0.0 || isnan(d) || isinf(d))) + return NAN; + + return KSfbar((int)en, d); +} + +static double kstwo( + NETDATA_DOUBLE baseline[], int baseline_points, + NETDATA_DOUBLE highlight[], int highlight_points, + uint32_t base_shifts) { + + // -1 in size, since the calculate_pairs_diffs() returns one less point + DIFFS_NUMBERS baseline_diffs[baseline_points - 1]; + DIFFS_NUMBERS highlight_diffs[highlight_points - 1]; + + int base_size = (int)calculate_pairs_diff(baseline_diffs, baseline, baseline_points); + int high_size = (int)calculate_pairs_diff(highlight_diffs, highlight, highlight_points); + + if(unlikely(!base_size || !high_size)) + return NAN; + + if(unlikely(base_size != baseline_points - 1 || high_size != highlight_points - 1)) { + netdata_log_error("Metric correlations: internal error - calculate_pairs_diff() returns the wrong number of entries"); + return NAN; + } + + return ks_2samp(baseline_diffs, base_size, highlight_diffs, high_size, base_shifts); +} + +NETDATA_DOUBLE *rrd2rrdr_ks2( + ONEWAYALLOC *owa, RRDHOST *host, + RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, RRDMETRIC_ACQUIRED *rma, + time_t after, time_t before, size_t points, RRDR_OPTIONS options, + RRDR_TIME_GROUPING time_group_method, const char *time_group_options, size_t tier, + WEIGHTS_STATS *stats, + size_t *entries, + STORAGE_POINT *sp + ) { + + NETDATA_DOUBLE *ret = NULL; + + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .host = host, + .rca = rca, + .ria = ria, + .rma = rma, + .after = after, + .before = before, + .points = points, + .options = options, + .time_group_method = time_group_method, + .time_group_options = time_group_options, + .tier = tier, + .query_source = QUERY_SOURCE_API_WEIGHTS, + .priority = STORAGE_PRIORITY_SYNCHRONOUS, + }; + + QUERY_TARGET *qt = query_target_create(&qtr); + RRDR *r = rrd2rrdr(owa, qt); + if(!r) + goto cleanup; + + stats->db_queries++; + stats->result_points += r->stats.result_points_generated; + stats->db_points += r->stats.db_points_read; + for(size_t tr = 0; tr < storage_tiers ; tr++) + stats->db_points_per_tier[tr] += r->internal.qt->db.tiers[tr].points; + + if(r->d != 1 || r->internal.qt->query.used != 1) { + netdata_log_error("WEIGHTS: on query '%s' expected 1 dimension in RRDR but got %zu r->d and %zu qt->query.used", + r->internal.qt->id, r->d, (size_t)r->internal.qt->query.used); + goto cleanup; + } + + if(unlikely(r->od[0] & RRDR_DIMENSION_HIDDEN)) + goto cleanup; + + if(unlikely(!(r->od[0] & RRDR_DIMENSION_QUERIED))) + goto cleanup; + + if(unlikely(!(r->od[0] & RRDR_DIMENSION_NONZERO))) + goto cleanup; + + if(rrdr_rows(r) < 2) + goto cleanup; + + *entries = rrdr_rows(r); + ret = onewayalloc_mallocz(owa, sizeof(NETDATA_DOUBLE) * rrdr_rows(r)); + + if(sp) + *sp = r->internal.qt->query.array[0].query_points; + + // copy the points of the dimension to a contiguous array + // there is no need to check for empty values, since empty values are already zero + // https://github.com/netdata/netdata/blob/6e3144683a73a2024d51425b20ecfd569034c858/web/api/queries/average/average.c#L41-L43 + memcpy(ret, r->v, rrdr_rows(r) * sizeof(NETDATA_DOUBLE)); + +cleanup: + rrdr_free(owa, r); + query_target_release(qt); + return ret; +} + +static void rrdset_metric_correlations_ks2( + RRDHOST *host, + RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, RRDMETRIC_ACQUIRED *rma, + DICTIONARY *results, + time_t baseline_after, time_t baseline_before, + time_t after, time_t before, + size_t points, RRDR_OPTIONS options, + RRDR_TIME_GROUPING time_group_method, const char *time_group_options, size_t tier, + uint32_t shifts, + WEIGHTS_STATS *stats, bool register_zero + ) { + + options |= RRDR_OPTION_NATURAL_POINTS; + + usec_t started_ut = now_monotonic_usec(); + ONEWAYALLOC *owa = onewayalloc_create(16 * 1024); + + size_t high_points = 0; + STORAGE_POINT highlighted_sp; + NETDATA_DOUBLE *highlight = rrd2rrdr_ks2( + owa, host, rca, ria, rma, after, before, points, + options, time_group_method, time_group_options, tier, stats, &high_points, &highlighted_sp); + + if(!highlight) + goto cleanup; + + size_t base_points = 0; + STORAGE_POINT baseline_sp; + NETDATA_DOUBLE *baseline = rrd2rrdr_ks2( + owa, host, rca, ria, rma, baseline_after, baseline_before, high_points << shifts, + options, time_group_method, time_group_options, tier, stats, &base_points, &baseline_sp); + + if(!baseline) + goto cleanup; + + stats->binary_searches += 2 * (base_points - 1) + 2 * (high_points - 1); + + double prob = kstwo(baseline, (int)base_points, highlight, (int)high_points, shifts); + if(!isnan(prob) && !isinf(prob)) { + + // these conditions should never happen, but still let's check + if(unlikely(prob < 0.0)) { + netdata_log_error("Metric correlations: kstwo() returned a negative number: %f", prob); + prob = -prob; + } + if(unlikely(prob > 1.0)) { + netdata_log_error("Metric correlations: kstwo() returned a number above 1.0: %f", prob); + prob = 1.0; + } + + usec_t ended_ut = now_monotonic_usec(); + + // to spread the results evenly, 0.0 needs to be the less correlated and 1.0 the most correlated + // so, we flip the result of kstwo() + register_result(results, host, rca, ria, rma, 1.0 - prob, RESULT_IS_BASE_HIGH_RATIO, &highlighted_sp, + &baseline_sp, stats, register_zero, ended_ut - started_ut); + } + +cleanup: + onewayalloc_destroy(owa); +} + +// ---------------------------------------------------------------------------- +// VOLUME algorithm functions + +static void merge_query_value_to_stats(QUERY_VALUE *qv, WEIGHTS_STATS *stats, size_t queries) { + stats->db_queries += queries; + stats->result_points += qv->result_points; + stats->db_points += qv->points_read; + for(size_t tier = 0; tier < storage_tiers ; tier++) + stats->db_points_per_tier[tier] += qv->storage_points_per_tier[tier]; +} + +static void rrdset_metric_correlations_volume( + RRDHOST *host, + RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, RRDMETRIC_ACQUIRED *rma, + DICTIONARY *results, + time_t baseline_after, time_t baseline_before, + time_t after, time_t before, + RRDR_OPTIONS options, RRDR_TIME_GROUPING time_group_method, const char *time_group_options, + size_t tier, + WEIGHTS_STATS *stats, bool register_zero) { + + options |= RRDR_OPTION_MATCH_IDS | RRDR_OPTION_ABSOLUTE | RRDR_OPTION_NATURAL_POINTS; + + QUERY_VALUE baseline_average = rrdmetric2value(host, rca, ria, rma, baseline_after, baseline_before, + options, time_group_method, time_group_options, tier, 0, + QUERY_SOURCE_API_WEIGHTS, STORAGE_PRIORITY_SYNCHRONOUS); + merge_query_value_to_stats(&baseline_average, stats, 1); + + if(!netdata_double_isnumber(baseline_average.value)) { + // this means no data for the baseline window, but we may have data for the highlighted one - assume zero + baseline_average.value = 0.0; + } + + QUERY_VALUE highlight_average = rrdmetric2value(host, rca, ria, rma, after, before, + options, time_group_method, time_group_options, tier, 0, + QUERY_SOURCE_API_WEIGHTS, STORAGE_PRIORITY_SYNCHRONOUS); + merge_query_value_to_stats(&highlight_average, stats, 1); + + if(!netdata_double_isnumber(highlight_average.value)) + return; + + if(baseline_average.value == highlight_average.value) { + // they are the same - let's move on + return; + } + + if((options & RRDR_OPTION_ANOMALY_BIT) && highlight_average.value < baseline_average.value) { + // when working on anomaly bits, we are looking for an increase in the anomaly rate + return; + } + + char highlight_countif_options[50 + 1]; + snprintfz(highlight_countif_options, 50, "%s" NETDATA_DOUBLE_FORMAT, highlight_average.value < baseline_average.value ? "<" : ">", baseline_average.value); + QUERY_VALUE highlight_countif = rrdmetric2value(host, rca, ria, rma, after, before, + options, RRDR_GROUPING_COUNTIF, highlight_countif_options, tier, 0, + QUERY_SOURCE_API_WEIGHTS, STORAGE_PRIORITY_SYNCHRONOUS); + merge_query_value_to_stats(&highlight_countif, stats, 1); + + if(!netdata_double_isnumber(highlight_countif.value)) { + netdata_log_info("WEIGHTS: highlighted countif query failed, but highlighted average worked - strange..."); + return; + } + + // this represents the percentage of time + // the highlighted window was above/below the baseline window + // (above or below depending on their averages) + highlight_countif.value = highlight_countif.value / 100.0; // countif returns 0 - 100.0 + + RESULT_FLAGS flags; + NETDATA_DOUBLE pcent = NAN; + if(isgreater(baseline_average.value, 0.0) || isless(baseline_average.value, 0.0)) { + flags = RESULT_IS_BASE_HIGH_RATIO; + pcent = (highlight_average.value - baseline_average.value) / baseline_average.value * highlight_countif.value; + } + else { + flags = RESULT_IS_PERCENTAGE_OF_TIME; + pcent = highlight_countif.value; + } + + register_result(results, host, rca, ria, rma, pcent, flags, &highlight_average.sp, &baseline_average.sp, stats, + register_zero, baseline_average.duration_ut + highlight_average.duration_ut + highlight_countif.duration_ut); +} + +// ---------------------------------------------------------------------------- +// VALUE / ANOMALY RATE algorithm functions + +static void rrdset_weights_value( + RRDHOST *host, + RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, RRDMETRIC_ACQUIRED *rma, + DICTIONARY *results, + time_t after, time_t before, + RRDR_OPTIONS options, RRDR_TIME_GROUPING time_group_method, const char *time_group_options, + size_t tier, + WEIGHTS_STATS *stats, bool register_zero) { + + options |= RRDR_OPTION_MATCH_IDS | RRDR_OPTION_NATURAL_POINTS; + + QUERY_VALUE qv = rrdmetric2value(host, rca, ria, rma, after, before, + options, time_group_method, time_group_options, tier, 0, + QUERY_SOURCE_API_WEIGHTS, STORAGE_PRIORITY_SYNCHRONOUS); + + merge_query_value_to_stats(&qv, stats, 1); + + if(netdata_double_isnumber(qv.value)) + register_result(results, host, rca, ria, rma, qv.value, 0, &qv.sp, NULL, stats, register_zero, qv.duration_ut); +} + +static void rrdset_weights_multi_dimensional_value(struct query_weights_data *qwd) { + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .scope_nodes = qwd->qwr->scope_nodes, + .scope_contexts = qwd->qwr->scope_contexts, + .nodes = qwd->qwr->nodes, + .contexts = qwd->qwr->contexts, + .instances = qwd->qwr->instances, + .dimensions = qwd->qwr->dimensions, + .labels = qwd->qwr->labels, + .alerts = qwd->qwr->alerts, + .after = qwd->qwr->after, + .before = qwd->qwr->before, + .points = 1, + .options = qwd->qwr->options | RRDR_OPTION_NATURAL_POINTS, + .time_group_method = qwd->qwr->time_group_method, + .time_group_options = qwd->qwr->time_group_options, + .tier = qwd->qwr->tier, + .timeout_ms = qwd->qwr->timeout_ms, + .query_source = QUERY_SOURCE_API_WEIGHTS, + .priority = STORAGE_PRIORITY_NORMAL, + }; + + ONEWAYALLOC *owa = onewayalloc_create(16 * 1024); + QUERY_TARGET *qt = query_target_create(&qtr); + RRDR *r = rrd2rrdr(owa, qt); + + if(!r || rrdr_rows(r) != 1 || !r->d || r->d != r->internal.qt->query.used) + goto cleanup; + + QUERY_VALUE qv = { + .after = r->view.after, + .before = r->view.before, + .points_read = r->stats.db_points_read, + .result_points = r->stats.result_points_generated, + }; + + size_t queries = 0; + for(size_t d = 0; d < r->d ;d++) { + if(!rrdr_dimension_should_be_exposed(r->od[d], qwd->qwr->options)) + continue; + + long i = 0; // only one row + NETDATA_DOUBLE *cn = &r->v[ i * r->d ]; + NETDATA_DOUBLE *ar = &r->ar[ i * r->d ]; + + qv.value = cn[d]; + qv.anomaly_rate = ar[d]; + storage_point_merge_to(qv.sp, r->internal.qt->query.array[d].query_points); + + if(netdata_double_isnumber(qv.value)) { + QUERY_METRIC *qm = query_metric(r->internal.qt, d); + QUERY_DIMENSION *qd = query_dimension(r->internal.qt, qm->link.query_dimension_id); + QUERY_INSTANCE *qi = query_instance(r->internal.qt, qm->link.query_instance_id); + QUERY_CONTEXT *qc = query_context(r->internal.qt, qm->link.query_context_id); + QUERY_NODE *qn = query_node(r->internal.qt, qm->link.query_node_id); + + register_result(qwd->results, qn->rrdhost, qc->rca, qi->ria, qd->rma, qv.value, 0, &qv.sp, + NULL, &qwd->stats, qwd->register_zero, qm->duration_ut); + } + + queries++; + } + + merge_query_value_to_stats(&qv, &qwd->stats, queries); + +cleanup: + rrdr_free(owa, r); + query_target_release(qt); + onewayalloc_destroy(owa); +} + +// ---------------------------------------------------------------------------- + +int compare_netdata_doubles(const void *left, const void *right) { + NETDATA_DOUBLE lt = *(NETDATA_DOUBLE *)left; + NETDATA_DOUBLE rt = *(NETDATA_DOUBLE *)right; + + // https://stackoverflow.com/a/3886497/1114110 + return (lt > rt) - (lt < rt); +} + +static inline int binary_search_bigger_than_netdata_double(const NETDATA_DOUBLE arr[], int left, int size, NETDATA_DOUBLE K) { + // binary search to find the index the smallest index + // of the first value in the array that is greater than K + + int right = size; + while(left < right) { + int middle = (int)(((unsigned int)(left + right)) >> 1); + + if(arr[middle] > K) + right = middle; + + else + left = middle + 1; + } + + return left; +} + +// ---------------------------------------------------------------------------- +// spread the results evenly according to their value + +static size_t spread_results_evenly(DICTIONARY *results, WEIGHTS_STATS *stats) { + struct register_result *t; + + // count the dimensions + size_t dimensions = dictionary_entries(results); + if(!dimensions) return 0; + + if(stats->max_base_high_ratio == 0.0) + stats->max_base_high_ratio = 1.0; + + // create an array of the right size and copy all the values in it + NETDATA_DOUBLE slots[dimensions]; + dimensions = 0; + dfe_start_read(results, t) { + if(t->flags & RESULT_IS_PERCENTAGE_OF_TIME) + t->value = t->value * stats->max_base_high_ratio; + + slots[dimensions++] = t->value; + } + dfe_done(t); + + if(!dimensions) return 0; // Coverity fix + + // sort the array with the values of all dimensions + qsort(slots, dimensions, sizeof(NETDATA_DOUBLE), compare_netdata_doubles); + + // skip the duplicates in the sorted array + NETDATA_DOUBLE last_value = NAN; + size_t unique_values = 0; + for(size_t i = 0; i < dimensions ;i++) { + if(likely(slots[i] != last_value)) + slots[unique_values++] = last_value = slots[i]; + } + + // this cannot happen, but coverity thinks otherwise... + if(!unique_values) + unique_values = dimensions; + + // calculate the weight of each slot, using the number of unique values + NETDATA_DOUBLE slot_weight = 1.0 / (NETDATA_DOUBLE)unique_values; + + dfe_start_read(results, t) { + int slot = binary_search_bigger_than_netdata_double(slots, 0, (int)unique_values, t->value); + NETDATA_DOUBLE v = slot * slot_weight; + if(unlikely(v > 1.0)) v = 1.0; + v = 1.0 - v; + t->value = v; + } + dfe_done(t); + + return dimensions; +} + +// ---------------------------------------------------------------------------- +// The main function + +static ssize_t weights_for_rrdmetric(void *data, RRDHOST *host, RRDCONTEXT_ACQUIRED *rca, RRDINSTANCE_ACQUIRED *ria, RRDMETRIC_ACQUIRED *rma) { + struct query_weights_data *qwd = data; + QUERY_WEIGHTS_REQUEST *qwr = qwd->qwr; + + if(qwd->qwr->interrupt_callback && qwd->qwr->interrupt_callback(qwd->qwr->interrupt_callback_data)) { + qwd->interrupted = true; + return -1; + } + + qwd->examined_dimensions++; + + switch(qwr->method) { + case WEIGHTS_METHOD_VALUE: + rrdset_weights_value( + host, rca, ria, rma, + qwd->results, + qwr->after, qwr->before, + qwr->options, qwr->time_group_method, qwr->time_group_options, qwr->tier, + &qwd->stats, qwd->register_zero + ); + break; + + case WEIGHTS_METHOD_ANOMALY_RATE: + qwr->options |= RRDR_OPTION_ANOMALY_BIT; + rrdset_weights_value( + host, rca, ria, rma, + qwd->results, + qwr->after, qwr->before, + qwr->options, qwr->time_group_method, qwr->time_group_options, qwr->tier, + &qwd->stats, qwd->register_zero + ); + break; + + case WEIGHTS_METHOD_MC_VOLUME: + rrdset_metric_correlations_volume( + host, rca, ria, rma, + qwd->results, + qwr->baseline_after, qwr->baseline_before, + qwr->after, qwr->before, + qwr->options, qwr->time_group_method, qwr->time_group_options, qwr->tier, + &qwd->stats, qwd->register_zero + ); + break; + + default: + case WEIGHTS_METHOD_MC_KS2: + rrdset_metric_correlations_ks2( + host, rca, ria, rma, + qwd->results, + qwr->baseline_after, qwr->baseline_before, + qwr->after, qwr->before, qwr->points, + qwr->options, qwr->time_group_method, qwr->time_group_options, qwr->tier, qwd->shifts, + &qwd->stats, qwd->register_zero + ); + break; + } + + qwd->timings.executed_ut = now_monotonic_usec(); + if(qwd->timings.executed_ut - qwd->timings.received_ut > qwd->timeout_us) { + qwd->timed_out = true; + return -1; + } + + query_progress_done_step(qwr->transaction, 1); + + return 1; +} + +static ssize_t weights_do_context_callback(void *data, RRDCONTEXT_ACQUIRED *rca, bool queryable_context) { + if(!queryable_context) + return false; + + struct query_weights_data *qwd = data; + + bool has_retention = false; + switch(qwd->qwr->method) { + case WEIGHTS_METHOD_VALUE: + case WEIGHTS_METHOD_ANOMALY_RATE: + has_retention = rrdcontext_retention_match(rca, qwd->qwr->after, qwd->qwr->before); + break; + + case WEIGHTS_METHOD_MC_KS2: + case WEIGHTS_METHOD_MC_VOLUME: + has_retention = rrdcontext_retention_match(rca, qwd->qwr->after, qwd->qwr->before); + if(has_retention) + has_retention = rrdcontext_retention_match(rca, qwd->qwr->baseline_after, qwd->qwr->baseline_before); + break; + } + + if(!has_retention) + return 0; + + ssize_t ret = weights_foreach_rrdmetric_in_context(rca, + qwd->instances_sp, + NULL, + qwd->labels_sp, + qwd->alerts_sp, + qwd->dimensions_sp, + true, true, qwd->qwr->version, + weights_for_rrdmetric, qwd); + return ret; +} + +ssize_t weights_do_node_callback(void *data, RRDHOST *host, bool queryable) { + if(!queryable) + return 0; + + struct query_weights_data *qwd = data; + + ssize_t ret = query_scope_foreach_context(host, qwd->qwr->scope_contexts, + qwd->scope_contexts_sp, qwd->contexts_sp, + weights_do_context_callback, queryable, qwd); + + return ret; +} + +int web_api_v12_weights(BUFFER *wb, QUERY_WEIGHTS_REQUEST *qwr) { + + char *error = NULL; + int resp = HTTP_RESP_OK; + + // if the user didn't give a timeout + // assume 60 seconds + if(!qwr->timeout_ms) + qwr->timeout_ms = 5 * 60 * MSEC_PER_SEC; + + // if the timeout is less than 1 second + // make it at least 1 second + if(qwr->timeout_ms < (long)(1 * MSEC_PER_SEC)) + qwr->timeout_ms = 1 * MSEC_PER_SEC; + + struct query_weights_data qwd = { + .qwr = qwr, + + .scope_nodes_sp = string_to_simple_pattern(qwr->scope_nodes), + .scope_contexts_sp = string_to_simple_pattern(qwr->scope_contexts), + .nodes_sp = string_to_simple_pattern(qwr->nodes), + .contexts_sp = string_to_simple_pattern(qwr->contexts), + .instances_sp = string_to_simple_pattern(qwr->instances), + .dimensions_sp = string_to_simple_pattern(qwr->dimensions), + .labels_sp = string_to_simple_pattern(qwr->labels), + .alerts_sp = string_to_simple_pattern(qwr->alerts), + .timeout_us = qwr->timeout_ms * USEC_PER_MS, + .timed_out = false, + .examined_dimensions = 0, + .register_zero = true, + .results = register_result_init(), + .stats = {}, + .shifts = 0, + .timings = { + .received_ut = now_monotonic_usec(), + } + }; + + if(!rrdr_relative_window_to_absolute_query(&qwr->after, &qwr->before, NULL, false)) + buffer_no_cacheable(wb); + else + buffer_cacheable(wb); + + if (qwr->before <= qwr->after) { + resp = HTTP_RESP_BAD_REQUEST; + error = "Invalid selected time-range."; + goto cleanup; + } + + if(qwr->method == WEIGHTS_METHOD_MC_KS2 || qwr->method == WEIGHTS_METHOD_MC_VOLUME) { + if(!qwr->points) qwr->points = 500; + + if(qwr->baseline_before <= API_RELATIVE_TIME_MAX) + qwr->baseline_before += qwr->after; + + rrdr_relative_window_to_absolute_query(&qwr->baseline_after, &qwr->baseline_before, NULL, false); + + if (qwr->baseline_before <= qwr->baseline_after) { + resp = HTTP_RESP_BAD_REQUEST; + error = "Invalid baseline time-range."; + goto cleanup; + } + + // baseline should be a power of two multiple of highlight + long long base_delta = qwr->baseline_before - qwr->baseline_after; + long long high_delta = qwr->before - qwr->after; + uint32_t multiplier = (uint32_t)round((double)base_delta / (double)high_delta); + + // check if the multiplier is a power of two + // https://stackoverflow.com/a/600306/1114110 + if((multiplier & (multiplier - 1)) != 0) { + // it is not power of two + // let's find the closest power of two + // https://stackoverflow.com/a/466242/1114110 + multiplier--; + multiplier |= multiplier >> 1; + multiplier |= multiplier >> 2; + multiplier |= multiplier >> 4; + multiplier |= multiplier >> 8; + multiplier |= multiplier >> 16; + multiplier++; + } + + // convert the multiplier to the number of shifts + // we need to do, to divide baseline numbers to match + // the highlight ones + while(multiplier > 1) { + qwd.shifts++; + multiplier = multiplier >> 1; + } + + // if the baseline size will not comply to MAX_POINTS + // lower the window of the baseline + while(qwd.shifts && (qwr->points << qwd.shifts) > MAX_POINTS) + qwd.shifts--; + + // if the baseline size still does not comply to MAX_POINTS + // lower the resolution of the highlight and the baseline + while((qwr->points << qwd.shifts) > MAX_POINTS) + qwr->points = qwr->points >> 1; + + if(qwr->points < 15) { + resp = HTTP_RESP_BAD_REQUEST; + error = "Too few points available, at least 15 are needed."; + goto cleanup; + } + + // adjust the baseline to be multiplier times bigger than the highlight + qwr->baseline_after = qwr->baseline_before - (high_delta << qwd.shifts); + } + + if(qwr->options & RRDR_OPTION_NONZERO) { + qwd.register_zero = false; + + // remove it to run the queries without it + qwr->options &= ~RRDR_OPTION_NONZERO; + } + + if(qwr->host && qwr->version == 1) + weights_do_node_callback(&qwd, qwr->host, true); + else { + if((qwd.qwr->method == WEIGHTS_METHOD_VALUE || qwd.qwr->method == WEIGHTS_METHOD_ANOMALY_RATE) && (qwd.contexts_sp || qwd.scope_contexts_sp)) { + rrdset_weights_multi_dimensional_value(&qwd); + } + else { + query_scope_foreach_host(qwd.scope_nodes_sp, qwd.nodes_sp, + weights_do_node_callback, &qwd, + &qwd.versions, + NULL); + } + } + + if(!qwd.register_zero) { + // put it back, to show it in the response + qwr->options |= RRDR_OPTION_NONZERO; + } + + if(qwd.timed_out) { + error = "timed out"; + resp = HTTP_RESP_GATEWAY_TIMEOUT; + goto cleanup; + } + + if(qwd.interrupted) { + error = "interrupted"; + resp = HTTP_RESP_CLIENT_CLOSED_REQUEST; + goto cleanup; + } + + if(!qwd.register_zero) + qwr->options |= RRDR_OPTION_NONZERO; + + if(!(qwr->options & RRDR_OPTION_RETURN_RAW) && qwr->method != WEIGHTS_METHOD_VALUE) + spread_results_evenly(qwd.results, &qwd.stats); + + usec_t ended_usec = qwd.timings.executed_ut = now_monotonic_usec(); + + // generate the json output we need + buffer_flush(wb); + + size_t added_dimensions = 0; + switch(qwr->format) { + case WEIGHTS_FORMAT_CHARTS: + added_dimensions = + registered_results_to_json_charts( + qwd.results, wb, + qwr->after, qwr->before, + qwr->baseline_after, qwr->baseline_before, + qwr->points, qwr->method, qwr->time_group_method, qwr->options, qwd.shifts, + qwd.examined_dimensions, + ended_usec - qwd.timings.received_ut, &qwd.stats); + break; + + case WEIGHTS_FORMAT_CONTEXTS: + added_dimensions = + registered_results_to_json_contexts( + qwd.results, wb, + qwr->after, qwr->before, + qwr->baseline_after, qwr->baseline_before, + qwr->points, qwr->method, qwr->time_group_method, qwr->options, qwd.shifts, + qwd.examined_dimensions, + ended_usec - qwd.timings.received_ut, &qwd.stats); + break; + + default: + case WEIGHTS_FORMAT_MULTINODE: + // we don't support these groupings in weights + qwr->group_by.group_by &= ~(RRDR_GROUP_BY_LABEL|RRDR_GROUP_BY_SELECTED|RRDR_GROUP_BY_PERCENTAGE_OF_INSTANCE); + if(qwr->group_by.group_by == RRDR_GROUP_BY_NONE) { + added_dimensions = + registered_results_to_json_multinode_no_group_by( + qwd.results, wb, + qwr->after, qwr->before, + qwr->baseline_after, qwr->baseline_before, + qwr->points, qwr->method, qwr->time_group_method, qwr->options, qwd.shifts, + qwd.examined_dimensions, + &qwd, &qwd.stats, &qwd.versions); + } + else { + added_dimensions = + registered_results_to_json_multinode_group_by( + qwd.results, wb, + qwr->after, qwr->before, + qwr->baseline_after, qwr->baseline_before, + qwr->points, qwr->method, qwr->time_group_method, qwr->options, qwd.shifts, + qwd.examined_dimensions, + &qwd, &qwd.stats, &qwd.versions); + } + break; + } + + if(!added_dimensions && qwr->version < 2) { + error = "no results produced."; + resp = HTTP_RESP_NOT_FOUND; + } + +cleanup: + simple_pattern_free(qwd.scope_nodes_sp); + simple_pattern_free(qwd.scope_contexts_sp); + simple_pattern_free(qwd.nodes_sp); + simple_pattern_free(qwd.contexts_sp); + simple_pattern_free(qwd.instances_sp); + simple_pattern_free(qwd.dimensions_sp); + simple_pattern_free(qwd.labels_sp); + simple_pattern_free(qwd.alerts_sp); + + register_result_destroy(qwd.results); + + if(error) { + buffer_flush(wb); + buffer_sprintf(wb, "{\"error\": \"%s\" }", error); + } + + return resp; +} + +// ---------------------------------------------------------------------------- +// unittest + +/* + +Unit tests against the output of this: + +https://github.com/scipy/scipy/blob/4cf21e753cf937d1c6c2d2a0e372fbc1dbbeea81/scipy/stats/_stats_py.py#L7275-L7449 + +import matplotlib.pyplot as plt +import pandas as pd +import numpy as np +import scipy as sp +from scipy import stats + +data1 = np.array([ 1111, -2222, 33, 100, 100, 15555, -1, 19999, 888, 755, -1, -730 ]) +data2 = np.array([365, -123, 0]) +data1 = np.sort(data1) +data2 = np.sort(data2) +n1 = data1.shape[0] +n2 = data2.shape[0] +data_all = np.concatenate([data1, data2]) +cdf1 = np.searchsorted(data1, data_all, side='right') / n1 +cdf2 = np.searchsorted(data2, data_all, side='right') / n2 +print(data_all) +print("\ndata1", data1, cdf1) +print("\ndata2", data2, cdf2) +cddiffs = cdf1 - cdf2 +print("\ncddiffs", cddiffs) +minS = np.clip(-np.min(cddiffs), 0, 1) +maxS = np.max(cddiffs) +print("\nmin", minS) +print("max", maxS) +m, n = sorted([float(n1), float(n2)], reverse=True) +en = m * n / (m + n) +d = max(minS, maxS) +prob = stats.distributions.kstwo.sf(d, np.round(en)) +print("\nprob", prob) + +*/ + +static int double_expect(double v, const char *str, const char *descr) { + char buf[100 + 1]; + snprintfz(buf, sizeof(buf) - 1, "%0.6f", v); + int ret = strcmp(buf, str) ? 1 : 0; + + fprintf(stderr, "%s %s, expected %s, got %s\n", ret?"FAILED":"OK", descr, str, buf); + return ret; +} + +static int mc_unittest1(void) { + int bs = 3, hs = 3; + DIFFS_NUMBERS base[3] = { 1, 2, 3 }; + DIFFS_NUMBERS high[3] = { 3, 4, 6 }; + + double prob = ks_2samp(base, bs, high, hs, 0); + return double_expect(prob, "0.222222", "3x3"); +} + +static int mc_unittest2(void) { + int bs = 6, hs = 3; + DIFFS_NUMBERS base[6] = { 1, 2, 3, 10, 10, 15 }; + DIFFS_NUMBERS high[3] = { 3, 4, 6 }; + + double prob = ks_2samp(base, bs, high, hs, 1); + return double_expect(prob, "0.500000", "6x3"); +} + +static int mc_unittest3(void) { + int bs = 12, hs = 3; + DIFFS_NUMBERS base[12] = { 1, 2, 3, 10, 10, 15, 111, 19999, 8, 55, -1, -73 }; + DIFFS_NUMBERS high[3] = { 3, 4, 6 }; + + double prob = ks_2samp(base, bs, high, hs, 2); + return double_expect(prob, "0.347222", "12x3"); +} + +static int mc_unittest4(void) { + int bs = 12, hs = 3; + DIFFS_NUMBERS base[12] = { 1111, -2222, 33, 100, 100, 15555, -1, 19999, 888, 755, -1, -730 }; + DIFFS_NUMBERS high[3] = { 365, -123, 0 }; + + double prob = ks_2samp(base, bs, high, hs, 2); + return double_expect(prob, "0.777778", "12x3"); +} + +int mc_unittest(void) { + int errors = 0; + + errors += mc_unittest1(); + errors += mc_unittest2(); + errors += mc_unittest3(); + errors += mc_unittest4(); + + return errors; +} + diff --git a/src/web/api/queries/weights.h b/src/web/api/queries/weights.h new file mode 100644 index 000000000..a93519b6f --- /dev/null +++ b/src/web/api/queries/weights.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_WEIGHTS_H +#define NETDATA_API_WEIGHTS_H 1 + +#include "query.h" + +typedef enum { + WEIGHTS_METHOD_MC_KS2 = 1, + WEIGHTS_METHOD_MC_VOLUME = 2, + WEIGHTS_METHOD_ANOMALY_RATE = 3, + WEIGHTS_METHOD_VALUE = 4, +} WEIGHTS_METHOD; + +typedef enum { + WEIGHTS_FORMAT_CHARTS = 1, + WEIGHTS_FORMAT_CONTEXTS = 2, + WEIGHTS_FORMAT_MULTINODE = 3, +} WEIGHTS_FORMAT; + +extern int enable_metric_correlations; +extern int metric_correlations_version; +extern WEIGHTS_METHOD default_metric_correlations_method; + +typedef bool (*weights_interrupt_callback_t)(void *data); + +typedef struct query_weights_request { + size_t version; + RRDHOST *host; + const char *scope_nodes; + const char *scope_contexts; + const char *nodes; + const char *contexts; + const char *instances; + const char *dimensions; + const char *labels; + const char *alerts; + + struct { + RRDR_GROUP_BY group_by; + char *group_by_label; + RRDR_GROUP_BY_FUNCTION aggregation; + } group_by; + + WEIGHTS_METHOD method; + WEIGHTS_FORMAT format; + RRDR_TIME_GROUPING time_group_method; + const char *time_group_options; + time_t baseline_after; + time_t baseline_before; + time_t after; + time_t before; + size_t points; + RRDR_OPTIONS options; + size_t tier; + time_t timeout_ms; + + weights_interrupt_callback_t interrupt_callback; + void *interrupt_callback_data; + + uuid_t *transaction; +} QUERY_WEIGHTS_REQUEST; + +int web_api_v12_weights(BUFFER *wb, QUERY_WEIGHTS_REQUEST *qwr); + +WEIGHTS_METHOD weights_string_to_method(const char *method); +const char *weights_method_to_string(WEIGHTS_METHOD method); +int mc_unittest(void); + +#endif //NETDATA_API_WEIGHTS_H diff --git a/src/web/api/tests/valid_urls.c b/src/web/api/tests/valid_urls.c new file mode 100644 index 000000000..764d02807 --- /dev/null +++ b/src/web/api/tests/valid_urls.c @@ -0,0 +1,789 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "libnetdata/libnetdata.h" +#include "libnetdata/required_dummies.h" +#include "database/rrd.h" +#include "web/server/web_client.h" +#include <setjmp.h> +#include <cmocka.h> +#include <stdbool.h> + +void free_temporary_host(RRDHOST *host) +{ + (void) host; +} + +void *__wrap_free_temporary_host(RRDHOST *host) +{ + (void) host; + return NULL; +} + +void repr(char *result, int result_size, char const *buf, int size) +{ + int n; + char *end = result + result_size - 1; + unsigned char const *ubuf = (unsigned char const *)buf; + while (size && result_size > 0) { + if (*ubuf <= 0x20 || *ubuf >= 0x80) { + n = snprintf(result, result_size, "\\%02X", *ubuf); + } else { + *result = *ubuf; + n = 1; + } + result += n; + result_size -= n; + ubuf++; + size--; + } + if (result_size > 0) + *(result++) = 0; + else + *end = 0; +} + +// ---------------------------------- Mocking accesses from web_client ------------------------------------------------ + +ssize_t send(int sockfd, const void *buf, size_t len, int flags) +{ + netdata_log_info("Mocking send: %zu bytes\n", len); + (void)sockfd; + (void)buf; + (void)flags; + return len; +} + +RRDHOST *__wrap_rrdhost_find_by_hostname(const char *hostname, uint32_t hash) +{ + (void)hostname; + (void)hash; + return NULL; +} + +/* Note: we've got some intricate code inside the global statistics module, might be useful to pull it inside the + test set instead of mocking it. */ +void __wrap_finished_web_request_statistics( + uint64_t dt, uint64_t bytes_received, uint64_t bytes_sent, uint64_t content_size, uint64_t compressed_content_size) +{ + (void)dt; + (void)bytes_received; + (void)bytes_sent; + (void)content_size; + (void)compressed_content_size; +} + +char *__wrap_config_get(struct config *root, const char *section, const char *name, const char *default_value) +{ + (void)root; + (void)section; + (void)name; + (void)default_value; + return "UNKNOWN FIX ME"; +} + +int __wrap_web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url) +{ + char url_repr[160]; + repr(url_repr, sizeof(url_repr), url, strlen(url)); + printf("web_client_api_request_v1(url=\"%s\")\n", url_repr); + check_expected_ptr(host); + check_expected_ptr(w); + check_expected_ptr(url_repr); + return HTTP_RESP_OK; +} + +int __wrap_mysendfile(struct web_client *w, char *filename) +{ + (void)w; + printf("mysendfile(filename=\"%s\"\n", filename); + check_expected_ptr(filename); + return HTTP_RESP_OK; +} + +int __wrap_rrdpush_receiver_thread_spawn(RRDHOST *host, struct web_client *w, char *url) +{ + (void)host; + (void)w; + (void)url; + return 0; +} + +RRDHOST *__wrap_rrdhost_find_by_guid(const char *guid, uint32_t hash) +{ + (void)guid; + (void)hash; + printf("FIXME: rrdset_find_guid\n"); + return NULL; +} + +RRDSET *__wrap_rrdset_find_byname(RRDHOST *host, const char *name) +{ + (void)host; + (void)name; + printf("FIXME: rrdset_find_byname\n"); + return NULL; +} + +RRDSET *__wrap_rrdset_find(RRDHOST *host, const char *id) +{ + (void)host; + (void)id; + printf("FIXME: rrdset_find\n"); + return NULL; +} + +// -------------------------------- Mocking the log - dump straight through -------------------------------------------- + +void __wrap_debug_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + printf(" DEBUG: "); + printf(fmt, args); + printf("\n"); + va_end(args); +} + +void __wrap_info_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + printf(" INFO: "); + printf(fmt, args); + printf("\n"); + va_end(args); +} + +void __wrap_error_int( + const char *prefix, const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)prefix; + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + printf(" ERROR: "); + printf(fmt, args); + printf("\n"); + va_end(args); +} + +void __wrap_fatal_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + printf("FATAL: "); + printf(fmt, args); + printf("\n"); + va_end(args); + fail(); +} + +WEB_SERVER_MODE web_server_mode = WEB_SERVER_MODE_STATIC_THREADED; +char *netdata_configured_web_dir = "UNKNOWN FIXME"; +RRDHOST *localhost = NULL; + +struct config netdata_config = { .first_section = NULL, + .last_section = NULL, + .mutex = NETDATA_MUTEX_INITIALIZER, + .index = { .avl_tree = { .root = NULL, .compar = appconfig_section_compare }, + .rwlock = AVL_LOCK_INITIALIZER } }; + +/* Note: this is not a CMocka group_test_setup/teardown pair. This is performed per-test. +*/ +static struct web_client *setup_fresh_web_client() +{ + struct web_client *w = (struct web_client *)malloc(sizeof(struct web_client)); + memset(w, 0, sizeof(struct web_client)); + w->response.data = buffer_create(NETDATA_WEB_RESPONSE_INITIAL_SIZE); + w->response.header = buffer_create(NETDATA_WEB_RESPONSE_HEADER_SIZE); + w->response.header_output = buffer_create(NETDATA_WEB_RESPONSE_HEADER_SIZE); + strcpy(w->origin, "*"); // Simulate web_client_create_on_fd() + w->cookie1[0] = 0; // Simulate web_client_create_on_fd() + w->cookie2[0] = 0; // Simulate web_client_create_on_fd() + w->acl = 0x1f; // Everything on + return w; +} + +static void destroy_web_client(struct web_client *w) +{ + buffer_free(w->response.data); + buffer_free(w->response.header); + buffer_free(w->response.header_output); + free(w); +} + +//////////////////////////// Test cases /////////////////////////////////////////////////////////////////////////////// + +static void only_root(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET / HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_string(__wrap_mysendfile, filename, "/"); + + web_client_process_request(w); + + //assert_string_equal(w->decoded_query_string, def->query_out); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +static void two_slashes(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET // HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_string(__wrap_mysendfile, filename, "//"); + + web_client_process_request(w); + + //assert_string_equal(w->decoded_query_string, def->query_out); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +static void absolute_url(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET http://localhost:19999/api/v1/info HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, w); + expect_string(__wrap_web_client_api_request_v1, url_repr, "info"); + + web_client_process_request(w); + + assert_string_equal(w->decoded_query_string, "?blah"); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +static void valid_url(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /api/v1/info?blah HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, w); + expect_string(__wrap_web_client_api_request_v1, url_repr, "info"); + + web_client_process_request(w); + + assert_string_equal(w->decoded_query_string, "?blah"); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +/* RFC2616, section 4.1: + + In the interest of robustness, servers SHOULD ignore any empty + line(s) received where a Request-Line is expected. In other words, if + the server is reading the protocol stream at the beginning of a + message and receives a CRLF first, it should ignore the CRLF. +*/ +static void leading_blanks(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "\r\n\r\nGET /api/v1/info?blah HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, w); + expect_string(__wrap_web_client_api_request_v1, url_repr, "info"); + + web_client_process_request(w); + + assert_string_equal(w->decoded_query_string, "?blah"); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +static void empty_url(void **state) +{ + (void)state; + + if (localhost != NULL) + free(localhost); + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET HTTP/1.1\r\n\r\n"); + + char debug[4096]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("-> \"%s\"\n", debug); + + //char expected_url_repr[4096]; + //repr(expected_url_repr, sizeof(expected_url_repr), def->url_out_repr, strlen(def->url_out_repr)); + + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, w); + expect_string(__wrap_web_client_api_request_v1, url_repr, "info"); + + web_client_process_request(w); + + assert_string_equal(w->decoded_query_string, "?blah"); + destroy_web_client(w); + free(localhost); + localhost = NULL; +} + +/* If the %-escape is being performed at the correct time then the url should not be treated as a query, but instead + as a path "/api/v1/info?blah?" which should dispatch into the API with the given values. +*/ +static void not_a_query(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /api/v1/info%3fblah%3f HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "info?blah?", 10); + + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, w); + expect_string(__wrap_web_client_api_request_v1, url_repr, expected_url_repr); + + web_client_process_request(w); + + assert_string_equal(w->decoded_query_string, ""); + destroy_web_client(w); + free(localhost); +} + +static void cr_in_url(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /api/v1/inf\ro\t?blah HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} +static void newline_in_url(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /api/v1/inf\no\t?blah HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void bad_version(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /api/v1/info?blah HTTP/1.2\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void pathless_query(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET ?blah HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void pathless_fragment(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET #blah HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void short_percent(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET % HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void short_percent2(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET %0 HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void short_percent3(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET %"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void percent_nulls(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET %00%00%00%00%00%00 HTTP/1.1\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void percent_invalid(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET /%x%x%x%x%x%x HTTP/1.1\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void space_in_url(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET / / HTTP/1.1\r\n\r\n"); + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void random_sploit1(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + // FIXME: Encoding probably needs to go through printf + buffer_need_bytes(w->response.data, 55); + memcpy( + w->response.data->buffer, + "GET \x03\x00\x00/*\xE0\x00\x00\x00\x00\x00Cookie: mstshash=Administr HTTP/1.1\r\n\r\n", 54); + w->response.data->len = 54; + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +static void null_in_url(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET / / HTTP/1.1\r\n\r\n"); + w->response.data->buffer[5] = 0; + + char debug[160]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} +static void many_ands(void **state) +{ + (void)state; + localhost = malloc(sizeof(RRDHOST)); + + struct web_client *w = setup_fresh_web_client(); + buffer_strcat(w->response.data, "GET foo?"); + for (size_t i = 0; i < 600; i++) + buffer_strcat(w->response.data, "&"); + buffer_strcat(w->response.data, " HTTP/1.1\r\n\r\n"); + + char debug[2048]; + repr(debug, sizeof(debug), w->response.data->buffer, w->response.data->len); + printf("->%s\n", debug); + + char expected_url_repr[160]; + repr(expected_url_repr, sizeof(expected_url_repr), "inf\no\t", 6); + + web_client_process_request(w); + + assert_int_equal(w->response.code, HTTP_RESP_BAD_REQUEST); + + destroy_web_client(w); + free(localhost); +} + +int main(void) +{ + debug_flags = 0xffffffffffff; + int fails = 0; + + struct CMUnitTest static_tests[] = { + cmocka_unit_test(only_root), cmocka_unit_test(two_slashes), cmocka_unit_test(valid_url), + cmocka_unit_test(leading_blanks), cmocka_unit_test(empty_url), cmocka_unit_test(newline_in_url), + cmocka_unit_test(not_a_query), cmocka_unit_test(cr_in_url), cmocka_unit_test(pathless_query), + cmocka_unit_test(pathless_fragment), cmocka_unit_test(short_percent), cmocka_unit_test(short_percent2), + cmocka_unit_test(short_percent3), cmocka_unit_test(percent_nulls), cmocka_unit_test(percent_invalid), + cmocka_unit_test(space_in_url), cmocka_unit_test(random_sploit1), cmocka_unit_test(null_in_url), + cmocka_unit_test(absolute_url), + // cmocka_unit_test(many_ands), CMocka cannot recover after this crash + cmocka_unit_test(bad_version) + }; + (void)many_ands; + + fails += cmocka_run_group_tests_name("static_tests", static_tests, NULL, NULL); + return fails; +} diff --git a/src/web/api/tests/web_api.c b/src/web/api/tests/web_api.c new file mode 100644 index 000000000..694929a94 --- /dev/null +++ b/src/web/api/tests/web_api.c @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "libnetdata/libnetdata.h" +#include "libnetdata/required_dummies.h" +#include "database/rrd.h" +#include "web/server/web_client.h" +#include <setjmp.h> +#include <cmocka.h> +#include <stdbool.h> + +void free_temporary_host(RRDHOST *host) +{ + (void) host; +} + +void *__wrap_free_temporary_host(RRDHOST *host) +{ + (void) host; + return NULL; +} + +void repr(char *result, int result_size, char const *buf, int size) +{ + int n; + char *end = result + result_size - 1; + unsigned char const *ubuf = (unsigned char const *)buf; + while (size && result_size > 0) { + if (*ubuf <= 0x20 || *ubuf >= 0x80) { + n = snprintf(result, result_size, "\\%02X", *ubuf); + } else { + *result = *ubuf; + n = 1; + } + result += n; + result_size -= n; + ubuf++; + size--; + } + if (result_size > 0) + *(result++) = 0; + else + *end = 0; +} + +// ---------------------------------- Mocking accesses from web_client ------------------------------------------------ + +ssize_t send(int sockfd, const void *buf, size_t len, int flags) +{ + netdata_log_info("Mocking send: %zu bytes\n", len); + (void)sockfd; + (void)buf; + (void)flags; + return len; +} + +RRDHOST *__wrap_rrdhost_find_by_hostname(const char *hostname, uint32_t hash) +{ + (void)hostname; + (void)hash; + return NULL; +} + +/* Note: we've got some intricate code inside the global statistics module, might be useful to pull it inside the + test set instead of mocking it. */ +void __wrap_finished_web_request_statistics( + uint64_t dt, uint64_t bytes_received, uint64_t bytes_sent, uint64_t content_size, uint64_t compressed_content_size) +{ + (void)dt; + (void)bytes_received; + (void)bytes_sent; + (void)content_size; + (void)compressed_content_size; +} + +char *__wrap_config_get(struct config *root, const char *section, const char *name, const char *default_value) +{ + (void)root; + (void)section; + (void)name; + (void)default_value; + return "UNKNOWN FIX ME"; +} + +int __wrap_web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url) +{ + char url_repr[160]; + repr(url_repr, sizeof(url_repr), url, strlen(url)); + netdata_log_info("web_client_api_request_v1(url=\"%s\")\n", url_repr); + check_expected_ptr(host); + check_expected_ptr(w); + check_expected_ptr(url_repr); + return HTTP_RESP_OK; +} + +int __wrap_rrdpush_receiver_thread_spawn(RRDHOST *host, struct web_client *w, char *url) +{ + (void)host; + (void)w; + (void)url; + return 0; +} + +RRDHOST *__wrap_rrdhost_find_by_guid(const char *guid, uint32_t hash) +{ + (void)guid; + (void)hash; + printf("FIXME: rrdset_find_guid\n"); + return NULL; +} + +RRDSET *__wrap_rrdset_find_byname(RRDHOST *host, const char *name) +{ + (void)host; + (void)name; + printf("FIXME: rrdset_find_byname\n"); + return NULL; +} + +RRDSET *__wrap_rrdset_find(RRDHOST *host, const char *id) +{ + (void)host; + (void)id; + printf("FIXME: rrdset_find\n"); + return NULL; +} + +// -------------------------------- Mocking the log - capture per-test ------------------------------------------------ + +char log_buffer[10240] = { 0 }; +void __wrap_debug_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + size_t cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, " DEBUG: "); + cur = strlen(log_buffer); + vsnprintf(log_buffer + cur, sizeof(log_buffer) - cur, fmt, args); + cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, "\n"); + va_end(args); +} + +void __wrap_info_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + size_t cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, " INFO: "); + cur = strlen(log_buffer); + vsnprintf(log_buffer + cur, sizeof(log_buffer) - cur, fmt, args); + cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, "\n"); + va_end(args); +} + +void __wrap_error_int( + const char *prefix, const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)prefix; + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + size_t cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, " ERROR: "); + cur = strlen(log_buffer); + vsnprintf(log_buffer + cur, sizeof(log_buffer) - cur, fmt, args); + cur = strlen(log_buffer); + snprintf(log_buffer + cur, sizeof(log_buffer) - cur, "\n"); + va_end(args); +} + +void __wrap_fatal_int(const char *file, const char *function, const unsigned long line, const char *fmt, ...) +{ + (void)file; + (void)function; + (void)line; + va_list args; + va_start(args, fmt); + printf("FATAL: "); + vprintf(fmt, args); + printf("\n"); + va_end(args); + fail(); +} + +WEB_SERVER_MODE web_server_mode = WEB_SERVER_MODE_STATIC_THREADED; +char *netdata_configured_web_dir = "UNKNOWN FIXME"; +RRDHOST *localhost = NULL; + +struct config netdata_config = { .first_section = NULL, + .last_section = NULL, + .mutex = NETDATA_MUTEX_INITIALIZER, + .index = { .avl_tree = { .root = NULL, .compar = appconfig_section_compare }, + .rwlock = AVL_LOCK_INITIALIZER } }; + +const char *http_headers[] = { "Host: 254.254.0.1", + "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_" // No , + "0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36", + "Connection: keep-alive", + "X-Forwarded-For: 1.254.1.251", + "Cookie: _ga=GA1.1.1227576758.1571113676; _gid=GA1.2.1222321739.1573628979", + "X-Requested-With: XMLHttpRequest", + "Accept-Encoding: gzip, deflate", + "Cache-Control: no-cache, no-store" }; +#define MAX_HEADERS (sizeof(http_headers) / (sizeof(const char *))) + +static void build_request(struct web_buffer *wb, const char *url, bool use_cr, size_t num_headers) +{ + buffer_reset(wb); + buffer_strcat(wb, "GET "); + buffer_strcat(wb, url); + buffer_strcat(wb, " HTTP/1.1"); + if (use_cr) + buffer_strcat(wb, "\r"); + buffer_strcat(wb, "\n"); + for (size_t i = 0; i < num_headers && i < MAX_HEADERS; i++) { + buffer_strcat(wb, http_headers[i]); + if (use_cr) + buffer_strcat(wb, "\r"); + buffer_strcat(wb, "\n"); + } + if (use_cr) + buffer_strcat(wb, "\r"); + buffer_strcat(wb, "\n"); +} + +/* Note: this is not a CMocka group_test_setup/teardown pair. This is performed per-test. +*/ +static struct web_client *setup_fresh_web_client() +{ + struct web_client *w = (struct web_client *)malloc(sizeof(struct web_client)); + memset(w, 0, sizeof(struct web_client)); + w->response.data = buffer_create(NETDATA_WEB_RESPONSE_INITIAL_SIZE); + w->response.data->date = 0; // Valgrind uninitialised value + w->response.data->expires = 0; // Valgrind uninitialised value + w->response.data->options = 0; // Valgrind uninitialised value + w->response.header = buffer_create(NETDATA_WEB_RESPONSE_HEADER_SIZE); + w->response.header_output = buffer_create(NETDATA_WEB_RESPONSE_HEADER_SIZE); + strcpy(w->origin, "*"); // Simulate web_client_create_on_fd() + w->cookie1[0] = 0; // Simulate web_client_create_on_fd() + w->cookie2[0] = 0; // Simulate web_client_create_on_fd() + w->acl = 0x1f; // Everything on + return w; +} + +static void destroy_web_client(struct web_client *w) +{ + buffer_free(w->response.data); + buffer_free(w->response.header); + buffer_free(w->response.header_output); + free(w); +} + +// ---------------------------------- Parameterized test-families ----------------------------------------------------- +// There is no way to pass a parameter block into the setup fixture, we would have to patch CMocka and maintain it +// locally. (The void **current_state in _run_group_tests would be set from a parameter). This is unfortunate as a +// parametric unit-tester needs to be to pass parameters to the fixtures. We are faking this by calculating the +// space of tests in the launcher, passing an array of identical unit-tests to CMocka and then counting through the +// parameters in the shared state passed between tests. To initialise this counter structure we use this global to +// pass from the launcher (test-builder) to the setup-fixture. + +void *shared_test_state = NULL; + +// -------------------------------- Test family for /api/v1/info ------------------------------------------------------ + +struct test_def { + size_t num_headers; // Index coordinate + size_t prefix_len; // Index coordinate + char name[80]; + size_t full_len; + struct web_client *instance; // Used within this single test + bool completed, use_cr; + struct test_def *next, *prev; +}; + +static void api_info(void **state) +{ + (void)state; + struct test_def *def = (struct test_def *)shared_test_state; + shared_test_state = def->next; + + if (def->prev != NULL && !def->prev->completed && strlen(log_buffer) > 0) { + printf("Log of failing case %s:\n", def->prev->name); + puts(log_buffer); + } + log_buffer[0] = 0; + if (localhost != NULL) + free(localhost); + localhost = calloc(1,sizeof(RRDHOST)); + + def->instance = setup_fresh_web_client(); + build_request(def->instance->response.data, "/api/v1/info", def->use_cr, def->num_headers); + def->instance->response.data->len = def->prefix_len; + + char buffer_repr[1024]; + repr(buffer_repr, sizeof(buffer_repr), def->instance->response.data->buffer,def->prefix_len); + netdata_log_info("Buffer contains: %s [first %zu]", buffer_repr,def->prefix_len); + if (def->prefix_len == def->full_len) { + expect_value(__wrap_web_client_api_request_v1, host, localhost); + expect_value(__wrap_web_client_api_request_v1, w, def->instance); + expect_string(__wrap_web_client_api_request_v1, url_repr, "info"); + } + + web_client_process_request(def->instance); + + if (def->prefix_len == def->full_len) + assert_int_equal(def->instance->flags & WEB_CLIENT_FLAG_WAIT_RECEIVE, 0); + else + assert_int_equal(def->instance->flags & WEB_CLIENT_FLAG_WAIT_RECEIVE, WEB_CLIENT_FLAG_WAIT_RECEIVE); + assert_int_equal(def->instance->mode, WEB_CLIENT_MODE_NORMAL); + def->completed = true; + log_buffer[0] = 0; +} + +static int api_info_launcher() +{ + size_t num_tests = 0; + struct web_client *template = setup_fresh_web_client(); + struct test_def *current, *head = NULL; + struct test_def *prev = NULL; + + for (size_t i = 0; i < MAX_HEADERS; i++) { + build_request(template->response.data, "/api/v1/info", true, i); + for (size_t j = 0; j <= template->response.data->len; j++) { + if (j == 0 && i > 0) + continue; // All zero-length prefixes are identical, skip after first time + current = malloc(sizeof(struct test_def)); + if (prev != NULL) + prev->next = current; + else + head = current; + current->prev = prev; + prev = current; + + current->num_headers = i; + current->prefix_len = j; + current->full_len = template->response.data->len; + current->instance = NULL; + current->next = NULL; + current->use_cr = true; + current->completed = false; + sprintf( + current->name, "/api/v1/info@%zu,%zu/%zu+%d", current->num_headers, current->prefix_len, + current->full_len,true); + num_tests++; + } + } + for (size_t i = 0; i < MAX_HEADERS; i++) { + build_request(template->response.data, "/api/v1/info", false, i); + for (size_t j = 0; j <= template->response.data->len; j++) { + if (j == 0 && i > 0) + continue; // All zero-length prefixes are identical, skip after first time + current = malloc(sizeof(struct test_def)); + if (prev != NULL) + prev->next = current; + else + head = current; + current->prev = prev; + prev = current; + + current->num_headers = i; + current->prefix_len = j; + current->full_len = template->response.data->len; + current->instance = NULL; + current->next = NULL; + current->use_cr = false; + current->completed = false; + sprintf( + current->name, "/api/v1/info@%zu,%zu/%zu+%d", current->num_headers, current->prefix_len, + current->full_len,false); + num_tests++; + } + } + + struct CMUnitTest *tests = calloc(num_tests, sizeof(struct CMUnitTest)); + current = head; + for (size_t i = 0; i < num_tests; i++) { + tests[i].name = current->name; + tests[i].test_func = api_info; + tests[i].setup_func = NULL; + tests[i].teardown_func = NULL; + tests[i].initial_state = NULL; + current = current->next; + } + + printf("Setup %zu tests in %p\n", num_tests, head); + shared_test_state = head; + int fails = _cmocka_run_group_tests("web_api", tests, num_tests, NULL, NULL); + free(tests); + destroy_web_client(template); + current = head; + while (current != NULL) { + struct test_def *c = current; + current = current->next; + if (c->instance != NULL) // Clean up resources from tests that failed + destroy_web_client(c->instance); + free(c); + } + if (localhost!=NULL) + free(localhost); + return fails; +} + +/* Raw notes for the cases that we did not use in the unit testing suite. + Leaving them here instead of deleting them in-case we expand the suite during the + work on the URL parser. + + Any ' ' in the URI -> invalid response (Description in 5.1 of RFC2616) + Characters that can't be in paths #;? + "GET /apb/../api/v1/info" HTTP/1.1\r\n" + + https://github.com/uriparser/uriparser/blob/uriparser-0.9.3/test/FourSuite.cpp + Not clear why some of these are illegal -> reserved chars? + + ASSERT_TRUE(testBadUri("beepbeep\x07\x07", 8)); + ASSERT_TRUE(testBadUri("\n", 0)); + ASSERT_TRUE(testBadUri("::", 0)); // not OK, per Roy Fielding on the W3C uri list on 2004-04-01 + + // the following test cases are from a Perl script by David A. Wheeler + // at http://www.dwheeler.com/secure-programs/url.pl + ASSERT_TRUE(testBadUri("http://www yahoo.com", 10)); + ASSERT_TRUE(testBadUri("http://www.yahoo.com/hello world/", 26)); + ASSERT_TRUE(testBadUri("http://www.yahoo.com/yelp.html#\"", 31)); + + // the following test cases are from a Haskell program by Graham Klyne + // at http://www.ninebynine.org/Software/HaskellUtils/Network/URITest.hs + ASSERT_TRUE(testBadUri("[2010:836B:4179::836B:4179]", 0)); + ASSERT_TRUE(testBadUri(" ", 0)); + ASSERT_TRUE(testBadUri("%", 1)); + ASSERT_TRUE(testBadUri("A%Z", 2)); + ASSERT_TRUE(testBadUri("%ZZ", 1)); + ASSERT_TRUE(testBadUri("%AZ", 2)); + ASSERT_TRUE(testBadUri("A C", 1)); + ASSERT_TRUE(testBadUri("A\\'C", 1)); // r"A\'C" + ASSERT_TRUE(testBadUri("A`C", 1)); + ASSERT_TRUE(testBadUri("A<C", 1)); + ASSERT_TRUE(testBadUri("A>C", 1)); + ASSERT_TRUE(testBadUri("A^C", 1)); + ASSERT_TRUE(testBadUri("A\\\\C", 1)); // r'A\\C' + ASSERT_TRUE(testBadUri("A{C", 1)); + ASSERT_TRUE(testBadUri("A|C", 1)); + ASSERT_TRUE(testBadUri("A}C", 1)); + ASSERT_TRUE(testBadUri("A[C", 1)); + ASSERT_TRUE(testBadUri("A]C", 1)); + ASSERT_TRUE(testBadUri("A[**]C", 1)); + ASSERT_TRUE(testBadUri("http://[xyz]/", 8)); + ASSERT_TRUE(testBadUri("http://]/", 7)); + ASSERT_TRUE(testBadUri("http://example.org/[2010:836B:4179::836B:4179]", 19)); + ASSERT_TRUE(testBadUri("http://example.org/abc#[2010:836B:4179::836B:4179]", 23)); + ASSERT_TRUE(testBadUri("http://example.org/xxx/[qwerty]#a[b]", 23)); + + // from a post to the W3C uri list on 2004-02-17 + // breaks at 22 instead of 17 because everything up to that point is a valid userinfo + ASSERT_TRUE(testBadUri("http://w3c.org:80path1/path2", 22)); + +*/ + +int main(void) +{ + debug_flags = 0xffffffffffff; + int fails = 0; + fails += api_info_launcher(); + + return fails; +} diff --git a/src/web/api/web_api.c b/src/web/api/web_api.c new file mode 100644 index 000000000..10a00e22e --- /dev/null +++ b/src/web/api/web_api.c @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web_api.h" + +int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_path_endpoint, struct web_api_command *api_commands) { + buffer_no_cacheable(w->response.data); + + internal_fatal(web_client_flags_check_auth(w) && !(w->access & HTTP_ACCESS_SIGNED_ID), + "signed-in permission should be set, but is missing"); + + internal_fatal(!web_client_flags_check_auth(w) && (w->access & HTTP_ACCESS_SIGNED_ID), + "signed-in permission is set, but it shouldn't"); + + if(!web_client_flags_check_auth(w)) { + w->user_role = (netdata_is_protected_by_bearer) ? HTTP_USER_ROLE_NONE : HTTP_USER_ROLE_ANY; + w->access = (netdata_is_protected_by_bearer) ? HTTP_ACCESS_NONE : HTTP_ACCESS_ANONYMOUS_DATA; + } + +#ifdef NETDATA_GOD_MODE + web_client_flag_set(w, WEB_CLIENT_FLAG_AUTH_GOD); + w->user_role = HTTP_USER_ROLE_ADMIN; + w->access = HTTP_ACCESS_ALL; +#endif + + if((w->access & HTTP_ACCESS_SIGNED_ID) && !(w->access & HTTP_ACCESS_SAME_SPACE)) { + // this should never happen: a signed-in user from a different space + return web_client_permission_denied(w); + } + + if(unlikely(!url_path_endpoint || !*url_path_endpoint)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Which API command?"); + return HTTP_RESP_BAD_REQUEST; + } + + char *api_command = strchr(url_path_endpoint, '/'); + if (likely(api_command == NULL)) // only config command supports subpaths for now + api_command = url_path_endpoint; + else { + size_t api_command_len = api_command - url_path_endpoint; + api_command = callocz(1, api_command_len + 1); + memcpy(api_command, url_path_endpoint, api_command_len); + } + + uint32_t hash = simple_hash(api_command); + + for(int i = 0; api_commands[i].api ; i++) { + if(unlikely(hash == api_commands[i].hash && !strcmp(api_command, api_commands[i].api))) { + if(unlikely(!api_commands[i].allow_subpaths && api_command != url_path_endpoint)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "API command '%s' does not support subpaths.", api_command); + freez(api_command); + return HTTP_RESP_BAD_REQUEST; + } + + if (api_command != url_path_endpoint) + freez(api_command); + + bool acl_allows = ((w->acl & api_commands[i].acl) == api_commands[i].acl) || (api_commands[i].acl & HTTP_ACL_NOCHECK); + if(!acl_allows) + return web_client_permission_denied_acl(w); + + bool permissions_allows = + http_access_user_has_enough_access_level_for_endpoint(w->access, api_commands[i].access); + if(!permissions_allows) + return web_client_permission_denied(w); + + char *query_string = (char *)buffer_tostring(w->url_query_string_decoded); + + if(*query_string == '?') + query_string = &query_string[1]; + + return api_commands[i].callback(host, w, query_string); + } + } + + if (api_command != url_path_endpoint) + freez(api_command); + + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Unsupported API command: "); + buffer_strcat_htmlescape(w->response.data, url_path_endpoint); + return HTTP_RESP_NOT_FOUND; +} + +RRDCONTEXT_TO_JSON_OPTIONS rrdcontext_to_json_parse_options(char *o) { + RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + + if(!strcmp(tok, "full") || !strcmp(tok, "all")) + options |= RRDCONTEXT_OPTIONS_ALL; + else if(!strcmp(tok, "charts") || !strcmp(tok, "instances")) + options |= RRDCONTEXT_OPTION_SHOW_INSTANCES; + else if(!strcmp(tok, "dimensions") || !strcmp(tok, "metrics")) + options |= RRDCONTEXT_OPTION_SHOW_METRICS; + else if(!strcmp(tok, "queue")) + options |= RRDCONTEXT_OPTION_SHOW_QUEUED; + else if(!strcmp(tok, "flags")) + options |= RRDCONTEXT_OPTION_SHOW_FLAGS; + else if(!strcmp(tok, "uuids")) + options |= RRDCONTEXT_OPTION_SHOW_UUIDS; + else if(!strcmp(tok, "deleted")) + options |= RRDCONTEXT_OPTION_SHOW_DELETED; + else if(!strcmp(tok, "labels")) + options |= RRDCONTEXT_OPTION_SHOW_LABELS; + else if(!strcmp(tok, "deepscan")) + options |= RRDCONTEXT_OPTION_DEEPSCAN; + else if(!strcmp(tok, "hidden")) + options |= RRDCONTEXT_OPTION_SHOW_HIDDEN; + } + + return options; +} + +int web_client_api_request_weights(RRDHOST *host, struct web_client *w, char *url, WEIGHTS_METHOD method, WEIGHTS_FORMAT format, size_t api_version) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + time_t baseline_after = 0, baseline_before = 0, after = 0, before = 0; + size_t points = 0; + RRDR_OPTIONS options = 0; + RRDR_TIME_GROUPING time_group_method = RRDR_GROUPING_AVERAGE; + time_t timeout_ms = 0; + size_t tier = 0; + const char *time_group_options = NULL, *scope_contexts = NULL, *scope_nodes = NULL, *contexts = NULL, *nodes = NULL, + *instances = NULL, *dimensions = NULL, *labels = NULL, *alerts = NULL; + + struct group_by_pass group_by = { + .group_by = RRDR_GROUP_BY_NONE, + .group_by_label = NULL, + .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, + }; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) + continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) + continue; + if (!value || !*value) + continue; + + if (!strcmp(name, "baseline_after")) + baseline_after = str2l(value); + + else if (!strcmp(name, "baseline_before")) + baseline_before = str2l(value); + + else if (!strcmp(name, "after") || !strcmp(name, "highlight_after")) + after = str2l(value); + + else if (!strcmp(name, "before") || !strcmp(name, "highlight_before")) + before = str2l(value); + + else if (!strcmp(name, "points") || !strcmp(name, "max_points")) + points = str2ul(value); + + else if (!strcmp(name, "timeout")) + timeout_ms = str2l(value); + + else if((api_version == 1 && !strcmp(name, "group")) || (api_version >= 2 && !strcmp(name, "time_group"))) + time_group_method = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + + else if((api_version == 1 && !strcmp(name, "group_options")) || (api_version >= 2 && !strcmp(name, "time_group_options"))) + time_group_options = value; + + else if(!strcmp(name, "options")) + options |= rrdr_options_parse(value); + + else if(!strcmp(name, "method")) + method = weights_string_to_method(value); + + else if(api_version == 1 && (!strcmp(name, "context") || !strcmp(name, "contexts"))) + scope_contexts = value; + + else if(api_version >= 2 && !strcmp(name, "scope_nodes")) scope_nodes = value; + else if(api_version >= 2 && !strcmp(name, "scope_contexts")) scope_contexts = value; + else if(api_version >= 2 && !strcmp(name, "nodes")) nodes = value; + else if(api_version >= 2 && !strcmp(name, "contexts")) contexts = value; + else if(api_version >= 2 && !strcmp(name, "instances")) instances = value; + else if(api_version >= 2 && !strcmp(name, "dimensions")) dimensions = value; + else if(api_version >= 2 && !strcmp(name, "labels")) labels = value; + else if(api_version >= 2 && !strcmp(name, "alerts")) alerts = value; + else if(api_version >= 2 && (!strcmp(name, "group_by") || !strcmp(name, "group_by[0]"))) { + group_by.group_by = group_by_parse(value); + } + else if(api_version >= 2 && (!strcmp(name, "group_by_label") || !strcmp(name, "group_by_label[0]"))) { + group_by.group_by_label = value; + } + else if(api_version >= 2 && (!strcmp(name, "aggregation") || !strcmp(name, "aggregation[0]"))) { + group_by.aggregation = group_by_aggregate_function_parse(value); + } + + else if(!strcmp(name, "tier")) { + tier = str2ul(value); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + } + + if(options == 0) + // the user did not set any options + options = RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO | RRDR_OPTION_NONZERO; + else + // the user set some options, add also these + options |= RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO; + + if(options & RRDR_OPTION_PERCENTAGE) + options |= RRDR_OPTION_ABSOLUTE; + + if(options & RRDR_OPTION_DEBUG) + options &= ~RRDR_OPTION_MINIFY; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + QUERY_WEIGHTS_REQUEST qwr = { + .version = api_version, + .host = (api_version == 1) ? NULL : host, + .scope_nodes = scope_nodes, + .scope_contexts = scope_contexts, + .nodes = nodes, + .contexts = contexts, + .instances = instances, + .dimensions = dimensions, + .labels = labels, + .alerts = alerts, + .group_by = { + .group_by = group_by.group_by, + .group_by_label = group_by.group_by_label, + .aggregation = group_by.aggregation, + }, + .method = method, + .format = format, + .time_group_method = time_group_method, + .time_group_options = time_group_options, + .baseline_after = baseline_after, + .baseline_before = baseline_before, + .after = after, + .before = before, + .points = points, + .options = options, + .tier = tier, + .timeout_ms = timeout_ms, + + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + + .transaction = &w->transaction, + }; + + return web_api_v12_weights(wb, &qwr); +} + +bool web_client_interrupt_callback(void *data) { + struct web_client *w = data; + + if(w->interrupt.callback) + return w->interrupt.callback(w, w->interrupt.callback_data); + + return sock_has_output_error(w->ofd); +} diff --git a/src/web/api/web_api.h b/src/web/api/web_api.h new file mode 100644 index 000000000..6d30630dc --- /dev/null +++ b/src/web/api/web_api.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_API_H +#define NETDATA_WEB_API_H 1 + +#include "daemon/common.h" +#include "web/api/http_header.h" +#include "web/api/http_auth.h" +#include "web/api/badges/web_buffer_svg.h" +#include "web/api/ilove/ilove.h" +#include "web/api/formatters/rrd2json.h" +#include "web/api/queries/weights.h" + +struct web_api_command { + const char *api; + uint32_t hash; + HTTP_ACL acl; + HTTP_ACCESS access; + int (*callback)(RRDHOST *host, struct web_client *w, char *url); + unsigned int allow_subpaths; +}; + +struct web_client; + +int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_path_endpoint, struct web_api_command *api_commands); + +static inline void fix_google_param(char *s) { + if(unlikely(!s || !*s)) return; + + for( ; *s ;s++) { + if(!isalnum(*s) && *s != '.' && *s != '_' && *s != '-') + *s = '_'; + } +} + +int web_client_api_request_weights(RRDHOST *host, struct web_client *w, char *url, WEIGHTS_METHOD method, WEIGHTS_FORMAT format, size_t api_version); + +bool web_client_interrupt_callback(void *data); + +#include "web_api_v1.h" +#include "web_api_v2.h" + +#endif //NETDATA_WEB_API_H diff --git a/src/web/api/web_api_v1.c b/src/web/api/web_api_v1.c new file mode 100644 index 000000000..386221d61 --- /dev/null +++ b/src/web/api/web_api_v1.c @@ -0,0 +1,1964 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web_api_v1.h" + +char *api_secret; + +static struct { + const char *name; + uint32_t hash; + RRDR_OPTIONS value; +} rrdr_options[] = { + { "nonzero" , 0 , RRDR_OPTION_NONZERO} + , {"flip" , 0 , RRDR_OPTION_REVERSED} + , {"reversed" , 0 , RRDR_OPTION_REVERSED} + , {"reverse" , 0 , RRDR_OPTION_REVERSED} + , {"jsonwrap" , 0 , RRDR_OPTION_JSON_WRAP} + , {"min2max" , 0 , RRDR_OPTION_DIMS_MIN2MAX} // rrdr2value() only + , {"average" , 0 , RRDR_OPTION_DIMS_AVERAGE} // rrdr2value() only + , {"min" , 0 , RRDR_OPTION_DIMS_MIN} // rrdr2value() only + , {"max" , 0 , RRDR_OPTION_DIMS_MAX} // rrdr2value() only + , {"ms" , 0 , RRDR_OPTION_MILLISECONDS} + , {"milliseconds" , 0 , RRDR_OPTION_MILLISECONDS} + , {"absolute" , 0 , RRDR_OPTION_ABSOLUTE} + , {"abs" , 0 , RRDR_OPTION_ABSOLUTE} + , {"absolute_sum" , 0 , RRDR_OPTION_ABSOLUTE} + , {"absolute-sum" , 0 , RRDR_OPTION_ABSOLUTE} + , {"display_absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} + , {"display-absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} + , {"seconds" , 0 , RRDR_OPTION_SECONDS} + , {"null2zero" , 0 , RRDR_OPTION_NULL2ZERO} + , {"objectrows" , 0 , RRDR_OPTION_OBJECTSROWS} + , {"google_json" , 0 , RRDR_OPTION_GOOGLE_JSON} + , {"google-json" , 0 , RRDR_OPTION_GOOGLE_JSON} + , {"percentage" , 0 , RRDR_OPTION_PERCENTAGE} + , {"unaligned" , 0 , RRDR_OPTION_NOT_ALIGNED} + , {"match_ids" , 0 , RRDR_OPTION_MATCH_IDS} + , {"match-ids" , 0 , RRDR_OPTION_MATCH_IDS} + , {"match_names" , 0 , RRDR_OPTION_MATCH_NAMES} + , {"match-names" , 0 , RRDR_OPTION_MATCH_NAMES} + , {"anomaly-bit" , 0 , RRDR_OPTION_ANOMALY_BIT} + , {"selected-tier" , 0 , RRDR_OPTION_SELECTED_TIER} + , {"raw" , 0 , RRDR_OPTION_RETURN_RAW} + , {"jw-anomaly-rates" , 0 , RRDR_OPTION_RETURN_JWAR} + , {"natural-points" , 0 , RRDR_OPTION_NATURAL_POINTS} + , {"virtual-points" , 0 , RRDR_OPTION_VIRTUAL_POINTS} + , {"all-dimensions" , 0 , RRDR_OPTION_ALL_DIMENSIONS} + , {"details" , 0 , RRDR_OPTION_SHOW_DETAILS} + , {"debug" , 0 , RRDR_OPTION_DEBUG} + , {"plan" , 0 , RRDR_OPTION_DEBUG} + , {"minify" , 0 , RRDR_OPTION_MINIFY} + , {"group-by-labels" , 0 , RRDR_OPTION_GROUP_BY_LABELS} + , {"label-quotes" , 0 , RRDR_OPTION_LABEL_QUOTES} + , {NULL , 0 , 0} +}; + +static struct { + const char *name; + uint32_t hash; + CONTEXTS_V2_OPTIONS value; +} contexts_v2_options[] = { + {"minify" , 0 , CONTEXT_V2_OPTION_MINIFY} + , {"debug" , 0 , CONTEXT_V2_OPTION_DEBUG} + , {"config" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_CONFIGURATIONS} + , {"instances" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_INSTANCES} + , {"values" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_VALUES} + , {"summary" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_SUMMARY} + , {NULL , 0 , 0} +}; + +static struct { + const char *name; + uint32_t hash; + CONTEXTS_V2_ALERT_STATUS value; +} contexts_v2_alert_status[] = { + {"uninitialized" , 0 , CONTEXT_V2_ALERT_UNINITIALIZED} + , {"undefined" , 0 , CONTEXT_V2_ALERT_UNDEFINED} + , {"clear" , 0 , CONTEXT_V2_ALERT_CLEAR} + , {"raised" , 0 , CONTEXT_V2_ALERT_RAISED} + , {"active" , 0 , CONTEXT_V2_ALERT_RAISED} + , {"warning" , 0 , CONTEXT_V2_ALERT_WARNING} + , {"critical" , 0 , CONTEXT_V2_ALERT_CRITICAL} + , {NULL , 0 , 0} +}; + +static struct { + const char *name; + uint32_t hash; + DATASOURCE_FORMAT value; +} api_v1_data_formats[] = { + { DATASOURCE_FORMAT_DATATABLE_JSON , 0 , DATASOURCE_DATATABLE_JSON} + , {DATASOURCE_FORMAT_DATATABLE_JSONP, 0 , DATASOURCE_DATATABLE_JSONP} + , {DATASOURCE_FORMAT_JSON , 0 , DATASOURCE_JSON} + , {DATASOURCE_FORMAT_JSON2 , 0 , DATASOURCE_JSON2} + , {DATASOURCE_FORMAT_JSONP , 0 , DATASOURCE_JSONP} + , {DATASOURCE_FORMAT_SSV , 0 , DATASOURCE_SSV} + , {DATASOURCE_FORMAT_CSV , 0 , DATASOURCE_CSV} + , {DATASOURCE_FORMAT_TSV , 0 , DATASOURCE_TSV} + , {"tsv-excel" , 0 , DATASOURCE_TSV} + , {DATASOURCE_FORMAT_HTML , 0 , DATASOURCE_HTML} + , {DATASOURCE_FORMAT_JS_ARRAY , 0 , DATASOURCE_JS_ARRAY} + , {DATASOURCE_FORMAT_SSV_COMMA , 0 , DATASOURCE_SSV_COMMA} + , {DATASOURCE_FORMAT_CSV_JSON_ARRAY , 0 , DATASOURCE_CSV_JSON_ARRAY} + , {DATASOURCE_FORMAT_CSV_MARKDOWN , 0 , DATASOURCE_CSV_MARKDOWN} + + // terminator + , {NULL, 0, 0} +}; + +static struct { + const char *name; + uint32_t hash; + DATASOURCE_FORMAT value; +} api_v1_data_google_formats[] = { + // this is not an error - when Google requests json, it expects javascript + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source#responseformat + {"json", 0, DATASOURCE_DATATABLE_JSONP} + , {"html", 0, DATASOURCE_HTML} + , {"csv", 0, DATASOURCE_CSV} + , {"tsv-excel", 0, DATASOURCE_TSV} + + // terminator + , {NULL, 0, 0} +}; + +void web_client_api_v1_init(void) { + int i; + + for(i = 0; contexts_v2_alert_status[i].name ; i++) + contexts_v2_alert_status[i].hash = simple_hash(contexts_v2_alert_status[i].name); + + for(i = 0; rrdr_options[i].name ; i++) + rrdr_options[i].hash = simple_hash(rrdr_options[i].name); + + for(i = 0; contexts_v2_options[i].name ; i++) + contexts_v2_options[i].hash = simple_hash(contexts_v2_options[i].name); + + for(i = 0; api_v1_data_formats[i].name ; i++) + api_v1_data_formats[i].hash = simple_hash(api_v1_data_formats[i].name); + + for(i = 0; api_v1_data_google_formats[i].name ; i++) + api_v1_data_google_formats[i].hash = simple_hash(api_v1_data_google_formats[i].name); + + time_grouping_init(); + + uuid_t uuid; + + // generate + uuid_generate(uuid); + + // unparse (to string) + char uuid_str[37]; + uuid_unparse_lower(uuid, uuid_str); +} + +char *get_mgmt_api_key(void) { + char filename[FILENAME_MAX + 1]; + snprintfz(filename, FILENAME_MAX, "%s/netdata.api.key", netdata_configured_varlib_dir); + char *api_key_filename=config_get(CONFIG_SECTION_REGISTRY, "netdata management api key file", filename); + static char guid[GUID_LEN + 1] = ""; + + if(likely(guid[0])) + return guid; + + // read it from disk + int fd = open(api_key_filename, O_RDONLY | O_CLOEXEC); + if(fd != -1) { + char buf[GUID_LEN + 1]; + if(read(fd, buf, GUID_LEN) != GUID_LEN) + netdata_log_error("Failed to read management API key from '%s'", api_key_filename); + else { + buf[GUID_LEN] = '\0'; + if(regenerate_guid(buf, guid) == -1) { + netdata_log_error("Failed to validate management API key '%s' from '%s'.", + buf, api_key_filename); + + guid[0] = '\0'; + } + } + close(fd); + } + + // generate a new one? + if(!guid[0]) { + uuid_t uuid; + + uuid_generate_time(uuid); + uuid_unparse_lower(uuid, guid); + guid[GUID_LEN] = '\0'; + + // save it + fd = open(api_key_filename, O_WRONLY|O_CREAT|O_TRUNC | O_CLOEXEC, 444); + if(fd == -1) { + netdata_log_error("Cannot create unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file.", api_key_filename); + goto temp_key; + } + + if(write(fd, guid, GUID_LEN) != GUID_LEN) { + netdata_log_error("Cannot write the unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file with enough space left.", api_key_filename); + close(fd); + goto temp_key; + } + + close(fd); + } + + return guid; + +temp_key: + netdata_log_info("You can still continue to use the alarm management API using the authorization token %s during this Netdata session only.", guid); + return guid; +} + +void web_client_api_v1_management_init(void) { + api_secret = get_mgmt_api_key(); +} + +inline RRDR_OPTIONS rrdr_options_parse_one(const char *o) { + RRDR_OPTIONS ret = 0; + + if(!o || !*o) return ret; + + uint32_t hash = simple_hash(o); + int i; + for(i = 0; rrdr_options[i].name ; i++) { + if (unlikely(hash == rrdr_options[i].hash && !strcmp(o, rrdr_options[i].name))) { + ret |= rrdr_options[i].value; + break; + } + } + + return ret; +} + +inline RRDR_OPTIONS rrdr_options_parse(char *o) { + RRDR_OPTIONS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + ret |= rrdr_options_parse_one(tok); + } + + return ret; +} + +inline CONTEXTS_V2_OPTIONS web_client_api_request_v2_context_options(char *o) { + CONTEXTS_V2_OPTIONS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + + uint32_t hash = simple_hash(tok); + int i; + for(i = 0; contexts_v2_options[i].name ; i++) { + if (unlikely(hash == contexts_v2_options[i].hash && !strcmp(tok, contexts_v2_options[i].name))) { + ret |= contexts_v2_options[i].value; + break; + } + } + } + + return ret; +} + +inline CONTEXTS_V2_ALERT_STATUS web_client_api_request_v2_alert_status(char *o) { + CONTEXTS_V2_ALERT_STATUS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + + uint32_t hash = simple_hash(tok); + int i; + for(i = 0; contexts_v2_alert_status[i].name ; i++) { + if (unlikely(hash == contexts_v2_alert_status[i].hash && !strcmp(tok, contexts_v2_alert_status[i].name))) { + ret |= contexts_v2_alert_status[i].value; + break; + } + } + } + + return ret; +} + +void web_client_api_request_v2_contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_ALERT_STATUS options) { + buffer_json_member_add_array(wb, key); + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + for(int i = 0; contexts_v2_alert_status[i].name ; i++) { + if (unlikely((contexts_v2_alert_status[i].value & options) && !(contexts_v2_alert_status[i].value & used))) { + const char *name = contexts_v2_alert_status[i].name; + used |= contexts_v2_alert_status[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void web_client_api_request_v2_contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_OPTIONS options) { + buffer_json_member_add_array(wb, key); + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + for(int i = 0; contexts_v2_options[i].name ; i++) { + if (unlikely((contexts_v2_options[i].value & options) && !(contexts_v2_options[i].value & used))) { + const char *name = contexts_v2_options[i].name; + used |= contexts_v2_options[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options) { + buffer_json_member_add_array(wb, key); + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options) { + RRDR_OPTIONS used = 0; // to prevent adding duplicates + size_t added = 0; + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + if(added++) buffer_strcat(wb, " "); + buffer_strcat(wb, name); + } + } +} + +void web_client_api_request_v1_data_options_to_string(char *buf, size_t size, RRDR_OPTIONS options) { + char *write = buf; + char *end = &buf[size - 1]; + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + int added = 0; + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + if(added && write < end) + *write++ = ','; + + while(*name && write < end) + *write++ = *name++; + + added++; + } + } + *write = *end = '\0'; +} + +inline uint32_t web_client_api_request_v1_data_format(char *name) { + uint32_t hash = simple_hash(name); + int i; + + for(i = 0; api_v1_data_formats[i].name ; i++) { + if (unlikely(hash == api_v1_data_formats[i].hash && !strcmp(name, api_v1_data_formats[i].name))) { + return api_v1_data_formats[i].value; + } + } + + return DATASOURCE_JSON; +} + +inline uint32_t web_client_api_request_v1_data_google_format(char *name) { + uint32_t hash = simple_hash(name); + int i; + + for(i = 0; api_v1_data_google_formats[i].name ; i++) { + if (unlikely(hash == api_v1_data_google_formats[i].hash && !strcmp(name, api_v1_data_google_formats[i].name))) { + return api_v1_data_google_formats[i].value; + } + } + + return DATASOURCE_JSON; +} + +int web_client_api_request_v1_alarms_select (char *url) { + int all = 0; + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + if(!strcmp(value, "all") || !strcmp(value, "all=true")) all = 1; + else if(!strcmp(value, "active") || !strcmp(value, "active=true")) all = 0; + } + + return all; +} + +inline int web_client_api_request_v1_alarms(RRDHOST *host, struct web_client *w, char *url) { + int all = web_client_api_request_v1_alarms_select(url); + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + health_alarms2json(host, w->response.data, all); + buffer_no_cacheable(w->response.data); + return HTTP_RESP_OK; +} + +inline int web_client_api_request_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url) { + int all = web_client_api_request_v1_alarms_select(url); + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + health_alarms_values2json(host, w->response.data, all); + buffer_no_cacheable(w->response.data); + return HTTP_RESP_OK; +} + +inline int web_client_api_request_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url) { + RRDCALC_STATUS status = RRDCALC_STATUS_RAISED; + BUFFER *contexts = NULL; + + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "["); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 alarm_count query param '%s' with value '%s'", w->id, name, value); + + char* p = value; + if(!strcmp(name, "status")) { + while ((*p = toupper(*p))) p++; + if (!strcmp("CRITICAL", value)) status = RRDCALC_STATUS_CRITICAL; + else if (!strcmp("WARNING", value)) status = RRDCALC_STATUS_WARNING; + else if (!strcmp("UNINITIALIZED", value)) status = RRDCALC_STATUS_UNINITIALIZED; + else if (!strcmp("UNDEFINED", value)) status = RRDCALC_STATUS_UNDEFINED; + else if (!strcmp("REMOVED", value)) status = RRDCALC_STATUS_REMOVED; + else if (!strcmp("CLEAR", value)) status = RRDCALC_STATUS_CLEAR; + } + else if(!strcmp(name, "context") || !strcmp(name, "ctx")) { + if(!contexts) contexts = buffer_create(255, &netdata_buffers_statistics.buffers_api); + buffer_strcat(contexts, "|"); + buffer_strcat(contexts, value); + } + } + + health_aggregate_alarms(host, w->response.data, contexts, status); + + buffer_sprintf(w->response.data, "]\n"); + w->response.data->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(w->response.data); + + buffer_free(contexts); + return 200; +} + +inline int web_client_api_request_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url) { + time_t after = 0; + char *chart = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + if (!strcmp(name, "after")) after = (time_t) strtoul(value, NULL, 0); + else if (!strcmp(name, "chart")) chart = value; + } + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + sql_health_alarm_log2json(host, w->response.data, after, chart); + return HTTP_RESP_OK; +} + +inline int web_client_api_request_single_chart(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)) { + int ret = HTTP_RESP_BAD_REQUEST; + char *chart = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "chart")) chart = value; + //else { + /// buffer_sprintf(w->response.data, "Unknown parameter '%s' in request.", name); + // goto cleanup; + //} + } + + if(!chart || !*chart) { + buffer_sprintf(w->response.data, "No chart id is given at the request."); + goto cleanup; + } + + RRDSET *st = rrdset_find(host, chart); + if(!st) st = rrdset_find_byname(host, chart); + if(!st) { + buffer_strcat(w->response.data, "Chart is not found: "); + buffer_strcat_htmlescape(w->response.data, chart); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + w->response.data->content_type = CT_APPLICATION_JSON; + st->last_accessed_time_s = now_realtime_sec(); + callback(st, w->response.data); + return HTTP_RESP_OK; + + cleanup: + return ret; +} + +static inline int web_client_api_request_variable(RRDHOST *host, struct web_client *w, char *url) { + int ret = HTTP_RESP_BAD_REQUEST; + char *chart = NULL; + char *variable = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "chart")) chart = value; + else if(!strcmp(name, "variable")) variable = value; + } + + if(!chart || !*chart || !variable || !*variable) { + buffer_sprintf(w->response.data, "A chart= and a variable= are required."); + goto cleanup; + } + + RRDSET *st = rrdset_find(host, chart); + if(!st) st = rrdset_find_byname(host, chart); + if(!st) { + buffer_strcat(w->response.data, "Chart is not found: "); + buffer_strcat_htmlescape(w->response.data, chart); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + w->response.data->content_type = CT_APPLICATION_JSON; + st->last_accessed_time_s = now_realtime_sec(); + alert_variable_lookup_trace(host, st, variable, w->response.data); + + return HTTP_RESP_OK; + +cleanup: + return ret; +} + +inline int web_client_api_request_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_single_chart(host, w, url, health_api_v1_chart_variables2json); +} + +static int web_client_api_request_v1_context(RRDHOST *host, struct web_client *w, char *url) { + char *context = NULL; + RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; + time_t after = 0, before = 0; + const char *chart_label_key = NULL, *chart_labels_filter = NULL; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "context") || !strcmp(name, "ctx")) context = value; + else if(!strcmp(name, "after")) after = str2l(value); + else if(!strcmp(name, "before")) before = str2l(value); + else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + } + + if(!context || !*context) { + buffer_sprintf(w->response.data, "No context is given at the request."); + return HTTP_RESP_BAD_REQUEST; + } + + SIMPLE_PATTERN *chart_label_key_pattern = NULL; + SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; + SIMPLE_PATTERN *chart_dimensions_pattern = NULL; + + if(chart_label_key) + chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); + + if(chart_labels_filter) + chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, + true); + + if(dimensions) { + chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", + SIMPLE_PATTERN_EXACT, true); + buffer_free(dimensions); + } + + w->response.data->content_type = CT_APPLICATION_JSON; + int ret = rrdcontext_to_json(host, w->response.data, after, before, options, context, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); + + simple_pattern_free(chart_label_key_pattern); + simple_pattern_free(chart_labels_filter_pattern); + simple_pattern_free(chart_dimensions_pattern); + + return ret; +} + +static int web_client_api_request_v1_contexts(RRDHOST *host, struct web_client *w, char *url) { + RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; + time_t after = 0, before = 0; + const char *chart_label_key = NULL, *chart_labels_filter = NULL; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "after")) after = str2l(value); + else if(!strcmp(name, "before")) before = str2l(value); + else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + } + + SIMPLE_PATTERN *chart_label_key_pattern = NULL; + SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; + SIMPLE_PATTERN *chart_dimensions_pattern = NULL; + + if(chart_label_key) + chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); + + if(chart_labels_filter) + chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, + true); + + if(dimensions) { + chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", + SIMPLE_PATTERN_EXACT, true); + buffer_free(dimensions); + } + + w->response.data->content_type = CT_APPLICATION_JSON; + int ret = rrdcontexts_to_json(host, w->response.data, after, before, options, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); + + simple_pattern_free(chart_label_key_pattern); + simple_pattern_free(chart_labels_filter_pattern); + simple_pattern_free(chart_dimensions_pattern); + + return ret; +} + +inline int web_client_api_request_v1_charts(RRDHOST *host, struct web_client *w, char *url) { + (void)url; + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + charts2json(host, w->response.data); + return HTTP_RESP_OK; +} + +inline int web_client_api_request_v1_chart(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_single_chart(host, w, url, rrd_stats_api_v1_chart); +} + +// returns the HTTP code +static inline int web_client_api_request_v1_data(RRDHOST *host, struct web_client *w, char *url) { + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data with URL '%s'", w->id, url); + + int ret = HTTP_RESP_BAD_REQUEST; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + char *google_version = "0.6", + *google_reqId = "0", + *google_sig = "0", + *google_out = "json", + *responseHandler = NULL, + *outFileName = NULL; + + time_t last_timestamp_in_data = 0, google_timestamp = 0; + + char *chart = NULL; + char *before_str = NULL; + char *after_str = NULL; + char *group_time_str = NULL; + char *points_str = NULL; + char *timeout_str = NULL; + char *context = NULL; + char *chart_label_key = NULL; + char *chart_labels_filter = NULL; + char *group_options = NULL; + size_t tier = 0; + RRDR_TIME_GROUPING group = RRDR_GROUPING_AVERAGE; + DATASOURCE_FORMAT format = DATASOURCE_JSON; + RRDR_OPTIONS options = 0; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data query param '%s' with value '%s'", w->id, name, value); + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "context")) context = value; + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "chart")) chart = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + else if(!strcmp(name, "show_dimensions")) options |= RRDR_OPTION_ALL_DIMENSIONS; + else if(!strcmp(name, "after")) after_str = value; + else if(!strcmp(name, "before")) before_str = value; + else if(!strcmp(name, "points")) points_str = value; + else if(!strcmp(name, "timeout")) timeout_str = value; + else if(!strcmp(name, "gtime")) group_time_str = value; + else if(!strcmp(name, "group_options")) group_options = value; + else if(!strcmp(name, "group")) { + group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + } + else if(!strcmp(name, "format")) { + format = web_client_api_request_v1_data_format(value); + } + else if(!strcmp(name, "options")) { + options |= rrdr_options_parse(value); + } + else if(!strcmp(name, "callback")) { + responseHandler = value; + } + else if(!strcmp(name, "filename")) { + outFileName = value; + } + else if(!strcmp(name, "tqx")) { + // parse Google Visualization API options + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source + char *tqx_name, *tqx_value; + + while(value) { + tqx_value = strsep_skip_consecutive_separators(&value, ";"); + if(!tqx_value || !*tqx_value) continue; + + tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); + if(!tqx_name || !*tqx_name) continue; + if(!tqx_value || !*tqx_value) continue; + + if(!strcmp(tqx_name, "version")) + google_version = tqx_value; + else if(!strcmp(tqx_name, "reqId")) + google_reqId = tqx_value; + else if(!strcmp(tqx_name, "sig")) { + google_sig = tqx_value; + google_timestamp = strtoul(google_sig, NULL, 0); + } + else if(!strcmp(tqx_name, "out")) { + google_out = tqx_value; + format = web_client_api_request_v1_data_google_format(google_out); + } + else if(!strcmp(tqx_name, "responseHandler")) + responseHandler = tqx_value; + else if(!strcmp(tqx_name, "outFileName")) + outFileName = tqx_value; + } + } + else if(!strcmp(name, "tier")) { + tier = str2ul(value); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + } + + // validate the google parameters given + fix_google_param(google_out); + fix_google_param(google_sig); + fix_google_param(google_reqId); + fix_google_param(google_version); + fix_google_param(responseHandler); + fix_google_param(outFileName); + + RRDSET *st = NULL; + ONEWAYALLOC *owa = onewayalloc_create(0); + QUERY_TARGET *qt = NULL; + + if(!is_valid_sp(chart) && !is_valid_sp(context)) { + buffer_sprintf(w->response.data, "No chart or context is given."); + goto cleanup; + } + + if(chart && !context) { + // check if this is a specific chart + st = rrdset_find(host, chart); + if (!st) st = rrdset_find_byname(host, chart); + } + + long long before = (before_str && *before_str)?str2l(before_str):0; + long long after = (after_str && *after_str) ?str2l(after_str):-600; + int points = (points_str && *points_str)?str2i(points_str):0; + int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; + long group_time = (group_time_str && *group_time_str)?str2l(group_time_str):0; + + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .after = after, + .before = before, + .host = host, + .st = st, + .nodes = NULL, + .contexts = context, + .instances = chart, + .dimensions = (dimensions)?buffer_tostring(dimensions):NULL, + .timeout_ms = timeout, + .points = points, + .format = format, + .options = options, + .time_group_method = group, + .time_group_options = group_options, + .resampling_time = group_time, + .tier = tier, + .chart_label_key = chart_label_key, + .labels = chart_labels_filter, + .query_source = QUERY_SOURCE_API_DATA, + .priority = STORAGE_PRIORITY_NORMAL, + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + .transaction = &w->transaction, + }; + qt = query_target_create(&qtr); + + if(!qt || !qt->query.used) { + buffer_sprintf(w->response.data, "No metrics where matched to query."); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + web_client_timeout_checkpoint_set(w, timeout); + if(web_client_timeout_checkpoint_and_check(w, NULL)) { + ret = w->response.code; + goto cleanup; + } + + if(outFileName && *outFileName) { + buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); + netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); + } + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(responseHandler == NULL) + responseHandler = "google.visualization.Query.setResponse"; + + netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", + w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName + ); + + buffer_sprintf( + w->response.data, + "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", + responseHandler, + google_version, + google_reqId, + (int64_t)(st ? st->last_updated.tv_sec : 0)); + } + else if(format == DATASOURCE_JSONP) { + if(responseHandler == NULL) + responseHandler = "callback"; + + buffer_strcat(w->response.data, responseHandler); + buffer_strcat(w->response.data, "("); + } + + ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(google_timestamp < last_timestamp_in_data) + buffer_strcat(w->response.data, "});"); + + else { + // the client already has the latest data + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, + "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", + responseHandler, google_version, google_reqId); + } + } + else if(format == DATASOURCE_JSONP) + buffer_strcat(w->response.data, ");"); + + if(qt->internal.relative) + buffer_no_cacheable(w->response.data); + else + buffer_cacheable(w->response.data); + +cleanup: + query_target_release(qt); + onewayalloc_destroy(owa); + buffer_free(dimensions); + return ret; +} + +// Pings a netdata server: +// /api/v1/registry?action=hello +// +// Access to a netdata registry: +// /api/v1/registry?action=access&machine=${machine_guid}&name=${hostname}&url=${url} +// +// Delete from a netdata registry: +// /api/v1/registry?action=delete&machine=${machine_guid}&name=${hostname}&url=${url}&delete_url=${delete_url} +// +// Search for the URLs of a machine: +// /api/v1/registry?action=search&for=${machine_guid} +// +// Impersonate: +// /api/v1/registry?action=switch&machine=${machine_guid}&name=${hostname}&url=${url}&to=${new_person_guid} +inline int web_client_api_request_v1_registry(RRDHOST *host, struct web_client *w, char *url) { + static uint32_t hash_action = 0, hash_access = 0, hash_hello = 0, hash_delete = 0, hash_search = 0, + hash_switch = 0, hash_machine = 0, hash_url = 0, hash_name = 0, hash_delete_url = 0, hash_for = 0, + hash_to = 0 /*, hash_redirects = 0 */; + + if(unlikely(!hash_action)) { + hash_action = simple_hash("action"); + hash_access = simple_hash("access"); + hash_hello = simple_hash("hello"); + hash_delete = simple_hash("delete"); + hash_search = simple_hash("search"); + hash_switch = simple_hash("switch"); + hash_machine = simple_hash("machine"); + hash_url = simple_hash("url"); + hash_name = simple_hash("name"); + hash_delete_url = simple_hash("delete_url"); + hash_for = simple_hash("for"); + hash_to = simple_hash("to"); +/* + hash_redirects = simple_hash("redirects"); +*/ + } + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry with URL '%s'", w->id, url); + + // TODO + // The browser may send multiple cookies with our id + + char person_guid[UUID_STR_LEN] = ""; + char *cookie = strstr(w->response.data->buffer, NETDATA_REGISTRY_COOKIE_NAME "="); + if(cookie) + strncpyz(person_guid, &cookie[sizeof(NETDATA_REGISTRY_COOKIE_NAME)], UUID_STR_LEN - 1); + else if(!extract_bearer_token_from_request(w, person_guid, sizeof(person_guid))) + person_guid[0] = '\0'; + + char action = '\0'; + char *machine_guid = NULL, + *machine_url = NULL, + *url_name = NULL, + *search_machine_guid = NULL, + *delete_url = NULL, + *to_person_guid = NULL; +/* + int redirects = 0; +*/ + + // Don't cache registry responses + buffer_no_cacheable(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry query param '%s' with value '%s'", w->id, name, value); + + uint32_t hash = simple_hash(name); + + if(hash == hash_action && !strcmp(name, "action")) { + uint32_t vhash = simple_hash(value); + + if(vhash == hash_access && !strcmp(value, "access")) action = 'A'; + else if(vhash == hash_hello && !strcmp(value, "hello")) action = 'H'; + else if(vhash == hash_delete && !strcmp(value, "delete")) action = 'D'; + else if(vhash == hash_search && !strcmp(value, "search")) action = 'S'; + else if(vhash == hash_switch && !strcmp(value, "switch")) action = 'W'; +#ifdef NETDATA_INTERNAL_CHECKS + else netdata_log_error("unknown registry action '%s'", value); +#endif /* NETDATA_INTERNAL_CHECKS */ + } +/* + else if(hash == hash_redirects && !strcmp(name, "redirects")) + redirects = atoi(value); +*/ + else if(hash == hash_machine && !strcmp(name, "machine")) + machine_guid = value; + + else if(hash == hash_url && !strcmp(name, "url")) + machine_url = value; + + else if(action == 'A') { + if(hash == hash_name && !strcmp(name, "name")) + url_name = value; + } + else if(action == 'D') { + if(hash == hash_delete_url && !strcmp(name, "delete_url")) + delete_url = value; + } + else if(action == 'S') { + if(hash == hash_for && !strcmp(name, "for")) + search_machine_guid = value; + } + else if(action == 'W') { + if(hash == hash_to && !strcmp(name, "to")) + to_person_guid = value; + } +#ifdef NETDATA_INTERNAL_CHECKS + else netdata_log_error("unused registry URL parameter '%s' with value '%s'", name, value); +#endif /* NETDATA_INTERNAL_CHECKS */ + } + + bool do_not_track = respect_web_browser_do_not_track_policy && web_client_has_donottrack(w); + + if(unlikely(action == 'H')) { + // HELLO request, dashboard ACL + analytics_log_dashboard(); + if(unlikely(!http_can_access_dashboard(w))) + return web_client_permission_denied_acl(w); + } + else { + // everything else, registry ACL + if(unlikely(!http_can_access_registry(w))) + return web_client_permission_denied_acl(w); + + if(unlikely(do_not_track)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Your web browser is sending 'DNT: 1' (Do Not Track). The registry requires persistent cookies on your browser to work."); + return HTTP_RESP_BAD_REQUEST; + } + } + + buffer_no_cacheable(w->response.data); + + switch(action) { + case 'A': + if(unlikely(!machine_guid || !machine_url || !url_name)) { + netdata_log_error("Invalid registry request - access requires these parameters: machine ('%s'), url ('%s'), name ('%s')", machine_guid ? machine_guid : "UNSET", machine_url ? machine_url : "UNSET", url_name ? url_name : "UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Access request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_access_json(host, w, person_guid, machine_guid, machine_url, url_name, now_realtime_sec()); + + case 'D': + if(unlikely(!machine_guid || !machine_url || !delete_url)) { + netdata_log_error("Invalid registry request - delete requires these parameters: machine ('%s'), url ('%s'), delete_url ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", delete_url?delete_url:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Delete request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_delete_json(host, w, person_guid, machine_guid, machine_url, delete_url, now_realtime_sec()); + + case 'S': + if(unlikely(!search_machine_guid)) { + netdata_log_error("Invalid registry request - search requires these parameters: for ('%s')", search_machine_guid?search_machine_guid:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Search request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_search_json(host, w, person_guid, search_machine_guid); + + case 'W': + if(unlikely(!machine_guid || !machine_url || !to_person_guid)) { + netdata_log_error("Invalid registry request - switching identity requires these parameters: machine ('%s'), url ('%s'), to ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", to_person_guid?to_person_guid:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Switch request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_switch_json(host, w, person_guid, machine_guid, machine_url, to_person_guid, now_realtime_sec()); + + case 'H': + return registry_request_hello_json(host, w, do_not_track); + + default: + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry request - you need to set an action: hello, access, delete, search"); + return HTTP_RESP_BAD_REQUEST; + } +} + +void web_client_api_request_v1_info_summary_alarm_statuses(RRDHOST *host, BUFFER *wb, const char *key) { + buffer_json_member_add_object(wb, key); + + size_t normal = 0, warning = 0, critical = 0; + RRDCALC *rc; + foreach_rrdcalc_in_rrdhost_read(host, rc) { + if(unlikely(!rc->rrdset || !rc->rrdset->last_collected_time.tv_sec)) + continue; + + switch(rc->status) { + case RRDCALC_STATUS_WARNING: + warning++; + break; + case RRDCALC_STATUS_CRITICAL: + critical++; + break; + default: + normal++; + } + } + foreach_rrdcalc_in_rrdhost_done(rc); + + buffer_json_member_add_uint64(wb, "normal", normal); + buffer_json_member_add_uint64(wb, "warning", warning); + buffer_json_member_add_uint64(wb, "critical", critical); + + buffer_json_object_close(wb); +} + +static inline void web_client_api_request_v1_info_mirrored_hosts_status(BUFFER *wb, RRDHOST *host) { + buffer_json_add_array_item_object(wb); + + buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); + buffer_json_member_add_uint64(wb, "hops", host->system_info ? host->system_info->hops : (host == localhost) ? 0 : 1); + buffer_json_member_add_boolean(wb, "reachable", (host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))); + + buffer_json_member_add_string(wb, "guid", host->machine_guid); + buffer_json_member_add_uuid(wb, "node_id", host->node_id); + rrdhost_aclk_state_lock(host); + buffer_json_member_add_string(wb, "claim_id", host->aclk_state.claimed_id); + rrdhost_aclk_state_unlock(host); + + buffer_json_object_close(wb); +} + +static inline void web_client_api_request_v1_info_mirrored_hosts(BUFFER *wb) { + RRDHOST *host; + + rrd_rdlock(); + + buffer_json_member_add_array(wb, "mirrored_hosts"); + rrdhost_foreach_read(host) + buffer_json_add_array_item_string(wb, rrdhost_hostname(host)); + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "mirrored_hosts_status"); + rrdhost_foreach_read(host) { + if ((host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { + web_client_api_request_v1_info_mirrored_hosts_status(wb, host); + } + } + rrdhost_foreach_read(host) { + if ((host != localhost && rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { + web_client_api_request_v1_info_mirrored_hosts_status(wb, host); + } + } + buffer_json_array_close(wb); + + rrd_unlock(); +} + +void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key) { + buffer_json_member_add_object(wb, key); + rrdlabels_to_buffer_json_members(host->rrdlabels, wb); + buffer_json_object_close(wb); +} + +static void host_collectors(RRDHOST *host, BUFFER *wb) { + buffer_json_member_add_array(wb, "collectors"); + + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE); + RRDSET *st; + char name[500]; + + time_t now = now_realtime_sec(); + + rrdset_foreach_read(st, host) { + if (!rrdset_is_available_for_viewers(st)) + continue; + + sprintf(name, "%s:%s", rrdset_plugin_name(st), rrdset_module_name(st)); + + bool old = 0; + bool *set = dictionary_set(dict, name, &old, sizeof(bool)); + if(!*set) { + *set = true; + st->last_accessed_time_s = now; + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "plugin", rrdset_plugin_name(st)); + buffer_json_member_add_string(wb, "module", rrdset_module_name(st)); + buffer_json_object_close(wb); + } + } + rrdset_foreach_done(st); + dictionary_destroy(dict); + + buffer_json_array_close(wb); +} + +extern int aclk_connected; +inline int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb) { + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + + buffer_json_member_add_string(wb, "version", rrdhost_program_version(host)); + buffer_json_member_add_string(wb, "uid", host->machine_guid); + + buffer_json_member_add_uint64(wb, "hosts-available", rrdhost_hosts_available()); + web_client_api_request_v1_info_mirrored_hosts(wb); + + web_client_api_request_v1_info_summary_alarm_statuses(host, wb, "alarms"); + + buffer_json_member_add_string_or_empty(wb, "os_name", host->system_info->host_os_name); + buffer_json_member_add_string_or_empty(wb, "os_id", host->system_info->host_os_id); + buffer_json_member_add_string_or_empty(wb, "os_id_like", host->system_info->host_os_id_like); + buffer_json_member_add_string_or_empty(wb, "os_version", host->system_info->host_os_version); + buffer_json_member_add_string_or_empty(wb, "os_version_id", host->system_info->host_os_version_id); + buffer_json_member_add_string_or_empty(wb, "os_detection", host->system_info->host_os_detection); + buffer_json_member_add_string_or_empty(wb, "cores_total", host->system_info->host_cores); + buffer_json_member_add_string_or_empty(wb, "total_disk_space", host->system_info->host_disk_space); + buffer_json_member_add_string_or_empty(wb, "cpu_freq", host->system_info->host_cpu_freq); + buffer_json_member_add_string_or_empty(wb, "ram_total", host->system_info->host_ram_total); + + buffer_json_member_add_string_or_omit(wb, "container_os_name", host->system_info->container_os_name); + buffer_json_member_add_string_or_omit(wb, "container_os_id", host->system_info->container_os_id); + buffer_json_member_add_string_or_omit(wb, "container_os_id_like", host->system_info->container_os_id_like); + buffer_json_member_add_string_or_omit(wb, "container_os_version", host->system_info->container_os_version); + buffer_json_member_add_string_or_omit(wb, "container_os_version_id", host->system_info->container_os_version_id); + buffer_json_member_add_string_or_omit(wb, "container_os_detection", host->system_info->container_os_detection); + buffer_json_member_add_string_or_omit(wb, "is_k8s_node", host->system_info->is_k8s_node); + + buffer_json_member_add_string_or_empty(wb, "kernel_name", host->system_info->kernel_name); + buffer_json_member_add_string_or_empty(wb, "kernel_version", host->system_info->kernel_version); + buffer_json_member_add_string_or_empty(wb, "architecture", host->system_info->architecture); + buffer_json_member_add_string_or_empty(wb, "virtualization", host->system_info->virtualization); + buffer_json_member_add_string_or_empty(wb, "virt_detection", host->system_info->virt_detection); + buffer_json_member_add_string_or_empty(wb, "container", host->system_info->container); + buffer_json_member_add_string_or_empty(wb, "container_detection", host->system_info->container_detection); + + buffer_json_member_add_string_or_omit(wb, "cloud_provider_type", host->system_info->cloud_provider_type); + buffer_json_member_add_string_or_omit(wb, "cloud_instance_type", host->system_info->cloud_instance_type); + buffer_json_member_add_string_or_omit(wb, "cloud_instance_region", host->system_info->cloud_instance_region); + + host_labels2json(host, wb, "host_labels"); + host_functions2json(host, wb); + host_collectors(host, wb); + + buffer_json_member_add_boolean(wb, "cloud-enabled", netdata_cloud_enabled); + +#ifdef ENABLE_ACLK + buffer_json_member_add_boolean(wb, "cloud-available", true); +#else + buffer_json_member_add_boolean(wb, "cloud-available", false); +#endif + + char *agent_id = get_agent_claimid(); + buffer_json_member_add_boolean(wb, "agent-claimed", agent_id != NULL); + freez(agent_id); + +#ifdef ENABLE_ACLK + buffer_json_member_add_boolean(wb, "aclk-available", aclk_connected); +#else + buffer_json_member_add_boolean(wb, "aclk-available", false); +#endif + + buffer_json_member_add_string(wb, "memory-mode", rrd_memory_mode_name(host->rrd_memory_mode)); +#ifdef ENABLE_DBENGINE + buffer_json_member_add_uint64(wb, "multidb-disk-quota", default_multidb_disk_quota_mb); + buffer_json_member_add_uint64(wb, "page-cache-size", default_rrdeng_page_cache_mb); +#endif // ENABLE_DBENGINE + buffer_json_member_add_boolean(wb, "web-enabled", web_server_mode != WEB_SERVER_MODE_NONE); + buffer_json_member_add_boolean(wb, "stream-enabled", default_rrdpush_enabled); + + buffer_json_member_add_boolean(wb, "stream-compression", + host->sender && host->sender->compressor.initialized); + +#ifdef ENABLE_HTTPS + buffer_json_member_add_boolean(wb, "https-enabled", true); +#else + buffer_json_member_add_boolean(wb, "https-enabled", false); +#endif + + buffer_json_member_add_quoted_string(wb, "buildinfo", analytics_data.netdata_buildinfo); + buffer_json_member_add_quoted_string(wb, "release-channel", analytics_data.netdata_config_release_channel); + buffer_json_member_add_quoted_string(wb, "notification-methods", analytics_data.netdata_notification_methods); + + buffer_json_member_add_boolean(wb, "exporting-enabled", analytics_data.exporting_enabled); + buffer_json_member_add_quoted_string(wb, "exporting-connectors", analytics_data.netdata_exporting_connectors); + + buffer_json_member_add_uint64(wb, "allmetrics-prometheus-used", analytics_data.prometheus_hits); + buffer_json_member_add_uint64(wb, "allmetrics-shell-used", analytics_data.shell_hits); + buffer_json_member_add_uint64(wb, "allmetrics-json-used", analytics_data.json_hits); + buffer_json_member_add_uint64(wb, "dashboard-used", analytics_data.dashboard_hits); + + buffer_json_member_add_uint64(wb, "charts-count", analytics_data.charts_count); + buffer_json_member_add_uint64(wb, "metrics-count", analytics_data.metrics_count); + +#if defined(ENABLE_ML) + buffer_json_member_add_object(wb, "ml-info"); + ml_host_get_info(host, wb); + buffer_json_object_close(wb); +#endif + + buffer_json_finalize(wb); + return 0; +} + +#if defined(ENABLE_ML) +int web_client_api_request_v1_ml_info(RRDHOST *host, struct web_client *w, char *url) { + (void) url; + + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + ml_host_get_detection_info(host, wb); + buffer_json_finalize(wb); + + buffer_no_cacheable(wb); + + return HTTP_RESP_OK; +} +#endif // ENABLE_ML + +inline int web_client_api_request_v1_info(RRDHOST *host, struct web_client *w, char *url) { + (void)url; + if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + web_client_api_request_v1_info_fill_buffer(host, wb); + + buffer_no_cacheable(wb); + return HTTP_RESP_OK; +} + +static int web_client_api_request_v1_aclk_state(RRDHOST *host, struct web_client *w, char *url) { + UNUSED(url); + UNUSED(host); + if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + + char *str = aclk_state_json(); + buffer_strcat(wb, str); + freez(str); + + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + return HTTP_RESP_OK; +} + +int web_client_api_request_v1_metric_correlations(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, default_metric_correlations_method, WEIGHTS_FORMAT_CHARTS, 1); +} + +int web_client_api_request_v1_weights(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_ANOMALY_RATE, WEIGHTS_FORMAT_CONTEXTS, 1); +} + +static void web_client_progress_functions_update(void *data, size_t done, size_t all) { + // handle progress updates from the plugin + struct web_client *w = data; + query_progress_functions_update(&w->transaction, done, all); +} + +int web_client_api_request_v1_function(RRDHOST *host, struct web_client *w, char *url) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + int timeout = 0; + const char *function = NULL; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) + continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) + continue; + + if (!strcmp(name, "function")) + function = value; + + else if (!strcmp(name, "timeout")) + timeout = (int) strtoul(value, NULL, 0); + } + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + + char transaction[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->transaction, transaction); + + CLEAN_BUFFER *source = buffer_create(100, NULL); + web_client_source2buffer(w, source); + + return rrd_function_run(host, wb, timeout, w->access, function, true, transaction, + NULL, NULL, + web_client_progress_functions_update, w, + web_client_interrupt_callback, w, NULL, + buffer_tostring(source)); +} + +int web_client_api_request_v1_functions(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + host_functions2json(host, wb); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} + +void web_client_source2buffer(struct web_client *w, BUFFER *source) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_CLOUD)) + buffer_sprintf(source, "method=NC"); + else if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_BEARER)) + buffer_sprintf(source, "method=api-bearer"); + else + buffer_sprintf(source, "method=api"); + + if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_GOD)) + buffer_strcat(source, ",role=god"); + else + buffer_sprintf(source, ",role=%s", http_id2user_role(w->user_role)); + + buffer_sprintf(source, ",permissions="HTTP_ACCESS_FORMAT, (HTTP_ACCESS_FORMAT_CAST)w->access); + + if(w->auth.client_name[0]) + buffer_sprintf(source, ",user=%s", w->auth.client_name); + + if(!uuid_is_null(w->auth.cloud_account_id)) { + char uuid_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->auth.cloud_account_id, uuid_str); + buffer_sprintf(source, ",account=%s", uuid_str); + } + + if(w->client_ip[0]) + buffer_sprintf(source, ",ip=%s", w->client_ip); + + if(w->forwarded_for) + buffer_sprintf(source, ",forwarded_for=%s", w->forwarded_for); +} + +static int web_client_api_request_v1_config(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { + char *action = "tree"; + char *path = "/"; + char *id = NULL; + char *add_name = NULL; + int timeout = 120; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "action")) + action = value; + else if(!strcmp(name, "path")) + path = value; + else if(!strcmp(name, "id")) + id = value; + else if(!strcmp(name, "name")) + add_name = value; + else if(!strcmp(name, "timeout")) { + timeout = (int)strtol(value, NULL, 10); + if(timeout < 10) + timeout = 10; + } + } + + char transaction[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->transaction, transaction); + + size_t len = strlen(action) + (id ? strlen(id) : 0) + strlen(path) + (add_name ? strlen(add_name) : 0) + 100; + + char cmd[len]; + if(strcmp(action, "tree") == 0) + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " tree '%s' '%s'", path, id?id:""); + else { + DYNCFG_CMDS c = dyncfg_cmds2id(action); + if(!id || !*id || !dyncfg_is_valid_id(id)) { + rrd_call_function_error(w->response.data, "invalid id given", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + if(c == DYNCFG_CMD_NONE) { + rrd_call_function_error(w->response.data, "invalid action given", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + else if(c == DYNCFG_CMD_ADD) { + if(!add_name || !*add_name || !dyncfg_is_valid_id(add_name)) { + rrd_call_function_error(w->response.data, "invalid name given", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s %s", id, dyncfg_id2cmd_one(c), add_name); + } + else + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s", id, dyncfg_id2cmd_one(c)); + } + + CLEAN_BUFFER *source = buffer_create(100, NULL); + web_client_source2buffer(w, source); + + buffer_flush(w->response.data); + int code = rrd_function_run(host, w->response.data, timeout, w->access, cmd, + true, transaction, + NULL, NULL, + web_client_progress_functions_update, w, + web_client_interrupt_callback, w, + w->payload, buffer_tostring(source)); + + return code; +} + +#ifndef ENABLE_DBENGINE +int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url __maybe_unused) { + return HTTP_RESP_NOT_FOUND; +} +#else +static void web_client_api_v1_dbengine_stats_for_tier(BUFFER *wb, size_t tier) { + RRDENG_SIZE_STATS stats = rrdeng_size_statistics(multidb_ctx[tier]); + + buffer_sprintf(wb, + "\n\t\t\"default_granularity_secs\":%zu" + ",\n\t\t\"sizeof_datafile\":%zu" + ",\n\t\t\"sizeof_page_in_cache\":%zu" + ",\n\t\t\"sizeof_point_data\":%zu" + ",\n\t\t\"sizeof_page_data\":%zu" + ",\n\t\t\"pages_per_extent\":%zu" + ",\n\t\t\"datafiles\":%zu" + ",\n\t\t\"extents\":%zu" + ",\n\t\t\"extents_pages\":%zu" + ",\n\t\t\"points\":%zu" + ",\n\t\t\"metrics\":%zu" + ",\n\t\t\"metrics_pages\":%zu" + ",\n\t\t\"extents_compressed_bytes\":%zu" + ",\n\t\t\"pages_uncompressed_bytes\":%zu" + ",\n\t\t\"pages_duration_secs\":%lld" + ",\n\t\t\"single_point_pages\":%zu" + ",\n\t\t\"first_t\":%ld" + ",\n\t\t\"last_t\":%ld" + ",\n\t\t\"database_retention_secs\":%lld" + ",\n\t\t\"average_compression_savings\":%0.2f" + ",\n\t\t\"average_point_duration_secs\":%0.2f" + ",\n\t\t\"average_metric_retention_secs\":%0.2f" + ",\n\t\t\"ephemeral_metrics_per_day_percent\":%0.2f" + ",\n\t\t\"average_page_size_bytes\":%0.2f" + ",\n\t\t\"estimated_concurrently_collected_metrics\":%zu" + ",\n\t\t\"currently_collected_metrics\":%zu" + ",\n\t\t\"disk_space\":%zu" + ",\n\t\t\"max_disk_space\":%zu" + , stats.default_granularity_secs + , stats.sizeof_datafile + , stats.sizeof_page_in_cache + , stats.sizeof_point_data + , stats.sizeof_page_data + , stats.pages_per_extent + , stats.datafiles + , stats.extents + , stats.extents_pages + , stats.points + , stats.metrics + , stats.metrics_pages + , stats.extents_compressed_bytes + , stats.pages_uncompressed_bytes + , (long long)stats.pages_duration_secs + , stats.single_point_pages + , stats.first_time_s + , stats.last_time_s + , (long long)stats.database_retention_secs + , stats.average_compression_savings + , stats.average_point_duration_secs + , stats.average_metric_retention_secs + , stats.ephemeral_metrics_per_day_percent + , stats.average_page_size_bytes + , stats.estimated_concurrently_collected_metrics + , stats.currently_collected_metrics + , stats.disk_space + , stats.max_disk_space + ); +} +int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + + if(!dbengine_enabled) { + buffer_strcat(wb, "dbengine is not enabled"); + return HTTP_RESP_NOT_FOUND; + } + + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + buffer_strcat(wb, "{"); + for(size_t tier = 0; tier < storage_tiers ;tier++) { + buffer_sprintf(wb, "%s\n\t\"tier%zu\": {", tier?",":"", tier); + web_client_api_v1_dbengine_stats_for_tier(wb, tier); + buffer_strcat(wb, "\n\t}"); + } + buffer_strcat(wb, "\n}"); + + return HTTP_RESP_OK; +} +#endif + +#define HLT_MGM "manage/health" +int web_client_api_request_v1_mgmt(RRDHOST *host, struct web_client *w, char *url) { + const char *haystack = buffer_tostring(w->url_path_decoded); + char *needle; + + buffer_flush(w->response.data); + + if ((needle = strstr(haystack, HLT_MGM)) == NULL) { + buffer_strcat(w->response.data, "Invalid management request. Curently only 'health' is supported."); + return HTTP_RESP_NOT_FOUND; + } + needle += strlen(HLT_MGM); + if (*needle != '\0') { + buffer_strcat(w->response.data, "Invalid management request. Currently only 'health' is supported."); + return HTTP_RESP_NOT_FOUND; + } + return web_client_api_request_v1_mgmt_health(host, w, url); +} + +static struct web_api_command api_commands_v1[] = { + // time-series data APIs + { + .api = "data", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_data, + .allow_subpaths = 0 + }, + { + .api = "weights", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_weights, + .allow_subpaths = 0 + }, + { + // deprecated - do not use anymore - use "weights" + .api = "metric_correlations", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_metric_correlations, + .allow_subpaths = 0 + }, + { + // exporting API + .api = "allmetrics", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_allmetrics, + .allow_subpaths = 0 + }, + { + // badges can be fetched with both dashboard and badge ACL + .api = "badge.svg", + .hash = 0, + .acl = HTTP_ACL_BADGES, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_badge, + .allow_subpaths = 0 + }, + + // alerts APIs + { + .api = "alarms", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_alarms, + .allow_subpaths = 0 + }, + { + .api = "alarms_values", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_alarms_values, + .allow_subpaths = 0 + }, + { + .api = "alarm_log", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_alarm_log, + .allow_subpaths = 0 + }, + { + .api = "alarm_variables", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_alarm_variables, + .allow_subpaths = 0 + }, + { + .api = "variable", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_variable, + .allow_subpaths = 0 + }, + { + .api = "alarm_count", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_alarm_count, + .allow_subpaths = 0 + }, + + // functions APIs - they check permissions per function call + { + .api = "function", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_function, + .allow_subpaths = 0 + }, + { + .api = "functions", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_functions, + .allow_subpaths = 0 + }, + + // time-series metadata APIs + { + .api = "chart", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_chart, + .allow_subpaths = 0 + }, + { + .api = "charts", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_charts, + .allow_subpaths = 0 + }, + { + .api = "context", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_context, + .allow_subpaths = 0 + }, + { + .api = "contexts", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_contexts, + .allow_subpaths = 0 + }, + + // registry APIs + { + // registry checks the ACL by itself, so we allow everything + .api = "registry", + .hash = 0, + .acl = HTTP_ACL_NONE, // it manages acl by itself + .access = HTTP_ACCESS_NONE, // it manages access by itself + .callback = web_client_api_request_v1_registry, + .allow_subpaths = 0 + }, + + // agent information APIs + { + .api = "info", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_info, + .allow_subpaths = 0 + }, + { + .api = "aclk", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_aclk_state, + .allow_subpaths = 0 + }, + { + // deprecated - use /api/v2/info + .api = "dbengine_stats", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_dbengine_stats, + .allow_subpaths = 0 + }, + + // dyncfg APIs + { + .api = "config", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_config, + .allow_subpaths = 0 + }, + +#if defined(ENABLE_ML) + { + .api = "ml_info", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v1_ml_info, + .allow_subpaths = 0 + }, +#endif + + { + // deprecated + .api = "manage", + .hash = 0, + .acl = HTTP_ACL_MANAGEMENT, + .access = HTTP_ACCESS_NONE, // it manages access by itself + .callback = web_client_api_request_v1_mgmt, + .allow_subpaths = 1 + }, + + { + // terminator - keep this last on this list + .api = NULL, + .hash = 0, + .acl = HTTP_ACL_NONE, + .access = HTTP_ACCESS_NONE, + .callback = NULL, + .allow_subpaths = 0 + }, +}; + +inline int web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url_path_endpoint) { + static int initialized = 0; + + if(unlikely(initialized == 0)) { + initialized = 1; + + for(int i = 0; api_commands_v1[i].api ; i++) + api_commands_v1[i].hash = simple_hash(api_commands_v1[i].api); + } + + return web_client_api_request_vX(host, w, url_path_endpoint, api_commands_v1); +} diff --git a/src/web/api/web_api_v1.h b/src/web/api/web_api_v1.h new file mode 100644 index 000000000..cf0efbd13 --- /dev/null +++ b/src/web/api/web_api_v1.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_API_V1_H +#define NETDATA_WEB_API_V1_H 1 + +#include "web_api.h" + +struct web_client; + +CONTEXTS_V2_OPTIONS web_client_api_request_v2_context_options(char *o); +CONTEXTS_V2_ALERT_STATUS web_client_api_request_v2_alert_status(char *o); +void web_client_api_request_v2_contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_OPTIONS options); +void web_client_api_request_v2_contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_ALERT_STATUS options); + +RRDR_OPTIONS rrdr_options_parse(char *o); +RRDR_OPTIONS rrdr_options_parse_one(const char *o); + +void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options); +void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options); +void web_client_api_request_v1_data_options_to_string(char *buf, size_t size, RRDR_OPTIONS options); + +uint32_t web_client_api_request_v1_data_format(char *name); +uint32_t web_client_api_request_v1_data_google_format(char *name); + +int web_client_api_request_v1_alarms(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_single_chart(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)); +int web_client_api_request_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_charts(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_chart(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_registry(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1_info(RRDHOST *host, struct web_client *w, char *url); +int web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url_path_endpoint); +int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb); + +void web_client_api_v1_init(void); +void web_client_api_v1_management_init(void); + +void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key); +void web_client_api_request_v1_info_summary_alarm_statuses(RRDHOST *host, BUFFER *wb, const char *key); + +void web_client_source2buffer(struct web_client *w, BUFFER *source); + +extern char *api_secret; + +#endif //NETDATA_WEB_API_V1_H diff --git a/src/web/api/web_api_v2.c b/src/web/api/web_api_v2.c new file mode 100644 index 000000000..4f24e8c68 --- /dev/null +++ b/src/web/api/web_api_v2.c @@ -0,0 +1,770 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web_api_v2.h" +#include "../rtc/webrtc.h" + +static bool verify_agent_uuids(const char *machine_guid, const char *node_id, const char *claim_id) { + if(!machine_guid || !node_id || !claim_id) + return false; + + if(strcmp(machine_guid, localhost->machine_guid) != 0) + return false; + + char *agent_claim_id = get_agent_claimid(); + + bool not_verified = (!agent_claim_id || strcmp(claim_id, agent_claim_id) != 0); + freez(agent_claim_id); + + if(not_verified || !localhost->node_id) + return false; + + char buf[UUID_STR_LEN]; + uuid_unparse_lower(*localhost->node_id, buf); + + if(strcmp(node_id, buf) != 0) + return false; + + return true; +} + +int api_v2_bearer_protection(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url) { + char *machine_guid = NULL; + char *claim_id = NULL; + char *node_id = NULL; + bool protection = netdata_is_protected_by_bearer; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + if(!strcmp(name, "bearer_protection")) { + if(!strcmp(value, "on") || !strcmp(value, "true") || !strcmp(value, "yes")) + protection = true; + else + protection = false; + } + else if(!strcmp(name, "machine_guid")) + machine_guid = value; + else if(!strcmp(name, "claim_id")) + claim_id = value; + else if(!strcmp(name, "node_id")) + node_id = value; + } + + if(!verify_agent_uuids(machine_guid, node_id, claim_id)) { + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); + return HTTP_RESP_BAD_REQUEST; + } + + netdata_is_protected_by_bearer = protection; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} + +int api_v2_bearer_token(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url __maybe_unused) { + char *machine_guid = NULL; + char *claim_id = NULL; + char *node_id = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + if(!strcmp(name, "machine_guid")) + machine_guid = value; + else if(!strcmp(name, "claim_id")) + claim_id = value; + else if(!strcmp(name, "node_id")) + node_id = value; + } + + if(!verify_agent_uuids(machine_guid, node_id, claim_id)) { + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); + return HTTP_RESP_BAD_REQUEST; + } + + uuid_t uuid; + time_t expires_s = bearer_create_token(&uuid, w); + + BUFFER *wb = w->response.data; + buffer_flush(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_string(wb, "mg", localhost->machine_guid); + buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); + buffer_json_member_add_uuid(wb, "token", &uuid); + buffer_json_member_add_time_t(wb, "expiration", expires_s); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} + +static int web_client_api_request_v2_contexts_internal(RRDHOST *host __maybe_unused, struct web_client *w, char *url, CONTEXTS_V2_MODE mode) { + struct api_v2_contexts_request req = { 0 }; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "scope_nodes")) + req.scope_nodes = value; + else if(!strcmp(name, "nodes")) + req.nodes = value; + else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "scope_contexts")) + req.scope_contexts = value; + else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "contexts")) + req.contexts = value; + else if((mode & CONTEXTS_V2_SEARCH) && !strcmp(name, "q")) + req.q = value; + else if(!strcmp(name, "options")) + req.options = web_client_api_request_v2_context_options(value); + else if(!strcmp(name, "after")) + req.after = str2l(value); + else if(!strcmp(name, "before")) + req.before = str2l(value); + else if(!strcmp(name, "timeout")) + req.timeout_ms = str2l(value); + else if(mode & (CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) { + if (!strcmp(name, "alert")) + req.alerts.alert = value; + else if (!strcmp(name, "transition")) + req.alerts.transition = value; + else if(mode & CONTEXTS_V2_ALERTS) { + if (!strcmp(name, "status")) + req.alerts.status = web_client_api_request_v2_alert_status(value); + } + else if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { + if (!strcmp(name, "last")) + req.alerts.last = strtoul(value, NULL, 0); + else if(!strcmp(name, "context")) + req.contexts = value; + else if (!strcmp(name, "anchor_gi")) { + req.alerts.global_id_anchor = str2ull(value, NULL); + } + else { + for(int i = 0; i < ATF_TOTAL_ENTRIES ;i++) { + if(!strcmp(name, alert_transition_facets[i].query_param)) + req.alerts.facets[i] = value; + } + } + } + } + } + + if ((mode & CONTEXTS_V2_ALERT_TRANSITIONS) && !req.alerts.last) + req.alerts.last = 1; + + buffer_flush(w->response.data); + buffer_no_cacheable(w->response.data); + return rrdcontext_to_json_v2(w->response.data, &req, mode); +} + +static int web_client_api_request_v2_alert_transitions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERT_TRANSITIONS | CONTEXTS_V2_NODES); +} + +static int web_client_api_request_v2_alerts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERTS | CONTEXTS_V2_NODES); +} + +static int web_client_api_request_v2_functions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} + +static int web_client_api_request_v2_versions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_VERSIONS); +} + +static int web_client_api_request_v2_q(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_SEARCH | CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} + +static int web_client_api_request_v2_contexts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} + +static int web_client_api_request_v2_nodes(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_NODES | CONTEXTS_V2_NODES_INFO); +} + +static int web_client_api_request_v2_info(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_AGENTS | CONTEXTS_V2_AGENTS_INFO); +} + +static int web_client_api_request_v2_node_instances(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_NODES | CONTEXTS_V2_NODE_INSTANCES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_AGENTS_INFO | CONTEXTS_V2_VERSIONS); +} + +static int web_client_api_request_v2_weights(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_VALUE, WEIGHTS_FORMAT_MULTINODE, 2); +} + +static int web_client_api_request_v2_claim(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_claim(w, url); +} + +static int web_client_api_request_v2_alert_config(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + const char *config = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "config")) + config = value; + } + + buffer_flush(w->response.data); + + if(!config) { + w->response.data->content_type = CT_TEXT_PLAIN; + buffer_strcat(w->response.data, "A config hash ID is required. Add ?config=UUID query param"); + return HTTP_RESP_BAD_REQUEST; + } + + return contexts_v2_alert_config_to_json(w, config); +} + + +#define GROUP_BY_KEY_MAX_LENGTH 30 +static struct { + char group_by[GROUP_BY_KEY_MAX_LENGTH + 1]; + char aggregation[GROUP_BY_KEY_MAX_LENGTH + 1]; + char group_by_label[GROUP_BY_KEY_MAX_LENGTH + 1]; +} group_by_keys[MAX_QUERY_GROUP_BY_PASSES]; + +__attribute__((constructor)) void initialize_group_by_keys(void) { + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + snprintfz(group_by_keys[g].group_by, GROUP_BY_KEY_MAX_LENGTH, "group_by[%zu]", g); + snprintfz(group_by_keys[g].aggregation, GROUP_BY_KEY_MAX_LENGTH, "aggregation[%zu]", g); + snprintfz(group_by_keys[g].group_by_label, GROUP_BY_KEY_MAX_LENGTH, "group_by_label[%zu]", g); + } +} + +static int web_client_api_request_v2_data(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + usec_t received_ut = now_monotonic_usec(); + + int ret = HTTP_RESP_BAD_REQUEST; + + buffer_flush(w->response.data); + + char *google_version = "0.6", + *google_reqId = "0", + *google_sig = "0", + *google_out = "json", + *responseHandler = NULL, + *outFileName = NULL; + + time_t last_timestamp_in_data = 0, google_timestamp = 0; + + char *scope_nodes = NULL; + char *scope_contexts = NULL; + char *nodes = NULL; + char *contexts = NULL; + char *instances = NULL; + char *dimensions = NULL; + char *before_str = NULL; + char *after_str = NULL; + char *resampling_time_str = NULL; + char *points_str = NULL; + char *timeout_str = NULL; + char *labels = NULL; + char *alerts = NULL; + char *time_group_options = NULL; + char *tier_str = NULL; + size_t tier = 0; + RRDR_TIME_GROUPING time_group = RRDR_GROUPING_AVERAGE; + DATASOURCE_FORMAT format = DATASOURCE_JSON2; + RRDR_OPTIONS options = RRDR_OPTION_VIRTUAL_POINTS | RRDR_OPTION_JSON_WRAP | RRDR_OPTION_RETURN_JWAR; + + struct group_by_pass group_by[MAX_QUERY_GROUP_BY_PASSES] = { + { + .group_by = RRDR_GROUP_BY_DIMENSION, + .group_by_label = NULL, + .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, + }, + }; + + size_t group_by_idx = 0, group_by_label_idx = 0, aggregation_idx = 0; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "scope_nodes")) scope_nodes = value; + else if(!strcmp(name, "scope_contexts")) scope_contexts = value; + else if(!strcmp(name, "nodes")) nodes = value; + else if(!strcmp(name, "contexts")) contexts = value; + else if(!strcmp(name, "instances")) instances = value; + else if(!strcmp(name, "dimensions")) dimensions = value; + else if(!strcmp(name, "labels")) labels = value; + else if(!strcmp(name, "alerts")) alerts = value; + else if(!strcmp(name, "after")) after_str = value; + else if(!strcmp(name, "before")) before_str = value; + else if(!strcmp(name, "points")) points_str = value; + else if(!strcmp(name, "timeout")) timeout_str = value; + else if(!strcmp(name, "group_by")) { + group_by[group_by_idx++].group_by = group_by_parse(value); + if(group_by_idx >= MAX_QUERY_GROUP_BY_PASSES) + group_by_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "group_by_label")) { + group_by[group_by_label_idx++].group_by_label = value; + if(group_by_label_idx >= MAX_QUERY_GROUP_BY_PASSES) + group_by_label_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "aggregation")) { + group_by[aggregation_idx++].aggregation = group_by_aggregate_function_parse(value); + if(aggregation_idx >= MAX_QUERY_GROUP_BY_PASSES) + aggregation_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "format")) format = web_client_api_request_v1_data_format(value); + else if(!strcmp(name, "options")) options |= rrdr_options_parse(value); + else if(!strcmp(name, "time_group")) time_group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + else if(!strcmp(name, "time_group_options")) time_group_options = value; + else if(!strcmp(name, "time_resampling")) resampling_time_str = value; + else if(!strcmp(name, "tier")) tier_str = value; + else if(!strcmp(name, "callback")) responseHandler = value; + else if(!strcmp(name, "filename")) outFileName = value; + else if(!strcmp(name, "tqx")) { + // parse Google Visualization API options + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source + char *tqx_name, *tqx_value; + + while(value) { + tqx_value = strsep_skip_consecutive_separators(&value, ";"); + if(!tqx_value || !*tqx_value) continue; + + tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); + if(!tqx_name || !*tqx_name) continue; + if(!tqx_value || !*tqx_value) continue; + + if(!strcmp(tqx_name, "version")) + google_version = tqx_value; + else if(!strcmp(tqx_name, "reqId")) + google_reqId = tqx_value; + else if(!strcmp(tqx_name, "sig")) { + google_sig = tqx_value; + google_timestamp = strtoul(google_sig, NULL, 0); + } + else if(!strcmp(tqx_name, "out")) { + google_out = tqx_value; + format = web_client_api_request_v1_data_google_format(google_out); + } + else if(!strcmp(tqx_name, "responseHandler")) + responseHandler = tqx_value; + else if(!strcmp(tqx_name, "outFileName")) + outFileName = tqx_value; + } + } + else { + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if(!strcmp(name, group_by_keys[g].group_by)) + group_by[g].group_by = group_by_parse(value); + else if(!strcmp(name, group_by_keys[g].group_by_label)) + group_by[g].group_by_label = value; + else if(!strcmp(name, group_by_keys[g].aggregation)) + group_by[g].aggregation = group_by_aggregate_function_parse(value); + } + } + } + + // validate the google parameters given + fix_google_param(google_out); + fix_google_param(google_sig); + fix_google_param(google_reqId); + fix_google_param(google_version); + fix_google_param(responseHandler); + fix_google_param(outFileName); + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if (group_by[g].group_by_label && *group_by[g].group_by_label) + group_by[g].group_by |= RRDR_GROUP_BY_LABEL; + } + + if(group_by[0].group_by == RRDR_GROUP_BY_NONE) + group_by[0].group_by = RRDR_GROUP_BY_DIMENSION; + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if ((group_by[g].group_by & ~(RRDR_GROUP_BY_DIMENSION)) || (options & RRDR_OPTION_PERCENTAGE)) { + options |= RRDR_OPTION_ABSOLUTE; + break; + } + } + + if(options & RRDR_OPTION_DEBUG) + options &= ~RRDR_OPTION_MINIFY; + + if(tier_str && *tier_str) { + tier = str2ul(tier_str); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + + time_t before = (before_str && *before_str)?str2l(before_str):0; + time_t after = (after_str && *after_str) ?str2l(after_str):-600; + size_t points = (points_str && *points_str)?str2u(points_str):0; + int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; + time_t resampling_time = (resampling_time_str && *resampling_time_str) ? str2l(resampling_time_str) : 0; + + QUERY_TARGET_REQUEST qtr = { + .version = 2, + .scope_nodes = scope_nodes, + .scope_contexts = scope_contexts, + .after = after, + .before = before, + .host = NULL, + .st = NULL, + .nodes = nodes, + .contexts = contexts, + .instances = instances, + .dimensions = dimensions, + .alerts = alerts, + .timeout_ms = timeout, + .points = points, + .format = format, + .options = options, + .time_group_method = time_group, + .time_group_options = time_group_options, + .resampling_time = resampling_time, + .tier = tier, + .chart_label_key = NULL, + .labels = labels, + .query_source = QUERY_SOURCE_API_DATA, + .priority = STORAGE_PRIORITY_NORMAL, + .received_ut = received_ut, + + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + + .transaction = &w->transaction, + }; + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) + qtr.group_by[g] = group_by[g]; + + QUERY_TARGET *qt = query_target_create(&qtr); + ONEWAYALLOC *owa = NULL; + + if(!qt) { + buffer_sprintf(w->response.data, "Failed to prepare the query."); + ret = HTTP_RESP_INTERNAL_SERVER_ERROR; + goto cleanup; + } + + web_client_timeout_checkpoint_set(w, timeout); + if(web_client_timeout_checkpoint_and_check(w, NULL)) { + ret = w->response.code; + goto cleanup; + } + + if(outFileName && *outFileName) { + buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); + netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); + } + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(responseHandler == NULL) + responseHandler = "google.visualization.Query.setResponse"; + + netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", + w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName + ); + + buffer_sprintf( + w->response.data, + "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", + responseHandler, + google_version, + google_reqId, + (int64_t)now_realtime_sec()); + } + else if(format == DATASOURCE_JSONP) { + if(responseHandler == NULL) + responseHandler = "callback"; + + buffer_strcat(w->response.data, responseHandler); + buffer_strcat(w->response.data, "("); + } + + owa = onewayalloc_create(0); + ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(google_timestamp < last_timestamp_in_data) + buffer_strcat(w->response.data, "});"); + + else { + // the client already has the latest data + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, + "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", + responseHandler, google_version, google_reqId); + } + } + else if(format == DATASOURCE_JSONP) + buffer_strcat(w->response.data, ");"); + + if(qt->internal.relative) + buffer_no_cacheable(w->response.data); + else + buffer_cacheable(w->response.data); + +cleanup: + query_target_release(qt); + onewayalloc_destroy(owa); + return ret; +} + +static int web_client_api_request_v2_webrtc(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { + return webrtc_new_connection(buffer_tostring(w->payload), w->response.data); +} + +static int web_client_api_request_v2_progress(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + char *transaction = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "transaction")) transaction = value; + } + + uuid_t tr; + uuid_parse_flexi(transaction, tr); + + rrd_function_call_progresser(&tr); + + return web_api_v2_report_progress(&tr, w->response.data); +} + +static struct web_api_command api_commands_v2[] = { + // time-series multi-node multi-instance data APIs + { + .api = "data", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_data, + .allow_subpaths = 0 + }, + { + .api = "weights", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_weights, + .allow_subpaths = 0 + }, + + // time-series multi-node multi-instance metadata APIs + { + .api = "contexts", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_contexts, + .allow_subpaths = 0 + }, + { + // full text search + .api = "q", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_q, + .allow_subpaths = 0 + }, + + // multi-node multi-instance alerts APIs + { + .api = "alerts", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_alerts, + .allow_subpaths = 0 + }, + { + .api = "alert_transitions", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_alert_transitions, + .allow_subpaths = 0 + }, + { + .api = "alert_config", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_alert_config, + .allow_subpaths = 0 + }, + + // agent information APIs + { + .api = "info", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_info, + .allow_subpaths = 0 + }, + { + .api = "nodes", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_nodes, + .allow_subpaths = 0 + }, + { + .api = "node_instances", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_node_instances, + .allow_subpaths = 0 + }, + { + .api = "versions", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_versions, + .allow_subpaths = 0 + }, + { + .api = "progress", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_progress, + .allow_subpaths = 0 + }, + + // functions APIs + { + .api = "functions", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_functions, + .allow_subpaths = 0 + }, + + // WebRTC APIs + { + .api = "rtc_offer", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, + .callback = web_client_api_request_v2_webrtc, + .allow_subpaths = 0 + }, + + // management APIs + { + .api = "claim", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_NONE, + .callback = web_client_api_request_v2_claim, + .allow_subpaths = 0 + }, + { + .api = "bearer_protection", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_VIEW_AGENT_CONFIG | HTTP_ACCESS_EDIT_AGENT_CONFIG, + .callback = api_v2_bearer_protection, + .allow_subpaths = 0 + }, + { + .api = "bearer_get_token", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, + .callback = api_v2_bearer_token, + .allow_subpaths = 0 + }, + + // Netdata branding APIs + { + .api = "ilove.svg", + .hash = 0, + .acl = HTTP_ACL_DASHBOARD, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = web_client_api_request_v2_ilove, + .allow_subpaths = 0 + }, + + { + // terminator + .api = NULL, + .hash = 0, + .acl = HTTP_ACL_NONE, + .access = HTTP_ACCESS_NONE, + .callback = NULL, + .allow_subpaths = 0 + }, +}; + +inline int web_client_api_request_v2(RRDHOST *host, struct web_client *w, char *url_path_endpoint) { + static int initialized = 0; + + if(unlikely(initialized == 0)) { + initialized = 1; + + for(int i = 0; api_commands_v2[i].api ; i++) + api_commands_v2[i].hash = simple_hash(api_commands_v2[i].api); + } + + return web_client_api_request_vX(host, w, url_path_endpoint, api_commands_v2); +} diff --git a/src/web/api/web_api_v2.h b/src/web/api/web_api_v2.h new file mode 100644 index 000000000..4a1893bd8 --- /dev/null +++ b/src/web/api/web_api_v2.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_API_V2_H +#define NETDATA_WEB_API_V2_H 1 + +#include "web_api.h" + +struct web_client; + +int web_client_api_request_v2(RRDHOST *host, struct web_client *w, char *url_path_endpoint); + +#endif //NETDATA_WEB_API_V2_H |