fix: normalize context-prefixed field keys (#9089)

* feat: normalize context-prefixed field keys

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* test: added tests validation for context-prefixed field

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* refactor: moved logic to parse.go

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* fix: attribute key edge case

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* fix: corrupt field context

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* fix: corrupt field context

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* refactor: parse and signal

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

* refactor: mismatch for unknown signal

Signed-off-by: “niladrix719” <niladrix719@gmail.com>

---------

Signed-off-by: “niladrix719” <niladrix719@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Niladri Adhikary
2025-12-28 23:17:44 +05:30
committed by GitHub
parent b9f9c00da5
commit 7c051601f2
3 changed files with 161 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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"])