github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/static/js/search-traces.js (about) 1 /* 2 Copyright 2023. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 'use strict'; 18 let chart; 19 let currList = []; 20 let curSpanTraceArray = [], 21 curErrorTraceArray = [], 22 timeList = [], 23 returnResTotal = []; 24 let pageNumber = 1, 25 traceSize = 0, 26 params = {}; 27 let limitation = -1; 28 let hasLoaded = false; 29 let allResultsFetched = false; 30 let totalTraces = 0; 31 $(document).ready(() => { 32 allResultsFetched = false; 33 if (Cookies.get("theme")) { 34 theme = Cookies.get("theme"); 35 $("body").attr("data-theme", theme); 36 } 37 $(".theme-btn").on("click", themePickerHandler); 38 $('.theme-btn').on('click', showScatterPlot); 39 40 initPage(); 41 }); 42 window.onload = function () { 43 hasLoaded = true; 44 }; 45 function initPage(){ 46 initChart(); 47 getValuesOfColumn("service", "Service"); 48 getValuesOfColumn("name", "Operation"); 49 handleSort(); 50 handleDownload(); 51 handleTimePicker(); 52 $("#search-trace-btn").on("click", searchTraceHandler); 53 } 54 55 function getValuesOfColumn(chooseColumn, spanName) { 56 let searchText = "SELECT DISTINCT " + chooseColumn + " FROM `traces`"; 57 let param = { 58 state: "query", 59 searchText: searchText, 60 startEpoch: "now-3h", 61 endEpoch: filterEndDate, 62 indexName: "traces", 63 queryLanguage: "SQL", 64 from: 0, 65 }; 66 $.ajax({ 67 method: "post", 68 url: "api/search", 69 headers: { 70 "Content-Type": "application/json; charset=utf-8", 71 Accept: "*/*", 72 }, 73 crossDomain: true, 74 dataType: "json", 75 data: JSON.stringify(param), 76 }).then((res) => { 77 let valuesOfColumn = new Set(); 78 valuesOfColumn.add("All"); 79 if (res && res.hits && res.hits.records) { 80 for (let i = 0; i < res.hits.records.length; i++) { 81 let cur = res.hits.records[i][chooseColumn]; 82 if (typeof cur == "string") valuesOfColumn.add(cur); 83 else valuesOfColumn.add(cur.toString()); 84 } 85 } 86 currList = Array.from(valuesOfColumn); 87 $(`#${chooseColumn}-dropdown`).singleBox({ 88 spanName: spanName, 89 dataList: currList, 90 defaultValue: "All", 91 dataUpdate: true, 92 clickedHead: async function(){ 93 await fetchData(chooseColumn); 94 return currList; 95 } 96 }); 97 }); 98 } 99 function fetchData(chooseColumn) { 100 return new Promise((resolve, reject) => { 101 let searchText = "SELECT DISTINCT " + chooseColumn + " FROM `traces`"; 102 if ( 103 chooseColumn == "name" && 104 $("#service-span-name").text() && 105 $("#service-span-name").text() != "All" 106 ) { 107 searchText += " WHERE service='" + $("#service-span-name").text() + "'"; 108 } else if ( 109 chooseColumn == "service" && 110 $("#operation-span-name").text() && 111 $("#operation-span-name").text() != "All" 112 ) { 113 searchText += " WHERE name='" + $("#operation-span-name").text() + "'"; 114 } 115 let param = { 116 state: "query", 117 searchText: searchText, 118 startEpoch: "now-3h", 119 endEpoch: filterEndDate, 120 indexName: "traces", 121 queryLanguage: "SQL", 122 from: 0, 123 }; 124 $.ajax({ 125 method: "post", 126 url: "api/search", 127 headers: { 128 "Content-Type": "application/json; charset=utf-8", 129 Accept: "*/*", 130 }, 131 crossDomain: true, 132 dataType: "json", 133 data: JSON.stringify(param), 134 }) 135 .then((res) => { 136 let valuesOfColumn = new Set(); 137 valuesOfColumn.add("All"); 138 if (res && res.hits && res.hits.records) { 139 for (let i = 0; i < res.hits.records.length; i++) { 140 let cur = res.hits.records[i][chooseColumn]; 141 if (typeof cur == "string") valuesOfColumn.add(cur); 142 else valuesOfColumn.add(cur.toString()); 143 } 144 } 145 currList = Array.from(valuesOfColumn); 146 resolve(currList); 147 }) 148 .catch((error) => { 149 reject(error); 150 }); 151 }); 152 } 153 function handleTimePicker(){ 154 Cookies.set("startEpoch", "now-3h"); 155 Cookies.set("endEpoch", "now"); 156 $("#lookback").timeTicker({ 157 spanName: "Last 3 Hrs", 158 }); 159 } 160 function handleSort(){ 161 let currList = ["Most Recent", "Longest First", "Shortest First", "Most Spans", "Least Spans"]; 162 $("#sort-dropdown").singleBox({ 163 spanName: "Most Recent", 164 defaultValue: "Most Recent", 165 dataList: currList, 166 clicked: function (e) { 167 if (e == "Most Recent") { 168 returnResTotal = returnResTotal.sort(compare("start_time", "most")); 169 } else if (e == "Longest First") { 170 returnResTotal = returnResTotal.sort(compareDuration("most")); 171 } else if (e == "Shortest First") { 172 returnResTotal = returnResTotal.sort(compareDuration("least")); 173 } else if (e == "Most Spans") { 174 returnResTotal = returnResTotal.sort(compare("span_count", "most")); 175 } else if (e == "Least Spans") { 176 returnResTotal = returnResTotal.sort(compare("span_count", "least")); 177 } 178 reSort(); 179 }, 180 }); 181 } 182 function compareDuration(method) { 183 return function (object1, object2) { 184 let value1 = object1["end_time"] - object1["start_time"]; 185 let value2 = object2["end_time"] - object2["start_time"]; 186 if (method == "most") return value2 - value1; 187 else return value1 - value2; 188 }; 189 } 190 function compare(property, method) { 191 return function (object1, object2) { 192 let value1 = object1[property]; 193 let value2 = object2[property]; 194 if(method == "most") return value2 - value1; 195 else return value1 - value2; 196 }; 197 } 198 function handleDownload(){ 199 let currList = ["Download as CSV", "Download as JSON"]; 200 $("#download-dropdown").singleBox({ 201 fillIn: false, 202 spanName: "Download Result", 203 dataList: currList, 204 clicked: function (e) { 205 if (e == "Download as CSV") { 206 $("#download-trace").download({ 207 data: returnResTotal, 208 downloadMethod: ".csv", 209 }); 210 } else if (e == "Download as JSON") { 211 $("#download-trace").download({ 212 data: returnResTotal, 213 downloadMethod: ".json", 214 }); 215 } 216 }, 217 }); 218 } 219 let requestFlag = 0; 220 function searchTraceHandler(e){ 221 e.stopPropagation(); 222 e.preventDefault(); 223 returnResTotal = []; 224 curSpanTraceArray = []; 225 curErrorTraceArray = []; 226 timeList = []; 227 pageNumber = 1; 228 traceSize = 0; 229 params = {}; 230 $(".warn-box").remove(); 231 $("#traces-number").text(""); 232 let serviceValue = $("#service-span-name").text(); 233 let operationValue = $("#operation-span-name").text(); 234 let tagValue = $("#tags-input").val(); 235 let maxDurationValue = $("#max-duration-input").val(); 236 let minDurationValue = $("#min-duration-input").val(); 237 let limitResValue = $("#limit-result-input").val(); 238 if (limitResValue) limitation = parseInt(limitResValue); 239 else limitation = -1; 240 if (limitation > 0 && limitation < 50) { 241 requestFlag = limitation; 242 limitation = 0; 243 } 244 let searchText = ""; 245 if(serviceValue != "All") searchText = "service=" + serviceValue + " "; 246 if (operationValue != "All") searchText += "name=" + operationValue + " "; 247 if (maxDurationValue) searchText += "EndTimeUnixNano<=" + maxDurationValue + " "; 248 if (minDurationValue) searchText += "StartTimeUnixNano>=" + minDurationValue + " "; 249 if (tagValue) searchText += tagValue; 250 if (searchText == "") searchText = "*"; 251 else searchText = searchText.trim(); 252 let queryParams = new URLSearchParams(window.location.search); 253 let stDate = queryParams.get("startEpoch") || Cookies.get('startEpoch') || "now-3h"; 254 let endDate = queryParams.get("endEpoch") || Cookies.get('endEpoch') || "now"; 255 pageNumber = 1; 256 params = { 257 searchText: searchText, 258 startEpoch: stDate, 259 endEpoch: endDate, 260 queryLanguage: "Splunk QL", 261 page: pageNumber, 262 }; 263 allResultsFetched = false; 264 if (chart != null && chart != "" && chart != undefined) { 265 echarts?.dispose(chart); 266 } 267 searchTrace(params); 268 handleSort(); 269 return false; 270 } 271 function initChart(){ 272 $("#graph-show").removeClass("empty-result-show"); 273 pageNumber = 1; traceSize = 0; 274 returnResTotal = []; 275 let stDate = "now-3h"; 276 let endDate = "now"; 277 params = { 278 searchText: "*", 279 startEpoch: stDate, 280 endEpoch: endDate, 281 queryLanguage: "Splunk QL", 282 page: pageNumber, 283 }; 284 searchTrace(params); 285 } 286 async function getTotalTraces(params) { 287 return $.ajax({ 288 method: "post", 289 url: "api/traces/count", 290 headers: { 291 "Content-Type": "application/json; charset=utf-8", 292 Accept: "*/*", 293 }, 294 crossDomain: true, 295 dataType: "json", 296 data: JSON.stringify(params), 297 }).then((res) => { 298 totalTraces = res; 299 // Update the total traces number with the response 300 $("#traces-number").text(res.toLocaleString("en-US") + " Traces"); 301 }); 302 } 303 function searchTrace(params){ 304 $.ajax({ 305 method: "post", 306 url: "api/traces/search", 307 headers: { 308 "Content-Type": "application/json; charset=utf-8", 309 Accept: "*/*", 310 }, 311 crossDomain: true, 312 dataType: "json", 313 data: JSON.stringify(params), 314 }).then(async (res) => { 315 if (res && res.traces && res.traces.length > 0) { 316 if ((limitation < 50 && limitation > 0) || limitation== 0) { 317 let newArr = res.traces.sort(compare("start_time", "most")); 318 if (limitation > 0) newArr.splice(limitation); 319 else newArr.splice(requestFlag); 320 limitation = 0; 321 requestFlag = 0; 322 returnResTotal = returnResTotal.concat(newArr); 323 } else { 324 returnResTotal = returnResTotal.concat(res.traces); 325 } 326 //concat new traces results 327 returnResTotal = returnResTotal.sort(compare("start_time", "most")); 328 //reset total size 329 traceSize = returnResTotal.length; 330 if ($("#traces-number").text().trim() === "") { 331 await getTotalTraces(params); 332 } 333 timeList = []; 334 for (let i = 0; i < traceSize; i++) { 335 let json = returnResTotal[i]; 336 let milliseconds = Number(json.start_time / 1000000); 337 let dataInfo = new Date(milliseconds); 338 let dataStr = dataInfo.toLocaleString().toLowerCase(); 339 let duration = Number((json.end_time - json.start_time) / 1000000); 340 let newArr = [i, duration, json.span_count, json.span_errors_count, json.service_name, json.operation_name, json.trace_id]; 341 timeList.push(dataStr); 342 if(json.span_errors_count == 0) curSpanTraceArray.push(newArr); 343 else curErrorTraceArray.push(newArr); 344 } 345 showScatterPlot(); 346 reSort(); 347 348 // If the number of traces returned is 50, call getData again 349 if (res.traces.length == 50 && params.page < 2) { 350 getData(params); 351 } 352 if(returnResTotal.length >= totalTraces && res.traces.length < 50){ 353 allResultsFetched = true; 354 } 355 } else { 356 if (returnResTotal.length == 0) { 357 if (chart != null && chart != "" && chart != undefined) { 358 chart.dispose(); 359 } 360 $("#traces-number").text("0 Traces"); 361 let queryText = "Your query returned no data, adjust your query."; 362 $("#graph-show").html(queryText); 363 $("#graph-show").addClass("empty-result-show"); 364 } 365 } 366 isLoading = false; // Set the flag to false after getting the response 367 }); 368 } 369 const resizeObserver = new ResizeObserver((entries) => { 370 if (chart != null && chart != "" && chart != undefined) chart.resize(); 371 }); 372 resizeObserver.observe(document.getElementById("graph-show")); 373 374 function showScatterPlot() { 375 $("#graph-show").removeClass("empty-result-show"); 376 let chartId = document.getElementById("graph-show"); 377 if (chart != null && chart != "" && chart != undefined) { 378 echarts.dispose(chart); 379 } 380 chart = echarts.init(chartId); 381 let theme = $('body').attr('data-theme') == "light" ? "light" : "dark"; 382 let normalColor = theme == "light" ? "rgba(99, 71, 217, 0.6)" : "rgba(99, 71, 217, 1)"; 383 let errorColor = theme == "light" ? "rgba(233, 49, 37, 0.6)" : "rgba(233, 49, 37, 1)"; 384 let axisLineColor = theme == "light" ? "#DCDBDF" : "#383148"; 385 let axisLabelColor = theme == "light" ? "#160F29" : "#FFFFFF"; 386 chart.setOption({ 387 xAxis: { 388 type: "category", 389 name: "Time", 390 nameTextStyle: { 391 color: axisLabelColor 392 }, 393 data: timeList, 394 scale: true, 395 axisLine: { 396 lineStyle: { 397 color: axisLineColor 398 } 399 }, 400 axisLabel: { 401 color: axisLabelColor 402 }, 403 splitLine: { show: false }, 404 }, 405 yAxis: { 406 type: "value", 407 name: "Duration", 408 nameTextStyle: { 409 color: axisLabelColor 410 }, 411 scale: true, 412 axisLine: { 413 show: true, 414 lineStyle: { 415 color: axisLineColor 416 } 417 }, 418 axisLabel: { 419 color: axisLabelColor 420 }, 421 splitLine: { show: false }, 422 }, 423 tooltip: { 424 show: true, 425 className: "tooltip-design", 426 formatter: function (param) { 427 var green = param.value[4]; 428 var red = param.value[5]; 429 var duration = param.value[1]; 430 var spans = param.value[2]; 431 var errors = param.value[3]; 432 var traceId = param.value[6] ? param.value[6].substring(0, 7) : ''; 433 434 return ( 435 "<div>" + green + ": " + red + 436 "<br>Trace ID: " + traceId + 437 "<br>Duration: " + duration + "ms" + 438 "<br>No. of Spans: " + spans + 439 "<br>No. of Error Spans: " + errors + 440 "</div>" 441 ); 442 }, 443 }, 444 series: [ 445 { 446 type: "effectScatter", 447 showEffectOn: "emphasis", 448 rippleEffect: { 449 scale: 1, 450 }, 451 data: curSpanTraceArray, 452 symbolSize: function (val) { 453 return val[2] < 5 ? 5 : val[2]; 454 }, 455 itemStyle: { 456 color: normalColor, 457 }, 458 }, 459 { 460 type: "effectScatter", 461 showEffectOn: "emphasis", 462 rippleEffect: { 463 scale: 1, 464 }, 465 data: curErrorTraceArray, 466 symbolSize: function (val) { 467 return val[3] < 5 ? 5 : val[3]; 468 }, 469 itemStyle: { 470 color: errorColor, 471 }, 472 }, 473 ], 474 }); 475 // Open Gantt Chart when click on Scatter Chart 476 chart.on('click', function (params) { 477 window.location.href = "trace.html?trace_id=" + params.data[6]; 478 }); 479 } 480 function reSort(){ 481 $(".warn-box").remove(); 482 for (let i = 0; i < returnResTotal.length; i++) { 483 $("#warn-bottom").append(`<div class="warn-box warn-box-${i}"><div class="warn-head"> 484 <div><span id="span-id-head-${i}"></span><span class="span-id-text" id="span-id-${i}"></span></div> 485 <span class = "duration-time" id = "duration-time-${i}"></span> 486 </div> 487 <div class="warn-content"> 488 <div class="spans-box"> 489 <div class = "total-span" id = "total-span-${i}"></div> 490 <div class = "error-span" id = "error-span-${i}"></div> 491 </div> 492 <div> </div> 493 <div class="warn-content-right"> 494 <span class = "start-time" id = "start-time-${i}"></span> 495 <span class = "how-long-time" id = "how-long-time-${i}"></span> 496 </div> 497 </div></div>`); 498 let json = returnResTotal[i]; 499 $(`.warn-box-${i}`).attr("id",json.trace_id ); 500 $(`#span-id-head-${i}`).text(json.service_name + ": " + json.operation_name + " "); 501 $(`#span-id-${i}`).text(json.trace_id.substring(0, 7)); 502 $(`#total-span-${i}`).text( 503 json.span_count + " Spans" 504 ); 505 $(`#error-span-${i}`).text( 506 json.span_errors_count + " Errors" 507 ); 508 let duration = Number((json.end_time - json.start_time) / 1000000); 509 $(`#duration-time-${i}`).text( 510 Math.round(duration * 100) / 100 + "ms" 511 ); 512 let milliseconds = Number(json.start_time / 1000000); 513 let dataStr = new Date(milliseconds).toLocaleString(); 514 let dateText = ""; 515 let date = dataStr.split(","); 516 let dateTime = date[0].split("/"); 517 //current date 518 const currentDate = new Date(); 519 const currentYear = currentDate.getFullYear() + ""; 520 const currentMonth = currentDate.getMonth() + 1 + ""; 521 const currentDay = currentDate.getDate() + ""; 522 if ( 523 currentYear === dateTime[2] && 524 currentMonth === dateTime[0] && 525 currentDay === dateTime[1] 526 ) { 527 dateText = "Today | "; 528 } else { 529 dateText = date[0] + " | "; 530 } 531 dateText = date[0] + " | "; 532 dateText += date[1].toLowerCase(); 533 $(`#start-time-${i}`).text(dateText); 534 let timePass = calculateTimeToNow(json.start_time); 535 let timePassText = ""; 536 if (timePass.days > 2) timePassText = "a few days ago"; 537 else if (timePass.days > 1) timePassText = "yesterday"; 538 else if (timePass.hours == 1) timePassText = timePass.hours + " hour ago"; 539 else if (timePass.hours >= 1) timePassText = timePass.hours + " hours ago"; 540 else if (timePass.minutes == 1) timePassText = timePass.minutes + " minute ago"; 541 else if (timePass.minutes > 1) timePassText = timePass.minutes + " minutes ago"; 542 else if (timePass.minutes < 1) timePassText = "a few seconds ago"; 543 else timePassText = timePass + " hours ago"; 544 $(`#how-long-time-${i}`).text(timePassText); 545 } 546 } 547 548 function calculateTimeToNow(startTime) { 549 const nanosecondsTimestamp = startTime; 550 const millisecondsTimestamp = nanosecondsTimestamp / 1000000; 551 const now = new Date(); 552 const timeDifference = now.getTime() - millisecondsTimestamp; 553 554 const hours = Math.floor(timeDifference / 3600000); 555 const minutes = Math.floor((timeDifference % 3600000) / 60000); 556 const days = Math.floor((timeDifference % 3600000) / 86400000); 557 558 return { 559 hours: hours, 560 minutes: minutes, 561 days: days, 562 }; 563 } 564 let lastScrollPosition = 0; 565 let isLoading = false; // Flag to indicate whether an API call is in progress 566 567 let dashboard = document.getElementById('dashboard'); 568 569 dashboard.onscroll = function () { 570 let scrollHeight = dashboard.scrollHeight; 571 let scrollPosition = dashboard.clientHeight + dashboard.scrollTop; 572 if (!isLoading && hasLoaded && !allResultsFetched && (scrollPosition / scrollHeight >= 0.6)) { // 60% scroll 573 isLoading = true; // Set the flag to true to indicate that an API call is in progress 574 lastScrollPosition = dashboard.scrollTop; 575 getData(); 576 dashboard.scrollTo({ 577 top: lastScrollPosition, 578 behavior: "smooth", 579 }); 580 } 581 }; 582 function getData() { 583 //users did not set limitation 584 if(limitation == -1){ 585 params.page = params.page + 1; 586 searchTrace(params); 587 } else if(limitation > 0){ 588 if (limitation >= 50) { 589 limitation = limitation - 50; 590 params.page = params.page + 1; 591 searchTrace(params); 592 } else { 593 params.page = params.page + 1; 594 searchTrace(params); 595 } 596 } 597 } 598 599 $("body").on("click", ".warn-box", function() { 600 var traceId = $(this).attr("id"); 601 window.location.href = "trace.html?trace_id=" + traceId; 602 });