Compare commits

...

2 Commits

Author SHA1 Message Date
Abhi Kumar
3155f778f0 fix(drilldown): carry the field type through a breakout
getBreakoutQuery rebuilt the group-by as `{ key, type }`, dropping `dataType`.
The breakout query then goes out with no field type for the backend to work
from, and a drilldown on the breakout's own result has nothing to branch on —
so a numeric column's filter value gets quoted like a string.

Carrying it through needed the type to be nameable, and it wasn't:
QueryKeyDataSuggestionsProps declared `fieldDataType` as the antlr key-type
enum (`string | number | boolean`), which that endpoint never returns. Five
call sites already cast it back to FieldDataType to get work done.

- declare the suggestions response's `fieldDataType` as FieldDataType, and drop
  the casts that existed only to undo the wrong declaration
- convert where a reported type becomes a query-builder one, so the crossing is
  a function rather than a cast. The table is keyed `Record<FieldDataType, …>`,
  so a type added to the API union fails to compile here instead of going
  missing at runtime
- carry `dataType` through getBreakoutQuery; the object now satisfies
  BaseAutocompleteData on its own, so that cast goes too

Depends on the operator-lookup fix in #12298: a numeric breakout now carries
`float64`, and before that change the menu threw on anything the operator map
didn't have.
2026-07-28 12:51:12 +05:30
Pandey
44828b4185 chore(deps): bump clickhouse-sql-parser to v0.5.2 (#12303)
Some checks are pending
build-staging / js-build (push) Blocked by required conditions
build-staging / prepare (push) Waiting to run
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* chore(deps): bump clickhouse-sql-parser to v0.5.2

The parser hangs in an infinite loop on an unterminated block comment and panics
on a settings clause with no value, both reachable from user-authored SQL. v0.5.2
fixes those, along with Accept and Walk missing AST children, which anything
walking the tree to inspect a query depends on.

String() on Expr is gone in favour of FormatSQL, so the callers that rendered a
node back to SQL now go through the Format helper, which formats compactly the
way String() did.

* test(querierlogs): cover the rate aggregation family

rate and rate_sum divide by the query window rather than the step, and nothing
exercised that path, so a change to how the aggregation expression is rendered
would have gone unnoticed. Assert both against the inserted logs.

The window is now passed to the request helper instead of relying on its default,
since these are the only assertions in the file that depend on it.

* test(querierlogs): match the response rounding for rate expectations

Scalar responses round floats to three significant figures below one, and the
rates are the only values in this test that do not divide evenly, so compare
against the rounded form rather than the exact quotient.

* test(queriertraces): cover the rate aggregation family

Traces derives the scalar rate window separately from logs, so a divergence
between the two would go unnoticed with only the logs case. rate_sum here is
taken over the intrinsic duration column, which also puts it on the other side
of the response rounding boundary from the logs test.
2026-07-27 20:30:16 +00:00
18 changed files with 149 additions and 44 deletions

View File

@@ -8,7 +8,6 @@ import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import {
FieldContext,
FieldDataType,
SignalType,
TelemetryFieldKey,
} from 'types/api/v5/queryRange';
@@ -55,7 +54,7 @@ function OtherFields({
key: buildCompositeKey(attr.name, attr.fieldContext as string),
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType as FieldDataType,
fieldDataType: attr.fieldDataType,
}),
);
const addedIds = new Set(

View File

@@ -25,12 +25,12 @@ import CodeMirror, {
} from '@uiw/react-codemirror';
import { Button, Popover, Tooltip } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Info, TriangleAlert } from '@signozhq/icons';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { FieldDataType } from 'types/api/v5/queryRange';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
@@ -323,16 +323,16 @@ function QueryAggregationSelect({
TracesAggregatorOperator.RATE,
];
const fieldDataType =
const fieldDataType: FieldDataType | undefined =
functionContextForFetch &&
operatorsWithoutDataType.includes(functionContextForFetch)
? undefined
: QUERY_BUILDER_KEY_TYPES.NUMBER;
: 'number';
return getKeySuggestions({
signal: queryData.dataSource,
searchText: '',
fieldDataType: fieldDataType as QUERY_BUILDER_KEY_TYPES,
fieldDataType,
});
},
{

View File

@@ -113,7 +113,7 @@ const useOptionsMenu = ({
(suggestion) => ({
name: suggestion.name,
signal: suggestion.signal as SignalType,
fieldDataType: suggestion.fieldDataType as FieldDataType,
fieldDataType: suggestion.fieldDataType,
fieldContext: suggestion.fieldContext as FieldContext,
}),
);
@@ -192,7 +192,7 @@ const useOptionsMenu = ({
name: e.name,
signal: e.signal as SignalType,
fieldContext: e.fieldContext as FieldContext,
fieldDataType: e.fieldDataType as FieldDataType,
fieldDataType: e.fieldDataType,
}));
}
if (dataSource === DataSource.TRACES) {

View File

@@ -4,11 +4,11 @@ import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import useDebounce from 'hooks/useDebounce';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
import { BreakoutOptionsProps } from './contextConfig';
import { BreakoutAttributeType } from './types';
@@ -80,7 +80,7 @@ function BreakoutOptions({
keyArray.forEach((keyData) => {
transformedOptions.push({
key: keyData.name,
dataType: keyData.fieldDataType as QUERY_BUILDER_KEY_TYPES,
dataType: fieldDataTypeToDataType(keyData.fieldDataType),
type: '',
});
});

View File

@@ -238,6 +238,10 @@ describe('TableDrilldown Breakout Functionality', () => {
expect(aggregateQueryData.groupBy).toHaveLength(1);
expect(aggregateQueryData.groupBy[0].key).toBe('deployment.environment');
// The picked field's type travels with it — dropping it leaves the breakout query
// untyped, so a drilldown on its result can't tell a number from a string.
expect(aggregateQueryData.groupBy[0].dataType).toBe('string');
// Verify that orderBy has been cleared (as per getBreakoutQuery logic)
expect(aggregateQueryData.orderBy).toStrictEqual([]);

View File

@@ -1,6 +1,5 @@
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import cloneDeep from 'lodash-es/cloneDeep';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { addFilterToSelectedQuery, FilterData } from './drilldownUtils';
@@ -89,7 +88,11 @@ export const getBreakoutQuery = (
.filter((item: IBuilderQuery) => item.queryName === aggregateData.queryName)
.map((item: IBuilderQuery) => ({
...item,
groupBy: [{ key: groupBy.key, type: groupBy.type } as BaseAutocompleteData],
// The picked field's type travels with it: dropped, the breakout query goes out
// untyped and a drilldown on its result can't tell a number from a string.
groupBy: [
{ key: groupBy.key, dataType: groupBy.dataType, type: groupBy.type },
],
orderBy: [],
legend: item.legend && groupBy.key ? `{{${groupBy.key}}}` : '',
}));

View File

@@ -1,6 +1,8 @@
import { ReactNode } from 'react';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type ContextMenuItem = ReactNode;
@@ -31,7 +33,8 @@ export interface AggregateContextMenuConfig {
export interface BreakoutAttributeType {
key: string;
dataType: QUERY_BUILDER_KEY_TYPES;
/** The picked field's type, in the vocabulary the group-by it becomes is read with. */
dataType: DataTypes;
type: string;
}

View File

@@ -1,4 +1,4 @@
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { FieldDataType } from 'types/api/v5/queryRange';
export interface QueryKeyDataSuggestionsProps {
label: string;
@@ -7,7 +7,12 @@ export interface QueryKeyDataSuggestionsProps {
apply?: string;
detail?: string;
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
/**
* The field's type as the API reports it. Was declared as the antlr key-type enum
* (`string | number | boolean`), which the endpoint never returns — every consumer cast it
* back to `FieldDataType`.
*/
fieldDataType?: FieldDataType;
name: string;
signal: 'traces' | 'logs' | 'metrics';
}
@@ -26,7 +31,7 @@ export interface QueryKeyRequestProps {
signal: 'traces' | 'logs' | 'metrics';
searchText: string;
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
fieldDataType?: FieldDataType;
metricName?: string;
metricNamespace?: string;
signalSource?: 'meter' | '';

View File

@@ -0,0 +1,34 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { FieldDataType } from 'types/api/v5/queryRange';
import { fieldDataTypeToDataType } from '../fieldDataType';
describe('fieldDataTypeToDataType', () => {
it('maps the reported numeric spellings onto the query-builder numerics', () => {
expect(fieldDataTypeToDataType('number')).toBe(DataTypes.Float64);
expect(fieldDataTypeToDataType('int64')).toBe(DataTypes.Int64);
expect(fieldDataTypeToDataType('float64')).toBe(DataTypes.Float64);
});
it('maps the list spellings onto the array types', () => {
expect(fieldDataTypeToDataType('[]string')).toBe(DataTypes.ArrayString);
expect(fieldDataTypeToDataType('[]bool')).toBe(DataTypes.ArrayBool);
expect(fieldDataTypeToDataType('[]int64')).toBe(DataTypes.ArrayInt64);
expect(fieldDataTypeToDataType('[]float64')).toBe(DataTypes.ArrayFloat64);
expect(fieldDataTypeToDataType('[]number')).toBe(DataTypes.ArrayFloat64);
});
it('passes through the spellings the two vocabularies share', () => {
expect(fieldDataTypeToDataType('string')).toBe(DataTypes.String);
expect(fieldDataTypeToDataType('bool')).toBe(DataTypes.bool);
});
it('returns EMPTY for empty, missing and not-yet-known types', () => {
expect(fieldDataTypeToDataType('')).toBe(DataTypes.EMPTY);
expect(fieldDataTypeToDataType(undefined)).toBe(DataTypes.EMPTY);
// The backend can store `[]json` / `[]dynamic`, which the API union doesn't list.
expect(fieldDataTypeToDataType('[]json' as FieldDataType)).toBe(
DataTypes.EMPTY,
);
});
});

View File

@@ -0,0 +1,31 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { FieldDataType } from 'types/api/v5/queryRange';
/**
* The field metadata APIs and the query builder spell field types differently: the metadata
* collapses the numerics to `number` and writes lists as `[]string`, while `DataTypes` — which
* keys the operator and attribute-value lookups — uses `float64` and `array(string)`. Convert
* where a reported type becomes a query-builder one, so those lookups can't miss.
*/
const TO_DATA_TYPE: Record<FieldDataType, DataTypes> = {
'': DataTypes.EMPTY,
string: DataTypes.String,
bool: DataTypes.bool,
int64: DataTypes.Int64,
float64: DataTypes.Float64,
number: DataTypes.Float64,
'[]string': DataTypes.ArrayString,
'[]bool': DataTypes.ArrayBool,
'[]int64': DataTypes.ArrayInt64,
'[]float64': DataTypes.ArrayFloat64,
'[]number': DataTypes.ArrayFloat64,
};
/**
* Exhaustive over `FieldDataType`, so a type added to the API union fails to compile here
* rather than going missing at runtime. The fallback is for a spelling the union doesn't know
* yet — the backend can store `[]json` and `[]dynamic` — not for any of the cases above.
*/
export const fieldDataTypeToDataType = (
fieldDataType?: FieldDataType,
): DataTypes => TO_DATA_TYPE[fieldDataType ?? ''] ?? DataTypes.EMPTY;

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.7
require (
dario.cat/mergo v1.0.2
github.com/AfterShip/clickhouse-sql-parser v0.4.16
github.com/AfterShip/clickhouse-sql-parser v0.5.2
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/clickhouse-go-mock v0.14.0

4
go.sum
View File

@@ -66,8 +66,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AfterShip/clickhouse-sql-parser v0.4.16 h1:gpl+wXclYUKT0p4+gBq22XeRYWwEoZ9f35vogqMvkLQ=
github.com/AfterShip/clickhouse-sql-parser v0.4.16/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus=
github.com/AfterShip/clickhouse-sql-parser v0.5.2 h1:KCxdmvuVawVF4yl0ZS33q7olgmLZwvZG5BjWHp6o414=
github.com/AfterShip/clickhouse-sql-parser v0.5.2/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=

View File

@@ -96,9 +96,9 @@ func (r *aggExprRewriter) Rewrite(
}
if visitor.isRate {
return fmt.Sprintf("%s/%d", sel.SelectItems[0].String(), rateInterval), visitor.chArgs, nil
return fmt.Sprintf("%s/%d", chparser.Format(sel.SelectItems[0]), rateInterval), visitor.chArgs, nil
}
return sel.SelectItems[0].String(), visitor.chArgs, nil
return chparser.Format(sel.SelectItems[0]), visitor.chArgs, nil
}
// RewriteMulti rewrites a slice of expressions.
@@ -207,7 +207,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
// Handle *If functions with predicate + values
if aggFunc.FuncCombinator {
// Map the predicate (last argument)
origPred := args[len(args)-1].String()
origPred := chparser.Format(args[len(args)-1])
whereClause, err := PrepareWhereClause(
origPred,
FilterExprVisitorOpts{
@@ -242,7 +242,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
// Map each value column argument
for i := 0; i < len(args)-1; i++ {
origVal := args[i].String()
origVal := chparser.Format(args[i])
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(origVal)
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, dataType, v.fieldKeys)
if err != nil {
@@ -259,7 +259,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
} else {
// Non-If functions: map every argument as a column/value
for i, arg := range args {
orig := arg.String()
orig := chparser.Format(arg)
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(orig)
expr, err := v.fieldMapper.ColumnExpressionFor(v.ctx, v.orgID, v.startNs, v.endNs, &fieldKey, dataType, v.fieldKeys)
if err != nil {

View File

@@ -316,17 +316,17 @@ func (e *ClickHouseFilterExtractor) extractColumnStrByExpr(expr clickhouse.Expr)
// FunctionExpr is a function call like "toDate(timestamp)"
case *clickhouse.FunctionExpr:
// For function expressions, return the complete function call string
return ex.String()
return clickhouse.Format(ex)
// ColumnExpr is a column expression like "m.region", "toDate(timestamp)"
case *clickhouse.ColumnExpr:
// ColumnExpr wraps another expression - extract the underlying expression
if ex.Expr != nil {
return e.extractColumnStrByExpr(ex.Expr)
}
return ex.String()
return clickhouse.Format(ex)
default:
// For other expression types, return the string representation
return expr.String()
return clickhouse.Format(expr)
}
}
@@ -524,7 +524,7 @@ func (e *ClickHouseFilterExtractor) extractFullExpression(expr clickhouse.Expr)
if expr == nil {
return ""
}
return expr.String()
return clickhouse.Format(expr)
}
// isSimpleColumnReference checks if an expression is just a simple column reference
@@ -657,7 +657,7 @@ func (e *ClickHouseFilterExtractor) extractCTEName(cte *clickhouse.CTEStmt) stri
case *clickhouse.Ident:
return name.Name
default:
return cte.Expr.String()
return clickhouse.Format(cte.Expr)
}
}

View File

@@ -27,7 +27,7 @@ func (v *TelemetryFieldVisitor) VisitColumnDef(expr *parser.ColumnDef) error {
}
// Parse column name to extract context and data type
columnName := expr.Name.String()
columnName := parser.Format(expr.Name)
// Remove backticks if present
columnName = strings.TrimPrefix(columnName, "`")
@@ -75,7 +75,7 @@ func (v *TelemetryFieldVisitor) VisitColumnDef(expr *parser.ColumnDef) error {
// Extract field name from the DEFAULT expression
// The DEFAULT expression should be something like: resources_string['k8s.cluster.name']
// We need to extract the key inside the square brackets
defaultExprStr := expr.DefaultExpr.String()
defaultExprStr := parser.Format(expr.DefaultExpr)
// Look for the pattern: map['key']
startIdx := strings.Index(defaultExprStr, "['")

View File

@@ -69,7 +69,7 @@ func (qp *QueryProcessor) ProcessQuery(query string, transformer FilterTransform
// Reconstruct the query
var resultBuilder strings.Builder
for _, stmt := range stmts {
resultBuilder.WriteString(stmt.String())
resultBuilder.WriteString(parser.Format(stmt))
resultBuilder.WriteString(";")
}

View File

@@ -20,12 +20,15 @@ from fixtures.querier import (
# Numeric aggregation-function coverage for logs — the logs counterpart of
# queriertraces/02_aggregation.py::test_traces_aggregate_functions. Logs querier
# tests elsewhere only ever use count()/count_distinct(); here a grouped scalar
# query computes sum / avg / min / max / p50 / p90 / p95 / p99 / countIf /
# count_distinct over a numeric log attribute and asserts every value.
# query computes sum / avg / min / max / p50 / p90 / p95 / p99 / countIf / rate /
# rate_sum / count_distinct over a numeric log attribute and asserts every value.
#
# Log number attributes are stored as Float64, so aggregates come back as floats;
# percentiles are matched with pytest.approx (ClickHouse quantile() and
# numpy.percentile both linear-interpolate, but avoid ULP-level exact equality).
#
# The rate family divides by a window the caller does not otherwise see, so the
# lookback is passed explicitly instead of taken from the request helper default.
def test_logs_aggregate_functions(
@@ -41,11 +44,14 @@ def test_logs_aggregate_functions(
Tests:
A grouped scalar query computes count / sum / avg / min / max / p50 / p90 /
p95 / p99 over latency_ms, countIf over a numeric threshold, and
count_distinct over a string attribute — all matching values derived from the
inserted logs, ordered by count() desc.
p95 / p99 over latency_ms, countIf over a numeric threshold, rate and
rate_sum over the query window, and count_distinct over a string attribute —
all matching values derived from the inserted logs, ordered by count() desc.
"""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
# A scalar rate divides by the whole query window, so both sides agree on it.
lookback_minutes = 5
rate_interval_seconds = lookback_minutes * 60
# (service, latency_ms, endpoint)
specs = [
("svc-a", 10, "/x"),
@@ -80,12 +86,14 @@ def test_logs_aggregate_functions(
build_aggregation("p95(latency_ms)", "p95_l"),
build_aggregation("p99(latency_ms)", "p99_l"),
build_aggregation("countIf(latency_ms >= 25)", "slow"),
build_aggregation("rate()", "rate_all"),
build_aggregation("rate_sum(latency_ms)", "rate_sum_l"),
build_aggregation("count_distinct(endpoint)", "endpoints"),
],
group_by=[build_group_by_field("service.name", "string", "resource")],
order=[build_order_by("count()", "desc")],
)
response = make_scalar_query_request(signoz, token, now, [query])
response = make_scalar_query_request(signoz, token, now, [query], lookback_minutes=lookback_minutes)
assert response.status_code == HTTPStatus.OK, response.text
data = get_scalar_table_data(response.json())
@@ -109,6 +117,12 @@ def test_logs_aggregate_functions(
float(np.percentile(latencies, 95)), # p95(latency_ms)
float(np.percentile(latencies, 99)), # p99(latency_ms)
sum(1 for latency in latencies if latency >= 25), # countIf(latency_ms >= 25)
# Response floats are rounded to 3 decimals at or above 1 and to 3
# significant figures below it (roundToNonZeroDecimals). Every other
# value here is exact; the rates are the only ones that repeat, and
# they stay below 1 for this fixture, so mirror the latter form.
float(f"{len(latencies) / rate_interval_seconds:.3g}"), # rate()
float(f"{sum(latencies) / rate_interval_seconds:.3g}"), # rate_sum(latency_ms)
len(set(endpoints)), # count_distinct(endpoint)
]
assert by_service[service] == pytest.approx(expected), f"{service}: {by_service[service]} != {expected}"

View File

@@ -150,12 +150,17 @@ def test_traces_aggregate_functions(
Tests:
A grouped scalar query computes count / sum / avg / min / max / p50 / p90 over
duration_nano, countIf over the intrinsic status_code and the calculated
response_status_code, and avg over a numeric attribute — all matching values
derived from the inserted spans. Under the corrupt variant the same-named
colliding attributes must not change any of these.
response_status_code, rate and rate_sum over the query window, and avg over a
numeric attribute — all matching values derived from the inserted spans. Under
the corrupt variant the same-named colliding attributes must not change any of
these.
"""
extra_attrs, extra_resources = trace_noise(noise)
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
# A scalar rate divides by the whole query window, so both sides agree on it.
# Traces derives this separately from logs (telemetrytraces/statement_builder.go).
lookback_minutes = 5
rate_interval_seconds = lookback_minutes * 60
def mk(service: str, dur_s: float, status: TracesStatusCode, rsc: str, latency: float) -> Traces:
return Traces(
@@ -189,6 +194,8 @@ def test_traces_aggregate_functions(
build_aggregation("p50(duration_nano)", "p50_d"),
build_aggregation("p90(duration_nano)", "p90_d"),
build_aggregation("countIf(status_code = 2)", "errs"),
build_aggregation("rate()", "rate_all"),
build_aggregation("rate_sum(duration_nano)", "rate_sum_d"),
build_aggregation("avg(latency_ms)", "avg_lat"),
]
query = build_traces_scalar_query(
@@ -196,7 +203,7 @@ def test_traces_aggregate_functions(
group_by=[build_group_by_field("service.name", "string", "resource")],
order=[build_order_by("count()", "desc")],
)
response = make_scalar_query_request(signoz, token, now, [query])
response = make_scalar_query_request(signoz, token, now, [query], lookback_minutes=lookback_minutes)
assert response.status_code == HTTPStatus.OK, response.text
data = get_scalar_table_data(response.json())
@@ -214,6 +221,11 @@ def test_traces_aggregate_functions(
float(np.percentile(durations, 50)), # p50(duration_nano)
float(np.percentile(durations, 90)), # p90(duration_nano)
sum(1 for s in group if int(s.status_code) == 2), # countIf(status_code = 2)
# Response floats are rounded to 3 significant figures below 1 and to 3
# decimals at or above it (roundToNonZeroDecimals). The two rates land on
# either side of that boundary, so each mirrors its own form.
float(f"{len(group) / rate_interval_seconds:.3g}"), # rate()
round(sum(durations) / rate_interval_seconds, 3), # rate_sum(duration_nano)
sum(latencies) / len(latencies), # avg(latency_ms)
)