mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-11 19:00:40 +00:00
Compare commits
1 Commits
main
...
issue_nerv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1702b08cb2 |
@@ -1,9 +1,7 @@
|
||||
import type { ErrorEvent, StackFrame } from '@sentry/react';
|
||||
import { AxiosError, AxiosHeaders } from 'axios';
|
||||
|
||||
import {
|
||||
hasNoAppFrames,
|
||||
isExpectedRequestFailure,
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from '../sentryFilters';
|
||||
@@ -140,36 +138,3 @@ describe('SENTRY_IGNORE_ERRORS', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpectedRequestFailure', () => {
|
||||
function axiosErrorWithStatus(status: number): AxiosError {
|
||||
const config = { headers: new AxiosHeaders() };
|
||||
return new AxiosError(
|
||||
`Request failed with status code ${status}`,
|
||||
AxiosError.ERR_BAD_REQUEST,
|
||||
config,
|
||||
undefined,
|
||||
{ status, statusText: '', headers: {}, config, data: {} },
|
||||
);
|
||||
}
|
||||
|
||||
it.each([401, 403])('drops a %i response', (status) => {
|
||||
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(true);
|
||||
});
|
||||
|
||||
it('drops a network error', () => {
|
||||
expect(
|
||||
isExpectedRequestFailure(
|
||||
new AxiosError('Network Error', AxiosError.ERR_NETWORK),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([400, 404, 500, 503])('keeps a %i response', (status) => {
|
||||
expect(isExpectedRequestFailure(axiosErrorWithStatus(status))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps an error that is not from axios', () => {
|
||||
expect(isExpectedRequestFailure(new Error('Network Error'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,6 @@ import defaultRoutes, {
|
||||
} from './routes';
|
||||
import {
|
||||
hasNoAppFrames,
|
||||
isExpectedRequestFailure,
|
||||
isExternalIntegrationError,
|
||||
SENTRY_IGNORE_ERRORS,
|
||||
} from './sentryFilters';
|
||||
@@ -342,9 +341,6 @@ function App(): JSX.Element {
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
if (isExpectedRequestFailure(hint?.originalException)) {
|
||||
return null;
|
||||
}
|
||||
// Ignore benign Monaco cancellation errors (name 'Canceled').
|
||||
if (error?.name === 'Canceled') {
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ErrorEvent } from '@sentry/react';
|
||||
import { AxiosError, isAxiosError } from 'axios';
|
||||
|
||||
export const SENTRY_IGNORE_ERRORS = [
|
||||
// A tab that outlived a deploy requests hashed assets the new build no longer
|
||||
@@ -15,18 +14,6 @@ export const SENTRY_IGNORE_ERRORS = [
|
||||
/Called on script loaded before session recording is available/,
|
||||
];
|
||||
|
||||
// Not UI bugs: 401/403 come from the session or role, ERR_NETWORK from a
|
||||
// dropped connection.
|
||||
export function isExpectedRequestFailure(error: unknown): boolean {
|
||||
if (!isAxiosError(error)) {
|
||||
return false;
|
||||
}
|
||||
const status = error.response?.status;
|
||||
return (
|
||||
error.code === AxiosError.ERR_NETWORK || status === 401 || status === 403
|
||||
);
|
||||
}
|
||||
|
||||
const APP_ASSETS_PATH = '/assets/';
|
||||
|
||||
// Frames without a position are `[native code]`, which says nothing about the
|
||||
|
||||
@@ -33,7 +33,7 @@ jest.mock('../../TracesTable/TracesTable', () => ({
|
||||
<button
|
||||
data-testid="remove-total-tokens"
|
||||
type="button"
|
||||
onClick={(): void => onColumnRemove?.('trace:total_tokens:float64')}
|
||||
onClick={(): void => onColumnRemove?.('trace:total_tokens:number')}
|
||||
>
|
||||
remove
|
||||
</button>
|
||||
@@ -48,7 +48,7 @@ const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v5/query_range`;
|
||||
const TOTAL_TOKENS = {
|
||||
name: 'total_tokens',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
};
|
||||
|
||||
const storedColumnNames = (): string[] | undefined => {
|
||||
|
||||
@@ -63,7 +63,7 @@ const mockFieldKeys = (names: string[]): void => {
|
||||
{
|
||||
name,
|
||||
fieldContext: TelemetrytypesFieldContextDTO.trace,
|
||||
fieldDataType: TelemetrytypesFieldDataTypeDTO.float64,
|
||||
fieldDataType: TelemetrytypesFieldDataTypeDTO.number,
|
||||
},
|
||||
],
|
||||
]),
|
||||
|
||||
@@ -73,7 +73,7 @@ export const TRACE_VIEW_DEFAULT_COLUMNS: TelemetryFieldKey[] = [
|
||||
name: 'estimated_total_cost',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{ name: 'trace_duration_nano' },
|
||||
{ name: 'span_count' },
|
||||
@@ -81,37 +81,37 @@ export const TRACE_VIEW_DEFAULT_COLUMNS: TelemetryFieldKey[] = [
|
||||
name: 'total_tokens',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'input_tokens',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'output_tokens',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'distinct_tool_count',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'llm_call_count',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'tool_call_count',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{ name: 'start_time' },
|
||||
{ name: 'end_time' },
|
||||
@@ -122,7 +122,7 @@ export const TRACE_VIEW_DEFAULT_COLUMNS: TelemetryFieldKey[] = [
|
||||
name: 'max_llm_duration_nano',
|
||||
signal: 'traces',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -274,7 +274,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewRestructureJiraChannelSpecFactory(sqlstore),
|
||||
sqlmigration.NewMigrateRulesV1ToV2Alpha1Factory(sqlstore),
|
||||
sqlmigration.NewAddLLMPricingRuleTuplesFactory(sqlstore),
|
||||
sqlmigration.NewRemoveUnsupportedQuickFiltersFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ func (m *createQuickFilters) Up(ctx context.Context, db *bun.DB) error {
|
||||
{"key": "httpMethod", "dataType": "string", "type": "tag"},
|
||||
{"key": "httpRoute", "dataType": "string", "type": "tag"},
|
||||
{"key": "httpUrl", "dataType": "string", "type": "tag"},
|
||||
{"key": "traceID", "dataType": "string", "type": "tag"},
|
||||
}},
|
||||
{"logs", []map[string]any{
|
||||
{"key": "severity_text", "dataType": "string", "type": "resource"},
|
||||
|
||||
@@ -58,6 +58,7 @@ func (migration *updateQuickFilters) Up(ctx context.Context, db *bun.DB) error {
|
||||
{"key": "http.method", "dataType": "string", "type": "tag"},
|
||||
{"key": "http.route", "dataType": "string", "type": "tag"},
|
||||
{"key": "http_url", "dataType": "string", "type": "tag"},
|
||||
{"key": "trace_id", "dataType": "string", "type": "tag"},
|
||||
}},
|
||||
{"logs", []map[string]any{
|
||||
{"key": "severity_text", "dataType": "string", "type": "resource"},
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
var unsupportedQuickFilterNames = map[string]struct{}{
|
||||
// IDs and per-record values
|
||||
"trace_id": {},
|
||||
"traceID": {},
|
||||
"span_id": {},
|
||||
"spanID": {},
|
||||
"parent_span_id": {},
|
||||
"parentSpanID": {},
|
||||
"timestamp": {},
|
||||
"body": {},
|
||||
"gen_ai.tool.call.id": {},
|
||||
|
||||
// Free-text payloads
|
||||
"gen_ai.input.messages": {},
|
||||
"gen_ai.output.messages": {},
|
||||
"gen_ai.system_instructions": {},
|
||||
"gen_ai.prompt": {},
|
||||
"gen_ai.completion": {},
|
||||
"gen_ai.tool.call.arguments": {},
|
||||
"gen_ai.tool.call.result": {},
|
||||
"ai.prompt.messages": {},
|
||||
"ai.toolCall.result": {},
|
||||
"llm.input_messages": {},
|
||||
"input.value": {},
|
||||
"output.value": {},
|
||||
"langfuse.observation.input": {},
|
||||
"langfuse.observation.output": {},
|
||||
"traceloop.entity.input": {},
|
||||
"traceloop.entity.output": {},
|
||||
"final_result": {},
|
||||
"exception.message": {},
|
||||
"exception.stacktrace": {},
|
||||
"db.statement": {},
|
||||
"db.query.text": {},
|
||||
|
||||
// Token counts
|
||||
"gen_ai.usage.input_tokens": {},
|
||||
"gen_ai.usage.output_tokens": {},
|
||||
"gen_ai.usage.total_tokens": {},
|
||||
"gen_ai.usage.prompt_tokens": {},
|
||||
"gen_ai.usage.completion_tokens": {},
|
||||
"gen_ai.usage.cached_input_tokens": {},
|
||||
"gen_ai.usage.input_tokens.cached": {},
|
||||
"gen_ai.usage.cache_read.input_tokens": {},
|
||||
"gen_ai.usage.cache_read_input_tokens": {},
|
||||
"gen_ai.usage.cache_creation.input_tokens": {},
|
||||
"gen_ai.usage.cache_creation_input_tokens": {},
|
||||
"gen_ai.usage.cache_write.input_tokens": {},
|
||||
"gen_ai.usage.reasoning.output_tokens": {},
|
||||
"gen_ai.usage.reasoning_tokens": {},
|
||||
"gen_ai.usage.prompt_token_details.cached_read_tokens": {},
|
||||
"gen_ai.usage.completion_token_details.reasoning_tokens": {},
|
||||
"ai.usage.inputTokens": {},
|
||||
"ai.usage.outputTokens": {},
|
||||
"ai.usage.promptTokens": {},
|
||||
"ai.usage.completionTokens": {},
|
||||
"llm.token_count.prompt": {},
|
||||
"llm.token_count.completion": {},
|
||||
"llm.token_count.total": {},
|
||||
"llm.token_count.prompt_details.cache_read": {},
|
||||
"llm.token_count.prompt_details.cache_write": {},
|
||||
"llm.token_count.completion_details.reasoning": {},
|
||||
"llm.usage.prompt_tokens": {},
|
||||
"llm.usage.completion_tokens": {},
|
||||
"llm.usage.total_tokens": {},
|
||||
"input_tokens": {},
|
||||
"output_tokens": {},
|
||||
"cache_read_tokens": {},
|
||||
"cache_creation_tokens": {},
|
||||
|
||||
// Per-span costs
|
||||
"signoz.gen_ai.usage.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.output_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.cache_read.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.cache_write.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.tokens.cost": {},
|
||||
}
|
||||
|
||||
type unsupportedQuickFilterRow struct {
|
||||
bun.BaseModel `bun:"table:quick_filter"`
|
||||
|
||||
ID string `bun:"id,pk"`
|
||||
Filter string `bun:"filter"`
|
||||
}
|
||||
|
||||
type unsupportedQuickFilterEntry struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type removeUnsupportedQuickFilters struct {
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewRemoveUnsupportedQuickFiltersFactory() factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("drop_unsupported_quick_filters"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &removeUnsupportedQuickFilters{settings: ps}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *removeUnsupportedQuickFilters) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *removeUnsupportedQuickFilters) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var rows []*unsupportedQuickFilterRow
|
||||
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var migrated, skipped int
|
||||
for _, row := range rows {
|
||||
filtered, changed, ok := removeUnsupportedQuickFilterEntries(row.Filter)
|
||||
if !ok {
|
||||
migration.settings.Logger.WarnContext(ctx, "quick filter could not be parsed, leaving it untouched", slog.String("quick_filter_id", row.ID), slog.String("raw_filter", row.Filter))
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
migrated++
|
||||
if _, err := tx.NewUpdate().Model((*unsupportedQuickFilterRow)(nil)).Set("filter = ?", filtered).Where("id = ?", row.ID).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
migration.settings.Logger.InfoContext(ctx, "removed unsupported quick filters", slog.Int("total", len(rows)), slog.Int("migrated", migrated), slog.Int("skipped", skipped))
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *removeUnsupportedQuickFilters) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeUnsupportedQuickFilterEntries drops entries whose name is in
|
||||
// unsupportedQuickFilterNames and re-encodes the rest unchanged. changed=false
|
||||
// means nothing was dropped and the row must not be written; ok=false means
|
||||
// unparseable.
|
||||
func removeUnsupportedQuickFilterEntries(filter string) (filtered string, changed bool, ok bool) {
|
||||
var entries []json.RawMessage
|
||||
if err := json.Unmarshal([]byte(filter), &entries); err != nil {
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
kept := make([]json.RawMessage, 0, len(entries))
|
||||
for _, rawEntry := range entries {
|
||||
var entry unsupportedQuickFilterEntry
|
||||
if err := json.Unmarshal(rawEntry, &entry); err != nil {
|
||||
kept = append(kept, rawEntry)
|
||||
continue
|
||||
}
|
||||
if _, unsupported := unsupportedQuickFilterNames[entry.Name]; unsupported {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, rawEntry)
|
||||
}
|
||||
|
||||
if len(kept) == len(entries) {
|
||||
return "", false, true
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(kept); err != nil {
|
||||
return "", false, false
|
||||
}
|
||||
return strings.TrimSuffix(buf.String(), "\n"), true, true
|
||||
}
|
||||
@@ -1,53 +1,14 @@
|
||||
package telemetrymetadata
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
func staticFieldMatchesAny(field telemetrytypes.TelemetryFieldKey, selectors []*telemetrytypes.FieldKeySelector) bool {
|
||||
for _, selector := range selectors {
|
||||
if staticFieldMatches(field, selector) {
|
||||
if selector.MatchesKey(&field) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// staticFieldMatches mirrors the keys-table lookup for a static field: the
|
||||
// requested context and data type, when given, must agree with the field's,
|
||||
// and the name matches case-insensitively, as a substring for fuzzy selectors
|
||||
// and as the whole name for exact ones.
|
||||
func staticFieldMatches(field telemetrytypes.TelemetryFieldKey, selector *telemetrytypes.FieldKeySelector) bool {
|
||||
if selector.FieldContext != telemetrytypes.FieldContextUnspecified && selector.FieldContext != field.FieldContext {
|
||||
return false
|
||||
}
|
||||
if selector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified && !sameDataTypeFamily(selector.FieldDataType, field.FieldDataType) {
|
||||
return false
|
||||
}
|
||||
if selector.Name == "" {
|
||||
return true
|
||||
}
|
||||
if selector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
|
||||
return strings.EqualFold(field.Name, selector.Name)
|
||||
}
|
||||
return strings.Contains(strings.ToLower(field.Name), strings.ToLower(selector.Name))
|
||||
}
|
||||
|
||||
// sameDataTypeFamily treats the numeric types as one family: static fields
|
||||
// declare "number" while callers may ask for int64 or float64.
|
||||
func sameDataTypeFamily(requested, actual telemetrytypes.FieldDataType) bool {
|
||||
if requested == actual {
|
||||
return true
|
||||
}
|
||||
return isNumericDataType(requested) && isNumericDataType(actual)
|
||||
}
|
||||
|
||||
func isNumericDataType(dataType telemetrytypes.FieldDataType) bool {
|
||||
switch dataType {
|
||||
case telemetrytypes.FieldDataTypeNumber, telemetrytypes.FieldDataTypeInt64, telemetrytypes.FieldDataTypeFloat64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -54,6 +54,6 @@ func traceAggregate(name string) telemetrytypes.TelemetryFieldKey {
|
||||
Name: name,
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextTrace,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
||||
}
|
||||
}
|
||||
|
||||
40
pkg/telemetryschema/aitelemetryschema/field_keys_test.go
Normal file
40
pkg/telemetryschema/aitelemetryschema/field_keys_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package aitelemetryschema
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
func TestFieldKeysDataTypeFilter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fieldDataType telemetrytypes.FieldDataType
|
||||
expectTrace bool
|
||||
}{
|
||||
{name: "Number_IncludesTraceAggregates", fieldDataType: telemetrytypes.FieldDataTypeNumber, expectTrace: true},
|
||||
{name: "Float64_IncludesTraceAggregates", fieldDataType: telemetrytypes.FieldDataTypeFloat64, expectTrace: true},
|
||||
{name: "Int64_IncludesTraceAggregates", fieldDataType: telemetrytypes.FieldDataTypeInt64, expectTrace: true},
|
||||
{name: "String_ExcludesTraceAggregates", fieldDataType: telemetrytypes.FieldDataTypeString, expectTrace: false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
selector := &telemetrytypes.FieldKeySelector{Signal: telemetrytypes.SignalTraces, FieldDataType: testCase.fieldDataType}
|
||||
keys := FieldKeys(map[string][]*telemetrytypes.TelemetryFieldKey{}, selector)
|
||||
|
||||
for name := range TraceAggregateFields {
|
||||
if !testCase.expectTrace {
|
||||
assert.NotContains(t, keys, name)
|
||||
continue
|
||||
}
|
||||
require.Len(t, keys[name], 1)
|
||||
assert.Equal(t, telemetrytypes.FieldContextTrace, keys[name][0].FieldContext)
|
||||
assert.Equal(t, telemetrytypes.FieldDataTypeNumber, keys[name][0].FieldDataType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -40,86 +40,6 @@ var (
|
||||
SourceAiObservability = Source{valuer.NewString("ai_observability")}
|
||||
)
|
||||
|
||||
// unsupportedFilterNames are fields whose values are (near-)unique per span or
|
||||
// log, so a quick filter on them would list nothing useful to pick from.
|
||||
var unsupportedFilterNames = map[string]struct{}{
|
||||
// IDs and per-record values
|
||||
"trace_id": {},
|
||||
"traceID": {},
|
||||
"span_id": {},
|
||||
"spanID": {},
|
||||
"parent_span_id": {},
|
||||
"parentSpanID": {},
|
||||
"timestamp": {},
|
||||
"body": {},
|
||||
"gen_ai.tool.call.id": {},
|
||||
|
||||
// Free-text payloads
|
||||
"gen_ai.input.messages": {},
|
||||
"gen_ai.output.messages": {},
|
||||
"gen_ai.system_instructions": {},
|
||||
"gen_ai.prompt": {},
|
||||
"gen_ai.completion": {},
|
||||
"gen_ai.tool.call.arguments": {},
|
||||
"gen_ai.tool.call.result": {},
|
||||
"ai.prompt.messages": {},
|
||||
"ai.toolCall.result": {},
|
||||
"llm.input_messages": {},
|
||||
"input.value": {},
|
||||
"output.value": {},
|
||||
"langfuse.observation.input": {},
|
||||
"langfuse.observation.output": {},
|
||||
"traceloop.entity.input": {},
|
||||
"traceloop.entity.output": {},
|
||||
"final_result": {},
|
||||
"exception.message": {},
|
||||
"exception.stacktrace": {},
|
||||
"db.statement": {},
|
||||
"db.query.text": {},
|
||||
|
||||
// Token counts
|
||||
"gen_ai.usage.input_tokens": {},
|
||||
"gen_ai.usage.output_tokens": {},
|
||||
"gen_ai.usage.total_tokens": {},
|
||||
"gen_ai.usage.prompt_tokens": {},
|
||||
"gen_ai.usage.completion_tokens": {},
|
||||
"gen_ai.usage.cached_input_tokens": {},
|
||||
"gen_ai.usage.input_tokens.cached": {},
|
||||
"gen_ai.usage.cache_read.input_tokens": {},
|
||||
"gen_ai.usage.cache_read_input_tokens": {},
|
||||
"gen_ai.usage.cache_creation.input_tokens": {},
|
||||
"gen_ai.usage.cache_creation_input_tokens": {},
|
||||
"gen_ai.usage.cache_write.input_tokens": {},
|
||||
"gen_ai.usage.reasoning.output_tokens": {},
|
||||
"gen_ai.usage.reasoning_tokens": {},
|
||||
"gen_ai.usage.prompt_token_details.cached_read_tokens": {},
|
||||
"gen_ai.usage.completion_token_details.reasoning_tokens": {},
|
||||
"ai.usage.inputTokens": {},
|
||||
"ai.usage.outputTokens": {},
|
||||
"ai.usage.promptTokens": {},
|
||||
"ai.usage.completionTokens": {},
|
||||
"llm.token_count.prompt": {},
|
||||
"llm.token_count.completion": {},
|
||||
"llm.token_count.total": {},
|
||||
"llm.token_count.prompt_details.cache_read": {},
|
||||
"llm.token_count.prompt_details.cache_write": {},
|
||||
"llm.token_count.completion_details.reasoning": {},
|
||||
"llm.usage.prompt_tokens": {},
|
||||
"llm.usage.completion_tokens": {},
|
||||
"llm.usage.total_tokens": {},
|
||||
"input_tokens": {},
|
||||
"output_tokens": {},
|
||||
"cache_read_tokens": {},
|
||||
"cache_creation_tokens": {},
|
||||
|
||||
// Per-span costs
|
||||
"signoz.gen_ai.usage.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.output_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.cache_read.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.cache_write.input_tokens.cost": {},
|
||||
"signoz.gen_ai.usage.tokens.cost": {},
|
||||
}
|
||||
|
||||
func (Source) Enum() []any {
|
||||
return []any{
|
||||
SourceTraces,
|
||||
@@ -264,6 +184,7 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
|
||||
{Name: "http.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
|
||||
{Name: "http.route", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
|
||||
{Name: "http_url", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString},
|
||||
{Name: "trace_id", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString},
|
||||
}
|
||||
|
||||
logsFilters := []telemetrytypes.TelemetryFieldKey{
|
||||
@@ -342,9 +263,6 @@ func validateFilters(filters []telemetrytypes.TelemetryFieldKey) error {
|
||||
if filter.Name == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "filter name is required")
|
||||
}
|
||||
if _, unsupported := unsupportedFilterNames[filter.Name]; unsupported {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "%s is not supported as a quick filter as it has too many unique values, use the search bar to filter by it instead", filter.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package quickfiltertypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewStorableQuickFilter(t *testing.T) {
|
||||
serviceName := telemetrytypes.TelemetryFieldKey{Name: "service.name", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
filter telemetrytypes.TelemetryFieldKey
|
||||
rejectedKey string
|
||||
}{
|
||||
{name: "SupportedNumberField_Accepted", filter: telemetrytypes.TelemetryFieldKey{Name: "duration_nano", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeNumber}},
|
||||
{name: "TraceID_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "trace_id", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString}, rejectedKey: "trace_id"},
|
||||
{name: "LogSpanID_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "span_id", FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString}, rejectedKey: "span_id"},
|
||||
{name: "LegacyParentSpanID_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "parentSpanID", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString}, rejectedKey: "parentSpanID"},
|
||||
{name: "LogBody_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "body", FieldContext: telemetrytypes.FieldContextLog, FieldDataType: telemetrytypes.FieldDataTypeString}, rejectedKey: "body"},
|
||||
{name: "GenAIInputTokens_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "gen_ai.usage.input_tokens", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, rejectedKey: "gen_ai.usage.input_tokens"},
|
||||
{name: "OpenInferenceTokenCount_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "llm.token_count.prompt", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, rejectedKey: "llm.token_count.prompt"},
|
||||
{name: "GenAITotalCost_Rejected", filter: telemetrytypes.TelemetryFieldKey{Name: "signoz.gen_ai.usage.tokens.cost", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, rejectedKey: "signoz.gen_ai.usage.tokens.cost"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
_, err := NewStorableQuickFilter(valuer.GenerateUUID(), SourceTraces, []telemetrytypes.TelemetryFieldKey{serviceName, testCase.filter})
|
||||
if testCase.rejectedKey == "" {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput))
|
||||
assert.Contains(t, err.Error(), testCase.rejectedKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDefaultQuickFilterHasNoUnsupportedFields(t *testing.T) {
|
||||
defaults, err := NewDefaultQuickFilter(valuer.GenerateUUID())
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, storable := range defaults {
|
||||
sourceFilters, err := NewSourceFilterFromStorableQuickFilter(storable)
|
||||
require.NoError(t, err)
|
||||
for _, filter := range sourceFilters.Filters {
|
||||
_, unsupported := unsupportedFilterNames[filter.Name]
|
||||
assert.False(t, unsupported, "default %s quick filters contain %s", storable.Source.StringValue(), filter.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ func (s *FieldKeySelector) MatchesKey(key *TelemetryFieldKey) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.FieldDataType != FieldDataTypeUnspecified && s.FieldDataType != key.FieldDataType {
|
||||
if s.FieldDataType != FieldDataTypeUnspecified && !sameDataTypeFamily(s.FieldDataType, key.FieldDataType) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -198,3 +198,20 @@ func (FieldDataType) Enum() []any {
|
||||
// FieldDataTypeArrayNumber,
|
||||
}
|
||||
}
|
||||
|
||||
// sameDataTypeFamily treats the numeric types as one family: static fields
|
||||
// declare "number" while callers may ask for int64 or float64.
|
||||
func sameDataTypeFamily(requested, actual FieldDataType) bool {
|
||||
if requested == actual {
|
||||
return true
|
||||
}
|
||||
return isNumericDataType(requested) && isNumericDataType(actual)
|
||||
}
|
||||
|
||||
func isNumericDataType(dataType FieldDataType) bool {
|
||||
switch dataType {
|
||||
case FieldDataTypeNumber, FieldDataTypeInt64, FieldDataTypeFloat64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -59,6 +59,24 @@ def test_ai_fields_trace_prefix_search(
|
||||
assert trace_context_names == {"output_tokens"}, keys
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_data_type", ["number", "float64", "int64"])
|
||||
def test_ai_fields_numeric_data_type_lists_aggregates(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
field_data_type: str,
|
||||
) -> None:
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = get_field_keys(signoz, token, {"fieldDataType": field_data_type}, AI_KEYS_PATH)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
keys = response.json()["data"]["keys"]
|
||||
trace_keys = [key for variants in keys.values() for key in variants if key["fieldContext"] == "trace"]
|
||||
assert {key["name"] for key in trace_keys} == AI_TRACE_AGGREGATES, keys
|
||||
assert {key["fieldDataType"] for key in trace_keys} == {"number"}, keys
|
||||
|
||||
|
||||
def test_ai_fields_bare_prefix_suggests_aggregate_and_attribute(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
|
||||
@@ -266,18 +266,6 @@ def test_update_quick_filters_rejects_invalid_input(
|
||||
{"filters": [{"key": "service.name", "dataType": "string", "type": "resource"}]},
|
||||
),
|
||||
("invalid", {"filters": []}),
|
||||
(
|
||||
"traces",
|
||||
{"filters": [{"name": "trace_id", "fieldContext": "span", "fieldDataType": "string"}]},
|
||||
),
|
||||
(
|
||||
"logs",
|
||||
{"filters": [{"name": "span_id", "fieldContext": "log", "fieldDataType": "string"}]},
|
||||
),
|
||||
(
|
||||
"ai_observability",
|
||||
{"filters": [{"name": "gen_ai.usage.input_tokens", "fieldContext": "attribute", "fieldDataType": "number"}]},
|
||||
),
|
||||
]:
|
||||
response = requests.put(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v2/quick_filters/{source}"),
|
||||
@@ -304,7 +292,6 @@ def test_default_traces_filters_are_served_as_the_fields_api_serves_them(
|
||||
filters = {field_key["name"]: field_key for field_key in response.json()["data"]["filters"]}
|
||||
|
||||
assert "hasError" not in filters
|
||||
assert "trace_id" not in filters
|
||||
assert (filters["has_error"]["fieldContext"], filters["has_error"]["fieldDataType"]) == ("span", "bool")
|
||||
assert (filters["name"]["fieldContext"], filters["name"]["fieldDataType"]) == ("span", "string")
|
||||
assert (filters["duration_nano"]["fieldContext"], filters["duration_nano"]["fieldDataType"]) == ("span", "number")
|
||||
|
||||
Reference in New Issue
Block a user