Compare commits

..

1 Commits

Author SHA1 Message Date
Ashwin Bhatkal
d7ef3b0a67 fix(api): send array query params as repeated keys 2026-11-06 22:27:11 +05:30
19 changed files with 140 additions and 868 deletions

View File

@@ -15996,23 +15996,13 @@ 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. 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.'
- 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.
in: query
name: name
schema:
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.'
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.
type: string
- description: Filter expression already applied in the explorer. When set,
relatedValues lists the values seen on data matching it.
@@ -28622,23 +28612,13 @@ 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. 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.'
- 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.
in: query
name: name
schema:
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.'
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.
type: string
- description: Filter expression already applied in the explorer. When set,
relatedValues lists the values seen on data matching it.

View File

@@ -15352,7 +15352,7 @@ export type GetFieldsValuesParams = {
searchText?: string;
/**
* @type string
* @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.
* @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;
/**
@@ -16957,7 +16957,7 @@ export type GetRuleHistoryFilterValuesParams = {
searchText?: string;
/**
* @type string
* @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.
* @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;
/**

View File

@@ -11,6 +11,8 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
// generated API Instance
const generatedAPIAxiosInstance = axios.create({
baseURL: ENVIRONMENT.baseURL,
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
paramsSerializer: { indexes: null },
});
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;

View File

@@ -3,13 +3,11 @@ import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRulesV3 } from 'api/generated/services/rules';
import type {
ListRulesV3200,
ListRulesV3Params,
RenderErrorResponseDTO,
RuletypesLabelPairDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { AxiosError } from 'axios';
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
import { isUndefined } from 'lodash-es';
import type { AlertRule } from './types';
@@ -35,18 +33,7 @@ export function useAlertRulesData(
const hasLoggedEvent = useRef(false);
const rulesResponse = useListRulesV3(params, {
query: {
keepPreviousData: true,
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
queryFn: ({ signal }) =>
GeneratedAPIInstance<ListRulesV3200>({
url: '/api/v3/rules',
method: 'GET',
params,
paramsSerializer: { indexes: null },
signal,
}),
},
query: { keepPreviousData: true },
});
const rules = useMemo(

View File

@@ -1,91 +0,0 @@
package telemetrymetadata
import (
"context"
"testing"
cmock "github.com/SigNoz/clickhouse-go-mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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"
)
func TestGetAllValuesExactNameFirst(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
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())
}
func TestMergeFieldValues(t *testing.T) {
testCases := []struct {
name string
limit int
values []*telemetrytypes.TelemetryFieldValues
expected *telemetrytypes.TelemetryFieldValues
expectedComplete bool
}{
{
name: "BoolValuesOutsideLimit",
limit: 1,
values: []*telemetrytypes.TelemetryFieldValues{{BoolValues: []bool{true, false}}, {StringValues: []string{"a"}}},
expected: &telemetrytypes.TelemetryFieldValues{StringValues: []string{"a"}, BoolValues: []bool{true, false}},
expectedComplete: true,
},
{
name: "DuplicateAtLimitKeepsComplete",
limit: 1,
values: []*telemetrytypes.TelemetryFieldValues{{StringValues: []string{"prod"}}, {StringValues: []string{"prod"}}},
expected: &telemetrytypes.TelemetryFieldValues{StringValues: []string{"prod"}},
expectedComplete: true,
},
{
name: "DroppedValueMarksIncomplete",
limit: 1,
values: []*telemetrytypes.TelemetryFieldValues{{StringValues: []string{"prod"}}, {StringValues: []string{"dev"}}},
expected: &telemetrytypes.TelemetryFieldValues{StringValues: []string{"prod"}},
expectedComplete: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
merged, complete := mergeFieldValues(testCase.limit, testCase.values...)
assert.Equal(t, testCase.expected, merged)
assert.Equal(t, testCase.expectedComplete, complete)
})
}
}

View File

@@ -1400,9 +1400,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
selectColumn = fmt.Sprintf("if(notEmpty(%s), %s, %s)", memberColumns[i], memberColumns[i], selectColumn)
}
sb := sqlbuilder.Select("DISTINCT " + sqlbuilder.Escape(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(sqlbuilder.Escape(fmt.Sprintf("notEmpty(%s)", selectColumn)))
sb := sqlbuilder.Select("DISTINCT " + selectColumn).From(t.relatedMetadataDBName + "." + t.relatedMetadataTblName)
if len(fieldValueSelector.ExistingQuery) != 0 {
keySelectors := querybuilder.QueryStringToKeysSelectors(fieldValueSelector.ExistingQuery)
@@ -1519,24 +1517,7 @@ 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) {
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
return t.getRelatedValues(ctx, orgID, fieldValueSelector)
}
func (t *telemetryMetaStore) getSpanFieldValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) (*telemetrytypes.TelemetryFieldValues, bool, error) {
@@ -1655,7 +1636,7 @@ func (t *telemetryMetaStore) getTagTableValues(ctx context.Context, table string
switch fieldValueSelector.FieldDataType {
case telemetrytypes.FieldDataTypeString:
sb.Where(sb.ILike("string_value", "%"+clickhousesql.LikePattern(fieldValueSelector.Value)+"%"))
case telemetrytypes.FieldDataTypeNumber, telemetrytypes.FieldDataTypeInt64, telemetrytypes.FieldDataTypeFloat64:
case telemetrytypes.FieldDataTypeNumber:
sb.Where(sb.IsNotNull("number_value"))
sb.Where(sb.ILike("toString(number_value)", "%"+clickhousesql.LikePattern(fieldValueSelector.Value)+"%"))
case telemetrytypes.FieldDataTypeUnspecified:
@@ -1742,7 +1723,7 @@ func (t *telemetryMetaStore) getAuditFieldValues(ctx context.Context, fieldValue
switch fieldValueSelector.FieldDataType {
case telemetrytypes.FieldDataTypeString:
sb.Where(sb.ILike("string_value", "%"+clickhousesql.LikePattern(fieldValueSelector.Value)+"%"))
case telemetrytypes.FieldDataTypeNumber, telemetrytypes.FieldDataTypeInt64, telemetrytypes.FieldDataTypeFloat64:
case telemetrytypes.FieldDataTypeNumber:
sb.Where(sb.IsNotNull("number_value"))
sb.Where(sb.ILike("toString(number_value)", "%"+clickhousesql.LikePattern(fieldValueSelector.Value)+"%"))
case telemetrytypes.FieldDataTypeUnspecified:
@@ -1845,7 +1826,7 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, orgID val
}
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextUnspecified {
sb.Where(sb.E("attr_type", fieldValueSelector.FieldContext.MetricAttrType()))
sb.Where(sb.E("attr_type", fieldValueSelector.FieldContext.TagType()))
}
if fieldValueSelector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
@@ -2111,58 +2092,6 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
return values, complete, nil
}
// mergeFieldValues dedupes the values into one result and reports false when the
// limit dropped any. Bool values stay outside the limit, as in getTagTableValues.
func mergeFieldValues(limit int, values ...*telemetrytypes.TelemetryFieldValues) (*telemetrytypes.TelemetryFieldValues, bool) {
if limit == 0 {
limit = 50
}
merged := &telemetrytypes.TelemetryFieldValues{}
seen := make(map[any]bool)
seenRelated := make(map[any]bool)
seenBool := make(map[bool]bool)
count := 0
complete := true
admit := func(seenValues map[any]bool, value any) bool {
if seenValues[value] {
return false
}
if count >= limit {
complete = false
return false
}
seenValues[value] = true
count++
return true
}
for _, v := range values {
for _, value := range v.StringValues {
if admit(seen, value) {
merged.StringValues = append(merged.StringValues, value)
}
}
for _, value := range v.NumberValues {
if admit(seen, value) {
merged.NumberValues = append(merged.NumberValues, value)
}
}
for _, value := range v.RelatedValues {
if admit(seenRelated, value) {
merged.RelatedValues = append(merged.RelatedValues, value)
}
}
for _, value := range v.BoolValues {
if !seenBool[value] {
seenBool[value] = true
merged.BoolValues = append(merged.BoolValues, value)
}
}
}
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)
@@ -2220,27 +2149,6 @@ 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

View File

@@ -147,8 +147,36 @@ 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()
// 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 {
potentialDataType := f.Name[colonIdx+1:]
if dt, ok := fieldDataTypes[potentialDataType]; ok && dt != FieldDataTypeUnspecified {
f.FieldDataType = dt
f.Name = f.Name[:colonIdx]
}
}
}
// Step 2: Parse field context from the left if not already specified
if f.FieldContext == FieldContextUnspecified {
if dotIdx := strings.Index(f.Name, "."); dotIdx != -1 {
potentialContext := f.Name[:dotIdx]
if fc, ok := fieldContexts[potentialContext]; ok && fc != FieldContextUnspecified {
f.Name = f.Name[dotIdx+1:]
f.FieldContext = fc
// Step 2a: Handle special case for log.body.* fields
if f.FieldContext == FieldContextLog && strings.HasPrefix(f.Name, BodyJSONStringSearchPrefix) {
f.FieldContext = FieldContextBody
f.Name = strings.TrimPrefix(f.Name, BodyJSONStringSearchPrefix)
}
}
}
}
}
// GetFieldKeyFromKeyText returns a TelemetryFieldKey parsed from a key text of the
@@ -251,9 +279,6 @@ 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 {
@@ -281,7 +306,7 @@ type GettableFieldValues struct {
type PostableFieldValueParams struct {
PostableFieldKeysParams
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."`
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."`
ExistingQuery string `query:"existingQuery" description:"Filter expression already applied in the explorer. When set, relatedValues lists the values seen on data matching it."`
}
@@ -333,36 +358,26 @@ func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysPara
}
func NewFieldValueSelectorFromPostableFieldValueParams(params PostableFieldValueParams) *FieldValueSelector {
// SearchText filters values here, so it must not be parsed as a key.
keyParams := params.PostableFieldKeysParams
keyParams.SearchText = ""
keySelector := NewFieldKeySelectorFromPostableFieldKeysParams(keyParams)
keySelector := NewFieldKeySelectorFromPostableFieldKeysParams(params.PostableFieldKeysParams)
fieldValueSelector := &FieldValueSelector{
FieldKeySelector: keySelector,
}
// 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
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
}
} 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
@@ -375,17 +390,6 @@ 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 (selector *FieldValueSelector) NameMatchSelectors() []*FieldValueSelector {
if selector.nameAsWritten != "" {
return []*FieldValueSelector{selector.withName(selector.nameAsWritten), selector}
}
return []*FieldValueSelector{selector}
}
func NewTelemetryFieldKey(name string, fieldContext FieldContext, fieldDataType FieldDataType) *TelemetryFieldKey {
return &TelemetryFieldKey{
Name: name,
@@ -417,49 +421,3 @@ type TelemetryFieldKeySkipIndex struct {
IndexExpression string `json:"indexExpression"`
Granularity int `json:"granularity"`
}
func (key *TelemetryFieldKey) normalizeDataType() {
// Step 1: Parse data type from the right (after the last ":") if not already specified
if key.FieldDataType == FieldDataTypeUnspecified {
if colonIdx := strings.LastIndex(key.Name, ":"); colonIdx != -1 {
potentialDataType := key.Name[colonIdx+1:]
if dt, ok := fieldDataTypes[potentialDataType]; ok && dt != FieldDataTypeUnspecified {
key.FieldDataType = dt
key.Name = key.Name[:colonIdx]
}
}
}
}
func (key *TelemetryFieldKey) normalizeContext() {
// Step 2: Parse field context from the left if not already specified
if key.FieldContext == FieldContextUnspecified {
if dotIdx := strings.Index(key.Name, "."); dotIdx != -1 {
potentialContext := key.Name[:dotIdx]
if fc, ok := fieldContexts[potentialContext]; ok && fc != FieldContextUnspecified {
key.Name = key.Name[dotIdx+1:]
key.FieldContext = fc
// Step 2a: Handle special case for log.body.* fields
if key.FieldContext == FieldContextLog && strings.HasPrefix(key.Name, BodyJSONStringSearchPrefix) {
key.FieldContext = FieldContextBody
key.Name = strings.TrimPrefix(key.Name, BodyJSONStringSearchPrefix)
}
}
}
}
}
// withName returns a copy looking up name in any context.
func (selector *FieldValueSelector) withName(name string) *FieldValueSelector {
keySelector := *selector.FieldKeySelector
keySelector.Name = name
keySelector.FieldContext = FieldContextUnspecified
renamed := *selector
renamed.FieldKeySelector = &keySelector
renamed.nameAsWritten = ""
return &renamed
}

View File

@@ -164,15 +164,6 @@ 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 (fieldContext FieldContext) MetricAttrType() string {
if fieldContext == FieldContextAttribute {
return "point"
}
return fieldContext.TagType()
}
func isContextValidForSignal(ctx FieldContext, signal Signal) bool {
if ctx == FieldContextResource ||
ctx == FieldContextAttribute ||

View File

@@ -1,25 +0,0 @@
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())
})
}
}

View File

@@ -3,9 +3,6 @@ package telemetrytypes
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetFieldKeyFromKeyText(t *testing.T) {
@@ -425,168 +422,3 @@ 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")
})
}
}

View File

@@ -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 (quickfilter/related_values.jsonl) drives every scenario:
# signal. One dataset (queriercommon/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 = "quickfilter/related_values.jsonl"
DATASET = "queriercommon/related_values.jsonl"
@pytest.fixture(name="seed_related_values", scope="function")

View File

@@ -106,3 +106,81 @@ 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

View File

@@ -1,97 +0,0 @@
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:
"""A prefixed name returns values of the name as written first, then with the prefix stripped. `resource.` is always a context."""
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
@pytest.mark.parametrize("name", ["latency", "latency:number", "latency:int64", "latency:float64"])
def test_fields_values_number_search_text(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
name: str,
) -> None:
now = datetime.now(tz=UTC)
insert_logs([Logs(timestamp=now, attributes={"latency": 42}), Logs(timestamp=now, attributes={"latency": 7})])
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
response = get_field_values(signoz, token, {"signal": "logs", "name": name, "searchText": "4"})
assert response.status_code == HTTPStatus.OK
assert response.json()["data"]["values"].get("numberValues", []) == [42]
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:
"""Metric attributes are stored with attr_type point."""
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"]

View File

@@ -1,106 +0,0 @@
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:
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:
"""A term on an unknown key reads as true whatever its operator: 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

View File

@@ -1,58 +0,0 @@
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:
"""Unlike values, keys search only the prefix's context."""
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] == []

View File

@@ -1,87 +0,0 @@
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