Skip to content

Commit 6a81802

Browse files
authored
chore: add stream count and result information to metrics.go (#20055)
1 parent 688dd20 commit 6a81802

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

pkg/logql/metrics.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/go-kit/log/level"
1313
"github.com/prometheus/client_golang/prometheus"
1414
"github.com/prometheus/client_golang/prometheus/promauto"
15+
"github.com/prometheus/prometheus/model/labels"
16+
"github.com/prometheus/prometheus/promql"
1517
promql_parser "github.com/prometheus/prometheus/promql/parser"
1618

1719
"github.com/grafana/loki/v3/pkg/analytics"
@@ -42,6 +44,24 @@ const (
4244
slowQueryThresholdSecond = float64(10)
4345
)
4446

47+
type componentCtxKey string
48+
49+
const (
50+
componentKey componentCtxKey = "logql_component"
51+
componentFrontend string = "frontend"
52+
)
53+
54+
// WithComponentContext adds a component identifier to the context
55+
func WithComponentContext(ctx context.Context, component string) context.Context {
56+
return context.WithValue(ctx, componentKey, component)
57+
}
58+
59+
// isFrontendContext checks if the context indicates this is being logged from the frontend
60+
func isFrontendContext(ctx context.Context) bool {
61+
component, _ := ctx.Value(componentKey).(string)
62+
return component == componentFrontend
63+
}
64+
4565
var (
4666
bytesPerSecond = promauto.NewHistogramVec(prometheus.HistogramOpts{
4767
Namespace: constants.Loki,
@@ -237,6 +257,31 @@ func RecordRangeAndInstantQueryMetrics(
237257
logValues = append(logValues, "has_labelfilter_before_parser", "false")
238258
}
239259

260+
// Add querier-specific metrics: total stream count
261+
// This is only logged from the querier component, not from the frontend
262+
// (where stats are merged and this value would be inaccurate)
263+
if !isFrontendContext(ctx) && stats.Index.TotalStreams > 0 {
264+
logValues = append(logValues, "total_stream_count", stats.Index.TotalStreams)
265+
}
266+
267+
// Add frontend-specific metrics: approximate result size, streams count, lines count
268+
// These are available when logging from the frontend component
269+
if result != nil {
270+
resultSize := calculateResultSize(result)
271+
if resultSize > 0 {
272+
// approx_result_size is an estimate of the result size in bytes (without serialization)
273+
logValues = append(logValues, "approx_result_size", util.HumanizeBytes(uint64(resultSize)))
274+
}
275+
276+
// Extract stream and line counts for log queries
277+
if streams, ok := result.(logqlmodel.Streams); ok {
278+
logValues = append(logValues,
279+
"result_streams_count", len(streams),
280+
"result_lines_count", streams.Lines(),
281+
)
282+
}
283+
}
284+
240285
level.Info(logger).Log(
241286
logValues...,
242287
)
@@ -581,6 +626,75 @@ func extractShard(shards []string) *astmapper.ShardAnnotation {
581626
return &shard
582627
}
583628

629+
// calculateResultSize calculates an approximate estimate of the result size in bytes
630+
// without serialization by summing up the actual data sizes plus estimated JSON overhead.
631+
// This is an approximation and may not match the exact serialized size. Used for frontend logging.
632+
func calculateResultSize(result promql_parser.Value) int {
633+
if result == nil {
634+
return 0
635+
}
636+
637+
switch v := result.(type) {
638+
case logqlmodel.Streams:
639+
var size int
640+
for _, stream := range v {
641+
size += len(stream.Labels) // Stream labels
642+
size += 20
643+
for _, entry := range stream.Entries {
644+
size += len(entry.Line) // Entry line content
645+
size += 20 // Timestamp as string (~20 bytes for RFC3339Nano)
646+
size += 10 // JSON overhead for entry array (~10 bytes: ["timestamp","line"])
647+
for _, label := range entry.StructuredMetadata {
648+
size += len(label.Name) + len(label.Value) + 10 // +10 for JSON overhead
649+
}
650+
for _, label := range entry.Parsed {
651+
size += len(label.Name) + len(label.Value) + 10 // +10 for JSON overhead
652+
}
653+
}
654+
}
655+
size += 2 // Account for [] brackets
656+
return size
657+
case promql.Vector:
658+
var size int
659+
for _, sample := range v {
660+
size += estimateLabelsSize(sample.Metric) // Metric labels
661+
size += 30 // Value array: [timestamp, value] (~30 bytes)
662+
size += 15 // JSON object overhead (~15 bytes)
663+
}
664+
size += 2 // Account for [] brackets
665+
return size
666+
case promql.Matrix:
667+
var size int
668+
for _, series := range v {
669+
size += estimateLabelsSize(series.Metric) // Metric labels
670+
size += 10 // Values array overhead
671+
size += len(series.Floats) * 20 // Each data point (~20 bytes: [timestamp, value])
672+
size += 15 // JSON object overhead (~15 bytes)
673+
}
674+
size += 2 // Account for [] brackets
675+
return size
676+
case promql.Scalar:
677+
return 30 // Scalar: [timestamp, value] (~30 bytes)
678+
case promql.String:
679+
return 20 + len(v.V) // String: [timestamp, value] (~20 bytes + string length)
680+
default:
681+
return 0 // For unknown types, return 0
682+
}
683+
}
684+
685+
// estimateLabelsSize estimates the JSON size of labels
686+
func estimateLabelsSize(lbs labels.Labels) int {
687+
if lbs.Len() == 0 {
688+
return 2 // Account for {} brackets
689+
}
690+
var size int
691+
size += 2 // Account for {} brackets
692+
lbs.Range(func(label labels.Label) {
693+
size += len(label.Name) + len(label.Value) + 5 // "name":"value",
694+
})
695+
return size
696+
}
697+
584698
func RecordDetectedLabelsQueryMetrics(ctx context.Context, log log.Logger, start time.Time, end time.Time, query string, status string, stats logql_stats.Result) {
585699
var (
586700
logger = fixLogger(ctx, log)

pkg/querier/queryrange/stats.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ var (
5252
// recordQueryMetrics will be called from Query Frontend middleware chain for any type of query.
5353
func recordQueryMetrics(data *queryData) {
5454
logger := log.With(util_log.Logger, "component", "frontend")
55+
// Mark context as frontend so metrics logging can distinguish between frontend and querier
56+
data.ctx = logql.WithComponentContext(data.ctx, "frontend")
5557

5658
switch data.queryType {
5759
case queryTypeLog, queryTypeMetric:

0 commit comments

Comments
 (0)