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 }