rozetko
2 years ago
14 changed files with 265 additions and 185 deletions
@ -1,134 +0,0 @@
|
||||
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; |
||||
} |
@ -1,4 +1,69 @@
|
||||
import { MetricResults, QueryType } from './metrics/metric'; |
||||
import { Metric } from './metrics/metrics_factory'; |
||||
import { queryDirect } from './services/direct_service'; |
||||
import { queryGrafana } from './services/grafana_service'; |
||||
import { BadRange } from './types'; |
||||
|
||||
export { Metric } from './metrics/metrics_factory'; |
||||
export { Datasource } from './metrics/metric' |
||||
// TODO: move queryByMetric from Grafana service
|
||||
export { queryByMetric, GrafanaUnavailable, DatasourceUnavailable } from './grafana_service'; |
||||
export { DatasourceUnavailable } from './types'; |
||||
export { GrafanaUnavailable } from './services/grafana_service'; |
||||
|
||||
|
||||
const CHUNK_SIZE = 50000; |
||||
|
||||
|
||||
/** |
||||
* @param metric to query to Grafana |
||||
* @returns { values: [time, value][], columns: string[] } |
||||
*/ |
||||
export async function queryByMetric( |
||||
// TODO: check how did we wanna use `url` field
|
||||
metric: Metric, url: string, from: number, to: number, queryType: QueryType, |
||||
// TODO: we need an abstract DatasourceConfig class which will differ in direct and grafana queries
|
||||
apiKey?: string |
||||
): Promise<MetricResults> { |
||||
|
||||
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`); |
||||
} |
||||
|
||||
let data: MetricResults = { |
||||
values: [], |
||||
columns: [] |
||||
}; |
||||
|
||||
while(true) { |
||||
let query = metric.metricQuery.getQuery(from, to, CHUNK_SIZE, data.values.length); |
||||
let res: any; |
||||
// TODO: use polymorphic `query` method instead
|
||||
switch(queryType) { |
||||
case QueryType.GRAFANA: |
||||
res = await queryGrafana(query, apiKey as string, metric.datasource); |
||||
break; |
||||
case QueryType.DIRECT: |
||||
res = await queryDirect(query, metric.datasource); |
||||
break; |
||||
default: |
||||
throw new Error(`Unknown query type: ${queryType}`); |
||||
} |
||||
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; |
||||
} |
||||
|
@ -0,0 +1,48 @@
|
||||
import { DatasourceUnavailable } from '../types'; |
||||
import { Datasource, MetricQuery } from '../metrics/metric'; |
||||
|
||||
import axios from 'axios'; |
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
// TODO: support direct queries auth
|
||||
// TODO: move to class and inherit from QueryService abstract class
|
||||
export async function queryDirect(query: MetricQuery, datasource: Datasource) { |
||||
let axiosQuery = { |
||||
url: query.url, |
||||
method: query.method, |
||||
}; |
||||
console.log(axiosQuery) |
||||
|
||||
_.defaults(axiosQuery, query.schema); |
||||
|
||||
try { |
||||
return axios(axiosQuery); |
||||
} catch (e) { |
||||
// TODO: seems like this error handler can be used for both Grafana and Direct queries
|
||||
const msg = `TSDB-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.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 credentials. ${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); |
||||
} |
||||
} |
@ -0,0 +1,77 @@
|
||||
import { Datasource, MetricQuery } from '../metrics/metric'; |
||||
import { TsdbKitError, DatasourceUnavailable } from '../types'; |
||||
|
||||
import axios from 'axios'; |
||||
import * as _ from 'lodash'; |
||||
|
||||
|
||||
export class GrafanaUnavailable extends TsdbKitError { }; |
||||
|
||||
// TODO: move to class and inherit from QueryService abstract class
|
||||
export async function queryGrafana(query: MetricQuery, apiKey: string, datasource: Datasource) { |
||||
let headers = { Authorization: `Bearer ${apiKey}` }; |
||||
|
||||
const grafanaUrl = getGrafanaUrl(query.url); |
||||
query.url = `${grafanaUrl}/${query.url}`; |
||||
|
||||
if(query.headers !== undefined) { |
||||
_.merge(headers, query.headers); |
||||
} |
||||
|
||||
let axiosQuery = { |
||||
headers, |
||||
url: query.url, |
||||
method: query.method, |
||||
}; |
||||
|
||||
_.defaults(axiosQuery, query.schema); |
||||
|
||||
try { |
||||
return axios(axiosQuery); |
||||
} catch (e) { |
||||
// TODO: seems like this error handler can be used for both Grafana and Direct queries
|
||||
const msg = `TSDB-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); |
||||
} |
||||
} |
||||
|
||||
function getGrafanaUrl(url: string): 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,15 @@
|
||||
import { DatasourceType } from './metrics/metric'; |
||||
|
||||
|
||||
export class TsdbKitError extends Error { |
||||
constructor( |
||||
message: string, |
||||
public datasourceType?: DatasourceType, |
||||
public datasourceUrl?: string |
||||
) { |
||||
super(message); |
||||
} |
||||
}; |
||||
|
||||
export class BadRange extends TsdbKitError {}; |
||||
export class DatasourceUnavailable extends TsdbKitError {}; |
Loading…
Reference in new issue