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  }