query_ctrl.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import _ from 'lodash';
  2. import { QueryCtrl } from 'app/plugins/sdk';
  3. import appEvents from 'app/core/app_events';
  4. export interface QueryMeta {
  5. rawQuery: string;
  6. rawQueryString: string;
  7. metricLabels: { [key: string]: string[] };
  8. resourceLabels: { [key: string]: string[] };
  9. }
  10. export interface Filter {
  11. key: string;
  12. operator: string;
  13. value: string;
  14. }
  15. export class StackdriverQueryCtrl extends QueryCtrl {
  16. static templateUrl = 'partials/query.editor.html';
  17. target: {
  18. project: {
  19. id: string;
  20. name: string;
  21. };
  22. metricType: string;
  23. refId: string;
  24. aggregation: {
  25. crossSeriesReducer: string;
  26. alignmentPeriod: string;
  27. perSeriesAligner: string;
  28. groupBys: string[];
  29. };
  30. filters: Filter[];
  31. };
  32. defaultDropdownValue = 'Select metric';
  33. defaults = {
  34. project: {
  35. id: 'default',
  36. name: 'loading project...',
  37. },
  38. metricType: this.defaultDropdownValue,
  39. aggregation: {
  40. crossSeriesReducer: 'REDUCE_MEAN',
  41. alignmentPeriod: '',
  42. perSeriesAligner: '',
  43. groupBys: [],
  44. },
  45. filters: [],
  46. };
  47. groupBySegments: any[];
  48. filterSegments: any[];
  49. removeSegment: any;
  50. aggOptions = [
  51. { text: 'none', value: 'REDUCE_NONE' },
  52. { text: 'mean', value: 'REDUCE_MEAN' },
  53. { text: 'min', value: 'REDUCE_MIN' },
  54. { text: 'max', value: 'REDUCE_MAX' },
  55. { text: 'sum', value: 'REDUCE_SUM' },
  56. { text: 'std. dev.', value: 'REDUCE_STDDEV' },
  57. { text: 'count', value: 'REDUCE_COUNT' },
  58. { text: '99th percentile', value: 'REDUCE_PERCENTILE_99' },
  59. { text: '95th percentile', value: 'REDUCE_PERCENTILE_95' },
  60. { text: '50th percentile', value: 'REDUCE_PERCENTILE_50' },
  61. { text: '5th percentile', value: 'REDUCE_PERCENTILE_05' },
  62. ];
  63. showHelp: boolean;
  64. showLastQuery: boolean;
  65. lastQueryMeta: QueryMeta;
  66. lastQueryError?: string;
  67. metricLabels: { [key: string]: string[] };
  68. resourceLabels: { [key: string]: string[] };
  69. /** @ngInject */
  70. constructor($scope, $injector, private uiSegmentSrv, private timeSrv) {
  71. super($scope, $injector);
  72. _.defaultsDeep(this.target, this.defaults);
  73. this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
  74. this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
  75. this.getCurrentProject()
  76. .then(this.getMetricTypes.bind(this))
  77. .then(this.getLabels.bind(this));
  78. this.initSegments();
  79. }
  80. initSegments() {
  81. this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => {
  82. return this.uiSegmentSrv.getSegmentForValue(groupBy);
  83. });
  84. this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: '-- remove group by --' });
  85. this.ensurePlusButton(this.groupBySegments);
  86. this.filterSegments = [];
  87. this.target.filters.forEach(f => {
  88. this.filterSegments.push(this.uiSegmentSrv.newKey(f.key));
  89. this.filterSegments.push(this.uiSegmentSrv.newOperator(f.operator));
  90. this.filterSegments.push(this.uiSegmentSrv.newKeyValue(f.value));
  91. });
  92. this.ensurePlusButton(this.filterSegments);
  93. }
  94. async getCurrentProject() {
  95. try {
  96. const projects = await this.datasource.getProjects();
  97. if (projects && projects.length > 0) {
  98. this.target.project = projects[0];
  99. } else {
  100. throw new Error('No projects found');
  101. }
  102. } catch (error) {
  103. let message = 'Projects cannot be fetched: ';
  104. message += error.statusText ? error.statusText + ': ' : '';
  105. if (error && error.data && error.data.error && error.data.error.message) {
  106. if (error.data.error.code === 403) {
  107. message += `
  108. A list of projects could not be fetched from the Google Cloud Resource Manager API.
  109. You might need to enable it first:
  110. https://console.developers.google.com/apis/library/cloudresourcemanager.googleapis.com`;
  111. } else {
  112. message += error.data.error.code + '. ' + error.data.error.message;
  113. }
  114. } else {
  115. message += 'Cannot connect to Stackdriver API';
  116. }
  117. appEvents.emit('ds-request-error', message);
  118. }
  119. }
  120. async getMetricTypes() {
  121. //projects/raintank-production/metricDescriptors/agent.googleapis.com/agent/api_request_count
  122. if (this.target.project.id !== 'default') {
  123. const metricTypes = await this.datasource.getMetricTypes(this.target.project.id);
  124. if (this.target.metricType === this.defaultDropdownValue && metricTypes.length > 0) {
  125. this.$scope.$apply(() => (this.target.metricType = metricTypes[0].name));
  126. }
  127. return metricTypes.map(mt => ({ value: mt.id, text: mt.id }));
  128. } else {
  129. return [];
  130. }
  131. }
  132. async getLabels() {
  133. const data = await this.datasource.getTimeSeries({
  134. targets: [
  135. {
  136. refId: this.target.refId,
  137. datasourceId: this.datasource.id,
  138. metricType: this.target.metricType,
  139. aggregation: {
  140. crossSeriesReducer: 'REDUCE_NONE',
  141. },
  142. view: 'HEADERS',
  143. },
  144. ],
  145. range: this.timeSrv.timeRange(),
  146. });
  147. this.metricLabels = data.results[this.target.refId].meta.metricLabels;
  148. this.resourceLabels = data.results[this.target.refId].meta.resourceLabels;
  149. }
  150. async onMetricTypeChange() {
  151. this.refresh();
  152. this.getLabels();
  153. }
  154. getGroupBys(segment, index, removeText?: string) {
  155. const metricLabels = Object.keys(this.metricLabels)
  156. .filter(ml => {
  157. return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1;
  158. })
  159. .map(l => {
  160. return this.uiSegmentSrv.newSegment({
  161. value: `metric.label.${l}`,
  162. expandable: false,
  163. });
  164. });
  165. const resourceLabels = Object.keys(this.resourceLabels)
  166. .filter(ml => {
  167. return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1;
  168. })
  169. .map(l => {
  170. return this.uiSegmentSrv.newSegment({
  171. value: `resource.label.${l}`,
  172. expandable: false,
  173. });
  174. });
  175. this.removeSegment.value = removeText || '-- remove group by --';
  176. return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]);
  177. }
  178. groupByChanged(segment, index) {
  179. if (segment.value === this.removeSegment.value) {
  180. this.groupBySegments.splice(index, 1);
  181. } else {
  182. segment.type = 'value';
  183. }
  184. const reducer = (memo, seg) => {
  185. if (!seg.fake) {
  186. memo.push(seg.value);
  187. }
  188. return memo;
  189. };
  190. this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []);
  191. this.ensurePlusButton(this.groupBySegments);
  192. this.refresh();
  193. }
  194. getFilters(segment, index) {
  195. if (segment.type === 'condition') {
  196. return [this.uiSegmentSrv.newSegment('AND')];
  197. }
  198. if (segment.type === 'operator') {
  199. return this.uiSegmentSrv.newOperators(['=', '!=', '=~', '!=~']);
  200. }
  201. if (segment.type === 'key' || segment.type === 'plus-button') {
  202. return this.getGroupBys(null, null, '-- remove filter --');
  203. }
  204. if (segment.type === 'value') {
  205. const filterKey = this.filterSegments[index - 2].value;
  206. if (this.metricLabels[filterKey]) {
  207. return this.getValuesForFilterKey(this.metricLabels[filterKey]);
  208. }
  209. if (this.resourceLabels[filterKey]) {
  210. return this.getValuesForFilterKey(this.resourceLabels[filterKey]);
  211. }
  212. }
  213. return [];
  214. }
  215. getValuesForFilterKey(labels: any[]) {
  216. const filterValues = labels.map(l => {
  217. return this.uiSegmentSrv.newSegment({
  218. value: `${l}`,
  219. expandable: false,
  220. });
  221. });
  222. return filterValues;
  223. }
  224. ensurePlusButton(segments) {
  225. const count = segments.length;
  226. const lastSegment = segments[Math.max(count - 1, 0)];
  227. if (!lastSegment || lastSegment.type !== 'plus-button') {
  228. segments.push(this.uiSegmentSrv.newPlusButton());
  229. }
  230. }
  231. onDataReceived(dataList) {
  232. this.lastQueryError = null;
  233. this.lastQueryMeta = null;
  234. const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
  235. if (anySeriesFromQuery) {
  236. this.lastQueryMeta = anySeriesFromQuery.meta;
  237. this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
  238. }
  239. }
  240. onDataError(err) {
  241. if (err.data && err.data.results) {
  242. const queryRes = err.data.results[this.target.refId];
  243. if (queryRes) {
  244. this.lastQueryMeta = queryRes.meta;
  245. this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
  246. let jsonBody;
  247. try {
  248. jsonBody = JSON.parse(queryRes.error);
  249. } catch {
  250. this.lastQueryError = queryRes.error;
  251. }
  252. this.lastQueryError = jsonBody.error.message;
  253. }
  254. }
  255. }
  256. }