-
Notifications
You must be signed in to change notification settings - Fork 2
Remote Data Integration (Time Series Databases and External APIs)
InfluxDB and Prometheus time-series databases (TSDBs) are integrated as data providers in Palindrome.js. Users can create custom remote data use cases based on three types of data providers:
- InfluxDB
- Prometheus
- External API (apiEndpoint)
Metrics from these different data providers are displayed in 3D in Palindrome.js. The remote data integration architecture is illustrated in the following figure.
This architecture demonstrates that TSDB querying can be performed either through the frontend or the backend (via Node.js). It comprises three main modules:
- Palindrome.js: The main frontend code.
-
TSDB Service: Located under
services/tsdbService
in the backend. - Time Series Databases (TSDBs): Currently supported databases are Prometheus and InfluxDB v2 Flux.
Palindrome.js includes use cases that require metrics data coming from InfluxDB
, Prometheus
, Node Exporter
, and Telegraf
. To execute the predefined remote data use cases, run the following command to launch the TSDBs with the default configuration:
docker compose -f ./services/tsdbService/tsdbSetup/docker-compose.yml up -d
Additionally, you can configure custom remote data use cases by defining your own data structure, as described here.
Palindrome.js includes the following default remote data use cases:
Use Case Name | Data Structure Location |
---|---|
Remote Data | data-examples/remote_data.js |
Heavy Remote Data | data-examples/heavy_remote_data.js |
api.open-meteo.com | data-examples/oth_api_open_meteo_com.js |
Local Live Monitoring | data-examples/oth_localLiveMonitoring.js |
All these use cases include the dataProviders
field in their data structure. By default, Prometheus' URL is http://localhost:9090
, and InfluxDB v2's URL is http://localhost:8086
. To configure them with your own data providers, update the dataProviders
settings accordingly.
For a remote data use case, a specific data structure is required. The data structure is a JSON object containing four main parts:
- Metrics: An array of JSON objects defining all metrics.
- Layers: An array of JSON objects defining layers and specifying the metrics used in each layer.
- Data Providers: An array of JSON objects defining your time-series databases or external APIs.
- Options: A JSON object containing additional options, such as fetch pace, whether to use the backend for querying data, etc.
{
"metrics": [],
"layers": [],
"dataProviders": [],
"options": {}
}
A data provider definition can contain up to six fields, as explained in the table below:
Field | Used for | Type | Description |
---|---|---|---|
id |
All data providers | String | The ID of the data provider |
type |
All data providers | influxdb | prometheus | apiEndpoint | The type of the data provider |
url |
All data providers | String | The URL of the data provider |
token |
InfluxDB | String | InfluxDB token |
org |
InfluxDB | String | InfluxDB organization |
remoteDataFetchPace |
All data providers | Integer | Fetch pace (in ms) for all metrics using this data provider |
Example of data providers defintions:
dataProviders: [
{
id: 'influxdb-1',
type: 'influxdb',
url: 'http://localhost:8086',
token: 'token',
org: 'smile_rnd',
remoteDataFetchPace: 1_000
},
{
id: 'prometheus-1',
type: 'prometheus',
url: 'http://localhost:9090',
remoteDataFetchPace: 5_000
},
{
id: 'api-1',
type: 'apiEndpoint',
url: 'http://example-api.com',
remoteDataFetchPace: 20_000
}
]
A metric definition should contain the following fields:
Field | Type | Description |
---|---|---|
id | String | A unique identifier for the metric. |
label | String | JSON |
The metric label to display. String label can be used, but if you're using an external API, and you want to get the label from the API. You can use a JSON object with jmesPath field that contains the JMESpath of the label in the API response. For example:
|
value | JSON |
For external API metrics only, specifies a jmesPath to extract the metric value. For example: value: {
jmesPath: 'metrics[2].value'
} |
query | String |
For TSDB metrics only, specifies the Flux or PromQL query. If this metric is gathered from an external API please use Example: query: "promhttp_metric_handler_requests_total{code='200'}" |
unit | String | JSON |
The unit of the metric. It should be a string for the TSDB metrics. It can be a string or a JSON with a jmesPath field for the external API metrics. For example: unit: {
jmesPath: 'metrics[1].unit'
} |
ranges |
Array <int> | Array of <string> |
The min, med, max values of the metrics. It should be an array of integers for TSDB metrics. It can be an array of JMESpath strings for external APIs metrics. For example: ranges: ['metrics.system.cpu.usage.perCore[1].value', 80, 100] |
remoteDataFetchPace | Integer | Fetch pace (in ms) for all metrics using this data provider |
dataProviderId | String | The ID of the data provider used. |
Example of a two metrics defintion:
metrics: [
{
label: 'HTTP Requests (success)',
unit: ' ',
id: 'http200',
query: "promhttp_metric_handler_requests_total{code='200'}",
ranges: [0, 1_000_000, 400_000_000],
dataProviderId: "prometheus-1",
remoteDataFetchPace: 8000
},
{
id: 'cpu-per-core-id',
label: 'Core 0',
value: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].value'
},
unit: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].units'
},
ranges: ['metrics.system.cpu.usage.perCore[1].value', 80, 100],
dataProviderId: 'api-1',
remoteDataFetchPace: 3000
}
]
A layer definition shoud contain 3 fields as explained in the table below:
Field | Type | Description |
---|---|---|
name |
String | The name of the layer |
metrics |
Array<String | JSON> | IDs of defined metrics or new metric definitions to include in the layer. |
options |
JSON | Palindrome.js layer options. Refer to the layers' behavior documentation. |
Example of one layer defintion:
layers : [
{
name: "Jmes-path-layer",
metrics: [
'request-count-id',
'memory-usage-id',
{
id: 'cpu-per-core-id',
label: 'Core 0',
value: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].value'
},
unit: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].units'
},
ranges: ['metrics.system.cpu.usage.perCore[1].value', 80, 100],
dataProviderId: 'api-1',
remoteDataFetchPace: 3000
}
],
options: {
"mainColorStatic": "#319b31",
"layerColorLow": "#ffffff",
"layerColorMed": "#f3c60a",
"layerColorHigh": "#0096FF",
"sphereColorLow": "#ffffff",
"sphereColorMed": "#f3c60a",
"sphereColorHigh": "#0096FF",
}
}
]
Options is JSON object that can contain 6 fields detailed in the table below:
Field | Type | Description |
---|---|---|
liveData |
Boolean | Fetch and update data every remoteDataFetchPace ms if set to true. |
useBackend |
Boolean | Fetch TSDB data via the backend if set to true. |
remoteDataFetchPace |
Integer | The global data fetch pace in milliseconds. |
benchmarkDataUpdate |
Boolean | Enables benchmarking of frontend vs backend query times. Results logged in the console. |
benchmarkDuration |
Integer | Duration of the benchmarkDataUpdate test (in ms). |
benchmarkInitial |
Boolean | Measures initial render time of remote data use cases. Results logged in the console. |
Fetch pace can be defined at multiple levels:
- Metric level: Overrides all other fetch pace definitions.
- Data provider level: Used if fetch pace is not defined at the metric level.
- Global options level: Default fetch pace if not defined at the metric or data provider levels.
export function remoteSchema() {
return {
metrics: [
{
id: 'request-count-id',
label: {
jmesPath: 'metrics[2].name'
},
value: {
jmesPath: 'metrics[2].value'
},
unit: {
jmesPath: 'metrics[2].unit'
},
ranges: [0, 50, 150],
dataProviderId: 'api-2',
},
{
id: 'memory-usage-id',
label: {
jmesPath: 'metrics[1].name'
},
value: {
jmesPath: 'metrics[1].value'
},
unit: {
jmesPath: 'metrics[1].unit'
},
ranges: [0, 500, 1600],
dataProviderId: 'api-2',
remoteDataFetchPace: 3000
},
{
label: 'Kernal',
unit: ' ',
id: 'kernal-id',
query: 'from(bucket: "Palindrome.js") |> range(start:-1m) |> filter(fn: (r) => r["_measurement"] == "kernel") |> filter(fn: (r) => r["_field"] == "processes_forked") |> map(fn: (r) => ({r with _value: r._value / 1000000}))',
ranges: [0, 10, 30],
dataProviderId: "influxdb-1",
remoteDataFetchPace: 6000
},
{
label: 'Disk Weighted IO Time',
unit: 'MS',
id: 'diskWIOT-id',
query: 'from(bucket: "Palindrome.js") |> range(start:-1m) |> filter(fn: (r) => r["_measurement"] == "diskio") |> filter(fn: (r) => r["_field"] == "weighted_io_time") |> filter(fn: (r) => r["name"] == "sda1")',
ranges: [0, 500, 1000],
dataProviderId: "influxdb-1",
remoteDataFetchPace: 6000
},
{
label: 'Disk Write Time',
unit: 'MS',
id: 'diskWT-id',
query: 'from(bucket: "Palindrome.js") |> range(start:-1m) |> filter(fn: (r) => r["_measurement"] == "diskio") |> filter(fn: (r) => r["_field"] == "write_time") |> filter(fn: (r) => r["name"] == "sda1")',
ranges: [0, 500, 1000],
dataProviderId: "influxdb-1"
},
{
label: 'HTTP Requests (success)',
unit: ' ',
id: 'http200',
query: "promhttp_metric_handler_requests_total{code='200'}",
ranges: [0, 1_000_000, 400_000_000],
dataProviderId: "prometheus-1",
remoteDataFetchPace: 8000
},
{
label: 'node_dmi_info',
unit: ' ',
id: 'node_dmi_info',
query: "node_dmi_info",
ranges: [0, 3, 5],
dataProviderId: "prometheus-1",
remoteDataFetchPace: 8000
},
{
label: 'node_filesystem_files',
unit: ' ',
id: 'node_filesystem_files',
query: 'node_filesystem_files{device="tmpfs", fstype="tmpfs", instance="node_exporter:9100", job="palindrome_remote_data_source", mountpoint="/run/snapd/ns"}',
ranges: [900_000, 1_000_000, 2_000_000],
dataProviderId: "prometheus-1"
},
{ label: 'node_cpu_seconds_total x 1000',
unit: ' ',
id: 'node_cpu_seconds_total',
query: 'rate(node_cpu_seconds_total{mode="user", cpu="2"}[5m])*1000',
ranges: [0, 5, 12],
dataProviderId: "prometheus-1"
},
{ label: 'node_intr_total',
unit: ' ',
id: 'node_intr_total',
query: 'rate(node_intr_total[5m])',
ranges: [200, 3000, 6000],
dataProviderId: "prometheus-1"
},
{ label: 'node_context_switches_total',
unit: ' ',
id: 'node_context_switches_total',
query: 'rate(node_context_switches_total[5m])',
ranges: [200, 500, 800],
dataProviderId: "prometheus-1"
},
],
layers : [
{
name: "Jmes-path-layer",
metrics: [
'request-count-id',
'memory-usage-id',
{
id: 'cpu-per-core-id',
label: 'Core 0',
value: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].value'
},
unit: {
jmesPath: 'metrics.system.cpu.usage.perCore[0].units'
},
ranges: ['metrics.system.cpu.usage.perCore[1].value', 80, 100],
dataProviderId: 'api-1',
remoteDataFetchPace: 3000
}
],
options: {
"mainColorStatic": "#319b31",
"layerColorLow": "#ffffff",
"layerColorMed": "#f3c60a",
"layerColorHigh": "#0096FF",
"sphereColorLow": "#ffffff",
"sphereColorMed": "#f3c60a",
"sphereColorHigh": "#0096FF",
}
},
{
name: "Server-layer",
metrics: [
{
label: 'CPU usage user',
unit: ' ',
id: 'usage_user',
query: 'from(bucket: "Palindrome.js") |> range(start:-1m) |> filter(fn: (r) => r["_measurement"] == "cpu") |> filter(fn: (r) => r["_field"] == "usage_user") |> filter(fn: (r) => r["cpu"] == "cpu-total")',
ranges: [0, 50, 150],
dataProviderId: "influxdb-1",
remoteDataFetchPace: 8500
},
{
label: 'RAM',
unit: 'GB',
id: 'ram-id',
query: 'from(bucket: "Palindrome.js") |> range(start:-1m) |> filter(fn: (r) => r["_measurement"] == "mem") |> filter(fn: (r) => r["_field"] == "used") |> map(fn: (r) => ({r with _value: r._value / 107374182}))',
ranges: [0, 16, 32],
dataProviderId: "influxdb-1"
},
'kernal-id',
'node_filesystem_files',
'node_intr_total',
'node_cpu_seconds_total'
]
},
{
name: "Container-layer",
metrics: [
'diskWIOT-id',
'diskWT-id',
'http200',
'node_dmi_info',
{
label: 'node_memory_Percpu_bytes',
unit: ' ',
id: 'node_memory_Percpu_bytes',
query: 'node_memory_Percpu_bytes',
ranges: [10_000_000, 50_000_000, 100_000_000],
dataProviderId: "prometheus-1"
},
'node_context_switches_total'
],
options: {
"mainColorStatic": "#319b31",
}
}
],
dataProviders: [
{
id: 'influxdb-1',
type: 'influxdb',
url: 'http://localhost:8086',
token: 'token',
org: 'smile_rnd',
remoteDataFetchPace: 1_000
},
{
id: 'prometheus-1',
type: 'prometheus',
url: 'http://localhost:9090',
remoteDataFetchPace: 5_000
},
{
id: 'api-1',
type: 'apiEndpoint',
url: '/apiV1.json',
remoteDataFetchPace: 20_000
},
{
id: 'api-2',
type: 'apiEndpoint',
url: '/apiV2.json',
remoteDataFetchPace: 20_000
},
],
options: {
liveData: false,
useBackend: false,
remoteDataFetchPace: 1_000 * 60,
benchmarkDataUpdate: false,
benchmarkDuration: 1_000 * 60 * 2,
benchmarkInitialData: false
}
}
}
We conducted a series of benchmarks to evaluate querying times for a single live time-series database (TSDB) use case along with other live TSDB use cases, arranged in a grid as show in the screenshot below.
The benchmarks aim to compare the performance between the frontend and the backend. Each benchmark ran for a total of 4 minutes, during which we measured the time taken by the frontend to query metrics from the TSDB and compared it with the backend's querying time.
In each iteration of the benchmark, we incrementally increased the number of use cases (thus metrics being fetched), and consequently, the number of queries being executed. At each step, we computed the time spent by the backend, the time spent by the frontend, and the performance gain observed as shown in the table below.
Frontend (ms) | Backend (ms) | Gain (%) | Number Of Metrics |
---|---|---|---|
4602.93 | 3496.29 | 24.04207755 | 118 |
6581.56 | 4935.47 | 25.01063578 | 236 |
8430.23 | 6703.73 | 20.47986828 | 354 |
10180.88 | 7893.17 | 22.47065087 | 472 |
13179.45 | 9624.82 | 26.9710041 | 590 |
17188.63 | 11134.39 | 35.22235338 | 708 |
29064.99 | 14806.37 | 49.05771514 | 826 |
33430.03 | 15896.79 | 52.44757483 | 944 |
35951.01 | 15839.43 | 55.94162723 | 1062 |
37902.94 | 16386.39 | 56.76749614 | 1180 |
38819.29 | 15324.63 | 60.52315743 | 1298 |
40902.43 | 18462.48 | 54.86214389 | 1416 |
45849.35 | 15015.27 | 67.25085525 | 1534 |
46605.03 | 13233.63 | 71.60471734 | 1652 |
51513.05 | 15213.56 | 70.4665905 | 1770 |
The results consistently demonstrated that the backend outperformed the frontend. This superior performance can be attributed to the backend's optimized query handling, reduced overhead in processing and transferring data, and better utilization of system resources compared to the browser-based frontend.
The figure below presents the execution time as well as the gain over benchmark iterations.
Here, you will find information about the available options for configuring your remote data use case in Storybook. Below is a table outlining the parameters:
Name | Description | Type | Default |
---|---|---|---|
liveData |
Enable or disable live data fetching | Boolean | False |
remoteDataFetchPace |
Specifies a periodic fetching pace in milliseconds | Number | 1000 |
webWorkersHTTP |
Enable or disable fetching data with web workers | Boolean | False |
In order to register your use case. To ensure the use case is defined in the dev environment, declare it inside the dev/utils/controls.js
file, within the palindromes
object.
For example:
export let palindromes = {
// ... other use cases
yourGroupName: [
{ name: "Your Use Case Name", isRemoteData: true, data: yourFunctionName()},
]
};
To ensure the use case is available in the Storybook environment, create a file in the stories
folder, for example, stories/yourStoryName.js
, with the following content:
import { defaultControls, defaultValues } from './controls/defaultControls';
import { yourFunctionName } from '../data-examples/yourUseCaseName';
export default {
title: 'Use Cases/Palindrome/Your Storybook Group',
argTypes: defaultControls(),
args: defaultValues(),
};
export const remoteData = createPalindrome.bind({});
remoteData.args = {
isRemoteData: true,
data: yourFunctionName()
};