From 982688ccc94cfc5d90950baab4e06f068eb10fbf Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 3 Jun 2025 01:13:48 +0530 Subject: [PATCH] chore: add field mapper and condition builder for ts v4 (#8100) --- pkg/telemetrymetrics/condition_builder.go | 145 +++++++++ .../condition_builder_test.go | 295 ++++++++++++++++++ pkg/telemetrymetrics/const.go | 8 + pkg/telemetrymetrics/field_mapper.go | 90 ++++++ pkg/telemetrymetrics/field_mapper_test.go | 220 +++++++++++++ 5 files changed, 758 insertions(+) create mode 100644 pkg/telemetrymetrics/condition_builder.go create mode 100644 pkg/telemetrymetrics/condition_builder_test.go create mode 100644 pkg/telemetrymetrics/const.go create mode 100644 pkg/telemetrymetrics/field_mapper.go create mode 100644 pkg/telemetrymetrics/field_mapper_test.go diff --git a/pkg/telemetrymetrics/condition_builder.go b/pkg/telemetrymetrics/condition_builder.go new file mode 100644 index 0000000000..28074c7c8c --- /dev/null +++ b/pkg/telemetrymetrics/condition_builder.go @@ -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 +} diff --git a/pkg/telemetrymetrics/condition_builder_test.go b/pkg/telemetrymetrics/condition_builder_test.go new file mode 100644 index 0000000000..9d6ae2e53e --- /dev/null +++ b/pkg/telemetrymetrics/condition_builder_test.go @@ -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) + } + }) + } +} diff --git a/pkg/telemetrymetrics/const.go b/pkg/telemetrymetrics/const.go new file mode 100644 index 0000000000..4522534829 --- /dev/null +++ b/pkg/telemetrymetrics/const.go @@ -0,0 +1,8 @@ +package telemetrymetrics + +var IntrinsicFields = []string{ + "temporality", + "metric_name", + "type", + "is_monotonic", +} diff --git a/pkg/telemetrymetrics/field_mapper.go b/pkg/telemetrymetrics/field_mapper.go new file mode 100644 index 0000000000..624dab3089 --- /dev/null +++ b/pkg/telemetrymetrics/field_mapper.go @@ -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 +} diff --git a/pkg/telemetrymetrics/field_mapper_test.go b/pkg/telemetrymetrics/field_mapper_test.go new file mode 100644 index 0000000000..9a82420879 --- /dev/null +++ b/pkg/telemetrymetrics/field_mapper_test.go @@ -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) + } + }) + } +}