mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-25 23:40:35 +01:00
Compare commits
3 Commits
main
...
scalar-enf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a80c6f6d5 | ||
|
|
507a5a9a3d | ||
|
|
207f36d357 |
@@ -40,20 +40,23 @@ func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var shadow struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &shadow)
|
||||
|
||||
var req dashboardtypes.PostableDashboardV2
|
||||
if err := binding.JSON.BindBody(bytes.NewReader(body), &req); err != nil {
|
||||
// Fallback: the body may be a legacy v1 dashboard. Migrate it to v2 (same
|
||||
// v4→v5 + v1→v2 pass the bulk migration runs) and retry; if it isn't a
|
||||
// convertible v1 payload either, surface the original v2 binding error.
|
||||
if shadow.SchemaVersion == "" && shadow.Version != "" {
|
||||
var data map[string]any
|
||||
if json.Unmarshal(body, &data) != nil {
|
||||
render.Error(rw, err)
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
render.Error(rw, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "%s", err.Error()))
|
||||
return
|
||||
}
|
||||
storable := dashboardtypes.StorableDashboard{Data: data, OrgID: orgID}
|
||||
transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil).Migrate(ctx, storable.Data)
|
||||
v2, convErr := storable.ConvertV1ToV2()
|
||||
if convErr != nil {
|
||||
v2, err := storable.ConvertV1ToV2()
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
@@ -63,6 +66,9 @@ func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
|
||||
Tags: tagtypes.NewPostableTagsFromTags(v2.Tags),
|
||||
Spec: v2.Spec,
|
||||
}
|
||||
} else if err := binding.JSON.BindBody(bytes.NewReader(body), &req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := handler.module.CreateV2(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
|
||||
|
||||
@@ -123,6 +123,10 @@ func newPromqlQuery(
|
||||
}
|
||||
|
||||
func (q *promqlQuery) Fingerprint() string {
|
||||
if q.requestType != qbv5.RequestTypeTimeSeries {
|
||||
return ""
|
||||
}
|
||||
|
||||
query, err := q.renderVars(q.query.Query, q.vars, q.tr.From, q.tr.To)
|
||||
if err != nil {
|
||||
q.logger.ErrorContext(context.TODO(), "failed render template variables", slog.String("query", q.query.Query))
|
||||
@@ -361,16 +365,37 @@ func (q *promqlQuery) Execute(ctx context.Context) (*qbv5.Result, error) {
|
||||
|
||||
warnings, _ := res.Warnings.AsStrings(query, 10, 0)
|
||||
|
||||
return &qbv5.Result{
|
||||
Type: q.requestType,
|
||||
Value: &qbv5.TimeSeriesData{
|
||||
QueryName: q.query.Name,
|
||||
Aggregations: []*qbv5.AggregationBucket{
|
||||
{
|
||||
Series: series,
|
||||
},
|
||||
tsData := &qbv5.TimeSeriesData{
|
||||
QueryName: q.query.Name,
|
||||
Aggregations: []*qbv5.AggregationBucket{
|
||||
{
|
||||
Series: series,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var payload any = tsData
|
||||
// Scalar requests must return scalar data; reduce each series to its
|
||||
// last point. This approximates an instant query evaluated at the end
|
||||
// of the window: the final range-eval step sees the same samples an
|
||||
// instant query at that timestamp would, except a series that went
|
||||
// stale mid-window still surfaces its most recent value instead of
|
||||
// dropping out of the result.
|
||||
// TODO(srikanthccv): "last" mirrors the instant-query semantics we
|
||||
// have always followed for PromQL scalar requests, but it should be
|
||||
// configurable on the PromQuery spec just like the builder reduceTo.
|
||||
if q.requestType == qbv5.RequestTypeScalar {
|
||||
for _, aggBucket := range tsData.Aggregations {
|
||||
for i, s := range aggBucket.Series {
|
||||
aggBucket.Series[i] = qbv5.FunctionReduceTo(s, qbv5.ReduceToLast)
|
||||
}
|
||||
}
|
||||
payload = convertTimeSeriesDataToScalar(tsData, q.query.Name)
|
||||
}
|
||||
|
||||
return &qbv5.Result{
|
||||
Type: q.requestType,
|
||||
Value: payload,
|
||||
Warnings: warnings,
|
||||
// TODO: map promql stats?
|
||||
}, nil
|
||||
|
||||
@@ -322,6 +322,10 @@ func (ReduceTo) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
func (r ReduceTo) IsValid() bool {
|
||||
return slices.ContainsFunc(r.Enum(), func(v any) bool { return v == r })
|
||||
}
|
||||
|
||||
// FunctionReduceTo applies the reduceTo operator to a time series and returns a new series with the reduced value
|
||||
// reduceTo can be one of: last, sum, avg, min, max, count, median
|
||||
// if reduceTo is not recognized, the function returns the original series.
|
||||
|
||||
@@ -78,6 +78,7 @@ type validationConfig struct {
|
||||
skipSelectFieldValidation bool
|
||||
skipGroupByValidation bool
|
||||
withTimestampGroupByValidation bool
|
||||
withReduceToValidation bool
|
||||
}
|
||||
|
||||
func applyValidationOptions(opts []ValidationOption) validationConfig {
|
||||
@@ -143,6 +144,15 @@ func WithTimestampGroupByValidation() ValidationOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithReduceToValidation enables validation that metric aggregations carry a
|
||||
// valid reduceTo operator. Used for scalar request types, where the time
|
||||
// series produced by a metric query must be reduced to a single value.
|
||||
func WithReduceToValidation() ValidationOption {
|
||||
return func(cfg *validationConfig) {
|
||||
cfg.withReduceToValidation = true
|
||||
}
|
||||
}
|
||||
|
||||
// Validate performs preliminary validation on QueryBuilderQuery.
|
||||
func (q *QueryBuilderQuery[T]) Validate(opts ...ValidationOption) error {
|
||||
cfg := applyValidationOptions(opts)
|
||||
@@ -279,6 +289,29 @@ func (q *QueryBuilderQuery[T]) validateAggregations(cfg validationConfig) error
|
||||
"invalid space aggregation, should be one of the following: [`sum`, `avg`, `min`, `max`, `count`, `p50`, `p75`, `p90`, `p95`, `p99`]",
|
||||
)
|
||||
}
|
||||
if cfg.withReduceToValidation && !v.ReduceTo.IsValid() {
|
||||
aggId := fmt.Sprintf("aggregation #%d", i+1)
|
||||
if q.Name != "" {
|
||||
aggId = fmt.Sprintf("aggregation #%d in query '%s'", i+1, q.Name)
|
||||
}
|
||||
if v.ReduceTo == ReduceToUnknown {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"reduceTo is required for %s for the scalar request type",
|
||||
aggId,
|
||||
).WithAdditional(
|
||||
"Metric queries produce a time series; scalar requests must specify how to reduce it to a single value. Valid values are: sum, count, avg, min, max, last, median",
|
||||
)
|
||||
}
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"invalid reduceTo `%s` for %s",
|
||||
v.ReduceTo.StringValue(),
|
||||
aggId,
|
||||
).WithAdditional(
|
||||
"Valid values are: sum, count, avg, min, max, last, median",
|
||||
)
|
||||
}
|
||||
case TraceAggregation:
|
||||
if v.Expression == "" {
|
||||
aggId := fmt.Sprintf("aggregation #%d", i+1)
|
||||
@@ -790,7 +823,7 @@ func GetValidationOptions(requestType RequestType) []ValidationOption {
|
||||
case RequestTypeTimeSeries:
|
||||
return []ValidationOption{WithSkipSelectFieldValidation(), WithTimestampGroupByValidation()}
|
||||
case RequestTypeScalar:
|
||||
return []ValidationOption{WithSkipSelectFieldValidation()}
|
||||
return []ValidationOption{WithSkipSelectFieldValidation(), WithReduceToValidation()}
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace:
|
||||
return []ValidationOption{WithSkipAggregationValidation(), WithSkipHavingValidation(), WithSkipAggregationOrderBy(), WithSkipGroupByValidation()}
|
||||
default:
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
@@ -769,6 +770,100 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "scalar request with metric query without reduceTo should return error",
|
||||
request: QueryRangeRequest{
|
||||
Start: 1640995200000,
|
||||
End: 1640998800000,
|
||||
RequestType: RequestTypeScalar,
|
||||
CompositeQuery: CompositeQuery{
|
||||
Queries: []QueryEnvelope{
|
||||
{
|
||||
Type: QueryTypeBuilder,
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []MetricAggregation{
|
||||
{MetricName: "test_metric", SpaceAggregation: metrictypes.SpaceAggregationSum},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "reduceTo is required",
|
||||
},
|
||||
{
|
||||
name: "scalar request with metric query with invalid reduceTo should return error",
|
||||
request: QueryRangeRequest{
|
||||
Start: 1640995200000,
|
||||
End: 1640998800000,
|
||||
RequestType: RequestTypeScalar,
|
||||
CompositeQuery: CompositeQuery{
|
||||
Queries: []QueryEnvelope{
|
||||
{
|
||||
Type: QueryTypeBuilder,
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []MetricAggregation{
|
||||
{MetricName: "test_metric", SpaceAggregation: metrictypes.SpaceAggregationSum, ReduceTo: ReduceTo{valuer.NewString("p99")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "invalid reduceTo",
|
||||
},
|
||||
{
|
||||
name: "scalar request with metric query with reduceTo should pass",
|
||||
request: QueryRangeRequest{
|
||||
Start: 1640995200000,
|
||||
End: 1640998800000,
|
||||
RequestType: RequestTypeScalar,
|
||||
CompositeQuery: CompositeQuery{
|
||||
Queries: []QueryEnvelope{
|
||||
{
|
||||
Type: QueryTypeBuilder,
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []MetricAggregation{
|
||||
{MetricName: "test_metric", SpaceAggregation: metrictypes.SpaceAggregationSum, ReduceTo: ReduceToLast},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "timeseries request with metric query without reduceTo should pass",
|
||||
request: QueryRangeRequest{
|
||||
Start: 1640995200000,
|
||||
End: 1640998800000,
|
||||
RequestType: RequestTypeTimeSeries,
|
||||
CompositeQuery: CompositeQuery{
|
||||
Queries: []QueryEnvelope{
|
||||
{
|
||||
Type: QueryTypeBuilder,
|
||||
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
Aggregations: []MetricAggregation{
|
||||
{MetricName: "test_metric", SpaceAggregation: metrictypes.SpaceAggregationSum},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
6
tests/fixtures/querier.py
vendored
6
tests/fixtures/querier.py
vendored
@@ -693,13 +693,17 @@ def build_metrics_aggregation(
|
||||
time_aggregation: str,
|
||||
space_aggregation: str,
|
||||
temporality: str = "cumulative",
|
||||
reduce_to: str | None = None,
|
||||
) -> dict:
|
||||
return {
|
||||
agg = {
|
||||
"metricName": metric_name,
|
||||
"temporality": temporality,
|
||||
"timeAggregation": time_aggregation,
|
||||
"spaceAggregation": space_aggregation,
|
||||
}
|
||||
if reduce_to:
|
||||
agg["reduceTo"] = reduce_to
|
||||
return agg
|
||||
|
||||
|
||||
def get_scalar_table_data(response_json: dict) -> list[list[Any]]:
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_metrics_group_by_context_collapse(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
group_by=[querier.build_group_by_field("region", "string", ctx)],
|
||||
)
|
||||
],
|
||||
@@ -96,7 +96,7 @@ def test_metrics_filter_label_context(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
group_by=[querier.build_group_by_field("region", "string", "")],
|
||||
filter_expression=expr,
|
||||
)
|
||||
@@ -116,7 +116,7 @@ def test_metrics_filter_label_context(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
filter_expression='resource.region = "us"',
|
||||
)
|
||||
],
|
||||
@@ -157,7 +157,7 @@ def test_metrics_group_by_unknown_label(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
group_by=[querier.build_group_by_field("does_not_exist_label", "string", "attribute")],
|
||||
)
|
||||
],
|
||||
@@ -201,7 +201,7 @@ def test_metrics_filter_unknown_label_matches_nothing(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
filter_expression='does_not_exist_label = "x"',
|
||||
)
|
||||
],
|
||||
@@ -247,7 +247,7 @@ def test_metrics_full_text_filter_does_not_error(
|
||||
querier.build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified")],
|
||||
aggregations=[querier.build_metrics_aggregation(METRIC, "latest", "sum", "unspecified", reduce_to="last")],
|
||||
filter_expression=expr,
|
||||
)
|
||||
],
|
||||
|
||||
@@ -44,7 +44,7 @@ def build_metrics_query(
|
||||
order_by: list[tuple[str, str]] | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict:
|
||||
aggs = [querier.build_metrics_aggregation(metric_name, time_aggregation, space_aggregation, "unspecified")]
|
||||
aggs = [querier.build_metrics_aggregation(metric_name, time_aggregation, space_aggregation, "unspecified", reduce_to="last")]
|
||||
gb = [querier.build_group_by_field(f, "string", "attribute") for f in group_by] if group_by else None
|
||||
order = [querier.build_order_by(name, direction) for name, direction in order_by] if order_by else None
|
||||
|
||||
|
||||
@@ -70,3 +70,39 @@ def test_metrics_scalar_reduce_to_modes(
|
||||
assert response.json()["status"] == "success"
|
||||
data = get_scalar_table_data(response.json())
|
||||
assert_scalar_result_order(data, [("service-a", expected)], f"metrics reduceTo={reduce_to}")
|
||||
|
||||
|
||||
def test_metrics_scalar_missing_reduce_to_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
query = build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[build_metrics_aggregation("test.reduce.metric", "latest", "sum", "unspecified")],
|
||||
)
|
||||
response = make_scalar_query_request(signoz, token, now, [query])
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "reduceTo is required" in response.json()["error"]["message"], response.text
|
||||
|
||||
|
||||
def test_metrics_scalar_invalid_reduce_to_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
aggregation = build_metrics_aggregation("test.reduce.metric", "latest", "sum", "unspecified")
|
||||
aggregation["reduceTo"] = "p99"
|
||||
query = build_scalar_query(name="A", signal="metrics", aggregations=[aggregation])
|
||||
response = make_scalar_query_request(signoz, token, now, [query])
|
||||
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "invalid reduceTo" in response.json()["error"]["message"], response.text
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_metrics_scalar_space_aggregations(
|
||||
query = build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[build_metrics_aggregation("test.metric", "latest", space_aggregation, "unspecified")],
|
||||
aggregations=[build_metrics_aggregation("test.metric", "latest", space_aggregation, "unspecified", reduce_to="last")],
|
||||
)
|
||||
response = make_scalar_query_request(signoz, token, now, [query])
|
||||
|
||||
@@ -96,7 +96,7 @@ def test_metrics_scalar_having(
|
||||
query = build_scalar_query(
|
||||
name="A",
|
||||
signal="metrics",
|
||||
aggregations=[build_metrics_aggregation("test.metric", "latest", "sum", "unspecified")],
|
||||
aggregations=[build_metrics_aggregation("test.metric", "latest", "sum", "unspecified", reduce_to="last")],
|
||||
group_by=[build_group_by_field("service.name", "string", "attribute")],
|
||||
having_expression=having_expression,
|
||||
)
|
||||
|
||||
179
tests/integration/tests/querierscalar/08_promql.py
Normal file
179
tests/integration/tests/querierscalar/08_promql.py
Normal file
@@ -0,0 +1,179 @@
|
||||
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.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
RequestType,
|
||||
find_named_result,
|
||||
get_scalar_table_data,
|
||||
make_query_request,
|
||||
make_scalar_query_request,
|
||||
)
|
||||
|
||||
# PromQL scalar contract: a scalar request must return scalar data
|
||||
# (columns + data rows), not time series data. PromQL has no reduceTo, so
|
||||
# each series is reduced with last-value semantics.
|
||||
|
||||
METRIC = "test_promql_scalar_metric"
|
||||
|
||||
|
||||
def build_promql_query(query: str, name: str = "A", step: int = 60) -> dict:
|
||||
return {"type": "promql", "spec": {"name": name, "query": query, "disabled": False, "step": step}}
|
||||
|
||||
|
||||
def insert_gauge_samples(insert_metrics: Callable[[list[Metrics]], None], base: datetime) -> None:
|
||||
samples = []
|
||||
for service, values in [("svc-a", (10.0, 20.0, 30.0)), ("svc-b", (40.0, 50.0, 60.0))]:
|
||||
for i, value in enumerate(values):
|
||||
samples.append(
|
||||
Metrics(
|
||||
metric_name=METRIC,
|
||||
labels={"service": service},
|
||||
timestamp=base - timedelta(seconds=121 - 60 * i),
|
||||
value=value,
|
||||
temporality="Unspecified",
|
||||
type_="Gauge",
|
||||
is_monotonic=False,
|
||||
)
|
||||
)
|
||||
insert_metrics(samples)
|
||||
|
||||
|
||||
def test_promql_scalar_returns_scalar_data(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
insert_gauge_samples(insert_metrics, now)
|
||||
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,
|
||||
end_ms,
|
||||
[build_promql_query(f"sum by (service) ({METRIC})")],
|
||||
request_type=RequestType.SCALAR,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
result = find_named_result(response.json()["data"]["data"]["results"], "A")
|
||||
assert result is not None, response.text
|
||||
assert "aggregations" not in result, f"scalar result must not carry time series aggregations: {result}"
|
||||
assert "columns" in result and "data" in result, result
|
||||
|
||||
group_cols = [c for c in result["columns"] if c["columnType"] == "group"]
|
||||
agg_cols = [c for c in result["columns"] if c["columnType"] == "aggregation"]
|
||||
assert [c["name"] for c in group_cols] == ["service"], result["columns"]
|
||||
assert [c["name"] for c in agg_cols] == ["__result_0"], result["columns"]
|
||||
|
||||
rows = sorted(result["data"], key=lambda r: r[0])
|
||||
assert rows == [["svc-a", 30.0], ["svc-b", 60.0]], rows
|
||||
|
||||
|
||||
def test_promql_scalar_format_for_ui(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
insert_gauge_samples(insert_metrics, now)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
now,
|
||||
[build_promql_query(f"sum by (service) ({METRIC})")],
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["status"] == "success"
|
||||
data = get_scalar_table_data(response.json())
|
||||
assert data == [["svc-b", 60.0], ["svc-a", 30.0]], data
|
||||
|
||||
|
||||
def test_promql_timeseries_unaffected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
insert_gauge_samples(insert_metrics, now)
|
||||
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,
|
||||
end_ms,
|
||||
[build_promql_query(f"sum by (service) ({METRIC})")],
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
result = find_named_result(response.json()["data"]["data"]["results"], "A")
|
||||
assert result is not None, response.text
|
||||
assert "aggregations" in result, result
|
||||
|
||||
series = result["aggregations"][0]["series"]
|
||||
assert len(series) == 2, series
|
||||
last_by_service = {s["labels"][0]["value"]: s["values"][-1]["value"] for s in series}
|
||||
assert last_by_service == {"svc-a": 30.0, "svc-b": 60.0}, last_by_service
|
||||
|
||||
|
||||
def test_promql_scalar_not_served_cached_time_series(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
base = now - timedelta(minutes=10)
|
||||
insert_gauge_samples(insert_metrics, base)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((base - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(base.timestamp() * 1000)
|
||||
queries = [build_promql_query(f"sum by (service) ({METRIC})")]
|
||||
|
||||
ts_response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
queries,
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
no_cache=False,
|
||||
)
|
||||
assert ts_response.status_code == HTTPStatus.OK, ts_response.text
|
||||
|
||||
scalar_response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
queries,
|
||||
request_type=RequestType.SCALAR,
|
||||
no_cache=False,
|
||||
)
|
||||
assert scalar_response.status_code == HTTPStatus.OK, scalar_response.text
|
||||
|
||||
result = find_named_result(scalar_response.json()["data"]["data"]["results"], "A")
|
||||
assert result is not None, scalar_response.text
|
||||
assert "aggregations" not in result, f"scalar result served cached time series data: {result}"
|
||||
rows = sorted(result["data"], key=lambda r: r[0])
|
||||
assert rows == [["svc-a", 30.0], ["svc-b", 60.0]], rows
|
||||
Reference in New Issue
Block a user