Compare commits

..

18 Commits

Author SHA1 Message Date
Abhi Kumar
cfac600d9b test(query-builder): split the qb driver into modules 2026-10-30 14:59:20 +05:30
Abhi Kumar
ad4c499311 Merge remote-tracking branch 'origin/fix/qb-add-query-signal' into test/qb-e2e 2026-10-30 14:50:04 +05:30
Abhi Kumar
4df6f3198d test(query-builder): split data-source tests and pin source to the last query 2026-10-30 14:49:40 +05:30
Abhi Kumar
1b5d1ebe3e test(query-builder): an added panel-editor query keeps the previous signal 2026-10-30 14:22:03 +05:30
Abhi Kumar
7031eeb712 Merge remote-tracking branch 'origin/fix/qb-add-query-signal' into test/qb-e2e 2026-10-30 14:09:26 +05:30
Abhi Kumar
abefec1d2e Merge remote-tracking branch 'origin/fix/qb-legacy-having-and-limit' into fix/qb-add-query-signal 2026-10-30 14:08:16 +05:30
Abhi Kumar
2aa5b67db5 fix(query-builder): added query follows the previous query's signal
Hosts with a fixed data source keep it; only the panel editor, which
has none, changes from always-metrics to the last query's signal.
2026-10-30 14:04:28 +05:30
Abhi Kumar
95768b1524 test(query-builder): run the legacy having and negative limit cases 2026-10-30 13:29:33 +05:30
Abhi Kumar
6c56fb2e4d test(query-builder): scope selectors to the query block and wait on own requests
Run waits on a request it started; panel-editor structural edits re-run
until the newest request matches, since their draft can lag the DOM.
2026-10-30 13:29:33 +05:30
Abhi Kumar
44d2ad1288 test(query-builder): cover trace operator, signal switching, query tabs and run/cancel
holdQueryRange ignores a request cancelled while held: there is nothing
left to answer.
2026-10-30 13:29:33 +05:30
Abhi Kumar
14a6ca4c5a test(query-builder): cover panel-kind add-ons, reduce-to, multi-query and formulas 2026-10-30 13:29:33 +05:30
Abhi Kumar
7c73635e62 test(query-builder): cover group by, having, order, limit and legend add-ons
Negative limit stays fixme: the input accepts it and the backend
rejects it with a 400.
2026-10-30 13:29:33 +05:30
Abhi Kumar
a33fc0c29f test(query-builder): cover metric-type defaults, aggregations and step interval
Gauge and histogram metrics are seeded per worker: the golden dataset
only has sums.
2026-10-30 13:29:33 +05:30
Abhi Kumar
f5105d46ab test(query-builder): cover explorer-to-dashboard and alert trace operator round trips
pickChannelByName re-clicks while the select is still closed: a condition
re-render could close it as the click opened it.
2026-10-30 13:29:33 +05:30
Abhi Kumar
27976cf5d4 test(query-builder): cover legacy conversion, history, IN lists and quick filters
Legacy having stays fixme: convertHavingToExpression keeps the stored
COUNT() casing and the v5 backend rejects it with a 400.
2026-10-30 13:29:33 +05:30
Abhi Kumar
7eb9dfa5e1 test(query-builder): add qb driver with url and filter editor specs
runShortcut waits two frames: a keypress in the same frame as the last
keystroke reads the pre-commit React state, which no user can produce.
2026-10-30 13:29:33 +05:30
Abhi Kumar
6802e3b983 test(query-builder): type the limit through userEvent 2026-10-30 13:29:32 +05:30
Abhi Kumar
cafcab336a fix(query-builder): legacy having casing and non-positive limits
Legacy having referenced COUNT(), which v5 rejects; only the function
name is lower-cased since attribute keys are case-sensitive. Limits
accept positive integers only, for queries and formulas.
2026-10-30 13:29:32 +05:30
49 changed files with 2374 additions and 314 deletions

View File

@@ -12131,8 +12131,6 @@ components:
additionalProperties:
type: string
type: object
scope:
$ref: '#/components/schemas/SpantypesScope'
spanId:
type: string
timestamp:
@@ -12149,7 +12147,6 @@ components:
- event
- attributes
- resource
- scope
type: object
SpantypesGettableFlamegraphTrace:
properties:
@@ -12390,16 +12387,6 @@ components:
nullable: true
type: array
type: object
SpantypesScope:
properties:
attributes:
additionalProperties: {}
type: object
name:
type: string
version:
type: string
type: object
SpantypesSpanAggregation:
properties:
aggregation:
@@ -12769,8 +12756,6 @@ components:
type: object
response_status_code:
type: string
scope:
$ref: '#/components/schemas/SpantypesScope'
span_id:
type: string
status_code:

View File

@@ -13264,23 +13264,6 @@ export type SpantypesFlamegraphSpanDTOAttributes = { [key: string]: unknown };
export type SpantypesFlamegraphSpanDTOResource = { [key: string]: string };
export type SpantypesScopeDTOAttributes = { [key: string]: unknown };
export interface SpantypesScopeDTO {
/**
* @type object
*/
attributes?: SpantypesScopeDTOAttributes;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
version?: string;
}
export interface SpantypesFlamegraphSpanDTO {
/**
* @type object
@@ -13316,7 +13299,6 @@ export interface SpantypesFlamegraphSpanDTO {
* @type object
*/
resource: SpantypesFlamegraphSpanDTOResource;
scope: SpantypesScopeDTO;
/**
* @type string
*/
@@ -13862,7 +13844,6 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
response_status_code?: string;
scope?: SpantypesScopeDTO;
/**
* @type string
*/

View File

@@ -26,6 +26,7 @@ import {
RAW_QUERY_FIELDS,
resolveQueryBuilderFields,
} from '../../queryBuilderFields.utils';
import { parseLimit } from '../../utils';
import BucketOptions from './BucketOptions/BucketOptions';
import HavingFilter from './HavingFilter/HavingFilter';
@@ -278,7 +279,7 @@ function QueryAddOns({
const handleChangeLimit = useCallback(
(value: string) => {
handleChangeQueryData('limit', Number(value) || null);
handleChangeQueryData('limit', parseLimit(value));
},
[handleChangeQueryData],
);

View File

@@ -169,8 +169,14 @@ describe('QueryAddOns', () => {
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
expect(input.value).toBe('5');
fireEvent.change(input, { target: { value: '10' } });
expect(mockHandleChangeQueryData).toHaveBeenCalledWith('limit', 10);
const user = userEvent.setup();
await user.clear(input);
await user.type(input, '10');
expect(mockHandleChangeQueryData).toHaveBeenLastCalledWith('limit', 10);
await user.clear(input);
await user.type(input, '-5');
expect(mockHandleChangeQueryData).toHaveBeenLastCalledWith('limit', null);
});
it.each([

View File

@@ -12,10 +12,49 @@ import {
convertExpressionToFilters,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
convertHavingToExpression,
formatValueForExpression,
parseLimit,
removeKeysFromExpression,
} from '../utils';
describe('convertHavingToExpression', () => {
it.each([
['COUNT()', 'count() > 5'],
['COUNT_DISTINCT(service.name)', 'count_distinct(service.name) > 5'],
['P99(duration_nano)', 'p99(duration_nano) > 5'],
['SUM(http.Request.Size)', 'sum(http.Request.Size) > 5'],
['count()', 'count() > 5'],
])('references %s by its lower-case function', (columnName, expected) => {
expect(
convertHavingToExpression([{ columnName, op: '>', value: 5 }]),
).toStrictEqual({ expression: expected });
});
it('joins several conditions and formats list values', () => {
expect(
convertHavingToExpression([
{ columnName: 'COUNT()', op: '>', value: 5 },
{ columnName: 'COUNT()', op: 'NOT IN', value: [1, 2] },
]),
).toStrictEqual({ expression: 'count() > 5 AND count() NOT IN [1, 2]' });
});
});
describe('parseLimit', () => {
it.each([
['10', 10],
['1', 1],
['', null],
['0', null],
['-5', null],
['2.5', null],
['abc', null],
])('parses %j as %j', (value, expected) => {
expect(parseLimit(value)).toBe(expected);
});
});
describe('convertFiltersToExpression', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -845,6 +845,12 @@ export const appendAndClause = (
* @param having - Array of old having objects with columnName, op, and value
* @returns New having format with expression string
*/
/** A query or formula limit: a positive integer, otherwise none. */
export const parseLimit = (value: string): number | null => {
const limit = Number(value);
return Number.isInteger(limit) && limit > 0 ? limit : null;
};
export const convertHavingToExpression = (
having: Having[],
): { expression: string } => {
@@ -871,7 +877,14 @@ export const convertHavingToExpression = (
formattedValue = String(value);
}
return `${columnName} ${op} ${formattedValue}`;
// The old having filter upper-cased the function (`COUNT()`); v5 only
// resolves the aggregation's own lower-case expression.
const reference = columnName.replace(
/^\s*([A-Za-z_]\w*)\(/,
(_, fn: string) => `${fn.toLowerCase()}(`,
);
return `${reference} ${op} ${formattedValue}`;
})
.filter((expression) => expression !== ''); // Remove empty expressions

View File

@@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { Col, Input, Row, Select } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import BucketOptions from 'components/QueryBuilderV2/QueryV2/QueryAddOns/BucketOptions/BucketOptions';
import { parseLimit } from 'components/QueryBuilderV2/utils';
import { LEGEND } from 'constants/global';
import { PANEL_TYPES } from 'constants/queryBuilder';
// ** Hooks
@@ -171,7 +172,7 @@ export function Formula({
<InputWithLabel
label="Limit"
type="number"
onChange={(value): void => handleChangeLimit(Number(value))}
onChange={(value): void => handleChangeLimit(parseLimit(value))}
initialValue={formula?.limit ?? undefined}
placeholder="Enter limit"
/>

View File

@@ -20,7 +20,6 @@ function makeSpan(
event: [],
resource: {},
attributes: {},
scope: {},
...overrides,
};
}

View File

@@ -12,7 +12,6 @@ export const MOCK_SPAN: FlamegraphSpan = {
event: [],
resource: {},
attributes: {},
scope: {},
};
/** Nested spans structure for findSpanById tests */

View File

@@ -518,7 +518,6 @@ const flamegraphSpan = (span: BuiltSpan): SpantypesFlamegraphSpanDTO => ({
level: span.level,
attributes: span.template.attributes,
resource: resourceOf(span),
scope: {},
event: [],
});

View File

@@ -475,12 +475,17 @@ export function QueryBuilderProvider({
const createNewBuilderQuery = useCallback(
(queries: IBuilderQuery[]): IBuilderQuery => {
const existNames = queries.map((item) => item.queryName);
// Hosts without a fixed data source (the panel editor) continue the
// previous query's signal rather than resetting to metrics.
const previous = queries[queries.length - 1];
const initialBuilderQuery =
initialQueryBuilderFormValuesMap[initialDataSource || DataSource.METRICS];
initialQueryBuilderFormValuesMap[
initialDataSource || previous?.dataSource || DataSource.METRICS
];
const newQuery: IBuilderQuery = {
...initialBuilderQuery,
source: queries?.[0]?.source || '',
source: previous?.source || '',
builderQueryType: queries?.[0]?.builderQueryType,
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
expression: createNewBuilderItemName({

View File

@@ -0,0 +1,104 @@
import {
initialQueriesMap,
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
const renderQueryBuilder = (
initialQuery: Query,
): ReturnType<
typeof renderHook<ReturnType<typeof useQueryBuilder>, unknown>
> => {
const hook = renderHook(() => useQueryBuilder(), {
wrapper: AllTheProviders,
});
act(() => {
hook.result.current.initQueryBuilderData(initialQuery);
});
return hook;
};
describe('createNewBuilderQuery data source', () => {
it.each([DataSource.LOGS, DataSource.TRACES, DataSource.METRICS])(
'continues the previous %s query when the host fixes no data source',
(dataSource) => {
const { result } = renderQueryBuilder(initialQueriesMap[dataSource]);
act(() => {
result.current.addNewBuilderQuery();
});
const [, added] = result.current.currentQuery.builder.queryData;
expect(added.dataSource).toBe(dataSource);
expect(added.aggregations).toStrictEqual(
initialQueryBuilderFormValuesMap[dataSource].aggregations,
);
},
);
it('follows the last query when queries mix signals', () => {
const { result } = renderQueryBuilder(initialQueriesMap.logs);
act(() => {
result.current.addNewBuilderQuery();
});
act(() => {
result.current.handleSetQueryData(1, {
...initialQueryBuilderFormValuesMap.traces,
queryName: 'B',
expression: 'B',
});
});
act(() => {
result.current.addNewBuilderQuery();
});
expect(result.current.currentQuery.builder.queryData[2].dataSource).toBe(
DataSource.TRACES,
);
});
it('takes the source from the last query, not the first', () => {
const [metricsQuery] = initialQueriesMap.metrics.builder.queryData;
const { result } = renderQueryBuilder({
...initialQueriesMap.metrics,
builder: {
...initialQueriesMap.metrics.builder,
queryData: [
{ ...metricsQuery, source: '' },
{ ...metricsQuery, queryName: 'B', expression: 'B', source: 'meter' },
],
},
});
act(() => {
result.current.addNewBuilderQuery();
});
expect(result.current.currentQuery.builder.queryData[2]).toMatchObject({
dataSource: DataSource.METRICS,
source: 'meter',
});
});
it('uses the host’s fixed data source over the previous query', () => {
const { result } = renderQueryBuilder(initialQueriesMap.metrics);
act(() => {
result.current.handleSetConfig(PANEL_TYPES.TIME_SERIES, DataSource.LOGS);
});
act(() => {
result.current.addNewBuilderQuery();
});
expect(result.current.currentQuery.builder.queryData[1].dataSource).toBe(
DataSource.LOGS,
);
});
});

View File

@@ -28,7 +28,7 @@ const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$
// JSON holds the attributes.
var storableSpanColumns = []string{
"DISTINCT ON (span_id) timestamp", "duration_nano", "span_id", "has_error", "kind",
colServiceName, "name", "attributes", "resources_string", "scope",
colServiceName, "name", "attributes", "resources_string",
"if(notEmpty(attributes), map(), attributes_string) AS attributes_string",
"if(notEmpty(attributes), map(), attributes_number) AS attributes_number",
"if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool",
@@ -382,7 +382,6 @@ func (s *traceStore) GetFlamegraphSpans(ctx context.Context, traceID string, sta
"if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number",
"if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool",
"any(resources_string) AS resources_string",
"any(scope) AS scope",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
conditions := []string{

View File

@@ -154,7 +154,7 @@ func TestGetSpanCountByField(t *testing.T) {
}
func TestGetTraceSpans(t *testing.T) {
expectedSQL := "SELECT DISTINCT ON (span_id) timestamp, duration_nano, span_id, has_error, kind, resource_string_service$$name, name, attributes, resources_string, scope, if(notEmpty(attributes), map(), attributes_string) AS attributes_string, if(notEmpty(attributes), map(), attributes_number) AS attributes_number, if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool, events, status_message, status_code_string, kind_string, parent_span_id, flags, is_remote, trace_state, status_code, db_name, db_operation, http_method, http_url, http_host, external_http_method, external_http_url, response_status_code, links as references FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp ASC, name ASC"
expectedSQL := "SELECT DISTINCT ON (span_id) timestamp, duration_nano, span_id, has_error, kind, resource_string_service$$name, name, attributes, resources_string, if(notEmpty(attributes), map(), attributes_string) AS attributes_string, if(notEmpty(attributes), map(), attributes_number) AS attributes_number, if(notEmpty(attributes), map(), attributes_bool) AS attributes_bool, events, status_message, status_code_string, kind_string, parent_span_id, flags, is_remote, trace_state, status_code, db_name, db_operation, http_method, http_url, http_host, external_http_method, external_http_url, response_status_code, links as references FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp ASC, name ASC"
s := newTestStore(sqlmock.QueryMatcherEqual)
s.Mock().ExpectSelect(expectedSQL).
@@ -171,12 +171,12 @@ func TestGetFlamegraphSpans(t *testing.T) {
}{
{
name: "NoSpanIDs_GeneratesBaseSQL",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string, any(scope) AS scope FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY span_id ORDER BY timestamp ASC, name ASC",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY span_id ORDER BY timestamp ASC, name ASC",
},
{
name: "WithSpanIDs_GeneratesInClauseSQL",
spanIDs: []string{"span-1", "span-2"},
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string, any(scope) AS scope FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND span_id IN (?, ?) GROUP BY span_id ORDER BY timestamp ASC, name ASC",
sql: "SELECT span_id, any(parent_span_id) AS parent_span_id, any(timestamp) AS timestamp, any(duration_nano) AS duration_nano, any(has_error) AS has_error, any(name) AS name, any(events) AS events, any(attributes) AS attributes, if(notEmpty(attributes), map(), any(attributes_string)) AS attributes_string, if(notEmpty(attributes), map(), any(attributes_number)) AS attributes_number, if(notEmpty(attributes), map(), any(attributes_bool)) AS attributes_bool, any(resources_string) AS resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE trace_id = ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND span_id IN (?, ?) GROUP BY span_id ORDER BY timestamp ASC, name ASC",
},
}

View File

@@ -43,15 +43,13 @@ func stripKeyAlias(name string) string {
// unwrapVariant decodes a scan envelope: the chcol.Variant the driver scans a Dynamic column (a JSON
// path such as body_v2.level) into, and the JSONValue a whole JSON column scans into. The attributes
// bag decodes flat so its dotted keys merge with the legacy attribute maps and a key stored at both a
// scalar and an object path stays two keys; the scope decodes flat too, keeping its attributes dotted;
// every other JSON column decodes nested.
// scalar and an object path stays two keys; every other JSON column decodes nested.
func unwrapVariant(name string, val any) any {
switch v := val.(type) {
case chcol.Variant:
return v.Any()
case telemetrystoretypes.JSONValue:
switch name {
case "attributes", "scope":
if name == "attributes" {
return v.Flatten()
}
return v.Nest()
@@ -628,9 +626,6 @@ func mergeSpanAttributeColumns(data map[string]any) {
if raw, ok := data["links"]; ok {
data["links"] = spantypes.ParseLinks(raw)
}
if column, ok := data["scope"].(map[string]any); ok {
data["scope"] = spantypes.NewScope(column)
}
}
// numericAsFloat converts numeric types to float64 efficiently.

View File

@@ -95,9 +95,6 @@ func jsonColumn(paths map[string]any) telemetrystoretypes.JSONValue {
func TestUnwrapVariant(t *testing.T) {
jsonCol := jsonColumn(map[string]any{"level": "error", "attrs.code": int64(500)})
nullCol := jsonColumn(map[string]any{"level": "error", "absent": chcol.Variant{}})
scopeAttrs := chcol.NewJSON()
scopeAttrs.SetValueAtPath("lib.lang", "go")
scopeCol := jsonColumn(map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": scopeAttrs})
testCases := []struct {
name string
@@ -111,7 +108,6 @@ func TestUnwrapVariant(t *testing.T) {
{name: "JSONColumnNested", column: "body_v2", input: jsonCol, want: map[string]any{"level": "error", "attrs": map[string]any{"code": float64(500)}}},
{name: "AttributesColumnFlat", column: "attributes", input: jsonCol, want: map[string]any{"level": "error", "attrs.code": float64(500)}},
{name: "AttributesColumnDropsNullPaths", column: "attributes", input: nullCol, want: map[string]any{"level": "error"}},
{name: "ScopeColumnFlat", column: "scope", input: scopeCol, want: map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}}},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -136,32 +132,6 @@ func TestMergeSpanAttributeColumns_EmptyEventsAndLinks(t *testing.T) {
}
}
func TestMergeSpanAttributeColumns_Scope(t *testing.T) {
testCases := []struct {
name string
scope map[string]any
want spantypes.Scope
}{
{
name: "SetFields_Kept",
scope: map[string]any{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}},
want: spantypes.Scope{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go"}},
},
{
name: "UnsetDeclaredPaths_Empty",
scope: map[string]any{"name": "", "version": "", "attributes": map[string]any{}},
want: spantypes.Scope{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
data := map[string]any{"scope": testCase.scope}
mergeSpanAttributeColumns(data)
assert.Equal(t, testCase.want, data["scope"])
})
}
}
// readAsRaw decodes the JSON column into a flat bag of dotted keys before this runs; mergeSpanAttributeColumns
// copies that bag and overlays the legacy string/number/bool maps, which win on collision.
func TestMergeSpanAttributeColumns_JSONColumn(t *testing.T) {

View File

@@ -45,7 +45,7 @@ SELECT timestamp AS __SELECT_KEY_0_timestamp, trace_id AS __SELECT_KEY_1_trace_i
http_url AS __SELECT_KEY_17_http_url, external_http_method AS __SELECT_KEY_18_external_http_method,
http_method AS __SELECT_KEY_19_http_method, http_host AS __SELECT_KEY_20_http_host, db_name AS __SELECT_KEY_21_db_name,
db_operation AS __SELECT_KEY_22_db_operation, has_error AS __SELECT_KEY_23_has_error, is_remote AS __SELECT_KEY_24_is_remote,
attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope
attributes_string, attributes_number, attributes_bool, resources_string, attributes
FROM signoz_traces.distributed_signoz_index_v3
WHERE trace_id GLOBAL IN (SELECT trace_id FROM __trace_scope)
AND (((mapContains(attributes_string, 'gen_ai.request.model')

View File

@@ -676,7 +676,7 @@ SELECT timestamp AS __SELECT_KEY_0_timestamp, trace_id AS __SELECT_KEY_1_trace_i
http_url AS __SELECT_KEY_17_http_url, external_http_method AS __SELECT_KEY_18_external_http_method,
http_method AS __SELECT_KEY_19_http_method, http_host AS __SELECT_KEY_20_http_host, db_name AS __SELECT_KEY_21_db_name,
db_operation AS __SELECT_KEY_22_db_operation, has_error AS __SELECT_KEY_23_has_error, is_remote AS __SELECT_KEY_24_is_remote,
attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope
attributes_string, attributes_number, attributes_bool, resources_string, attributes
FROM signoz_traces.distributed_signoz_index_v3
WHERE (((mapContains(attributes_string, 'gen_ai.request.model')
OR mapContains(attributes_string, 'gen_ai.tool.name')

View File

@@ -581,7 +581,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY multiIf(mapContains(attributes_string, 'user.id'), attributes_string['user.id'], NULL) desc LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY multiIf(mapContains(attributes_string, 'user.id'), attributes_string['user.id'], NULL) desc LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -837,7 +837,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -879,7 +879,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
}},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp asc LIMIT ?",
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp asc LIMIT ?",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -906,7 +906,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String <> '') AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "SELECT timestamp AS `__SELECT_KEY_0_timestamp`, trace_id AS `__SELECT_KEY_1_trace_id`, span_id AS `__SELECT_KEY_2_span_id`, trace_state AS `__SELECT_KEY_3_trace_state`, parent_span_id AS `__SELECT_KEY_4_parent_span_id`, flags AS `__SELECT_KEY_5_flags`, name AS `__SELECT_KEY_6_name`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, duration_nano AS `__SELECT_KEY_9_duration_nano`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String <> '') AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1.0.0", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
},

View File

@@ -124,7 +124,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
},
expectedErr: nil,
@@ -161,7 +161,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
},
expectedErr: nil,
@@ -198,7 +198,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
},
expectedErr: nil,
@@ -235,7 +235,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -400,7 +400,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
},
expectedErr: nil,
@@ -434,7 +434,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -476,7 +476,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes, scope FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `__SELECT_KEY_3_trace_state`, flags AS `__SELECT_KEY_5_flags`, kind AS `__SELECT_KEY_7_kind`, kind_string AS `__SELECT_KEY_8_kind_string`, status_code AS `__SELECT_KEY_10_status_code`, status_message AS `__SELECT_KEY_11_status_message`, status_code_string AS `__SELECT_KEY_12_status_code_string`, events AS `__SELECT_KEY_13_events`, links AS `__SELECT_KEY_14_links`, response_status_code AS `__SELECT_KEY_15_response_status_code`, external_http_url AS `__SELECT_KEY_16_external_http_url`, http_url AS `__SELECT_KEY_17_http_url`, external_http_method AS `__SELECT_KEY_18_external_http_method`, http_method AS `__SELECT_KEY_19_http_method`, http_host AS `__SELECT_KEY_20_http_host`, db_name AS `__SELECT_KEY_21_db_name`, db_operation AS `__SELECT_KEY_22_db_operation`, has_error AS `__SELECT_KEY_23_has_error`, is_remote AS `__SELECT_KEY_24_is_remote`, attributes_string, attributes_number, attributes_bool, resources_string, attributes FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,

View File

@@ -46,7 +46,6 @@ const (
SpanAttributesColumn = "attributes"
SpanAttributesPromotedColumn = "attributes_promoted"
SpanResourcesStringColumn = "resources_string"
SpanScopeColumn = "scope"
)
var (
@@ -456,7 +455,6 @@ var (
SpanAttributesBoolColumn,
SpanResourcesStringColumn,
SpanAttributesColumn,
SpanScopeColumn,
}
DefaultFields = map[string]telemetrytypes.TelemetryFieldKey{

View File

@@ -17,7 +17,6 @@ type FlamegraphSpan struct {
Events []Event `json:"event" required:"true" nullable:"false"`
Attributes map[string]any `json:"attributes" required:"true" nullable:"false"`
Resource map[string]string `json:"resource" required:"true" nullable:"false"`
Scope Scope `json:"scope" required:"true" nullable:"false"`
Children []*FlamegraphSpan `json:"-"` // internal tree use only
}
@@ -67,13 +66,11 @@ func NewFlamegraphSpanFromStorable(s *StorableSpan, level int64, selectFields []
Events: s.UnmarshalledEvents(),
Attributes: make(map[string]any),
Resource: make(map[string]string),
Scope: Scope{},
}
if len(selectFields) == 0 {
return span
}
var attributes map[string]any
var scopeFields []string
for _, field := range selectFields {
switch field.FieldContext {
case telemetrytypes.FieldContextResource:
@@ -87,13 +84,8 @@ func NewFlamegraphSpanFromStorable(s *StorableSpan, level int64, selectFields []
if v, ok := attributes[field.Name]; ok {
span.Attributes[field.Name] = v
}
case telemetrytypes.FieldContextScope:
scopeFields = append(scopeFields, field.Name)
}
}
if len(scopeFields) > 0 {
span.Scope = NewSelectedScope(s.Scope.Flatten(), scopeFields)
}
return span
}
@@ -104,7 +96,6 @@ func NewMissingParentFlamegraphSpan(node *FlamegraphSpan) *FlamegraphSpan {
Timestamp: node.Timestamp,
DurationNano: node.DurationNano,
Events: []Event{},
Scope: Scope{},
Children: []*FlamegraphSpan{node},
}
}

View File

@@ -1,11 +1,9 @@
package spantypes
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -17,7 +15,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
selectFields []telemetrytypes.TelemetryFieldKey
wantAttrs map[string]any
wantResource map[string]string
wantScope Scope
}{
{
name: "JSONColumn_ResolvesDottedPaths",
@@ -29,7 +26,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{"http.route": "/orders", "http.retry.count": float64(2)},
wantResource: map[string]string{},
wantScope: Scope{},
},
{
name: "LegacyMaps_Resolve",
@@ -41,7 +37,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{"http.route": "/cart", "cache.hit": true},
wantResource: map[string]string{},
wantScope: Scope{},
},
{
name: "ResourceFields_EmptyValueSkipped",
@@ -52,30 +47,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
},
wantAttrs: map[string]any{},
wantResource: map[string]string{"service.name": "checkout"},
wantScope: Scope{},
},
{
name: "ScopeFields_DeclaredPathsAndAttributes",
span: StorableSpan{Scope: scopeColumn("io.opentelemetry.http", "1.2.0", map[string]any{"lib.lang": "go", "team": "payments"})},
selectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "scope.name", FieldContext: telemetrytypes.FieldContextScope},
{Name: "lib.lang", FieldContext: telemetrytypes.FieldContextScope},
{Name: "missing", FieldContext: telemetrytypes.FieldContextScope},
},
wantAttrs: map[string]any{},
wantResource: map[string]string{},
wantScope: Scope{"name": "io.opentelemetry.http", "attributes": map[string]any{"lib.lang": "go"}},
},
{
name: "ScopeFields_UnsetSelection_Empty",
span: StorableSpan{Scope: scopeColumn("io.opentelemetry.grpc", "", map[string]any{"rpc.system": "grpc"})},
selectFields: []telemetrytypes.TelemetryFieldKey{
{Name: "scope.version", FieldContext: telemetrytypes.FieldContextScope},
{Name: "absent", FieldContext: telemetrytypes.FieldContextScope},
},
wantAttrs: map[string]any{},
wantResource: map[string]string{},
wantScope: Scope{},
},
}
for _, testCase := range testCases {
@@ -83,15 +54,6 @@ func TestNewFlamegraphSpanFromStorable_SelectFields(t *testing.T) {
span := NewFlamegraphSpanFromStorable(&testCase.span, 0, testCase.selectFields)
assert.Equal(t, testCase.wantAttrs, span.Attributes)
assert.Equal(t, testCase.wantResource, span.Resource)
assert.Equal(t, testCase.wantScope, span.Scope)
})
}
}
func TestNewMissingParentFlamegraphSpan_EmptyScope(t *testing.T) {
span := NewMissingParentFlamegraphSpan(&FlamegraphSpan{SpanID: "child", ParentSpanID: "parent"})
raw, err := json.Marshal(span)
require.NoError(t, err)
assert.Contains(t, string(raw), `"scope":{}`)
}

View File

@@ -1,72 +0,0 @@
package spantypes
import (
"strings"
"github.com/swaggest/jsonschema-go"
)
const (
scopeName = "name"
scopeVersion = "version"
scopeAttributes = "attributes"
)
var _ jsonschema.Preparer = Scope{}
// Scope is a span's instrumentation scope holding only the fields that are set: name, version and
// attributes, a flat bag of dotted keys like the span attributes.
type Scope map[string]any
// NewScope builds a Scope from the flattened scope column, where an unset declared path reads back
// as its zero value (name and version as empty strings).
func NewScope(column map[string]any) Scope {
scope := Scope{}
if name, _ := column[scopeName].(string); name != "" {
scope[scopeName] = name
}
if version, _ := column[scopeVersion].(string); version != "" {
scope[scopeVersion] = version
}
if attributes, _ := column[scopeAttributes].(map[string]any); len(attributes) > 0 {
scope[scopeAttributes] = attributes
}
return scope
}
// NewSelectedScope builds a Scope with only the selected fields from the flattened scope column:
// scope.name and scope.version select the declared paths, any other name a scope attribute.
func NewSelectedScope(column map[string]any, names []string) Scope {
attributes, _ := column[scopeAttributes].(map[string]any)
selected := map[string]any{}
selectedAttributes := map[string]any{}
for _, name := range names {
switch name {
case "scope.name":
selected[scopeName] = column[scopeName]
case "scope.version":
selected[scopeVersion] = column[scopeVersion]
default:
name = strings.TrimPrefix(name, "attribute.")
if v, ok := attributes[name]; ok {
selectedAttributes[name] = v
}
}
}
selected[scopeAttributes] = selectedAttributes
return NewScope(selected)
}
// PrepareJSONSchema types the map's fields. Scope is a map rather than a struct so unset fields are
// left out instead of sent empty; omitempty would do that but decodes back with nil fields.
func (Scope) PrepareJSONSchema(schema *jsonschema.Schema) error {
attributes := jsonschema.Schema{}
attributes.WithType(jsonschema.Object.Type())
attributes.WithAdditionalProperties(jsonschema.SchemaOrBool{TypeObject: &jsonschema.Schema{}})
schema.AdditionalProperties = nil
schema.WithPropertiesItem(scopeName, jsonschema.String.ToSchemaOrBool())
schema.WithPropertiesItem(scopeVersion, jsonschema.String.ToSchemaOrBool())
schema.WithPropertiesItem(scopeAttributes, attributes.ToSchemaOrBool())
return nil
}

View File

@@ -1,58 +0,0 @@
package spantypes
import (
"testing"
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
"github.com/stretchr/testify/assert"
"github.com/SigNoz/signoz/pkg/types/telemetrystoretypes"
)
// scopeColumn mirrors the driver's scan of the scope column: declared String paths as plain strings and the
// declared attributes path as a nested *chcol.JSON.
func scopeColumn(name, version string, attributes map[string]any) telemetrystoretypes.JSONValue {
j := chcol.NewJSON()
j.SetValueAtPath("name", name)
j.SetValueAtPath("version", version)
attrs := chcol.NewJSON()
for path, value := range attributes {
attrs.SetValueAtPath(path, value)
}
j.SetValueAtPath("attributes", attrs)
return telemetrystoretypes.NewJSONValue(*j)
}
func TestNewScope(t *testing.T) {
testCases := []struct {
name string
column telemetrystoretypes.JSONValue
want Scope
}{
{
name: "AttributesStayDotted",
column: scopeColumn("io.opentelemetry.http", "1.2.0", map[string]any{"lib.lang": "go", "lib.lang.version": "1.22", "retry": int64(3)}),
want: Scope{"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": map[string]any{"lib.lang": "go", "lib.lang.version": "1.22", "retry": float64(3)}},
},
{
name: "DropsNullAttributePaths_UnsetVersion",
column: scopeColumn("checkout", "", map[string]any{"team": "payments", "absent": chcol.Variant{}}),
want: Scope{"name": "checkout", "attributes": map[string]any{"team": "payments"}},
},
{
name: "UnsetDeclaredPaths_Empty",
column: scopeColumn("", "", nil),
want: Scope{},
},
{
name: "EmptyColumn",
column: telemetrystoretypes.JSONValue{},
want: Scope{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, NewScope(testCase.column.Flatten()))
})
}
}

View File

@@ -64,7 +64,6 @@ type WaterfallSpan struct {
Name string `json:"name"`
ParentSpanID string `json:"parent_span_id"`
Resource map[string]string `json:"resource"`
Scope Scope `json:"scope"`
SpanID string `json:"span_id"`
TimeUnix uint64 `json:"time_unix"`
TraceID string `json:"trace_id"`
@@ -114,7 +113,6 @@ type StorableSpan struct {
AttributesBool map[string]bool `ch:"attributes_bool"`
AttributesJSON telemetrystoretypes.JSONValue `ch:"attributes"`
ResourcesString map[string]string `ch:"resources_string"`
Scope telemetrystoretypes.JSONValue `ch:"scope"`
Events []string `ch:"events"`
StatusMessage string `ch:"status_message"`
StatusCodeString string `ch:"status_code_string"`
@@ -155,7 +153,6 @@ func (item *MinimalSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
HasError: item.HasError,
ServiceName: item.ServiceName,
Resource: map[string]string{"service.name": item.ServiceName},
Scope: Scope{},
Children: make([]*WaterfallSpan, 0),
Attributes: make(map[string]any),
Events: make([]Event, 0),
@@ -185,7 +182,6 @@ func NewMissingWaterfallSpan(spanID, traceID string, timeUnixNano, durationNano
Children: make([]*WaterfallSpan, 0),
Attributes: make(map[string]any),
Resource: make(map[string]string),
Scope: Scope{},
}
}
@@ -340,7 +336,6 @@ func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan {
Name: item.Name,
ParentSpanID: item.ParentSpanID,
Resource: resources,
Scope: NewScope(item.Scope.Flatten()),
ResponseStatusCode: item.ResponseStatusCode,
SpanID: item.SpanID,
StatusCode: item.StatusCode,

View File

@@ -39,7 +39,7 @@ func (v JSONValue) Flatten() map[string]any {
if isNull(value) {
continue
}
flat[path] = flatObject(value)
flat[path] = value
}
return unwrap(flat)
}
@@ -57,15 +57,6 @@ func (v JSONValue) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Flatten())
}
// flatObject renders a declared JSON sub-path, which the driver scans as a nested chcol.JSON (such as
// scope.attributes), as a flat attribute bag.
func flatObject(value any) any {
if object, ok := value.(*chcol.JSON); ok && object != nil {
return JSONValue{column: *object}
}
return value
}
func isNull(value any) bool {
if value == nil {
return true

261
tests/e2e/drivers/query-builder.ts generated Normal file
View File

@@ -0,0 +1,261 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
import {
expect,
type Locator,
type Page,
type Request,
} from '@playwright/test';
import { watchConsole } from '../helpers/common';
import { isQueryRange } from '../helpers/query-range-mock';
import { dismissQuickFiltersAnnouncement } from '../helpers/quick-filters';
import { driver, kit } from './kit';
import { EXPLORER, RELATIVE_TIME } from './query-builder/constants';
import {
addOnsAt,
exactly,
metricAt,
openSelect,
} from './query-builder/controls';
import { afterRender, editorAt } from './query-builder/editor';
import type { QueryBuilderDriver, V5Body } from './query-builder/types';
function queryBuilderDriver(page: Page): QueryBuilderDriver {
const bodies: V5Body[] = [];
page.on('request', (request: Request) => {
if (isQueryRange(request.url()) && request.method() === 'POST') {
bodies.push(request.postDataJSON() as V5Body);
}
});
const consoleWatch = watchConsole(page, {
// ANTLR reports every partial expression on console.error, prefixed
// with its position (`line 1:14 ...`).
ignore: ['line 1:', 'line 2:', 'line 3:'],
});
// Waits on a request the action started: a response to one already in
// flight (e.g. after a structural edit) would end the wait too early.
const firedBy = async (action: () => Promise<void>): Promise<V5Body[]> => {
const before = bodies.length;
const answered = page
.waitForRequest((r) => isQueryRange(r.url()) && r.method() === 'POST')
.then((request) => request.response());
await action();
await answered;
return bodies.slice(before);
};
const qb: QueryBuilderDriver = {
async openExplorer(path, { query, raw, view } = {}) {
await dismissQuickFiltersAnnouncement(page);
const params = [`relativeTime=${RELATIVE_TIME}`];
if (view) {
params.push(`panelTypes=${encodeURIComponent(JSON.stringify(view))}`);
}
const composite =
raw ??
(query === undefined
? undefined
: encodeURIComponent(JSON.stringify(query)));
if (composite !== undefined) {
params.push(`compositeQuery=${composite}`);
}
const url = `${path}?${params.join('&')}`;
// The metrics explorer runs nothing until a metric is chosen; its
// settle point is the metric list instead.
if (path === EXPLORER.metrics) {
await Promise.all([
page.waitForResponse((r) => r.url().includes('/api/v2/metrics')),
page.goto(url),
]);
await expect(qb.query('A').filter.content()).toBeVisible();
return [];
}
const fired = await firedBy(async () => {
await page.goto(url);
});
await expect(qb.query('A').filter.content()).toBeVisible();
return fired;
},
query(name) {
const root = (): Locator => page.getByTestId(`query-${name}`);
// Index-suffixed testids follow screen position, not the query's
// name (A, C, B after a delete and re-add): match them in the block.
const signalSelect = (): Locator =>
root().locator('[data-testid^="query-data-source-selector-"]');
return {
root,
filter: editorAt(page, () =>
root().getByTestId('query-where-clause-editor'),
),
filterStatus: () => root().getByTestId('query-validation-status'),
aggregation: editorAt(page, () =>
root().getByTestId('query-aggregation-editor'),
),
step: () => root().getByTestId('input-Seconds'),
metric: metricAt(page, root),
addOns: addOnsAt(page, root),
actions: () => root().getByTestId('query-actions'),
menuItem: (action) =>
page.getByTestId(`query-actions-item-${action}-query`),
async clone() {
await root().getByTestId('query-actions').click();
await page.getByTestId('query-actions-item-clone-query').click();
await afterRender(page);
},
async delete() {
await root().getByTestId('query-actions').click();
await page.getByTestId('query-actions-item-delete-query').click();
await expect(root()).toHaveCount(0);
await afterRender(page);
},
async toggleVisibility() {
await root().getByTestId('qb-entity-visibility').click();
// The icon is the only visible change and carries no state.
await afterRender(page);
},
collapse: () => root().getByTestId('qb-entity-collapse').click(),
signal: () => signalSelect().locator('.ant-select-selection-item'),
async setSignal(label) {
const options = await openSelect(page, signalSelect());
await options.filter({ hasText: exactly(label) }).click();
await expect(this.signal()).toHaveText(label);
},
};
},
tab: (queryType) => page.getByTestId(`query-type-tab-${queryType}`),
promql: () => page.getByTestId('promql-query-input').first(),
async addQuery(name) {
await page.getByTestId('add-new-query-button').click();
const block = qb.query(name);
await expect(block.filter.content()).toBeVisible();
return block;
},
queryNames: () =>
page.locator('[data-testid^="query-"]').evaluateAll((nodes) =>
nodes
.map((node) => node.getAttribute('data-testid') ?? '')
.filter((id) => /^query-[A-Z]$/.test(id))
.map((id) => id.slice('query-'.length)),
),
formula(name) {
const root = (): Locator => page.getByTestId(`formula-${name}`);
return {
root,
expression: () => root().getByTestId('formula-expression'),
legend: () => root().getByTestId('formula-legend'),
async delete() {
await root().getByTestId('qb-entity-delete').click();
await expect(root()).toHaveCount(0);
// The block leaves the DOM before the editor's draft follows.
await afterRender(page);
},
};
},
async addFormula(name) {
await page.getByTestId('add-formula-button').click();
const formula = qb.formula(name);
await expect(formula.expression()).toBeVisible();
return formula;
},
traceOperator: {
root: () => page.getByTestId('trace-operator'),
// No testid; the label is followed by a "Beta" tag inside the button.
addButton: () =>
page.getByRole('button').filter({ hasText: /^Add Trace Matching/ }),
async add() {
await qb.traceOperator.addButton().click();
await expect(qb.traceOperator.editor.content()).toBeVisible();
},
async remove() {
// The only direct button child: the trash icon, no testid.
await qb.traceOperator.root().locator(':scope > button').click();
await expect(qb.traceOperator.root()).toHaveCount(0);
},
editor: editorAt(page, () => page.getByTestId('trace-operator-editor')),
},
async addToDashboard(title) {
await page.getByTestId('explorer-add-to-dashboard').click();
const dialog = page.getByTestId('export-panel-dialog');
await dialog
.getByTestId('export-dashboard-select')
.locator('input')
.fill(title);
await dialog.locator(`.ant-select-item-option[title="${title}"]`).click();
await dialog.getByTestId('export-panel-export').click();
await page.waitForURL(/\/panel\/new\?/);
await expect(qb.query('A').filter.content()).toBeVisible();
},
runButton: () => page.locator('[data-run-query-btn]').first(),
// No testid on the container.
logsResults: () => page.locator('.logs-explorer-views'),
async run() {
await afterRender(page);
return firedBy(() => qb.runButton().click());
},
async rerunUntil(read, expected) {
await expect(async () => {
await expect(qb.runButton()).toHaveText(/Run Query/);
await afterRender(page);
await qb.runButton().click();
await expect.poll(read, { timeout: 3_000 }).toEqual(expected);
}).toPass({ timeout: 25_000 });
},
async runShortcut() {
await afterRender(page);
return firedBy(() => page.keyboard.press('ControlOrMeta+Enter'));
},
quickFilter(title, value) {
const checkbox = (): Locator =>
page.getByTestId(`checkbox-${title}-${value}`);
return {
checkbox,
toggle: () => firedBy(() => checkbox().click()),
};
},
spec(fired, name) {
for (const body of fired) {
const match = body.compositeQuery?.queries?.find(
(q) => q.spec?.name === name,
);
if (match) {
return match.spec;
}
}
throw new Error(`no query ${name} in ${fired.length} query_range bodies`);
},
sent: () => bodies,
latest: (name) =>
[...bodies]
.reverse()
.flatMap((body) => body.compositeQuery?.queries ?? [])
.find((query) => query.spec?.name === name)?.spec,
urlQuery() {
const raw = new URL(page.url()).searchParams.get('compositeQuery');
if (!raw) {
return null;
}
// The app encodes before URLSearchParams encodes again.
return JSON.parse(decodeURIComponent(raw)) as Record<string, unknown>;
},
console: consoleWatch,
};
return qb;
}
/** Layers `qb` onto another host's test, e.g. the alert-rule fixtures. */
export const withQueryBuilder = driver('qb', ({ page }) =>
queryBuilderDriver(page),
);
export const test = withQueryBuilder(kit);
export { expect } from './kit';
export * from './query-builder/constants';
export * from './query-builder/types';
export { reduceToLabel } from './query-builder/controls';
export { withMetricTypes } from './query-builder/seed';

View File

@@ -0,0 +1,17 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
export const EXPLORER = {
logs: '/logs/logs-explorer',
traces: '/traces-explorer',
metrics: '/metrics-explorer/explorer',
} as const;
/** A monotonic Sum in the golden dataset. */
export const SUM_METRIC = 'signoz_calls_total';
/** Another golden Sum, for switching between metrics of one type. */
export const OTHER_SUM_METRIC = 'signoz_latency_count';
/** Inside the golden dataset's window. */
export const RELATIVE_TIME = '6h';

View File

@@ -0,0 +1,136 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
import { expect, type Locator, type Page } from '@playwright/test';
import { ownDropdown } from '../../helpers/alert-forms/shared';
import { editorAt } from './editor';
import type { AddOnField, AddOns, MetricControls } from './types';
/**
* Opens the antd select inside `select` and returns its options. Scoped to the
* list the select owns: a previous select's list can still be mid-close.
*/
export async function openSelect(
page: Page,
select: Locator,
): Promise<Locator> {
await select.locator('.ant-select-selector').click();
const list = await ownDropdown(page, select);
await expect(list).toBeVisible();
return list.locator('.ant-select-item-option');
}
/** antd renders select options with trailing whitespace ("Sum "). */
export const exactly = (label: string): RegExp =>
new RegExp(`^\\s*${label}\\s*$`);
/** A reduce-to option or value by its operator: "Sum of values in timeframe". */
export const reduceToLabel = (operator: string): RegExp =>
new RegExp(`^\\s*${operator} of values`);
const ADD_ON_CONTENT: Record<AddOnField, string> = {
group_by: 'group-by-content',
having: 'having-content',
order_by: 'order-by-content',
limit: 'limit-content',
legend_format: 'legend-format-content',
reduce_to: 'reduce-to-content',
};
export function addOnsAt(page: Page, root: () => Locator): AddOns {
const content = (field: AddOnField): Locator =>
root().getByTestId(ADD_ON_CONTENT[field]);
const groupBy = (): Locator => root().getByTestId('group-by');
const addOns: AddOns = {
toggles: () => root().locator('[data-testid^="query-add-on-"]'),
async open(field) {
if (await content(field).isVisible()) {
return;
}
await root().getByTestId(`query-add-on-${field}`).click();
await expect(content(field)).toBeVisible();
},
content,
groupBy: {
async add(key) {
const options = await openSelect(page, groupBy());
await groupBy().locator('input').fill(key);
await options
.filter({ hasText: exactly(key) })
.first()
.click();
await page.keyboard.press('Escape');
await expect(
groupBy().locator('.ant-select-selection-item', { hasText: key }),
).toBeVisible();
},
async remove(key) {
await groupBy()
.locator('.ant-select-selection-item', { hasText: key })
.locator('.ant-select-selection-item-remove')
.click();
},
async values() {
const texts = await groupBy()
.locator('.ant-select-selection-item-content')
.allTextContents();
return texts.map((text) => text.trim());
},
},
having: editorAt(page, () => root().getByTestId('having-editor')),
limit: () => content('limit').getByTestId('input-Limit'),
legend: () => content('legend_format').getByTestId('input-Legend format'),
reduceTo: {
value: () =>
root().getByTestId('reduce-to').locator('.ant-select-selection-item'),
// Labels read "<Op> of values in timeframe"; matched on the operator.
async set(label) {
const options = await openSelect(page, root().getByTestId('reduce-to'));
await options.filter({ hasText: reduceToLabel(label) }).click();
await expect(addOns.reduceTo.value()).toHaveText(reduceToLabel(label));
},
},
};
return addOns;
}
export function metricAt(page: Page, root: () => Locator): MetricControls {
const picker = (): Locator =>
root().locator('[data-testid^="metric-name-selector-"]');
const time = (): Locator => root().getByTestId('metrics-time-aggregation');
const space = (): Locator => root().getByTestId('metrics-space-aggregation');
return {
async select(name) {
// An antd AutoComplete: the testid is on a wrapper.
const field = picker();
const options = await openSelect(page, field);
await field.locator('input').fill(name);
// Options carry the metric's type and unit next to its name.
await options.filter({ hasText: name }).first().click();
},
// The selection item, not the root: these selects render their list
// inside the root, so its text includes every option.
input: () => picker().locator('input'),
timeAggregation: () => time().locator('.ant-select-selection-item'),
spaceAggregation: () => space().locator('.ant-select-selection-item'),
async setTimeAggregation(label) {
const options = await openSelect(page, time());
await options.filter({ hasText: exactly(label) }).click();
await expect(this.timeAggregation()).toHaveText(label);
},
async setSpaceAggregation(label) {
const options = await openSelect(page, space());
await options.filter({ hasText: exactly(label) }).click();
await expect(this.spaceAggregation()).toHaveText(label);
},
async spaceOptions() {
const options = await openSelect(page, space());
const labels = await options.allTextContents();
await page.keyboard.press('Escape');
return labels.map((label) => label.trim());
},
};
}

View File

@@ -0,0 +1,77 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
import { expect, type Locator, type Page } from '@playwright/test';
import type { FilterEditor } from './types';
/** CodeMirror renders its placeholder inside the empty line. */
const EDITOR_DOC = async (root: Locator): Promise<string> => {
const lines = root.locator('.cm-content .cm-line');
const texts = await lines.allTextContents();
const placeholders = await lines.evaluateAll((nodes) =>
nodes.map((node) => node.querySelector('.cm-placeholder')?.textContent ?? ''),
);
return texts
.map((text, i) =>
placeholders[i] ? text.replace(placeholders[i], '') : text,
)
.join('\n');
};
export function editorAt(page: Page, root: () => Locator): FilterEditor {
const content = (): Locator => root().locator('.cm-content');
const editor: FilterEditor = {
root,
content,
async type(text) {
await editor.clear();
await page.keyboard.type(text);
},
async append(text) {
await content().click();
await page.keyboard.press('End');
await page.keyboard.type(text);
},
async clear() {
await content().click();
await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.press('Backspace');
await expect.poll(() => EDITOR_DOC(root())).toBe('');
},
press: (key) => page.keyboard.press(key),
expression: () => EDITOR_DOC(root()),
lineCount: () => root().locator('.cm-line'),
suggestions: () => page.locator('.cm-tooltip-autocomplete'),
async openSuggestions() {
await expect(async () => {
await page.keyboard.press('Control+Space');
await expect(editor.suggestions()).toBeVisible({ timeout: 2_000 });
}).toPass({ timeout: 15_000 });
},
suggestion: (label) =>
page
.locator('.cm-tooltip-autocomplete [role="option"]')
.filter({ hasText: label }),
};
return editor;
}
/**
* Two frames: an edit reaches React state on its next scheduler task, and an
* action in the same frame reads the previous render. A long render delays
* the frame itself, so this cannot pass before the commit.
*/
export async function afterRender(page: Page): Promise<void> {
await page.evaluate(
() =>
new Promise<void>((resolve) => {
// Runs in the browser; the e2e tsconfig has no DOM lib.
const { requestAnimationFrame: frame } = globalThis as unknown as {
requestAnimationFrame(this: void, callback: () => void): number;
};
frame(() => frame(() => resolve()));
}),
);
}

50
tests/e2e/drivers/query-builder/seed.ts generated Normal file
View File

@@ -0,0 +1,50 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
import { postToSeeder } from '../../helpers/alerts/seeding';
import { seeded } from '../kit';
import type { MetricTypes } from './types';
const HISTOGRAM_BOUNDS = ['1', '2', '4', '+Inf'];
/** Ten minutes of a gauge and a cumulative histogram, names unique per worker. */
export const withMetricTypes = seeded(
'metricTypes',
async ({ page, workerIndex }) => {
const suffix = `${workerIndex}_${Date.now()}`;
const types: MetricTypes = {
gauge: `e2e_qb_gauge_${suffix}`,
histogram: `e2e_qb_histogram_${suffix}`,
};
const now = Date.now();
const points: Record<string, unknown>[] = [];
for (let minute = 0; minute < 10; minute += 1) {
const timestamp = new Date(now - (10 - minute) * 60_000).toISOString();
points.push({
metric_name: types.gauge,
labels: { host: 'qb-host' },
timestamp,
value: 10 + minute,
type_: 'Gauge',
temporality: 'Unspecified',
is_monotonic: false,
});
HISTOGRAM_BOUNDS.forEach((le, bucket) => {
points.push({
metric_name: types.histogram,
labels: { le },
timestamp,
value: (minute + 1) * (bucket + 1),
type_: 'Histogram',
temporality: 'Cumulative',
is_monotonic: false,
});
});
}
await postToSeeder(page, '/telemetry/metrics', points);
return types;
},
);

225
tests/e2e/drivers/query-builder/types.ts generated Normal file
View File

@@ -0,0 +1,225 @@
/**
* AI-owned. Generated and maintained by coding agents alongside the specs that
* use it. Do not hand-edit: regenerate it with the spec that needs the change.
*/
import type { Locator } from '@playwright/test';
import type { ConsoleWatch } from '../../helpers/common';
/** The golden dataset has only Sums; these are seeded per worker. */
export interface MetricTypes {
gauge: string;
histogram: string;
}
/** One entry of a v5 `query_range` body's `compositeQuery.queries`. */
export interface V5Query {
type: string;
spec: {
name?: string;
signal?: string;
disabled?: boolean;
filter?: { expression?: string };
aggregations?: {
expression?: string;
metricName?: string;
timeAggregation?: string;
spaceAggregation?: string;
reduceTo?: string;
}[];
groupBy?: { name: string }[];
having?: { expression?: string };
order?: { key: { name: string }; direction: string }[];
limit?: number;
stepInterval?: number;
[key: string]: unknown;
};
}
export interface V5Body {
requestType: string;
compositeQuery: { queries: V5Query[] };
}
export interface FilterEditor {
root(): Locator;
/** The contenteditable CodeMirror surface. */
content(): Locator;
/** Replaces the expression by typing it key by key. */
type(text: string): Promise<void>;
/** Appends at the caret without clearing. */
append(text: string): Promise<void>;
clear(): Promise<void>;
press(key: string): Promise<void>;
/** The document text, placeholder excluded, lines joined with `\n`. */
expression(): Promise<string>;
lineCount(): Locator;
/** The open autocomplete popup. */
suggestions(): Locator;
/**
* Ctrl+Space: opens the popup at the caret. Typing opens it only once the
* debounced key fetch answers, which a loaded stack can delay past a test's
* next step.
*/
openSuggestions(): Promise<void>;
suggestion(label: string | RegExp): Locator;
}
export interface QueryBlock {
root(): Locator;
filter: FilterEditor;
/** The filter's status icon: shown only while unfocused and non-empty. */
filterStatus(): Locator;
aggregation: FilterEditor;
/** The aggregation's "every … Seconds" input. */
step(): Locator;
metric: MetricControls;
addOns: AddOns;
/** The "…" menu: offered only where several queries are allowed. */
actions(): Locator;
/** A row of the open "…" menu. */
menuItem(action: 'clone' | 'delete'): Locator;
clone(): Promise<void>;
delete(): Promise<void>;
toggleVisibility(): Promise<void>;
collapse(): Promise<void>;
/** The data-source dropdown's value; only on hosts that let it change. */
signal(): Locator;
setSignal(label: 'Logs' | 'Traces' | 'Metrics'): Promise<void>;
}
export interface FormulaBlock {
root(): Locator;
expression(): Locator;
legend(): Locator;
delete(): Promise<void>;
}
/** `query-add-on-<field>` toggle values: a documented DOM contract. */
export type AddOnField =
| 'group_by'
| 'having'
| 'order_by'
| 'limit'
| 'legend_format'
| 'reduce_to';
export interface AddOns {
/** The toggle row; its children name the add-ons on offer. */
toggles(): Locator;
/** Idempotent: leaves an already-open add-on open. */
open(field: AddOnField): Promise<void>;
content(field: AddOnField): Locator;
groupBy: {
/** Types `key` and picks the suggestion with that exact name. */
add(key: string): Promise<void>;
remove(key: string): Promise<void>;
/** The selected keys, in order. */
values(): Promise<string[]>;
};
having: FilterEditor;
limit(): Locator;
legend(): Locator;
reduceTo: {
/** The selected value. */
value(): Locator;
set(label: string): Promise<void>;
};
}
/** The metrics aggregation section: metric picker plus time/space selects. */
export interface MetricControls {
select(name: string): Promise<void>;
/** The metric name input. */
input(): Locator;
/** The selected value. Absent for histograms. */
timeAggregation(): Locator;
/** The selected value. */
spaceAggregation(): Locator;
setTimeAggregation(label: string): Promise<void>;
setSpaceAggregation(label: string): Promise<void>;
/** Labels offered by the space-aggregation select, as shown. */
spaceOptions(): Promise<string[]>;
}
export interface QuickFilterValue {
checkbox(): Locator;
/** Toggles the value; returns the query_range bodies the toggle fired. */
toggle(): Promise<V5Body[]>;
}
/** The explorer's tab, carried in the `panelTypes` param. */
export type ExplorerView = 'list' | 'graph' | 'table';
export interface OpenOptions {
/** Serialised the way the app writes it. */
query?: unknown;
/** A `compositeQuery` value sent as given, for malformed input. */
raw?: string;
view?: ExplorerView;
}
export interface QueryBuilderDriver {
/**
* Resolves once the page's own first query_range has answered: the explorer
* rewrites its URL and re-mounts the builder until then, which drops
* keystrokes. Returns the bodies that load fired.
*/
openExplorer(path: string, options?: OpenOptions): Promise<V5Body[]>;
query(name: string): QueryBlock;
/** Adds the next query (B, C, …) and waits for its block. */
addQuery(name: string): Promise<QueryBlock>;
/** Builder query names, top to bottom. */
queryNames(): Promise<string[]>;
/** Panel-editor query-type tab, by its queryType (`builder`, `promql`, …). */
tab(queryType: string): Locator;
promql(): Locator;
formula(name: string): FormulaBlock;
addFormula(name: string): Promise<FormulaBlock>;
traceOperator: {
root(): Locator;
/** "Add Trace Matching": offered on traces hosts that allow it. */
addButton(): Locator;
add(): Promise<void>;
remove(): Promise<void>;
editor: FilterEditor;
};
/**
* Explorer "Add to dashboard": picks the dashboard titled `title` and waits
* for the new-panel editor the export navigates to.
*/
addToDashboard(title: string): Promise<void>;
runButton(): Locator;
/** The logs explorer's results area, below the builder. */
logsResults(): Locator;
/** Clicks Run; returns every query_range body the click fired. */
run(): Promise<V5Body[]>;
/**
* Clicks Run until `read()` equals `expected`. For panel-editor structural
* edits, whose draft can lag the DOM: an early Run is a cache hit that
* sends nothing. Never clicks while the button is Cancel.
*/
rerunUntil(read: () => unknown, expected: unknown): Promise<void>;
/**
* Ctrl/Cmd+Enter from the focused field; returns the fired bodies. Waits two
* frames first: editor changes reach React state on its next scheduler task,
* and a keypress inside the same frame as the last keystroke reads the
* previous render — a gap no person can produce.
*/
runShortcut(): Promise<V5Body[]>;
/** A value in an explorer's checkbox quick filter, by its header title. */
quickFilter(title: string, value: string): QuickFilterValue;
/** The spec of query `name` in the first body that has it. */
spec(bodies: V5Body[], name: string): V5Query['spec'];
/** Every query_range body sent since the test started. */
sent(): V5Body[];
/**
* Query `name`'s spec in the newest body that has it, whatever sent it. The
* panel editor fetches on structural edits, so Run after one can be a cache
* hit that sends nothing: poll this rather than waiting on Run.
*/
latest(name: string): V5Query['spec'] | undefined;
/** The URL's `compositeQuery`, parsed. */
urlQuery(): Record<string, unknown> | null;
console: ConsoleWatch;
}

View File

@@ -85,8 +85,17 @@ export async function pickChannelByName(
channelName: string,
): Promise<void> {
const tagsBefore = await selectedTags(select).count();
await select.click();
await expect(select).toHaveClass(/ant-select-open/);
// A re-render of the condition section (e.g. after a query edit) can close
// the select as the click opens it; re-click only while it is still closed.
await expect(async () => {
const isOpen = await select.evaluate((node) =>
node.classList.contains('ant-select-open'),
);
if (!isOpen) {
await select.click();
}
await expect(select).toHaveClass(/ant-select-open/, { timeout: 2_000 });
}).toPass({ timeout: 15_000 });
// `fill` on the combobox input rather than `keyboard.type`: the query is a ~30
// character channel name and every keystroke re-runs antd's filter, so typing it

View File

@@ -15,7 +15,7 @@ import type {
// ─── Seeding telemetry ───────────────────────────────────────────────────
async function postToSeeder(
export async function postToSeeder(
page: Page,
path: string,
data: unknown,

View File

@@ -272,7 +272,9 @@ export async function holdQueryRange(
});
await page.route(QUERY_RANGE_GLOB, async (route) => {
await gate;
await fulfill(route, body);
// A cancelled run aborts its request while it is held; there is
// nothing left to answer.
await fulfill(route, body).catch(() => undefined);
});
return { release: () => release() };
}

View File

@@ -0,0 +1,153 @@
import {
expect,
EXPLORER,
SUM_METRIC,
test,
} from '../../drivers/query-builder';
const HAVING = ['count() > 10', 'count() IN [1, 2]', 'count() NOT IN [1, 2]'];
test.describe('Query builder — add-ons', () => {
for (const explorer of ['logs', 'traces'] as const) {
test(`TC-21 ${explorer} group by keeps order through removal and reload`, async ({
qb,
}) => {
await qb.openExplorer(EXPLORER[explorer], { view: 'graph' });
const addOns = qb.query('A').addOns;
await addOns.open('group_by');
await addOns.groupBy.add('service.name');
await addOns.groupBy.add('deployment.environment');
await addOns.groupBy.add('k8s.namespace.name');
await addOns.groupBy.remove('service.name');
const fired = await qb.run();
const expected = ['deployment.environment', 'k8s.namespace.name'];
expect(qb.spec(fired, 'A').groupBy?.map((key) => key.name)).toEqual(
expected,
);
const reloaded = await qb.openExplorer(EXPLORER[explorer], {
view: 'graph',
query: qb.urlQuery(),
});
expect(qb.spec(reloaded, 'A').groupBy?.map((key) => key.name)).toEqual(
expected,
);
expect(await qb.query('A').addOns.groupBy.values()).toEqual(expected);
});
}
test('TC-21 metrics group by offers the metric’s own attributes', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.metrics);
const query = qb.query('A');
await query.metric.select(SUM_METRIC);
await query.addOns.groupBy.add('operation');
const fired = await qb.run();
expect(qb.spec(fired, 'A').groupBy?.map((key) => key.name)).toEqual([
'operation',
]);
});
for (const having of HAVING) {
test(`TC-22 having "${having}" is sent verbatim and accepted`, async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
const addOns = qb.query('A').addOns;
await addOns.open('group_by');
await addOns.groupBy.add('service.name');
await addOns.open('having');
await addOns.having.type(having);
await addOns.having.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').having?.expression).toBe(having);
expect(qb.console.failedResponses).toEqual([]);
});
}
test('TC-23 the logs list orders by timestamp then id, traces by timestamp only', async ({
qb,
}) => {
const logs = await qb.openExplorer(EXPLORER.logs, { view: 'list' });
expect(qb.spec(logs, 'A').order?.map((o) => o.key.name)).toEqual([
'timestamp',
'id',
]);
const traces = await qb.openExplorer(EXPLORER.traces, { view: 'list' });
expect(qb.spec(traces, 'A').order?.map((o) => o.key.name)).toEqual([
'timestamp',
]);
});
test('TC-24 a positive limit is sent; empty and zero send none', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
const addOns = qb.query('A').addOns;
await addOns.open('group_by');
await addOns.groupBy.add('service.name');
await addOns.open('limit');
await addOns.limit().fill('10');
expect(qb.spec(await qb.run(), 'A').limit).toBe(10);
await addOns.limit().fill('');
expect(qb.spec(await qb.run(), 'A')).not.toHaveProperty('limit');
await addOns.limit().fill('3');
await qb.run();
await addOns.limit().fill('0');
expect(qb.spec(await qb.run(), 'A')).not.toHaveProperty('limit');
});
test('TC-24 a negative limit means no limit', async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
const addOns = qb.query('A').addOns;
await addOns.open('group_by');
await addOns.groupBy.add('service.name');
await addOns.open('limit');
await addOns.limit().fill('10');
expect(qb.spec(await qb.run(), 'A').limit).toBe(10);
await addOns.limit().fill('-5');
const fired = await qb.run();
expect(qb.spec(fired, 'A')).not.toHaveProperty('limit');
expect(qb.console.failedResponses).toEqual([]);
});
test('TC-25 the legend prefills from group by and a custom one is kept', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
const addOns = qb.query('A').addOns;
await addOns.open('group_by');
await addOns.groupBy.add('service.name');
await addOns.groupBy.add('deployment.environment');
await addOns.open('legend_format');
await expect(addOns.legend()).toHaveValue(
'service.name = {{service.name}}, deployment.environment = {{deployment.environment}}',
);
await addOns.legend().fill('{{service.name}} logs');
await addOns.groupBy.add('k8s.namespace.name');
await expect(addOns.legend()).toHaveValue('{{service.name}} logs');
expect(qb.spec(await qb.run(), 'A').legend).toBe('{{service.name}} logs');
});
test('TC-25 the legend opens empty without a group by', async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
await qb.query('A').addOns.open('legend_format');
await expect(qb.query('A').addOns.legend()).toHaveValue('');
});
});

View File

@@ -0,0 +1,80 @@
import { expect, EXPLORER, test } from '../../drivers/query-builder';
// One comma-separated term per entry in the request's `aggregations`.
const EXPRESSIONS: Record<string, string[]> = {
'a count': ['count()'],
'a distinct count': ['count_distinct(service.name)'],
'a percentile of a span field': ['p99(duration_nano)'],
'two aggregations': ['count()', 'p99(duration_nano)'],
};
test.describe('Query builder — logs and traces aggregations', () => {
for (const [label, terms] of Object.entries(EXPRESSIONS)) {
test(`TC-17 ${label} reaches the request`, async ({ qb }) => {
await qb.openExplorer(EXPLORER.traces, { view: 'graph' });
const aggregation = qb.query('A').aggregation;
await aggregation.type(terms.join(', '));
await aggregation.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').aggregations?.map((a) => a.expression)).toEqual(
terms,
);
});
}
test('TC-18 the step interval reaches the request', async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER.traces, { view: 'graph' });
await qb.query('A').step().fill('120');
await page.keyboard.press('Tab');
const fired = await qb.run();
expect(qb.spec(fired, 'A').stepInterval).toBe(120);
});
test('TC-30 a trace operator replaces per-query aggregation until removed', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.traces, { view: 'graph' });
await (await qb.addQuery('B')).filter.type("http.status_code = '500'");
await qb.query('B').filter.press('Escape');
await qb.traceOperator.add();
await qb.traceOperator.editor.type('A => B');
await qb.traceOperator.editor.press('Escape');
for (const name of ['A', 'B']) {
await expect(qb.query(name).aggregation.root()).toHaveCount(0);
await expect(qb.query(name).addOns.toggles()).toHaveCount(0);
}
const fired = await qb.run();
const operator = fired
.flatMap((body) => body.compositeQuery.queries)
.find((query) => query.type === 'builder_trace_operator');
expect(operator?.spec).toMatchObject({ expression: 'A => B' });
await qb.traceOperator.remove();
await expect(qb.query('A').aggregation.root()).toBeVisible();
await qb.rerunUntil(
() =>
qb
.sent()
.at(-1)
?.compositeQuery.queries.some(
(query) => query.type === 'builder_trace_operator',
),
false,
);
});
test('TC-30 the logs explorer offers no trace matching', async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
await expect(qb.query('A').root()).toBeVisible();
await expect(qb.traceOperator.addButton()).toHaveCount(0);
});
});

View File

@@ -0,0 +1,85 @@
import { withQueryBuilder } from '../../drivers/query-builder';
import {
expect,
test as alertRulesTest,
} from '../../fixtures/alerts/alert-rules';
import { AlertType } from '../../helpers/alert-forms/constants';
import { gotoCreateAlertV2 } from '../../helpers/alert-forms/navigation';
import {
selectThresholdChannel,
v2SaveButton,
} from '../../helpers/alert-forms/v2';
import { gotoAlertOverview } from '../../helpers/alerts/navigation';
const test = withQueryBuilder(alertRulesTest);
const FILTER_A = "http.method = 'GET'";
const FILTER_B = "http.status_code = '500'";
const TRACE_MATCH = 'A => B';
interface SavedRule {
condition?: {
compositeQuery?: {
queries?: {
type: string;
spec: {
name?: string;
expression?: string;
filter?: { expression?: string };
};
}[];
};
};
}
test.describe('Query builder — alerts', () => {
test('TC-06 a trace-matching traces alert saves and reopens intact', async ({
authedPage: page,
alertChannel,
ownedRules,
qb,
}) => {
await gotoCreateAlertV2(page, { alertType: AlertType.TRACES });
await page.getByTestId('alert-name-input').fill(`e2e-qb-tc06-${Date.now()}`);
await qb.query('A').filter.type(FILTER_A);
await qb.query('A').filter.press('Escape');
const b = await qb.addQuery('B');
await b.filter.type(FILTER_B);
await b.filter.press('Escape');
await qb.traceOperator.add();
await qb.traceOperator.editor.type(TRACE_MATCH);
await qb.traceOperator.editor.press('Escape');
// Blur first: the editor's blur re-renders the condition section, which
// otherwise closes the channel select as its click opens it.
await qb.traceOperator.editor.content().blur();
await selectThresholdChannel(page, 0, alertChannel.name);
const [response] = await Promise.all([
page.waitForResponse(
(r) => r.url().includes('/api/v2/rules') && r.request().method() === 'POST',
),
v2SaveButton(page).click(),
]);
await ownedRules.register(response);
const saved = response.request().postDataJSON() as SavedRule;
const queries = saved.condition?.compositeQuery?.queries ?? [];
expect(queries).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'builder_trace_operator',
spec: expect.objectContaining({ expression: TRACE_MATCH }),
}),
]),
);
const { data } = (await response.json()) as { data: { id: string } };
await gotoAlertOverview(page, String(data.id));
await expect.poll(() => qb.query('A').filter.expression()).toBe(FILTER_A);
await expect.poll(() => qb.query('B').filter.expression()).toBe(FILTER_B);
await expect
.poll(() => qb.traceOperator.editor.expression())
.toBe(TRACE_MATCH);
});
});

View File

@@ -0,0 +1,72 @@
import { emptyDashboard, test as editorTest } from '../../drivers/panel-editor';
import {
expect,
EXPLORER,
withQueryBuilder,
} from '../../drivers/query-builder';
const test = withQueryBuilder(editorTest);
const FILTER = "severity_text = 'ERROR'";
const AGGREGATION = 'count_distinct(service.name)';
interface StoredQuery {
spec: {
filter?: { expression?: string };
aggregations?: { expression?: string }[];
};
}
/** Where a non-List panel keeps its builder queries in the v2 dashboard spec. */
interface StoredPanel {
spec: {
queries: {
spec: { plugin: { kind: string; spec: { queries: StoredQuery[] } } };
}[];
};
}
test.describe('Query builder — explorer to dashboard', () => {
test('TC-05 a logs explorer query survives add to dashboard, save and reopen', async ({
dashboards,
editor,
qb,
}) => {
const dashboard = emptyDashboard();
const title = dashboard.spec.display.name;
const id = await dashboards.seed(dashboard);
await qb.openExplorer(EXPLORER.logs, { view: 'graph' });
await qb.query('A').filter.type(FILTER);
await qb.query('A').filter.press('Escape');
await qb.query('A').aggregation.type(AGGREGATION);
await qb.query('A').aggregation.press('Escape');
await qb.run();
await qb.addToDashboard(title);
await expect.poll(() => qb.query('A').filter.expression()).toBe(FILTER);
await expect
.poll(() => qb.query('A').aggregation.expression())
.toBe(AGGREGATION);
await editor.save();
// The shared spec types model only bare BuilderQuery plugins.
const panels = (await dashboards.get(id)).spec.panels as unknown as Record<
string,
StoredPanel
>;
const [panelId] = Object.keys(panels);
const plugin = panels[panelId].spec.queries[0].spec.plugin;
expect(plugin.kind).toBe('signoz/CompositeQuery');
expect(plugin.spec.queries[0].spec).toMatchObject({
filter: { expression: FILTER },
aggregations: [{ expression: AGGREGATION }],
});
await dashboards.openEditor(id, panelId);
await expect.poll(() => qb.query('A').filter.expression()).toBe(FILTER);
await expect
.poll(() => qb.query('A').aggregation.expression())
.toBe(AGGREGATION);
});
});

View File

@@ -0,0 +1,284 @@
import { expect, EXPLORER, test } from '../../drivers/query-builder';
// Typing is real keystrokes: the regressions here (caret jumps, swallowed
// Enter, the run shortcut) only show up through the keyboard path.
const RECENT_KEPT = "service.name = 'frontend' AND severity_text = 'WARN'";
const RECENT_REMOVED = 'logger.name EXISTS';
const COMPOUND: Record<string, string> = {
'NOT over a parenthesised OR':
"NOT (severity_text = 'INFO' OR severity_text = 'WARN') AND service.name = 'frontend'",
'nested groups':
"(service.name = 'frontend' AND (severity_text = 'ERROR' OR severity_text = 'WARN'))",
'NOT IN with EXISTS':
"service.name NOT IN ['frontend', 'adservice'] AND logger.name EXISTS",
};
const queryWithFilter = (expression: string): unknown => ({
queryType: 'builder',
builder: {
queryFormulas: [],
queryData: [
{
queryName: 'A',
dataSource: 'logs',
expression: 'A',
filter: { expression },
aggregations: [{ expression: 'count()' }],
},
],
},
});
const VERBATIM: Record<string, string> = {
'an escaped quote': "logger.name = 'it\\'s'",
'a double-quoted string': 'logger.name = "double quoted"',
'a quoted numeric string': "logger.name = '123'",
'a bare number': 'logger.name = 123',
'a boolean': 'logger.name = true',
'spaces and a backslash': "logger.name = 'a b\\\\c'",
};
test.describe('Query builder — filter editor', () => {
test('TC-07 key, operator and value suggestions build the expression', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
await filter.type('severity_te');
await filter.openSuggestions();
await filter.suggestion('severity_text').first().click();
await expect.poll(() => filter.expression()).toMatch(/^severity_text\s*$/);
await filter.openSuggestions();
await expect(filter.suggestion(/^=/).first()).toBeVisible();
await filter.append('= ');
await filter.openSuggestions();
await filter.suggestion('ERROR').first().click();
await expect
.poll(() => filter.expression())
.toMatch(/^severity_text = 'ERROR'\s*$/);
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression?.trim()).toBe(
"severity_text = 'ERROR'",
);
});
for (const [label, expression] of Object.entries(VERBATIM)) {
test(`TC-08 ${label} is sent and restored verbatim`, async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
await filter.type(expression);
await filter.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe(expression);
await page.reload();
await expect(filter.content()).toBeVisible();
await expect.poll(() => filter.expression()).toBe(expression);
await expect(qb.query('A').filterStatus()).toHaveCount(0);
});
}
test('TC-11 the status icon shows only for an unfocused invalid filter', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const query = qb.query('A');
await query.filter.type('service.name =');
await expect(query.filterStatus()).toHaveCount(0);
await query.filter.content().blur();
await expect(query.filterStatus()).toBeVisible();
await query.filter.type("service.name = 'frontend'");
await query.filter.content().blur();
await expect(query.filterStatus()).toHaveCount(0);
await query.filter.clear();
await query.filter.content().blur();
await expect(query.filterStatus()).toHaveCount(0);
});
test('TC-12 Enter and Shift+Enter never insert a newline or run', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
await filter.type("severity_text = 'ERROR'");
await filter.press('Escape');
await expect(filter.suggestions()).toHaveCount(0);
const sentBefore = qb.sent().length;
await filter.press('Enter');
await filter.press('Shift+Enter');
await expect(filter.lineCount()).toHaveCount(1);
await expect.poll(() => filter.expression()).toBe("severity_text = 'ERROR'");
expect(qb.sent()).toHaveLength(sentBefore);
});
for (const explorer of ['logs', 'traces'] as const) {
test(`TC-13 Cmd/Ctrl+Enter in the ${explorer} filter runs the edited query`, async ({
qb,
}) => {
await qb.openExplorer(EXPLORER[explorer]);
const filter = qb.query('A').filter;
await filter.type("service.name = 'frontend'");
const fired = await qb.runShortcut();
expect(qb.spec(fired, 'A').filter?.expression).toBe(
"service.name = 'frontend'",
);
await expect(filter.lineCount()).toHaveCount(1);
expect(qb.urlQuery()).toMatchObject({ queryType: 'builder' });
});
}
test('TC-14 Escape closes suggestions and keeps the text', async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
await filter.type('serv');
await filter.openSuggestions();
await filter.press('Escape');
await expect(filter.suggestions()).toHaveCount(0);
await expect.poll(() => filter.expression()).toBe('serv');
await filter.press('i');
await expect.poll(() => filter.expression()).toBe('servi');
await filter.press('Escape');
await filter.press('Escape');
await expect.poll(() => filter.expression()).toBe('servi');
await expect(filter.content()).toBeFocused();
});
test('TC-09 value suggestions fill an IN list', async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
await filter.type('service.name IN ');
await filter.openSuggestions();
await filter.suggestion('frontend').first().click();
await expect
.poll(() => filter.expression())
.toBe("service.name IN ['frontend'] ");
await filter.type("service.name IN ['frontend', 'cartservice']");
await filter.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe(
"service.name IN ['frontend', 'cartservice']",
);
await expect(qb.query('A').filterStatus()).toHaveCount(0);
});
for (const [label, expression] of Object.entries(COMPOUND)) {
test(`TC-10 ${label} is valid and sent verbatim`, async ({ qb }) => {
await qb.openExplorer(EXPLORER.logs);
const query = qb.query('A');
await query.filter.type(expression);
await query.filter.press('Escape');
await query.filter.content().blur();
await expect(query.filterStatus()).toHaveCount(0);
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe(expression);
});
}
test('TC-15 an expression with CRLF from the URL loads without a RangeError', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs, {
query: queryWithFilter("body CONTAINS 'line1\r\nline2'"),
});
await expect.poll(() => qb.query('A').filter.expression()).toContain('line1');
expect(qb.console.errors.filter((e) => e.includes('RangeError'))).toEqual([]);
await qb.query('A').filter.append(" AND severity_text = 'ERROR'");
await expect
.poll(() => qb.query('A').filter.expression())
.toMatch(/severity_text = 'ERROR'$/);
expect(qb.console.errors).toEqual([]);
});
test('TC-15 recent searches are offered, applied and deleted', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
for (const expression of [RECENT_KEPT, RECENT_REMOVED]) {
await filter.type(expression);
await filter.press('Escape');
await qb.run();
}
await filter.clear();
await filter.openSuggestions();
await expect(filter.suggestion(RECENT_KEPT)).toBeVisible();
await expect(filter.suggestion(RECENT_REMOVED)).toBeVisible();
await filter.suggestion(RECENT_KEPT).click();
await expect.poll(() => filter.expression()).toBe(RECENT_KEPT);
await filter.clear();
await filter.openSuggestions();
await filter
.suggestion(RECENT_REMOVED)
.getByRole('button', { name: 'Remove from recent searches' })
.click();
await expect(filter.suggestion(RECENT_REMOVED)).toHaveCount(0);
});
test('TC-15 recent searches survive a reload and stay with their signal', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
await qb.query('A').filter.type(RECENT_KEPT);
await qb.query('A').filter.press('Escape');
await qb.run();
await qb.openExplorer(EXPLORER.logs);
await qb.query('A').filter.clear();
await qb.query('A').filter.openSuggestions();
await expect(qb.query('A').filter.suggestion(RECENT_KEPT)).toBeVisible();
await qb.openExplorer(EXPLORER.traces);
await qb.query('A').filter.clear();
await qb.query('A').filter.openSuggestions();
await expect(qb.query('A').filter.suggestion(RECENT_KEPT)).toHaveCount(0);
});
test('TC-16 a quick-filter value toggles between excluded and cleared', async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const filter = qb.query('A').filter;
const handTyped = "service.name = 'frontend'";
const excluded = `${handTyped} severity_text not in ['ERROR']`;
const error = qb.quickFilter('Severity Text', 'ERROR');
await filter.type(handTyped);
await qb.run();
for (const expected of [excluded, handTyped, excluded]) {
const fired = await error.toggle();
expect(qb.spec(fired, 'A').filter?.expression).toBe(expected);
await expect.poll(() => filter.expression()).toBe(expected);
}
await expect(error.checkbox()).toHaveAttribute('aria-checked', 'false');
});
});

