Skip to content

fix(nb-36647): fingerprint findings per resource, not per instance - #580

Merged
blue4209211 merged 2 commits into
mainfrom
fix/nb-36647-finding-fingerprint-fanout
Aug 20, 2026
Merged

fix(nb-36647): fingerprint findings per resource, not per instance#580
blue4209211 merged 2 commits into
mainfrom
fix/nb-36647-finding-fingerprint-fanout

Conversation

@mayankpande88

Copy link
Copy Markdown
Contributor

What

Three agent matchers built each problem's identity from a value unique to that one instance, so two occurrences of the same problem never produced the same fingerprint.

Dedup in this product is not the events upsert — that keys on finding_id, which is a fresh UUID per finding by design (one row per occurrence). Dedup is the occurrence chain in event_duplicates, keyed on (fingerprint, cloud_account_id). A fingerprint that changes every time therefore starts a fresh chain of length 1 every time, and the recurring problem never collapses into one entry with a repeat count.

Matcher Was Now
ConfigurationChange/KubernetesResource/Change ns, name, metadata.resourceVersion ns, kind, name
job_failure ns, metadata.uid ns, CronJob owner — else job family
image_pull_backoff_reporter ns, owner name (a per-run Job name), image ns, job-family-collapsed owner, image

Adds JobFamily(), which strips generated per-run Job-name suffixes (-<digits>, -<8+ hex>, -<uuid>) in the same spirit as the existing stripPodTemplateHash heuristic. This is needed because a directly-created Job has no ownerReferences to walk — its own per-run name is the only identity available, and that is the dominant case in production.

job_failure also gains a 10m rate limit. Its RateLimit: 0 was justified by per-UID fingerprints ("terminal — fingerprint by UID is enough"); with a shared fingerprint, leaving it unlimited would re-emit for as long as the failed Job lingers. The transition gate can't be relied on to prevent that — same kubewatch pointer-aliasing caveat already documented for pod_crash_loop, and prod shows single failed Jobs emitting ~68 times. The occurrence chain still counts every repeat; this only bounds emission rate.

Evidence

Occurrence chains in production before this change (30d, 8 accounts):

SELECT e.aggregation_key, count(*) chain_rows, max(ed.occurrence_number) max_occ, round(avg(ed.occurrence_number),2) avg_occ
FROM event_duplicates ed JOIN events e ON e.id = ed.event_id
WHERE ed.created_at > now() - interval '30 days' GROUP BY 1;
aggregation_key chain rows max occurrence avg occurrence
ConfigurationChange 8,824 1 1.00
node_unschedulable 358 2 1.01
job_failure 1,307 136 14.84
image_pull_backoff_reporter 30,902 1,017 70.53
report_crash_loop 3,586 988 345.91

report_crash_loop is the control case — it chains correctly, proving the mechanism works when the fingerprint is stable.

Replaying the new identity rules over the same 30 days of production rows:

Matcher fingerprints now after
ConfigurationChange 5,028 267
job_failure 538 36
image_pull_backoff (excl. image component) 4,008 72

I also reviewed every Job name the new suffix rule rewrites in that window; the transformations are all of the intended shape (trivy-image-scan-24e032a5trivy-image-scan, blinq-api-healthchecks-29764215blinq-api-healthchecks, pinot-segment-push-v2-29751385pinot-segment-push-v2). The UUID rule was added after that review caught nb-llm-ct-<uuid> being only partially stripped.

cd runner && make validate — exit 0, 37 packages, no failures.

Not doing

  • Not making finding_id deterministic to force the events upsert to collapse rows. The idempotency guard in triage/processor.go returns early when a chain row already exists for an event_id, so a stable finding_id would freeze occurrence_number at 1 permanently — destroying the signal this PR restores. #35979 hit the same symptom for Prometheus alerts and chose a notification cooldown instead.
  • Not adding cluster/account to fp(). The chain query is already scoped by cloud_account_id, and there are zero same-account cross-cluster fingerprint collisions in 30d of prod. Rotating every fingerprint would orphan every open chain for no observable gain.
  • Not touching node_unschedulable (per cordon episode, ~6 fingerprints/day) or report_crash_loop (hour-bucketed, chains correctly). Neither shows fanout in the data.

