datasource.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import { stackdriverUnitMappings } from './constants';
  2. import appEvents from 'app/core/app_events';
  3. import _ from 'lodash';
  4. import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
  5. import { StackdriverQuery, MetricDescriptor, StackdriverOptions } from './types';
  6. import { DataSourceApi, DataQueryRequest, DataSourceInstanceSettings } from '@grafana/ui';
  7. import { ScopedVars } from '@grafana/data';
  8. import { BackendSrv } from 'app/core/services/backend_srv';
  9. import { TemplateSrv } from 'app/features/templating/template_srv';
  10. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  11. export default class StackdriverDatasource extends DataSourceApi<StackdriverQuery, StackdriverOptions> {
  12. url: string;
  13. baseUrl: string;
  14. projectName: string;
  15. authenticationType: string;
  16. queryPromise: Promise<any>;
  17. metricTypes: any[];
  18. /** @ngInject */
  19. constructor(
  20. instanceSettings: DataSourceInstanceSettings<StackdriverOptions>,
  21. private backendSrv: BackendSrv,
  22. private templateSrv: TemplateSrv,
  23. private timeSrv: TimeSrv
  24. ) {
  25. super(instanceSettings);
  26. this.baseUrl = `/stackdriver/`;
  27. this.url = instanceSettings.url;
  28. this.projectName = instanceSettings.jsonData.defaultProject || '';
  29. this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
  30. this.metricTypes = [];
  31. }
  32. async getTimeSeries(options: any) {
  33. const queries = options.targets
  34. .filter((target: any) => {
  35. return !target.hide && target.metricType;
  36. })
  37. .map((t: any) => {
  38. return {
  39. refId: t.refId,
  40. intervalMs: options.intervalMs,
  41. datasourceId: this.id,
  42. metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
  43. crossSeriesReducer: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
  44. perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
  45. alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
  46. groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
  47. view: t.view || 'FULL',
  48. filters: this.interpolateFilters(t.filters, options.scopedVars),
  49. aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
  50. type: 'timeSeriesQuery',
  51. };
  52. });
  53. if (queries.length > 0) {
  54. const { data } = await this.backendSrv.datasourceRequest({
  55. url: '/api/tsdb/query',
  56. method: 'POST',
  57. data: {
  58. from: options.range.from.valueOf().toString(),
  59. to: options.range.to.valueOf().toString(),
  60. queries,
  61. },
  62. });
  63. return data;
  64. } else {
  65. return { results: [] };
  66. }
  67. }
  68. interpolateFilters(filters: string[], scopedVars: ScopedVars) {
  69. return (filters || []).map(f => {
  70. return this.templateSrv.replace(f, scopedVars || {}, 'regex');
  71. });
  72. }
  73. async getLabels(metricType: string, refId: string) {
  74. const response = await this.getTimeSeries({
  75. targets: [
  76. {
  77. refId: refId,
  78. datasourceId: this.id,
  79. metricType: this.templateSrv.replace(metricType),
  80. crossSeriesReducer: 'REDUCE_NONE',
  81. view: 'HEADERS',
  82. },
  83. ],
  84. range: this.timeSrv.timeRange(),
  85. });
  86. return response.results[refId];
  87. }
  88. interpolateGroupBys(groupBys: string[], scopedVars: {}): string[] {
  89. let interpolatedGroupBys: any[] = [];
  90. (groupBys || []).forEach(gb => {
  91. const interpolated = this.templateSrv.replace(gb, scopedVars || {}, 'csv').split(',');
  92. if (Array.isArray(interpolated)) {
  93. interpolatedGroupBys = interpolatedGroupBys.concat(interpolated);
  94. } else {
  95. interpolatedGroupBys.push(interpolated);
  96. }
  97. });
  98. return interpolatedGroupBys;
  99. }
  100. resolvePanelUnitFromTargets(targets: any[]) {
  101. let unit;
  102. if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
  103. if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
  104. // @ts-ignore
  105. unit = stackdriverUnitMappings[targets[0].unit];
  106. }
  107. }
  108. return unit;
  109. }
  110. async query(options: DataQueryRequest<StackdriverQuery>) {
  111. const result: any[] = [];
  112. const data = await this.getTimeSeries(options);
  113. if (data.results) {
  114. Object['values'](data.results).forEach((queryRes: any) => {
  115. if (!queryRes.series) {
  116. return;
  117. }
  118. const unit = this.resolvePanelUnitFromTargets(options.targets);
  119. queryRes.series.forEach((series: any) => {
  120. let timeSerie: any = {
  121. target: series.name,
  122. datapoints: series.points,
  123. refId: queryRes.refId,
  124. meta: queryRes.meta,
  125. };
  126. if (unit) {
  127. timeSerie = { ...timeSerie, unit };
  128. }
  129. result.push(timeSerie);
  130. });
  131. });
  132. return { data: result };
  133. } else {
  134. return { data: [] };
  135. }
  136. }
  137. async annotationQuery(options: any) {
  138. const annotation = options.annotation;
  139. const queries = [
  140. {
  141. refId: 'annotationQuery',
  142. datasourceId: this.id,
  143. metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}),
  144. crossSeriesReducer: 'REDUCE_NONE',
  145. perSeriesAligner: 'ALIGN_NONE',
  146. title: this.templateSrv.replace(annotation.target.title, options.scopedVars || {}),
  147. text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
  148. tags: this.templateSrv.replace(annotation.target.tags, options.scopedVars || {}),
  149. view: 'FULL',
  150. filters: (annotation.target.filters || []).map((f: any) => {
  151. return this.templateSrv.replace(f, options.scopedVars || {});
  152. }),
  153. type: 'annotationQuery',
  154. },
  155. ];
  156. const { data } = await this.backendSrv.datasourceRequest({
  157. url: '/api/tsdb/query',
  158. method: 'POST',
  159. data: {
  160. from: options.range.from.valueOf().toString(),
  161. to: options.range.to.valueOf().toString(),
  162. queries,
  163. },
  164. });
  165. const results = data.results['annotationQuery'].tables[0].rows.map((v: any) => {
  166. return {
  167. annotation: annotation,
  168. time: Date.parse(v[0]),
  169. title: v[1],
  170. tags: [],
  171. text: v[3],
  172. } as any;
  173. });
  174. return results;
  175. }
  176. async metricFindQuery(query: string) {
  177. const stackdriverMetricFindQuery = new StackdriverMetricFindQuery(this);
  178. return stackdriverMetricFindQuery.execute(query);
  179. }
  180. async testDatasource() {
  181. let status, message;
  182. const defaultErrorMessage = 'Cannot connect to Stackdriver API';
  183. try {
  184. const projectName = await this.getDefaultProject();
  185. const path = `v3/projects/${projectName}/metricDescriptors`;
  186. const response = await this.doRequest(`${this.baseUrl}${path}`);
  187. if (response.status === 200) {
  188. status = 'success';
  189. message = 'Successfully queried the Stackdriver API.';
  190. } else {
  191. status = 'error';
  192. message = response.statusText ? response.statusText : defaultErrorMessage;
  193. }
  194. } catch (error) {
  195. status = 'error';
  196. if (_.isString(error)) {
  197. message = error;
  198. } else {
  199. message = 'Stackdriver: ';
  200. message += error.statusText ? error.statusText : defaultErrorMessage;
  201. if (error.data && error.data.error && error.data.error.code) {
  202. message += ': ' + error.data.error.code + '. ' + error.data.error.message;
  203. }
  204. }
  205. } finally {
  206. return {
  207. status,
  208. message,
  209. };
  210. }
  211. }
  212. formatStackdriverError(error: any) {
  213. let message = 'Stackdriver: ';
  214. message += error.statusText ? error.statusText + ': ' : '';
  215. if (error.data && error.data.error) {
  216. try {
  217. const res = JSON.parse(error.data.error);
  218. message += res.error.code + '. ' + res.error.message;
  219. } catch (err) {
  220. message += error.data.error;
  221. }
  222. } else {
  223. message += 'Cannot connect to Stackdriver API';
  224. }
  225. return message;
  226. }
  227. async getDefaultProject() {
  228. try {
  229. if (this.authenticationType === 'gce' || !this.projectName) {
  230. const { data } = await this.backendSrv.datasourceRequest({
  231. url: '/api/tsdb/query',
  232. method: 'POST',
  233. data: {
  234. queries: [
  235. {
  236. refId: 'ensureDefaultProjectQuery',
  237. type: 'ensureDefaultProjectQuery',
  238. datasourceId: this.id,
  239. },
  240. ],
  241. },
  242. });
  243. this.projectName = data.results.ensureDefaultProjectQuery.meta.defaultProject;
  244. return this.projectName;
  245. } else {
  246. return this.projectName;
  247. }
  248. } catch (error) {
  249. throw this.formatStackdriverError(error);
  250. }
  251. }
  252. async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
  253. try {
  254. if (this.metricTypes.length === 0) {
  255. const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
  256. const { data } = await this.doRequest(`${this.baseUrl}${metricsApiPath}`);
  257. this.metricTypes = data.metricDescriptors.map((m: any) => {
  258. const [service] = m.type.split('/');
  259. const [serviceShortName] = service.split('.');
  260. m.service = service;
  261. m.serviceShortName = serviceShortName;
  262. m.displayName = m.displayName || m.type;
  263. return m;
  264. });
  265. }
  266. return this.metricTypes;
  267. } catch (error) {
  268. appEvents.emit('ds-request-error', this.formatStackdriverError(error));
  269. return [];
  270. }
  271. }
  272. async doRequest(url: string, maxRetries = 1) {
  273. return this.backendSrv
  274. .datasourceRequest({
  275. url: this.url + url,
  276. method: 'GET',
  277. })
  278. .catch((error: any) => {
  279. if (maxRetries > 0) {
  280. return this.doRequest(url, maxRetries - 1);
  281. }
  282. throw error;
  283. });
  284. }
  285. }