View File

@@ -0,0 +1,106 @@
import {
expect,
EXPLORER,
type MetricTypes,
OTHER_SUM_METRIC,
SUM_METRIC,
test as base,
withMetricTypes,
} from '../../drivers/query-builder';
const test = withMetricTypes(base);
const MOMENT_AGGREGATIONS = ['Sum', 'Avg', 'Min', 'Max'];
interface TypeDefaults {
metric: (types: MetricTypes) => string;
time?: string;
space: string;
spaceOptions: string[];
}
const DEFAULTS: Record<string, TypeDefaults> = {
sum: {
metric: () => SUM_METRIC,
time: 'Rate',
space: 'Sum',
spaceOptions: MOMENT_AGGREGATIONS,
},
gauge: {
metric: (types) => types.gauge,
time: 'Avg',
space: 'Avg',
spaceOptions: MOMENT_AGGREGATIONS,
},
histogram: {
metric: (types) => types.histogram,
space: 'P90',
spaceOptions: ['P50', 'P75', 'P90', 'P95', 'P99'],
},
};
test.describe('Query builder — metric aggregations', () => {
for (const [type, expected] of Object.entries(DEFAULTS)) {
test(`TC-19 a ${type} metric gets its type's default aggregations`, async ({
metricTypes,
qb,
}) => {
const metricName = expected.metric(metricTypes);
await qb.openExplorer(EXPLORER.metrics);
const metric = qb.query('A').metric;
await metric.select(metricName);
await expect(metric.spaceAggregation()).toHaveText(expected.space);
expect(await metric.spaceOptions()).toEqual(expected.spaceOptions);
const fired = await qb.run();
expect(qb.spec(fired, 'A').aggregations?.[0]).toMatchObject({
metricName,
spaceAggregation: expected.space.toLowerCase(),
...(expected.time && { timeAggregation: expected.time.toLowerCase() }),
});
});
}
test('TC-19 a histogram has no time aggregation control', async ({
metricTypes,
qb,
}) => {
await qb.openExplorer(EXPLORER.metrics);
await qb.query('A').metric.select(metricTypes.histogram);
await expect(qb.query('A').metric.spaceAggregation()).toHaveText('P90');
await expect(qb.query('A').metric.timeAggregation()).toHaveCount(0);
});
test('TC-20 aggregations reset only when the metric type changes', async ({
metricTypes,
qb,
}) => {
await qb.openExplorer(EXPLORER.metrics);
const metric = qb.query('A').metric;
await metric.select(SUM_METRIC);
await metric.setTimeAggregation('Increase');
await metric.setSpaceAggregation('Max');
await metric.select(OTHER_SUM_METRIC);
await expect(metric.timeAggregation()).toHaveText('Increase');
await expect(metric.spaceAggregation()).toHaveText('Max');
await metric.select(metricTypes.gauge);
await expect(metric.timeAggregation()).toHaveText('Avg');
await expect(metric.spaceAggregation()).toHaveText('Avg');
await metric.select(SUM_METRIC);
await expect(metric.timeAggregation()).toHaveText('Rate');
await expect(metric.spaceAggregation()).toHaveText('Sum');
const fired = await qb.run();
expect(qb.spec(fired, 'A').aggregations?.[0]).toMatchObject({
metricName: SUM_METRIC,
timeAggregation: 'rate',
spaceAggregation: 'sum',
});
});
});

