mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-13 13:40:30 +01:00
Compare commits
19 Commits
feat/trace
...
traceop-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dafa81f3b4 | ||
|
|
a992a13f56 | ||
|
|
79b36abbd7 | ||
|
|
181c307d1a | ||
|
|
becdd4d3b4 | ||
|
|
de0311201a | ||
|
|
1804bfe802 | ||
|
|
357444c94e | ||
|
|
a8598f3bfa | ||
|
|
bca71f9a33 | ||
|
|
c93660357d | ||
|
|
5651e3b7a8 | ||
|
|
cf2cfbc7d4 | ||
|
|
a969c38224 | ||
|
|
b892a0f0a5 | ||
|
|
4d47762eba | ||
|
|
77396a0bb3 | ||
|
|
28c05e1bab | ||
|
|
2b9e383994 |
@@ -70,12 +70,39 @@ func (b *traceOperatorCTEBuilder) build(ctx context.Context, requestType qbtypes
|
||||
|
||||
selectFromCTE := rootCTEName
|
||||
if b.operator.ReturnSpansFrom != "" {
|
||||
selectFromCTE = b.queryToCTEName[b.operator.ReturnSpansFrom]
|
||||
if selectFromCTE == "" {
|
||||
sourceQueryCTE := b.queryToCTEName[b.operator.ReturnSpansFrom]
|
||||
if sourceQueryCTE == "" {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"returnSpansFrom references query '%s' which has no corresponding CTE",
|
||||
b.operator.ReturnSpansFrom)
|
||||
}
|
||||
filteredCTEName := fmt.Sprintf("__return_from_%s", b.operator.ReturnSpansFrom)
|
||||
|
||||
// DISTINCT is essential here. The operator CTE (rootCTEName) holds one row
|
||||
// per matching *span*, not one row per matching *trace*. A single trace can
|
||||
// satisfy the operator through multiple spans — e.g. for "A -> B", every
|
||||
// A-span that is an indirect ancestor of any B-span appears as a separate
|
||||
// row. If we joined sourceQueryCTE directly against rootCTEName on trace_id,
|
||||
// each source span would be duplicated once for every operator-matching span
|
||||
// on the same trace. DISTINCT collapses rootCTEName to one row per trace_id,
|
||||
// making the join a clean membership test with no fan-out.
|
||||
matchingTracedSB := sqlbuilder.NewSelectBuilder()
|
||||
matchingTracedSB.Select("DISTINCT trace_id")
|
||||
matchingTracedSB.From(rootCTEName)
|
||||
matchedTracesSQL, matchedTracesArgs := matchingTracedSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
filteredSB := sqlbuilder.NewSelectBuilder()
|
||||
filteredSB.Select("src.*")
|
||||
filteredSB.From(fmt.Sprintf("%s AS src", sourceQueryCTE))
|
||||
filteredSB.JoinWithOption(
|
||||
sqlbuilder.InnerJoin,
|
||||
fmt.Sprintf("(%s) AS matched_traces", matchedTracesSQL),
|
||||
"src.trace_id = matched_traces.trace_id",
|
||||
)
|
||||
filteredSQL, filteredArgs := filteredSB.BuildWithFlavor(sqlbuilder.ClickHouse, matchedTracesArgs...)
|
||||
|
||||
b.addCTE(filteredCTEName, filteredSQL, filteredArgs, []string{sourceQueryCTE, rootCTEName})
|
||||
selectFromCTE = filteredCTEName
|
||||
}
|
||||
|
||||
finalStmt, err := b.buildFinalQuery(ctx, selectFromCTE, requestType)
|
||||
|
||||
@@ -385,6 +385,82 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "returnSpansFrom B: A -> B return B spans filtered by operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
operator: qbtypes.QueryBuilderTraceOperator{
|
||||
Expression: "A -> B",
|
||||
ReturnSpansFrom: "B",
|
||||
Limit: 10,
|
||||
},
|
||||
compositeQuery: &qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'gateway'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'database'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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 <= ?), 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 <= ?), 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 src.* FROM B AS src INNER JOIN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B) AS matched_traces ON src.trace_id = matched_traces.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id 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,
|
||||
},
|
||||
{
|
||||
name: "returnSpansFrom C: (A -> B) && C return C spans filtered by operator",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
operator: qbtypes.QueryBuilderTraceOperator{
|
||||
Expression: "(A -> B) && C",
|
||||
ReturnSpansFrom: "C",
|
||||
Limit: 10,
|
||||
},
|
||||
compositeQuery: &qbtypes.CompositeQuery{
|
||||
Queries: []qbtypes.QueryEnvelope{
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'gateway'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "B",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'database'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: qbtypes.QueryTypeBuilder,
|
||||
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Name: "C",
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
Filter: &qbtypes.Filter{Expression: "service.name = 'auth'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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 <= ?), 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 <= ?), 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 <= ?), 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 src.* FROM C AS src INNER JOIN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C) AS matched_traces ON src.trace_id = matched_traces.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id 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,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
3
tests/fixtures/querier.py
vendored
3
tests/fixtures/querier.py
vendored
@@ -72,6 +72,7 @@ class TraceOperatorQuery:
|
||||
return_spans_from: str
|
||||
limit: int | None = None
|
||||
order: list[OrderBy] | None = None
|
||||
select_fields: list[TelemetryFieldKey] | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
spec: dict[str, Any] = {
|
||||
@@ -83,6 +84,8 @@ class TraceOperatorQuery:
|
||||
spec["limit"] = self.limit
|
||||
if self.order:
|
||||
spec["order"] = [o.to_dict() if hasattr(o, "to_dict") else o for o in self.order]
|
||||
if self.select_fields:
|
||||
spec["selectFields"] = [f.to_dict() for f in self.select_fields]
|
||||
return {"type": "builder_trace_operator", "spec": spec}
|
||||
|
||||
|
||||
|
||||
537
tests/integration/tests/querier/15_trace_operator.py
Normal file
537
tests/integration/tests/querier/15_trace_operator.py
Normal file
@@ -0,0 +1,537 @@
|
||||
"""
|
||||
Integration tests for TraceOperatorQuery (builder_trace_operator) through the
|
||||
/api/v5/query_range endpoint.
|
||||
|
||||
Covers:
|
||||
1. Basic trace operator (A => B) — returns matched spans from the correct trace.
|
||||
2. Order by a field absent from selectFields — must not return a server error.
|
||||
Guards against the ClickHouse NOT_FOUND_COLUMN_IN_BLOCK regression where
|
||||
ordering by a column absent from an outer SELECT caused a query failure.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
OrderBy,
|
||||
TelemetryFieldKey,
|
||||
TraceOperatorQuery,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
|
||||
def _builder_query(name: str, filter_expr: str, limit: int = 100) -> dict:
|
||||
return {
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": name,
|
||||
"signal": "traces",
|
||||
"filter": {"expression": filter_expr},
|
||||
"limit": limit,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_trace_operator_query_basic(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Insert one parent span and one child span in the same trace.
|
||||
|
||||
Tests:
|
||||
A => B (parent has a direct child) returns the parent span (returnSpansFrom=A)
|
||||
from the correct trace.
|
||||
"""
|
||||
parent_trace_id = TraceIdGenerator.trace_id()
|
||||
parent_span_id = TraceIdGenerator.span_id()
|
||||
child_span_id = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=parent_span_id,
|
||||
parent_span_id="",
|
||||
name="parent-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=2),
|
||||
trace_id=parent_trace_id,
|
||||
span_id=child_span_id,
|
||||
parent_span_id=parent_span_id,
|
||||
name="child-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["data"]["trace_id"] == parent_trace_id
|
||||
assert rows[0]["data"]["name"] == "parent-op"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_field_not_in_select_fields(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces, each with a grandparent → middle → grandchild chain:
|
||||
Trace 1: grandparent (svc-a, http.method=POST) → middle → grandchild
|
||||
Trace 2: grandparent (svc-b, http.method=GET) → middle → grandchild
|
||||
|
||||
Tests:
|
||||
A -> B (indirect descendant) with selectFields=[service.name] and
|
||||
order=[http.method DESC], where http.method is NOT in selectFields.
|
||||
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error from ClickHouse).
|
||||
2. Results are actually ordered: POST sorts before GET descending, so
|
||||
svc-a must come before svc-b.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
|
||||
gp_span_id_1 = TraceIdGenerator.span_id()
|
||||
mid_span_id_1 = TraceIdGenerator.span_id()
|
||||
gc_span_id_1 = TraceIdGenerator.span_id()
|
||||
|
||||
gp_span_id_2 = TraceIdGenerator.span_id()
|
||||
mid_span_id_2 = TraceIdGenerator.span_id()
|
||||
gc_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
# Trace 1 — grandparent has http.method=POST (sorts first in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gp_span_id_1,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=mid_span_id_1,
|
||||
parent_span_id=gp_span_id_1,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=gc_span_id_1,
|
||||
parent_span_id=mid_span_id_1,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-a"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
# Trace 2 — grandparent has http.method=GET (sorts second in DESC)
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gp_span_id_2,
|
||||
parent_span_id="",
|
||||
name="gp-op",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandparent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=6),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=mid_span_id_2,
|
||||
parent_span_id=gp_span_id_2,
|
||||
name="mid-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "middle"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=gc_span_id_2,
|
||||
parent_span_id=mid_span_id_2,
|
||||
name="gc-op",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-b"},
|
||||
attributes={"operation.type": "grandchild"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'grandparent'"),
|
||||
_builder_query("B", "operation.type = 'grandchild'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A -> B", # indirect descendant
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[
|
||||
TelemetryFieldKey(name="service.name", field_data_type="string", field_context="resource"),
|
||||
],
|
||||
order=[
|
||||
# http.method is intentionally absent from select_fields
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="http.method", field_data_type="string", field_context="attribute"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
# Both grandparent spans must be returned
|
||||
assert len(rows) == 2
|
||||
|
||||
# Ordering: POST > GET in DESC — svc-a (POST) must come before svc-b (GET)
|
||||
assert rows[0]["data"]["service.name"] == "svc-a", f"Expected svc-a (POST) first in http.method DESC order, got {rows[0]['data']['service.name']}"
|
||||
assert rows[1]["data"]["service.name"] == "svc-b", f"Expected svc-b (GET) second in http.method DESC order, got {rows[1]['data']['service.name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_select_field(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; the parents have distinct durations
|
||||
(5 s and 1 s).
|
||||
|
||||
Tests:
|
||||
A => B with order=[duration_nano DESC] and no explicit selectFields.
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. Longer-duration parent (5 s) appears before the shorter one (1 s).
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=5),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-long",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-long",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-long"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-short",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "parent"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-short",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-short"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
order=[
|
||||
OrderBy(
|
||||
key=TelemetryFieldKey(name="duration_nano", field_context="span"),
|
||||
direction="desc",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# DESC: 5 s parent first, 1 s parent second
|
||||
assert rows[0]["data"]["name"] == "parent-long", f"Expected parent-long (5s) first in duration_nano DESC, got {rows[0]['data']['name']}"
|
||||
assert rows[1]["data"]["name"] == "parent-short", f"Expected parent-short (1s) second in duration_nano DESC, got {rows[1]['data']['name']}"
|
||||
|
||||
|
||||
def test_trace_operator_query_order_by_non_core_field_in_select(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Setup:
|
||||
Two traces each with a parent → child pair; parents have distinct http.method
|
||||
values (POST and GET).
|
||||
|
||||
Tests:
|
||||
A => B with selectFields=[http.method] and order=[http.method DESC].
|
||||
1. Query succeeds (no NOT_FOUND_COLUMN_IN_BLOCK error).
|
||||
2. http.method is present in every result row (it is in selectFields).
|
||||
3. Results are ordered DESC — POST before GET.
|
||||
"""
|
||||
trace_id_1 = TraceIdGenerator.trace_id()
|
||||
trace_id_2 = TraceIdGenerator.trace_id()
|
||||
parent_span_id_1 = TraceIdGenerator.span_id()
|
||||
child_span_id_1 = TraceIdGenerator.span_id()
|
||||
parent_span_id_2 = TraceIdGenerator.span_id()
|
||||
child_span_id_2 = TraceIdGenerator.span_id()
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=10),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_1,
|
||||
span_id=parent_span_id_1,
|
||||
parent_span_id="",
|
||||
name="parent-post",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "parent", "http.method": "POST"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=9),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_1,
|
||||
span_id=child_span_id_1,
|
||||
parent_span_id=parent_span_id_1,
|
||||
name="child-post",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-post"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=8),
|
||||
duration=timedelta(seconds=3),
|
||||
trace_id=trace_id_2,
|
||||
span_id=parent_span_id_2,
|
||||
parent_span_id="",
|
||||
name="parent-get",
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "parent", "http.method": "GET"},
|
||||
),
|
||||
Traces(
|
||||
timestamp=now - timedelta(seconds=7),
|
||||
duration=timedelta(seconds=1),
|
||||
trace_id=trace_id_2,
|
||||
span_id=child_span_id_2,
|
||||
parent_span_id=parent_span_id_2,
|
||||
name="child-get",
|
||||
kind=TracesKind.SPAN_KIND_INTERNAL,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
status_message="",
|
||||
resources={"service.name": "svc-get"},
|
||||
attributes={"operation.type": "child"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
http_method_field = TelemetryFieldKey(
|
||||
name="http.method",
|
||||
field_data_type="string",
|
||||
field_context="attribute",
|
||||
)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
request_type="raw",
|
||||
queries=[
|
||||
_builder_query("A", "operation.type = 'parent'"),
|
||||
_builder_query("B", "operation.type = 'child'"),
|
||||
TraceOperatorQuery(
|
||||
name="C",
|
||||
expression="A => B",
|
||||
return_spans_from="A",
|
||||
limit=100,
|
||||
select_fields=[http_method_field],
|
||||
order=[OrderBy(key=http_method_field, direction="desc")],
|
||||
).to_dict(),
|
||||
],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
results = response.json()["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
rows = results[0].get("rows") or []
|
||||
|
||||
assert len(rows) == 2
|
||||
# http.method must be present in every row (it is in selectFields)
|
||||
for row in rows:
|
||||
assert "http.method" in row["data"], f"http.method missing from row: {row['data']}"
|
||||
# DESC: POST before GET
|
||||
assert rows[0]["data"]["http.method"] == "POST", f"Expected POST first in http.method DESC, got {rows[0]['data']['http.method']}"
|
||||
assert rows[1]["data"]["http.method"] == "GET", f"Expected GET second in http.method DESC, got {rows[1]['data']['http.method']}"
|
||||
Reference in New Issue
Block a user