|
|
@ -1,7 +1,6 @@ |
|
|
|
import { Target } from '../models/target'; |
|
|
|
|
|
|
|
import { URL } from 'url'; |
|
|
|
import { URL } from 'url'; |
|
|
|
import { apiKeys } from '../config'; |
|
|
|
import { apiKeys } from '../config'; |
|
|
|
import { promisify } from '../utils'; |
|
|
|
import { promisify, toIsoString } from '../utils'; |
|
|
|
import { DashboardQuery, ExportProgress, ExportStatus, ExportTask } from '../types'; |
|
|
|
import { DashboardQuery, ExportProgress, ExportStatus, ExportTask } from '../types'; |
|
|
|
|
|
|
|
|
|
|
|
import { QueryConfig, queryByConfig } from '@corpglory/tsdb-kit'; |
|
|
|
import { QueryConfig, queryByConfig } from '@corpglory/tsdb-kit'; |
|
|
@ -27,29 +26,26 @@ const DEFAULT_PROGRESS = { |
|
|
|
export class Exporter { |
|
|
|
export class Exporter { |
|
|
|
private _task: ExportTask; |
|
|
|
private _task: ExportTask; |
|
|
|
|
|
|
|
|
|
|
|
public async export(task: ExportTask, datasourceUrl: string) { |
|
|
|
public async export(task: ExportTask, datasourceUrl: string, timeZoneName: string) { |
|
|
|
|
|
|
|
try { |
|
|
|
this._task = _.cloneDeep(task); |
|
|
|
this._task = _.cloneDeep(task); |
|
|
|
this._task.id = uuidv4(); |
|
|
|
this._task.id = uuidv4(); |
|
|
|
this._task.progress = _.cloneDeep(DEFAULT_PROGRESS); |
|
|
|
this._task.progress = _.cloneDeep(DEFAULT_PROGRESS); |
|
|
|
|
|
|
|
|
|
|
|
const targets = task.queries.map((query: DashboardQuery) => new Target( |
|
|
|
this._validateQueries(task.queries); |
|
|
|
query.panel, |
|
|
|
this._validateDatasourceUrl(datasourceUrl); |
|
|
|
query.datasource, |
|
|
|
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._validateTargets(datasourceUrl, targets); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await this._updateProgress(); |
|
|
|
await this._updateProgress(); |
|
|
|
|
|
|
|
|
|
|
|
const queryConfigs = targets.map( |
|
|
|
const queryConfigs = task.queries.map( |
|
|
|
target => |
|
|
|
query => |
|
|
|
new QueryConfig( |
|
|
|
new QueryConfig( |
|
|
|
QueryType.GRAFANA, |
|
|
|
QueryType.GRAFANA, |
|
|
|
{ |
|
|
|
{ |
|
|
|
...target.datasource, |
|
|
|
...query.datasource, |
|
|
|
url: datasourceUrl |
|
|
|
url: datasourceUrl, |
|
|
|
}, |
|
|
|
}, |
|
|
|
target.panel.targets |
|
|
|
[query.target] |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
@ -60,7 +56,6 @@ export class Exporter { |
|
|
|
console.log(`Total days: ${days}`); |
|
|
|
console.log(`Total days: ${days}`); |
|
|
|
|
|
|
|
|
|
|
|
const stream = this._initCsvStream(); |
|
|
|
const stream = this._initCsvStream(); |
|
|
|
try { |
|
|
|
|
|
|
|
for(let day = 0; day < days; day++) { |
|
|
|
for(let day = 0; day < days; day++) { |
|
|
|
to = from + MS_IN_DAY; |
|
|
|
to = from + MS_IN_DAY; |
|
|
|
|
|
|
|
|
|
|
@ -75,21 +70,40 @@ export class Exporter { |
|
|
|
|
|
|
|
|
|
|
|
const datasourceMetrics = await queryByConfig(queryConfig, datasourceUrl, from, to, apiKey); |
|
|
|
const datasourceMetrics = await queryByConfig(queryConfig, datasourceUrl, from, to, apiKey); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(_.isEmpty(columns)) { |
|
|
|
columns = datasourceMetrics.columns; |
|
|
|
columns = datasourceMetrics.columns; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
columns = _.concat(columns, datasourceMetrics.columns.slice(1)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(_.isEmpty(values)) { |
|
|
|
values = datasourceMetrics.values; |
|
|
|
values = datasourceMetrics.values; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
if(values.length !== datasourceMetrics.values.length) { |
|
|
|
|
|
|
|
throw new Error(`All queries should return rows of the same lengths`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for(const rowIdx in values) { |
|
|
|
|
|
|
|
if(datasourceMetrics.values[rowIdx][0] !== values[rowIdx][0]) { |
|
|
|
|
|
|
|
throw new Error('Queries should return the same timestamps'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
values[rowIdx] = _.concat(values[rowIdx], datasourceMetrics.values[rowIdx].slice(1)); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
values = values.map((row: number[]) => [toIsoString(row[0], timeZoneName), ...row.slice(1)]); |
|
|
|
|
|
|
|
|
|
|
|
if(columns.length > 0) { |
|
|
|
if(columns.length > 0) { |
|
|
|
this._writeCsv(stream, { columns, values, }); |
|
|
|
this._writeCsv(stream, { columns, values }); |
|
|
|
} |
|
|
|
} |
|
|
|
await this._updateProgress({ status: ExportStatus.EXPORTING, progress: (day + 1) / days }); |
|
|
|
await this._updateProgress({ status: ExportStatus.EXPORTING, progress: (day + 1) / days }); |
|
|
|
|
|
|
|
|
|
|
|
from += MS_IN_DAY; |
|
|
|
from += MS_IN_DAY; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
stream.end(); |
|
|
|
} catch (e) { |
|
|
|
} catch (e) { |
|
|
|
await this._updateProgress({ status: ExportStatus.ERROR, errorMessage: e.message }); |
|
|
|
await this._updateProgress({ status: ExportStatus.ERROR, errorMessage: e.message }); |
|
|
|
} |
|
|
|
} |
|
|
|
stream.end(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private _initCsvStream() { |
|
|
|
private _initCsvStream() { |
|
|
@ -127,15 +141,22 @@ export class Exporter { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private _validateTargets(datasourceUrl: string, targets: Target[]) { |
|
|
|
private _validateQueries(queries: DashboardQuery[]) { |
|
|
|
if(!targets || !Array.isArray(targets)) { |
|
|
|
if(!_.isArray(queries)) { |
|
|
|
throw new Error('Incorrect targets format'); |
|
|
|
throw new Error('`queries` field is required and should be an array'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if(targets.length > 1) { |
|
|
|
for(const query of queries) { |
|
|
|
throw new Error(`Multiple queries are not supported yet`); |
|
|
|
if(_.isEmpty(query.datasource)) { |
|
|
|
|
|
|
|
throw new Error('all queries should have a `datasource` field'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if(_.isEmpty(query.target)) { |
|
|
|
|
|
|
|
throw new Error('all queries should have a `target` field'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private _validateDatasourceUrl(datasourceUrl: string) { |
|
|
|
const host = new URL(datasourceUrl).origin; |
|
|
|
const host = new URL(datasourceUrl).origin; |
|
|
|
const apiKey = apiKeys[host]; |
|
|
|
const apiKey = apiKeys[host]; |
|
|
|
|
|
|
|
|
|
|
@ -146,11 +167,6 @@ export class Exporter { |
|
|
|
|
|
|
|
|
|
|
|
private _writeCsv(stream, series) { |
|
|
|
private _writeCsv(stream, series) { |
|
|
|
for(let row of series.values) { |
|
|
|
for(let row of series.values) { |
|
|
|
const isEmpty = _.every( |
|
|
|
|
|
|
|
_.slice(row, 1), |
|
|
|
|
|
|
|
val => val === null |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
if(!isEmpty) { |
|
|
|
|
|
|
|
let csvRow = {}; |
|
|
|
let csvRow = {}; |
|
|
|
for(let col in series.columns) { |
|
|
|
for(let col in series.columns) { |
|
|
|
csvRow[series.columns[col]] = row[col]; |
|
|
|
csvRow[series.columns[col]] = row[col]; |
|
|
@ -159,7 +175,6 @@ export class Exporter { |
|
|
|
this._task.progress.exportedRowsCount++; |
|
|
|
this._task.progress.exportedRowsCount++; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private _getFilename(extension: string): string { |
|
|
|
private _getFilename(extension: string): string { |
|
|
|
return `${this._task.id}.${extension}`; |
|
|
|
return `${this._task.id}.${extension}`; |
|
|
|