View File

@@ -0,0 +1,136 @@
import {
logsCountQuery,
metricsQuery,
SINGLE_PANEL_ID,
singlePanelDashboard,
test as editorTest,
} from '../../drivers/panel-editor';
import {
expect,
OTHER_SUM_METRIC,
SUM_METRIC,
withQueryBuilder,
} from '../../drivers/query-builder';
// An added query continues the previous query's signal. On these metrics
// panels it starts with no metric, and Run does nothing until one is picked.
const test = withQueryBuilder(editorTest);
const SIGNAL_LABEL = { logs: 'Logs', traces: 'Traces' } as const;
const CLONED_FILTER = "operation = '/ads/get'";
test.describe('Query builder — multiple queries', () => {
for (const signal of ['logs', 'traces'] as const) {
test(`TC-28 a query added to a ${signal} panel stays ${signal}`, async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: logsCountQuery({ signal }) }),
SINGLE_PANEL_ID,
);
const added = await qb.addQuery('B');
await expect(added.signal()).toHaveText(SIGNAL_LABEL[signal]);
await expect(added.aggregation.root()).toBeVisible();
await expect(added.metric.input()).toHaveCount(0);
await added.filter.type("service.name = 'frontend'");
await added.filter.press('Escape');
await qb.rerunUntil(() => qb.latest('B')?.signal, signal);
});
}
test('TC-28 a deleted name is reused by the next added query', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: metricsQuery() }),
SINGLE_PANEL_ID,
);
for (const name of ['B', 'C']) {
await (await qb.addQuery(name)).metric.select(OTHER_SUM_METRIC);
}
expect(await qb.queryNames()).toEqual(['A', 'B', 'C']);
await qb.query('B').delete();
expect(await qb.queryNames()).toEqual(['A', 'C']);
const reAdded = await qb.addQuery('B');
expect(await qb.queryNames()).toEqual(['A', 'C', 'B']);
await reAdded.metric.select(SUM_METRIC);
await expect(reAdded.metric.input()).toHaveValue(SUM_METRIC);
await expect(qb.query('C').metric.input()).toHaveValue(OTHER_SUM_METRIC);
});
test('TC-28 a clone and a hidden query reach the request', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: metricsQuery({ filter: CLONED_FILTER }) }),
SINGLE_PANEL_ID,
);
await (await qb.addQuery('B')).metric.select(OTHER_SUM_METRIC);
await qb.query('A').clone();
await expect
.poll(() => qb.query('C').filter.expression())
.toBe(CLONED_FILTER);
await qb.query('B').toggleVisibility();
await qb.runButton().click();
await expect.poll(() => qb.latest('B')?.disabled).toBe(true);
expect(qb.latest('C')).toMatchObject({
filter: { expression: CLONED_FILTER },
aggregations: [expect.objectContaining({ metricName: SUM_METRIC })],
});
});
test('TC-28 the last remaining query offers no delete', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: metricsQuery() }),
SINGLE_PANEL_ID,
);
await (await qb.addQuery('B')).metric.select(OTHER_SUM_METRIC);
await qb.query('B').delete();
await qb.query('A').actions().click();
await expect(qb.query('A').menuItem('clone')).toBeVisible();
await expect(qb.query('A').menuItem('delete')).toHaveCount(0);
});
test('TC-29 a formula over two queries reaches the request', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: metricsQuery() }),
SINGLE_PANEL_ID,
);
await (await qb.addQuery('B')).metric.select(OTHER_SUM_METRIC);
const formula = await qb.addFormula('F1');
await formula.expression().fill('A / B');
await formula.legend().fill('ratio');
await qb.runButton().click();
await expect
.poll(() => qb.latest('F1'))
.toMatchObject({ expression: 'A / B', legend: 'ratio' });
await formula.delete();
await qb.rerunUntil(
() =>
qb
.sent()
.at(-1)
?.compositeQuery.queries.map((q) => q.spec.name),
['A', 'B'],
);
});
});

