datasource.ts 9.0 KB

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