CorpGlory Inc.
5 years ago
25 changed files with 1454 additions and 0 deletions
@ -0,0 +1,7 @@
|
||||
dist/ |
||||
dist-test/ |
||||
node_modules/ |
||||
npm-debug.log |
||||
.vscode/ |
||||
lib/ |
||||
package-lock.json |
@ -0,0 +1,6 @@
|
||||
src |
||||
spec |
||||
.travis.yml |
||||
jest.config.js |
||||
tsconfig.jest.json |
||||
tsconfig.json |
@ -0,0 +1,24 @@
|
||||
language: node_js |
||||
node_js: |
||||
- '8' |
||||
before_script: |
||||
- npm install |
||||
script: |
||||
- npm test |
||||
|
||||
jobs: |
||||
include: |
||||
- stage: npm release |
||||
script: |
||||
- npm run build |
||||
deploy: |
||||
provider: npm |
||||
skip_cleanup: true |
||||
email: ping@corpglory.com |
||||
api_key: |
||||
secure: hdGJrwq7fny1EPGZqxX7/Khyw4kokW5/304JAvKVSdSVuonGNCNzgPO5UJppdN9UrX3RZTvs5NdaJUGt0Xhq+9UlxfGxg6Gl44kf8AVNFHy6+YsZu4kWCEEeFFLraELeQ+K+2U6LOeoQ7muGvTlLpmfkT+J9NVUgdxHsrmziktt+iWIY2a6gOjJwLXC8lbwBy7UzQq7v8YJX6hU5t4FwlsNFwObpaKRK4xRwSDnTnHurJnTzLNcR5+sp6Ltx0EKAcbwqTXv8iTJsKMfTXimXdWuIrQpuyfpNyfYyjWxK2AU01qFAA3+ianv2sRQHqm56R9oXu+rTC9v8djutwuR4uCaTeeSVIO2zp6HcnWHciNVjUXe1DijjqBU1NIDq5wPPbW9V2meXXCWgW0m2iY+2PDQDa26PIIxS6NvYpwITW903FhBuB6VHGppPu/1J87hzo7FJrWkies4rIpi2xD9tosIQ0EInIi1m2o65oncOGNzUvS9UMyU/e0jGPnQ6Q5sqrUm8juvn+elrevFCrYIYKvQ5k+MJWurTyaq0S0xMx7pacVImKb2pirtxSVmo0nCSpFgagKAkN6+dXLO+siuDMmwMJvKqRg0+9SclYcYjobexiKNLaOulgLfOlSpjbFdVhQjWPJLZL50/y4R5NuiAzOCSeKNvRjw2YHIKaTvCWZg= |
||||
on: |
||||
tags: true |
||||
|
||||
notifications: |
||||
email: false |
@ -0,0 +1,22 @@
|
||||
# tsdb-kit |
||||
|
||||
[![Build Status](https://travis-ci.org/CorpGlory/tsdb-kit.svg?branch=master)](https://travis-ci.org/CorpGlory/tsdb-kit) |
||||
|
||||
Node.js library for running Grafana datasources on backend plus utils. |
||||
You can send your datasource metric from Grafana to compile it on Node.js and query your datasource via Grafana API in background. |
||||
|
||||
User gets unified interface to all datasources. Library gives single output format: fields order, time units, etc |
||||
|
||||
## Supported datasources |
||||
|
||||
* Influxdb |
||||
* Graphite |
||||
* Prometheus |
||||
* PostgreSQL / TimescaleDB |
||||
* ElasticSearch |
||||
|
||||
Please write us a letter if you want your datasource to be supported: ping@corpglory.com |
||||
|
||||
## Projects based on library |
||||
* [grafana-data-exporter](https://github.com/CorpGlory/grafana-data-exporter) |
||||
* [Hastic](https://github.com/hastic/hastic-server) |
@ -0,0 +1,21 @@
|
||||
module.exports = { |
||||
"verbose": true, |
||||
"globals": { |
||||
"ts-jest": { |
||||
"useBabelrc": true, |
||||
"tsConfigFile": "tsconfig.jest.json" |
||||
} |
||||
}, |
||||
"transform": { |
||||
"\\.ts": "ts-jest" |
||||
}, |
||||
"testRegex": "(\\.|/)([jt]est)\\.[jt]s$", |
||||
"moduleFileExtensions": [ |
||||
"ts", |
||||
"js", |
||||
"json" |
||||
], |
||||
"setupFiles": [ |
||||
"<rootDir>/spec/setup_tests.ts" |
||||
] |
||||
}; |
@ -0,0 +1,36 @@
|
||||
{ |
||||
"name": "tsdb-kit", |
||||
"version": "0.1.17", |
||||
"description": "", |
||||
"scripts": { |
||||
"build": "tsc", |
||||
"dev": "tsc -w", |
||||
"test": "jest --config jest.config.js" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "git+https://github.com/CorpGlory/tsdb-kit.git" |
||||
}, |
||||
"author": "CorpGlory Inc.", |
||||
"publishConfig": { |
||||
"access": "public" |
||||
}, |
||||
"license": "", |
||||
"bugs": { |
||||
"url": "https://github.com/CorpGlory/tsdb-kit/issues" |
||||
}, |
||||
"homepage": "https://github.com/CorpGlory/tsdb-kit", |
||||
"dependencies": { |
||||
"axios": "^0.18.0", |
||||
"moment": "^2.22.2", |
||||
"url": "^0.11.0" |
||||
}, |
||||
"devDependencies": { |
||||
"@types/jest": "24.0.0", |
||||
"jest": "24.0.0", |
||||
"ts-jest": "23.10.5", |
||||
"typescript": "3.3.1" |
||||
}, |
||||
"main": "./lib/index.js", |
||||
"typings": "./lib/index.d.ts" |
||||
} |
@ -0,0 +1,254 @@
|
||||
import { ElasticsearchMetric } from '../src/metrics/elasticsearch_metric'; |
||||
import { MetricQuery, Datasource } from '../src/metrics/metric'; |
||||
|
||||
import 'jest'; |
||||
import * as _ from 'lodash'; |
||||
|
||||
describe('simple query', function(){ |
||||
|
||||
let datasourse: Datasource = { |
||||
url: "api/datasources/proxy/1/_msearch", |
||||
data: [{ |
||||
"search_type": "query_then_fetch", |
||||
"ignore_unavailable": true, |
||||
"index": "metricbeat-*" |
||||
}, |
||||
{ |
||||
"size": 0, |
||||
"query": { |
||||
"bool": { |
||||
"filter": [ |
||||
{ |
||||
"range": { |
||||
"@timestamp": { |
||||
"gte": "1545933121101", |
||||
"lte": "1545954721101", |
||||
"format": "epoch_millis" |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
"query_string": { |
||||
"analyze_wildcard": true, |
||||
"query": "beat.hostname:opt-project.ru AND !system.network.name:\"IBM USB Remote NDIS Network Device\"" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"aggs": { |
||||
"2": { |
||||
"date_histogram": { |
||||
"interval": "30s", |
||||
"field": "@timestamp", |
||||
"min_doc_count": 0, |
||||
"extended_bounds": { |
||||
"min": "1545933121101", |
||||
"max": "1545954721101" |
||||
}, |
||||
"format": "epoch_millis" |
||||
}, |
||||
"aggs": { |
||||
"1": { |
||||
"avg": { |
||||
"field": "system.network.in.bytes" |
||||
} |
||||
}, |
||||
"3": { |
||||
"derivative": { |
||||
"buckets_path": "1" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}], |
||||
type: "elasticsearch" |
||||
}; |
||||
datasourse.data = datasourse.data.map(d => JSON.stringify(d)).join('\n'); |
||||
|
||||
let targets = [ |
||||
{ |
||||
"bucketAggs": [ |
||||
{ |
||||
"field": "@timestamp", |
||||
"id": "2", |
||||
"settings": { |
||||
"interval": "auto", |
||||
"min_doc_count": 0, |
||||
"trimEdges": 0 |
||||
}, |
||||
"type": "date_histogram" |
||||
} |
||||
], |
||||
"metrics": [ |
||||
{ |
||||
"field": "system.network.in.bytes", |
||||
"hide": true, |
||||
"id": "1", |
||||
"meta": {}, |
||||
"pipelineAgg": "select metric", |
||||
"settings": {}, |
||||
"type": "avg" |
||||
}, |
||||
{ |
||||
"field": "1", |
||||
"id": "3", |
||||
"meta": {}, |
||||
"pipelineAgg": "1", |
||||
"settings": {}, |
||||
"type": "derivative" |
||||
} |
||||
], |
||||
"query": "beat.hostname:opt-project.ru AND !system.network.name:\"IBM USB Remote NDIS Network Device\"", |
||||
"refId": "A", |
||||
"target": "carbon.agents.0b0226864dc8-a.cpuUsage", |
||||
"timeField": "@timestamp" |
||||
} |
||||
]; |
||||
|
||||
let queryTemplate = [{ |
||||
"search_type": "query_then_fetch", |
||||
"ignore_unavailable": true, |
||||
"index": "metricbeat-*" |
||||
}, |
||||
{ |
||||
"size": 0, |
||||
"query": { |
||||
"bool": { |
||||
"filter": [ |
||||
{ |
||||
"range": { |
||||
"@timestamp": { |
||||
"gte": "0", |
||||
"lte": "1", |
||||
"format": "epoch_millis" |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
"query_string": { |
||||
"analyze_wildcard": true, |
||||
"query": "beat.hostname:opt-project.ru AND !system.network.name:\"IBM USB Remote NDIS Network Device\"" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"aggs": { |
||||
"2": { |
||||
"date_histogram": { |
||||
"interval": "30s", |
||||
"field": "@timestamp", |
||||
"min_doc_count": 0, |
||||
"extended_bounds": { |
||||
"min": "1545933121101", |
||||
"max": "1545954721101" |
||||
}, |
||||
"format": "epoch_millis" |
||||
}, |
||||
"aggs": { |
||||
"1": { |
||||
"avg": { |
||||
"field": "system.network.in.bytes" |
||||
} |
||||
}, |
||||
"3": { |
||||
"derivative": { |
||||
"buckets_path": "1" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}]; |
||||
|
||||
let elasticMetric = new ElasticsearchMetric(datasourse, targets); |
||||
|
||||
it('check correct time processing', function() { |
||||
let expectedQuery = { |
||||
url: datasourse.url, |
||||
method: 'POST', |
||||
schema: { |
||||
data: queryTemplate.map(e => JSON.stringify(e)).join('\n') |
||||
} |
||||
}; |
||||
|
||||
let from = 0; |
||||
let to = 1; |
||||
let limit = 222; |
||||
let offset = 333; |
||||
|
||||
expect(elasticMetric.getQuery(from, to, limit, offset)).toEqual(expectedQuery); |
||||
}); |
||||
|
||||
|
||||
let result = { |
||||
"data": { |
||||
"responses": [ |
||||
{ |
||||
"took": 39, |
||||
"timed_out": false, |
||||
"_shards": { |
||||
"total": 37, |
||||
"successful": 37, |
||||
"failed": 0 |
||||
}, |
||||
"hits": { |
||||
"total": 63127, |
||||
"max_score": 0.0, |
||||
"hits": [] |
||||
}, |
||||
"aggregations": { |
||||
"2": { |
||||
"buckets": [ |
||||
{ |
||||
"key_as_string": "1545934140000", |
||||
"key": 1545934140000, |
||||
"doc_count": 118, |
||||
"1": { |
||||
"value": 8.640455022375E9 |
||||
} |
||||
}, |
||||
{ |
||||
"key_as_string": "1545934200000", |
||||
"key": 1545934200000, |
||||
"doc_count": 178, |
||||
"1": { |
||||
"value": 8.641446309833334E9 |
||||
}, |
||||
"3": { |
||||
"value": 991287.4583339691 |
||||
} |
||||
}, |
||||
{ |
||||
"key_as_string": "1545934260000", |
||||
"key": 1545934260000, |
||||
"doc_count": 177, |
||||
"1": { |
||||
"value": 8.642345302333334E9 |
||||
}, |
||||
"3": { |
||||
"value": 898992.5 |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
] |
||||
} |
||||
}; |
||||
|
||||
it('check results parsing', function() { |
||||
let expectedResult = { |
||||
columns: ['timestamp', 'target'], |
||||
values: [[1545934140000, null], |
||||
[1545934200000, 991287.4583339691], |
||||
[1545934260000, 898992.5] |
||||
] |
||||
} |
||||
|
||||
expect(elasticMetric.getResults(result)).toEqual(expectedResult); |
||||
}); |
||||
}); |
@ -0,0 +1,25 @@
|
||||
import { Datasource, Metric } from '../src/index'; |
||||
|
||||
import 'jest'; |
||||
|
||||
describe('correct Graphite query', function() { |
||||
let datasource: Datasource = { |
||||
url: 'http://example.com:1234', |
||||
type: 'graphite', |
||||
params: { |
||||
db: '', |
||||
q: '', |
||||
epoch: '' |
||||
}, |
||||
data: 'target=target=template(hosts.$hostname.cpu, hostname=\"worker1\")&from=00:00_00000000&until=00:00_00000000&maxDataPoints=000' |
||||
}; |
||||
|
||||
let target = `target=template(hosts.$hostname.cpu, hostname="worker1")`; |
||||
let query = new Metric(datasource, [target]); |
||||
|
||||
it("test simple query with time clause", function () { |
||||
expect(query.metricQuery.getQuery(1534809600000, 1537488000000, 500, 0).url).toBe( |
||||
`${datasource.url}?target=${target}&from=1534809600&until=1537488000&maxDataPoints=500` |
||||
) |
||||
}); |
||||
}) |
@ -0,0 +1,254 @@
|
||||
import { PostgresMetric } from '../src/metrics/postgres_metric'; |
||||
import { MetricQuery } from '../src/metrics/metric'; |
||||
|
||||
import 'jest'; |
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
describe('Test query creation', function() { |
||||
|
||||
let limit = 1000; |
||||
let offset = 0; |
||||
let from = 1542983750857; |
||||
let to = 1542984313292; |
||||
let postgres = getDefaultMetric(); |
||||
let mQuery: MetricQuery = postgres.getQuery(from, to, limit, offset); |
||||
|
||||
it('test that payload placed to data field', function() { |
||||
expect('data' in mQuery.schema).toBeTruthy(); |
||||
expect('queries' in mQuery.schema.data).toBeTruthy(); |
||||
expect(mQuery.schema.data.queries).toBeInstanceOf(Array); |
||||
}); |
||||
|
||||
it('test from/to casting to string', function() { |
||||
expect(typeof mQuery.schema.data.from).toBe('string'); |
||||
expect(typeof mQuery.schema.data.to).toBe('string'); |
||||
}); |
||||
|
||||
it('method should be POST', function() { |
||||
expect(mQuery.method.toLocaleLowerCase()).toBe('post'); |
||||
}); |
||||
}); |
||||
|
||||
describe('Test result parsing', function() { |
||||
let postgres = getDefaultMetric(); |
||||
let timestamps = [1542983800000, 1542983800060, 1542983800120] |
||||
let response = { |
||||
data: { |
||||
results: { |
||||
A: { |
||||
refId: 'A', |
||||
meta: { |
||||
rowCount:0, |
||||
sql: 'SELECT "time" AS "time", val FROM local ORDER BY 1' |
||||
}, |
||||
series: [ |
||||
{ |
||||
name:"val", |
||||
points: [ |
||||
[622, timestamps[0]], |
||||
[844, timestamps[1]], |
||||
[648, timestamps[2]] |
||||
] |
||||
} |
||||
], |
||||
tables: 'null' |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
let result = postgres.getResults(response); |
||||
|
||||
it('check results columns order', function() { |
||||
let timestampColumnNumber = result.columns.indexOf('timestamp'); |
||||
expect(result.values.map(v => v[timestampColumnNumber])).toEqual(timestamps); |
||||
}); |
||||
}); |
||||
|
||||
describe('Test sql processing', function() { |
||||
let limit = 1000; |
||||
let offset = 77; |
||||
let from = 1542983750857; |
||||
let to = 1542984313292; |
||||
|
||||
let check = function(original: string, expected: string) { |
||||
checkExpectation(original, expected, from, to, limit, offset); |
||||
} |
||||
|
||||
it('simple sql with one select', function() { |
||||
let original = `SELECT
|
||||
\"time\" AS \"time\", |
||||
val |
||||
FROM local |
||||
ORDER BY 1`;
|
||||
let expected = `SELECT
|
||||
\"time\" AS \"time\", |
||||
val |
||||
FROM local |
||||
ORDER BY 1 LIMIT ${limit} OFFSET ${offset}`;
|
||||
check(original, expected); |
||||
}); |
||||
|
||||
it('sql with order by rows', function() { |
||||
let original = `SELECT
|
||||
$__time(time), |
||||
AVG(power) OVER(ORDER BY speed ROWS BETWEEN 150 PRECEDING AND CURRENT ROW) |
||||
FROM |
||||
wind_pwr_spd |
||||
WHERE |
||||
$__timeFilter(time)`;
|
||||
let expected = `SELECT
|
||||
$__time(time), |
||||
AVG(power) OVER(ORDER BY speed ROWS BETWEEN 150 PRECEDING AND CURRENT ROW) |
||||
FROM |
||||
wind_pwr_spd |
||||
WHERE |
||||
$__timeFilter(time) LIMIT ${limit} OFFSET ${offset}`;
|
||||
check(original,expected); |
||||
}); |
||||
|
||||
it('sql with offset limit', function() { |
||||
let original = `WITH RECURSIVE t(n) AS (
|
||||
VALUES (1) |
||||
UNION ALL |
||||
SELECT n+1 FROM t WHERE n < 100 |
||||
) |
||||
SELECT sum(n) FROM t OFFSET 0 LIMIT 0;`;
|
||||
|
||||
|
||||
let expected = `WITH RECURSIVE t(n) AS (
|
||||
VALUES (1) |
||||
UNION ALL |
||||
SELECT n+1 FROM t WHERE n < 100 |
||||
) |
||||
SELECT sum(n) FROM t OFFSET ${offset} LIMIT ${limit};`;
|
||||
check(original, expected); |
||||
}); |
||||
|
||||
it('sql with macroses', function() { |
||||
let original = `SELECT
|
||||
time |
||||
FROM metric_values |
||||
WHERE time > $__timeFrom() |
||||
OR time < $__timeFrom() |
||||
OR 1 < $__unixEpochFrom() |
||||
OR $__unixEpochTo() > 1 ORDER BY 1`;
|
||||
let expected = `SELECT
|
||||
time |
||||
FROM metric_values |
||||
WHERE time > $__timeFrom() |
||||
OR time < $__timeFrom() |
||||
OR 1 < $__unixEpochFrom() |
||||
OR $__unixEpochTo() > 1 ORDER BY 1 LIMIT ${limit} OFFSET ${offset}`;
|
||||
check(original, expected); |
||||
}); |
||||
|
||||
it('complex sql with one select', function() { |
||||
let original = `SELECT
|
||||
statistics.created_at as time, |
||||
CAST(statistics.value AS decimal) as value, |
||||
sensor.title as metric |
||||
FROM statistics |
||||
INNER JOIN sensor |
||||
ON sensor.id = statistics.sensor_id |
||||
WHERE |
||||
statistics.device_id = '000-aaaa-bbbb' |
||||
AND sensor.type = 5 |
||||
AND sensor.section_id IN($section_id) |
||||
AND statistics.value != 'ERR' |
||||
AND statistics.value !='???' |
||||
AND $__timeFilter(statistics.created_at)`;
|
||||
let expected = `SELECT
|
||||
statistics.created_at as time, |
||||
CAST(statistics.value AS decimal) as value, |
||||
sensor.title as metric |
||||
FROM statistics |
||||
INNER JOIN sensor |
||||
ON sensor.id = statistics.sensor_id |
||||
WHERE |
||||
statistics.device_id = '000-aaaa-bbbb' |
||||
AND sensor.type = 5 |
||||
AND sensor.section_id IN($section_id) |
||||
AND statistics.value != 'ERR' |
||||
AND statistics.value !='???' |
||||
AND $__timeFilter(statistics.created_at) LIMIT ${limit} OFFSET ${offset}`;
|
||||
check(original, expected); |
||||
}) |
||||
|
||||
it('sql with number of nested select', function() { |
||||
let original = `WITH regional_sales AS (
|
||||
SELECT region, SUM(amount) AS total_sales |
||||
FROM orders |
||||
GROUP BY region LIMIT 5 OFFSET 1 |
||||
), top_regions AS ( |
||||
SELECT region |
||||
FROM regional_sales |
||||
WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales) |
||||
LIMIT 3 |
||||
) |
||||
SELECT region, |
||||
product, |
||||
SUM(quantity) AS product_units, |
||||
SUM(amount) AS product_sales |
||||
FROM orders |
||||
WHERE region IN (SELECT region FROM top_regions) |
||||
GROUP BY region, product OFFSET 500;`;
|
||||
let expected = `WITH regional_sales AS (
|
||||
SELECT region, SUM(amount) AS total_sales |
||||
FROM orders |
||||
GROUP BY region LIMIT 5 OFFSET 1 |
||||
), top_regions AS ( |
||||
SELECT region |
||||
FROM regional_sales |
||||
WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales) |
||||
LIMIT 3 |
||||
) |
||||
SELECT region, |
||||
product, |
||||
SUM(quantity) AS product_units, |
||||
SUM(amount) AS product_sales |
||||
FROM orders |
||||
WHERE region IN (SELECT region FROM top_regions) |
||||
GROUP BY region, product OFFSET ${offset} LIMIT ${limit};`;
|
||||
check(original, expected); |
||||
}); |
||||
}); |
||||
|
||||
function checkExpectation(original: string, expected: string, from: number, to: number, limit: number, offset: number) { |
||||
let metric = getMetricWithSql(original); |
||||
expect(metric.getQuery(from, to, limit, offset).schema.data.queries[0].rawSql).toBe(expected); |
||||
} |
||||
|
||||
function getMetricWithSql(sql: string): PostgresMetric { |
||||
let metric = getDefaultMetric(); |
||||
metric.datasource.data.queries[0].rawSql = sql; |
||||
return metric; |
||||
} |
||||
|
||||
function getDefaultMetric(): PostgresMetric { |
||||
let queryPayload = { |
||||
from: 1542983750857, |
||||
to: 1542984313292, |
||||
queries:[{ |
||||
refId: 'A', |
||||
intervalMs:2000, |
||||
maxDataPoints:191, |
||||
datasourceId:1, |
||||
rawSql: 'SELECT\n \"time\" AS \"time\",\n val\nFROM local\nORDER BY 1', |
||||
format: 'time_series' |
||||
}] |
||||
}; |
||||
|
||||
let datasource = { |
||||
url: 'api/tsdb/query', |
||||
type: 'postgres', |
||||
data: queryPayload |
||||
}; |
||||
|
||||
let targets = [{ |
||||
refId: 'A', |
||||
}]; |
||||
|
||||
return new PostgresMetric(datasource, targets); |
||||
} |
@ -0,0 +1,21 @@
|
||||
import { PrometheusMetric } from '../src/metrics/prometheus_metric'; |
||||
|
||||
import 'jest'; |
||||
|
||||
|
||||
describe('Test Prometheus time range processing', function() { |
||||
let datasource = { |
||||
type: 'prometheus', |
||||
url: 'api/datasources/proxy/4/api/v1/query_range?query=node_disk_io_time_ms&start=1543411320&end=1543432950&step=30' |
||||
} |
||||
let targets = []; |
||||
let prometheus = new PrometheusMetric(datasource, targets); |
||||
|
||||
it('check that from/to present in url', function() { |
||||
let from = 1234567891234; //milliseconds
|
||||
let to = 1234567899999; |
||||
let query = prometheus.getQuery(from, to, 1000, 0); |
||||
expect(query.url.indexOf(`start=${Math.floor(from / 1000)}`) !== -1).toBeTruthy(); |
||||
expect(query.url.indexOf(`end=${Math.floor(to / 1000)}`) !== -1).toBeTruthy(); |
||||
}); |
||||
}); |
@ -0,0 +1,2 @@
|
||||
console.log = jest.fn(); |
||||
console.error = jest.fn(); |
@ -0,0 +1,42 @@
|
||||
import { Datasource, Metric } from '../src/index'; |
||||
|
||||
import 'jest'; |
||||
|
||||
|
||||
describe('Correct InfluxDB query', function() { |
||||
let datasource: Datasource = { |
||||
url: 'url', |
||||
type: 'influxdb', |
||||
params: { |
||||
db: 'db', |
||||
q: `SELECT mean("value") FROM "db" WHERE time > xxx AND time <= xxx LIMIT 100 OFFSET 20`, |
||||
epoch: '' |
||||
} |
||||
}; |
||||
|
||||
let target = 'mean("value")'; |
||||
|
||||
it("test query with two time expressions", function() { |
||||
let query = new Metric(datasource, [target]); |
||||
expect(query.metricQuery.getQuery(1534809600,1537488000,666,10).schema.params.q).toBe( |
||||
`SELECT mean("value") FROM "db" WHERE time >= 1534809600ms AND time <= 1537488000ms LIMIT 666 OFFSET 10` |
||||
) |
||||
}); |
||||
|
||||
it('test query with one time expression', function() { |
||||
datasource.params.q = `SELECT mean("value") FROM "cpu_value" WHERE time >= now() - 6h GROUP BY time(30s) fill(null)`; |
||||
let query = new Metric(datasource, [target]); |
||||
expect(query.metricQuery.getQuery(1534809600,1537488000,666,10).schema.params.q).toBe( |
||||
`SELECT mean("value") FROM "cpu_value" WHERE time >= 1534809600ms AND time <= 1537488000ms GROUP BY time(30s) fill(null) LIMIT 666 OFFSET 10` |
||||
) |
||||
}); |
||||
|
||||
it('test query with time expression', function() { |
||||
datasource.params.q = `SELECT mean("value") FROM "cpu_value" WHERE time>= now() - 6h AND time<xxx GROUP BY time(30s) fill(null)`; |
||||
let query = new Metric(datasource, [target]); |
||||
expect(query.metricQuery.getQuery(1534809600,1537488000,666,10).schema.params.q).toBe( |
||||
`SELECT mean("value") FROM "cpu_value" WHERE time >= 1534809600ms AND time <= 1537488000ms GROUP BY time(30s) fill(null) LIMIT 666 OFFSET 10` |
||||
) |
||||
}); |
||||
|
||||
}) |
@ -0,0 +1,18 @@
|
||||
import { processSQLLimitOffset } from '../src/metrics/utils'; |
||||
|
||||
import 'jest'; |
||||
|
||||
|
||||
describe('test utils methods', function(){ |
||||
it('test SQL limit offset processing', function() { |
||||
expect(processSQLLimitOffset('without', 10, 5)).toBe('without LIMIT 10 OFFSET 5'); |
||||
expect(processSQLLimitOffset('limit 3 OFFSET 1', 10, 5)).toBe('LIMIT 10 OFFSET 5'); |
||||
expect(processSQLLimitOffset('xxx \nlimit 11\nxxx', 10, 5)).toBe('xxx \nLIMIT 10\nxxx OFFSET 5'); |
||||
expect(processSQLLimitOffset('xxx offset 4 xxx', 10, 5)).toBe('xxx OFFSET 5 xxx LIMIT 10'); |
||||
expect(processSQLLimitOffset('xxx\nlimit 0\noffset 4\nxxx', 10, 5)).toBe('xxx\nLIMIT 10\nOFFSET 5\nxxx'); |
||||
expect(processSQLLimitOffset('()()(limit 3 OFFSET 1) (())', 10, 5)).toBe('()()(limit 3 OFFSET 1) (()) LIMIT 10 OFFSET 5'); |
||||
expect(processSQLLimitOffset('()(limit 3) OFFSET 1 ()', 10, 5)).toBe('()(limit 3) OFFSET 5 () LIMIT 10'); |
||||
expect(processSQLLimitOffset('(offset 9)(limit 3) OFFSET 1 ()()(()) LIMIT 8 ()', 10, 5)) |
||||
.toBe('(offset 9)(limit 3) OFFSET 5 ()()(()) LIMIT 10 ()'); |
||||
}); |
||||
}); |
@ -0,0 +1,134 @@
|
||||
import { Metric } from './metrics/metrics_factory'; |
||||
import { MetricQuery, Datasource } from './metrics/metric'; |
||||
|
||||
import { URL } from 'url'; |
||||
import axios from 'axios'; |
||||
import * as _ from 'lodash'; |
||||
|
||||
export class DataKitError extends Error { |
||||
constructor( |
||||
message: string, |
||||
public datasourceType?: string, |
||||
public datasourceUrl?: string |
||||
) { |
||||
super(message); |
||||
} |
||||
}; |
||||
|
||||
export class BadRange extends DataKitError {}; |
||||
export class GrafanaUnavailable extends DataKitError {}; |
||||
export class DatasourceUnavailable extends DataKitError {}; |
||||
|
||||
const CHUNK_SIZE = 50000; |
||||
|
||||
|
||||
/** |
||||
* @param metric to query to Grafana |
||||
* @returns { values: [time, value][], columns: string[] } |
||||
*/ |
||||
export async function queryByMetric( |
||||
metric: Metric, url: string, from: number, to: number, apiKey: string |
||||
): Promise<{ values: [number, number][], columns: string[] }> { |
||||
|
||||
if(from > to) { |
||||
throw new BadRange( |
||||
`Data-kit got wrong range: from ${from} > to ${to}`, |
||||
metric.datasource.type, |
||||
url |
||||
); |
||||
} |
||||
|
||||
if(from === to) { |
||||
console.warn(`Data-kit got from === to`); |
||||
} |
||||
|
||||
const grafanaUrl = getGrafanaUrl(url); |
||||
|
||||
let data = { |
||||
values: [], |
||||
columns: [] |
||||
}; |
||||
|
||||
while(true) { |
||||
let query = metric.metricQuery.getQuery(from, to, CHUNK_SIZE, data.values.length); |
||||
query.url = `${grafanaUrl}/${query.url}`; |
||||
let res = await queryGrafana(query, apiKey, metric.datasource); |
||||
let chunk = metric.metricQuery.getResults(res); |
||||
let values = chunk.values; |
||||
data.values = data.values.concat(values); |
||||
data.columns = chunk.columns; |
||||
|
||||
if(values.length < CHUNK_SIZE) { |
||||
// because if we get less that we could, then there is nothing more
|
||||
break; |
||||
} |
||||
} |
||||
return data; |
||||
} |
||||
|
||||
async function queryGrafana(query: MetricQuery, apiKey: string, datasource: Datasource) { |
||||
let headers = { Authorization: `Bearer ${apiKey}` }; |
||||
|
||||
if(query.headers !== undefined) { |
||||
_.merge(headers, query.headers); |
||||
} |
||||
|
||||
|
||||
let axiosQuery = { |
||||
headers, |
||||
url: query.url, |
||||
method: query.method, |
||||
}; |
||||
|
||||
_.defaults(axiosQuery, query.schema); |
||||
|
||||
try { |
||||
var res = await axios(axiosQuery); |
||||
} catch (e) { |
||||
const msg = `Data kit: fail while request data: ${e.message}`; |
||||
const parsedUrl = new URL(query.url); |
||||
const queryUrl = `query url: ${JSON.stringify(parsedUrl.pathname)}`; |
||||
console.error(`${msg} ${queryUrl}`); |
||||
if(e.errno === 'ECONNREFUSED') { |
||||
throw new GrafanaUnavailable(e.message); |
||||
} |
||||
if(e.response !== undefined) { |
||||
console.error(`Response: \ |
||||
status: ${e.response.status}, \ |
||||
response data: ${JSON.stringify(e.response.data)}, \ |
||||
headers: ${JSON.stringify(e.response.headers)} |
||||
`);
|
||||
if(e.response.status === 401) { |
||||
throw new Error(`Unauthorized. Check the API_KEY. ${e.message}`); |
||||
} |
||||
if(e.response.status === 502) { |
||||
let datasourceError = new DatasourceUnavailable( |
||||
`datasource ${parsedUrl.pathname} unavailable, message: ${e.message}`, |
||||
datasource.type, |
||||
query.url |
||||
); |
||||
throw datasourceError; |
||||
} |
||||
} |
||||
throw new Error(msg); |
||||
} |
||||
|
||||
return res; |
||||
} |
||||
|
||||
function getGrafanaUrl(url: string) { |
||||
const parsedUrl = new URL(url); |
||||
const path = parsedUrl.pathname; |
||||
const panelUrl = path.match(/^\/*([^\/]*)\/d\//); |
||||
if(panelUrl === null) { |
||||
return url; |
||||
} |
||||
|
||||
const origin = parsedUrl.origin; |
||||
const grafanaSubPath = panelUrl[1]; |
||||
if(grafanaSubPath.length > 0) { |
||||
return `${origin}/${grafanaSubPath}`; |
||||
} |
||||
|
||||
return origin; |
||||
} |
@ -0,0 +1,3 @@
|
||||
export { Metric } from './metrics/metrics_factory'; |
||||
export { Datasource } from './metrics/metric' |
||||
export { queryByMetric, GrafanaUnavailable, DatasourceUnavailable } from './grafana_service'; |
@ -0,0 +1,132 @@
|
||||
import { AbstractMetric, Datasource, MetricId, MetricQuery, MetricResults } from './metric'; |
||||
import { DataKitError } from '../grafana_service'; |
||||
|
||||
import * as _ from 'lodash'; |
||||
|
||||
export type RangeFilter = { range: { [key: string]: { gte: String, lte: String } } }; |
||||
export type QueryStringFilter = { query_string: { analyze_wildcard: Boolean, query: String } }; |
||||
|
||||
export type QueryConfig = { |
||||
size: number, |
||||
query: { |
||||
bool: { |
||||
filter: (RangeFilter | QueryStringFilter)[] |
||||
} |
||||
}, |
||||
aggs: { [key: string]: Aggregation } |
||||
}; |
||||
|
||||
export type Aggregation = { |
||||
date_histogram: { |
||||
interval: String, |
||||
field: String, |
||||
min_doc_count: Number, |
||||
extended_bounds: { min: String, max: String }, |
||||
format: String |
||||
} |
||||
}; |
||||
|
||||
const DATE_HISTOGRAM_FIELD = 'date_histogram'; |
||||
|
||||
export class ElasticsearchMetric extends AbstractMetric { |
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
super(datasource, targets, id); |
||||
} |
||||
|
||||
getQuery(from: number, to: number, limit: number, offset: number): MetricQuery { |
||||
let data = this.datasource.data.split('\n').map(d => d === '' ? d: JSON.parse(d)); |
||||
if(data.length === 0) { |
||||
throw new DataKitError('Datasource data is empty'); |
||||
} |
||||
|
||||
const queryConfig: QueryConfig = data[1]; |
||||
|
||||
queryConfig.size = 0; |
||||
let timeField = null; |
||||
|
||||
let aggs = _.filter(queryConfig.aggs, f => _.has(f, DATE_HISTOGRAM_FIELD)); |
||||
_.each(aggs, (agg: Aggregation) => { |
||||
agg[DATE_HISTOGRAM_FIELD].extended_bounds = { |
||||
min: from.toString(), |
||||
max: to.toString() |
||||
}; |
||||
|
||||
if(timeField !== null) { |
||||
console.warn( |
||||
`got more than one datasource time field, change ${timeField} to ${agg[DATE_HISTOGRAM_FIELD].field}` |
||||
); |
||||
} |
||||
timeField = agg[DATE_HISTOGRAM_FIELD].field; |
||||
}); |
||||
|
||||
if(timeField === null) { |
||||
throw new Error('datasource time field not found'); |
||||
} |
||||
|
||||
let filters = queryConfig.query.bool.filter.filter(f => _.has(f, 'range')) as RangeFilter[]; |
||||
if(filters.length === 0) { |
||||
throw new DataKitError('Empty filters'); |
||||
} |
||||
let range = filters[0].range; |
||||
range[timeField].gte = from.toString(); |
||||
range[timeField].lte = to.toString(); |
||||
|
||||
|
||||
data = data |
||||
.filter(d => d !== '') |
||||
.map(d => JSON.stringify(d)) |
||||
.join('\n'); |
||||
data += '\n'; |
||||
|
||||
return { |
||||
url: this.datasource.url, |
||||
method: 'POST', |
||||
schema: { data }, |
||||
headers: {'Content-Type': 'application/json'} |
||||
} |
||||
} |
||||
|
||||
getResults(res): MetricResults { |
||||
let columns = ['timestamp', 'target']; |
||||
let values = []; |
||||
|
||||
if(res.data === undefined || res.data.responses.length < 1) { |
||||
console.log('datasource return empty response, no data'); |
||||
return { |
||||
columns, |
||||
values |
||||
}; |
||||
} |
||||
|
||||
let aggregations = res.data.responses[0].aggregations; |
||||
let aggrgAgg: any = this.targets[0].bucketAggs.filter(a => { |
||||
return !a.fake && _.has(aggregations, a.id) |
||||
}); |
||||
if(_.isEmpty(aggrgAgg)) { |
||||
const bucketAggs = JSON.stringify(this.targets[0].bucketAggs); |
||||
const aggregationKeys = JSON.stringify(_.keys(aggregations)); |
||||
console.error(`can't find related aggregation id. bucketAggs:${bucketAggs} aggregationKeys:${aggregationKeys}`); |
||||
throw new DataKitError(`can't find related aggregation id`); |
||||
} else { |
||||
aggrgAgg = aggrgAgg[0].id; |
||||
} |
||||
let responseValues = aggregations[aggrgAgg].buckets; |
||||
let agg = this.targets[0].metrics.filter(m => !m.hide).map(m => m.id); |
||||
|
||||
if(agg.length > 1) { |
||||
throw new DataKitError(`multiple series for metric are not supported currently: ${JSON.stringify(agg)}`); |
||||
} |
||||
|
||||
agg = agg[0]; |
||||
|
||||
if(responseValues.length > 0) { |
||||
values = responseValues.map(r => [r.key, _.has(r, agg) ? r[agg].value: null]); |
||||
} |
||||
|
||||
return { |
||||
columns, |
||||
values |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,65 @@
|
||||
import { AbstractMetric, Datasource, MetricId, MetricQuery, MetricResults } from './metric'; |
||||
|
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
export class GraphiteMetric extends AbstractMetric { |
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
super(datasource, targets, id); |
||||
} |
||||
|
||||
getQuery(from: number, to: number, limit: number, offset: number): MetricQuery { |
||||
let fromDate = Math.floor(from / 1000); |
||||
let toDate = Math.floor(to / 1000); |
||||
|
||||
let fromRegex = /from=[^\&]+/i; |
||||
let untilRegex = /until=[^\&]+/i; |
||||
let limitRegex = /maxDataPoints=[^\&]+/i; |
||||
|
||||
let query: string = this.datasource.data; |
||||
let replacements: [RegExp, string][] = [ |
||||
[fromRegex, `from=${fromDate}`], |
||||
[untilRegex, `until=${toDate}`], |
||||
[limitRegex, `maxDataPoints=${limit}`] |
||||
]; |
||||
|
||||
_.each(replacements, r => { |
||||
let k = r[0]; |
||||
let v = r[1]; |
||||
if(query.search(k)) { |
||||
query = query.replace(k, v); |
||||
} else { |
||||
query += v; |
||||
} |
||||
}); |
||||
|
||||
return { |
||||
url: `${this.datasource.url}?${query}`, |
||||
method: 'GET', |
||||
schema: { |
||||
params: this.datasource.params |
||||
} |
||||
} |
||||
} |
||||
|
||||
getResults(res): MetricResults { |
||||
|
||||
if(res.data === undefined || res.data.length < 1) { |
||||
console.log('datasource return empty response, no data'); |
||||
return { |
||||
columns: ['timestamp', 'target'], |
||||
values: [] |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
columns: ['timestamp', res.data[0]['target']], |
||||
values: res.data[0].datapoints.map(point => { |
||||
let val = point[0]; |
||||
let timestamp = point[1] * 1000; //convert seconds -> ms
|
||||
return [timestamp, val]; |
||||
}) |
||||
}; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,63 @@
|
||||
import { AbstractMetric, Datasource, MetricId, MetricQuery, MetricResults } from "./metric"; |
||||
import { processSQLLimitOffset } from './utils'; |
||||
|
||||
|
||||
const INFLUX_QUERY_TIME_REGEX = /time ?[><=]+ ?[^A-Z]+(AND ?time ?[><=]+ ?[^A-Z]+)?/; |
||||
|
||||
|
||||
export class InfluxdbMetric extends AbstractMetric { |
||||
|
||||
private _queryParts: string[]; |
||||
|
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
super(datasource, targets, id); |
||||
|
||||
var queryStr = datasource.params.q; |
||||
this._queryParts = queryStr.split(INFLUX_QUERY_TIME_REGEX); |
||||
if(this._queryParts.length == 1) { |
||||
throw new Error( |
||||
`Query "${queryStr}" is not replaced with LIMIT/OFFSET oeprators. Missing time clause.` |
||||
); |
||||
} |
||||
if(this._queryParts.length > 3) { |
||||
throw new Error(`Query "${queryStr}" has multiple time clauses. Can't parse.`); |
||||
} |
||||
} |
||||
|
||||
getQuery(from: number, to: number, limit: number, offset: number): MetricQuery { |
||||
let timeClause = `time >= ${from}ms AND time <= ${to}ms`; |
||||
let q = `${this._queryParts[0]} ${timeClause} ${this._queryParts[2]}`; |
||||
q = processSQLLimitOffset(q, limit, offset); |
||||
return { |
||||
url: this.datasource.url, |
||||
method: 'GET', |
||||
schema: { |
||||
params: { |
||||
q, |
||||
db: this.datasource.params.db, |
||||
epoch: this.datasource.params.epoch |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
getResults(res): MetricResults { |
||||
let emptyResult = { |
||||
columns: ['timestamp', 'target'], |
||||
values: [] |
||||
}; |
||||
|
||||
if(res.data === undefined || res.data.results.length < 1) { |
||||
console.log('datasource return empty response, no data'); |
||||
return emptyResult; |
||||
} |
||||
|
||||
// TODO: support more than 1 metric (each res.data.results item is a metric)
|
||||
let results = res.data.results[0]; |
||||
if (results.series === undefined) { |
||||
return emptyResult; |
||||
} |
||||
|
||||
return results.series[0]; |
||||
} |
||||
} |
@ -0,0 +1,39 @@
|
||||
export declare type Datasource = { |
||||
url: string; |
||||
type: string; |
||||
params?: { |
||||
db: string; |
||||
q: string; |
||||
epoch: string; |
||||
}; |
||||
data?: any; |
||||
}; |
||||
|
||||
export type MetricQuery = { |
||||
url: string; |
||||
method: string; |
||||
schema: any; |
||||
headers?: any; |
||||
} |
||||
|
||||
export type MetricResults = { |
||||
values: any; |
||||
columns: any; |
||||
} |
||||
|
||||
export type MetricId = string; |
||||
|
||||
export abstract class AbstractMetric { |
||||
constructor( |
||||
public datasource: Datasource, |
||||
public targets: any[], |
||||
public id?: MetricId |
||||
) {}; |
||||
abstract getQuery(from: number, to: number, limit: number, offset: number): MetricQuery; |
||||
/* |
||||
from / to - timestamp in ms |
||||
limit - max number of items in result |
||||
offset - number of items to skip from timerange start |
||||
*/ |
||||
abstract getResults(res): MetricResults; |
||||
} |
@ -0,0 +1,77 @@
|
||||
import { InfluxdbMetric } from './influxdb_metric'; |
||||
import { GraphiteMetric } from './graphite_metric'; |
||||
import { AbstractMetric, Datasource, MetricId } from './metric'; |
||||
import { PrometheusMetric } from './prometheus_metric'; |
||||
import { PostgresMetric } from './postgres_metric'; |
||||
import { ElasticsearchMetric } from './elasticsearch_metric'; |
||||
|
||||
|
||||
export function metricFactory( |
||||
datasource: Datasource, |
||||
targets: any[], |
||||
id?: MetricId |
||||
): AbstractMetric { |
||||
|
||||
let classMap = { |
||||
'influxdb': InfluxdbMetric, |
||||
'graphite': GraphiteMetric, |
||||
'prometheus': PrometheusMetric, |
||||
'postgres': PostgresMetric, |
||||
'elasticsearch': ElasticsearchMetric |
||||
}; |
||||
if(classMap[datasource.type] === undefined) { |
||||
console.error(`Datasources of type ${datasource.type} are not supported currently`); |
||||
throw new Error(`Datasources of type ${datasource.type} are not supported currently`); |
||||
} else { |
||||
return new classMap[datasource.type](datasource, targets, id); |
||||
} |
||||
} |
||||
|
||||
export class Metric { |
||||
datasource: Datasource; |
||||
targets: any[]; |
||||
id?: MetricId; |
||||
private _metricQuery: AbstractMetric = undefined; |
||||
|
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
if(datasource === undefined) { |
||||
throw new Error('datasource is undefined'); |
||||
} |
||||
if(targets === undefined) { |
||||
throw new Error('targets is undefined'); |
||||
} |
||||
if(targets.length === 0) { |
||||
throw new Error('targets is empty'); |
||||
} |
||||
this.datasource = datasource; |
||||
this.targets = targets; |
||||
this.id = id; |
||||
} |
||||
|
||||
public get metricQuery() { |
||||
if(this._metricQuery === undefined) { |
||||
this._metricQuery = metricFactory(this.datasource, this.targets, this.id); |
||||
} |
||||
return this._metricQuery; |
||||
} |
||||
|
||||
|
||||
public toObject() { |
||||
return { |
||||
datasource: this.datasource, |
||||
targets: this.targets, |
||||
_id: this.id |
||||
}; |
||||
} |
||||
|
||||
static fromObject(obj: any): Metric { |
||||
if(obj === undefined) { |
||||
throw new Error('obj is undefined'); |
||||
} |
||||
return new Metric( |
||||
obj.datasource, |
||||
obj.targets, |
||||
obj._id |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,63 @@
|
||||
import { AbstractMetric, Datasource, MetricId, MetricQuery, MetricResults } from './metric'; |
||||
import { processSQLLimitOffset } from './utils'; |
||||
|
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
export class PostgresMetric extends AbstractMetric { |
||||
|
||||
private _targetName: string; //save first target name, while multi metric not implemented
|
||||
|
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
super(datasource, targets, id); |
||||
|
||||
if(targets.length === 0) { |
||||
throw Error('got empty targets list'); |
||||
} |
||||
this._targetName = targets[0].refId; |
||||
} |
||||
|
||||
getQuery(from: number, to: number, limit: number, offset: number): MetricQuery { |
||||
let queries = this.datasource.data.queries; |
||||
_.forEach(queries, q => { |
||||
q.rawSql = processSQLLimitOffset(q.rawSql, limit, offset); |
||||
}); |
||||
return { |
||||
url: this.datasource.url, |
||||
method: 'POST', |
||||
schema: { |
||||
data: { |
||||
from: String(from), |
||||
to: String(to), |
||||
queries: queries |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
getResults(res): MetricResults { |
||||
let emptyResult = { |
||||
columns: ['timestamp', 'target'], |
||||
values: [] |
||||
}; |
||||
|
||||
if(res.data === undefined || res.data.results.length < 1) { |
||||
console.log('datasource return empty response, no data'); |
||||
return emptyResult; |
||||
} |
||||
|
||||
// TODO: support more than 1 metric (each res.data.results item is a metric)
|
||||
let results = res.data.results[this._targetName]; |
||||
if (results.series === undefined) { |
||||
return emptyResult; |
||||
} |
||||
|
||||
let points = results.series[0].points; |
||||
points.forEach(p => p.reverse()); |
||||
|
||||
return { |
||||
columns: ['timestamp', results.series[0].name], |
||||
values: points |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,83 @@
|
||||
import { AbstractMetric, Datasource, MetricId, MetricQuery, MetricResults } from './metric'; |
||||
|
||||
|
||||
const QUERY_TIME_REGEX = /\&start=[^\&]*\&end=[^\&]*\&/; |
||||
|
||||
export class PrometheusMetric extends AbstractMetric { |
||||
|
||||
constructor(datasource: Datasource, targets: any[], id?: MetricId) { |
||||
super(datasource, targets, id); |
||||
} |
||||
|
||||
getQuery(from: number, to: number, limit: number, offset: number): MetricQuery { |
||||
let url = this.datasource.url; |
||||
from = Math.floor(from / 1000); //prometheus uses seconds for timestamp
|
||||
to = Math.floor(to / 1000); |
||||
|
||||
url = url.replace(/\&start=[^\&]+/, `&start=${from}`); |
||||
url = url.replace(/\&end=[^\&]+/, `&end=${to}`); |
||||
return { |
||||
url, |
||||
method: 'GET', |
||||
schema: { |
||||
params: this.datasource.params |
||||
} |
||||
} |
||||
} |
||||
|
||||
getResults(res): MetricResults { |
||||
|
||||
if(res.data === undefined || res.data.data.result.length < 1) { |
||||
console.log('datasource return empty response, no data'); |
||||
return { |
||||
columns: ['timestamp', 'target'], |
||||
values: [] |
||||
}; |
||||
} |
||||
|
||||
let result = res.data.data.result; |
||||
let result_matrix = { |
||||
columns: ['timestamp'], |
||||
values: [] |
||||
}; |
||||
|
||||
result.map(r => { |
||||
let keys = []; |
||||
for(let key in r.metric) { |
||||
keys.push(`${key}=${r.metric[key]}`); |
||||
} |
||||
result_matrix.columns.push(keys.join(':')); |
||||
}); |
||||
|
||||
let values = result.map(r => r.values); |
||||
|
||||
let timestamps = []; |
||||
values.map(v => v.map(row => timestamps.push(row[0]))); |
||||
timestamps = timestamps.filter(function(item, i, ar) { |
||||
return ar.indexOf(item) === i; //uniq values
|
||||
}); |
||||
|
||||
for(let t of timestamps) { |
||||
let row = [t]; |
||||
values.map(v => { |
||||
if(v[0] === undefined) { |
||||
row.push(0); |
||||
} |
||||
|
||||
let currentTimestamp = v[0][0]; |
||||
let currentValue = v[0][1]; |
||||
|
||||
if(currentTimestamp === t) { |
||||
row.push(+currentValue); |
||||
v.shift(); |
||||
} |
||||
else { |
||||
row.push(null); |
||||
} |
||||
}); |
||||
row[0] = +row[0] * 1000; //convert timestamp to ms
|
||||
result_matrix.values.push(row); |
||||
}; |
||||
return result_matrix; |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
export function processSQLLimitOffset(sql: string, limit: number, offset: number): string { |
||||
let splits = sql.split(';'); |
||||
if(splits.length > 1 && splits[1] !== '' ) { |
||||
throw Error('multiple metrics currently not supported'); |
||||
} |
||||
sql = splits[0]; // removes ";" from EOL
|
||||
|
||||
let relim = /limit [0-9]+/ig; |
||||
let reoff = /offset [0-9]+/ig; |
||||
|
||||
let limIdx = ensureParentheses(relim, sql); |
||||
if(limIdx.index !== -1) { |
||||
sql = `${sql.slice(0, limIdx.index)}LIMIT ${limit}${sql.slice(limIdx.index + limIdx.length)}`; |
||||
} else { |
||||
sql += ` LIMIT ${limit}`; |
||||
} |
||||
|
||||
let offIdx = ensureParentheses(reoff, sql); |
||||
if(offIdx.index !== -1) { |
||||
sql = `${sql.slice(0, offIdx.index)}OFFSET ${offset}${sql.slice(offIdx.index + offIdx.length)}`; |
||||
} else { |
||||
sql += ` OFFSET ${offset}`; |
||||
} |
||||
|
||||
if(splits.length === 2) { |
||||
sql += ';'; |
||||
} |
||||
return sql; |
||||
} |
||||
|
||||
function ensureParentheses(regex: RegExp, str: string): { index: number, length: number } { |
||||
let occurence: RegExpExecArray; |
||||
while((occurence = regex.exec(str)) !== null) { |
||||
let leftPart = str.slice(0, occurence.index) |
||||
let rightPart = str.slice(occurence.index + occurence[0].length); |
||||
|
||||
let leftPairing = (leftPart.match(/\(/g) || []).length === (leftPart.match(/\)/g) || []).length; |
||||
let rightPairing = (rightPart.match(/\(/g) || []).length === (rightPart.match(/\)/g) || []).length; |
||||
|
||||
if(leftPairing && rightPairing) { |
||||
return { index: occurence.index, length: occurence[0].length }; |
||||
} |
||||
} |
||||
return { index: -1, length: 0 }; |
||||
} |
Loading…
Reference in new issue