View File

@@ -0,0 +1,203 @@
import {
logsCountQuery,
metricsQuery,
PanelKind,
SINGLE_PANEL_ID,
singlePanelDashboard,
test as editorTest,
} from '../../drivers/panel-editor';
import {
expect,
reduceToLabel,
SUM_METRIC,
withMetricTypes,
withQueryBuilder,
} from '../../drivers/query-builder';
const test = withMetricTypes(withQueryBuilder(editorTest));
const LOGS_FILTER = "severity_text = 'ERROR'";
const TRACES_FILTER = "http.method = 'GET'";
const PROMQL = 'sum(rate(signoz_calls_total[5m]))';
const QUERIES = {
logs: () => logsCountQuery(),
traces: () => logsCountQuery({ signal: 'traces' }),
metrics: () => metricsQuery(),
};
const ALL = ['group_by', 'having', 'order_by', 'limit', 'legend_format'];
const NO_HAVING = ['group_by', 'order_by', 'limit', 'legend_format'];
const METRICS = ['having', 'order_by', 'limit', 'legend_format'];
/**
* Add-on toggles each panel kind offers per signal. Metrics group by lives in
* the aggregation row, so it is never a toggle there.
*/
const ADD_ONS: Record<
keyof typeof QUERIES,
Partial<Record<string, string[]>>
> = {
logs: {
[PanelKind.TimeSeries]: ALL,
[PanelKind.BarChart]: ALL,
[PanelKind.Table]: ALL,
[PanelKind.PieChart]: ALL,
[PanelKind.Number]: ['legend_format'],
[PanelKind.Heatmap]: NO_HAVING,
},
traces: {
[PanelKind.TimeSeries]: ALL,
[PanelKind.BarChart]: ALL,
[PanelKind.Table]: ALL,
[PanelKind.PieChart]: ALL,
[PanelKind.Number]: ['legend_format'],
[PanelKind.Heatmap]: NO_HAVING,
},
metrics: {
[PanelKind.TimeSeries]: METRICS,
[PanelKind.BarChart]: METRICS,
[PanelKind.Table]: [...METRICS, 'reduce_to'],
[PanelKind.PieChart]: [...METRICS, 'reduce_to'],
[PanelKind.Number]: ['legend_format', 'reduce_to'],
[PanelKind.Heatmap]: ['order_by', 'limit', 'legend_format', 'bucket_options'],
},
};
test.describe('Query builder — panel editor', () => {
for (const [signal, kinds] of Object.entries(ADD_ONS)) {
for (const [kind, expected] of Object.entries(kinds)) {
test(`TC-27 ${signal} on ${kind.replace('signoz/', '')} offers its add-ons`, async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({
kind: kind as PanelKind,
query: QUERIES[signal as keyof typeof QUERIES](),
}),
SINGLE_PANEL_ID,
);
await expect(qb.query('A').addOns.toggles()).toHaveCount(
expected?.length ?? 0,
);
const toggles = await qb
.query('A')
.addOns.toggles()
.evaluateAll((nodes) =>
nodes.map((node) =>
(node.getAttribute('data-testid') ?? '').replace('query-add-on-', ''),
),
);
expect(toggles).toEqual(expected);
});
}
}
test('TC-27 a limit hidden by a kind switch reopens with its value', async ({
dashboards,
editor,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: logsCountQuery() }),
SINGLE_PANEL_ID,
);
const addOns = qb.query('A').addOns;
await addOns.open('limit');
await addOns.limit().fill('7');
await editor.switchType(PanelKind.Number);
await expect(addOns.content('limit')).toHaveCount(0);
await editor.switchType(PanelKind.TimeSeries);
await expect(addOns.limit()).toHaveValue('7');
// The kind switch already fetched: Run can be a cache hit.
await qb.runButton().click();
await expect.poll(() => qb.latest('A')?.limit).toBe(7);
});
test('TC-26 reduce-to follows the metric type and keeps a choice within it', async ({
dashboards,
metricTypes,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ kind: PanelKind.Table, query: metricsQuery() }),
SINGLE_PANEL_ID,
);
const query = qb.query('A');
await expect(query.addOns.reduceTo.value()).toHaveText(reduceToLabel('Sum'));
await query.addOns.reduceTo.set('Max');
await query.metric.select('signoz_latency_count');
await expect(query.addOns.reduceTo.value()).toHaveText(reduceToLabel('Max'));
await query.metric.select(metricTypes.gauge);
await expect(query.addOns.reduceTo.value()).toHaveText(
reduceToLabel('Average'),
);
await query.metric.select(SUM_METRIC);
await expect(query.addOns.reduceTo.value()).toHaveText(reduceToLabel('Sum'));
expect(qb.spec(await qb.run(), 'A').aggregations?.[0]).toMatchObject({
metricName: SUM_METRIC,
reduceTo: 'sum',
});
});
test('TC-31 switching signal restores the query left on each signal', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: logsCountQuery({ groupBy: [] }) }),
SINGLE_PANEL_ID,
);
const query = qb.query('A');
await query.filter.type(LOGS_FILTER);
await query.filter.press('Escape');
await query.setSignal('Traces');
await expect.poll(() => query.filter.expression()).toBe('');
await query.filter.type(TRACES_FILTER);
await query.filter.press('Escape');
await query.setSignal('Logs');
await expect.poll(() => query.filter.expression()).toBe(LOGS_FILTER);
await query.setSignal('Traces');
await expect.poll(() => query.filter.expression()).toBe(TRACES_FILTER);
await qb.runButton().click();
await expect
.poll(() => qb.latest('A'))
.toMatchObject({
signal: 'traces',
filter: { expression: TRACES_FILTER },
});
});
test('TC-33 a PromQL query is sent as promql and kept across tab switches', async ({
dashboards,
qb,
}) => {
await dashboards.seedAndEdit(
singlePanelDashboard({ query: metricsQuery() }),
SINGLE_PANEL_ID,
);
await qb.tab('promql').click();
await qb.promql().fill(PROMQL);
const fired = await qb.run();
expect(fired.at(-1)?.compositeQuery.queries[0]).toMatchObject({
type: 'promql',
spec: { name: 'A', query: PROMQL },
});
await qb.tab('clickhouse_sql').click();
await qb.tab('builder').click();
await expect(qb.query('A').metric.spaceAggregation()).toHaveText('Sum');
await qb.tab('promql').click();
await expect(qb.promql()).toHaveValue(PROMQL);
});
});

