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="<a href="' + url + '"><img src="' + url + '&.png=png"></a>">'; 625 button.popover({ 626 content: text, 627 }); 628 }); 629 }, 630 }; 631 });