Compare commits

..

4 Commits

Author SHA1 Message Date
Nikhil Soni
c8fa1507ed chore: use typed argument in logs 2026-06-03 10:28:18 +05:30
Nikhil Soni
4b311c90c4 chore: try removing high cardinality attributes 2026-06-03 10:27:52 +05:30
Nikhil Soni
8f200f2c1d chore: use traces instead of tracedetail in metric name 2026-06-03 09:24:49 +05:30
Nikhil Soni
17f6028647 chore: add metric for waterfall monitoring
Gauge for config limit and counter to count large traces
2026-06-03 09:24:49 +05:30
31 changed files with 209 additions and 807 deletions

View File

@@ -52,7 +52,6 @@ jobs:
- rootuser
- serviceaccount
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
sqlstore-provider:
- postgres

View File

@@ -359,7 +359,8 @@ function CustomTimePickerPopoverContent({
<Clock
color={Color.BG_ROBIN_400}
className="timezone-container__clock-icon"
size={14}
height={12}
width={12}
/>
<span className="timezone__name">{timezone.name}</span>

View File

@@ -109,16 +109,6 @@ $custom-border-color: #2c3044;
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
}
.ant-select-clear {
background-color: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
&:hover {
color: var(--l1-foreground);
}
}
// Customize tags in multiselect (dark mode by default)
.ant-select-selection-item {
background-color: var(--l1-border);
@@ -402,9 +392,7 @@ $custom-border-color: #2c3044;
// Custom dropdown styles for multi-select
.custom-multiselect-dropdown {
padding: 8px 0 0 0;
// Tall enough to hold the react-virtuoso list (<=300px) + header/footer
// so only the list scrolls (avoids a second scrollbar on this container).
max-height: 410px;
max-height: 350px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
@@ -495,12 +483,8 @@ $custom-border-color: #2c3044;
.option-checkbox {
width: 100%;
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
// shrink) so the option text below can truncate instead of overflowing.
> label {
flex: 1 1 auto;
min-width: 0;
> span:not(.ant-checkbox) {
width: 100%;
}
.all-option-text {
@@ -517,12 +501,7 @@ $custom-border-color: #2c3044;
width: 100%;
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option-badge {
@@ -535,30 +514,26 @@ $custom-border-color: #2c3044;
}
}
// Size the buttons to the row's resting content height (20px) and fully
// override antd's default 32px Button box, so revealing them on hover
// never changes the row height.
.only-btn,
.only-btn {
display: none;
}
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
padding: 0 6px;
font-size: 12px;
line-height: 1;
border: none;
box-shadow: none;
}
&:hover {
background-color: unset;
}
.only-btn:hover {
background-color: unset;
}
.toggle-btn:hover {
background-color: unset;
}
.option-content:hover {
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.toggle-btn {
display: none;
@@ -573,6 +548,9 @@ $custom-border-color: #2c3044;
.option-checkbox:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
.option-badge {
display: none;

View File

@@ -67,8 +67,8 @@
gap: 4px;
&--success {
background: color-mix(in srgb, var(--text-forest-500) 10%, transparent);
color: var(--text-forest-400);
background: color-mix(in srgb, var(--success-background) 10%, transparent);
color: var(--success-foreground);
}
&--error {

View File

@@ -153,7 +153,6 @@
font-size: 10px;
color: var(--l2-foreground);
margin-top: 4px;
display: block;
}
}

View File

@@ -47,10 +47,18 @@
}
.ant-tabs-tab-active {
.overview-btn,
.variables-btn,
.overview-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.variables-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.public-dashboard-btn {
color: var(--primary-background);
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
}

View File

@@ -127,15 +127,6 @@
align-items: center;
justify-content: center;
gap: 4px;
.sidenav-beta-tag {
margin-left: 4px;
}
div {
display: flex;
align-items: center;
}
}
.variable-type-btn + .variable-type-btn {
@@ -186,7 +177,6 @@
.multiple-values-section {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
.typography-variables {
@@ -203,7 +193,6 @@
.all-option-section {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
.typography-variables {

View File

@@ -518,6 +518,7 @@ function VariableItem({
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
@@ -613,6 +614,7 @@ function VariableItem({
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}

View File

@@ -19,7 +19,6 @@
}
.ant-btn-default {
border-color: transparent;
box-shadow: none;
}
}
.ant-tabs-tab-active {

View File

@@ -72,20 +72,8 @@
.alert-rule-scope {
margin-bottom: 12px;
// `.createForm label` styles field labels (font-weight 500, 14px,
// 6px bottom padding). Those bleed into the @signozhq/ui RadioGroup
// option labels, making them bold and vertically misaligned with the
// radio control. Reset them back to plain option-text styling.
label {
padding: 0;
font-weight: 400;
line-height: normal;
}
// Loosen the design-system default (grid gap 0.5rem) between options.
.silence-alerts-radio-group {
margin-top: 8px;
gap: 12px;
.ant-radio-wrapper {
color: var(--l1-foreground);
}
}
@@ -156,7 +144,10 @@
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
.ant-btn {
margin-top: 8px;
}
}
.schedule-created-at {

View File

@@ -54,7 +54,8 @@ import {
} from './PlannedDowntimeutils';
import './PlannedDowntime.styles.scss';
import { RadioGroupItem, RadioGroup } from '@signozhq/ui/radio-group';
import { RadioGroupItem } from '@signozhq/ui/radio-group';
import { RadioGroup } from '@signozhq/ui/radio-group';
dayjs.locale('en');
dayjs.extend(utc);
@@ -470,7 +471,7 @@ export function PlannedDowntimeForm(
initialValue="specific"
className="alert-rule-scope"
>
<RadioGroup className="silence-alerts-radio-group">
<RadioGroup>
<RadioGroupItem value="all">All alert rules</RadioGroupItem>
<RadioGroupItem value="specific">Specific alert rules</RadioGroupItem>
</RadioGroup>

View File

@@ -35,7 +35,10 @@
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
.ant-btn {
margin-top: 8px;
}
}
.routing-policies-table {

View File

@@ -8,7 +8,7 @@
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
) !important;
);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 0;
@@ -34,9 +34,9 @@
}
.refresh-interval-text {
padding: 12px 14px 8px 14px !important;
padding: 12px 14px 8px 14px;
color: var(--muted-foreground);
font-size: 13px;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */

View File

@@ -9,7 +9,7 @@
}
.ant-tabs-nav {
padding-left: 16px;
padding: 0 8px;
margin-bottom: 0px;
&::before {

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
"github.com/SigNoz/signoz/pkg/types/spantypes"
@@ -13,15 +14,27 @@ type module struct {
store spantypes.TraceStore
settings factory.ScopedProviderSettings
config tracedetail.Config
metrics *moduleMetrics
}
func NewModule(traceStore spantypes.TraceStore, providerSettings factory.ProviderSettings, cfg tracedetail.Config) *module {
scopedProviderSettings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail")
return &module{
metrics, err := newModuleMetrics(scopedProviderSettings.Meter())
if err != nil {
scopedProviderSettings.Logger().WarnContext(context.Background(), "tracedetail: failed to initialize metrics", errors.Attr(err))
}
m := &module{
config: cfg,
store: traceStore,
settings: scopedProviderSettings,
metrics: metrics,
}
m.metrics.waterfallMaxLimitToSelectAllSpans.Record(context.Background(), int64(cfg.Waterfall.MaxLimitToSelectAllSpans))
return m
}
func (m *module) GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error) {
@@ -80,6 +93,7 @@ func (m *module) GetWaterfallV4(ctx context.Context, traceID string, selectedSpa
}
effectiveLimit := min(selectAllLimit, m.config.Waterfall.MaxLimitToSelectAllSpans)
if summary.NumSpans > uint64(effectiveLimit) {
m.metrics.waterfallWindowedResponseCount.Add(ctx, 1)
return m.getWindowedWaterfall(ctx, traceID, selectedSpanID, uncollapsedSpans, summary.Start, summary.End)
}
return m.getFullWaterfall(ctx, traceID, summary)

View File

@@ -0,0 +1,37 @@
package impltracedetail
import (
"github.com/SigNoz/signoz/pkg/errors"
"go.opentelemetry.io/otel/metric"
)
type moduleMetrics struct {
waterfallMaxLimitToSelectAllSpans metric.Int64Gauge
waterfallWindowedResponseCount metric.Int64Counter
}
func newModuleMetrics(meter metric.Meter) (*moduleMetrics, error) {
var errs error
maxLimitGauge, err := meter.Int64Gauge(
"signoz.traces.waterfall.max_limit_to_select_all_spans",
metric.WithDescription("The span count limit above which windowed waterfall is returned instead of the full waterfall."),
metric.WithUnit("{spans}"),
)
if err != nil {
errs = errors.Join(errs, err)
}
windowedCounter, err := meter.Int64Counter(
"signoz.traces.waterfall.windowed_responses",
metric.WithDescription("Total number of waterfall requests that used the windowed path."),
)
if err != nil {
errs = errors.Join(errs, err)
}
return &moduleMetrics{
waterfallMaxLimitToSelectAllSpans: maxLimitGauge,
waterfallWindowedResponseCount: windowedCounter,
}, errs
}

View File

@@ -7,12 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
)
type SkipResourceFingerprint struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
// If count of fingerprint is above threshold, skip the fingerprint subquery and filter on main table instead.
Threshold uint64 `yaml:"threshold" mapstructure:"threshold"`
}
// Config represents the configuration for the querier.
type Config struct {
// CacheTTL is the TTL for cached query results
@@ -21,8 +15,6 @@ type Config struct {
FluxInterval time.Duration `yaml:"flux_interval" mapstructure:"flux_interval"`
// MaxConcurrentQueries is the maximum number of concurrent queries for missing ranges
MaxConcurrentQueries int `yaml:"max_concurrent_queries" mapstructure:"max_concurrent_queries"`
// SkipResourceFingerprint configures when the resource fingerprint subquery is skipped in favor of main-table filtering.
SkipResourceFingerprint SkipResourceFingerprint `yaml:"skip_resource_fingerprint" mapstructure:"skip_resource_fingerprint"`
}
// NewConfigFactory creates a new config factory for querier.
@@ -36,10 +28,6 @@ func newConfig() factory.Config {
CacheTTL: 168 * time.Hour,
FluxInterval: 5 * time.Minute,
MaxConcurrentQueries: 4,
SkipResourceFingerprint: SkipResourceFingerprint{
Enabled: false,
Threshold: 100000,
},
}
}
@@ -54,9 +42,6 @@ func (c Config) Validate() error {
if c.MaxConcurrentQueries <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "max_concurrent_queries must be positive, got %v", c.MaxConcurrentQueries)
}
if c.SkipResourceFingerprint.Enabled && c.SkipResourceFingerprint.Threshold == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "skip_resource_fingerprint.threshold must be > 0 when enabled")
}
return nil
}

View File

@@ -88,8 +88,6 @@ func newProvider(
traceAggExprRewriter,
telemetryStore,
flagger,
cfg.SkipResourceFingerprint.Enabled,
cfg.SkipResourceFingerprint.Threshold,
)
// Create trace operator statement builder
@@ -123,9 +121,6 @@ func newProvider(
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
flagger,
telemetryStore,
cfg.SkipResourceFingerprint.Enabled,
cfg.SkipResourceFingerprint.Threshold,
)
// Create audit statement builder

View File

@@ -89,9 +89,6 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
fl,
nil,
false,
100000,
)
return querier.New(
@@ -137,8 +134,6 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
traceAggExprRewriter,
telemetryStore,
fl,
false,
100000,
)
return querier.New(

View File

@@ -1205,9 +1205,6 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStat
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
return statementBuilder, mockMetadataStore

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetryresourcefilter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -20,14 +19,13 @@ import (
)
type logQueryStatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterResolver *telemetryresourcefilter.ResourceFingerprintResolver[qbtypes.LogAggregation]
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
skipResourceFingerprintEnabled bool
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
@@ -44,13 +42,10 @@ func NewLogQueryStatementBuilder(
fullTextColumn *telemetrytypes.TelemetryFieldKey,
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
fl flagger.Flagger,
telemetryStore telemetrystore.TelemetryStore,
skipResourceFingerprintEnable bool,
skipResourceFingerprintThreshold uint64,
) *logQueryStatementBuilder {
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
resourceFilterResolver := telemetryresourcefilter.NewResolver[qbtypes.LogAggregation](
resourceFilterStmtBuilder := telemetryresourcefilter.New[qbtypes.LogAggregation](
settings,
DBName,
LogsResourceV2TableName,
@@ -60,21 +55,18 @@ func NewLogQueryStatementBuilder(
fullTextColumn,
jsonKeyToKey,
fl,
telemetryStore,
skipResourceFingerprintThreshold,
)
return &logQueryStatementBuilder{
logger: logsSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterResolver: resourceFilterResolver,
aggExprRewriter: aggExprRewriter,
fl: fl,
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
logger: logsSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
aggExprRewriter: aggExprRewriter,
fl: fl,
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
}
}
@@ -279,11 +271,9 @@ func (b *logQueryStatementBuilder) buildListQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -325,7 +315,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -383,11 +373,9 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -431,7 +419,7 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
// Add FROM clause
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -543,11 +531,9 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" && !skipResourceCTE {
} else if frag != "" && !skipResourceCTE {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -590,7 +576,7 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -654,7 +640,6 @@ func (b *logQueryStatementBuilder) addFilterCondition(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
skipResourceFilter bool,
) (querybuilder.PreparedWhereClause, error) {
var preparedWhereClause querybuilder.PreparedWhereClause
@@ -671,7 +656,7 @@ func (b *logQueryStatementBuilder) addFilterCondition(
ConditionBuilder: b.cb,
FieldKeys: keys,
BodyJSONEnabled: bodyJSONEnabled,
SkipResourceFilter: skipResourceFilter,
SkipResourceFilter: true,
FullTextColumn: b.fullTextColumn,
JsonKeyToKey: b.jsonKeyToKey,
Variables: variables,
@@ -722,30 +707,33 @@ func (b *logQueryStatementBuilder) maybeAttachResourceFilter(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (cteSQL string, cteArgs []any, skipResourceFilter bool, err error) {
) (cteSQL string, cteArgs []any, err error) {
if b.skipResourceFingerprintEnabled {
decision, err := b.resourceFilterResolver.Resolve(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
}
switch decision {
case qbtypes.ResourceFilterResolveKindNoOp:
return "", nil, true, nil
case qbtypes.ResourceFilterResolveKindFallback:
return "", nil, false, nil
}
}
stmt, err := b.resourceFilterResolver.StatementBuilder().Build(
ctx, start, end, qbtypes.RequestTypeRaw, query, variables,
)
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
return "", nil, err
}
if stmt == nil {
return "", nil, true, nil
return "", nil, nil
}
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
}
func (b *logQueryStatementBuilder) buildResourceFilterCTE(
ctx context.Context,
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
return b.resourceFilterStmtBuilder.Build(
ctx,
start,
end,
qbtypes.RequestTypeRaw,
query,
variables,
)
}

View File

@@ -2,36 +2,19 @@ package telemetrylogs
import (
"context"
"regexp"
"testing"
"time"
cmock "github.com/SigNoz/clickhouse-go-mock"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/stretchr/testify/require"
)
type regexQueryMatcher struct{}
func (m *regexQueryMatcher) Match(expectedSQL, actualSQL string) error {
re, err := regexp.Compile(expectedSQL)
if err != nil {
return err
}
if !re.MatchString(actualSQL) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expected query to match %s, got %s", expectedSQL, actualSQL)
}
return nil
}
func TestStatementBuilderTimeSeries(t *testing.T) {
// Create a test release time
releaseTime := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
@@ -229,9 +212,6 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -373,9 +353,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -522,9 +499,6 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -601,9 +575,6 @@ func TestStatementBuilderTimeSeriesBodyGroupBy(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -699,9 +670,6 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -926,9 +894,6 @@ func TestAdjustKey(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
for _, c := range cases {
@@ -1074,9 +1039,6 @@ func TestStmtBuilderBodyField(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
@@ -1176,9 +1138,6 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
@@ -1198,110 +1157,3 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
})
}
}
// TestSkipResourceFingerprintLogs exercises the three resolver outcomes for
// logs: use-CTE (count < threshold), fallback (count >= threshold), and the
// legacy path (feature disabled).
func TestSkipResourceFingerprintLogs(t *testing.T) {
const (
startMs = uint64(1747947419000)
endMs = uint64(1747983448000)
threshold = uint64(10)
)
query := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{
Expression: "service.name = 'redis-manual'",
},
Limit: 5,
}
t.Run("disabled uses the legacy CTE", func(t *testing.T) {
sb := newSkipResourceFingerprintLogsBuilder(t, nil, false, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
})
t.Run("CTE attached when count below threshold", func(t *testing.T) {
mockStore := telemetrystoretest.New(telemetrystore.Config{}, &regexQueryMatcher{})
mock := mockStore.Mock()
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
{Name: "count", Type: "UInt64"},
}, []any{uint64(2)}))
sb := newSkipResourceFingerprintLogsBuilder(t, mockStore, true, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
require.NoError(t, mock.ExpectationsWereMet())
})
t.Run("fallback when count at or above threshold", func(t *testing.T) {
mockStore := telemetrystoretest.New(telemetrystore.Config{}, &regexQueryMatcher{})
mock := mockStore.Mock()
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_logs\.distributed_logs_v2_resource`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
{Name: "count", Type: "UInt64"},
}, []any{threshold}))
sb := newSkipResourceFingerprintLogsBuilder(t, mockStore, true, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.NotContains(t, stmt.Query, "__resource_filter AS")
require.NotContains(t, stmt.Query, "resource_fingerprint")
require.Contains(t, stmt.Query, "service.name")
require.NoError(t, mock.ExpectationsWereMet())
})
}
func newSkipResourceFingerprintLogsBuilder(
t *testing.T,
telemetryStore telemetrystore.TelemetryStore,
skipEnable bool,
threshold uint64,
) *logQueryStatementBuilder {
t.Helper()
fl := flaggertest.New(t)
fm := NewFieldMapper(fl)
cb := NewConditionBuilder(fm, fl)
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC))
aggExprRewriter := querybuilder.NewAggExprRewriter(
instrumentationtest.New().ToProviderSettings(),
DefaultFullTextColumn,
fm,
cb,
GetBodyJSONKey,
fl,
)
return NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
telemetryStore,
skipEnable,
threshold,
)
}

View File

@@ -1,77 +0,0 @@
package telemetryresourcefilter
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/telemetrystore"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type ResourceFingerprintResolver[T any] struct {
stmtBuilder *resourceFilterStatementBuilder[T]
telemetryStore telemetrystore.TelemetryStore
threshold uint64
}
func NewResolver[T any](
settings factory.ProviderSettings,
dbName string,
tableName string,
signal telemetrytypes.Signal,
source telemetrytypes.Source,
metadataStore telemetrytypes.MetadataStore,
fullTextColumn *telemetrytypes.TelemetryFieldKey,
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
fl flagger.Flagger,
telemetryStore telemetrystore.TelemetryStore,
threshold uint64,
) *ResourceFingerprintResolver[T] {
return &ResourceFingerprintResolver[T]{
stmtBuilder: New[T](
settings,
dbName,
tableName,
signal,
source,
metadataStore,
fullTextColumn,
jsonKeyToKey,
fl,
),
telemetryStore: telemetryStore,
threshold: threshold,
}
}
func (r *ResourceFingerprintResolver[T]) StatementBuilder() qbtypes.StatementBuilder[T] {
return r.stmtBuilder
}
func (r *ResourceFingerprintResolver[T]) Resolve(
ctx context.Context,
query qbtypes.QueryBuilderQuery[T],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (qbtypes.ResourceFilterResolveKind, error) {
countStmt, err := r.stmtBuilder.BuildCount(ctx, start, end, query, variables)
if err != nil {
return qbtypes.ResourceFilterResolveKindNoOp, err
}
if countStmt == nil {
return qbtypes.ResourceFilterResolveKindNoOp, nil
}
var count uint64
row := r.telemetryStore.ClickhouseDB().QueryRow(ctx, countStmt.Query, countStmt.Args...)
if err := row.Scan(&count); err != nil {
return qbtypes.ResourceFilterResolveKindNoOp, err
}
if count >= r.threshold {
return qbtypes.ResourceFilterResolveKindFallback, nil
}
return qbtypes.ResourceFilterResolveKindUseCTE, nil
}

View File

@@ -127,25 +127,6 @@ func (b *resourceFilterStatementBuilder[T]) Build(
}, nil
}
// BuildCount returns a statement that counts the distinct fingerprints matching
// the resource filter. Returns (nil, nil) when the filter is a no-op.
func (b *resourceFilterStatementBuilder[T]) BuildCount(
ctx context.Context,
start uint64,
end uint64,
query qbtypes.QueryBuilderQuery[T],
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
inner, err := b.Build(ctx, start, end, qbtypes.RequestTypeRaw, query, variables)
if err != nil || inner == nil {
return nil, err
}
return &qbtypes.Statement{
Query: fmt.Sprintf("SELECT count() FROM (%s)", inner.Query),
Args: inner.Args,
}, nil
}
// addConditions adds both filter and time conditions to the query.
// Returns true (isNoOp) when the filter expression evaluated to no resource conditions,
// meaning the CTE would select all fingerprints and should be skipped entirely.

View File

@@ -24,13 +24,13 @@ var (
)
type traceQueryStatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterResolver *telemetryresourcefilter.ResourceFingerprintResolver[qbtypes.TraceAggregation]
aggExprRewriter qbtypes.AggExprRewriter
skipResourceFingerprintEnabled bool
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
aggExprRewriter qbtypes.AggExprRewriter
telemetryStore telemetrystore.TelemetryStore
}
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*traceQueryStatementBuilder)(nil)
@@ -43,12 +43,10 @@ func NewTraceQueryStatementBuilder(
aggExprRewriter qbtypes.AggExprRewriter,
telemetryStore telemetrystore.TelemetryStore,
flagger flagger.Flagger,
skipResourceFingerprintEnable bool,
skipResourceFingerprintThreshold uint64,
) *traceQueryStatementBuilder {
tracesSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrytraces")
resourceFilterResolver := telemetryresourcefilter.NewResolver[qbtypes.TraceAggregation](
resourceFilterStmtBuilder := telemetryresourcefilter.New[qbtypes.TraceAggregation](
settings,
DBName,
TracesResourceV3TableName,
@@ -58,18 +56,16 @@ func NewTraceQueryStatementBuilder(
nil,
nil,
flagger,
telemetryStore,
skipResourceFingerprintThreshold,
)
return &traceQueryStatementBuilder{
logger: tracesSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterResolver: resourceFilterResolver,
aggExprRewriter: aggExprRewriter,
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
logger: tracesSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
aggExprRewriter: aggExprRewriter,
telemetryStore: telemetryStore,
}
}
@@ -312,11 +308,9 @@ func (b *traceQueryStatementBuilder) buildListQuery(
cteArgs [][]any
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -334,7 +328,7 @@ func (b *traceQueryStatementBuilder) buildListQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
}
@@ -395,17 +389,15 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
cteArgs [][]any
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, distSB, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, distSB, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, distSB, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, distSB, start, end, query, keys, variables)
if err != nil {
return nil, err
}
@@ -506,11 +498,9 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
cteArgs [][]any
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -551,7 +541,7 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
}
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
}
@@ -660,11 +650,9 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
cteArgs [][]any
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" && !skipResourceCTE {
} else if frag != "" && !skipResourceCTE {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -706,7 +694,7 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
}
@@ -769,7 +757,6 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
skipResourceFilter bool,
) (querybuilder.PreparedWhereClause, error) {
var preparedWhereClause querybuilder.PreparedWhereClause
@@ -783,7 +770,7 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
FieldMapper: b.fm,
ConditionBuilder: b.cb,
FieldKeys: keys,
SkipResourceFilter: skipResourceFilter,
SkipResourceFilter: true,
Variables: variables,
StartNs: start,
EndNs: end,
@@ -824,30 +811,34 @@ func (b *traceQueryStatementBuilder) maybeAttachResourceFilter(
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (cteSQL string, cteArgs []any, skipResourceFilter bool, err error) {
) (cteSQL string, cteArgs []any, err error) {
if b.skipResourceFingerprintEnabled {
decision, err := b.resourceFilterResolver.Resolve(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
}
switch decision {
case qbtypes.ResourceFilterResolveKindNoOp:
return "", nil, true, nil
case qbtypes.ResourceFilterResolveKindFallback:
return "", nil, false, nil
}
}
stmt, err := b.resourceFilterResolver.StatementBuilder().Build(
ctx, start, end, qbtypes.RequestTypeRaw, query, variables,
)
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
return "", nil, err
}
if stmt == nil {
return "", nil, true, nil
return "", nil, nil
}
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
}
func (b *traceQueryStatementBuilder) buildResourceFilterCTE(
ctx context.Context,
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
return b.resourceFilterStmtBuilder.Build(
ctx,
start,
end,
qbtypes.RequestTypeRaw,
query,
variables,
)
}

View File

@@ -2,36 +2,19 @@ package telemetrytraces
import (
"context"
"regexp"
"testing"
"time"
cmock "github.com/SigNoz/clickhouse-go-mock"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/stretchr/testify/require"
)
type regexQueryMatcher struct{}
func (m *regexQueryMatcher) Match(expectedSQL, actualSQL string) error {
re, err := regexp.Compile(expectedSQL)
if err != nil {
return err
}
if !re.MatchString(actualSQL) {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expected query to match %s, got %s", expectedSQL, actualSQL)
}
return nil
}
func TestStatementBuilder(t *testing.T) {
// releaseTime is chosen so it lands inside the standard [1747947419000, 1747983448000]ms
// test window, keeping the multiIf SQL form for resource fields.
@@ -387,8 +370,6 @@ func TestStatementBuilder(t *testing.T) {
aggExprRewriter,
nil,
fl,
false,
100000,
)
vars := map[string]qbtypes.VariableItem{
@@ -685,8 +666,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
aggExprRewriter,
nil,
fl,
false,
100000,
)
for _, c := range cases {
@@ -815,8 +794,6 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
aggExprRewriter,
nil,
fl,
false,
100000,
)
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
@@ -887,8 +864,6 @@ func TestStatementBuilderGroupByResourceEvolution(t *testing.T) {
aggExprRewriter,
nil,
fl,
false,
100000,
)
query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
@@ -1054,8 +1029,6 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
aggExprRewriter,
nil,
fl,
false,
100000,
)
for _, c := range cases {
@@ -1588,115 +1561,3 @@ func TestAdjustKeys(t *testing.T) {
})
}
}
// TestSkipResourceFingerprint exercises the three resolver outcomes when
// skip_resource_fingerprint is enabled: use-CTE (count < threshold),
// fallback (count >= threshold), and the legacy path (feature disabled).
func TestSkipResourceFingerprint(t *testing.T) {
const (
startMs = uint64(1747947419000)
endMs = uint64(1747983448000)
threshold = uint64(10)
)
query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{
Expression: "service.name = 'redis-manual'",
},
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "name",
FieldContext: telemetrytypes.FieldContextSpan,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
Limit: 5,
}
t.Run("disabled uses the legacy CTE", func(t *testing.T) {
sb := newSkipResourceFingerprintBuilder(t, nil, false, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
})
t.Run("CTE attached when count below threshold", func(t *testing.T) {
mockStore := telemetrystoretest.New(telemetrystore.Config{}, &regexQueryMatcher{})
mock := mockStore.Mock()
// Only the count query runs against the telemetry store; the CTE
// itself is embedded as SQL in the main query (no extra round trip).
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
{Name: "count", Type: "UInt64"},
}, []any{uint64(2)}))
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
require.NoError(t, mock.ExpectationsWereMet())
})
t.Run("fallback when count at or above threshold", func(t *testing.T) {
mockStore := telemetrystoretest.New(telemetrystore.Config{}, &regexQueryMatcher{})
mock := mockStore.Mock()
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM signoz_traces\.distributed_traces_v3_resource`).
WillReturnRow(cmock.NewRow([]cmock.ColumnType{
{Name: "count", Type: "UInt64"},
}, []any{threshold}))
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
stmt, err := sb.Build(context.Background(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
require.NoError(t, err)
require.NotContains(t, stmt.Query, "__resource_filter AS")
require.NotContains(t, stmt.Query, "resource_fingerprint")
// resource conditions are pushed onto the main table via the
// resource.`service.name` / resources_string lookup
require.Contains(t, stmt.Query, "service.name")
require.NoError(t, mock.ExpectationsWereMet())
})
}
func newSkipResourceFingerprintBuilder(
t *testing.T,
telemetryStore telemetrystore.TelemetryStore,
skipEnable bool,
threshold uint64,
) *traceQueryStatementBuilder {
t.Helper()
fm := NewFieldMapper()
cb := NewConditionBuilder(fm)
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
fl := flaggertest.New(t)
aggExprRewriter := querybuilder.NewAggExprRewriter(
instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl,
)
return NewTraceQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
aggExprRewriter,
telemetryStore,
fl,
skipEnable,
threshold,
)
}

View File

@@ -25,7 +25,7 @@ func newTestTraceOperatorStatementBuilder(t *testing.T) *traceOperatorStatementB
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
traceStmtBuilder := NewTraceQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore, fm, cb, aggExprRewriter, nil, fl, false, 100000,
mockMetadataStore, fm, cb, aggExprRewriter, nil, fl,
)
return NewTraceOperatorStatementBuilder(
instrumentationtest.New().ToProviderSettings(),

View File

@@ -6,13 +6,13 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/stretchr/testify/assert"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/stretchr/testify/require"
)
@@ -46,10 +46,8 @@ func TestTraceTimeRangeOptimization(t *testing.T) {
fm,
cb,
aggExprRewriter,
nil, // telemetryStore is nil - adaptive path is disabled
nil, // telemetryStore is nil - optimization won't happen but code path is tested
fl,
false,
100000,
)
tests := []struct {

View File

@@ -1,9 +0,0 @@
package querybuildertypesv5
type ResourceFilterResolveKind int
const (
ResourceFilterResolveKindNoOp ResourceFilterResolveKind = iota
ResourceFilterResolveKindUseCTE
ResourceFilterResolveKindFallback
)

View File

@@ -1,105 +0,0 @@
"""
Transparency check for the skip_resource_fingerprint optimization (traces and logs).
At or above the configured fingerprint threshold the optimization pushes resource
conditions onto the main spans/logs table instead of the fingerprint CTE. That
rewrite must change ClickHouse performance, never the rows: each test runs the same
query against the primary instance (optimization on, threshold=2) and
`signoz_fingerprint` (optimization off) and asserts the responses are identical.
"""
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.logs import Logs
from fixtures.querier import (
BuilderQuery,
OrderBy,
TelemetryFieldKey,
assert_identical_query_response,
get_rows,
make_query_request,
)
from fixtures.traces import Traces
def test_skip_resource_fingerprint_traces_fallback_matches_fingerprint(
signoz: types.SigNoz,
signoz_fingerprint: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[list[Traces]], None],
) -> None:
"""A >= 2-fingerprint filter drives the fallback path; rows must match the fingerprint baseline."""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
# 3 distinct services share one env (3 fingerprints > threshold 2 -> fallback);
# the 4th has a different env and must be excluded.
env = {"deployment.environment": "skip-fallback"}
insert_traces(
[
Traces(timestamp=now - timedelta(seconds=10), resources={"service.name": "skip-fb-svc-a", **env}),
Traces(timestamp=now - timedelta(seconds=9), resources={"service.name": "skip-fb-svc-b", **env}),
Traces(timestamp=now - timedelta(seconds=8), resources={"service.name": "skip-fb-svc-c", **env}),
Traces(timestamp=now - timedelta(seconds=7), resources={"service.name": "skip-fb-other", "deployment.environment": "skip-other"}),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = BuilderQuery(
signal="traces",
limit=50,
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
filter_expression="deployment.environment = 'skip-fallback'",
select_fields=[TelemetryFieldKey("service.name", "string", "resource")],
).to_dict()
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
assert len(get_rows(optimized)) == 3
assert_identical_query_response(optimized, fingerprint)
def test_skip_resource_fingerprint_logs_fallback_matches_fingerprint(
signoz: types.SigNoz,
signoz_fingerprint: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_logs: Callable[[list[Logs]], None],
) -> None:
"""A >= 2-fingerprint filter drives the fallback path; rows must match the fingerprint baseline."""
now = datetime.now(tz=UTC)
# 3 distinct services share one env (3 fingerprints > threshold 2 -> fallback);
# the 4th has a different env and must be excluded.
env = {"deployment.environment": "skip-logs-fallback"}
insert_logs(
[
Logs(timestamp=now - timedelta(seconds=10), resources={"service.name": "skip-logs-fb-svc-a", **env}, body="a"),
Logs(timestamp=now - timedelta(seconds=9), resources={"service.name": "skip-logs-fb-svc-b", **env}, body="b"),
Logs(timestamp=now - timedelta(seconds=8), resources={"service.name": "skip-logs-fb-svc-c", **env}, body="c"),
Logs(timestamp=now - timedelta(seconds=7), resources={"service.name": "skip-logs-fb-other", "deployment.environment": "skip-logs-other"}, body="noise"),
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = BuilderQuery(
signal="logs",
limit=50,
order=[OrderBy(TelemetryFieldKey("timestamp"), "asc")],
filter_expression="deployment.environment = 'skip-logs-fallback'",
select_fields=[TelemetryFieldKey("service.name", "string", "resource"), TelemetryFieldKey("body")],
).to_dict()
start_ms = int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000)
end_ms = int(datetime.now(tz=UTC).timestamp() * 1000)
optimized = make_query_request(signoz, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
fingerprint = make_query_request(signoz_fingerprint, token, start_ms=start_ms, end_ms=end_ms, request_type="raw", queries=[query])
assert len(get_rows(optimized)) == 3
assert_identical_query_response(optimized, fingerprint)

View File

@@ -1,71 +0,0 @@
import pytest
from testcontainers.core.container import Network
from fixtures import types
from fixtures.signoz import create_signoz
@pytest.fixture(name="signoz", scope="package")
def signoz_skip_resource_fingerprint(
network: Network,
migrator: types.Operation, # pylint: disable=unused-argument
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
sqlstore: types.TestContainerSQL,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.SigNoz:
"""
Package-scoped SigNoz instance with the skip_resource_fingerprint
optimization enabled and a low threshold so the fallback resolver path is
exercised (filters matching >= 2 fingerprints skip the fingerprint CTE).
"""
return create_signoz(
network=network,
zeus=zeus,
gateway=gateway,
sqlstore=sqlstore,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="signoz-skip-resource-fingerprint",
env_overrides={
"SIGNOZ_QUERIER_SKIP__RESOURCE__FINGERPRINT_ENABLED": True,
"SIGNOZ_QUERIER_SKIP__RESOURCE__FINGERPRINT_THRESHOLD": 2,
},
)
@pytest.fixture(name="signoz_fingerprint", scope="package")
def signoz_fingerprint(
network: Network,
migrator: types.Operation, # pylint: disable=unused-argument
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
sqlstore: types.TestContainerSQL,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.SigNoz:
"""
A second SigNoz instance with the optimization disabled, so every query
resolves resource filters through the fingerprint CTE (the path the primary
also takes below the threshold). It shares the same ClickHouse (telemetry)
and sqlstore (users) as the primary instance, which lets a single admin
token authenticate against both and lets the tests diff the optimized
result against this fingerprint baseline for the same query and data.
"""
return create_signoz(
network=network,
zeus=zeus,
gateway=gateway,
sqlstore=sqlstore,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="signoz-skip-resource-fingerprint-baseline",
env_overrides={
"SIGNOZ_QUERIER_SKIP__RESOURCE__FINGERPRINT_ENABLED": False,
},
)