View File

@@ -0,0 +1,80 @@
import { expect, EXPLORER, test } from '../../drivers/query-builder';
import {
holdQueryRange,
mockQueryRange,
mockQueryRangeError,
QueryRange,
unmockQueryRange,
} from '../../helpers/query-range-mock';
test.describe('Query builder — run and cancel', () => {
test('TC-34 a held run can be cancelled and the next run goes through', async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
const held = await holdQueryRange(page, QueryRange.empty());
await qb.query('A').filter.type("severity_text = 'WARN'");
await qb.query('A').filter.press('Escape');
await qb.runButton().click();
await expect(qb.runButton()).toHaveText('Cancel');
await qb.runButton().click();
await expect(qb.runButton()).toHaveText(/Run Query/);
await expect(qb.logsResults()).toContainText('Query cancelled');
held.release();
await unmockQueryRange(page);
await qb.query('A').filter.type("severity_text = 'ERROR'");
await qb.query('A').filter.press('Escape');
// The cancelled query is re-sent alongside; the newest request wins.
await qb.run();
expect(qb.latest('A')?.filter?.expression).toBe("severity_text = 'ERROR'");
await expect(qb.logsResults()).not.toContainText('Query cancelled');
});
test('TC-35 a failed run keeps the query editable and the next run recovers', async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
await mockQueryRangeError(page, 'mocked query failure');
await qb.query('A').filter.type("severity_text = 'INFO'");
await qb.query('A').filter.press('Escape');
await qb.runButton().click();
// The failed request is retried before the error settles.
await expect(qb.runButton()).toHaveText(/Run Query/, { timeout: 20_000 });
await expect(qb.logsResults()).toContainText('mocked query failure');
await expect
.poll(() => qb.query('A').filter.expression())
.toBe("severity_text = 'INFO'");
await unmockQueryRange(page);
await qb.query('A').filter.type("severity_text = 'WARN'");
await qb.query('A').filter.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe("severity_text = 'WARN'");
await expect(qb.logsResults()).not.toContainText('mocked query failure');
});
test('TC-35 an empty result leaves the query unchanged', async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER.logs);
await mockQueryRange(page, QueryRange.empty());
await qb.query('A').filter.type("service.name = 'nothing-here'");
await qb.query('A').filter.press('Escape');
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe(
"service.name = 'nothing-here'",
);
await expect
.poll(() => qb.query('A').filter.expression())
.toBe("service.name = 'nothing-here'");
expect(qb.console.errors).toEqual([]);
});
});

