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  });