From 7c051601f25fa41502684c798d09f0c2932d7fb1 Mon Sep 17 00:00:00 2001 From: Niladri Adhikary <91966855+niladrix719@users.noreply.github.com> Date: Sun, 28 Dec 2025 23:17:44 +0530 Subject: [PATCH] fix: normalize context-prefixed field keys (#9089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: normalize context-prefixed field keys Signed-off-by: “niladrix719” * test: added tests validation for context-prefixed field Signed-off-by: “niladrix719” * refactor: moved logic to parse.go Signed-off-by: “niladrix719” * fix: attribute key edge case Signed-off-by: “niladrix719” * fix: corrupt field context Signed-off-by: “niladrix719” * fix: corrupt field context Signed-off-by: “niladrix719” * refactor: parse and signal Signed-off-by: “niladrix719” * refactor: mismatch for unknown signal Signed-off-by: “niladrix719” --------- Signed-off-by: “niladrix719” Co-authored-by: Srikanth Chekuri --- pkg/apis/fields/parse.go | 39 +++++++++++ tests/integration/src/querier/a_logs.py | 82 +++++++++++++++++++++++ tests/integration/src/querier/b_traces.py | 40 +++++++++++ 3 files changed, 161 insertions(+) diff --git a/pkg/apis/fields/parse.go b/pkg/apis/fields/parse.go index 913b48b150..749d128439 100644 --- a/pkg/apis/fields/parse.go +++ b/pkg/apis/fields/parse.go @@ -80,6 +80,17 @@ func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, er name := r.URL.Query().Get("searchText") + if name != "" && fieldContext == telemetrytypes.FieldContextUnspecified { + parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name) + if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified { + // Only apply inferred context if it is valid for the current signal + if isContextValidForSignal(parsedFieldKey.FieldContext, signal) { + name = parsedFieldKey.Name + fieldContext = parsedFieldKey.FieldContext + } + } + } + req = telemetrytypes.FieldKeySelector{ StartUnixMilli: startUnixMilli, EndUnixMilli: endUnixMilli, @@ -102,6 +113,16 @@ func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector } name := r.URL.Query().Get("name") + if name != "" && keySelector.FieldContext == telemetrytypes.FieldContextUnspecified { + parsedFieldKey := telemetrytypes.GetFieldKeyFromKeyText(name) + if parsedFieldKey.FieldContext != telemetrytypes.FieldContextUnspecified { + // Only apply inferred context if it is valid for the current signal + if isContextValidForSignal(parsedFieldKey.FieldContext, keySelector.Signal) { + name = parsedFieldKey.Name + keySelector.FieldContext = parsedFieldKey.FieldContext + } + } + } keySelector.Name = name existingQuery := r.URL.Query().Get("existingQuery") value := r.URL.Query().Get("searchText") @@ -121,3 +142,21 @@ func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector return &req, nil } + +func isContextValidForSignal(ctx telemetrytypes.FieldContext, signal telemetrytypes.Signal) bool { + if ctx == telemetrytypes.FieldContextResource || + ctx == telemetrytypes.FieldContextAttribute || + ctx == telemetrytypes.FieldContextScope { + return true + } + + switch signal.StringValue() { + case telemetrytypes.SignalLogs.StringValue(): + return ctx == telemetrytypes.FieldContextLog || ctx == telemetrytypes.FieldContextBody + case telemetrytypes.SignalTraces.StringValue(): + return ctx == telemetrytypes.FieldContextSpan || ctx == telemetrytypes.FieldContextEvent || ctx == telemetrytypes.FieldContextTrace + case telemetrytypes.SignalMetrics.StringValue(): + return ctx == telemetrytypes.FieldContextMetric + } + return true +} diff --git a/tests/integration/src/querier/a_logs.py b/tests/integration/src/querier/a_logs.py index b661c11fe1..bfd632f629 100644 --- a/tests/integration/src/querier/a_logs.py +++ b/tests/integration/src/querier/a_logs.py @@ -67,6 +67,7 @@ def test_logs_list( "code.file": "/opt/integration.go", "code.function": "com.example.Integration.process", "code.line": 120, + "metric.domain_id": "d-001", "telemetry.sdk.language": "go", }, body="This is a log message, coming from a go application", @@ -141,6 +142,7 @@ def test_logs_list( "code.function": "com.example.Integration.process", "log.iostream": "stdout", "logtag": "F", + "metric.domain_id": "d-001", "telemetry.sdk.language": "go", } assert rows[0]["data"]["attributes_number"] == {"code.line": 120} @@ -308,6 +310,86 @@ def test_logs_list( assert len(values) == 1 assert 120 in values + # Query keys from the fields API with context specified in the key + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/keys"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "logs", + "searchText": "resource.servic", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + keys = response.json()["data"]["keys"] + assert "service.name" in keys + assert any(k["fieldContext"] == "resource" for k in keys["service.name"]) + + # Do not treat `metric.` as a context prefix for logs + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/keys"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "logs", + "searchText": "metric.do", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + keys = response.json()["data"]["keys"] + assert "metric.domain_id" in keys + + # Query values of service.name resource attribute using context-prefixed key + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/values"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "logs", + "name": "resource.service.name", + "searchText": "", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + values = response.json()["data"]["values"]["stringValues"] + assert "go" in values + assert "java" in values + + # Query values of metric.domain_id (string attribute) and ensure context collision doesn't break it + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/values"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "logs", + "name": "metric.domain_id", + "searchText": "", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + values = response.json()["data"]["values"]["stringValues"] + assert "d-001" in values + def test_logs_time_series_count( signoz: types.SigNoz, diff --git a/tests/integration/src/querier/b_traces.py b/tests/integration/src/querier/b_traces.py index 23d921ed65..5cb90e9324 100644 --- a/tests/integration/src/querier/b_traces.py +++ b/tests/integration/src/querier/b_traces.py @@ -373,3 +373,43 @@ def test_traces_list( assert len(values) == 2 assert set(values) == set(["POST", "PATCH"]) + + # Query keys from the fields API with context specified in the key + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/keys"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "traces", + "searchText": "resource.servic", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + keys = response.json()["data"]["keys"] + assert "service.name" in keys + assert any(k["fieldContext"] == "resource" for k in keys["service.name"]) + + # Query values of service.name resource attribute using context-prefixed key + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/fields/values"), + timeout=2, + headers={ + "authorization": f"Bearer {token}", + }, + params={ + "signal": "traces", + "name": "resource.service.name", + "searchText": "", + }, + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + + values = response.json()["data"]["values"]["stringValues"] + assert set(values) == set(["topic-service", "http-service"])