View File

@@ -0,0 +1,194 @@
import { expect, EXPLORER, test } from '../../drivers/query-builder';
// A shared or hand-edited explorer link must never take the page down: the
// param is parsed, repaired and rewritten before the builder mounts.
const MALFORMED: Record<string, string> = {
'not JSON': 'not-json',
'truncated JSON': '%7B',
'a JSON string': encodeURIComponent('"a string"'),
'a JSON array': encodeURIComponent('[]'),
'JSON null': 'null',
'an empty JSON string': '%22%22',
'double encoded': encodeURIComponent(
encodeURIComponent(JSON.stringify({ queryType: 'builder' })),
),
};
const PARTIAL: Record<string, unknown> = {
'an empty object': {},
'builder with no builder block': { queryType: 'builder' },
'builder with no queryData': { queryType: 'builder', builder: {} },
'empty queryData': {
queryType: 'builder',
builder: { queryData: [], queryFormulas: [] },
},
'a query with no dataSource': {
queryType: 'builder',
builder: { queryData: [{ queryName: 'A' }] },
},
'an unknown queryType': { queryType: 'zzz' },
'non-array promql and clickhouse blocks': {
queryType: 'builder',
promql: {},
clickhouse_sql: 'x',
builder: { queryData: [{ queryName: 'A', dataSource: 'logs' }] },
},
};
/**
* Saved before the v5 builder: structured filters, a having array and an
* aggregate operator. The old having filter wrote `columnName` upper-cased
* (`COUNT()`).
*/
const legacyLogsQuery = (having: unknown[]): unknown => ({
queryType: 'builder',
promql: [],
clickhouse_sql: [],
builder: {
queryFormulas: [],
queryData: [
{
queryName: 'A',
dataSource: 'logs',
expression: 'A',
disabled: false,
aggregateOperator: 'count',
aggregateAttribute: { key: '', dataType: '', type: '', isColumn: false },
filters: {
op: 'AND',
items: [
{
id: 'f1',
key: {
key: 'severity_text',
dataType: 'string',
type: '',
isColumn: true,
},
op: '=',
value: 'ERROR',
},
],
},
having,
groupBy: [],
orderBy: [],
functions: [],
legend: '',
limit: null,
stepInterval: 60,
reduceTo: 'avg',
},
],
},
});
const FIRST = "service.name = 'frontend'";
const SECOND = "service.name = 'cartservice'";
interface UrlQuery {
builder?: { queryData?: { filter?: { expression?: string } }[] };
}
test.describe('Query builder — URL handling', () => {
for (const [label, raw] of Object.entries(MALFORMED)) {
test(`TC-01 logs explorer survives ${label} and still runs`, async ({
qb,
}) => {
await qb.openExplorer(EXPLORER.logs, { raw });
await expect(qb.query('A').filter.content()).toBeVisible();
await qb.query('A').filter.type("severity_text = 'ERROR'");
const fired = await qb.run();
expect(qb.spec(fired, 'A').filter?.expression).toBe(
"severity_text = 'ERROR'",
);
expect(qb.urlQuery()).toMatchObject({ queryType: 'builder' });
expect(qb.console.errors).toEqual([]);
});
}
for (const [label, query] of Object.entries(PARTIAL)) {
test(`TC-02 logs explorer repairs ${label}`, async ({ qb }) => {
const loaded = await qb.openExplorer(EXPLORER.logs, { query });
expect(qb.spec(loaded, 'A').signal).toBe('logs');
await expect
.poll(() => qb.urlQuery())
.toMatchObject({
queryType: 'builder',
builder: { queryData: [{ queryName: 'A', dataSource: 'logs' }] },
});
expect(qb.console.errors).toEqual([]);
});
}
test('TC-03 legacy filters and aggregate operator convert to v5 expressions', async ({
qb,
}) => {
const loaded = await qb.openExplorer(EXPLORER.logs, {
view: 'graph',
query: legacyLogsQuery([]),
});
const spec = qb.spec(loaded, 'A');
expect(spec.filter?.expression).toMatch(/^severity_text\s*=\s*'ERROR'$/);
expect(spec.aggregations?.[0]?.expression).toBe('count()');
expect(spec).not.toHaveProperty('filters');
await expect
.poll(() => qb.query('A').filter.expression())
.toMatch(/^severity_text\s*=\s*'ERROR'$/);
expect(qb.console.errors).toEqual([]);
});
test('TC-03 a legacy having as the old builder stored it still runs', async ({
qb,
}) => {
const loaded = await qb.openExplorer(EXPLORER.logs, {
view: 'graph',
query: legacyLogsQuery([{ columnName: 'COUNT()', op: '>', value: 5 }]),
});
expect(qb.spec(loaded, 'A').having?.expression).toBe('count() > 5');
expect(qb.console.failedResponses).toEqual([]);
});
for (const explorer of ['logs', 'traces'] as const) {
test(`TC-04 ${explorer} explorer query follows back, forward and reload`, async ({
authedPage: page,
qb,
}) => {
await qb.openExplorer(EXPLORER[explorer]);
const filter = qb.query('A').filter;
const urlFilter = (): unknown =>
(qb.urlQuery() as UrlQuery | null)?.builder?.queryData?.[0]?.filter
?.expression;
await filter.type(FIRST);
await qb.run();
await filter.type(SECOND);
await qb.run();
await page.goBack();
await expect.poll(urlFilter).toBe(FIRST);
await expect.poll(() => filter.expression()).toBe(FIRST);
await page.goForward();
await expect.poll(urlFilter).toBe(SECOND);
await expect.poll(() => filter.expression()).toBe(SECOND);
await filter.type("service.name = 'unsaved'");
const sentBefore = qb.sent().length;
await page.reload();
await expect.poll(() => filter.expression()).toBe(SECOND);
await expect
.poll(() => qb.sent().slice(sentBefore).length)
.toBeGreaterThan(0);
expect(qb.spec(qb.sent().slice(sentBefore), 'A').filter?.expression).toBe(
SECOND,
);
});
}
});

