mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-06 16:30:42 +00:00
Compare commits
6 Commits
feat/input
...
tvats-fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5dd69ab92 | ||
|
|
1821847457 | ||
|
|
ddad2a5f93 | ||
|
|
c493811ac6 | ||
|
|
219e6ac54c | ||
|
|
a04f65a4f2 |
@@ -15826,13 +15826,23 @@ paths:
|
||||
where it is case-sensitive and % and _ act as wildcards. For keys, a context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.'
|
||||
type: string
|
||||
- description: The key whose values to return, such as service.name. A context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.
|
||||
- description: 'The key whose values to return, such as service.name. With fieldContext
|
||||
set, it is the stored name as is. Otherwise a context prefix such as resource.service.name
|
||||
sets the context and a type suffix such as service.name:string sets fieldDataType
|
||||
when that is empty; as in filter expressions, a prefixed name other than
|
||||
resource. also matches a key stored under the full name, whose values come
|
||||
first: log.level returns values of a key named log.level, then of the log
|
||||
field level.'
|
||||
in: query
|
||||
name: name
|
||||
schema:
|
||||
description: The key whose values to return, such as service.name. A context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.
|
||||
description: 'The key whose values to return, such as service.name. With
|
||||
fieldContext set, it is the stored name as is. Otherwise a context prefix
|
||||
such as resource.service.name sets the context and a type suffix such
|
||||
as service.name:string sets fieldDataType when that is empty; as in filter
|
||||
expressions, a prefixed name other than resource. also matches a key stored
|
||||
under the full name, whose values come first: log.level returns values
|
||||
of a key named log.level, then of the log field level.'
|
||||
type: string
|
||||
- description: Filter expression already applied in the explorer. When set,
|
||||
relatedValues lists the values seen on data matching it.
|
||||
@@ -28411,13 +28421,23 @@ paths:
|
||||
where it is case-sensitive and % and _ act as wildcards. For keys, a context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.'
|
||||
type: string
|
||||
- description: The key whose values to return, such as service.name. A context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.
|
||||
- description: 'The key whose values to return, such as service.name. With fieldContext
|
||||
set, it is the stored name as is. Otherwise a context prefix such as resource.service.name
|
||||
sets the context and a type suffix such as service.name:string sets fieldDataType
|
||||
when that is empty; as in filter expressions, a prefixed name other than
|
||||
resource. also matches a key stored under the full name, whose values come
|
||||
first: log.level returns values of a key named log.level, then of the log
|
||||
field level.'
|
||||
in: query
|
||||
name: name
|
||||
schema:
|
||||
description: The key whose values to return, such as service.name. A context
|
||||
prefix such as resource.service.name sets fieldContext when it is empty.
|
||||
description: 'The key whose values to return, such as service.name. With
|
||||
fieldContext set, it is the stored name as is. Otherwise a context prefix
|
||||
such as resource.service.name sets the context and a type suffix such
|
||||
as service.name:string sets fieldDataType when that is empty; as in filter
|
||||
expressions, a prefixed name other than resource. also matches a key stored
|
||||
under the full name, whose values come first: log.level returns values
|
||||
of a key named log.level, then of the log field level.'
|
||||
type: string
|
||||
- description: Filter expression already applied in the explorer. When set,
|
||||
relatedValues lists the values seen on data matching it.
|
||||
|
||||
50
pkg/telemetrymetadata/context_prefixed_values_test.go
Normal file
50
pkg/telemetrymetadata/context_prefixed_values_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetAllValuesExactNameFirst(t *testing.T) {
|
||||
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, ®exMatcher{})
|
||||
mock := mockTelemetryStore.Mock()
|
||||
metadata := NewTelemetryMetaStore(instrumentationtest.New().ToProviderSettings(), mockTelemetryStore, flaggertest.New(t))
|
||||
|
||||
boolColumns := []cmock.ColumnType{{Name: "1", Type: "UInt8"}}
|
||||
valueColumns := []cmock.ColumnType{{Name: "string_value", Type: "String"}, {Name: "number_value", Type: "Float64"}}
|
||||
|
||||
mock.ExpectQuery(`SELECT 1 FROM .*tag_key = \? AND tag_data_type = \? LIMIT`).
|
||||
WithArgs("log.level", "bool", 1).
|
||||
WillReturnRows(cmock.NewRows(boolColumns, [][]any{}))
|
||||
mock.ExpectQuery(`SELECT DISTINCT string_value, number_value FROM .*tag_key = \? AND tag_data_type <> \? LIMIT`).
|
||||
WithArgs("log.level", "bool", 3).
|
||||
WillReturnRows(cmock.NewRows(valueColumns, [][]any{{"warn", float64(0)}, {"info", float64(0)}}))
|
||||
mock.ExpectQuery(`SELECT 1 FROM .*tag_key = \? AND tag_data_type = \? AND tag_type = \?`).
|
||||
WithArgs("level", "bool", "logfield", 1).
|
||||
WillReturnRows(cmock.NewRows(boolColumns, [][]any{}))
|
||||
mock.ExpectQuery(`SELECT DISTINCT string_value, number_value FROM .*tag_key = \? AND tag_data_type <> \? AND tag_type = \?`).
|
||||
WithArgs("level", "bool", "logfield", 3).
|
||||
WillReturnRows(cmock.NewRows(valueColumns, [][]any{{"info", float64(0)}, {"debug", float64(0)}}))
|
||||
|
||||
selector := telemetrytypes.NewFieldValueSelectorFromPostableFieldValueParams(telemetrytypes.PostableFieldValueParams{
|
||||
Name: "log.level",
|
||||
PostableFieldKeysParams: telemetrytypes.PostableFieldKeysParams{Signal: telemetrytypes.SignalLogs, Limit: 2},
|
||||
})
|
||||
|
||||
values, complete, err := metadata.GetAllValues(context.Background(), valuer.GenerateUUID(), selector)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"warn", "info"}, values.StringValues)
|
||||
assert.False(t, complete)
|
||||
assert.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
@@ -1401,6 +1401,8 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
|
||||
}
|
||||
|
||||
sb := sqlbuilder.Select("DISTINCT " + selectColumn).From(t.relatedMetadataDBName + "." + t.relatedMetadataTblName)
|
||||
// rows without the key read as empty; drop them so they do not count towards the limit
|
||||
sb.Where(fmt.Sprintf("notEmpty(%s)", selectColumn))
|
||||
|
||||
if len(fieldValueSelector.ExistingQuery) != 0 {
|
||||
keySelectors := querybuilder.QueryStringToKeysSelectors(fieldValueSelector.ExistingQuery)
|
||||
@@ -1517,7 +1519,24 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) GetRelatedValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
|
||||
return t.getRelatedValues(ctx, orgID, fieldValueSelector)
|
||||
selectors := fieldValueSelector.NameMatchSelectors()
|
||||
if len(selectors) == 1 {
|
||||
return t.getRelatedValues(ctx, orgID, fieldValueSelector)
|
||||
}
|
||||
|
||||
all := make([]*telemetrytypes.TelemetryFieldValues, 0, len(selectors))
|
||||
complete := true
|
||||
for _, selector := range selectors {
|
||||
values, selectorComplete, err := t.getRelatedValues(ctx, orgID, selector)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
all = append(all, &telemetrytypes.TelemetryFieldValues{RelatedValues: values})
|
||||
complete = complete && selectorComplete
|
||||
}
|
||||
|
||||
merged, mergeComplete := mergeFieldValues(fieldValueSelector.Limit, all...)
|
||||
return merged.RelatedValues, complete && mergeComplete, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) getSpanFieldValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) (*telemetrytypes.TelemetryFieldValues, bool, error) {
|
||||
@@ -1826,7 +1845,7 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, orgID val
|
||||
}
|
||||
|
||||
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
sb.Where(sb.E("attr_type", fieldValueSelector.FieldContext.TagType()))
|
||||
sb.Where(sb.E("attr_type", fieldValueSelector.FieldContext.MetricAttrType()))
|
||||
}
|
||||
|
||||
if fieldValueSelector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
|
||||
@@ -2092,6 +2111,23 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
|
||||
return values, complete, nil
|
||||
}
|
||||
|
||||
// mergeFieldValues dedupes the values into one result capped at limit, and reports
|
||||
// false when the cap dropped any.
|
||||
func mergeFieldValues(limit int, values ...*telemetrytypes.TelemetryFieldValues) (*telemetrytypes.TelemetryFieldValues, bool) {
|
||||
if limit == 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
merged := &telemetrytypes.TelemetryFieldValues{}
|
||||
mapOfValues := make(map[any]bool)
|
||||
mapOfRelatedValues := make(map[any]bool)
|
||||
complete := true
|
||||
for _, v := range values {
|
||||
complete = populateAllUnspecifiedValues(merged, mapOfValues, mapOfRelatedValues, v, limit) && complete
|
||||
}
|
||||
return merged, complete
|
||||
}
|
||||
|
||||
func populateAllUnspecifiedValues(allUnspecifiedValues *telemetrytypes.TelemetryFieldValues, mapOfValues map[any]bool, mapOfRelatedValues map[any]bool, values *telemetrytypes.TelemetryFieldValues, limit int) bool {
|
||||
complete := true
|
||||
totalCount := len(mapOfValues) + len(mapOfRelatedValues)
|
||||
@@ -2149,6 +2185,27 @@ func populateAllUnspecifiedValues(allUnspecifiedValues *telemetrytypes.Telemetry
|
||||
|
||||
// GetAllValues returns all values and whether the result is complete.
|
||||
func (t *telemetryMetaStore) GetAllValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) (*telemetrytypes.TelemetryFieldValues, bool, error) {
|
||||
selectors := fieldValueSelector.NameMatchSelectors()
|
||||
if len(selectors) == 1 {
|
||||
return t.getAllValues(ctx, orgID, fieldValueSelector)
|
||||
}
|
||||
|
||||
all := make([]*telemetrytypes.TelemetryFieldValues, 0, len(selectors))
|
||||
complete := true
|
||||
for _, selector := range selectors {
|
||||
values, selectorComplete, err := t.getAllValues(ctx, orgID, selector)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
all = append(all, values)
|
||||
complete = complete && selectorComplete
|
||||
}
|
||||
|
||||
merged, mergeComplete := mergeFieldValues(fieldValueSelector.Limit, all...)
|
||||
return merged, complete && mergeComplete, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) getAllValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) (*telemetrytypes.TelemetryFieldValues, bool, error) {
|
||||
values := &telemetrytypes.TelemetryFieldValues{}
|
||||
var complete = true
|
||||
var err error
|
||||
|
||||
@@ -147,7 +147,11 @@ func (f *TelemetryFieldKey) Equal(key *TelemetryFieldKey) bool {
|
||||
// key.Normalize()
|
||||
// // Result: Name: "service.name", FieldContext: FieldContextResource, FieldDataType: FieldDataTypeString
|
||||
func (f *TelemetryFieldKey) Normalize() {
|
||||
f.normalizeDataType()
|
||||
f.normalizeContext()
|
||||
}
|
||||
|
||||
func (f *TelemetryFieldKey) normalizeDataType() {
|
||||
// Step 1: Parse data type from the right (after the last ":") if not already specified
|
||||
if f.FieldDataType == FieldDataTypeUnspecified {
|
||||
if colonIdx := strings.LastIndex(f.Name, ":"); colonIdx != -1 {
|
||||
@@ -158,7 +162,9 @@ func (f *TelemetryFieldKey) Normalize() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TelemetryFieldKey) normalizeContext() {
|
||||
// Step 2: Parse field context from the left if not already specified
|
||||
if f.FieldContext == FieldContextUnspecified {
|
||||
if dotIdx := strings.Index(f.Name, "."); dotIdx != -1 {
|
||||
@@ -279,6 +285,9 @@ type FieldValueSelector struct {
|
||||
ExistingQuery string `json:"existingQuery" description:"Filter expression already applied in the explorer. Used to compute relatedValues; empty disables them."`
|
||||
Value string `json:"value" description:"Substring the returned values must contain, case-insensitive except for the meter source, where it is case-sensitive and % and _ act as wildcards. Empty returns any value."`
|
||||
Limit int `json:"limit" description:"Maximum number of values to return."`
|
||||
|
||||
// nameAsWritten is set when FieldContext was parsed from the name's prefix.
|
||||
nameAsWritten string
|
||||
}
|
||||
|
||||
type GettableFieldKeys struct {
|
||||
@@ -306,7 +315,7 @@ type GettableFieldValues struct {
|
||||
|
||||
type PostableFieldValueParams struct {
|
||||
PostableFieldKeysParams
|
||||
Name string `query:"name" description:"The key whose values to return, such as service.name. A context prefix such as resource.service.name sets fieldContext when it is empty."`
|
||||
Name string `query:"name" description:"The key whose values to return, such as service.name. With fieldContext set, it is the stored name as is. Otherwise a context prefix such as resource.service.name sets the context and a type suffix such as service.name:string sets fieldDataType when that is empty; as in filter expressions, a prefixed name other than resource. also matches a key stored under the full name, whose values come first: log.level returns values of a key named log.level, then of the log field level."`
|
||||
ExistingQuery string `query:"existingQuery" description:"Filter expression already applied in the explorer. When set, relatedValues lists the values seen on data matching it."`
|
||||
}
|
||||
|
||||
@@ -358,26 +367,36 @@ func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysPara
|
||||
}
|
||||
|
||||
func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValueParams) *FieldValueSelector {
|
||||
|
||||
keySelector := NewFieldKeySelectorFromPostableFieldKeysParams(params.PostableFieldKeysParams)
|
||||
// SearchText filters values here, so it must not be parsed as a key.
|
||||
keyParams := params.PostableFieldKeysParams
|
||||
keyParams.SearchText = ""
|
||||
keySelector := NewFieldKeySelectorFromPostableFieldKeysParams(keyParams)
|
||||
|
||||
fieldValueSelector := &FieldValueSelector{
|
||||
FieldKeySelector: keySelector,
|
||||
}
|
||||
|
||||
fieldValueSelector.Name = params.Name
|
||||
|
||||
if params.Name != "" && fieldValueSelector.FieldContext == FieldContextUnspecified {
|
||||
parsedFieldKey := GetFieldKeyFromKeyText(params.Name)
|
||||
if parsedFieldKey.FieldContext != FieldContextUnspecified {
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(parsedFieldKey.FieldContext, fieldValueSelector.Signal) {
|
||||
fieldValueSelector.Name = parsedFieldKey.Name
|
||||
fieldValueSelector.FieldContext = parsedFieldKey.FieldContext
|
||||
// An explicit context means the name is the stored name as is.
|
||||
key := TelemetryFieldKey{Name: params.Name, FieldContext: keySelector.FieldContext, FieldDataType: keySelector.FieldDataType}
|
||||
if keySelector.FieldContext == FieldContextUnspecified {
|
||||
key.normalizeDataType()
|
||||
nameWithContext := key.Name
|
||||
key.normalizeContext()
|
||||
// Only apply inferred context if it is valid for the current signal
|
||||
if isContextValidForSignal(key.FieldContext, keySelector.Signal) {
|
||||
// resource is a keyword prefix, never part of a stored name.
|
||||
if key.FieldContext != FieldContextUnspecified && key.FieldContext != FieldContextResource {
|
||||
fieldValueSelector.nameAsWritten = nameWithContext
|
||||
}
|
||||
} else {
|
||||
key.Name = nameWithContext
|
||||
key.FieldContext = FieldContextUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
fieldValueSelector.Name = key.Name
|
||||
fieldValueSelector.FieldContext = key.FieldContext
|
||||
fieldValueSelector.FieldDataType = key.FieldDataType
|
||||
fieldValueSelector.ExistingQuery = params.ExistingQuery
|
||||
fieldValueSelector.Value = params.SearchText
|
||||
|
||||
@@ -390,6 +409,29 @@ func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValue
|
||||
return fieldValueSelector
|
||||
}
|
||||
|
||||
// NameMatchSelectors returns the lookups for the requested key, highest priority first.
|
||||
// A context parsed from the name may instead be part of the stored name (attribute.key
|
||||
// may be the attribute named attribute.key), so like the query builder both readings are
|
||||
// looked up: the name as written, in any context, before the name with its prefix stripped.
|
||||
func (s *FieldValueSelector) NameMatchSelectors() []*FieldValueSelector {
|
||||
if s.nameAsWritten != "" {
|
||||
return []*FieldValueSelector{s.withName(s.nameAsWritten), s}
|
||||
}
|
||||
return []*FieldValueSelector{s}
|
||||
}
|
||||
|
||||
// withName returns a copy looking up name in any context.
|
||||
func (s *FieldValueSelector) withName(name string) *FieldValueSelector {
|
||||
keySelector := *s.FieldKeySelector
|
||||
keySelector.Name = name
|
||||
keySelector.FieldContext = FieldContextUnspecified
|
||||
|
||||
selector := *s
|
||||
selector.FieldKeySelector = &keySelector
|
||||
selector.nameAsWritten = ""
|
||||
return &selector
|
||||
}
|
||||
|
||||
func NewTelemetryFieldKey(name string, fieldContext FieldContext, fieldDataType FieldDataType) *TelemetryFieldKey {
|
||||
return &TelemetryFieldKey{
|
||||
Name: name,
|
||||
|
||||
@@ -164,6 +164,15 @@ func (f FieldContext) TagType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// MetricAttrType returns the attr_type the metrics metadata table stores for the context;
|
||||
// metric attributes are stored as point.
|
||||
func (f FieldContext) MetricAttrType() string {
|
||||
if f == FieldContextAttribute {
|
||||
return "point"
|
||||
}
|
||||
return f.TagType()
|
||||
}
|
||||
|
||||
func isContextValidForSignal(ctx FieldContext, signal Signal) bool {
|
||||
if ctx == FieldContextResource ||
|
||||
ctx == FieldContextAttribute ||
|
||||
|
||||
25
pkg/types/telemetrytypes/field_context_test.go
Normal file
25
pkg/types/telemetrytypes/field_context_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package telemetrytypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFieldContextMetricAttrType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
context FieldContext
|
||||
expected string
|
||||
}{
|
||||
{name: "Attribute_Point", context: FieldContextAttribute, expected: "point"},
|
||||
{name: "Resource_Resource", context: FieldContextResource, expected: "resource"},
|
||||
{name: "Scope_Scope", context: FieldContextScope, expected: "scope"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expected, testCase.context.MetricAttrType())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package telemetrytypes
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetFieldKeyFromKeyText(t *testing.T) {
|
||||
@@ -422,3 +425,168 @@ func TestNewFieldKeySelectorFromPostableFieldKeysParamsMetricNamespace(t *testin
|
||||
t.Fatalf("expected metric name to remain empty, got %q", selector.MetricContext.MetricName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFieldValueSelectorFromPostableFieldValueParams(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
params PostableFieldValueParams
|
||||
expectedName string
|
||||
expectedContext FieldContext
|
||||
expectedDataType FieldDataType
|
||||
expectedValue string
|
||||
expectedLookups []TelemetryFieldKey
|
||||
}{
|
||||
{
|
||||
name: "ResourcePrefix_AlwaysContext_SingleLookup",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "resource.service.name"},
|
||||
expectedName: "service.name",
|
||||
expectedContext: FieldContextResource,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "service.name", FieldContext: FieldContextResource}},
|
||||
},
|
||||
{
|
||||
name: "AttributePrefix_Parsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "attribute.key"},
|
||||
expectedName: "key",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "attribute.key", FieldContext: FieldContextUnspecified}, {Name: "key", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "DoubleAttributePrefix_OnlyFirstStripped",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "attribute.attribute.key"},
|
||||
expectedName: "attribute.key",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "attribute.attribute.key", FieldContext: FieldContextUnspecified}, {Name: "attribute.key", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "TagAlias_ExactMatchAsWritten",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "tag.region"},
|
||||
expectedName: "region",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "tag.region", FieldContext: FieldContextUnspecified}, {Name: "region", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "LogPrefix_Parsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "log.level"},
|
||||
expectedName: "level",
|
||||
expectedContext: FieldContextLog,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "log.level", FieldContext: FieldContextUnspecified}, {Name: "level", FieldContext: FieldContextLog}},
|
||||
},
|
||||
{
|
||||
name: "SpanPrefix_Parsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalTraces}, Name: "span.kind"},
|
||||
expectedName: "kind",
|
||||
expectedContext: FieldContextSpan,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "span.kind", FieldContext: FieldContextUnspecified}, {Name: "kind", FieldContext: FieldContextSpan}},
|
||||
},
|
||||
{
|
||||
name: "ContextInvalidForSignal_KeptAsWritten",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "span.kind"},
|
||||
expectedName: "span.kind",
|
||||
expectedContext: FieldContextUnspecified,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "span.kind", FieldContext: FieldContextUnspecified}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitContext_NameNotParsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldContext: FieldContextAttribute}, Name: "log.level"},
|
||||
expectedName: "log.level",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "log.level", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitBodyContext_SingleLookup",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldContext: FieldContextBody}, Name: "key"},
|
||||
expectedName: "key",
|
||||
expectedContext: FieldContextBody,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "key", FieldContext: FieldContextBody}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitAttributeContext_SingleLookup",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldContext: FieldContextAttribute}, Name: "key"},
|
||||
expectedName: "key",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "key", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitAttributeContext_PrefixedStoredName",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldContext: FieldContextAttribute}, Name: "attribute.key"},
|
||||
expectedName: "attribute.key",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "attribute.key", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitContext_TypeSuffixNotParsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldContext: FieldContextAttribute}, Name: "http.method:string"},
|
||||
expectedName: "http.method:string",
|
||||
expectedContext: FieldContextAttribute,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "http.method:string", FieldContext: FieldContextAttribute}},
|
||||
},
|
||||
{
|
||||
name: "DataTypeSuffix_WithoutContext",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "service.name:string"},
|
||||
expectedName: "service.name",
|
||||
expectedContext: FieldContextUnspecified,
|
||||
expectedDataType: FieldDataTypeString,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "service.name", FieldContext: FieldContextUnspecified}},
|
||||
},
|
||||
{
|
||||
name: "DataTypeSuffix_WithContext",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "resource.host.name:string"},
|
||||
expectedName: "host.name",
|
||||
expectedContext: FieldContextResource,
|
||||
expectedDataType: FieldDataTypeString,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "host.name", FieldContext: FieldContextResource}},
|
||||
},
|
||||
{
|
||||
name: "DataTypeSuffix_WithInvalidContext",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs}, Name: "span.kind:int64"},
|
||||
expectedName: "span.kind",
|
||||
expectedContext: FieldContextUnspecified,
|
||||
expectedDataType: FieldDataTypeInt64,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "span.kind", FieldContext: FieldContextUnspecified}},
|
||||
},
|
||||
{
|
||||
name: "ExplicitDataType_SuffixKept",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, FieldDataType: FieldDataTypeBool}, Name: "http.method:string"},
|
||||
expectedName: "http.method:string",
|
||||
expectedContext: FieldContextUnspecified,
|
||||
expectedDataType: FieldDataTypeBool,
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "http.method:string", FieldContext: FieldContextUnspecified}},
|
||||
},
|
||||
{
|
||||
name: "SearchTextWithContextPrefix_NotParsedAsKey",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, SearchText: "log.x"}, Name: "service.name"},
|
||||
expectedName: "service.name",
|
||||
expectedContext: FieldContextUnspecified,
|
||||
expectedValue: "log.x",
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "service.name", FieldContext: FieldContextUnspecified}},
|
||||
},
|
||||
{
|
||||
name: "SearchTextWithContextPrefix_NameStillParsed",
|
||||
params: PostableFieldValueParams{PostableFieldKeysParams: PostableFieldKeysParams{Signal: SignalLogs, SearchText: "attribute.x"}, Name: "resource.k8s.pod.name"},
|
||||
expectedName: "k8s.pod.name",
|
||||
expectedContext: FieldContextResource,
|
||||
expectedValue: "attribute.x",
|
||||
expectedLookups: []TelemetryFieldKey{{Name: "k8s.pod.name", FieldContext: FieldContextResource}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
selector := NewFieldValueSelectorFromPostableFieldValueParams(testCase.params)
|
||||
|
||||
assert.Equal(t, testCase.expectedName, selector.Name)
|
||||
assert.Equal(t, testCase.expectedContext, selector.FieldContext)
|
||||
assert.Equal(t, testCase.expectedDataType, selector.FieldDataType)
|
||||
assert.Equal(t, testCase.expectedValue, selector.Value)
|
||||
|
||||
lookups := selector.NameMatchSelectors()
|
||||
require.Len(t, lookups, len(testCase.expectedLookups))
|
||||
for i, expected := range testCase.expectedLookups {
|
||||
assert.Equal(t, expected.Name, lookups[i].Name)
|
||||
assert.Equal(t, expected.FieldContext, lookups[i].FieldContext)
|
||||
assert.Equal(t, selector.FieldDataType, lookups[i].FieldDataType)
|
||||
}
|
||||
assert.Equal(t, testCase.expectedName, selector.Name, "lookups must not mutate the selector")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,81 +106,3 @@ def test_list_meter_metric_names(
|
||||
metrics = data.get("data", {}).get("metrics", [])
|
||||
metric_names = [m["metricName"] for m in metrics]
|
||||
assert metric_name in metric_names, f"Expected {metric_name} in metric names, got: {metric_names}"
|
||||
|
||||
|
||||
# Verify /api/v1/fields/values with source=meter filters label values by metricNamespace
|
||||
# prefix. Inserts meter-source metrics under ns.a and ns.b, then asserts a specific
|
||||
# prefix returns only matching values while a common prefix returns both.
|
||||
def test_metric_namespace_meter_values_filtering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_meter_samples: Callable[[list[MeterSample]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
samples_a = make_meter_samples(
|
||||
"meter.ns.a.cost",
|
||||
{"service": "billing-a"},
|
||||
now,
|
||||
count=5,
|
||||
base_value=10.0,
|
||||
temporality="Delta",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
samples_b = make_meter_samples(
|
||||
"meter.ns.b.cost",
|
||||
{"service": "billing-b"},
|
||||
now,
|
||||
count=5,
|
||||
base_value=20.0,
|
||||
temporality="Delta",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
insert_meter_samples(samples_a + samples_b)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Specific prefix: metricNamespace=meter.ns.a should return only billing-a
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=5,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params={
|
||||
"signal": "metrics",
|
||||
"source": "meter",
|
||||
"name": "service",
|
||||
"searchText": "",
|
||||
"metricNamespace": "meter.ns.a",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "billing-a" in values
|
||||
assert "billing-b" not in values
|
||||
|
||||
# Common prefix: metricNamespace=meter.ns should return both
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=5,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params={
|
||||
"signal": "metrics",
|
||||
"source": "meter",
|
||||
"name": "service",
|
||||
"searchText": "",
|
||||
"metricNamespace": "meter.ns",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "billing-a" in values
|
||||
assert "billing-b" in values
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metadata import get_field_values
|
||||
from fixtures.metrics import Metrics
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,expected,complete",
|
||||
[
|
||||
pytest.param({"name": "key"}, ["attr-plain"], True, id="unprefixed_name_matches_stored_name"),
|
||||
pytest.param({"name": "attribute.key"}, ["attr-prefixed", "attr-plain"], True, id="prefixed_name_matches_exact_then_stripped"),
|
||||
pytest.param({"name": "attribute.key", "limit": 1}, ["attr-prefixed"], False, id="exact_match_wins_the_limit"),
|
||||
pytest.param({"name": "attribute.attribute.key"}, ["attr-prefixed"], True, id="double_prefix_reaches_prefixed_stored_name"),
|
||||
pytest.param({"name": "log.severity_text"}, ["attr-severity", "WARN"], True, id="log_prefix_matches_attribute_then_log_field"),
|
||||
pytest.param({"name": "resource.service.name"}, ["svc-resource"], True, id="resource_prefix_is_always_context"),
|
||||
pytest.param({"name": "key", "fieldContext": "attribute"}, ["attr-plain"], True, id="explicit_context_single_lookup"),
|
||||
pytest.param({"name": "attribute.key", "fieldContext": "attribute"}, ["attr-prefixed"], True, id="explicit_context_name_not_parsed"),
|
||||
pytest.param({"name": "service.name:string"}, ["svc-resource"], True, id="type_suffix_without_context"),
|
||||
pytest.param({"name": "path", "searchText": "log.arch"}, ["log.archive"], True, id="search_text_not_parsed_as_key"),
|
||||
pytest.param({"name": "no_such_key"}, [], True, id="unknown_name_returns_no_values"),
|
||||
pytest.param({"name": "attribute.no_such_key"}, [], True, id="unknown_prefixed_name_returns_no_values"),
|
||||
],
|
||||
)
|
||||
def test_fields_values_name_resolution(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
params: dict,
|
||||
expected: list[str],
|
||||
complete: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert a log whose attributes include names that start with a context word: attribute.key next
|
||||
to key, log.severity_text next to the severity_text log field, and resource.service.name next to
|
||||
the service.name resource.
|
||||
|
||||
Tests:
|
||||
1. A name with a context prefix returns values of the name as written first, then of the name
|
||||
with the prefix stripped; resource. is always a context.
|
||||
2. An explicit fieldContext takes the name as is.
|
||||
3. A :type suffix is stripped, and searchText filters values without being read as a key.
|
||||
"""
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC),
|
||||
resources={"service.name": "svc-resource"},
|
||||
attributes={
|
||||
"key": "attr-plain",
|
||||
"attribute.key": "attr-prefixed",
|
||||
"log.severity_text": "attr-severity",
|
||||
"resource.service.name": "attr-resource",
|
||||
"path": "log.archive",
|
||||
},
|
||||
severity_text="WARN",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = get_field_values(signoz, token, {"signal": "logs", **params})
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["values"].get("stringValues", []) == expected
|
||||
assert response.json()["data"]["complete"] is complete
|
||||
|
||||
|
||||
def test_fields_values_metric_attribute_context(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert a metric with a service label, stored with attr_type point.
|
||||
|
||||
Tests:
|
||||
1. fieldContext=attribute returns the label's values.
|
||||
"""
|
||||
insert_metrics([Metrics(metric_name="fields.requests_total", labels={"service": "svc-metric"}, value=1.0)])
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = get_field_values(signoz, token, {"signal": "metrics", "name": "service", "fieldContext": "attribute"})
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["values"]["stringValues"] == ["svc-metric"]
|
||||
@@ -0,0 +1,126 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metadata import AttributesMetadata, get_field_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,expected,complete",
|
||||
[
|
||||
pytest.param({"name": "key"}, ["rel-plain"], True, id="unprefixed_name_matches_stored_name"),
|
||||
pytest.param({"name": "attribute.key"}, ["rel-prefixed", "rel-plain"], True, id="prefixed_name_matches_exact_then_stripped"),
|
||||
pytest.param({"name": "attribute.key", "limit": 1}, ["rel-prefixed"], False, id="exact_match_wins_the_limit"),
|
||||
pytest.param({"name": "attribute.key", "searchText": "plain"}, ["rel-plain"], True, id="search_text_filters_both_lookups"),
|
||||
pytest.param({"name": "attribute.attribute.key"}, ["rel-prefixed"], True, id="double_prefix_reaches_prefixed_stored_name"),
|
||||
pytest.param({"name": "resource.region"}, ["rel-region"], True, id="resource_prefix_is_always_context"),
|
||||
pytest.param({"name": "key", "fieldContext": "attribute"}, ["rel-plain"], True, id="explicit_context_single_lookup"),
|
||||
pytest.param({"name": "attribute.key", "fieldContext": "attribute"}, ["rel-prefixed"], True, id="explicit_context_name_not_parsed"),
|
||||
],
|
||||
)
|
||||
def test_related_values_name_resolution(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
insert_attributes_metadata: Callable[[list[AttributesMetadata]], None],
|
||||
params: dict,
|
||||
expected: list[str],
|
||||
complete: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert attributes_metadata rows for logs carrying key and attribute.key on service svc-rel, the
|
||||
same keys on svc-other, and an attribute named resource.region next to a region resource.
|
||||
A log registers service.name so existingQuery can resolve it.
|
||||
|
||||
Tests:
|
||||
1. Related values resolve the name like all values: name as written first, then the prefix
|
||||
stripped; resource. is always a context and an explicit fieldContext takes the name as is.
|
||||
2. Rows outside existingQuery never surface.
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now, resources={"service.name": "svc-registration"}, body="registers service.name")])
|
||||
insert_attributes_metadata(
|
||||
[
|
||||
AttributesMetadata(data_source="logs", resource_attributes={"service.name": "svc-rel"}, attributes={"key": "rel-plain"}, timestamp=now),
|
||||
AttributesMetadata(data_source="logs", resource_attributes={"service.name": "svc-rel"}, attributes={"attribute.key": "rel-prefixed"}, timestamp=now),
|
||||
AttributesMetadata(
|
||||
data_source="logs",
|
||||
resource_attributes={"service.name": "svc-rel", "region": "rel-region"},
|
||||
attributes={"resource.region": "rel-attr-region"},
|
||||
timestamp=now,
|
||||
),
|
||||
AttributesMetadata(
|
||||
data_source="logs",
|
||||
resource_attributes={"service.name": "svc-other"},
|
||||
attributes={"key": "other-plain", "attribute.key": "other-prefixed"},
|
||||
timestamp=now,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = get_field_values(signoz, token, {"signal": "logs", "existingQuery": "service.name = 'svc-rel'", **params})
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["values"].get("relatedValues") == expected
|
||||
assert response.json()["data"]["complete"] is complete
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,existing_query,expected",
|
||||
[
|
||||
pytest.param("no_such_key", "service.name = 'svc-a'", set(), id="unknown_name_returns_no_values"),
|
||||
pytest.param("key", "service.name = 'svc-a'", {"rel-a"}, id="known_filter_key_filters"),
|
||||
pytest.param("key", "no_such_filter = 'x'", {"rel-a", "rel-b"}, id="unknown_key_equal_drops_filter"),
|
||||
pytest.param("key", "service.name = 'svc-a' AND no_such_filter = 'x'", {"rel-a"}, id="unknown_key_equal_dropped_from_and"),
|
||||
pytest.param("key", "service.name = 'svc-a' OR no_such_filter = 'x'", {"rel-a", "rel-b"}, id="unknown_key_equal_drops_whole_or"),
|
||||
pytest.param("key", "no_such_filter != 'x'", {"rel-a", "rel-b"}, id="unknown_key_not_equal_drops_filter"),
|
||||
pytest.param("key", "service.name = 'svc-a' AND no_such_filter != 'x'", {"rel-a"}, id="unknown_key_not_equal_dropped_from_and"),
|
||||
pytest.param("key", "service.name = 'svc-a' OR no_such_filter != 'x'", {"rel-a", "rel-b"}, id="unknown_key_not_equal_drops_whole_or"),
|
||||
pytest.param("key", "NOT no_such_filter = 'x'", {"rel-a", "rel-b"}, id="not_over_unknown_key_drops_filter"),
|
||||
pytest.param("key", "service.name = 'svc-a' AND NOT no_such_filter = 'x'", {"rel-a"}, id="not_over_unknown_key_dropped_from_and"),
|
||||
pytest.param("key", "no_such_filter NOT EXISTS", {"rel-a", "rel-b"}, id="unknown_key_not_exists_drops_filter"),
|
||||
],
|
||||
)
|
||||
def test_related_values_unknown_keys(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
insert_attributes_metadata: Callable[[list[AttributesMetadata]], None],
|
||||
name: str,
|
||||
existing_query: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert attributes_metadata rows for logs with key on svc-a and on svc-b. A log registers
|
||||
service.name so existingQuery can resolve it.
|
||||
|
||||
Tests:
|
||||
1. An unknown name returns no values and no error.
|
||||
2. A term on an unknown key is dropped whatever its operator, and NOT does not flip it: it reads
|
||||
as true, so it vanishes from an AND and drops the whole OR.
|
||||
"""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_logs([Logs(timestamp=now, resources={"service.name": "svc-registration"}, body="registers service.name")])
|
||||
insert_attributes_metadata(
|
||||
[
|
||||
AttributesMetadata(data_source="logs", resource_attributes={"service.name": "svc-a"}, attributes={"key": "rel-a"}, timestamp=now),
|
||||
AttributesMetadata(data_source="logs", resource_attributes={"service.name": "svc-b"}, attributes={"key": "rel-b"}, timestamp=now),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = get_field_values(signoz, token, {"signal": "logs", "name": name, "existingQuery": existing_query})
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert set(response.json()["data"]["values"].get("relatedValues") or []) == expected
|
||||
assert response.json()["data"]["complete"] is True
|
||||
@@ -0,0 +1,68 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metadata import get_field_keys
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,present,absent",
|
||||
[
|
||||
pytest.param({"searchText": "key"}, {"key": "attribute", "attribute.key": "attribute"}, ["severity_text"], id="unprefixed_search_matches_names_containing_it"),
|
||||
pytest.param({"searchText": "attribute.key"}, {"key": "attribute", "attribute.key": "attribute"}, ["service.name"], id="attribute_prefix_sets_context"),
|
||||
pytest.param({"searchText": "attribute.attribute"}, {"attribute.key": "attribute"}, ["key"], id="double_prefix_searches_prefixed_stored_name"),
|
||||
pytest.param({"searchText": "attribute.key", "fieldContext": "attribute"}, {"attribute.key": "attribute"}, ["key"], id="explicit_context_search_not_parsed"),
|
||||
pytest.param({"searchText": "resource.service"}, {"service.name": "resource"}, ["resource.service.name"], id="resource_prefix_is_always_context"),
|
||||
pytest.param({"searchText": "log.severity"}, {"severity_text": "log"}, ["log.severity_text"], id="log_prefix_searches_log_context_only"),
|
||||
pytest.param({"searchText": "span.kind"}, {"span.kind": "attribute"}, ["kind"], id="context_invalid_for_signal_kept_in_search"),
|
||||
],
|
||||
)
|
||||
def test_fields_keys_name_resolution(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
params: dict,
|
||||
present: dict[str, str],
|
||||
absent: list[str],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert a log whose attributes include names that start with a context word: attribute.key next
|
||||
to key, log.severity_text next to the severity_text log field, and resource.service.name next to
|
||||
the service.name resource.
|
||||
|
||||
Tests:
|
||||
1. A context prefix in searchText that is valid for the signal sets the context and is searched
|
||||
without it; an explicit fieldContext searches the text as is.
|
||||
2. Keys search stays within that context, so prefixed stored names in another context are not listed.
|
||||
"""
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=datetime.now(tz=UTC),
|
||||
resources={"service.name": "svc-resource"},
|
||||
attributes={
|
||||
"key": "attr-plain",
|
||||
"attribute.key": "attr-prefixed",
|
||||
"log.severity_text": "attr-severity",
|
||||
"resource.service.name": "attr-resource",
|
||||
"span.kind": "attr-kind",
|
||||
},
|
||||
severity_text="WARN",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = get_field_keys(signoz, token, {"signal": "logs", **params})
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
keys = response.json()["data"]["keys"]
|
||||
assert {name: [key["fieldContext"] for key in keys.get(name, [])] for name in present} == {name: [context] for name, context in present.items()}
|
||||
assert [name for name in absent if name in keys] == []
|
||||
@@ -15,13 +15,13 @@ from fixtures.traces import Traces
|
||||
|
||||
# Related values (/api/v1/fields/values with existingQuery) are served from the
|
||||
# shared signoz_metadata.attributes_metadata table, scoped by data_source =
|
||||
# signal. One dataset (queriercommon/related_values.jsonl) drives every scenario:
|
||||
# signal. One dataset (quickfilter/related_values.jsonl) drives every scenario:
|
||||
# - Filter attributes (namespace/statefulset/cluster) are identical across
|
||||
# data_sources, so the same existingQuery matches under any signal.
|
||||
# - Target values (k8s.pod.name / host.name) are suffixed per data_source, so
|
||||
# the returned set differs by signal -> every scenario also proves the
|
||||
# data_source scoping (no cross-signal leakage).
|
||||
DATASET = "queriercommon/related_values.jsonl"
|
||||
DATASET = "quickfilter/related_values.jsonl"
|
||||
|
||||
|
||||
@pytest.fixture(name="seed_related_values", scope="function")
|
||||
87
tests/integration/tests/quickfilter/08_meter_values.py
Normal file
87
tests/integration/tests/quickfilter/08_meter_values.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.meter import MeterSample, make_meter_samples
|
||||
|
||||
|
||||
# Verify /api/v1/fields/values with source=meter filters label values by metricNamespace
|
||||
# prefix. Inserts meter-source metrics under ns.a and ns.b, then asserts a specific
|
||||
# prefix returns only matching values while a common prefix returns both.
|
||||
def test_metric_namespace_meter_values_filtering(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_meter_samples: Callable[[list[MeterSample]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
samples_a = make_meter_samples(
|
||||
"meter.ns.a.cost",
|
||||
{"service": "billing-a"},
|
||||
now,
|
||||
count=5,
|
||||
base_value=10.0,
|
||||
temporality="Delta",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
samples_b = make_meter_samples(
|
||||
"meter.ns.b.cost",
|
||||
{"service": "billing-b"},
|
||||
now,
|
||||
count=5,
|
||||
base_value=20.0,
|
||||
temporality="Delta",
|
||||
type_="Sum",
|
||||
is_monotonic=True,
|
||||
)
|
||||
insert_meter_samples(samples_a + samples_b)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Specific prefix: metricNamespace=meter.ns.a should return only billing-a
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=5,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params={
|
||||
"signal": "metrics",
|
||||
"source": "meter",
|
||||
"name": "service",
|
||||
"searchText": "",
|
||||
"metricNamespace": "meter.ns.a",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "billing-a" in values
|
||||
assert "billing-b" not in values
|
||||
|
||||
# Common prefix: metricNamespace=meter.ns should return both
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
|
||||
timeout=5,
|
||||
headers={"authorization": f"Bearer {token}"},
|
||||
params={
|
||||
"signal": "metrics",
|
||||
"source": "meter",
|
||||
"name": "service",
|
||||
"searchText": "",
|
||||
"metricNamespace": "meter.ns",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
values = response.json()["data"]["values"]["stringValues"]
|
||||
assert "billing-a" in values
|
||||
assert "billing-b" in values
|
||||
Reference in New Issue
Block a user