bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/web/static/js/graph.ts (about)

     1  /// <reference path="0-bosun.ts" />
     2  
     3  class TagSet {
     4  	[tagk: string]: string;
     5  }
     6  
     7  class TagV {
     8  	[tagk: string]: string[];
     9  }
    10  
    11  class RateOptions {
    12  	counter: boolean;
    13  	counterMax: number;
    14  	resetValue: number;
    15  }
    16  
    17  class Filter {
    18  	type: string;
    19  	tagk: string;
    20  	filter: string;
    21  	groupBy: boolean;
    22  	constructor(f?: Filter) {
    23  		this.type = f && f.type || "auto";
    24  		this.tagk = f && f.tagk || "";
    25  		this.filter = f && f.filter || "";
    26  		this.groupBy = f && f.groupBy || false;
    27  	}
    28  }
    29  
    30  class FilterMap {
    31  	[tagk: string]: Filter;
    32  }
    33  
    34  
    35  class Query {
    36  	aggregator: string;
    37  	metric: string;
    38  	rate: boolean;
    39  	rateOptions: RateOptions;
    40  	tags: TagSet;
    41  	filters: Filter[];
    42  	gbFilters: FilterMap;
    43  	nGbFilters: FilterMap;
    44  	metric_tags: any;
    45  	downsample: string;
    46  	ds: string;
    47  	dstime: string;
    48  	derivative: string;
    49  	constructor(filterSupport: boolean, q?: any) {
    50  		this.aggregator = q && q.aggregator || 'sum';
    51  		this.metric = q && q.metric || '';
    52  		this.rate = q && q.rate || false;
    53  		this.rateOptions = q && q.rateOptions || new RateOptions;
    54  		if (q && !q.derivative) {
    55  			// back compute derivative from q
    56  			if (!this.rate) {
    57  				this.derivative = 'gauge';
    58  			} else if (this.rateOptions.counter) {
    59  				this.derivative = 'counter';
    60  			} else {
    61  				this.derivative = 'rate';
    62  			}
    63  		} else {
    64  			this.derivative = q && q.derivative || 'auto';
    65  		}
    66  		this.ds = q && q.ds || '';
    67  		this.dstime = q && q.dstime || '';
    68  		this.tags = q && q.tags || new TagSet;
    69  		this.gbFilters = q && q.gbFilters || new FilterMap;
    70  		this.nGbFilters = q && q.nGbFilters || new FilterMap;
    71  		var that = this;
    72  		// Copy tags with values to group by filters so old links work
    73  		if (filterSupport) {
    74  			_.each(this.tags, function (v, k) {
    75  				if (v === "") {
    76  					return
    77  				}
    78  				var f = new (Filter);
    79  				f.filter = v;
    80  				f.groupBy = true;
    81  				f.tagk = k;
    82  				that.gbFilters[k] = f;
    83  			});
    84  			// Load filters from raw query and turn them into gb and nGbFilters.
    85  			// This makes links from other pages work (i.e. the expr page)
    86  			if (_.has(q, 'filters')) {
    87  				_.each(q.filters, function (filter: Filter) {
    88  					if (filter.groupBy) {
    89  						that.gbFilters[filter.tagk] = filter;
    90  						return;
    91  					}
    92  					that.nGbFilters[filter.tagk] = filter;
    93  				});
    94  			}
    95  		}
    96  		this.setFilters();
    97  		this.setDs();
    98  		this.setDerivative();
    99  	}
   100  	setFilters() {
   101  		this.filters = [];
   102  		var that = this;
   103  		_.each(this.gbFilters, function (filter: Filter, tagk) {
   104  			if (filter.filter && filter.type) {
   105  				that.filters.push(filter);
   106  			}
   107  		});
   108  		_.each(this.nGbFilters, function (filter: Filter, tagk) {
   109  			if (filter.filter && filter.type) {
   110  				that.filters.push(filter);
   111  			}
   112  		});
   113  	}
   114  	setDs() {
   115  		if (this.dstime && this.ds) {
   116  			this.downsample = this.dstime + '-' + this.ds;
   117  		} else {
   118  			this.downsample = '';
   119  		}
   120  	}
   121  	setDerivative() {
   122  		var max = this.rateOptions.counterMax;
   123  		this.rate = false;
   124  		this.rateOptions = new RateOptions();
   125  		switch (this.derivative) {
   126  			case "rate":
   127  				this.rate = true;
   128  				break;
   129  			case "counter":
   130  				this.rate = true;
   131  				this.rateOptions.counter = true;
   132  				this.rateOptions.counterMax = max;
   133  				this.rateOptions.resetValue = 1;
   134  				break;
   135  			case "gauge":
   136  				this.rate = false;
   137  				break;
   138  		}
   139  	}
   140  }
   141  
   142  class GraphRequest {
   143  	start: string;
   144  	end: string;
   145  	queries: Query[];
   146  	constructor() {
   147  		this.start = '1h-ago';
   148  		this.queries = [];
   149  	}
   150  	prune() {
   151  		for (var i = 0; i < this.queries.length; i++) {
   152  			angular.forEach(this.queries[i], (v, k) => {
   153  				var qi: any = this.queries[i];
   154  				switch (typeof v) {
   155  					case "string":
   156  						if (!v) {
   157  							delete qi[k];
   158  						}
   159  						break;
   160  					case "boolean":
   161  						if (!v) {
   162  							delete qi[k];
   163  						}
   164  						break;
   165  					case "object":
   166  						if (Object.keys(v).length == 0) {
   167  							delete qi[k];
   168  						}
   169  						break;
   170  				}
   171  			});
   172  		}
   173  	}
   174  }
   175  
   176  var graphRefresh: any;
   177  
   178  class Version {
   179  	Major: number;
   180  	Minor: number;
   181  }
   182  
   183  interface IGraphScope extends ng.IScope {
   184  	index: number;
   185  	url: string;
   186  	error: string;
   187  	running: string;
   188  	warning: string;
   189  	metrics: string[];
   190  	tagvs: TagV[];
   191  	tags: TagSet;
   192  	sorted_tagks: string[][];
   193  	query: string;
   194  	aggregators: string[];
   195  	version: any;
   196  	rate_options: string[];
   197  	dsaggregators: string[];
   198  	GetTagKByMetric: (index: number) => void;
   199  	Query: () => void;
   200  	TagsAsQs: (ts: TagSet) => string;
   201  	MakeParam: (k: string, v: string) => string;
   202  	GetTagVs: (k: string) => void;
   203  	result: any;
   204  	queries: string[];
   205  	dt: any;
   206  	series: any;
   207  	query_p: Query[];
   208  	start: string;
   209  	end: string;
   210  	AddTab: () => void;
   211  	setIndex: (i: number) => void;
   212  	autods: boolean;
   213  	refresh: boolean;
   214  	SwitchTimes: () => void;
   215  	duration_map: any;
   216  	animate: () => any;
   217  	stop: () => any;
   218  	canAuto: {};
   219  	meta: {};
   220  	y_labels: string[];
   221  	min: number;
   222  	max: number;
   223  	queryTime: string;
   224  	normalize: boolean;
   225  	filterSupport: boolean;
   226  	filters: string[];
   227  	annotations: any[];
   228  	annotation: Annotation;
   229  	submitAnnotation: () => void;
   230  	deleteAnnotation: () => void;
   231  	owners: string[];
   232  	hosts: string[];
   233  	categories: string[];
   234  	annotateEnabled: boolean;
   235  	showAnnotations: boolean;
   236  	setShowAnnotations: (something: any) => void;
   237  	exprText: string;
   238  	keydown: ($event: any) => void;
   239  }
   240  
   241  bosunControllers.controller('GraphCtrl', ['$scope', '$http', '$location', '$route', '$timeout','authService', function ($scope: IGraphScope, $http: ng.IHttpService, $location: ng.ILocationService, $route: ng.route.IRouteService, $timeout: ng.ITimeoutService, auth: IAuthService) {
   242  	$scope.aggregators = ["sum", "min", "max", "avg", "dev", "zimsum", "mimmin", "mimmax"];
   243  	$scope.dsaggregators = ["", "sum", "min", "max", "avg", "dev", "zimsum", "mimmin", "mimmax"];
   244  	$scope.filters = ["auto", "iliteral_or", "iwildcard", "literal_or", "not_iliteral_or", "not_literal_or", "regexp", "wildcard"];
   245  	if ($scope.version.Major >= 2 && $scope.version.Minor >= 2) {
   246  		$scope.filterSupport = true;
   247  	}
   248  	$scope.rate_options = ["auto", "gauge", "counter", "rate"];
   249  	$scope.canAuto = {};
   250  	$scope.showAnnotations = (getShowAnnotations() == "true");
   251  	$scope.setShowAnnotations = () => {
   252  		if ($scope.showAnnotations) {
   253  			setShowAnnotations("true");
   254  			return;
   255  		}
   256  		setShowAnnotations("false");
   257  	}
   258  	var search = $location.search();
   259  	var j = search.json;
   260  	if (search.b64) {
   261  		j = atob(search.b64);
   262  	}
   263  	$scope.annotation = new Annotation();
   264  	var request = j ? JSON.parse(j) : new GraphRequest;
   265  	$scope.index = parseInt($location.hash()) || 0;
   266  	$scope.tagvs = [];
   267  	$scope.sorted_tagks = [];
   268  	$scope.query_p = [];
   269  	angular.forEach(request.queries, (q, i) => {
   270  		$scope.query_p[i] = new Query($scope.filterSupport, q);
   271  	});
   272  	$scope.start = request.start;
   273  	$scope.end = request.end;
   274  	$scope.autods = search.autods != 'false';
   275  	$scope.refresh = search.refresh == 'true';
   276  	$scope.normalize = search.normalize == 'true';
   277  	if (search.min) {
   278  		$scope.min = +search.min;
   279  	}
   280  	if (search.max) {
   281  		$scope.max = +search.max;
   282  	}
   283  	var duration_map: any = {
   284  		"s": "s",
   285  		"m": "m",
   286  		"h": "h",
   287  		"d": "d",
   288  		"w": "w",
   289  		"n": "M",
   290  		"y": "y",
   291  	};
   292  	var isRel = /^(\d+)(\w)-ago$/;
   293  	function RelToAbs(m: RegExpExecArray) {
   294  		return moment().utc().subtract(parseFloat(m[1]), duration_map[m[2]]).format();
   295  	}
   296  	function AbsToRel(s: string) {
   297  		//Not strict parsing of the time format. For example, just "2014" will be valid
   298  		var t = moment.utc(s, moment.defaultFormat).fromNow();
   299  		return t;
   300  	}
   301  	function SwapTime(s: string) {
   302  		if (!s) {
   303  			return moment().utc().format();
   304  		}
   305  		var m = isRel.exec(s);
   306  		if (m) {
   307  			return RelToAbs(m);
   308  		}
   309  		return AbsToRel(s);
   310  	}
   311  	$scope.submitAnnotation = () => {
   312  		$scope.annotation.CreationUser = auth.GetUsername();
   313  		$http.post('/api/annotation', $scope.annotation)
   314  		.success((data) => {
   315  			//debugger;
   316  			if ($scope.annotation.Id == "" && $scope.annotation.Owner != "") {
   317  				setOwner($scope.annotation.Owner);
   318  			}
   319  			$scope.annotation = new Annotation(data);
   320  			$scope.error = "";
   321  			// This seems to make angular refresh, where a push doesn't
   322  			$scope.annotations = $scope.annotations.concat($scope.annotation);
   323  		})
   324  		.error((error) => {
   325  			$scope.error = error;
   326  		});
   327  	}
   328  	$scope.deleteAnnotation = () => $http.delete('/api/annotation/' + $scope.annotation.Id)
   329  		.success((data) => {
   330  			$scope.error = "";
   331  			$scope.annotations = _.without($scope.annotations, _.findWhere($scope.annotations, { Id: $scope.annotation.Id }));
   332  		})
   333  		.error((error) => {
   334  			$scope.error = error;
   335  		});
   336  	$scope.SwitchTimes = function () {
   337  		$scope.start = SwapTime($scope.start);
   338  		$scope.end = SwapTime($scope.end);
   339  	};
   340  	$scope.AddTab = function () {
   341  		$scope.index = $scope.query_p.length;
   342  		$scope.query_p.push(new Query($scope.filterSupport));
   343  	};
   344  	$scope.setIndex = function (i: number) {
   345  		$scope.index = i;
   346  	};
   347  	var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
   348  	if ($scope.annotateEnabled) {
   349  		$http.get('/api/annotation/values/Owner')
   350  			.success((data: string[]) => {
   351  				$scope.owners = data;
   352  			});
   353  		$http.get('/api/annotation/values/Category')
   354  			.success((data: string[]) => {
   355  				$scope.categories = data;
   356  			});
   357  		$http.get('/api/annotation/values/Host')
   358  			.success((data: string[]) => {
   359  				$scope.hosts = data;
   360  			});
   361  	}
   362  	$scope.GetTagKByMetric = function (index: number) {
   363  		$scope.tagvs[index] = new TagV;
   364  		var metric = $scope.query_p[index].metric;
   365  		if (!metric) {
   366  			$scope.canAuto[metric] = true;
   367  			return;
   368  		}
   369  		$http.get('/api/tagk/' + encodeURIComponent(metric))
   370  			.success(function (data: string[]) {
   371  				var q = $scope.query_p[index];
   372  				var tags = new TagSet;
   373  				q.metric_tags = {};
   374  				if (!q.gbFilters) {
   375  					q.gbFilters = new FilterMap;
   376  				}
   377  				if (!q.nGbFilters) {
   378  					q.nGbFilters = new FilterMap;
   379  				}
   380  				for (var i = 0; i < data.length; i++) {
   381  					var d = data[i];
   382  					if ($scope.filterSupport) {
   383  						if (!q.gbFilters[d]) {
   384  							var filter = new Filter;
   385  							filter.tagk = d;
   386  							filter.groupBy = true;
   387  							q.gbFilters[d] = filter;
   388  						}
   389  						if (!q.nGbFilters[d]) {
   390  							var filter = new Filter;
   391  							filter.tagk = d;
   392  							q.nGbFilters[d] = filter;
   393  						}
   394  					}
   395  					if (q.tags) {
   396  						tags[d] = q.tags[d];
   397  					}
   398  					if (!tags[d]) {
   399  						tags[d] = '';
   400  					}
   401  					q.metric_tags[d] = true;
   402  					GetTagVs(d, index);
   403  				}
   404  				angular.forEach(q.tags, (val, key) => {
   405  					if (val) {
   406  						tags[key] = val;
   407  					}
   408  				});
   409  				q.tags = tags;
   410  				// Make sure host is always the first tag.
   411  				$scope.sorted_tagks[index] = Object.keys(tags);
   412  				$scope.sorted_tagks[index].sort((a, b) => {
   413  					if (a == 'host') {
   414  						return -1;
   415  					} else if (b == 'host') {
   416  						return 1;
   417  					}
   418  					return a.localeCompare(b);
   419  				});
   420  			})
   421  			.error(function (error) {
   422  				$scope.error = 'Unable to fetch metrics: ' + error;
   423  			});
   424  		$http.get('/api/metadata/metrics?metric=' + encodeURIComponent(metric))
   425  			.success((data: any) => {
   426  				var canAuto = data && data.Rate;
   427  				$scope.canAuto[metric] = canAuto;
   428  			})
   429  			.error(err => {
   430  				$scope.error = err;
   431  			});
   432  	};
   433  	if ($scope.query_p.length == 0) {
   434  		$scope.AddTab();
   435  	}
   436  	$http.get('/api/metric' + "?since=" + moment().utc().subtract(2, "days").unix())
   437  		.success(function (data: string[]) {
   438  			$scope.metrics = data;
   439  		})
   440  		.error(function (error) {
   441  			$scope.error = 'Unable to fetch metrics: ' + error;
   442  		});
   443  	function GetTagVs(k: string, index: number) {
   444  		$http.get('/api/tagv/' + encodeURIComponent(k) + '/' + $scope.query_p[index].metric)
   445  			.success(function (data: string[]) {
   446  				data.sort();
   447  				$scope.tagvs[index][k] = data;
   448  			})
   449  			.error(function (error) {
   450  				$scope.error = 'Unable to fetch metrics: ' + error;
   451  			});
   452  	}
   453  	function getRequest() {
   454  		request = new GraphRequest;
   455  		request.start = $scope.start;
   456  		request.end = $scope.end;
   457  		angular.forEach($scope.query_p, function (p) {
   458  			if (!p.metric) {
   459  				return;
   460  			}
   461  			var q = new Query($scope.filterSupport, p);
   462  			var tags = q.tags;
   463  			q.tags = new TagSet;
   464  			if (!$scope.filterSupport) {
   465  				angular.forEach(tags, function (v, k) {
   466  					if (v && k) {
   467  						q.tags[k] = v;
   468  					}
   469  				});
   470  			}
   471  			request.queries.push(q);
   472  		});
   473  		return request;
   474  	}
   475  	$scope.keydown = function ($event: any) {
   476  		if ($event.shiftKey && $event.keyCode == 13) {
   477  			$scope.Query();
   478  		}
   479  	};
   480  	$scope.Query = function () {
   481  		var r = getRequest();
   482  		angular.forEach($scope.query_p, (q, index) => {
   483  			var m = q.metric_tags;
   484  			if (!m) {
   485  				return;
   486  			}
   487  			if (!r.queries[index]) {
   488  				return;
   489  			}
   490  			angular.forEach(q.tags, (key, tag) => {
   491  				if (m[tag]) {
   492  					return;
   493  				}
   494  				delete r.queries[index].tags[tag];
   495  			});
   496  			if ($scope.filterSupport) {
   497  				_.each(r.queries[index].filters, (f: Filter) => {
   498  					if (m[f.tagk]) {
   499  						return
   500  					}
   501  					delete r.queries[index].nGbFilters[f.tagk];
   502  					delete r.queries[index].gbFilters[f.tagk];
   503  					r.queries[index].filters = _.without(r.queries[index].filters, _.findWhere(r.queries[index].filters, { tagk: f.tagk }));
   504  				});
   505  			}
   506  		});
   507  		r.prune();
   508  		$location.search('b64', btoa(JSON.stringify(r)));
   509  		$location.search('autods', $scope.autods ? undefined : 'false');
   510  		$location.search('refresh', $scope.refresh ? 'true' : undefined);
   511  		$location.search('normalize', $scope.normalize ? 'true' : undefined);
   512  		var min = angular.isNumber($scope.min) ? $scope.min.toString() : null;
   513  		var max = angular.isNumber($scope.max) ? $scope.max.toString() : null;
   514  		$location.search('min', min);
   515  		$location.search('max', max);
   516  		$route.reload();
   517  	}
   518  	request = getRequest();
   519  	if (!request.queries.length) {
   520  		return;
   521  	}
   522  	var autods = $scope.autods ? '&autods=' + $('#chart').width() : '';
   523  	function getMetricMeta(metric: string) {
   524  		$http.get('/api/metadata/metrics?metric=' + encodeURIComponent(metric))
   525  			.success((data) => {
   526  				$scope.meta[metric] = data;
   527  			})
   528  			.error((error) => {
   529  				console.log("Error getting metadata for metric " + metric);
   530  			})
   531  	}
   532  	function get(noRunning: boolean) {
   533  		$timeout.cancel(graphRefresh);
   534  		if (!noRunning) {
   535  			$scope.running = 'Running';
   536  		}
   537  		var autorate = '';
   538  		$scope.meta = {};
   539  		for (var i = 0; i < request.queries.length; i++) {
   540  			if (request.queries[i].derivative == 'auto') {
   541  				autorate += '&autorate=' + i;
   542  			}
   543  			getMetricMeta(request.queries[i].metric);
   544  		}
   545  		_.each(request.queries, (q: Query, qIndex) => {
   546  			request.queries[qIndex].filters = _.map(q.filters, (filter: Filter) => {
   547  				var f = new Filter(filter);
   548  				if (f.filter && f.type) {
   549  					if (f.type == "auto") {
   550  						if (f.filter.indexOf("*") > -1) {
   551  							f.type = f.filter == "*" ? f.type = "wildcard" : "iwildcard";
   552  						} else {
   553  							f.type = "literal_or";
   554  						}
   555  					}
   556  				}
   557  				return f;
   558  			});
   559  		});
   560  		var min = angular.isNumber($scope.min) ? '&min=' + encodeURIComponent($scope.min.toString()) : '';
   561  		var max = angular.isNumber($scope.max) ? '&max=' + encodeURIComponent($scope.max.toString()) : '';
   562  		$scope.animate();
   563  		$scope.queryTime = '';
   564  		if (request.end && !isRel.exec(request.end)) {
   565  			var t = moment.utc(request.end, moment.defaultFormat);
   566  			$scope.queryTime = '&date=' + t.format('YYYY-MM-DD');
   567  			$scope.queryTime += '&time=' + t.format('HH:mm');
   568  		}
   569  		$http.get('/api/graph?' + 'b64=' + encodeURIComponent(btoa(JSON.stringify(request))) + autods + autorate + min + max)
   570  			.success((data: any) => {
   571  				$scope.result = data.Series;
   572  				if ($scope.annotateEnabled) {
   573  					$scope.annotations = _.sortBy(data.Annotations, (d: Annotation) => { return d.StartDate; });
   574  				}
   575  				$scope.warning = '';
   576  				if (!$scope.result) {
   577  					$scope.warning = 'No Results';
   578  				}
   579  				if (data.Warnings.length > 0) {
   580  					$scope.warning += data.Warnings.join(" ");
   581  				}
   582  				$scope.queries = data.Queries;
   583  				$scope.exprText = "";
   584  				_.each($scope.queries, (q, i) => {
   585  					$scope.exprText += "$" + alphabet[i] + " = " + q + "\n";
   586  					if (i == $scope.queries.length - 1) {
   587  						$scope.exprText += "avg($" + alphabet[i] + ")"
   588  					}
   589  				});
   590  				$scope.running = '';
   591  				$scope.error = '';
   592  				var u = $location.absUrl();
   593  				u = u.substr(0, u.indexOf('?')) + '?';
   594  				u += 'b64=' + search.b64 + autods + autorate + min + max;
   595  				$scope.url = u;
   596  			})
   597  			.error((error) => {
   598  				$scope.error = error;
   599  				$scope.running = '';
   600  			})
   601  			.finally(() => {
   602  				$scope.stop();
   603  				if ($scope.refresh) {
   604  					graphRefresh = $timeout(() => { get(true); }, 5000);
   605  				};
   606  			});
   607  	};
   608  	get(false);
   609  }]);
   610  
   611  bosunApp.directive('tsPopup', () => {
   612  	return {
   613  		restrict: 'E',
   614  		scope: {
   615  			url: '=',
   616  		},
   617  		template: '<button class="btn btn-default" data-html="true" data-placement="bottom">embed</button>',
   618  		link: (scope: any, elem: any, attrs: any) => {
   619  			var button = $('button', elem);
   620  			scope.$watch(attrs.url, (url: any) => {
   621  				if (!url) {
   622  					return;
   623  				}
   624  				var text = '<input type="text" onClick="this.select();" readonly="readonly" value="&lt;a href=&quot;' + url + '&quot;&gt;&lt;img src=&quot;' + url + '&.png=png&quot;&gt;&lt;/a&gt;">';
   625  				button.popover({
   626  					content: text,
   627  				});
   628  			});
   629  		},
   630  	};
   631  });