View File

@@ -50,7 +50,6 @@ ALL_SELECT_FIELDS = [
# all contextual columns (merged in response layer)
"attributes",
"resource",
"scope",
]

View File

@@ -545,7 +545,7 @@ def test_traces_list_response_shape_values(
now = datetime.now(tz=UTC).replace(microsecond=0)
trace_id = TraceIdGenerator.trace_id()
def span(marker: str, *, kind: TracesKind, status_code: TracesStatusCode = TracesStatusCode.STATUS_CODE_UNSET, status_message: str = "", flags: int = 0, attributes: dict[str, Any], scope: dict[str, Any] | None = None) -> Traces:
def span(marker: str, *, kind: TracesKind, status_code: TracesStatusCode = TracesStatusCode.STATUS_CODE_UNSET, status_message: str = "", flags: int = 0, attributes: dict[str, Any]) -> Traces:
return Traces(
timestamp=now - timedelta(seconds=1),
duration=timedelta(seconds=1),
@@ -558,11 +558,10 @@ def test_traces_list_response_shape_values(
flags=flags,
resources={"service.name": "shape-service", **extra_resources},
attributes={"marker": marker, **attributes, **extra_attrs},
scope=scope or {},
)
spans = [
span("error", kind=TracesKind.SPAN_KIND_SERVER, status_code=TracesStatusCode.STATUS_CODE_ERROR, status_message="boom", attributes={"str_a": "x", "num_a": 7.5, "bool_a": True}, scope={"name": "io.signoz.shape", "version": "0.1.0", "attributes": {"lib.lang": "go"}}),
span("error", kind=TracesKind.SPAN_KIND_SERVER, status_code=TracesStatusCode.STATUS_CODE_ERROR, status_message="boom", attributes={"str_a": "x", "num_a": 7.5, "bool_a": True}),
span("client", kind=TracesKind.SPAN_KIND_CLIENT, attributes={"http.url": "https://api.example.com/v1/orders", "http.request.method": "GET"}),
span("db", kind=TracesKind.SPAN_KIND_CLIENT, attributes={"db.name": "orders_db", "db.operation": "SELECT"}),
span("host", kind=TracesKind.SPAN_KIND_PRODUCER, attributes={"server.address": "payments.internal:8080"}),
@@ -623,7 +622,6 @@ def test_traces_list_response_shape_values(
"is_remote": span_obj.is_remote,
"attributes": {**span_obj.attribute_string, **span_obj.attributes_number, **span_obj.attributes_bool},
"resource": span_obj.resources_string,
"scope": {k: v for k, v in span_obj.scope_json.items() if v},
}
# `==` treats True/1 and 7.5/7.5 as equal regardless of type, so pin the
# string/number/bool type preservation for the error span explicitly.
@@ -1156,7 +1154,6 @@ def test_traces_list_span_scope(
"is_remote": x[3].is_remote,
"attributes": {**x[3].attribute_string, **x[3].attributes_number, **x[3].attributes_bool},
"resource": x[3].resources_string,
"scope": {k: v for k, v in x[3].scope_json.items() if v},
},
id="order_timestamp_full_shape",
),
@@ -1194,7 +1191,6 @@ def test_traces_list_span_scope(
"is_remote": x[3].is_remote,
"attributes": {**x[3].attribute_string, **x[3].attributes_number, **x[3].attributes_bool},
"resource": x[3].resources_string,
"scope": {k: v for k, v in x[3].scope_json.items() if v},
},
id="order_attribute_timestamp_full_shape",
),

View File

@@ -21,13 +21,12 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
trace_id = TraceIdGenerator.trace_id()
legacy_id, json_id, dual_id = (TraceIdGenerator.span_id() for _ in range(3))
resources = {"service.name": "tracedetail-attribute-homes"}
scope = {"name": "io.opentelemetry.http", "version": "1.2.0", "attributes": {"lib.lang": "go"}}
insert_traces(
[
Traces(timestamp=now - timedelta(seconds=10), duration=timedelta(seconds=9), trace_id=trace_id, span_id=legacy_id, name="GET /legacy", kind=TracesKind.SPAN_KIND_SERVER, resources=resources, attributes={"http.route": "/legacy", "http.retry.count": 1, "cache.hit": True}, attribute_write_mode="legacy_only"),
Traces(timestamp=now - timedelta(seconds=8), trace_id=trace_id, span_id=json_id, parent_span_id=legacy_id, name="db call", resources=resources, attributes={"http.route": "/json", "db.function": "node_refresh", "db.function.arg_count": 2}, attribute_write_mode="json_only"),
Traces(timestamp=now - timedelta(seconds=6), trace_id=trace_id, span_id=dual_id, parent_span_id=legacy_id, name="GET /dual", resources=resources, attributes={"http.route": "/dual"}, scope=scope),
Traces(timestamp=now - timedelta(seconds=6), trace_id=trace_id, span_id=dual_id, parent_span_id=legacy_id, name="GET /dual", resources=resources, attributes={"http.route": "/dual"}),
]
)
@@ -41,14 +40,11 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
timeout=10,
)
assert response.status_code == HTTPStatus.OK, response.text
spans = {span["span_id"]: span for span in response.json()["data"]["spans"]}
waterfall = {span_id: span["attributes"] for span_id, span in spans.items()}
waterfall = {span["span_id"]: span["attributes"] for span in response.json()["data"]["spans"]}
assert waterfall[legacy_id] == {"http.route": "/legacy", "http.retry.count": 1, "cache.hit": True}
# a key stored at both a scalar and an object path stays two flat keys
assert waterfall[json_id] == {"http.route": "/json", "db.function": "node_refresh", "db.function.arg_count": 2}
assert waterfall[dual_id] == {"http.route": "/dual"}
assert spans[dual_id]["scope"] == scope
assert spans[legacy_id]["scope"] == {}
response = requests.post(
signoz.self.host_configs["8080"].get(f"/api/v3/traces/{trace_id}/flamegraph"),
@@ -58,8 +54,6 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
{"name": "http.route", "fieldContext": "attribute"},
{"name": "db.function.arg_count", "fieldContext": "attribute"},
{"name": "service.name", "fieldContext": "resource"},
{"name": "scope.name", "fieldContext": "scope"},
{"name": "lib.lang", "fieldContext": "scope"},
],
},
headers=headers,
@@ -71,5 +65,3 @@ def test_waterfall_and_flamegraph_read_each_attribute_home(
assert flamegraph[json_id]["attributes"] == {"http.route": "/json", "db.function.arg_count": 2}
assert flamegraph[dual_id]["attributes"] == {"http.route": "/dual"}
assert all(span["resource"] == resources for span in flamegraph.values())
assert flamegraph[dual_id]["scope"] == {"name": "io.opentelemetry.http", "attributes": {"lib.lang": "go"}}
assert flamegraph[json_id]["scope"] == {}