@@ -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+
4565var (
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+
584698func 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 )
0 commit comments