-
Notifications
You must be signed in to change notification settings - Fork 371
feat: track cypher query error metrics BED-7854 #3175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,6 @@ package v2 | |
|
|
||
| import ( | ||
| "errors" | ||
|
|
||
| "log/slog" | ||
| "maps" | ||
| "net/http" | ||
|
|
@@ -31,12 +30,11 @@ import ( | |
| "github.com/specterops/bloodhound/cmd/api/src/queries" | ||
| "github.com/specterops/bloodhound/packages/go/bhlog/attr" | ||
| "github.com/specterops/bloodhound/packages/go/graphschema" | ||
| "github.com/specterops/dawgs/ops" | ||
| "github.com/specterops/dawgs/util" | ||
| ) | ||
|
|
||
| var ( | ||
| errUnauthorizedGraphMutation = errors.New("unauthorized graph mutation") | ||
| ) | ||
| var errUnauthorizedGraphMutation = errors.New("unauthorized graph mutation") | ||
|
|
||
| type CypherQueryPayload struct { | ||
| Query string `json:"query"` | ||
|
|
@@ -45,13 +43,30 @@ type CypherQueryPayload struct { | |
|
|
||
| // Helper function to handle error conditions in CypherQuery. | ||
| func handleCypherDBErrors(response http.ResponseWriter, request *http.Request, err error) { | ||
| var ( | ||
| errorResp *api.ErrorWrapper | ||
| errorCategoryLabel string | ||
| ) | ||
|
|
||
| if errors.Is(err, errUnauthorizedGraphMutation) { | ||
| api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, "Permission denied: User may not modify the graph.", request), response) | ||
| } else if util.IsNeoTimeoutError(err) { | ||
| api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "transaction timed out, reduce query complexity or try again later", request), response) | ||
| return | ||
| } else if util.IsNeoTimeoutError(err) || util.IsPostgresTimeoutError(err) { | ||
| errorCategoryLabel = cypherQueryErrorTypeTimeout | ||
|
brandonshearin marked this conversation as resolved.
|
||
| errorResp = api.BuildErrorResponse(http.StatusInternalServerError, "transaction timed out, reduce query complexity or try again later", request) | ||
| } else if errors.Is(err, ops.ErrGraphQueryMemoryLimit) { | ||
| errorCategoryLabel = cypherQueryErrorTypeMemory | ||
| errorResp = api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request) | ||
| } else if errors.Is(err, ops.ErrGraphQueryExecutionFailed) { | ||
| errorCategoryLabel = cypherQueryErrorTypeExecute | ||
| errorResp = api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request) | ||
| } else { | ||
| api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response) | ||
| errorCategoryLabel = cypherQueryErrorTypeUnknown | ||
| errorResp = api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request) | ||
| } | ||
|
|
||
| cypherQueryErrors.WithLabelValues(errorCategoryLabel).Inc() | ||
| api.WriteErrorResponse(request.Context(), errorResp, response) | ||
|
Comment on lines
+46
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 4 \
'GetPrimaryDisplayKinds|HandleDatabaseError|handleCypherDBErrors|cypherQueryErrors' \
cmd/api/src/api/v2Repository: SpecterOps/BloodHound Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- cypherquery.go ---'
sed -n '1,210p' cmd/api/src/api/v2/cypherquery.go
printf '%s\n' '--- CypherQuery declarations and callers ---'
rg -n -C 8 'func .*CypherQuery|Resources\.CypherQuery|GetPrimaryDisplayKinds' cmd/api/src/api/v2 --glob '*.go'
printf '%s\n' '--- HandleDatabaseError definitions and callers in relevant packages ---'
rg -n -C 12 'func HandleDatabaseError|HandleDatabaseError\(' cmd/api/src --glob '*.go' | head -n 500
printf '%s\n' '--- metric declarations and uses ---'
rg -n -C 8 'cypherQueryErrors|cypherQueryErrorType' cmd/api/src --glob '*.go'Repository: SpecterOps/BloodHound Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate files ---'
git ls-files '*cypherquery.go' '*api*.go' | head -n 200
printf '%s\n' '--- target file ---'
cat -n cmd/api/src/api/v2/cypherquery.go
printf '%s\n' '--- exact target references ---'
rg -n -C 10 \
'GetPrimaryDisplayKinds|HandleDatabaseError|handleCypherDBErrors|cypherQueryErrors|Resources\.CypherQuery|func .*CypherQuery' \
cmd/api/src/api/v2/cypherquery.go
printf '%s\n' '--- HandleDatabaseError definitions ---'
rg -l 'func HandleDatabaseError' --glob '*.go' | while IFS= read -r file; do
echo "FILE: $file"
rg -n -A 30 -B 5 'func HandleDatabaseError' "$file"
done
printf '%s\n' '--- metric references outside the target file ---'
rg -n -C 6 'cypherQueryErrors|cypherQueryErrorType' --glob '*.go' --glob '!cmd/api/src/api/v2/cypherquery.go' .Repository: SpecterOps/BloodHound Length of output: 7578 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- target file ---'
cat -n cmd/api/src/api/v2/cypherquery.go
printf '%s\n' '--- target references ---'
rg -n -C 10 \
'GetPrimaryDisplayKinds|HandleDatabaseError|handleCypherDBErrors|cypherQueryErrors|Resources\.CypherQuery|func .*CypherQuery' \
cmd/api/src/api/v2/cypherquery.go || true
printf '%s\n' '--- HandleDatabaseError definition ---'
rg -l 'func HandleDatabaseError' --glob '*.go' . | while IFS= read -r file; do
echo "FILE: $file"
rg -n -A 35 -B 5 'func HandleDatabaseError' "$file"
done
printf '%s\n' '--- metric references outside target ---'
rg -n -C 6 'cypherQueryErrors|cypherQueryErrorType' --glob '*.go' --glob '!cmd/api/src/api/v2/cypherquery.go' . || trueRepository: SpecterOps/BloodHound Length of output: 19562 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
cypher = Path("cmd/api/src/api/v2/cypherquery.go").read_text()
errors = Path("cmd/api/src/api/error.go").read_text()
lookup = "primaryDisplayKinds, err := s.DB.GetPrimaryDisplayKinds(request.Context())"
lookup_pos = cypher.index(lookup)
lookup_block = cypher[lookup_pos:cypher.index("\n\t}", lookup_pos) + 3]
handler_start = errors.index("func HandleDatabaseError")
handler_end = errors.index("\n}", handler_start) + 2
handler = errors[handler_start:handler_end]
print("metadata lookup uses HandleDatabaseError:",
"api.HandleDatabaseError(request, response, err)" in lookup_block)
print("metadata lookup block increments cypherQueryErrors:",
"cypherQueryErrors" in lookup_block)
print("HandleDatabaseError increments cypherQueryErrors:",
"cypherQueryErrors" in handler)
print("CypherQuery has other metric increments:",
cypher.count("cypherQueryErrors.WithLabelValues("))
assert "api.HandleDatabaseError(request, response, err)" in lookup_block
assert "cypherQueryErrors" not in lookup_block
assert "cypherQueryErrors" not in handler
assert cypher.count("cypherQueryErrors.WithLabelValues(") >= 3
PYRepository: SpecterOps/BloodHound Length of output: 363 Instrument 🧰 Tools🪛 GitHub Actions: Run Go Unit Tests / 0_run-go-unit-tests.txt[error] 54-54: Go test build failed: undefined: util.IsPostgresTimeoutError. 🪛 GitHub Actions: Run Go Unit Tests / run-go-unit-tests[error] 54-54: Go build failed during 'go tool stbernard test -g -r' / 'go test -json': undefined: util.IsPostgresTimeoutError. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // Helper function to handle processing of property keys. | ||
|
|
@@ -95,11 +110,19 @@ func (s Resources) CypherQuery(response http.ResponseWriter, request *http.Reque | |
| } | ||
|
|
||
| if err := api.ReadJSONRequestPayloadLimited(&payload, request); err != nil { | ||
| cypherQueryErrors.WithLabelValues(cypherQueryErrorTypeDecode).Inc() | ||
| api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "JSON malformed.", request), response) | ||
| return | ||
| } | ||
|
|
||
| if preparedQuery, err = s.GraphQuery.PrepareCypherQuery(payload.Query, queries.DefaultQueryFitnessLowerBoundExplore); err != nil { | ||
| if errors.Is(err, queries.ErrCypherQueryTooComplex) { | ||
| cypherQueryErrors.WithLabelValues(cypherQueryErrorTypeFitness).Inc() | ||
| } else if errors.Is(err, queries.ErrCypherQueryUnparseable) { | ||
| cypherQueryErrors.WithLabelValues(cypherQueryErrorTypeParse).Inc() | ||
| } else { | ||
| cypherQueryErrors.WithLabelValues(cypherQueryErrorTypeUnknown).Inc() | ||
| } | ||
| api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) | ||
| return | ||
| } | ||
|
|
@@ -219,5 +242,4 @@ func (s Resources) cypherMutation(request *http.Request, primaryDisplayKinds gra | |
| } | ||
|
|
||
| return graphResponse, err | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright 2026 Specter Ops, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package v2 | ||
|
|
||
| import ( | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| ) | ||
|
|
||
| const ( | ||
| cypherQueryErrorTypeTimeout = "timeout" | ||
| cypherQueryErrorTypeMemory = "memory" | ||
| cypherQueryErrorTypeDecode = "decode" | ||
| cypherQueryErrorTypeFitness = "fitness" | ||
| cypherQueryErrorTypeParse = "parse" | ||
| cypherQueryErrorTypeExecute = "execute" | ||
| cypherQueryErrorTypeUnknown = "unknown" | ||
| ) | ||
|
|
||
| var cypherQueryErrors = prometheus.NewCounterVec( | ||
| prometheus.CounterOpts{ | ||
| Namespace: "bh", | ||
| Subsystem: "api", | ||
| Name: "cypher_query_errors", | ||
| }, | ||
| []string{"error_type"}, | ||
| ) | ||
|
|
||
| func RegisterApiEndpointMetrics(registry prometheus.Registerer) error { | ||
| return registry.Register(cypherQueryErrors) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.