github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/usageStats/stats.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 usageStats 18 19 import ( 20 "encoding/csv" 21 "io" 22 "os" 23 "path" 24 "strconv" 25 "strings" 26 "sync/atomic" 27 "time" 28 29 "github.com/siglens/siglens/pkg/config" 30 "github.com/siglens/siglens/pkg/hooks" 31 . "github.com/siglens/siglens/pkg/segment/utils" 32 log "github.com/sirupsen/logrus" 33 "golang.org/x/text/language" 34 "golang.org/x/text/message" 35 ) 36 37 type UsageStatsGranularity uint8 38 39 const ( 40 Hourly UsageStatsGranularity = iota + 1 41 Daily 42 ) 43 44 type Stats struct { 45 BytesCount uint64 46 LogLinesCount uint64 47 TotalBytesCount uint64 48 TotalLogLinesCount uint64 49 MetricsDatapointsCount uint64 50 TotalMetricsDatapointsCount uint64 51 } 52 53 var ustats = make(map[uint64]*Stats) 54 55 var msgPrinter *message.Printer 56 57 type QueryStats struct { 58 QueryCount uint64 59 QueriesSinceInstall uint64 60 TotalRespTime float64 61 } 62 63 var QueryStatsMap = make(map[uint64]*QueryStats) 64 65 type ReadStats struct { 66 BytesCount uint64 67 EventCount uint64 68 MetricsDatapointsCount uint64 69 TimeStamp time.Time 70 } 71 72 func StartUsageStats() { 73 msgPrinter = message.NewPrinter(language.English) 74 75 if hook := hooks.GlobalHooks.GetQueryCountHook; hook != nil { 76 hook() 77 } else { 78 GetQueryCount() 79 } 80 81 go writeUsageStats() 82 } 83 84 func GetQueryCount() { 85 QueryStatsMap[0] = &QueryStats{ 86 QueryCount: 0, 87 QueriesSinceInstall: 0, 88 TotalRespTime: 0, 89 } 90 err := ReadQueryStats(0) 91 if err != nil { 92 log.Errorf("ReadQueryStats from file failed:%v\n", err) 93 } 94 } 95 96 func ReadQueryStats(orgid uint64) error { 97 filename := getQueryStatsFilename(getBaseQueryStatsDir(orgid)) 98 fd, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 99 if err != nil { 100 return err 101 } 102 defer fd.Close() 103 r := csv.NewReader(fd) 104 val, err := r.ReadAll() 105 if err != nil { 106 log.Errorf("readQueryStats: read records failed, err=%v", err) 107 return err 108 } 109 if len(val) > 0 { 110 flushedQueriesSinceInstall, err := strconv.ParseUint(val[len(val)-1][0], 10, 64) 111 if err != nil { 112 return err 113 } 114 if QueryStatsMap[orgid] != nil { 115 QueryStatsMap[orgid].QueriesSinceInstall = flushedQueriesSinceInstall 116 } 117 } 118 return nil 119 } 120 121 func GetBaseStatsDir(orgid uint64) string { 122 123 var sb strings.Builder 124 timeNow := uint64(time.Now().UnixNano()) / uint64(time.Millisecond) 125 sb.WriteString(config.GetDataPath() + "ingestnodes/" + config.GetHostID() + "/usageStats/") 126 if orgid != 0 { 127 sb.WriteString(strconv.FormatUint(orgid, 10)) 128 sb.WriteString("/") 129 } 130 t1 := time.Unix(int64(timeNow/1000), int64((timeNow%1000)*1000)) 131 sb.WriteString(t1.UTC().Format("2006/01/02")) 132 sb.WriteString("/") 133 basedir := sb.String() 134 return basedir 135 } 136 137 func getBaseQueryStatsDir(orgid uint64) string { 138 139 var sb strings.Builder 140 sb.WriteString(config.GetDataPath() + "querynodes/" + config.GetHostID() + "/") 141 if orgid != 0 { 142 sb.WriteString(strconv.FormatUint(orgid, 10)) 143 sb.WriteString("/") 144 } 145 basedir := sb.String() 146 return basedir 147 } 148 149 func getBaseStatsDirs(startTime, endTime time.Time, orgid uint64) []string { 150 startTOD := (startTime.UnixMilli() / MS_IN_DAY) * MS_IN_DAY 151 endTOD := (endTime.UnixMilli() / MS_IN_DAY) * MS_IN_DAY 152 ingestDir := config.GetIngestNodeBaseDir() 153 // read all files in dir 154 155 files, err := os.ReadDir(ingestDir) 156 if err != nil { 157 log.Errorf("ReadAllSegmetas: read dir err=%v ", err) 158 return make([]string, 0) 159 } 160 161 // read all iNodes 162 iNodes := make([]string, 0) 163 for _, file := range files { 164 fName := file.Name() 165 iNodes = append(iNodes, fName) 166 } 167 168 statsDirs := make([]string, 0) 169 for _, iNode := range iNodes { 170 mDir := path.Join(ingestDir, iNode, "usageStats") 171 if _, err := os.Stat(mDir); err != nil { 172 continue 173 } 174 fileStartTOD := startTOD 175 fileEndTOD := endTOD 176 fileStartTime := startTime 177 for fileEndTOD >= fileStartTOD { 178 var sb strings.Builder 179 sb.WriteString(mDir) 180 sb.WriteString("/") 181 if orgid != 0 { 182 sb.WriteString(strconv.FormatUint(orgid, 10)) 183 } 184 sb.WriteString("/") 185 timeNow := uint64(fileStartTime.UnixNano()) / uint64(time.Millisecond) 186 t1 := time.Unix(int64(timeNow/1000), int64((timeNow%1000)*1000)) 187 sb.WriteString(t1.UTC().Format("2006/01/02")) 188 sb.WriteString("/") 189 statsDirs = append(statsDirs, sb.String()) 190 fileStartTOD = fileStartTOD + MS_IN_DAY 191 fileStartTime = fileStartTime.AddDate(0, 0, 1) 192 } 193 194 } 195 196 return statsDirs 197 } 198 199 func getStatsFilename(baseDir string) string { 200 var sb strings.Builder 201 202 err := os.MkdirAll(baseDir, 0764) 203 if err != nil { 204 log.Errorf("getStatsFilename, mkdirall failed, basedir=%v, err=%v", baseDir, err) 205 return "" 206 } 207 _, err = sb.WriteString(baseDir) 208 if err != nil { 209 log.Errorf("getStatsFilename, writestring basedir failed,err=%v", err) 210 } 211 _, err = sb.WriteString("usage_stats.csv") 212 if err != nil { 213 log.Errorf("getStatsFilename, writestring file failed,err=%v", err) 214 } 215 return sb.String() 216 } 217 218 func getQueryStatsFilename(baseDir string) string { 219 var sb strings.Builder 220 221 err := os.MkdirAll(baseDir, 0764) 222 if err != nil { 223 log.Errorf("getQueryStatsFilename, mkdirall failed, basedir=%v, err=%v", baseDir, err) 224 return "" 225 } 226 _, err = sb.WriteString(baseDir) 227 if err != nil { 228 log.Errorf("getQueryStatsFilename, writestring basedir failed,err=%v", err) 229 } 230 _, err = sb.WriteString("usage_queryStats.csv") 231 if err != nil { 232 log.Errorf("getQueryStatsFilename, writestring file failed,err=%v", err) 233 } 234 return sb.String() 235 } 236 237 func writeUsageStats() { 238 for { 239 alreadyHandled := false 240 if hook := hooks.GlobalHooks.WriteUsageStatsIfConditionHook; hook != nil { 241 alreadyHandled = hook() 242 } 243 244 if !alreadyHandled { 245 go func() { 246 err := FlushStatsToFile(0) 247 if err != nil { 248 log.Errorf("WriteUsageStats failed:%v\n", err) 249 } 250 errC := flushCompressedStatsToFile(0) 251 if errC != nil { 252 log.Errorf("WriteUsageStats failed:%v\n", errC) 253 } 254 255 }() 256 257 if hook := hooks.GlobalHooks.WriteUsageStatsElseExtraLogicHook; hook != nil { 258 hook() 259 } 260 } 261 time.Sleep(1 * time.Minute) 262 } 263 } 264 265 func ForceFlushStatstoFile() { 266 alreadyHandled := false 267 if hook := hooks.GlobalHooks.ForceFlushIfConditionHook; hook != nil { 268 alreadyHandled = hook() 269 } 270 if alreadyHandled { 271 return 272 } 273 274 err := FlushStatsToFile(0) 275 if err != nil { 276 log.Errorf("ForceFlushStatstoFile failed:%v\n", err) 277 } 278 } 279 280 func logStatSummary(orgid uint64) { 281 if _, ok := ustats[orgid]; ok { 282 log.Infof("Ingest stats: past minute : events=%v, metrics=%v, bytes=%v", 283 msgPrinter.Sprintf("%v", ustats[orgid].LogLinesCount), 284 msgPrinter.Sprintf("%v", ustats[orgid].MetricsDatapointsCount), 285 msgPrinter.Sprintf("%v", ustats[orgid].BytesCount)) 286 287 log.Infof("Ingest stats: total so far: events=%v, metrics=%v, bytes=%v", 288 msgPrinter.Sprintf("%v", ustats[orgid].TotalLogLinesCount), 289 msgPrinter.Sprintf("%v", ustats[orgid].TotalMetricsDatapointsCount), 290 msgPrinter.Sprintf("%v", ustats[orgid].TotalBytesCount)) 291 } 292 } 293 294 func GetTotalLogLines(orgid uint64) uint64 { 295 return ustats[orgid].TotalLogLinesCount 296 } 297 298 func FlushStatsToFile(orgid uint64) error { 299 if _, ok := QueryStatsMap[orgid]; ok { 300 filename := getQueryStatsFilename(getBaseQueryStatsDir(orgid)) 301 fd, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 302 if err != nil { 303 return err 304 } 305 defer fd.Close() 306 w := csv.NewWriter(fd) 307 var records [][]string 308 var record []string 309 queriesSinceInstallAsString := strconv.FormatUint(QueryStatsMap[orgid].QueriesSinceInstall, 10) 310 record = []string{queriesSinceInstallAsString} 311 records = append(records, record) 312 err = w.WriteAll(records) 313 if err != nil { 314 log.Errorf("flushStatsToFile: write records failed, err=%v", err) 315 return err 316 } 317 log.Debugf("flushQueryStatsToFile: flushed queryStats' queriesSinceInstall=%v", QueryStatsMap[orgid].QueriesSinceInstall) 318 } 319 320 if _, ok := ustats[orgid]; ok { 321 logStatSummary(orgid) 322 if ustats[orgid].BytesCount > 0 { 323 filename := getStatsFilename(GetBaseStatsDir(orgid)) 324 fd, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 325 if err != nil { 326 return err 327 } 328 defer fd.Close() 329 w := csv.NewWriter(fd) 330 var records [][]string 331 var record []string 332 bytesAsString := strconv.FormatUint(ustats[orgid].BytesCount, 10) 333 logLinesAsString := strconv.FormatUint(ustats[orgid].LogLinesCount, 10) 334 metricCountAsString := strconv.FormatUint(ustats[orgid].MetricsDatapointsCount, 10) 335 epochAsString := strconv.FormatUint(uint64(time.Now().Unix()), 10) 336 record = []string{bytesAsString, logLinesAsString, metricCountAsString, epochAsString} 337 records = append(records, record) 338 err = w.WriteAll(records) 339 if err != nil { 340 log.Errorf("flushStatsToFile: write records failed, err=%v", err) 341 return err 342 } 343 log.Debugf("flushStatsToFile: flushed stats evCount=%v, metricsCount=%v, bytes=%v", ustats[orgid].LogLinesCount, 344 ustats[orgid].MetricsDatapointsCount, ustats[orgid].BytesCount) 345 346 atomic.StoreUint64(&ustats[orgid].BytesCount, 0) 347 atomic.StoreUint64(&ustats[orgid].LogLinesCount, 0) 348 atomic.StoreUint64(&ustats[orgid].MetricsDatapointsCount, 0) 349 350 return nil 351 } 352 } 353 return nil 354 } 355 356 func UpdateStats(bytesCount uint64, logLinesCount uint64, orgid uint64) { 357 if _, ok := ustats[orgid]; !ok { 358 ustats[orgid] = &Stats{ 359 BytesCount: 0, 360 LogLinesCount: 0, 361 TotalBytesCount: 0, 362 TotalLogLinesCount: 0, 363 } 364 } 365 atomic.AddUint64(&ustats[orgid].BytesCount, bytesCount) 366 atomic.AddUint64(&ustats[orgid].LogLinesCount, logLinesCount) 367 atomic.AddUint64(&ustats[orgid].TotalBytesCount, bytesCount) 368 atomic.AddUint64(&ustats[orgid].TotalLogLinesCount, logLinesCount) 369 } 370 371 func UpdateMetricsStats(bytesCount uint64, incomingMetrics uint64, orgid uint64) { 372 if _, ok := ustats[orgid]; !ok { 373 ustats[orgid] = &Stats{ 374 BytesCount: 0, 375 MetricsDatapointsCount: 0, 376 TotalBytesCount: 0, 377 TotalMetricsDatapointsCount: 0, 378 } 379 } 380 atomic.AddUint64(&ustats[orgid].BytesCount, bytesCount) 381 atomic.AddUint64(&ustats[orgid].MetricsDatapointsCount, incomingMetrics) 382 atomic.AddUint64(&ustats[orgid].TotalBytesCount, bytesCount) 383 atomic.AddUint64(&ustats[orgid].TotalMetricsDatapointsCount, incomingMetrics) 384 } 385 386 func GetQueryStats(orgid uint64) (uint64, float64, uint64) { 387 if _, ok := QueryStatsMap[orgid]; !ok { 388 return 0, 0, 0 389 } 390 return QueryStatsMap[orgid].QueryCount, QueryStatsMap[orgid].TotalRespTime, QueryStatsMap[orgid].QueriesSinceInstall 391 } 392 393 func GetCurrentMetricsStats(orgid uint64) (uint64, uint64) { 394 return ustats[orgid].TotalBytesCount, ustats[orgid].TotalMetricsDatapointsCount 395 } 396 397 func UpdateQueryStats(queryCount uint64, totalRespTime float64, orgid uint64) { 398 if _, ok := QueryStatsMap[orgid]; !ok { 399 QueryStatsMap[orgid] = &QueryStats{ 400 QueryCount: 0, 401 TotalRespTime: 0, 402 } 403 } 404 atomic.AddUint64(&QueryStatsMap[orgid].QueryCount, queryCount) 405 atomic.AddUint64(&QueryStatsMap[orgid].QueriesSinceInstall, queryCount) 406 QueryStatsMap[orgid].TotalRespTime += totalRespTime 407 } 408 409 // Calculate total bytesCount,linesCount and return hourly / daily count 410 func GetUsageStats(pastXhours uint64, granularity UsageStatsGranularity, orgid uint64) (map[string]ReadStats, error) { 411 endEpoch := time.Now() 412 startEpoch := endEpoch.Add(-(time.Duration(pastXhours) * time.Hour)) 413 startTOD := (startEpoch.UnixMilli() / MS_IN_DAY) * MS_IN_DAY 414 endTOD := (endEpoch.UnixMilli() / MS_IN_DAY) * MS_IN_DAY 415 startTOH := (startEpoch.UnixMilli() / MS_IN_HOUR) * MS_IN_HOUR 416 endTOH := (endEpoch.UnixMilli() / MS_IN_HOUR) * MS_IN_HOUR 417 statsFnames := getBaseStatsDirs(startEpoch, endEpoch, orgid) // usageStats 418 419 allStatsMap := make([]ReadStats, 0) 420 resultMap := make(map[string]ReadStats) 421 var bucketInterval string 422 runningTs := startEpoch 423 if granularity == Daily { 424 for endTOD >= startTOD { 425 bucketInterval = runningTs.Format("2006-01-02") 426 runningTs = runningTs.Add(24 * time.Hour) 427 startTOD = startTOD + MS_IN_DAY 428 resultMap[bucketInterval] = ReadStats{} 429 } 430 } else if granularity == Hourly { 431 for endTOH >= startTOH { 432 bucketInterval = runningTs.Format("2006-01-02T15") 433 runningTs = runningTs.Add(1 * time.Hour) 434 startTOH = startTOH + MS_IN_HOUR 435 resultMap[bucketInterval] = ReadStats{} 436 } 437 } 438 439 for _, statsFile := range statsFnames { 440 filename := getStatsFilename(statsFile) 441 fd, err := os.OpenFile(filename, os.O_RDONLY, 0666) 442 if err != nil { 443 continue 444 } 445 defer fd.Close() 446 447 r := csv.NewReader(fd) 448 for { 449 var readStats ReadStats 450 record, err := r.Read() 451 if err == io.EOF { 452 break 453 } 454 if err != nil { 455 log.Errorf("GetUsageStats: error reading stats file = %v, err= %v", filename, err) 456 break 457 } 458 if len(record) < 3 { 459 log.Errorf("GetUsageStats: invalid stats entry in fname %+v = %v, err= %v", filename, record, err) 460 continue 461 } 462 readStats.BytesCount, _ = strconv.ParseUint(record[0], 10, 64) 463 readStats.EventCount, _ = strconv.ParseUint(record[1], 10, 64) 464 465 // Prior to metrics, format is bytes,eventCount,time 466 // After metrics, format is bytes,eventCount,metricCount,time 467 if len(record) == 4 { 468 readStats.MetricsDatapointsCount, _ = strconv.ParseUint(record[2], 10, 64) 469 tsString, _ := strconv.ParseInt(record[3], 10, 64) 470 readStats.TimeStamp = time.Unix(tsString, 0) 471 } else { 472 tsString, _ := strconv.ParseInt(record[2], 10, 64) 473 readStats.TimeStamp = time.Unix(tsString, 0) 474 } 475 if readStats.TimeStamp.After(startEpoch) && readStats.TimeStamp.Before(endEpoch) { 476 allStatsMap = append(allStatsMap, readStats) 477 } 478 } 479 } 480 481 for _, rStat := range allStatsMap { 482 if granularity == Daily { 483 bucketInterval = rStat.TimeStamp.Format("2006-01-02") 484 } else if granularity == Hourly { 485 bucketInterval = rStat.TimeStamp.Format("2006-01-02T15") 486 } 487 if entry, ok := resultMap[bucketInterval]; ok { 488 entry.EventCount += rStat.EventCount 489 entry.MetricsDatapointsCount += rStat.MetricsDatapointsCount 490 entry.BytesCount += rStat.BytesCount 491 entry.TimeStamp = rStat.TimeStamp 492 resultMap[bucketInterval] = entry 493 } else { 494 resultMap[bucketInterval] = rStat 495 } 496 } 497 return resultMap, nil 498 }