github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/health/clusterStatsHandler.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 health 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "sort" 24 "strconv" 25 "strings" 26 27 "github.com/dustin/go-humanize" 28 jsoniter "github.com/json-iterator/go" 29 "github.com/siglens/siglens/pkg/hooks" 30 segwriter "github.com/siglens/siglens/pkg/segment/writer" 31 "github.com/siglens/siglens/pkg/segment/writer/metrics" 32 mmeta "github.com/siglens/siglens/pkg/segment/writer/metrics/meta" 33 "github.com/siglens/siglens/pkg/usageStats" 34 "github.com/siglens/siglens/pkg/utils" 35 vtable "github.com/siglens/siglens/pkg/virtualtable" 36 log "github.com/sirupsen/logrus" 37 "github.com/valyala/fasthttp" 38 ) 39 40 func ProcessClusterStatsHandler(ctx *fasthttp.RequestCtx, myid uint64) { 41 42 var httpResp utils.ClusterStatsResponseInfo 43 var err error 44 if hook := hooks.GlobalHooks.MiddlewareExtractOrgIdHook; hook != nil { 45 myid, err = hook(ctx) 46 if err != nil { 47 log.Errorf("ProcessClusterStatsHandler: failed to extract orgId from context. Err=%+v", err) 48 utils.SetBadMsg(ctx, "") 49 return 50 } 51 } 52 indexData, logsEventCount, logsIncomingBytes, logsOnDiskBytes := getIngestionStats(myid) 53 queryCount, totalResponseTime, querieSinceInstall := usageStats.GetQueryStats(myid) 54 55 metricsIncomingBytes, metricsDatapointsCount, metricsOnDiskBytes := getMetricsStats(myid) 56 metricsImMemBytes := metrics.GetTotalEncodedSize() 57 58 if hook := hooks.GlobalHooks.AddMultinodeStatsHook; hook != nil { 59 hook(indexData, myid, &logsIncomingBytes, &logsOnDiskBytes, &logsEventCount, 60 &metricsIncomingBytes, &metricsOnDiskBytes, &metricsDatapointsCount, 61 &queryCount, &totalResponseTime) 62 } 63 64 httpResp.IngestionStats = make(map[string]interface{}) 65 httpResp.QueryStats = make(map[string]interface{}) 66 httpResp.MetricsStats = make(map[string]interface{}) 67 68 httpResp.IngestionStats["Log Incoming Volume"] = convertBytesToGB(logsIncomingBytes) 69 httpResp.IngestionStats["Incoming Volume"] = convertBytesToGB(logsIncomingBytes + float64(metricsIncomingBytes)) 70 71 httpResp.IngestionStats["Metrics Incoming Volume"] = convertBytesToGB(float64(metricsIncomingBytes)) 72 73 httpResp.IngestionStats["Event Count"] = humanize.Comma(int64(logsEventCount)) 74 75 httpResp.IngestionStats["Log Storage Used"] = convertBytesToGB(logsOnDiskBytes) 76 httpResp.IngestionStats["Metrics Storage Used"] = convertBytesToGB(float64(metricsOnDiskBytes + metricsImMemBytes)) 77 totalOnDiskBytes := logsOnDiskBytes + float64(metricsOnDiskBytes) + float64(metricsImMemBytes) 78 httpResp.IngestionStats["Storage Saved"] = (1 - (totalOnDiskBytes / (logsIncomingBytes + float64(metricsIncomingBytes)))) * 100 79 80 if hook := hooks.GlobalHooks.SetExtraIngestionStatsHook; hook != nil { 81 hook(httpResp.IngestionStats) 82 } 83 84 httpResp.MetricsStats["Incoming Volume"] = convertBytesToGB(float64(metricsIncomingBytes)) 85 httpResp.MetricsStats["Datapoints Count"] = humanize.Comma(int64(metricsDatapointsCount)) 86 87 httpResp.QueryStats["Query Count"] = queryCount 88 httpResp.QueryStats["Queries Since Install"] = querieSinceInstall 89 90 if queryCount > 1 { 91 httpResp.QueryStats["Average Latency"] = fmt.Sprintf("%v", utils.ToFixed(totalResponseTime/float64(queryCount), 3)) + " ms" 92 } else { 93 httpResp.QueryStats["Average Latency"] = fmt.Sprintf("%v", utils.ToFixed(totalResponseTime, 3)) + " ms" 94 } 95 96 httpResp.IndexStats = convertIndexDataToSlice(indexData) 97 utils.WriteJsonResponse(ctx, httpResp) 98 99 } 100 101 func convertIndexDataToSlice(indexData map[string]utils.ResultPerIndex) []utils.ResultPerIndex { 102 retVal := make([]utils.ResultPerIndex, 0, len(indexData)) 103 i := 0 104 for idx, v := range indexData { 105 nextVal := make(utils.ResultPerIndex) 106 nextVal[idx] = make(map[string]interface{}) 107 nextVal[idx]["ingestVolume"] = convertBytesToGB(v[idx]["ingestVolume"].(float64)) 108 nextVal[idx]["eventCount"] = humanize.Comma(int64(v[idx]["eventCount"].(uint64))) 109 retVal = append(retVal, nextVal) 110 i++ 111 } 112 return retVal[:i] 113 } 114 115 func ProcessClusterIngestStatsHandler(ctx *fasthttp.RequestCtx, orgId uint64) { 116 var err error 117 if hook := hooks.GlobalHooks.MiddlewareExtractOrgIdHook; hook != nil { 118 orgId, err = hook(ctx) 119 if err != nil { 120 log.Errorf("ProcessClusterIngestStatsHandler: failed to extract orgId from context. Err=%+v", err) 121 utils.SetBadMsg(ctx, "") 122 return 123 } 124 } 125 126 var httpResp utils.ClusterStatsResponseInfo 127 rawJSON := ctx.PostBody() 128 if rawJSON == nil { 129 log.Errorf(" ClusterIngestStatsHandler: received empty search request body ") 130 utils.SetBadMsg(ctx, "") 131 return 132 } 133 134 readJSON := make(map[string]interface{}) 135 var jsonc = jsoniter.ConfigCompatibleWithStandardLibrary 136 decoder := jsonc.NewDecoder(bytes.NewReader(rawJSON)) 137 decoder.UseNumber() 138 err = decoder.Decode(&readJSON) 139 if err != nil { 140 ctx.SetStatusCode(fasthttp.StatusBadRequest) 141 _, err = ctx.WriteString(err.Error()) 142 if err != nil { 143 log.Errorf(" ClusterIngestStatsHandler: could not write error message err=%v", err) 144 } 145 log.Errorf(" ClusterIngestStatsHandler: failed to decode search request body! Err=%+v", err) 146 return 147 } 148 149 pastXhours, granularity := parseIngestionStatsRequest(readJSON) 150 rStats, _ := usageStats.GetUsageStats(pastXhours, granularity, orgId) 151 httpResp.ChartStats = make(map[string]map[string]interface{}) 152 153 for k, entry := range rStats { 154 httpResp.ChartStats[k] = make(map[string]interface{}, 2) 155 httpResp.ChartStats[k]["EventCount"] = entry.EventCount 156 httpResp.ChartStats[k]["MetricsCount"] = entry.MetricsDatapointsCount 157 httpResp.ChartStats[k]["GBCount"] = float64(entry.BytesCount) / 1_000_000_000 158 } 159 utils.WriteJsonResponse(ctx, httpResp) 160 } 161 162 func parseAlphaNumTime(inp string, defValue uint64) (uint64, usageStats.UsageStatsGranularity) { 163 granularity := usageStats.Daily 164 sanTime := strings.ReplaceAll(inp, " ", "") 165 retVal := defValue 166 167 strln := len(sanTime) 168 if strln < 6 { 169 return retVal, usageStats.Daily 170 } 171 172 unit := sanTime[strln-1] 173 num, err := strconv.ParseInt(sanTime[4:strln-1], 0, 64) 174 if err != nil { 175 return defValue, usageStats.Daily 176 } 177 178 switch unit { 179 case 'h': 180 retVal = uint64(num) 181 granularity = usageStats.Hourly 182 case 'd': 183 retVal = 24 * uint64(num) 184 granularity = usageStats.Daily 185 } 186 //for past 2 days , set granularity to Hourly 187 if num <= 2 { 188 granularity = usageStats.Hourly 189 } 190 return retVal, granularity 191 } 192 193 func parseIngestionStatsRequest(jsonSource map[string]interface{}) (uint64, usageStats.UsageStatsGranularity) { 194 var pastXhours uint64 195 granularity := usageStats.Daily 196 startE, ok := jsonSource["startEpoch"] 197 if !ok || startE == nil { 198 pastXhours = uint64(7 * 24) 199 } else { 200 switch val := startE.(type) { 201 case json.Number: 202 temp, _ := val.Int64() 203 pastXhours = uint64(temp) 204 case float64: 205 pastXhours = uint64(val) 206 case int64: 207 pastXhours = uint64(val) 208 case uint64: 209 pastXhours = uint64(val) 210 case string: 211 defValue := uint64(7 * 24) 212 pastXhours, granularity = parseAlphaNumTime(string(val), defValue) 213 default: 214 pastXhours = uint64(7 * 24) 215 } 216 } 217 return pastXhours, granularity 218 } 219 220 func getIngestionStats(myid uint64) (map[string]utils.ResultPerIndex, int64, float64, float64) { 221 222 totalIncomingBytes := float64(0) 223 totalEventCount := int64(0) 224 totalOnDiskBytes := float64(0) 225 226 ingestionStats := make(map[string]utils.ResultPerIndex) 227 allVirtualTableNames, err := vtable.GetVirtualTableNames(myid) 228 sortedIndices := make([]string, 0, len(allVirtualTableNames)) 229 230 for k := range allVirtualTableNames { 231 sortedIndices = append(sortedIndices, k) 232 } 233 sort.Strings(sortedIndices) 234 235 if err != nil { 236 log.Errorf("getIngestionStats: Error in getting virtual table names, err:%v", err) 237 } 238 239 allvtableCnts := segwriter.GetVTableCountsForAll(myid) 240 241 for _, indexName := range sortedIndices { 242 if indexName == "" { 243 log.Errorf("getIngestionStats: skipping an empty index name indexName=%v", indexName) 244 continue 245 } 246 247 cnts, ok := allvtableCnts[indexName] 248 if !ok { 249 continue 250 } 251 252 unrotatedByteCount, unrotatedEventCount, unrotatedOnDiskBytesCount := segwriter.GetUnrotatedVTableCounts(indexName, myid) 253 254 totalEventsForIndex := uint64(cnts.RecordCount) + uint64(unrotatedEventCount) 255 totalEventCount += int64(totalEventsForIndex) 256 257 totalBytesReceivedForIndex := float64(cnts.BytesCount + unrotatedByteCount) 258 totalIncomingBytes += totalBytesReceivedForIndex 259 260 totalOnDiskBytesCountForIndex := uint64(cnts.OnDiskBytesCount + unrotatedOnDiskBytesCount) 261 totalOnDiskBytes += float64(totalOnDiskBytesCountForIndex) 262 263 perIndexStat := make(map[string]map[string]interface{}) 264 265 perIndexStat[indexName] = make(map[string]interface{}) 266 267 perIndexStat[indexName]["ingestVolume"] = totalBytesReceivedForIndex 268 perIndexStat[indexName]["eventCount"] = totalEventsForIndex 269 270 ingestionStats[indexName] = perIndexStat 271 } 272 return ingestionStats, totalEventCount, totalIncomingBytes, totalOnDiskBytes 273 } 274 275 func convertBytesToGB(bytes float64) string { 276 convertedGB := bytes / 1_000_000_000 277 finalStr := fmt.Sprintf("%.3f", convertedGB) + " GB" 278 return finalStr 279 } 280 281 func getMetricsStats(myid uint64) (uint64, uint64, uint64) { 282 bytesCount := uint64(0) 283 onDiskBytesCount := uint64(0) 284 recCount := uint64(0) 285 allMetricsMetas, err := mmeta.GetAllMetricsMetaEntries(myid) 286 if err != nil { 287 log.Errorf("populateMetricsMetadata: unable to get all the metrics meta entries. Error: %v", err) 288 return bytesCount, recCount, onDiskBytesCount 289 } 290 for _, mMetaInfo := range allMetricsMetas { 291 if mMetaInfo.OrgId == myid { 292 onDiskBytesCount += mMetaInfo.OnDiskBytes 293 bytesCount += mMetaInfo.BytesReceivedCount 294 recCount += mMetaInfo.DatapointCount 295 } 296 } 297 unrotatedIncoming, unrotatedOnDisk, unrotatedRecs := metrics.GetUnrotatedMetricStats(myid) 298 bytesCount += unrotatedIncoming 299 onDiskBytesCount += unrotatedOnDisk 300 recCount += unrotatedRecs 301 return bytesCount, recCount, onDiskBytesCount 302 }