github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/logqlmodel/stats/context.go (about)

     1  /*
     2  Package stats provides primitives for recording metrics across the query path.
     3  Statistics are passed through the query context.
     4  To start a new query statistics context use:
     5  
     6  	statsCtx, ctx := stats.NewContext(ctx)
     7  
     8  Then you can update statistics by mutating data by using:
     9  
    10  	statsCtx.Add...(1)
    11  
    12  To get the  statistic from the current context you can use:
    13  
    14  	statsCtx := stats.FromContext(ctx)
    15  
    16  Finally to get a snapshot of the current query statistic use
    17  
    18  	statsCtx.Result(time.Since(start), queueTime, totalEntriesReturned)
    19  
    20  */
    21  package stats
    22  
    23  import (
    24  	"context"
    25  	"sync"
    26  	"sync/atomic" //lint:ignore faillint we can't use go.uber.org/atomic with a protobuf struct without wrapping it.
    27  	"time"
    28  
    29  	"github.com/dustin/go-humanize"
    30  	"github.com/go-kit/log"
    31  )
    32  
    33  type (
    34  	ctxKeyType string
    35  	Component  int64
    36  )
    37  
    38  const (
    39  	statsKey ctxKeyType = "stats"
    40  )
    41  
    42  // Context is the statistics context. It is passed through the query path and accumulates statistics.
    43  type Context struct {
    44  	querier  Querier
    45  	ingester Ingester
    46  	caches   Caches
    47  
    48  	// store is the store statistics collected across the query path
    49  	store Store
    50  	// result accumulates results for JoinResult.
    51  	result Result
    52  
    53  	mtx sync.Mutex
    54  }
    55  
    56  type CacheType string
    57  
    58  const (
    59  	ChunkCache       CacheType = "chunk" //nolint:staticcheck
    60  	IndexCache                 = "index"
    61  	ResultCache                = "result"
    62  	WriteDedupeCache           = "write-dedupe"
    63  )
    64  
    65  // NewContext creates a new statistics context
    66  func NewContext(ctx context.Context) (*Context, context.Context) {
    67  	contextData := &Context{}
    68  	ctx = context.WithValue(ctx, statsKey, contextData)
    69  	return contextData, ctx
    70  }
    71  
    72  // FromContext returns the statistics context.
    73  func FromContext(ctx context.Context) *Context {
    74  	v, ok := ctx.Value(statsKey).(*Context)
    75  	if !ok {
    76  		return &Context{}
    77  	}
    78  	return v
    79  }
    80  
    81  // Ingester returns the ingester statistics accumulated so far.
    82  func (c *Context) Ingester() Ingester {
    83  	return Ingester{
    84  		TotalReached:       c.ingester.TotalReached,
    85  		TotalChunksMatched: c.ingester.TotalChunksMatched,
    86  		TotalBatches:       c.ingester.TotalBatches,
    87  		TotalLinesSent:     c.ingester.TotalLinesSent,
    88  		Store:              c.store,
    89  	}
    90  }
    91  
    92  // Caches returns the cache statistics accumulated so far.
    93  func (c *Context) Caches() Caches {
    94  	return Caches{
    95  		Chunk:  c.caches.Chunk,
    96  		Index:  c.caches.Index,
    97  		Result: c.caches.Result,
    98  	}
    99  }
   100  
   101  // Reset clears the statistics.
   102  func (c *Context) Reset() {
   103  	c.mtx.Lock()
   104  	defer c.mtx.Unlock()
   105  
   106  	c.store.Reset()
   107  	c.querier.Reset()
   108  	c.ingester.Reset()
   109  	c.result.Reset()
   110  	c.caches.Reset()
   111  }
   112  
   113  // Result calculates the summary based on store and ingester data.
   114  func (c *Context) Result(execTime time.Duration, queueTime time.Duration, totalEntriesReturned int) Result {
   115  	r := c.result
   116  
   117  	r.Merge(Result{
   118  		Querier: Querier{
   119  			Store: c.store,
   120  		},
   121  		Ingester: c.ingester,
   122  		Caches:   c.caches,
   123  	})
   124  
   125  	r.ComputeSummary(execTime, queueTime, totalEntriesReturned)
   126  
   127  	return r
   128  }
   129  
   130  // JoinResults merges a Result with the embedded Result in a context in a concurrency-safe manner.
   131  func JoinResults(ctx context.Context, res Result) {
   132  	stats := FromContext(ctx)
   133  	stats.mtx.Lock()
   134  	defer stats.mtx.Unlock()
   135  
   136  	stats.result.Merge(res)
   137  }
   138  
   139  // JoinIngesterResult joins the ingester result statistics in a concurrency-safe manner.
   140  func JoinIngesters(ctx context.Context, inc Ingester) {
   141  	stats := FromContext(ctx)
   142  	stats.mtx.Lock()
   143  	defer stats.mtx.Unlock()
   144  
   145  	stats.ingester.Merge(inc)
   146  }
   147  
   148  // ComputeSummary compute the summary of the statistics.
   149  func (r *Result) ComputeSummary(execTime time.Duration, queueTime time.Duration, totalEntriesReturned int) {
   150  	r.Summary.TotalBytesProcessed = r.Querier.Store.Chunk.DecompressedBytes + r.Querier.Store.Chunk.HeadChunkBytes +
   151  		r.Ingester.Store.Chunk.DecompressedBytes + r.Ingester.Store.Chunk.HeadChunkBytes
   152  	r.Summary.TotalLinesProcessed = r.Querier.Store.Chunk.DecompressedLines + r.Querier.Store.Chunk.HeadChunkLines +
   153  		r.Ingester.Store.Chunk.DecompressedLines + r.Ingester.Store.Chunk.HeadChunkLines
   154  	r.Summary.ExecTime = execTime.Seconds()
   155  	if execTime != 0 {
   156  		r.Summary.BytesProcessedPerSecond = int64(float64(r.Summary.TotalBytesProcessed) /
   157  			execTime.Seconds())
   158  		r.Summary.LinesProcessedPerSecond = int64(float64(r.Summary.TotalLinesProcessed) /
   159  			execTime.Seconds())
   160  	}
   161  	if queueTime != 0 {
   162  		r.Summary.QueueTime = queueTime.Seconds()
   163  	}
   164  
   165  	r.Summary.TotalEntriesReturned = int64(totalEntriesReturned)
   166  }
   167  
   168  func (s *Store) Merge(m Store) {
   169  	s.TotalChunksRef += m.TotalChunksRef
   170  	s.TotalChunksDownloaded += m.TotalChunksDownloaded
   171  	s.ChunksDownloadTime += m.ChunksDownloadTime
   172  	s.Chunk.HeadChunkBytes += m.Chunk.HeadChunkBytes
   173  	s.Chunk.HeadChunkLines += m.Chunk.HeadChunkLines
   174  	s.Chunk.DecompressedBytes += m.Chunk.DecompressedBytes
   175  	s.Chunk.DecompressedLines += m.Chunk.DecompressedLines
   176  	s.Chunk.CompressedBytes += m.Chunk.CompressedBytes
   177  	s.Chunk.TotalDuplicates += m.Chunk.TotalDuplicates
   178  }
   179  
   180  func (q *Querier) Merge(m Querier) {
   181  	q.Store.Merge(m.Store)
   182  }
   183  
   184  func (i *Ingester) Merge(m Ingester) {
   185  	i.Store.Merge(m.Store)
   186  	i.TotalBatches += m.TotalBatches
   187  	i.TotalLinesSent += m.TotalLinesSent
   188  	i.TotalChunksMatched += m.TotalChunksMatched
   189  	i.TotalReached += m.TotalReached
   190  }
   191  
   192  func (c *Caches) Merge(m Caches) {
   193  	c.Chunk.Merge(m.Chunk)
   194  	c.Index.Merge(m.Index)
   195  	c.Result.Merge(m.Result)
   196  }
   197  
   198  func (c *Cache) Merge(m Cache) {
   199  	c.EntriesFound += m.EntriesFound
   200  	c.EntriesRequested += m.EntriesRequested
   201  	c.EntriesStored += m.EntriesStored
   202  	c.Requests += m.Requests
   203  	c.BytesSent += m.BytesSent
   204  	c.BytesReceived += m.BytesReceived
   205  }
   206  
   207  // Merge merges two results of statistics.
   208  // This will increase the total number of Subqueries.
   209  func (r *Result) Merge(m Result) {
   210  	r.Summary.Subqueries++
   211  	r.Querier.Merge(m.Querier)
   212  	r.Ingester.Merge(m.Ingester)
   213  	r.Caches.Merge(m.Caches)
   214  	r.ComputeSummary(ConvertSecondsToNanoseconds(r.Summary.ExecTime+m.Summary.ExecTime),
   215  		ConvertSecondsToNanoseconds(r.Summary.QueueTime+m.Summary.QueueTime), int(r.Summary.TotalEntriesReturned))
   216  }
   217  
   218  // ConvertSecondsToNanoseconds converts time.Duration representation of seconds (float64)
   219  // into time.Duration representation of nanoseconds (int64)
   220  func ConvertSecondsToNanoseconds(seconds float64) time.Duration {
   221  	return time.Duration(int64(seconds * float64(time.Second)))
   222  }
   223  
   224  func (r Result) ChunksDownloadTime() time.Duration {
   225  	return time.Duration(r.Querier.Store.ChunksDownloadTime + r.Ingester.Store.ChunksDownloadTime)
   226  }
   227  
   228  func (r Result) TotalDuplicates() int64 {
   229  	return r.Querier.Store.Chunk.TotalDuplicates + r.Ingester.Store.Chunk.TotalDuplicates
   230  }
   231  
   232  func (r Result) TotalChunksDownloaded() int64 {
   233  	return r.Querier.Store.TotalChunksDownloaded + r.Ingester.Store.TotalChunksDownloaded
   234  }
   235  
   236  func (r Result) TotalChunksRef() int64 {
   237  	return r.Querier.Store.TotalChunksRef + r.Ingester.Store.TotalChunksRef
   238  }
   239  
   240  func (r Result) TotalDecompressedBytes() int64 {
   241  	return r.Querier.Store.Chunk.DecompressedBytes + r.Ingester.Store.Chunk.DecompressedBytes
   242  }
   243  
   244  func (r Result) TotalDecompressedLines() int64 {
   245  	return r.Querier.Store.Chunk.DecompressedLines + r.Ingester.Store.Chunk.DecompressedLines
   246  }
   247  
   248  func (c *Context) AddIngesterBatch(size int64) {
   249  	atomic.AddInt64(&c.ingester.TotalBatches, 1)
   250  	atomic.AddInt64(&c.ingester.TotalLinesSent, size)
   251  }
   252  
   253  func (c *Context) AddIngesterTotalChunkMatched(i int64) {
   254  	atomic.AddInt64(&c.ingester.TotalChunksMatched, i)
   255  }
   256  
   257  func (c *Context) AddIngesterReached(i int32) {
   258  	atomic.AddInt32(&c.ingester.TotalReached, i)
   259  }
   260  
   261  func (c *Context) AddHeadChunkLines(i int64) {
   262  	atomic.AddInt64(&c.store.Chunk.HeadChunkLines, i)
   263  }
   264  
   265  func (c *Context) AddHeadChunkBytes(i int64) {
   266  	atomic.AddInt64(&c.store.Chunk.HeadChunkBytes, i)
   267  }
   268  
   269  func (c *Context) AddCompressedBytes(i int64) {
   270  	atomic.AddInt64(&c.store.Chunk.CompressedBytes, i)
   271  }
   272  
   273  func (c *Context) AddDecompressedBytes(i int64) {
   274  	atomic.AddInt64(&c.store.Chunk.DecompressedBytes, i)
   275  }
   276  
   277  func (c *Context) AddDecompressedLines(i int64) {
   278  	atomic.AddInt64(&c.store.Chunk.DecompressedLines, i)
   279  }
   280  
   281  func (c *Context) AddDuplicates(i int64) {
   282  	atomic.AddInt64(&c.store.Chunk.TotalDuplicates, i)
   283  }
   284  
   285  func (c *Context) AddChunksDownloadTime(i time.Duration) {
   286  	atomic.AddInt64(&c.store.ChunksDownloadTime, int64(i))
   287  }
   288  
   289  func (c *Context) AddChunksDownloaded(i int64) {
   290  	atomic.AddInt64(&c.store.TotalChunksDownloaded, i)
   291  }
   292  
   293  func (c *Context) AddChunksRef(i int64) {
   294  	atomic.AddInt64(&c.store.TotalChunksRef, i)
   295  }
   296  
   297  // AddCacheEntriesFound counts the number of cache entries requested and found
   298  func (c *Context) AddCacheEntriesFound(t CacheType, i int) {
   299  	stats := c.getCacheStatsByType(t)
   300  	if stats == nil {
   301  		return
   302  	}
   303  
   304  	atomic.AddInt32(&stats.EntriesFound, int32(i))
   305  }
   306  
   307  // AddCacheEntriesRequested counts the number of keys requested from the cache
   308  func (c *Context) AddCacheEntriesRequested(t CacheType, i int) {
   309  	stats := c.getCacheStatsByType(t)
   310  	if stats == nil {
   311  		return
   312  	}
   313  
   314  	atomic.AddInt32(&stats.EntriesRequested, int32(i))
   315  }
   316  
   317  // AddCacheEntriesStored counts the number of keys *attempted* to be stored in the cache
   318  // It should be noted that if a background writeback (https://grafana.com/docs/loki/latest/configuration/#cache_config)
   319  // is configured we cannot know if these store attempts succeeded or not as this happens asynchronously
   320  func (c *Context) AddCacheEntriesStored(t CacheType, i int) {
   321  	stats := c.getCacheStatsByType(t)
   322  	if stats == nil {
   323  		return
   324  	}
   325  
   326  	atomic.AddInt32(&stats.EntriesStored, int32(i))
   327  }
   328  
   329  // AddCacheBytesRetrieved counts the amount of bytes retrieved from the cache
   330  func (c *Context) AddCacheBytesRetrieved(t CacheType, i int) {
   331  	stats := c.getCacheStatsByType(t)
   332  	if stats == nil {
   333  		return
   334  	}
   335  
   336  	atomic.AddInt64(&stats.BytesReceived, int64(i))
   337  }
   338  
   339  // AddCacheBytesSent counts the amount of bytes sent to the cache
   340  // It should be noted that if a background writeback (https://grafana.com/docs/loki/latest/configuration/#cache_config)
   341  // is configured we cannot know if these bytes actually got stored or not as this happens asynchronously
   342  func (c *Context) AddCacheBytesSent(t CacheType, i int) {
   343  	stats := c.getCacheStatsByType(t)
   344  	if stats == nil {
   345  		return
   346  	}
   347  
   348  	atomic.AddInt64(&stats.BytesSent, int64(i))
   349  }
   350  
   351  // AddCacheRequest counts the number of fetch/store requests to the cache
   352  func (c *Context) AddCacheRequest(t CacheType, i int) {
   353  	stats := c.getCacheStatsByType(t)
   354  	if stats == nil {
   355  		return
   356  	}
   357  
   358  	atomic.AddInt32(&stats.Requests, int32(i))
   359  }
   360  
   361  func (c *Context) getCacheStatsByType(t CacheType) *Cache {
   362  	var stats *Cache
   363  	switch t {
   364  	case ChunkCache:
   365  		stats = &c.caches.Chunk
   366  	case IndexCache:
   367  		stats = &c.caches.Index
   368  	case ResultCache:
   369  		stats = &c.caches.Result
   370  	default:
   371  		return nil
   372  	}
   373  	return stats
   374  }
   375  
   376  // Log logs a query statistics result.
   377  func (r Result) Log(log log.Logger) {
   378  	_ = log.Log(
   379  		"Ingester.TotalReached", r.Ingester.TotalReached,
   380  		"Ingester.TotalChunksMatched", r.Ingester.TotalChunksMatched,
   381  		"Ingester.TotalBatches", r.Ingester.TotalBatches,
   382  		"Ingester.TotalLinesSent", r.Ingester.TotalLinesSent,
   383  		"Ingester.TotalChunksRef", r.Ingester.Store.TotalChunksRef,
   384  		"Ingester.TotalChunksDownloaded", r.Ingester.Store.TotalChunksDownloaded,
   385  		"Ingester.ChunksDownloadTime", time.Duration(r.Ingester.Store.ChunksDownloadTime),
   386  		"Ingester.HeadChunkBytes", humanize.Bytes(uint64(r.Ingester.Store.Chunk.HeadChunkBytes)),
   387  		"Ingester.HeadChunkLines", r.Ingester.Store.Chunk.HeadChunkLines,
   388  		"Ingester.DecompressedBytes", humanize.Bytes(uint64(r.Ingester.Store.Chunk.DecompressedBytes)),
   389  		"Ingester.DecompressedLines", r.Ingester.Store.Chunk.DecompressedLines,
   390  		"Ingester.CompressedBytes", humanize.Bytes(uint64(r.Ingester.Store.Chunk.CompressedBytes)),
   391  		"Ingester.TotalDuplicates", r.Ingester.Store.Chunk.TotalDuplicates,
   392  
   393  		"Querier.TotalChunksRef", r.Querier.Store.TotalChunksRef,
   394  		"Querier.TotalChunksDownloaded", r.Querier.Store.TotalChunksDownloaded,
   395  		"Querier.ChunksDownloadTime", time.Duration(r.Querier.Store.ChunksDownloadTime),
   396  		"Querier.HeadChunkBytes", humanize.Bytes(uint64(r.Querier.Store.Chunk.HeadChunkBytes)),
   397  		"Querier.HeadChunkLines", r.Querier.Store.Chunk.HeadChunkLines,
   398  		"Querier.DecompressedBytes", humanize.Bytes(uint64(r.Querier.Store.Chunk.DecompressedBytes)),
   399  		"Querier.DecompressedLines", r.Querier.Store.Chunk.DecompressedLines,
   400  		"Querier.CompressedBytes", humanize.Bytes(uint64(r.Querier.Store.Chunk.CompressedBytes)),
   401  		"Querier.TotalDuplicates", r.Querier.Store.Chunk.TotalDuplicates,
   402  	)
   403  	r.Caches.Log(log)
   404  	r.Summary.Log(log)
   405  }
   406  
   407  func (s Summary) Log(log log.Logger) {
   408  	_ = log.Log(
   409  		"Summary.BytesProcessedPerSecond", humanize.Bytes(uint64(s.BytesProcessedPerSecond)),
   410  		"Summary.LinesProcessedPerSecond", s.LinesProcessedPerSecond,
   411  		"Summary.TotalBytesProcessed", humanize.Bytes(uint64(s.TotalBytesProcessed)),
   412  		"Summary.TotalLinesProcessed", s.TotalLinesProcessed,
   413  		"Summary.ExecTime", ConvertSecondsToNanoseconds(s.ExecTime),
   414  		"Summary.QueueTime", ConvertSecondsToNanoseconds(s.QueueTime),
   415  	)
   416  }
   417  
   418  func (c Caches) Log(log log.Logger) {
   419  	_ = log.Log(
   420  		"Cache.Chunk.Requests", c.Chunk.Requests,
   421  		"Cache.Chunk.EntriesRequested", c.Chunk.EntriesRequested,
   422  		"Cache.Chunk.EntriesFound", c.Chunk.EntriesFound,
   423  		"Cache.Chunk.EntriesStored", c.Chunk.EntriesStored,
   424  		"Cache.Chunk.BytesSent", humanize.Bytes(uint64(c.Chunk.BytesSent)),
   425  		"Cache.Chunk.BytesReceived", humanize.Bytes(uint64(c.Chunk.BytesReceived)),
   426  		"Cache.Index.Requests", c.Index.Requests,
   427  		"Cache.Index.EntriesRequested", c.Index.EntriesRequested,
   428  		"Cache.Index.EntriesFound", c.Index.EntriesFound,
   429  		"Cache.Index.EntriesStored", c.Index.EntriesStored,
   430  		"Cache.Index.BytesSent", humanize.Bytes(uint64(c.Index.BytesSent)),
   431  		"Cache.Index.BytesReceived", humanize.Bytes(uint64(c.Index.BytesReceived)),
   432  		"Cache.Result.Requests", c.Result.Requests,
   433  		"Cache.Result.EntriesRequested", c.Result.EntriesRequested,
   434  		"Cache.Result.EntriesFound", c.Result.EntriesFound,
   435  		"Cache.Result.EntriesStored", c.Result.EntriesStored,
   436  		"Cache.Result.BytesSent", humanize.Bytes(uint64(c.Result.BytesSent)),
   437  		"Cache.Result.BytesReceived", humanize.Bytes(uint64(c.Result.BytesReceived)),
   438  	)
   439  }