Risk

JobFamily is a heuristic and errs toward collapsing: two genuinely different Jobs differing only by a generated-looking tail become one family. That is the grouping we want here, and the 5-digit floor on the numeric form keeps meaningful tails like postgres-15 intact. Existing open chains for the three affected matchers will not match the new fingerprints, so each affected problem starts one fresh chain after rollout — a one-time effect, not ongoing.

Replica collapse (already working) is unchanged and now has a regression test.

Fixes nudgebee/nudgebee-enterprise#36647

Three matchers mixed an instance-unique value into their fingerprint, so
two occurrences of the same problem never looked alike and the server's
occurrence chain (event_duplicates, keyed on fingerprint) never formed.

- ConfigurationChange hashed metadata.resourceVersion, which advances on
  every write. All 8824 of these in 30d of prod had occurrence_number=1.
  Now keyed on (namespace, kind, name).
- job_failure hashed metadata.uid, new per run. Now prefers the CronJob
  owner and falls back to the job family.
- image_pull_backoff resolved the owner but stopped at the Job, whose
  name carries a per-run suffix. Now collapses a Job owner to its family.

Adds JobFamily(), which strips generated per-run Job-name suffixes
(-<digits>, -<8+ hex>, -<uuid>) in the same spirit as the existing
stripPodTemplateHash heuristic. Directly-created Jobs have no
ownerReferences to walk, so the name is the only identity available.

job_failure also gains a 10m rate limit; its RateLimit:0 was justified by
per-UID fingerprints, and prod shows single failed Jobs emitting ~68
times because the transition gate cannot be relied on (kubewatch pointer
aliasing). The occurrence chain still counts every repeat.

Replica collapse is unchanged and now covered by a regression test.
Removes metaUID, orphaned by the job_failure change.

Projected against 30d of prod: ConfigurationChange 5028 -> 267
fingerprints, job_failure 538 -> 36, image_pull_backoff 4008 -> 72.
@mayankpande88
mayankpande88 requested a review from a team as a code owner August 20, 2026 09:39

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies how fingerprints are generated for Kubernetes Jobs and resource changes to ensure that repeated runs of the same logical job or repeated edits to the same resource share a single fingerprint. It introduces a JobFamily helper to strip generated per-run suffixes (such as UUIDs or timestamp-based digits) from Job names. Additionally, it updates the imagePullBackoffMatcher, jobFailureMatcher, and babysitterChangeMatcher to leverage this family-based grouping instead of using unique identifiers like resourceVersion or uid. Corresponding unit tests have been added to verify these fingerprinting and collapsing behaviors. There are no review comments to address.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Image Tags Updated

I've automatically updated the image tags in `charts/nudgebee-agent/values.yaml` to the latest versions from GHCR for the `main` branch.

The image tags are now synchronized with the latest builds and ready for release.

@blue4209211
blue4209211 merged commit 1e7426c into main Aug 20, 2026
7 checks passed
@blue4209211
blue4209211 deleted the fix/nb-36647-finding-fingerprint-fanout branch August 20, 2026 09:53
mayankpande88 added a commit that referenced this pull request Aug 20, 2026
Follow-up to #580, caught verifying that change on the dev cluster.

#580 collapsed a Job owner to its family so repeated runs would share a
fingerprint, but kept the failing image in the hash. For a one-Job-per-
image creator the image IS the per-run identity, so it re-forked the
fingerprint immediately: after #580 shipped, every trivy-image-scan Job
in nudgebee-agent-dev still had its own fingerprint, each pulling a
different image under scan.

Drop the image only when we collapsed a Job owner. Ordinary workloads
keep it, so a single typo'd container is still its own Finding. The
failing image remains in the evidence blocks either way.

Measured on 30d of prod: job-owned 3961 -> 9 fingerprints. Every other
owner kind is unchanged (deployment 35, daemonset 8, statefulset 3) —
the image component only ever forked identity for Job-owned Pods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants