github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/integrations/loki/loki.go (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  package loki
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/golang/snappy"
    27  	"github.com/siglens/siglens/pkg/ast"
    28  	"github.com/siglens/siglens/pkg/ast/pipesearch"
    29  	dtu "github.com/siglens/siglens/pkg/common/dtypeutils"
    30  	"github.com/siglens/siglens/pkg/es/writer"
    31  	lokilog "github.com/siglens/siglens/pkg/integrations/loki/log"
    32  	rutils "github.com/siglens/siglens/pkg/readerUtils"
    33  	"github.com/siglens/siglens/pkg/segment"
    34  	"github.com/siglens/siglens/pkg/segment/query/metadata"
    35  	"github.com/siglens/siglens/pkg/segment/reader/record"
    36  	"github.com/siglens/siglens/pkg/segment/search"
    37  	"github.com/siglens/siglens/pkg/segment/structs"
    38  	segwriter "github.com/siglens/siglens/pkg/segment/writer"
    39  	"github.com/siglens/siglens/pkg/utils"
    40  	vtable "github.com/siglens/siglens/pkg/virtualtable"
    41  	log "github.com/sirupsen/logrus"
    42  	"github.com/valyala/fasthttp"
    43  	"google.golang.org/protobuf/encoding/protojson"
    44  	"google.golang.org/protobuf/proto"
    45  )
    46  
    47  const (
    48  	ContentJson        = "application/json; charset=utf-8"
    49  	LOKIINDEX          = "loki-index"
    50  	TimeStamp          = "timestamp"
    51  	Index              = "_index"
    52  	DefaultLimit       = 100
    53  	MsToNanoConversion = 1_000_000
    54  )
    55  
    56  func parseLabels(labelsString string) map[string]string {
    57  	labelsString = strings.Trim(labelsString, "{}")
    58  	labelPairs := strings.Split(labelsString, ", ")
    59  
    60  	labels := make(map[string]string)
    61  
    62  	for _, pair := range labelPairs {
    63  		parts := strings.Split(pair, "=")
    64  		if len(parts) == 2 {
    65  			key := strings.Trim(parts[0], "\"")
    66  			value := strings.Trim(parts[1], "\"")
    67  			labels[key] = value
    68  		}
    69  	}
    70  
    71  	return labels
    72  }
    73  
    74  // Format of loki logs generated by promtail:
    75  //
    76  //	{
    77  //		"streams": [
    78  //		  {
    79  //			"labels": "{filename=\"test.log\",job=\"test\"}",
    80  //			"entries": [
    81  //			  {
    82  //				"timestamp": "2021-03-31T18:00:00.000Z",
    83  //				"line": "test log line"
    84  //			  }
    85  //			]
    86  //		  }
    87  //		]
    88  //	}
    89  func ProcessLokiLogsIngestRequest(ctx *fasthttp.RequestCtx, myid uint64) {
    90  
    91  	responsebody := make(map[string]interface{})
    92  	buf, err := snappy.Decode(nil, ctx.PostBody())
    93  	if err != nil {
    94  		log.Errorf("ProcessLokiLogsIngestRequest: error decompressing request body, err: %v", err)
    95  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
    96  		responsebody["error"] = "Error decompressing request body"
    97  		utils.WriteJsonResponse(ctx, responsebody)
    98  		return
    99  	}
   100  
   101  	logLine := lokilog.PushRequest{}
   102  
   103  	err = proto.Unmarshal(buf, &logLine)
   104  	if err != nil {
   105  		log.Errorf("ProcessLokiLogsIngestRequest: Unable to unmarshal request body, err=%v", err)
   106  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   107  		responsebody["error"] = "Unable to unmarshal request body"
   108  		utils.WriteJsonResponse(ctx, responsebody)
   109  		return
   110  	}
   111  
   112  	logJson := protojson.Format(&logLine)
   113  
   114  	tsNow := utils.GetCurrentTimeInMs()
   115  	indexNameIn := LOKIINDEX
   116  	if !vtable.IsVirtualTablePresent(&indexNameIn, myid) {
   117  		log.Errorf("ProcessLokiLogsIngestRequest: Index name %v does not exist. Adding virtual table name and mapping.", indexNameIn)
   118  		body := logJson
   119  		err = vtable.AddVirtualTable(&indexNameIn, myid)
   120  		if err != nil {
   121  			ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
   122  			responsebody["error"] = "Failed to add virtual table for index"
   123  			utils.WriteJsonResponse(ctx, responsebody)
   124  			return
   125  		}
   126  		err = vtable.AddMappingFromADoc(&indexNameIn, &body, myid)
   127  		if err != nil {
   128  			ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
   129  			responsebody["error"] = "Failed to add mapping from a doc for index"
   130  			utils.WriteJsonResponse(ctx, responsebody)
   131  			return
   132  		}
   133  	}
   134  
   135  	localIndexMap := make(map[string]string)
   136  
   137  	var jsonData map[string][]map[string]interface{}
   138  	err = json.Unmarshal([]byte(logJson), &jsonData)
   139  	if err != nil {
   140  		log.Errorf("ProcessLokiLogsIngestRequest: Unable to unmarshal request body, err=%v", err)
   141  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   142  		responsebody["error"] = "Unable to unmarshal request body"
   143  		utils.WriteJsonResponse(ctx, responsebody)
   144  		return
   145  	}
   146  
   147  	streams := jsonData["streams"]
   148  	allIngestData := make(map[string]interface{})
   149  
   150  	for _, stream := range streams {
   151  		labels := stream["labels"].(string)
   152  		ingestCommonFields := parseLabels(labels)
   153  
   154  		// Note: We might not need separate filename and job fields in the future
   155  		allIngestData["filename"] = ingestCommonFields["filename"]
   156  		allIngestData["job"] = ingestCommonFields["job"]
   157  		allIngestData["labels"] = labels
   158  
   159  		entries, ok := stream["entries"].([]interface{})
   160  		if !ok {
   161  			log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert entries to []interface{}")
   162  			ctx.SetStatusCode(fasthttp.StatusBadRequest)
   163  			responsebody["error"] = "Unable to convert entries to []interface{}"
   164  			utils.WriteJsonResponse(ctx, responsebody)
   165  			return
   166  		}
   167  
   168  		if len(entries) > 0 {
   169  			for _, entry := range entries {
   170  				entryMap, ok := entry.(map[string]interface{})
   171  				if !ok {
   172  					log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert entry to map[string]interface{}")
   173  					ctx.SetStatusCode(fasthttp.StatusBadRequest)
   174  					responsebody["error"] = "Unable to convert entry to map[string]interface{}"
   175  					utils.WriteJsonResponse(ctx, responsebody)
   176  					return
   177  				}
   178  				timestamp, ok := entryMap["timestamp"].(string)
   179  				if !ok {
   180  					log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert timestamp to string")
   181  					ctx.SetStatusCode(fasthttp.StatusBadRequest)
   182  					responsebody["error"] = "Unable to convert timestamp to string"
   183  					utils.WriteJsonResponse(ctx, responsebody)
   184  					return
   185  				}
   186  				line, ok := entryMap["line"].(string)
   187  				if !ok {
   188  					log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert line to string")
   189  					ctx.SetStatusCode(fasthttp.StatusBadRequest)
   190  					responsebody["error"] = "Unable to convert line to string"
   191  					utils.WriteJsonResponse(ctx, responsebody)
   192  					return
   193  				}
   194  
   195  				allIngestData["timestamp"] = timestamp
   196  				allIngestData["line"] = line
   197  
   198  				test, err := json.Marshal(allIngestData)
   199  				if err != nil {
   200  					log.Errorf("ProcessLokiLogsIngestRequest: Unable to marshal data, err=%v", err)
   201  					ctx.SetStatusCode(fasthttp.StatusBadRequest)
   202  					responsebody["error"] = "Unable to marshal request body"
   203  					utils.WriteJsonResponse(ctx, responsebody)
   204  					return
   205  				}
   206  
   207  				err = writer.ProcessIndexRequest([]byte(test), tsNow, indexNameIn, uint64(len(test)), false, localIndexMap, myid)
   208  				if err != nil {
   209  					ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
   210  					responsebody["error"] = "Failed to add entry to in mem buffer"
   211  					utils.WriteJsonResponse(ctx, responsebody)
   212  					return
   213  				}
   214  			}
   215  		}
   216  	}
   217  
   218  	responsebody["status"] = "Success"
   219  	utils.WriteJsonResponse(ctx, responsebody)
   220  	ctx.SetStatusCode(fasthttp.StatusOK)
   221  }
   222  
   223  func ProcessLokiLabelRequest(ctx *fasthttp.RequestCtx) {
   224  	indexName := []string{LOKIINDEX}
   225  	responsebody := make(map[string]interface{})
   226  	colNames := remove(metadata.GetAllColNames(indexName), "line")
   227  	responsebody["data"] = colNames
   228  	responsebody["status"] = "Success"
   229  	utils.WriteJsonResponse(ctx, responsebody)
   230  	ctx.SetStatusCode(fasthttp.StatusOK)
   231  }
   232  
   233  func ProcessLokiLabelValuesRequest(ctx *fasthttp.RequestCtx, myid uint64) {
   234  	responsebody := make(map[string]interface{})
   235  
   236  	labelName := utils.ExtractParamAsString(ctx.UserValue("labelName"))
   237  	indexName := LOKIINDEX
   238  	qid := rutils.GetNextQid()
   239  	colVals, err := ast.GetColValues(labelName, indexName, qid, myid)
   240  
   241  	if err != nil {
   242  		ctx.SetStatusCode(fasthttp.StatusUnauthorized)
   243  		responsebody["error"] = err.Error()
   244  		utils.WriteJsonResponse(ctx, responsebody)
   245  		return
   246  	}
   247  
   248  	responsebody["data"] = colVals
   249  	responsebody["status"] = "Success"
   250  	utils.WriteJsonResponse(ctx, responsebody)
   251  	ctx.SetStatusCode(fasthttp.StatusOK)
   252  }
   253  
   254  func ProcessQueryRequest(ctx *fasthttp.RequestCtx, myid uint64) {
   255  	query := removeUnimplementedMethods(string(ctx.QueryArgs().Peek("query")))
   256  	if query == "" {
   257  		log.Errorf(" ProcessQueryRequest: received empty search request body ")
   258  		responsebody := make(map[string]interface{})
   259  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   260  		responsebody["error"] = "received empty search request body"
   261  		utils.WriteJsonResponse(ctx, responsebody)
   262  		return
   263  	}
   264  
   265  	limit, err := strconv.ParseUint(string(ctx.QueryArgs().Peek("limit")), 10, 64)
   266  	if err != nil {
   267  		responsebody := make(map[string]interface{})
   268  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   269  		responsebody["error"] = err.Error()
   270  		utils.WriteJsonResponse(ctx, responsebody)
   271  		return
   272  	}
   273  
   274  	qid := rutils.GetNextQid()
   275  
   276  	ti := structs.InitTableInfo(LOKIINDEX, myid, false)
   277  	simpleNode, aggs, err := pipesearch.ParseRequest(query, 0, 0, qid, "Log QL", LOKIINDEX)
   278  
   279  	if aggs != nil && aggs.GroupByRequest != nil {
   280  		aggs.GroupByRequest.GroupByColumns = remove(aggs.GroupByRequest.GroupByColumns, "line")
   281  	}
   282  
   283  	if err != nil {
   284  		responsebody := make(map[string]interface{})
   285  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   286  		responsebody["error"] = err.Error()
   287  		utils.WriteJsonResponse(ctx, responsebody)
   288  		return
   289  	}
   290  
   291  	segment.LogASTNode("logql query parser", simpleNode, qid)
   292  	segment.LogQueryAggsNode("logql aggs parser", aggs, qid)
   293  	startTime := utils.GetCurrentTimeInMs()
   294  	qc := structs.InitQueryContextWithTableInfo(ti, limit, 0, myid, false)
   295  	queryResult := segment.ExecuteQuery(simpleNode, aggs, qid, qc)
   296  
   297  	allJsons, allCols, err := record.GetJsonFromAllRrc(queryResult.AllRecords, false, qid, queryResult.SegEncToKey, aggs)
   298  
   299  	if len(queryResult.MeasureResults) > 0 {
   300  		lokiMetricsResponse := getMetricsResponse(queryResult)
   301  		utils.WriteJsonResponse(ctx, lokiMetricsResponse)
   302  		ctx.SetStatusCode(fasthttp.StatusOK)
   303  		return
   304  	}
   305  
   306  	lokiQueryResponse := LokiQueryResponse{}
   307  
   308  	if len(allJsons) > 0 {
   309  		lokiQueryResponse.Data = Data{ResultType: "streams"}
   310  		lokiQueryResponse.Data.Result = make([]StreamValue, 0)
   311  		for _, row := range allJsons {
   312  			queryResultLine := StreamValue{Values: make([][]string, 0)}
   313  			line, ok := row["line"].(string)
   314  			if !ok {
   315  				responsebody := make(map[string]interface{})
   316  				log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert line to string")
   317  				ctx.SetStatusCode(fasthttp.StatusBadRequest)
   318  				responsebody["error"] = "ProcessLokiLogsIngestRequest: Unable to convert line to string"
   319  				utils.WriteJsonResponse(ctx, responsebody)
   320  			}
   321  			valuesRow := make([]string, 0)
   322  			timeStamp, ok := row["timestamp"].(uint64)
   323  			if !ok {
   324  				responsebody := make(map[string]interface{})
   325  				log.Errorf("ProcessLokiLogsIngestRequest: Unable to convert line to string")
   326  				ctx.SetStatusCode(fasthttp.StatusBadRequest)
   327  				responsebody["error"] = "ProcessLokiLogsIngestRequest: Unable to convert line to string"
   328  				utils.WriteJsonResponse(ctx, responsebody)
   329  			}
   330  			valuesRow = append(valuesRow, fmt.Sprintf("%v", timeStamp*MsToNanoConversion), line)
   331  			queryResultLine.Values = append(queryResultLine.Values, valuesRow)
   332  
   333  			newRow := make(map[string]interface{}, 0)
   334  			labelsKeys := remove(allCols, "line")
   335  			for _, label := range labelsKeys {
   336  				if label != TimeStamp && label != Index {
   337  					newRow[label] = row[label]
   338  				}
   339  			}
   340  
   341  			queryResultLine.Stream = newRow
   342  			lokiQueryResponse.Data.Result = append(lokiQueryResponse.Data.Result, queryResultLine)
   343  		}
   344  	} else {
   345  		lokiQueryResponse.Data.Result = make([]StreamValue, 0)
   346  	}
   347  
   348  	if err != nil {
   349  		log.Errorf(" ProcessMetricsSearchRequest: received empty search request body ")
   350  		responsebody := make(map[string]interface{})
   351  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   352  		responsebody["error"] = "received empty search request body"
   353  		utils.WriteJsonResponse(ctx, responsebody)
   354  		_, err = ctx.WriteString(err.Error())
   355  		if err != nil {
   356  			log.Errorf("qid=%v, ProcessMetricsSearchRequest: could not write error message err=%v", qid, err)
   357  		}
   358  		log.Errorf("qid=%v, ProcessMetricsSearchRequest: failed to decode search request body! Err=%+v", qid, err)
   359  		return
   360  	}
   361  
   362  	lokiQueryResponse.Data.Stats = getQueryStats(queryResult, startTime, myid)
   363  
   364  	utils.WriteJsonResponse(ctx, lokiQueryResponse)
   365  	ctx.SetStatusCode(fasthttp.StatusOK)
   366  
   367  }
   368  
   369  func ProcessIndexStatsRequest(ctx *fasthttp.RequestCtx, myid uint64) {
   370  	query := removeUnimplementedMethods(string(ctx.QueryArgs().Peek("query")))
   371  
   372  	qid := rutils.GetNextQid()
   373  
   374  	ti := structs.InitTableInfo(LOKIINDEX, myid, false)
   375  	simpleNode, aggs, err := pipesearch.ParseQuery(query, qid, "Log QL")
   376  	if err != nil {
   377  		writeEmptyIndexStatsResponse(ctx)
   378  		return
   379  	}
   380  
   381  	segment.LogASTNode("logql query parser", simpleNode, qid)
   382  	segment.LogQueryAggsNode("logql aggs parser", aggs, qid)
   383  
   384  	simpleNode.TimeRange = rutils.GetESDefaultQueryTimeRange()
   385  	qc := structs.InitQueryContextWithTableInfo(ti, rutils.DefaultBucketCount, 0, myid, false)
   386  
   387  	queryResult := segment.ExecuteQuery(simpleNode, aggs, qid, qc)
   388  	allJsons, allCols, err := record.GetJsonFromAllRrc(queryResult.AllRecords, false, qid, queryResult.SegEncToKey, aggs)
   389  	if err != nil {
   390  		writeEmptyIndexStatsResponse(ctx)
   391  		return
   392  	}
   393  
   394  	responsebody := make(map[string]interface{})
   395  	responsebody["streams"] = len(allCols)
   396  	responsebody["chunks"] = getChunkCount(queryResult)
   397  	responsebody["entries"] = len(allJsons)
   398  	byteCount := 0
   399  	for _, row := range allJsons {
   400  		lineString, ok := row["line"].(string)
   401  		if !ok {
   402  			writeEmptyIndexStatsResponse(ctx)
   403  			return
   404  		}
   405  		byteCount += len([]byte(lineString))
   406  	}
   407  	responsebody["bytes"] = byteCount
   408  	utils.WriteJsonResponse(ctx, responsebody)
   409  	ctx.SetStatusCode(fasthttp.StatusOK)
   410  
   411  }
   412  
   413  func ProcessLokiSeriesRequest(ctx *fasthttp.RequestCtx, myid uint64) {
   414  	responsebody := make(map[string]interface{})
   415  	query := string(ctx.QueryArgs().Peek("match[]"))
   416  	startEpoch, err := strconv.ParseUint(string(ctx.QueryArgs().Peek("start")), 10, 64)
   417  	if err != nil {
   418  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   419  		responsebody["error"] = err.Error()
   420  		utils.WriteJsonResponse(ctx, responsebody)
   421  		return
   422  	}
   423  	endEpoch, err := strconv.ParseUint(string(ctx.QueryArgs().Peek("end")), 10, 64)
   424  	if err != nil {
   425  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   426  		responsebody["error"] = err.Error()
   427  		utils.WriteJsonResponse(ctx, responsebody)
   428  		return
   429  	}
   430  	responsebody["status"] = "success"
   431  
   432  	qid := rutils.GetNextQid()
   433  
   434  	ti := structs.InitTableInfo(LOKIINDEX, myid, false)
   435  	simpleNode, aggs, err := pipesearch.ParseQuery(query, qid, "Log QL")
   436  	if err != nil {
   437  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   438  		responsebody["error"] = err.Error()
   439  		utils.WriteJsonResponse(ctx, responsebody)
   440  		return
   441  	}
   442  
   443  	segment.LogASTNode("logql query parser", simpleNode, qid)
   444  	segment.LogQueryAggsNode("logql aggs parser", aggs, qid)
   445  
   446  	simpleNode.TimeRange = &dtu.TimeRange{
   447  		StartEpochMs: startEpoch / MsToNanoConversion,
   448  		EndEpochMs:   endEpoch / MsToNanoConversion,
   449  	}
   450  	qc := structs.InitQueryContextWithTableInfo(ti, DefaultLimit, 0, myid, false)
   451  	queryResult := segment.ExecuteQuery(simpleNode, aggs, qid, qc)
   452  	allJsons, _, err := record.GetJsonFromAllRrc(queryResult.AllRecords, false, qid, queryResult.SegEncToKey, aggs)
   453  	if err != nil {
   454  		ctx.SetStatusCode(fasthttp.StatusBadRequest)
   455  		responsebody["error"] = err.Error()
   456  		utils.WriteJsonResponse(ctx, responsebody)
   457  		return
   458  	}
   459  	responsebody["data"] = allJsons
   460  	utils.WriteJsonResponse(ctx, responsebody)
   461  	ctx.SetStatusCode(fasthttp.StatusOK)
   462  }
   463  
   464  func writeEmptyIndexStatsResponse(ctx *fasthttp.RequestCtx) {
   465  	responsebody := make(map[string]interface{})
   466  	responsebody["streams"] = 0
   467  	responsebody["chunks"] = 0
   468  	responsebody["entries"] = 0
   469  	responsebody["bytes"] = 0
   470  	utils.WriteJsonResponse(ctx, responsebody)
   471  	ctx.SetStatusCode(fasthttp.StatusOK)
   472  }
   473  
   474  func remove(slice []string, stringToRemove string) []string {
   475  	for i, v := range slice {
   476  		if v == stringToRemove {
   477  			return append(slice[:i], slice[i+1:]...)
   478  		}
   479  	}
   480  	return slice
   481  }
   482  
   483  // needs to be modified as changes are made to logql parsing. Logfmt, json, label/line expression
   484  // and stream selectors are supported
   485  func removeUnimplementedMethods(queryString string) string {
   486  
   487  	// Define the regular expression pattern
   488  	pattern := `sum\s+by\s+\(level\)\s+\(count_over_time\(`
   489  	regex := regexp.MustCompile(pattern)
   490  	matchIndex := regex.FindStringIndex(queryString)
   491  	if matchIndex != nil {
   492  		extractedString := queryString[matchIndex[1]:]
   493  		return extractedString
   494  	} else {
   495  		return queryString
   496  	}
   497  }
   498  
   499  func getMetricsResponse(queryResult *structs.NodeResult) LokiMetricsResponse {
   500  	lokiMetricsResponse := LokiMetricsResponse{}
   501  	lokiMetricsResponse.Status = "success"
   502  	lokiMetricsResponse.Data = MetricsData{ResultType: "vector"}
   503  	lokiMetricsResponse.Data.MetricResult = make([]MetricValue, 0)
   504  	if queryResult.MeasureResults != nil {
   505  		metricResult := make([]MetricValue, 0)
   506  		for _, bucket := range queryResult.MeasureResults {
   507  			groupByCols := queryResult.GroupByCols
   508  			newMetricVal := MetricValue{}
   509  			newMetricVal.Stream = make(map[string]interface{})
   510  			for index, colName := range groupByCols {
   511  				newMetricVal.Stream[colName] = bucket.GroupByValues[index]
   512  			}
   513  			valResult := make([]interface{}, 0)
   514  			valResult = append(valResult, 1689919818.158, queryResult.MeasureFunctions[0])
   515  			newMetricVal.Values = valResult
   516  			metricResult = append(metricResult, newMetricVal)
   517  		}
   518  		lokiMetricsResponse.Data.MetricResult = metricResult
   519  	}
   520  
   521  	lokiMetricsResponse.Data.Stats = MetricStats{}
   522  	return lokiMetricsResponse
   523  }
   524  func getQueryStats(queryResult *structs.NodeResult, startTime uint64, myid uint64) Stats {
   525  	lokiQueryStats := Stats{}
   526  	if queryResult == nil {
   527  		return lokiQueryStats
   528  	}
   529  
   530  	bytesReceivedCount, recordCount, onDiskBytesCount := segwriter.GetVTableCounts(LOKIINDEX, myid)
   531  	unrotatedByteCount, unrotatedEventCount, unrotatedOnDiskBytesCount := segwriter.GetUnrotatedVTableCounts(LOKIINDEX, myid)
   532  
   533  	chunkCount := getChunkCount(queryResult)
   534  
   535  	ingesterStats := Ingester{}
   536  	ingesterStats.CompressedBytes = int(onDiskBytesCount + unrotatedOnDiskBytesCount)
   537  	ingesterStats.DecompressedBytes = int(bytesReceivedCount + unrotatedByteCount)
   538  	ingesterStats.DecompressedLines = unrotatedEventCount
   539  	ingesterStats.TotalReached = 1 //single node
   540  	ingesterStats.TotalLinesSent = len(queryResult.AllRecords)
   541  	ingesterStats.TotalChunksMatched = chunkCount
   542  	ingesterStats.TotalBatches = chunkCount * search.BLOCK_BATCH_SIZE
   543  	ingesterStats.HeadChunkBytes = int(onDiskBytesCount)
   544  	ingesterStats.HeadChunkLines = int(recordCount)
   545  
   546  	lokiQueryStats.Ingester = ingesterStats
   547  
   548  	storeStats := Store{}
   549  	storeStats.DecompressedBytes = int(unrotatedOnDiskBytesCount) + int(onDiskBytesCount)
   550  	summaryStats := Summary{}
   551  
   552  	summaryStats.TotalBytesProcessed = int(bytesReceivedCount) + int(unrotatedOnDiskBytesCount)
   553  	summaryStats.ExecTime = float64(utils.GetCurrentTimeInMs() - startTime)
   554  	summaryStats.TotalLinesProcessed = len(queryResult.AllRecords)
   555  
   556  	return lokiQueryStats
   557  }
   558  
   559  func getChunkCount(queryResult *structs.NodeResult) int {
   560  	uniqueChunks := make(map[uint16]bool)
   561  	for _, record := range queryResult.AllRecords {
   562  		uniqueChunks[record.BlockNum] = true
   563  	}
   564  	return len(uniqueChunks)
   565  }