302 lines
7.6 KiB
Go
302 lines
7.6 KiB
Go
|
package postprocess
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
|
||
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||
|
)
|
||
|
|
||
|
func roundToTwoDecimal(number float64) float64 {
|
||
|
// Handle very small numbers
|
||
|
if math.Abs(number) < 0.000001 {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// Determine the number of decimal places to round to
|
||
|
decimalPlaces := 2
|
||
|
if math.Abs(number) < 0.01 {
|
||
|
decimalPlaces = int(math.Ceil(-math.Log10(math.Abs(number)))) + 1
|
||
|
}
|
||
|
|
||
|
// Round to the determined number of decimal places
|
||
|
scale := math.Pow(10, float64(decimalPlaces))
|
||
|
return math.Round(number*scale) / scale
|
||
|
}
|
||
|
|
||
|
func TransformToTableForBuilderQueries(results []*v3.Result, params *v3.QueryRangeParamsV3) []*v3.Result {
|
||
|
if len(results) == 0 {
|
||
|
return []*v3.Result{}
|
||
|
}
|
||
|
|
||
|
// Sort results by QueryName
|
||
|
sort.Slice(results, func(i, j int) bool {
|
||
|
return results[i].QueryName < results[j].QueryName
|
||
|
})
|
||
|
|
||
|
// Create a map to store all unique labels
|
||
|
seen := make(map[string]struct{})
|
||
|
labelKeys := []string{}
|
||
|
for _, result := range results {
|
||
|
for _, series := range result.Series {
|
||
|
for _, labels := range series.LabelsArray {
|
||
|
for key := range labels {
|
||
|
if _, ok := seen[key]; !ok {
|
||
|
seen[key] = struct{}{}
|
||
|
labelKeys = append(labelKeys, key)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create columns
|
||
|
// There will be one column for each label key and one column for each query name
|
||
|
columns := make([]*v3.TableColumn, 0, len(labelKeys)+len(results))
|
||
|
for _, key := range labelKeys {
|
||
|
columns = append(columns, &v3.TableColumn{Name: key, IsValueColumn: false})
|
||
|
}
|
||
|
for _, result := range results {
|
||
|
columns = append(columns, &v3.TableColumn{Name: result.QueryName, QueryName: result.QueryName, IsValueColumn: true})
|
||
|
}
|
||
|
|
||
|
// Create a map to store unique rows
|
||
|
rowMap := make(map[string]*v3.TableRow)
|
||
|
|
||
|
for _, result := range results {
|
||
|
for _, series := range result.Series {
|
||
|
if len(series.Points) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Create a key for the row based on labels
|
||
|
var keyParts []string
|
||
|
rowData := make(map[string]interface{}, len(columns))
|
||
|
for _, key := range labelKeys {
|
||
|
value := "n/a"
|
||
|
for _, labels := range series.LabelsArray {
|
||
|
if v, ok := labels[key]; ok {
|
||
|
value = v
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
keyParts = append(keyParts, fmt.Sprintf("%s=%s", key, value))
|
||
|
rowData[key] = value
|
||
|
}
|
||
|
rowKey := strings.Join(keyParts, ",")
|
||
|
|
||
|
// Get or create the row
|
||
|
row, ok := rowMap[rowKey]
|
||
|
if !ok {
|
||
|
row = &v3.TableRow{Data: rowData, QueryName: result.QueryName}
|
||
|
rowMap[rowKey] = row
|
||
|
}
|
||
|
|
||
|
// Add the value for this query
|
||
|
for _, col := range columns {
|
||
|
if col.Name == result.QueryName {
|
||
|
row.Data[col.Name] = roundToTwoDecimal(series.Points[0].Value)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Convert rowMap to a slice of TableRows
|
||
|
rows := make([]*v3.TableRow, 0, len(rowMap))
|
||
|
for _, row := range rowMap {
|
||
|
rows = append(rows, row)
|
||
|
}
|
||
|
|
||
|
// Get sorted query names
|
||
|
queryNames := make([]string, 0, len(params.CompositeQuery.BuilderQueries))
|
||
|
for queryName := range params.CompositeQuery.BuilderQueries {
|
||
|
queryNames = append(queryNames, queryName)
|
||
|
}
|
||
|
sort.Strings(queryNames)
|
||
|
|
||
|
// Sort rows based on OrderBy from BuilderQueries
|
||
|
sortRows(rows, params.CompositeQuery.BuilderQueries, queryNames)
|
||
|
|
||
|
for _, row := range rows {
|
||
|
for _, col := range columns {
|
||
|
if col.IsValueColumn {
|
||
|
if row.Data[col.Name] == nil {
|
||
|
row.Data[col.Name] = "n/a"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create the final result
|
||
|
tableResult := v3.Result{
|
||
|
Table: &v3.Table{
|
||
|
Columns: columns,
|
||
|
Rows: rows,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
return []*v3.Result{&tableResult}
|
||
|
}
|
||
|
|
||
|
func sortRows(rows []*v3.TableRow, builderQueries map[string]*v3.BuilderQuery, queryNames []string) {
|
||
|
// use reverse order of queryNames
|
||
|
for i := len(queryNames) - 1; i >= 0; i-- {
|
||
|
queryName := queryNames[i]
|
||
|
sort.SliceStable(rows, func(i, j int) bool {
|
||
|
query := builderQueries[queryName]
|
||
|
orderByList := query.OrderBy
|
||
|
if len(orderByList) == 0 {
|
||
|
// If no orderBy is specified, sort by value in descending order
|
||
|
orderByList = []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "desc"}}
|
||
|
}
|
||
|
for _, orderBy := range orderByList {
|
||
|
name := orderBy.ColumnName
|
||
|
if name == constants.SigNozOrderByValue {
|
||
|
name = queryName
|
||
|
}
|
||
|
|
||
|
valI := rows[i].Data[name]
|
||
|
valJ := rows[j].Data[name]
|
||
|
|
||
|
if valI == nil || valJ == nil {
|
||
|
return rows[i].QueryName < rows[j].QueryName
|
||
|
}
|
||
|
|
||
|
// Compare based on the data type
|
||
|
switch v := valI.(type) {
|
||
|
case float64:
|
||
|
switch w := valJ.(type) {
|
||
|
case float64:
|
||
|
if v != w {
|
||
|
return (v < w) == (orderBy.Order == "asc")
|
||
|
}
|
||
|
default:
|
||
|
// For any other type, sort float64 first
|
||
|
return orderBy.Order == "asc"
|
||
|
}
|
||
|
case string:
|
||
|
switch w := valJ.(type) {
|
||
|
case float64:
|
||
|
// If types are different, sort numbers before strings
|
||
|
return orderBy.Order != "asc"
|
||
|
case string:
|
||
|
if v != w {
|
||
|
return (v < w) == (orderBy.Order == "asc")
|
||
|
}
|
||
|
default:
|
||
|
// For any other type, sort strings before bools
|
||
|
return orderBy.Order == "asc"
|
||
|
}
|
||
|
case bool:
|
||
|
switch w := valJ.(type) {
|
||
|
case float64, string:
|
||
|
// If types are different, sort bools after numbers and strings
|
||
|
return orderBy.Order != "asc"
|
||
|
case bool:
|
||
|
if v != w {
|
||
|
return (!v && w) == (orderBy.Order == "asc")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TransformToTableForClickHouseQueries(results []*v3.Result) []*v3.Result {
|
||
|
if len(results) == 0 {
|
||
|
return []*v3.Result{}
|
||
|
}
|
||
|
|
||
|
// Sort results by QueryName
|
||
|
sort.Slice(results, func(i, j int) bool {
|
||
|
return results[i].QueryName < results[j].QueryName
|
||
|
})
|
||
|
|
||
|
// Create a map to store all unique labels
|
||
|
seen := make(map[string]struct{})
|
||
|
labelKeys := []string{}
|
||
|
for _, result := range results {
|
||
|
for _, series := range result.Series {
|
||
|
for _, labels := range series.LabelsArray {
|
||
|
for key := range labels {
|
||
|
if _, ok := seen[key]; !ok {
|
||
|
seen[key] = struct{}{}
|
||
|
labelKeys = append(labelKeys, key)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create columns
|
||
|
// Why don't we have a column for each query name?
|
||
|
// Because we don't know if the query is an aggregation query or a non-aggregation query
|
||
|
// So we create a column for each query name that has at least one point
|
||
|
columns := make([]*v3.TableColumn, 0)
|
||
|
for _, key := range labelKeys {
|
||
|
columns = append(columns, &v3.TableColumn{Name: key, IsValueColumn: false})
|
||
|
}
|
||
|
for _, result := range results {
|
||
|
if len(result.Series) > 0 && len(result.Series[0].Points) > 0 {
|
||
|
columns = append(columns, &v3.TableColumn{Name: result.QueryName, QueryName: result.QueryName, IsValueColumn: true})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
rows := make([]*v3.TableRow, 0)
|
||
|
for _, result := range results {
|
||
|
for _, series := range result.Series {
|
||
|
|
||
|
// Create a key for the row based on labels
|
||
|
rowData := make(map[string]interface{}, len(columns))
|
||
|
for _, key := range labelKeys {
|
||
|
value := "n/a"
|
||
|
for _, labels := range series.LabelsArray {
|
||
|
if v, ok := labels[key]; ok {
|
||
|
value = v
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
rowData[key] = value
|
||
|
}
|
||
|
|
||
|
// Get or create the row
|
||
|
row := &v3.TableRow{Data: rowData, QueryName: result.QueryName}
|
||
|
|
||
|
// Add the value for this query
|
||
|
for _, col := range columns {
|
||
|
if col.Name == result.QueryName && len(series.Points) > 0 {
|
||
|
row.Data[col.Name] = roundToTwoDecimal(series.Points[0].Value)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
rows = append(rows, row)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, row := range rows {
|
||
|
for _, col := range columns {
|
||
|
if col.IsValueColumn {
|
||
|
if row.Data[col.Name] == nil {
|
||
|
row.Data[col.Name] = "n/a"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create the final result
|
||
|
tableResult := v3.Result{
|
||
|
Table: &v3.Table{
|
||
|
Columns: columns,
|
||
|
Rows: rows,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
return []*v3.Result{&tableResult}
|
||
|
}
|