Compare commits

..

2 Commits

Author SHA1 Message Date
Naman Verma
c58f76f998 Merge branch 'main' into nv/exec-stats 2026-11-03 11:34:10 +05:30
Naman Verma
e39ad9e221 test: check how exec stats fail 2026-11-02 22:24:22 +05:30
6 changed files with 79 additions and 48 deletions

View File

@@ -164,12 +164,12 @@ export const TILE_DRAWINGS = {
<path d="M4 20 C 20 20, 22 12, 32 12 S 44 4, 60 4" />
</TileSvg>
),
barsOverlap: (
barsSideBySide: (
<TileSvg>
<rect x={14} y={6} width={11} height={16} {...FILLED} opacity={0.5} />
<rect x={14} y={12} width={11} height={10} {...FILLED} />
<rect x={38} y={10} width={11} height={12} {...FILLED} opacity={0.5} />
<rect x={38} y={14} width={11} height={8} {...FILLED} />
<rect x={10} y={10} width={7} height={12} {...FILLED} opacity={0.5} />
<rect x={18} y={4} width={7} height={18} {...FILLED} />
<rect x={36} y={12} width={7} height={10} {...FILLED} opacity={0.5} />
<rect x={44} y={7} width={7} height={15} {...FILLED} />
</TileSvg>
),
barsStacked: (

View File

@@ -56,7 +56,7 @@ function VisualizationSection({
value?.timePreference ?? DashboardtypesTimePreferenceDTO.global_time;
const barLayout = value?.stackedBarChart
? BarLayout.STACKED
: BarLayout.OVERLAP;
: BarLayout.SIDE_BY_SIDE;
const stack = value?.stack ?? defaultValue?.stack;
return (

View File

@@ -4,15 +4,15 @@ import type { ConfigTileItem } from '../../controls/ConfigTiles/ConfigTiles';
import { TILE_DRAWINGS } from '../../controls/drawings/tileDrawings';
export enum BarLayout {
OVERLAP = 'overlap',
SIDE_BY_SIDE = 'side',
STACKED = 'stacked',
}
export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
{
value: BarLayout.OVERLAP,
label: 'Overlap',
drawing: TILE_DRAWINGS.barsOverlap,
value: BarLayout.SIDE_BY_SIDE,
label: 'Side by side',
drawing: TILE_DRAWINGS.barsSideBySide,
},
{
value: BarLayout.STACKED,
@@ -22,7 +22,8 @@ export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
];
export const BAR_LAYOUT_HELP: Record<BarLayout, string> = {
[BarLayout.OVERLAP]: 'Each series is drawn from zero and may hide others.',
[BarLayout.SIDE_BY_SIDE]:
'Each series gets its own bar, side by side, for easy comparison.',
[BarLayout.STACKED]:
'Bars sit on top of each other, so the height shows the total.',
};

View File

@@ -150,7 +150,7 @@ func (b *traceQueryStatementBuilder) Build(
end = querybuilder.ToNanoSecs(end)
isSelectFieldsEmpty := false
if requestType == qbtypes.RequestTypeRaw || requestType == qbtypes.RequestTypeTrace {
if requestType == qbtypes.RequestTypeRaw {
isSelectFieldsEmpty = len(query.SelectFields) == 0
// we are expanding here to ensure that all the conflicts are taken care in adjustKeys
// i.e if there is a conflict we strip away context of the key in adjustKeys
@@ -180,7 +180,7 @@ func (b *traceQueryStatementBuilder) Build(
case qbtypes.RequestTypeScalar:
return b.buildScalarQuery(ctx, orgID, q, query, start, end, keys, variables, false, false)
case qbtypes.RequestTypeTrace:
return b.buildTraceQuery(ctx, orgID, q, query, start, end, keys, variables, isSelectFieldsEmpty)
return b.buildTraceQuery(ctx, orgID, q, query, start, end, keys, variables)
}
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported request type: %s", requestType)
@@ -446,7 +446,6 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
start, end uint64,
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
isSelectFieldsEmpty bool,
) (*qbtypes.Statement, error) {
startBucket := start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
@@ -484,25 +483,6 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
// Build the inner subquery for root spans
innerSB := sqlbuilder.NewSelectBuilder()
innerSB.Select("trace_id", "duration_nano", sqlbuilder.Escape("resource_string_service$$name as `service.name`"), "name")
var rootFieldAliases []string
info := querybuilder.NewQueryInfo(ctx, orgID, b.fl, telemetrytypes.SignalTraces, nil, start, end)
for i, field := range query.SelectFields {
expr, err := querybuilder.ResolveColumn(ctx, info, b.storage, &field, telemetrytypes.FieldDataTypeUnspecified, keys)
if err != nil {
return nil, err
}
alias := selectColumnAlias(i, field.Name)
innerSB.SelectMore(sqlbuilder.Escape(fmt.Sprintf("%s AS %s", expr, alias)))
rootFieldAliases = append(rootFieldAliases, alias)
}
if isSelectFieldsEmpty {
for _, col := range tracestelemetryschema.ContextualSpanColumns {
innerSB.SelectMore(col)
rootFieldAliases = append(rootFieldAliases, col)
}
}
innerSB.From(fmt.Sprintf("%s.%s", tracestelemetryschema.DBName, tracestelemetryschema.SpanIndexV3TableName))
innerSB.Where("parent_span_id = ''")
@@ -532,23 +512,25 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
mainSB.Select(
"__toe_duration_sorted.`service.name` AS `service.name`",
"__toe_duration_sorted.name AS `name`",
"__toe_span_count.span_count AS span_count",
"count() AS span_count",
"__toe_duration_sorted.duration_nano AS `duration_nano`",
"__toe_duration_sorted.trace_id AS `trace_id`",
)
for _, alias := range rootFieldAliases {
mainSB.SelectMore(sqlbuilder.Escape(fmt.Sprintf("__toe_duration_sorted.%s AS %s", alias, alias)))
}
// Join the distributed table with the inner subquery
// spans are counted before the join so the root span fields are not copied onto every span
mainSB.SQL("FROM __toe_duration_sorted")
mainSB.SQL("INNER JOIN (SELECT trace_id, count() AS span_count FROM __toe GROUP BY trace_id) AS __toe_span_count")
mainSB.SQL("ON __toe_span_count.trace_id = __toe_duration_sorted.trace_id")
mainSB.SQL("FROM __toe")
mainSB.SQL("INNER JOIN __toe_duration_sorted")
mainSB.SQL("ON __toe.trace_id = __toe_duration_sorted.trace_id")
// Group by trace-level fields
mainSB.GroupBy("trace_id", "duration_nano", "name", "`service.name`")
// order by duration only supported for now
mainSB.OrderBy("duration_nano DESC")
// Limit by trace_id to ensure one row per trace
mainSB.SQL("LIMIT 1 BY trace_id")
if query.Limit > 0 {
mainSB.Limit(query.Limit)
} else {

File diff suppressed because one or more lines are too long

View File

@@ -442,3 +442,51 @@ def test_builder_refreshing_a_sliding_time_range(
assert uncached.status_code == HTTPStatus.OK, uncached.text
assert_results_equal(from_cache.json(), uncached.json(), "A", "the twentieth refresh")
def test_builder_rows_scanned_when_refreshing(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[list[Metrics]], None],
) -> None:
metric_name = f"cache_rows_scanned_{uuid4().hex[:8]}"
# 90 minutes back so even the last refresh closes clear of the flux
# interval, which holds recent data out of the cache
start_time = datetime.fromtimestamp(int((datetime.now(tz=UTC) - timedelta(minutes=90)).timestamp()) // 60 * 60, tz=UTC)
start_time_ms = int(start_time.timestamp() * 1000)
query = [build_builder_query("A", metric_name, "max", "max")]
insert_metrics(
[
Metrics(
metric_name=metric_name,
labels={"service": "api"},
timestamp=start_time + timedelta(minutes=minute),
value=minute,
type_="Gauge",
is_monotonic=False,
)
for minute in range(70)
]
)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# a dashboard left open on a one hour range, re-running a minute later each
# time. rowsScanned is what answering the request cost, so a refresh served
# mostly out of the cache cannot report more than reading its range afresh
for refresh in range(10):
refresh_start_ms = start_time_ms + refresh * MINUTE_MS
from_cache = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 60 * MINUTE_MS, query, no_cache=False)
assert from_cache.status_code == HTTPStatus.OK, from_cache.text
uncached = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 60 * MINUTE_MS, query, no_cache=True)
assert uncached.status_code == HTTPStatus.OK, uncached.text
rows_scanned_from_cache = from_cache.json()["data"]["meta"]["rowsScanned"]
rows_scanned_uncached = uncached.json()["data"]["meta"]["rowsScanned"]
assert rows_scanned_uncached > 0, f"refresh {refresh}"
assert rows_scanned_from_cache <= rows_scanned_uncached, f"refresh {refresh}: {rows_scanned_from_cache} rows scanned when served from the cache, {rows_scanned_uncached} when read afresh"