mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-16 03:05:53 +08:00
chore: add series limit
This commit is contained in:
parent
1af688e61b
commit
fa13a0a140
170
pkg/types/querybuildertypes/querybuildertypesv5/series_limit.go
Normal file
170
pkg/types/querybuildertypes/querybuildertypesv5/series_limit.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package querybuildertypesv5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultOrderByKey = "__result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyLimit applies limit and ordering to a list of time series
|
||||||
|
// This function sorts the series based on the provided order criteria and applies the limit
|
||||||
|
func ApplySeriesLimit(series []*TimeSeries, orderBy []OrderBy, limit int) []*TimeSeries {
|
||||||
|
if len(series) == 0 {
|
||||||
|
return series
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no orderBy is specified, sort by value in descending order
|
||||||
|
effectiveOrderBy := orderBy
|
||||||
|
if len(effectiveOrderBy) == 0 {
|
||||||
|
effectiveOrderBy = []OrderBy{
|
||||||
|
{
|
||||||
|
Key: OrderByKey{
|
||||||
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: DefaultOrderByKey,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Direction: OrderDirectionDesc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the series based on the order criteria
|
||||||
|
sort.SliceStable(series, func(i, j int) bool {
|
||||||
|
return compareSeries(series[i], series[j], effectiveOrderBy)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply limit if specified
|
||||||
|
if limit > 0 && len(series) > limit {
|
||||||
|
return series[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return series
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareSeries compares two time series based on the order criteria
|
||||||
|
// Returns true if series i should come before series j
|
||||||
|
func compareSeries(seriesI, seriesJ *TimeSeries, orderBy []OrderBy) bool {
|
||||||
|
for _, order := range orderBy {
|
||||||
|
columnName := order.Key.Name
|
||||||
|
direction := order.Direction
|
||||||
|
|
||||||
|
if columnName == DefaultOrderByKey {
|
||||||
|
// Sort based on aggregated values
|
||||||
|
valueI := calculateSeriesValue(seriesI)
|
||||||
|
valueJ := calculateSeriesValue(seriesJ)
|
||||||
|
|
||||||
|
if valueI != valueJ {
|
||||||
|
if direction == OrderDirectionAsc {
|
||||||
|
return valueI < valueJ
|
||||||
|
} else { // desc
|
||||||
|
return valueI > valueJ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sort based on labels
|
||||||
|
labelI, existsI := findLabelValue(seriesI, columnName)
|
||||||
|
labelJ, existsJ := findLabelValue(seriesJ, columnName)
|
||||||
|
|
||||||
|
if existsI != existsJ {
|
||||||
|
// Handle missing labels - non-existent labels come first
|
||||||
|
return !existsI
|
||||||
|
}
|
||||||
|
|
||||||
|
if existsI && existsJ {
|
||||||
|
comparison := strings.Compare(labelI, labelJ)
|
||||||
|
if comparison != 0 {
|
||||||
|
if direction == OrderDirectionAsc {
|
||||||
|
return comparison < 0
|
||||||
|
} else { // desc
|
||||||
|
return comparison > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all order criteria are equal, preserve original order
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateSeriesValue calculates the representative value for a time series
|
||||||
|
// For single-point series (like table queries), returns that value
|
||||||
|
// For multi-point series, returns the average of non-NaN/non-Inf values
|
||||||
|
func calculateSeriesValue(series *TimeSeries) float64 {
|
||||||
|
if len(series.Values) == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single-point series, return that value directly
|
||||||
|
if len(series.Values) == 1 {
|
||||||
|
value := series.Values[0].Value
|
||||||
|
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multi-point series, calculate average of valid values
|
||||||
|
var sum float64
|
||||||
|
var count float64
|
||||||
|
|
||||||
|
for _, point := range series.Values {
|
||||||
|
if math.IsNaN(point.Value) || math.IsInf(point.Value, 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sum += point.Value
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid division by zero
|
||||||
|
if count == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / count
|
||||||
|
}
|
||||||
|
|
||||||
|
// findLabelValue finds the value of a label with the given key in a time series
|
||||||
|
// Returns the label value and whether it was found
|
||||||
|
func findLabelValue(series *TimeSeries, key string) (string, bool) {
|
||||||
|
for _, label := range series.Labels {
|
||||||
|
if label.Key.Name == key {
|
||||||
|
// Convert label value to string
|
||||||
|
if strVal, ok := label.Value.(string); ok {
|
||||||
|
return strVal, true
|
||||||
|
}
|
||||||
|
// Handle non-string values by converting to string
|
||||||
|
return convertValueToString(label.Value), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertValueToString converts various types to string for comparison
|
||||||
|
func convertValueToString(value any) string {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return strconv.FormatInt(int64(v), 10)
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(v, 10)
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
package querybuildertypesv5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplySeriesLimit(t *testing.T) {
|
||||||
|
t.Run("Sort by value with limit", func(t *testing.T) {
|
||||||
|
// Create test series with different values
|
||||||
|
series := []*TimeSeries{
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "service-a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 10.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "service-b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 50.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "service-c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 30.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "service-d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 20.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by value descending with limit of 2
|
||||||
|
orderBy := []OrderBy{
|
||||||
|
{
|
||||||
|
Key: OrderByKey{
|
||||||
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: DefaultOrderByKey,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Direction: OrderDirectionDesc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ApplySeriesLimit(series, orderBy, 2)
|
||||||
|
|
||||||
|
// Should return top 2 series by value: service-b (50.0), service-c (30.0)
|
||||||
|
assert.Len(t, result, 2)
|
||||||
|
|
||||||
|
// First series should be service-b with value 50.0
|
||||||
|
assert.Equal(t, "service-b", result[0].Labels[0].Value)
|
||||||
|
assert.Equal(t, 50.0, result[0].Values[0].Value)
|
||||||
|
|
||||||
|
// Second series should be service-c with value 30.0
|
||||||
|
assert.Equal(t, "service-c", result[1].Labels[0].Value)
|
||||||
|
assert.Equal(t, 30.0, result[1].Values[0].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Sort by labels with two keys", func(t *testing.T) {
|
||||||
|
// Create test series with different label combinations
|
||||||
|
series := []*TimeSeries{
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "backend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "environment",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 10.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "frontend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "environment",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 20.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "backend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "environment",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 30.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Labels: []*Label{
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "frontend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "environment",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
Value: "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Values: []*TimeSeriesValue{
|
||||||
|
{Timestamp: 1000, Value: 40.0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by service (asc) then by environment (desc) with limit of 3
|
||||||
|
orderBy := []OrderBy{
|
||||||
|
{
|
||||||
|
Key: OrderByKey{
|
||||||
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Direction: OrderDirectionAsc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: OrderByKey{
|
||||||
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "environment",
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Direction: OrderDirectionDesc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ApplySeriesLimit(series, orderBy, 3)
|
||||||
|
|
||||||
|
// Should return 3 series sorted by service (asc), then environment (desc)
|
||||||
|
// Expected order:
|
||||||
|
// 1. backend + prod
|
||||||
|
// 2. backend + dev
|
||||||
|
// 3. frontend + prod
|
||||||
|
assert.Len(t, result, 3)
|
||||||
|
|
||||||
|
// First: backend + prod
|
||||||
|
assert.Equal(t, "backend", result[0].Labels[0].Value)
|
||||||
|
assert.Equal(t, "prod", result[0].Labels[1].Value)
|
||||||
|
assert.Equal(t, 10.0, result[0].Values[0].Value)
|
||||||
|
|
||||||
|
// Second: backend + dev
|
||||||
|
assert.Equal(t, "backend", result[1].Labels[0].Value)
|
||||||
|
assert.Equal(t, "dev", result[1].Labels[1].Value)
|
||||||
|
assert.Equal(t, 30.0, result[1].Values[0].Value)
|
||||||
|
|
||||||
|
// Third: frontend + prod
|
||||||
|
assert.Equal(t, "frontend", result[2].Labels[0].Value)
|
||||||
|
assert.Equal(t, "prod", result[2].Labels[1].Value)
|
||||||
|
assert.Equal(t, 40.0, result[2].Values[0].Value)
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user