blob: ea621e9f5d6cc7a723a6251ca6d6379abfbfee82 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import defaults from 'lodash/defaults';
import {
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
MutableDataFrame,
FieldType,
ArrayVector,
NodeGraphDataFrameFieldNames,
} from '@grafana/data';
import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import {
MyQuery,
MyDataSourceOptions,
DEFAULT_QUERY,
DurationTime,
MetricData,
Call,
Node,
Recordable
} from './types';
import { Fragments, RoutePath, TimeType, Calculations } from './constant';
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
URL: string;
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
super(instanceSettings);
// proxy url
this.URL = instanceSettings.url || '';
dayjs.extend(utc)
dayjs.extend(timezone)
}
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
const { range } = options;
const from = range!.from.valueOf();
const to = range!.to.valueOf();
const v = {
query: Fragments.utc,
variables: {},
};
const resp = await this.doRequest(v);
const utc = resp.data.getTimeInfo.timezone / 100;
const dates = this.timeFormat([this.getLocalTime(utc, new Date(from)), this.getLocalTime(utc, new Date(to))]);
const duration = {
start: this.dateFormatStep(dates.start, dates.step),
end: this.dateFormatStep(dates.end, dates.step),
step: dates.step,
};
const promises = options.targets.map(async (target) => {
const query = defaults(target, DEFAULT_QUERY);
const layer = getTemplateSrv().replace(query.layer, options.scopedVars);
const serviceName = getTemplateSrv().replace(query.service, options.scopedVars);
if (!layer && !serviceName) {
return [];
}
const nodeMetricsStr = getTemplateSrv().replace(query.nodeMetrics, options.scopedVars);
const nodeMetrics = nodeMetricsStr ? this.parseMetrics(nodeMetricsStr) : [];
const edgeMetricsStr = getTemplateSrv().replace(query.edgeMetrics, options.scopedVars);
const edgeMetrics = edgeMetricsStr ? this.parseMetrics(edgeMetricsStr) : [];
let services = [];
// fetch services from api
const s = {
query: Fragments.services,
variables: {duration, keyword: ''},
};
const resp = await this.doRequest(s);
services = resp.data.services || [];
const t: {
query: string;
variables: Recordable;
} = {
query: Fragments.servicesTopolgy,
variables: { duration },
};
let serviceObj;
if (serviceName) {
serviceObj = services.filter((d: {name: string, id: string}) => d.name === serviceName);
} else {
serviceObj = services.filter((d: {layers: string[], id: string}) => d.layers.includes(layer));
}
t.variables.serviceIds = serviceObj.map((d: {layers: string[], id: string}) => d.id);
// fetch topology data from api
const res = await this.doRequest(t);
if (!res.data.topology) {
throw new Error('Layer or service is not found');
}
const {nodes, calls} = this.setTopologyData({nodes: res.data.topology.nodes || [], calls: res.data.topology.calls || []});
const idsS = calls.filter((i: Call) => i.detectPoints.includes('SERVER')).map((b: Call) => b.id);
const idsC = calls.filter((i: Call) => i.detectPoints.includes('CLIENT')).map((b: Call) => b.id);
const serverMetrics = [];
const clientMetrics = [];
for (const m of edgeMetrics) {
if (m.type === 'SERVER') {
serverMetrics.push(m);
} else {
clientMetrics.push(m);
}
}
const ids = nodes.map((d: Node) => d.id);
// fetch topology metrics from api
const nodeMetricsResp = nodeMetrics.length ? await this.queryMetrics(nodeMetrics, ids, duration) : null;
const edgeServerMetricsResp = serverMetrics.length && idsS.length ? await this.queryMetrics(serverMetrics, idsS, duration) : null;
const edgeClientMetricsResp = clientMetrics.length && idsC.length ? await this.queryMetrics(clientMetrics, idsC, duration) : null;
const edgeMetricsResp: any = (edgeServerMetricsResp || edgeClientMetricsResp) ? {
data: {...(edgeServerMetricsResp && edgeServerMetricsResp.data || {}), ...(edgeClientMetricsResp && edgeClientMetricsResp.data || {})}
} : null;
const fieldTypes = this.setFieldTypes({
nodes,
calls,
nodeMetrics: nodeMetricsResp ? {...nodeMetricsResp, config: nodeMetrics} : undefined,
edgeMetrics: edgeMetricsResp ? {...edgeMetricsResp, config: edgeMetrics} : undefined,
});
const nodeFrame = new MutableDataFrame({
name: 'Nodes',
refId: target.refId,
fields: fieldTypes.nodeFieldTypes,
meta: {
preferredVisualisationType: 'nodeGraph',
}
});
const edgeFrame = new MutableDataFrame({
name: 'Edges',
refId: target.refId,
fields: fieldTypes.edgeFieldTypes,
meta: {
preferredVisualisationType: 'nodeGraph',
}
});
return [nodeFrame, edgeFrame];
});
return Promise.all(promises).then(data => ({ data: data[0] }));
}
async doRequest(params?: Recordable) {
// Do the request on proxy; the server will replace url + routePath with the url
// defined in plugin.json
const result = getBackendSrv().post(`${this.URL}${RoutePath}`, params);
return result;
}
parseMetrics(params: string) {
const regex = /{[^}]+}/g;
const arr = params.match(regex);
const metrics = arr?.map((d: string) => JSON.parse(d)) || [];
return metrics;
}
async queryMetrics(params: MetricData[], ids: string[], duration: DurationTime) {
const names = params.map((d: MetricData) => d.name);
const m = this.queryTopologyMetrics(names, ids, duration);
const metricJson = await this.doRequest(m);
return metricJson;
}
setTopologyData(params: {nodes: Node[], calls: Call[]}) {
if (!(params.nodes.length && params.calls.length)) {
return {nodes: [], calls: []}
}
const obj = {} as Recordable;
const nodes = (params.nodes || []).reduce((prev: Node[], next: Node) => {
if (!obj[next.id]) {
obj[next.id] = true;
prev.push(next);
}
return prev;
}, []);
const calls = (params.calls || []).reduce((prev: Call[], next: Call) => {
if (!obj[next.id]) {
obj[next.id] = true;
prev.push(next);
}
return prev;
}, []);
return {nodes, calls}
}
setFieldTypes(params: {nodes: Node[], calls: Call[], nodeMetrics: Recordable, edgeMetrics: Recordable}) {
const nodeMetrics = params.nodeMetrics || {config: [], data: {}};
const edgeMetrics = params.edgeMetrics || {config: [], data: {}};
if (!(params.nodes && params.calls)) {
return {nodeFieldTypes: [], edgeFieldTypes: []};;
}
const nodeFieldTypes = this.getNodeTypes(params.nodes || [], nodeMetrics);
const edgeFieldTypes = this.getEdgeTypes(params.calls || [], edgeMetrics);
return {nodeFieldTypes, edgeFieldTypes};
}
getNodeTypes(nodes: Node[], metrics: Recordable) {
const idField = { name: NodeGraphDataFrameFieldNames.id, type: FieldType.string, values: new ArrayVector(), config: {}};
const titleField = { name: NodeGraphDataFrameFieldNames.title, type: FieldType.string, values: new ArrayVector(), config: {}};
const mainStatField = { name: NodeGraphDataFrameFieldNames.mainStat, type: FieldType.number, values: new ArrayVector(), config: {}};
const secondaryStatField = { name: NodeGraphDataFrameFieldNames.secondaryStat, type: FieldType.number, values: new ArrayVector(), config: {}};
const detailsFields: any = [];
for (let i = 0; i < metrics.config.length; i++) {
const c = metrics.config[i];
const config = {displayName: c.label, unit: c.unit};
if (i === 0) {
mainStatField.config = config;
} else if (i === 1) {
secondaryStatField.config = config;
} else {
detailsFields.push({
name: `${NodeGraphDataFrameFieldNames.detail}${c.name}`,
type: FieldType.number,
values: new ArrayVector(),
config: {displayName: `${c.label || c.name } ${c.unit || ''}`}
});
}
}
for (const node of nodes) {
idField.values.add(node.id || '');
titleField.values.add(node.name);
for (let i = 0; i < metrics.config.length; i++) {
const item = metrics.config[i];
const m = (metrics.data[item.name].values).find((v: {id: string}) => v.id === node.id) || {isEmptyValue: true};
const value = m.isEmptyValue ? NaN : this.expression(Number(m.value), item.calculation);
if(i > 1) {
detailsFields[i - 2]?.values.add(Number(value));
} else {
if (i === 0) {
mainStatField.values.add(Number(value));
}
if (i === 1) {
secondaryStatField.values.add(Number(value));
}
}
}
}
return [idField, titleField, mainStatField, secondaryStatField, ...detailsFields];
}
getEdgeTypes(calls: Call[], metrics: Recordable) {
const idField = { name: NodeGraphDataFrameFieldNames.id, type: FieldType.string, values: new ArrayVector(), config: {}};
const targetField = { name: NodeGraphDataFrameFieldNames.target, type: FieldType.string, values: new ArrayVector(), config: {}};
const sourceField = { name: NodeGraphDataFrameFieldNames.source, type: FieldType.string, values: new ArrayVector(), config: {}};
const mainStatField = { name: NodeGraphDataFrameFieldNames.mainStat, type: FieldType.number, values: new ArrayVector(), config: {}};
const secondaryStatField = { name: NodeGraphDataFrameFieldNames.secondaryStat, type: FieldType.number, values: new ArrayVector(), config: {}};
const detailsFields: any = [];
for (let i = 0; i < metrics.config.length; i++) {
const k = metrics.config[i];
const config = {displayName: k.label, unit: k.unit};
if (i === 0) {
mainStatField.config = config;
}
else if (i === 1) {
secondaryStatField.config = config;
} else {
detailsFields.push({
name: `${NodeGraphDataFrameFieldNames.detail}${k.name}`,
type: FieldType.number,
values: new ArrayVector(),
config: {displayName: `${k.label || k.name } ${k.unit || ''}`}
});
}
}
for (const call of calls) {
idField.values.add(call.id);
targetField.values.add(call.target);
sourceField.values.add(call.source);
for (let i = 0; i < metrics.config.length; i++) {
const item = metrics.config[i];
const m = (metrics.data[item.name].values).find((v: {id: string}) => v.id === call.id) || {isEmptyValue: true};
const value = m.isEmptyValue ? NaN : this.expression(Number(m.value), item.calculation);
if (i === 0) {
mainStatField.values.add(Number(value));
} else if (i === 1) {
secondaryStatField.values.add(Number(value));
} else {
detailsFields[i - 2]?.values.add(Number(value));
}
}
}
return [idField, targetField, sourceField, mainStatField, secondaryStatField, ...detailsFields];
}
expression(val: number, calculation: string): number | string {
let data: number | string = Number(val);
switch (calculation) {
case Calculations.Percentage:
data = (val / 100).toFixed(2);
break;
case Calculations.PercentageAvg:
data = (val / 100).toFixed(2);
break;
case Calculations.ByteToKB:
data = (val / 1024).toFixed(2);
break;
case Calculations.ByteToMB:
data = (val / 1024 / 1024).toFixed(2);
break;
case Calculations.ByteToGB:
data = (val / 1024 / 1024 / 1024).toFixed(2);
break;
case Calculations.Apdex:
data = (val / 10000).toFixed(2);
break;
case Calculations.ConvertSeconds:
data = dayjs(val * 1000).format('YYYY-MM-DD HH:mm:ss');
break;
case Calculations.ConvertMilliseconds:
data = dayjs(val).format('YYYY-MM-DD HH:mm:ss');
break;
case Calculations.MsToS:
data = (val / 1000).toFixed(2);
break;
case Calculations.SecondToDay:
data = (val / 86400).toFixed(2);
break;
case Calculations.NanosecondToMillisecond:
data = (val / 1000 / 1000).toFixed(2);
break;
case Calculations.ApdexAvg:
data = (val / 10000).toFixed(2);
break;
default:
data = data;
break;
}
return data;
}
queryTopologyMetrics(metrics: string[], ids: string[], duration: DurationTime) {
const conditions: { [key: string]: unknown } = {
duration,
ids,
};
const variables: string[] = [`$duration: Duration!`, `$ids: [ID!]!`];
const fragmentList = metrics.map((d: string, index: number) => {
conditions[`m${index}`] = d;
variables.push(`$m${index}: String!`);
return `${d}: getValues(metric: {
name: $m${index}
ids: $ids
}, duration: $duration) {
values {
id
value
}
}`;
});
const query = `query queryData(${variables}) {${fragmentList.join(" ")}}`;
return { query, variables: conditions };
}
timeFormat(time: Date[]) {
let step: TimeType;
const unix = Math.round(time[1].getTime()) - Math.round(time[0].getTime());
if (unix <= 60 * 60 * 1000) {
step = TimeType.MINUTE_TIME;
} else if (unix <= 24 * 60 * 60 * 1000) {
step = TimeType.HOUR_TIME;
} else {
step = TimeType.DAY_TIME;
}
return { start: time[0], end: time[1], step };
}
dateFormatStep(date: Date, step: string, monthDayDiff?: boolean): string {
const year = date.getFullYear();
const monthTemp = date.getMonth() + 1;
let month = `${monthTemp}`;
if (monthTemp < 10) {
month = `0${monthTemp}`;
}
if (step === 'MONTH' && monthDayDiff) {
return `${year}-${month}`;
}
const dayTemp = date.getDate();
let day = `${dayTemp}`;
if (dayTemp < 10) {
day = `0${dayTemp}`;
}
if (step === 'DAY' || step === 'MONTH') {
return `${year}-${month}-${day}`;
}
const hourTemp = date.getHours();
let hour = `${hourTemp}`;
if (hourTemp < 10) {
hour = `0${hourTemp}`;
}
if (step === 'HOUR') {
return `${year}-${month}-${day} ${hour}`;
}
const minuteTemp = date.getMinutes();
let minute = `${minuteTemp}`;
if (minuteTemp < 10) {
minute = `0${minuteTemp}`;
}
if (step === 'MINUTE') {
return `${year}-${month}-${day} ${hour}${minute}`;
}
const secondTemp = date.getSeconds();
let second = `${secondTemp}`;
if (secondTemp < 10) {
second = `0${secondTemp}`;
}
if (step === 'SECOND') {
return `${year}-${month}-${day} ${hour}${minute}${second}`;
}
return '';
}
getLocalTime(utc: number, time: Date): Date {
const d = new Date(time);
const len = d.getTime();
const offset = d.getTimezoneOffset() * 60000;
const utcTime = len + offset;
return new Date(utcTime + 3600000 * utc);
}
async testDatasource() {
// Implement a health check for your data source.
const defaultErrorMessage = 'Cannot connect to API';
try {
const v = {
query: Fragments.version,
variables: {},
};
const resp = await this.doRequest(v);
if (resp.data && resp.data.version !== undefined) {
return {
status: 'success',
message: 'Success',
};
} else {
return {
status: 'error',
message: resp.statusText ? resp.statusText : defaultErrorMessage,
};
}
} catch(err: any) {
const toString = Object.prototype.toString;
if (toString.call(err) === `[object String]`) {
return {
status: 'error',
message: err,
};
} else {
let message = '';
message += err.statusText ? err.statusText : defaultErrorMessage;
if (err.data && err.data.error && err.data.error.code) {
message += ': ' + err.data.error.code + '. ' + err.data.error.message;
}
return {
status: 'error',
message,
};
}
}
}
}