logs-analyzer/signoz/pkg/query-service/postprocess/table.go
2024-09-02 22:47:30 +03:00

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}
}