mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:29:02 +08:00
chore: add field mapper and condition builder for ts v4 (#8100)
This commit is contained in:
parent
74bbb26033
commit
982688ccc9
145
pkg/telemetrymetrics/condition_builder.go
Normal file
145
pkg/telemetrymetrics/condition_builder.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package telemetrymetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
|
||||||
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type conditionBuilder struct {
|
||||||
|
fm qbtypes.FieldMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
|
||||||
|
return &conditionBuilder{fm: fm}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conditionBuilder) conditionFor(
|
||||||
|
ctx context.Context,
|
||||||
|
key *telemetrytypes.TelemetryFieldKey,
|
||||||
|
operator qbtypes.FilterOperator,
|
||||||
|
value any,
|
||||||
|
sb *sqlbuilder.SelectBuilder,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
tblFieldName, err := c.fm.FieldFor(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch operator {
|
||||||
|
case qbtypes.FilterOperatorEqual:
|
||||||
|
return sb.E(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorNotEqual:
|
||||||
|
return sb.NE(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorGreaterThan:
|
||||||
|
return sb.G(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorGreaterThanOrEq:
|
||||||
|
return sb.GE(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorLessThan:
|
||||||
|
return sb.LT(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorLessThanOrEq:
|
||||||
|
return sb.LE(tblFieldName, value), nil
|
||||||
|
|
||||||
|
// like and not like
|
||||||
|
case qbtypes.FilterOperatorLike:
|
||||||
|
return sb.Like(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorNotLike:
|
||||||
|
return sb.NotLike(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorILike:
|
||||||
|
return sb.ILike(tblFieldName, value), nil
|
||||||
|
case qbtypes.FilterOperatorNotILike:
|
||||||
|
return sb.NotILike(tblFieldName, value), nil
|
||||||
|
|
||||||
|
case qbtypes.FilterOperatorContains:
|
||||||
|
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
|
case qbtypes.FilterOperatorNotContains:
|
||||||
|
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
||||||
|
|
||||||
|
case qbtypes.FilterOperatorRegexp:
|
||||||
|
return fmt.Sprintf(`match(%s, %s)`, tblFieldName, sb.Var(value)), nil
|
||||||
|
case qbtypes.FilterOperatorNotRegexp:
|
||||||
|
return fmt.Sprintf(`NOT match(%s, %s)`, tblFieldName, sb.Var(value)), nil
|
||||||
|
|
||||||
|
// between and not between
|
||||||
|
case qbtypes.FilterOperatorBetween:
|
||||||
|
values, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return "", qbtypes.ErrBetweenValues
|
||||||
|
}
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", qbtypes.ErrBetweenValues
|
||||||
|
}
|
||||||
|
return sb.Between(tblFieldName, values[0], values[1]), nil
|
||||||
|
case qbtypes.FilterOperatorNotBetween:
|
||||||
|
values, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return "", qbtypes.ErrBetweenValues
|
||||||
|
}
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", qbtypes.ErrBetweenValues
|
||||||
|
}
|
||||||
|
return sb.NotBetween(tblFieldName, values[0], values[1]), nil
|
||||||
|
|
||||||
|
// in and not in
|
||||||
|
case qbtypes.FilterOperatorIn:
|
||||||
|
values, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return "", qbtypes.ErrInValues
|
||||||
|
}
|
||||||
|
// instead of using IN, we use `=` + `OR` to make use of index
|
||||||
|
conditions := []string{}
|
||||||
|
for _, value := range values {
|
||||||
|
conditions = append(conditions, sb.E(tblFieldName, value))
|
||||||
|
}
|
||||||
|
return sb.Or(conditions...), nil
|
||||||
|
case qbtypes.FilterOperatorNotIn:
|
||||||
|
values, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return "", qbtypes.ErrInValues
|
||||||
|
}
|
||||||
|
// instead of using NOT IN, we use `!=` + `AND` to make use of index
|
||||||
|
conditions := []string{}
|
||||||
|
for _, value := range values {
|
||||||
|
conditions = append(conditions, sb.NE(tblFieldName, value))
|
||||||
|
}
|
||||||
|
return sb.And(conditions...), nil
|
||||||
|
|
||||||
|
// exists and not exists
|
||||||
|
// in the UI based query builder, `exists` and `not exists` are used for
|
||||||
|
// key membership checks, so depending on the column type, the condition changes
|
||||||
|
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||||
|
|
||||||
|
// if the field is intrinsic, it always exists
|
||||||
|
if slices.Contains(IntrinsicFields, key.Name) {
|
||||||
|
return "true", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if operator == qbtypes.FilterOperatorExists {
|
||||||
|
return fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", key.Name), nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", key.Name), nil
|
||||||
|
}
|
||||||
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conditionBuilder) ConditionFor(
|
||||||
|
ctx context.Context,
|
||||||
|
key *telemetrytypes.TelemetryFieldKey,
|
||||||
|
operator qbtypes.FilterOperator,
|
||||||
|
value any,
|
||||||
|
sb *sqlbuilder.SelectBuilder,
|
||||||
|
) (string, error) {
|
||||||
|
condition, err := c.conditionFor(ctx, key, operator, value, sb)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition, nil
|
||||||
|
}
|
295
pkg/telemetrymetrics/condition_builder_test.go
Normal file
295
pkg/telemetrymetrics/condition_builder_test.go
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
package telemetrymetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
"github.com/huandu/go-sqlbuilder"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConditionFor(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key telemetrytypes.TelemetryFieldKey
|
||||||
|
operator qbtypes.FilterOperator
|
||||||
|
value any
|
||||||
|
expectedSQL string
|
||||||
|
expectedArgs []any
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Equal operator - string",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorEqual,
|
||||||
|
value: "http.server.duration",
|
||||||
|
expectedSQL: "metric_name = ?",
|
||||||
|
expectedArgs: []any{"http.server.duration"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not Equal operator - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotEqual,
|
||||||
|
value: "http.server.duration",
|
||||||
|
expectedSQL: "metric_name <> ?",
|
||||||
|
expectedArgs: []any{"http.server.duration"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Like operator - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorLike,
|
||||||
|
value: "%error%",
|
||||||
|
expectedSQL: "metric_name LIKE ?",
|
||||||
|
expectedArgs: []any{"%error%"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not Like operator - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotLike,
|
||||||
|
value: "%error%",
|
||||||
|
expectedSQL: "metric_name NOT LIKE ?",
|
||||||
|
expectedArgs: []any{"%error%"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ILike operator - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorILike,
|
||||||
|
value: "%admin%",
|
||||||
|
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) LIKE LOWER(?)",
|
||||||
|
expectedArgs: []any{"%admin%"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not ILike operator - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotILike,
|
||||||
|
value: "%admin%",
|
||||||
|
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) NOT LIKE LOWER(?)",
|
||||||
|
expectedArgs: []any{"%admin%"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Contains operator - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorContains,
|
||||||
|
value: "admin",
|
||||||
|
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) LIKE LOWER(?)",
|
||||||
|
expectedArgs: []any{"%admin%"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "In operator - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorIn,
|
||||||
|
value: []any{"http.server.duration", "http.server.request.duration", "http.server.response.duration"},
|
||||||
|
expectedSQL: "(metric_name = ? OR metric_name = ? OR metric_name = ?)",
|
||||||
|
expectedArgs: []any{"http.server.duration", "http.server.request.duration", "http.server.response.duration"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "In operator - invalid value",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorIn,
|
||||||
|
value: "error",
|
||||||
|
expectedSQL: "",
|
||||||
|
expectedError: qbtypes.ErrInValues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not In operator - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotIn,
|
||||||
|
value: []any{"debug", "info", "trace"},
|
||||||
|
expectedSQL: "(metric_name <> ? AND metric_name <> ? AND metric_name <> ?)",
|
||||||
|
expectedArgs: []any{"debug", "info", "trace"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exists operator - string field",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorExists,
|
||||||
|
value: nil,
|
||||||
|
expectedSQL: "true",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not Exists operator - string field",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotExists,
|
||||||
|
value: nil,
|
||||||
|
expectedSQL: "true",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exists operator - type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "type",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorExists,
|
||||||
|
value: nil,
|
||||||
|
expectedSQL: "true",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Exists operator - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorExists,
|
||||||
|
value: nil,
|
||||||
|
expectedSQL: "has(JSONExtractKeys(labels), 'user.id')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Not Exists operator - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorNotExists,
|
||||||
|
value: nil,
|
||||||
|
expectedSQL: "not has(JSONExtractKeys(labels), 'user.id')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existent column",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "nonexistent_field",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorEqual,
|
||||||
|
value: "value",
|
||||||
|
expectedSQL: "",
|
||||||
|
expectedError: qbtypes.ErrColumnNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
conditionBuilder := NewConditionBuilder(fm)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cond, err := conditionBuilder.ConditionFor(ctx, &tc.key, tc.operator, tc.value, sb)
|
||||||
|
sb.Where(cond)
|
||||||
|
|
||||||
|
if tc.expectedError != nil {
|
||||||
|
assert.Equal(t, tc.expectedError, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
assert.Contains(t, sql, tc.expectedSQL)
|
||||||
|
assert.Equal(t, tc.expectedArgs, args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConditionForMultipleKeys(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
keys []telemetrytypes.TelemetryFieldKey
|
||||||
|
operator qbtypes.FilterOperator
|
||||||
|
value any
|
||||||
|
expectedSQL string
|
||||||
|
expectedArgs []any
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Equal operator - string",
|
||||||
|
keys: []telemetrytypes.TelemetryFieldKey{
|
||||||
|
{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "type",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
operator: qbtypes.FilterOperatorEqual,
|
||||||
|
value: "error message",
|
||||||
|
expectedSQL: "metric_name = ? AND type = ?",
|
||||||
|
expectedArgs: []any{"error message", "error message"},
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
conditionBuilder := NewConditionBuilder(fm)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
sb := sqlbuilder.NewSelectBuilder()
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
for _, key := range tc.keys {
|
||||||
|
cond, err := conditionBuilder.ConditionFor(ctx, &key, tc.operator, tc.value, sb)
|
||||||
|
sb.Where(cond)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error getting condition for key %s: %v", key.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil {
|
||||||
|
assert.Equal(t, tc.expectedError, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||||
|
assert.Contains(t, sql, tc.expectedSQL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
8
pkg/telemetrymetrics/const.go
Normal file
8
pkg/telemetrymetrics/const.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package telemetrymetrics
|
||||||
|
|
||||||
|
var IntrinsicFields = []string{
|
||||||
|
"temporality",
|
||||||
|
"metric_name",
|
||||||
|
"type",
|
||||||
|
"is_monotonic",
|
||||||
|
}
|
90
pkg/telemetrymetrics/field_mapper.go
Normal file
90
pkg/telemetrymetrics/field_mapper.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package telemetrymetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeSeriesV4Columns = map[string]*schema.Column{
|
||||||
|
"temporality": {Name: "temporality", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||||
|
"metric_name": {Name: "metric_name", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||||
|
"type": {Name: "type", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||||
|
"is_monotonic": {Name: "is_monotonic", Type: schema.ColumnTypeBool},
|
||||||
|
"fingerprint": {Name: "fingerprint", Type: schema.ColumnTypeUInt64},
|
||||||
|
"unix_milli": {Name: "unix_milli", Type: schema.ColumnTypeInt64},
|
||||||
|
"labels": {Name: "labels", Type: schema.ColumnTypeString},
|
||||||
|
"attrs": {Name: "attrs", Type: schema.MapColumnType{
|
||||||
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
||||||
|
ValueType: schema.ColumnTypeString,
|
||||||
|
}},
|
||||||
|
"scope_attrs": {Name: "scope_attrs", Type: schema.MapColumnType{
|
||||||
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
||||||
|
ValueType: schema.ColumnTypeString,
|
||||||
|
}},
|
||||||
|
"resource_attrs": {Name: "resource_attrs", Type: schema.MapColumnType{
|
||||||
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
||||||
|
ValueType: schema.ColumnTypeString,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type fieldMapper struct{}
|
||||||
|
|
||||||
|
func NewFieldMapper() qbtypes.FieldMapper {
|
||||||
|
return &fieldMapper{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
|
||||||
|
|
||||||
|
switch key.FieldContext {
|
||||||
|
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextScope, telemetrytypes.FieldContextAttribute:
|
||||||
|
return timeSeriesV4Columns["labels"], nil
|
||||||
|
case telemetrytypes.FieldContextMetric, telemetrytypes.FieldContextUnspecified:
|
||||||
|
col, ok := timeSeriesV4Columns[key.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, qbtypes.ErrColumnNotFound
|
||||||
|
}
|
||||||
|
return col, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, qbtypes.ErrColumnNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (string, error) {
|
||||||
|
column, err := m.getColumn(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key.FieldContext {
|
||||||
|
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextScope, telemetrytypes.FieldContextAttribute:
|
||||||
|
return fmt.Sprintf("JSONExtractString(%s, '%s')", column.Name, key.Name), nil
|
||||||
|
case telemetrytypes.FieldContextMetric:
|
||||||
|
return column.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return column.Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fieldMapper) ColumnFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
|
||||||
|
return m.getColumn(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fieldMapper) ColumnExpressionFor(
|
||||||
|
ctx context.Context,
|
||||||
|
field *telemetrytypes.TelemetryFieldKey,
|
||||||
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||||
|
) (string, error) {
|
||||||
|
|
||||||
|
colName, err := m.FieldFor(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s AS `%s`", colName, field.Name), nil
|
||||||
|
}
|
220
pkg/telemetrymetrics/field_mapper_test.go
Normal file
220
pkg/telemetrymetrics/field_mapper_test.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package telemetrymetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||||
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetColumn(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key telemetrytypes.TelemetryFieldKey
|
||||||
|
expectedCol *schema.Column
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Resource field",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service.name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attribute field - string type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attribute field - number type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "request.size",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attribute field - int64 type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "request.duration",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeInt64,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attribute field - float64 type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "cpu.utilization",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attribute field - bool type",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "request.success",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeBool,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Metric field - temporality",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "temporality",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["temporality"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Metric field - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["metric_name"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Metric field - nonexistent",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "nonexistent_field",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
expectedCol: nil,
|
||||||
|
expectedError: qbtypes.ErrColumnNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "did_user_login",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "did_user_login",
|
||||||
|
Signal: telemetrytypes.SignalMetrics,
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeBool,
|
||||||
|
},
|
||||||
|
expectedCol: timeSeriesV4Columns["labels"],
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
col, err := fm.ColumnFor(ctx, &tc.key)
|
||||||
|
|
||||||
|
if tc.expectedError != nil {
|
||||||
|
assert.Equal(t, tc.expectedError, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedCol, col)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFieldKeyName(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
key telemetrytypes.TelemetryFieldKey
|
||||||
|
expectedResult string
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Simple column type - metric_name",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "metric_name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
expectedResult: "metric_name",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map column type - string label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "user.id",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||||
|
},
|
||||||
|
expectedResult: "JSONExtractString(labels, 'user.id')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map column type - number label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "request.size",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||||
|
},
|
||||||
|
expectedResult: "JSONExtractString(labels, 'request.size')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map column type - bool label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "request.success",
|
||||||
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||||
|
FieldDataType: telemetrytypes.FieldDataTypeBool,
|
||||||
|
},
|
||||||
|
expectedResult: "JSONExtractString(labels, 'request.success')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Map column type - resource label",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "service.name",
|
||||||
|
FieldContext: telemetrytypes.FieldContextResource,
|
||||||
|
},
|
||||||
|
expectedResult: "JSONExtractString(labels, 'service.name')",
|
||||||
|
expectedError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existent column",
|
||||||
|
key: telemetrytypes.TelemetryFieldKey{
|
||||||
|
Name: "nonexistent_field",
|
||||||
|
FieldContext: telemetrytypes.FieldContextMetric,
|
||||||
|
},
|
||||||
|
expectedResult: "",
|
||||||
|
expectedError: qbtypes.ErrColumnNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := NewFieldMapper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := fm.FieldFor(ctx, &tc.key)
|
||||||
|
|
||||||
|
if tc.expectedError != nil {
|
||||||
|
assert.Equal(t, tc.expectedError, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedResult, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user