From 14011bc277d13a9988c40a0e4b7771ca1fd61b6f Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 20 Jan 2026 16:47:08 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20do=20not=20sort=20in=20descending=20loca?= =?UTF-8?q?lly=20if=20the=20other=20is=20explicitly=20spe=E2=80=A6=20(#100?= =?UTF-8?q?33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/querier/postprocess.go | 55 +- .../querybuildertypesv5/req.go | 25 + tests/integration/fixtures/idputils.py | 78 +- tests/integration/fixtures/querier.py | 127 +++ .../src/callbackauthn/01_domain.py | 7 +- .../integration/src/callbackauthn/02_saml.py | 76 +- .../integration/src/callbackauthn/03_oidc.py | 76 +- .../src/querier/06_order_by_table_querier.py | 1004 +++++++++++++++++ 8 files changed, 1340 insertions(+), 108 deletions(-) create mode 100644 tests/integration/src/querier/06_order_by_table_querier.py diff --git a/pkg/querier/postprocess.go b/pkg/querier/postprocess.go index a3a65112af..eb83a203a8 100644 --- a/pkg/querier/postprocess.go +++ b/pkg/querier/postprocess.go @@ -318,13 +318,14 @@ func (q *querier) applyFormulas(ctx context.Context, results map[string]*qbtypes } // Check if we're dealing with time series or scalar data - if req.RequestType == qbtypes.RequestTypeTimeSeries { + switch req.RequestType { + case qbtypes.RequestTypeTimeSeries: result := q.processTimeSeriesFormula(ctx, results, formula, req) if result != nil { result = q.applySeriesLimit(result, formula.Limit, formula.Order) results[name] = result } - } else if req.RequestType == qbtypes.RequestTypeScalar { + case qbtypes.RequestTypeScalar: result := q.processScalarFormula(ctx, results, formula, req) if result != nil { result = q.applySeriesLimit(result, formula.Limit, formula.Order) @@ -581,11 +582,14 @@ func (q *querier) filterDisabledQueries(results map[string]*qbtypes.Result, req } // formatScalarResultsAsTable formats scalar results as a unified table for UI display -func (q *querier) formatScalarResultsAsTable(results map[string]*qbtypes.Result, _ *qbtypes.QueryRangeRequest) map[string]any { +func (q *querier) formatScalarResultsAsTable(results map[string]*qbtypes.Result, req *qbtypes.QueryRangeRequest) map[string]any { if len(results) == 0 { return map[string]any{"table": &qbtypes.ScalarData{}} } + // apply default sorting if no order specified + applyDefaultSort := !req.HasOrderSpecified() + // Convert all results to ScalarData first scalarResults := make(map[string]*qbtypes.ScalarData) for name, result := range results { @@ -600,13 +604,13 @@ func (q *querier) formatScalarResultsAsTable(results map[string]*qbtypes.Result, if len(scalarResults) == 1 { for _, sd := range scalarResults { if hasMultipleQueries(sd) { - return map[string]any{"table": deduplicateRows(sd)} + return map[string]any{"table": deduplicateRows(sd, applyDefaultSort)} } } } // Otherwise merge all results - merged := mergeScalarData(scalarResults) + merged := mergeScalarData(scalarResults, applyDefaultSort) return map[string]any{"table": merged} } @@ -687,7 +691,7 @@ func hasMultipleQueries(sd *qbtypes.ScalarData) bool { } // deduplicateRows removes duplicate rows based on group columns -func deduplicateRows(sd *qbtypes.ScalarData) *qbtypes.ScalarData { +func deduplicateRows(sd *qbtypes.ScalarData, applyDefaultSort bool) *qbtypes.ScalarData { // Find group column indices groupIndices := []int{} for i, col := range sd.Columns { @@ -696,8 +700,9 @@ func deduplicateRows(sd *qbtypes.ScalarData) *qbtypes.ScalarData { } } - // Build unique rows map + // Build unique rows map, preserve order uniqueRows := make(map[string][]any) + var keyOrder []string for _, row := range sd.Data { key := buildRowKey(row, groupIndices) if existing, found := uniqueRows[key]; found { @@ -711,17 +716,20 @@ func deduplicateRows(sd *qbtypes.ScalarData) *qbtypes.ScalarData { rowCopy := make([]any, len(row)) copy(rowCopy, row) uniqueRows[key] = rowCopy + keyOrder = append(keyOrder, key) } } - // Convert back to slice + // Convert back to slice, preserve the original order data := make([][]any, 0, len(uniqueRows)) - for _, row := range uniqueRows { - data = append(data, row) + for _, key := range keyOrder { + data = append(data, uniqueRows[key]) } - // Sort by first aggregation column - sortByFirstAggregation(data, sd.Columns) + // sort by first aggregation (descending) if no order was specified + if applyDefaultSort { + sortByFirstAggregation(data, sd.Columns) + } return &qbtypes.ScalarData{ Columns: sd.Columns, @@ -730,7 +738,7 @@ func deduplicateRows(sd *qbtypes.ScalarData) *qbtypes.ScalarData { } // mergeScalarData merges multiple scalar data results -func mergeScalarData(results map[string]*qbtypes.ScalarData) *qbtypes.ScalarData { +func mergeScalarData(results map[string]*qbtypes.ScalarData, applyDefaultSort bool) *qbtypes.ScalarData { // Collect unique group columns groupCols := []string{} groupColMap := make(map[string]*qbtypes.ColumnDescriptor) @@ -770,10 +778,12 @@ func mergeScalarData(results map[string]*qbtypes.ScalarData) *qbtypes.ScalarData } } - // Merge rows + // Merge rows, preserve order rowMap := make(map[string][]any) + var keyOrder []string - for queryName, sd := range results { + for _, queryName := range queryNames { + sd := results[queryName] // Create index mappings groupMap := make(map[string]int) for i, col := range sd.Columns { @@ -802,6 +812,7 @@ func mergeScalarData(results map[string]*qbtypes.ScalarData) *qbtypes.ScalarData newRow[i] = "n/a" } rowMap[key] = newRow + keyOrder = append(keyOrder, key) } // Set aggregation values for this query @@ -825,14 +836,16 @@ func mergeScalarData(results map[string]*qbtypes.ScalarData) *qbtypes.ScalarData } } - // Convert to slice + // Convert to slice, preserving insertion order data := make([][]any, 0, len(rowMap)) - for _, row := range rowMap { - data = append(data, row) + for _, key := range keyOrder { + data = append(data, rowMap[key]) } - // Sort by first aggregation column - sortByFirstAggregation(data, columns) + // sort by first aggregation (descending) if no order was specified + if applyDefaultSort { + sortByFirstAggregation(data, columns) + } return &qbtypes.ScalarData{ Columns: columns, @@ -888,7 +901,7 @@ func sortByFirstAggregation(data [][]any, columns []*qbtypes.ColumnDescriptor) { // compareValues compares two values for sorting (handles n/a and numeric types) func compareValues(a, b any) int { - // Handle n/a values + // n/a values gets pushed to the end if a == "n/a" && b == "n/a" { return 0 } diff --git a/pkg/types/querybuildertypes/querybuildertypesv5/req.go b/pkg/types/querybuildertypes/querybuildertypesv5/req.go index 0f2c64204e..985809d866 100644 --- a/pkg/types/querybuildertypes/querybuildertypesv5/req.go +++ b/pkg/types/querybuildertypes/querybuildertypesv5/req.go @@ -276,6 +276,31 @@ func (r *QueryRangeRequest) NumAggregationForQuery(name string) int64 { return int64(numAgg) } +// HasOrderSpecified returns true if any query has an explicit order provided. +func (r *QueryRangeRequest) HasOrderSpecified() bool { + for _, query := range r.CompositeQuery.Queries { + switch spec := query.Spec.(type) { + case QueryBuilderQuery[TraceAggregation]: + if len(spec.Order) > 0 { + return true + } + case QueryBuilderQuery[LogAggregation]: + if len(spec.Order) > 0 { + return true + } + case QueryBuilderQuery[MetricAggregation]: + if len(spec.Order) > 0 { + return true + } + case QueryBuilderFormula: + if len(spec.Order) > 0 { + return true + } + } + } + return false +} + func (r *QueryRangeRequest) FuncsForQuery(name string) []Function { funcs := []Function{} for _, query := range r.CompositeQuery.Queries { diff --git a/tests/integration/fixtures/idputils.py b/tests/integration/fixtures/idputils.py index 30c6a54566..56007dd556 100644 --- a/tests/integration/fixtures/idputils.py +++ b/tests/integration/fixtures/idputils.py @@ -122,7 +122,7 @@ def create_saml_client( "config": { "full.path": "false", "attribute.nameformat": "Basic", - "single": "true", # ! this was changed to true as we need the groups in the single attribute section + "single": "true", # ! this was changed to true as we need the groups in the single attribute section "friendly.name": "groups", "attribute.name": "groups", }, @@ -322,7 +322,9 @@ def get_oidc_settings(idp: types.TestContainerIDP) -> dict: @pytest.fixture(name="create_user_idp", scope="function") -def create_user_idp(idp: types.TestContainerIDP) -> Callable[[str, str, bool, str, str], None]: +def create_user_idp( + idp: types.TestContainerIDP, +) -> Callable[[str, str, bool, str, str], None]: client = KeycloakAdmin( server_url=idp.container.host_configs["6060"].base(), username=IDP_ROOT_USERNAME, @@ -332,7 +334,13 @@ def create_user_idp(idp: types.TestContainerIDP) -> Callable[[str, str, bool, st created_users = [] - def _create_user_idp(email: str, password: str, verified: bool = True, first_name: str = "", last_name: str = "") -> None: + def _create_user_idp( + email: str, + password: str, + verified: bool = True, + first_name: str = "", + last_name: str = "", + ) -> None: payload = { "username": email, "email": email, @@ -400,14 +408,14 @@ def create_group_idp(idp: types.TestContainerIDP) -> Callable[[str], str]: for group_id in created_groups: try: client.delete_group(group_id) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # pylint: disable=broad-exception-caught pass @pytest.fixture(name="create_user_idp_with_groups", scope="function") def create_user_idp_with_groups( idp: types.TestContainerIDP, - create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name + create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name ) -> Callable[[str, str, bool, List[str]], None]: """Creates a user in Keycloak IDP with specified groups.""" client = KeycloakAdmin( @@ -450,14 +458,14 @@ def create_user_idp_with_groups( for user_id in created_users: try: client.delete_user(user_id) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # pylint: disable=broad-exception-caught pass @pytest.fixture(name="add_user_to_group", scope="function") def add_user_to_group( idp: types.TestContainerIDP, - create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name + create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name ) -> Callable[[str, str], None]: """Adds an existing user to a group.""" client = KeycloakAdmin( @@ -478,7 +486,7 @@ def add_user_to_group( @pytest.fixture(name="create_user_idp_with_role", scope="function") def create_user_idp_with_role( idp: types.TestContainerIDP, - create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name + create_group_idp: Callable[[str], str], # pylint: disable=redefined-outer-name ) -> Callable[[str, str, bool, str, List[str]], None]: """Creates a user in Keycloak IDP with a custom role attribute and optional groups.""" client = KeycloakAdmin( @@ -524,13 +532,14 @@ def create_user_idp_with_role( for user_id in created_users: try: client.delete_user(user_id) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # pylint: disable=broad-exception-caught pass @pytest.fixture(name="setup_user_profile", scope="package") def setup_user_profile(idp: types.TestContainerIDP) -> Callable[[], None]: """Setup Keycloak User Profile with signoz_role attribute.""" + def _setup_user_profile() -> None: client = KeycloakAdmin( server_url=idp.container.host_configs["6060"].base(), @@ -538,35 +547,36 @@ def setup_user_profile(idp: types.TestContainerIDP) -> Callable[[], None]: password=IDP_ROOT_PASSWORD, realm_name="master", ) - + # Get current user profile config profile = client.get_realm_users_profile() - + # Check if signoz_role attribute already exists attributes = profile.get("attributes", []) - signoz_role_exists = any(attr.get("name") == "signoz_role" for attr in attributes) - + signoz_role_exists = any( + attr.get("name") == "signoz_role" for attr in attributes + ) + if not signoz_role_exists: # Add signoz_role attribute to user profile - attributes.append({ - "name": "signoz_role", - "displayName": "SigNoz Role", - "validations": {}, - "annotations": {}, - # "required": { - # "roles": [] # Not required - # }, - "permissions": { - "view": ["admin", "user"], - "edit": ["admin"] - }, - "multivalued": False - }) + attributes.append( + { + "name": "signoz_role", + "displayName": "SigNoz Role", + "validations": {}, + "annotations": {}, + # "required": { + # "roles": [] # Not required + # }, + "permissions": {"view": ["admin", "user"], "edit": ["admin"]}, + "multivalued": False, + } + ) profile["attributes"] = attributes - + # Update the realm user profile client.update_realm_users_profile(payload=profile) - + return _setup_user_profile @@ -575,7 +585,7 @@ def _ensure_groups_client_scope(client: KeycloakAdmin) -> None: # Check if groups scope exists scopes = client.get_client_scopes() groups_scope_exists = any(s.get("name") == "groups" for s in scopes) - + if not groups_scope_exists: # Create the groups client scope client.create_client_scope( @@ -652,11 +662,11 @@ def get_user_by_email(signoz: types.SigNoz, admin_token: str, email: str) -> dic def perform_oidc_login( - signoz: types.SigNoz, # pylint: disable=unused-argument + signoz: types.SigNoz, # pylint: disable=unused-argument idp: types.TestContainerIDP, driver: webdriver.Chrome, get_session_context: Callable[[str], str], - idp_login: Callable[[str, str], None], # pylint: disable=redefined-outer-name + idp_login: Callable[[str, str], None], # pylint: disable=redefined-outer-name email: str, password: str, ) -> None: @@ -688,10 +698,10 @@ def get_saml_domain(signoz: types.SigNoz, admin_token: str) -> dict: def perform_saml_login( - signoz: types.SigNoz, # pylint: disable=unused-argument + signoz: types.SigNoz, # pylint: disable=unused-argument driver: webdriver.Chrome, get_session_context: Callable[[str], str], - idp_login: Callable[[str, str], None], # pylint: disable=redefined-outer-name + idp_login: Callable[[str, str], None], # pylint: disable=redefined-outer-name email: str, password: str, ) -> None: diff --git a/tests/integration/fixtures/querier.py b/tests/integration/fixtures/querier.py index a3090e7098..8710afb1fe 100644 --- a/tests/integration/fixtures/querier.py +++ b/tests/integration/fixtures/querier.py @@ -329,3 +329,130 @@ def find_named_result( ), None, ) + + +def build_scalar_query( + name: str, + signal: str, + aggregations: List[Dict], + *, + group_by: Optional[List[Dict]] = None, + order: Optional[List[Dict]] = None, + limit: Optional[int] = None, + filter_expression: Optional[str] = None, + having_expression: Optional[str] = None, + step_interval: int = DEFAULT_STEP_INTERVAL, + disabled: bool = False, +) -> Dict: + spec: Dict[str, Any] = { + "name": name, + "signal": signal, + "stepInterval": step_interval, + "disabled": disabled, + "aggregations": aggregations, + } + + if group_by: + spec["groupBy"] = group_by + + if order: + spec["order"] = order + + if limit is not None: + spec["limit"] = limit + + if filter_expression: + spec["filter"] = {"expression": filter_expression} + + if having_expression: + spec["having"] = {"expression": having_expression} + + return {"type": "builder_query", "spec": spec} + + +def build_group_by_field( + name: str, + field_data_type: str = "string", + field_context: str = "resource", +) -> Dict: + return { + "name": name, + "fieldDataType": field_data_type, + "fieldContext": field_context, + } + + +def build_order_by(name: str, direction: str = "desc") -> Dict: + return {"key": {"name": name}, "direction": direction} + + +def build_logs_aggregation(expression: str, alias: Optional[str] = None) -> Dict: + agg: Dict[str, Any] = {"expression": expression} + if alias: + agg["alias"] = alias + return agg + + +def build_metrics_aggregation( + metric_name: str, + time_aggregation: str, + space_aggregation: str, + temporality: str = "cumulative", +) -> Dict: + return { + "metricName": metric_name, + "temporality": temporality, + "timeAggregation": time_aggregation, + "spaceAggregation": space_aggregation, + } + + +def get_scalar_table_data(response_json: Dict) -> List[List[Any]]: + results = response_json.get("data", {}).get("data", {}).get("results", []) + if not results: + return [] + return results[0].get("data", []) + + +def get_scalar_columns(response_json: Dict) -> List[Dict]: + results = response_json.get("data", {}).get("data", {}).get("results", []) + if not results: + return [] + return results[0].get("columns", []) + + +def assert_scalar_result_order( + data: List[List[Any]], + expected_order: List[tuple], + context: str = "", +) -> None: + assert len(data) == len(expected_order), ( + f"{context}: Expected {len(expected_order)} rows, got {len(data)}. " + f"Data: {data}" + ) + + for i, (row, expected) in enumerate(zip(data, expected_order)): + for j, expected_val in enumerate(expected): + actual_val = row[j] + assert actual_val == expected_val, ( + f"{context}: Row {i}, column {j} mismatch. " + f"Expected {expected_val}, got {actual_val}. " + f"Full row: {row}, expected: {expected}" + ) + + +def assert_scalar_column_order( + data: List[List[Any]], + column_index: int, + expected_values: List[Any], + context: str = "", +) -> None: + assert len(data) == len( + expected_values + ), f"{context}: Expected {len(expected_values)} rows, got {len(data)}" + + actual_values = [row[column_index] for row in data] + assert actual_values == expected_values, ( + f"{context}: Column {column_index} order mismatch. " + f"Expected {expected_values}, got {actual_values}" + ) diff --git a/tests/integration/src/callbackauthn/01_domain.py b/tests/integration/src/callbackauthn/01_domain.py index 338e07d149..b71bfe0cdd 100644 --- a/tests/integration/src/callbackauthn/01_domain.py +++ b/tests/integration/src/callbackauthn/01_domain.py @@ -78,11 +78,14 @@ def test_create_and_get_domain( assert response.status_code == HTTPStatus.OK assert response.json()["status"] == "success" data = response.json()["data"] - + assert len(data) == 2 for domain in data: - assert domain["name"] in ["domain-google.integration.test", "domain-saml.integration.test"] + assert domain["name"] in [ + "domain-google.integration.test", + "domain-saml.integration.test", + ] assert domain["ssoType"] in ["google_auth", "saml"] diff --git a/tests/integration/src/callbackauthn/02_saml.py b/tests/integration/src/callbackauthn/02_saml.py index 5d9794d839..cd68486791 100644 --- a/tests/integration/src/callbackauthn/02_saml.py +++ b/tests/integration/src/callbackauthn/02_saml.py @@ -1,17 +1,21 @@ +import uuid from http import HTTPStatus from typing import Any, Callable, Dict, List import requests from selenium import webdriver from wiremock.resources.mappings import Mapping -import uuid from fixtures.auth import ( USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license, ) -from fixtures.idputils import get_saml_domain, perform_saml_login, get_user_by_email, delete_keycloak_client +from fixtures.idputils import ( + get_saml_domain, + get_user_by_email, + perform_saml_login, +) from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP @@ -258,7 +262,9 @@ def test_saml_role_mapping_single_group_admin( email = "admin-group-user@saml.integration.test" create_user_idp_with_groups(email, "password", True, ["signoz-admins"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -282,7 +288,9 @@ def test_saml_role_mapping_single_group_editor( email = "editor-group-user@saml.integration.test" create_user_idp_with_groups(email, "password", True, ["signoz-editors"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -306,9 +314,13 @@ def test_saml_role_mapping_multiple_groups_highest_wins( Expected: User gets EDITOR (highest of VIEWER and EDITOR). """ email = f"multi-group-user-{uuid.uuid4().hex[:8]}@saml.integration.test" - create_user_idp_with_groups(email, "password", True, ["signoz-viewers", "signoz-editors"]) + create_user_idp_with_groups( + email, "password", True, ["signoz-viewers", "signoz-editors"] + ) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -333,7 +345,9 @@ def test_saml_role_mapping_explicit_viewer_group( email = "viewer-group-user@saml.integration.test" create_user_idp_with_groups(email, "password", True, ["signoz-viewers"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -357,7 +371,9 @@ def test_saml_role_mapping_unmapped_group_uses_default( email = "unmapped-group-user@saml.integration.test" create_user_idp_with_groups(email, "password", True, ["some-other-group"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -432,7 +448,9 @@ def test_saml_role_mapping_role_claim_takes_precedence( email = "role-claim-precedence@saml.integration.test" create_user_idp_with_role(email, "password", True, "ADMIN", ["signoz-editors"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -460,7 +478,9 @@ def test_saml_role_mapping_invalid_role_claim_fallback( email = "invalid-role-user@saml.integration.test" create_user_idp_with_role(email, "password", True, "SUPERADMIN", ["signoz-editors"]) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -488,7 +508,9 @@ def test_saml_role_mapping_case_insensitive( email = "lowercase-role-user@saml.integration.test" create_user_idp_with_role(email, "password", True, "admin", []) - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -499,7 +521,7 @@ def test_saml_role_mapping_case_insensitive( def test_saml_name_mapping( signoz: SigNoz, - idp: TestContainerIDP, # pylint: disable=unused-argument + idp: TestContainerIDP, # pylint: disable=unused-argument driver: webdriver.Chrome, create_user_idp: Callable[[str, str, bool, str, str], None], idp_login: Callable[[str, str], None], @@ -508,22 +530,26 @@ def test_saml_name_mapping( ) -> None: """Test that user's display name is mapped from SAML displayName attribute.""" email = "named-user@saml.integration.test" - + create_user_idp(email, "password", True, "Jane", "Smith") - - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") - + + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) + admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) - + assert found_user is not None - assert found_user["displayName"] == "Jane" # We are only mapping the first name here + assert ( + found_user["displayName"] == "Jane" + ) # We are only mapping the first name here assert found_user["role"] == "VIEWER" def test_saml_empty_name_fallback( signoz: SigNoz, - idp: TestContainerIDP, # pylint: disable=unused-argument + idp: TestContainerIDP, # pylint: disable=unused-argument driver: webdriver.Chrome, create_user_idp: Callable[[str, str, bool, str, str], None], idp_login: Callable[[str, str], None], @@ -532,13 +558,15 @@ def test_saml_empty_name_fallback( ) -> None: """Test that user without displayName in IDP still gets created.""" email = "no-name@saml.integration.test" - + create_user_idp(email, "password", True) - - perform_saml_login(signoz, driver, get_session_context, idp_login, email, "password") - + + perform_saml_login( + signoz, driver, get_session_context, idp_login, email, "password" + ) + admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) - + assert found_user is not None assert found_user["role"] == "VIEWER" diff --git a/tests/integration/src/callbackauthn/03_oidc.py b/tests/integration/src/callbackauthn/03_oidc.py index d940dcb6fd..a2625bd58d 100644 --- a/tests/integration/src/callbackauthn/03_oidc.py +++ b/tests/integration/src/callbackauthn/03_oidc.py @@ -11,7 +11,11 @@ from fixtures.auth import ( USER_ADMIN_PASSWORD, add_license, ) -from fixtures.idputils import get_oidc_domain, get_user_by_email, perform_oidc_login, delete_keycloak_client +from fixtures.idputils import ( + get_oidc_domain, + get_user_by_email, + perform_oidc_login, +) from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP @@ -196,7 +200,9 @@ def test_oidc_role_mapping_single_group_admin( email = "admin-group-user@oidc.integration.test" create_user_idp_with_groups(email, "password123", True, ["signoz-admins"]) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -220,7 +226,9 @@ def test_oidc_role_mapping_single_group_editor( email = "editor-group-user@oidc.integration.test" create_user_idp_with_groups(email, "password123", True, ["signoz-editors"]) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -244,9 +252,13 @@ def test_oidc_role_mapping_multiple_groups_highest_wins( Expected: User gets ADMIN (highest of the two). """ email = "multi-group-user@oidc.integration.test" - create_user_idp_with_groups(email, "password123", True, ["signoz-viewers", "signoz-admins"]) + create_user_idp_with_groups( + email, "password123", True, ["signoz-viewers", "signoz-admins"] + ) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -271,7 +283,9 @@ def test_oidc_role_mapping_explicit_viewer_group( email = "viewer-group-user@oidc.integration.test" create_user_idp_with_groups(email, "password123", True, ["signoz-viewers"]) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -295,7 +309,9 @@ def test_oidc_role_mapping_unmapped_group_uses_default( email = "unmapped-group-user@oidc.integration.test" create_user_idp_with_groups(email, "password123", True, ["some-other-group"]) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -373,7 +389,9 @@ def test_oidc_role_mapping_role_claim_takes_precedence( email = "role-claim-precedence@oidc.integration.test" create_user_idp_with_role(email, "password123", True, "ADMIN", ["signoz-editors"]) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -399,9 +417,13 @@ def test_oidc_role_mapping_invalid_role_claim_fallback( """ setup_user_profile() email = "invalid-role-user@oidc.integration.test" - create_user_idp_with_role(email, "password123", True, "SUPERADMIN", ["signoz-editors"]) + create_user_idp_with_role( + email, "password123", True, "SUPERADMIN", ["signoz-editors"] + ) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -429,7 +451,9 @@ def test_oidc_role_mapping_case_insensitive( email = "lowercase-role-user@oidc.integration.test" create_user_idp_with_role(email, "password123", True, "editor", []) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) found_user = get_user_by_email(signoz, admin_token, email) @@ -449,29 +473,25 @@ def test_oidc_name_mapping( ) -> None: """Test that user's display name is mapped from IDP name claim.""" email = "named-user@oidc.integration.test" - + # Create user with explicit first/last name - create_user_idp( - email, - "password123", - True, - first_name="John", - last_name="Doe" + create_user_idp(email, "password123", True, first_name="John", last_name="Doe") + + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" ) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") - admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/user"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5, ) - + assert response.status_code == HTTPStatus.OK users = response.json()["data"] found_user = next((u for u in users if u["email"] == email), None) - + assert found_user is not None # Keycloak concatenates firstName + lastName into "name" claim assert found_user["displayName"] == "John Doe" @@ -489,23 +509,25 @@ def test_oidc_empty_name_uses_fallback( ) -> None: """Test that user without name in IDP still gets created (may have empty displayName).""" email = "no-name@oidc.integration.test" - + # Create user without first/last name create_user_idp(email, "password123", True) - perform_oidc_login(signoz, idp, driver, get_session_context, idp_login, email, "password123") - + perform_oidc_login( + signoz, idp, driver, get_session_context, idp_login, email, "password123" + ) + admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/user"), headers={"Authorization": f"Bearer {admin_token}"}, timeout=5, ) - + assert response.status_code == HTTPStatus.OK users = response.json()["data"] found_user = next((u for u in users if u["email"] == email), None) - + # User should still be created even with empty name assert found_user is not None assert found_user["role"] == "VIEWER" diff --git a/tests/integration/src/querier/06_order_by_table_querier.py b/tests/integration/src/querier/06_order_by_table_querier.py new file mode 100644 index 0000000000..c3933a9aa2 --- /dev/null +++ b/tests/integration/src/querier/06_order_by_table_querier.py @@ -0,0 +1,1004 @@ +from datetime import datetime, timedelta, timezone +from http import HTTPStatus +from typing import Callable, Dict, List, Optional, Tuple + +import requests + +from fixtures import querier, types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logs import Logs +from fixtures.metrics import Metrics +from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode + +log_or_trace_service_counts = { + "service-a": 5, + "service-b": 3, + "service-c": 7, + "service-d": 1, +} + +metric_values_for_test = { + "service-a": 50.0, + "service-b": 30.0, + "service-c": 70.0, + "service-d": 10.0, +} + + +def generate_logs_with_counts( + now: datetime, + service_counts: Dict[str, int], +) -> List[Logs]: + logs = [] + for service, count in service_counts.items(): + for i in range(count): + logs.append( + Logs( + timestamp=now - timedelta(seconds=i + 1), + resources={"service.name": service}, + body=f"{service} log {i}", + ) + ) + return logs + + +def generate_traces_with_counts( + now: datetime, + service_counts: Dict[str, int], +) -> List[Traces]: + traces = [] + for service, count in service_counts.items(): + for i in range(count): + trace_id = TraceIdGenerator.trace_id() + span_id = TraceIdGenerator.span_id() + traces.append( + Traces( + timestamp=now - timedelta(seconds=i + 1), + kind=TracesKind.SPAN_KIND_SERVER, + status_code=TracesStatusCode.STATUS_CODE_OK, + trace_id=trace_id, + span_id=span_id, + resources={"service.name": service}, + name=f"{service} span {i}", + ) + ) + return traces + + +def generate_metrics_with_values( + now: datetime, + service_values: Dict[str, float], +) -> List[Metrics]: + metrics = [] + for service, value in service_values.items(): + metrics.append( + Metrics( + metric_name="test.metric", + labels={"service.name": service}, + timestamp=now - timedelta(seconds=1), + temporality="Unspecified", + type_="Gauge", + is_monotonic=False, + value=value, + ) + ) + return metrics + + +def make_scalar_query_request( + signoz: types.SigNoz, + token: str, + now: datetime, + queries: List[Dict], + lookback_minutes: int = 5, +) -> requests.Response: + return requests.post( + signoz.self.host_configs["8080"].get("/api/v5/query_range"), + timeout=5, + headers={"authorization": f"Bearer {token}"}, + json={ + "schemaVersion": "v1", + "start": int( + (now - timedelta(minutes=lookback_minutes)).timestamp() * 1000 + ), + "end": int(now.timestamp() * 1000), + "requestType": "scalar", + "compositeQuery": {"queries": queries}, + "formatOptions": {"formatTableResultForUI": True, "fillGaps": False}, + }, + ) + + +def build_logs_query( + name: str = "A", + aggregations: Optional[List[str]] = None, + group_by: Optional[List[str]] = None, + order_by: Optional[List[Tuple[str, str]]] = None, + limit: Optional[int] = None, +) -> Dict: + if aggregations is None: + aggregations = ["count()"] + + aggs = [querier.build_logs_aggregation(expr) for expr in aggregations] + gb = ( + [querier.build_group_by_field(f, "string", "resource") 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 + ) + + return querier.build_scalar_query( + name=name, + signal="logs", + aggregations=aggs, + group_by=gb, + order=order, + limit=limit, + ) + + +def build_traces_query( + name: str = "A", + aggregations: Optional[List[str]] = None, + group_by: Optional[List[str]] = None, + order_by: Optional[List[Tuple[str, str]]] = None, + limit: Optional[int] = None, +) -> Dict: + if aggregations is None: + aggregations = ["count()"] + + aggs = [querier.build_logs_aggregation(expr) for expr in aggregations] + gb = ( + [querier.build_group_by_field(f, "string", "resource") 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 + ) + + return querier.build_scalar_query( + name=name, + signal="traces", + aggregations=aggs, + group_by=gb, + order=order, + limit=limit, + ) + + +def build_metrics_query( + name: str = "A", + metric_name: str = "test.metric", + time_aggregation: str = "latest", + space_aggregation: str = "sum", + group_by: Optional[List[str]] = None, + order_by: Optional[List[Tuple[str, str]]] = None, + limit: Optional[int] = None, +) -> Dict: + aggs = [ + querier.build_metrics_aggregation( + metric_name, time_aggregation, space_aggregation, "unspecified" + ) + ] + 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 + ) + + return querier.build_scalar_query( + name=name, + signal="metrics", + aggregations=aggs, + group_by=gb, + order=order, + limit=limit, + ) + + +def test_logs_scalar_group_by_single_agg_no_order( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_logs_query(group_by=["service.name"])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3), ("service-d", 1)], + "Logs no order - default desc", + ) + + +def test_logs_scalar_group_by_single_agg_order_by_agg_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_logs_query(group_by=["service.name"], order_by=[("count()", "asc")])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-b", 3), ("service-a", 5), ("service-c", 7)], + "Logs order by agg asc", + ) + + +def test_logs_scalar_group_by_single_agg_order_by_agg_desc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_logs_query(group_by=["service.name"], order_by=[("count()", "desc")])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3), ("service-d", 1)], + "Logs order by agg desc", + ) + + +def test_logs_scalar_group_by_single_agg_order_by_grouping_key_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], order_by=[("service.name", "asc")] + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-a", 5), ("service-b", 3), ("service-c", 7), ("service-d", 1)], + "Logs order by grouping key asc", + ) + + +def test_logs_scalar_group_by_single_agg_order_by_grouping_key_desc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], order_by=[("service.name", "desc")] + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-c", 7), ("service-b", 3), ("service-a", 5)], + "Logs order by grouping key desc", + ) + + +def test_logs_scalar_group_by_multiple_aggs_order_by_first_agg_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], + aggregations=["count()", "count_distinct(body)"], + order_by=[("count()", "asc")], + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_column_order( + data, 0, ["service-d", "service-b", "service-a", "service-c"], "First column" + ) + + +def test_logs_scalar_group_by_multiple_aggs_order_by_second_agg_desc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], + aggregations=["count()", "count_distinct(body)"], + order_by=[("count_distinct(body)", "desc")], + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + # count_distinct(body) should equal count() since each log has unique body + querier.assert_scalar_column_order( + data, 0, ["service-c", "service-a", "service-b", "service-d"], "First column" + ) + + +def test_logs_scalar_group_by_single_agg_order_by_agg_asc_limit_2( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], order_by=[("count()", "asc")], limit=2 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-b", 3)], + "Logs order by agg asc with limit 2", + ) + + +def test_logs_scalar_group_by_single_agg_order_by_agg_desc_limit_3( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], order_by=[("count()", "desc")], limit=3 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3)], + "Logs order by agg desc with limit 3", + ) + + +def test_logs_scalar_group_by_order_by_grouping_key_asc_limit_2( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_logs_query( + group_by=["service.name"], order_by=[("service.name", "asc")], limit=2 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-a", 5), ("service-b", 3)], + "Logs order by grouping key asc with limit 2", + ) + + +def test_traces_scalar_group_by_single_agg_no_order( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_traces_query(group_by=["service.name"])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3), ("service-d", 1)], + "Traces no order - default desc", + ) + + +def test_traces_scalar_group_by_single_agg_order_by_agg_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_traces_query(group_by=["service.name"], order_by=[("count()", "asc")])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-b", 3), ("service-a", 5), ("service-c", 7)], + "Traces order by agg asc", + ) + + +def test_traces_scalar_group_by_single_agg_order_by_agg_desc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_traces_query(group_by=["service.name"], order_by=[("count()", "desc")])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3), ("service-d", 1)], + "Traces order by agg desc", + ) + + +def test_traces_scalar_group_by_single_agg_order_by_grouping_key_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], order_by=[("service.name", "asc")] + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-a", 5), ("service-b", 3), ("service-c", 7), ("service-d", 1)], + "Traces order by grouping key asc", + ) + + +def test_traces_scalar_group_by_single_agg_order_by_grouping_key_desc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], order_by=[("service.name", "desc")] + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-c", 7), ("service-b", 3), ("service-a", 5)], + "Traces order by grouping key desc", + ) + + +def test_traces_scalar_group_by_multiple_aggs_order_by_first_agg_asc( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], + aggregations=["count()", "count_distinct(trace_id)"], + order_by=[("count()", "asc")], + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_column_order( + data, 0, ["service-d", "service-b", "service-a", "service-c"], "First column" + ) + + +def test_traces_scalar_group_by_single_agg_order_by_agg_asc_limit_2( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], order_by=[("count()", "asc")], limit=2 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 1), ("service-b", 3)], + "Traces order by agg asc with limit 2", + ) + + +def test_traces_scalar_group_by_single_agg_order_by_agg_desc_limit_3( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], order_by=[("count()", "desc")], limit=3 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 7), ("service-a", 5), ("service-b", 3)], + "Traces order by agg desc with limit 3", + ) + + +def test_traces_scalar_group_by_order_by_grouping_key_asc_limit_2( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_traces: Callable[[List[Traces]], None], +) -> None: + now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0) + insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_traces_query( + group_by=["service.name"], order_by=[("service.name", "asc")], limit=2 + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-a", 5), ("service-b", 3)], + "Traces order by grouping key asc with limit 2", + ) + + +def test_metrics_scalar_group_by_single_agg_no_order( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [build_metrics_query(group_by=["service.name"])], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [ + ("service-c", 70.0), + ("service-a", 50.0), + ("service-b", 30.0), + ("service-d", 10.0), + ], + "Metrics no order - default desc", + ) + + +def test_metrics_scalar_group_by_single_agg_order_by_agg_asc( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_metrics_query( + group_by=["service.name"], + order_by=[("sum(test.metric)", "asc")], + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [ + ("service-d", 10.0), + ("service-b", 30.0), + ("service-a", 50.0), + ("service-c", 70.0), + ], + "Metrics order by agg asc", + ) + + +def test_metrics_scalar_group_by_single_agg_order_by_grouping_key_asc( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_metrics_query( + group_by=["service.name"], + order_by=[("service.name", "asc")], + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [ + ("service-a", 50.0), + ("service-b", 30.0), + ("service-c", 70.0), + ("service-d", 10.0), + ], + "Metrics order by grouping key asc", + ) + + +def test_metrics_scalar_group_by_single_agg_order_by_agg_asc_limit_2( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_metrics_query( + group_by=["service.name"], + order_by=[("sum(test.metric)", "asc")], + limit=2, + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-d", 10.0), ("service-b", 30.0)], + "Metrics order by agg asc with limit 2", + ) + + +def test_metrics_scalar_group_by_single_agg_order_by_agg_desc_limit_3( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_metrics_query( + group_by=["service.name"], + order_by=[("sum(test.metric)", "desc")], + limit=3, + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-c", 70.0), ("service-a", 50.0), ("service-b", 30.0)], + "Metrics order by agg desc with limit 3", + ) + + +def test_metrics_scalar_group_by_order_by_grouping_key_asc_limit_2( + 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=timezone.utc).replace(second=0, microsecond=0) + insert_metrics(generate_metrics_with_values(now, metric_values_for_test)) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + response = make_scalar_query_request( + signoz, + token, + now, + [ + build_metrics_query( + group_by=["service.name"], + order_by=[("service.name", "asc")], + limit=2, + ) + ], + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + data = querier.get_scalar_table_data(response.json()) + querier.assert_scalar_result_order( + data, + [("service-a", 50.0), ("service-b", 30.0)], + "Metrics order by grouping key asc with limit 2", + )