fix(observability): right-size webhook log severities for oncall triage - #1037
Conversation
Three webhook log sites drowned the warn stream, making a severity-based triage scan useless. Each now logs at the level its condition warrants: - A webhook signed for a repository with no config entry at all is routine traffic from an unmanaged repository (a shared App forwards deliveries for every repo it is installed on), not config drift. The rejection now logs at debug for that cause and stays a warning for a declared repo whose App mapping is broken. ResolveGitHubAppForRepo wraps the new api.ErrRepoNotConfigured sentinel so the handler can tell the two apart. The two causes also split on the events counter: unmanaged traffic counts under its own repo_not_configured status without the repository attribute (unmanaged repo names are unbounded; the debug log carries the repo), so a nonzero app_repo_mismatch is always an actionable drift signal and the repository attribute stays bounded by config. - The reconciler's report-only missing-delivery line re-reports the same missing head on every pass until synthesis is enabled or an organic delivery arrives, so it logs at info; the per-repo metric and the per-pass summary carry the operator signal. - The merge-group path logged its passing aggregate post under a different message than the pull-request path, so a search for one missed the other. Both now log "posted passing aggregate". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR improves oncall triage signal quality by reducing non-actionable warn-level webhook logs, aligning passing-aggregate log messages across webhook paths, and splitting webhook rejection metrics so unmanaged-repo traffic can’t drown out (or inflate) actionable drift signals.
Changes:
- Split webhook ownership rejections into two causes (unmanaged repo vs configured repo with App mismatch), mapping them to different log levels and metric statuses while keeping fail-closed behavior unchanged.
- Downgrade reconciler report-only “missing delivery” per-head logging from warn to info to avoid repeated warn spam.
- Standardize the “posted passing aggregate” log message across pull-request and merge-group posting paths, and add targeted tests around the new ownership-rejection severity behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/webhook/merge_group.go | Align passing-aggregate log message with the pull-request path for consistent log searching. |
| pkg/webhook/handler.go | Differentiate unmanaged-repo vs App-mismatch ownership rejections; adjust log severity and metrics labeling to preserve actionable signals and bounded cardinality. |
| pkg/webhook/durable_reconcile.go | Reduce report-only reconciler missing-delivery per-head log severity from warn to info. |
| pkg/webhook/dispatch_test.go | Add logger-injectable handler helper and a new test asserting the two ownership-rejection severities. |
| pkg/serve/serve.go | Update comments to reflect the “missing-delivery log” wording (severity change context). |
| pkg/metrics/metrics.go | Clarify (in comments) the contract for passing repo as empty when recording statuses for unmanaged repos. |
| pkg/api/config.go | Introduce ErrRepoNotConfigured sentinel and wrap it from ResolveGitHubAppForRepo for reliable cause detection via errors.Is. |
| pkg/api/config_test.go | Update resolver tests to assert ErrRepoNotConfigured classification and ensure declared-but-broken mappings don’t get misclassified as unmanaged. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
🤖 Review findings - created by Kiran's code review agent - for pull/1037, 0067898. Verdict: 7 findings — 1 blocking (README metric contract), 2 non-blocking (operator doc, metric test coverage), 4 suggestions. CI 33/33 green at head 0067898 (state OPEN); GitHub still reports mergeable UNKNOWN, but merge-tree vs origin/main is clean — the 2 commits main is ahead touch only pkg/metrics/README.md and unrelated dispatch-supersede code. Blocking
Non-blocking
General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism is the Verified correct
This review was generated by Claude Code (claude-fable-5). |
…contract Records both ownership-rejection statuses in the metric contract and pins them with tests: - `pkg/metrics/README.md` lists `repo_not_configured` and `app_repo_mismatch`, marks `repository` as conditional on the events_total row, and adds a Webhook Ownership Rejections section saying which status is routine background traffic and which is actionable config drift. - `docs/configuration.md` documents both rejection outcomes and their log levels, so debugging constant 401s for an unmanaged repository points at the debug stream rather than the warn stream. - `TestDispatch_OwnershipRejectionSeverity` now asserts the metric status per cause and that only the declared-repo rejection carries a `repository` attribute. - `TestWebhookReconcilerMissingHeadSeverityByMode` pins the report-only vs synthesizing per-head line at info vs warn. - The two "posted passing aggregate" log sites use the same attribute keys, so one query returns both.
…verity-cleanup # Conflicts: # pkg/metrics/README.md
|
🤖 All 7 addressed in c0f29a1 (fixes in 0df9cc6). 1–5 implemented as described: both statuses plus the conditional 6: correctly identified, but we're accepting the Note on the merge: main moved ahead and #1032 had landed an identical Addressed by Claude Code (claude-opus-5). |
Why this matters
A severity-scanned log stream is the first tool an oncall operator reaches for, and today three webhook log sites bury it. A shared GitHub App installed org-wide forwards webhook deliveries for every repository it is installed on, so a deployment managing a handful of repos rejects a constant stream of deliveries for repos it does not manage — each one logged at warn as if it were config drift. The reconciler's report-only mode re-warns the same missing head on every pass. And the two paths that post a passing aggregate check log it under different messages, so a log search for one silently misses the other.
The rejection metric had the same conflation: routine unmanaged-repo traffic and genuine App-ownership drift counted under one status, so the drift signal could never be alerted on. The unmanaged case also recorded arbitrary repository names as a metric attribute — unbounded cardinality on a counter whose repository label is meant to be bounded by config.
What it does
ResolveGitHubAppForRepowraps the newapi.ErrRepoNotConfiguredsentinel for undeclared repos so the webhook handler can separate the two causes.Two safety properties worth calling out:
app_repo_mismatchis now always actionable. With unmanaged traffic on its own status, the drift status can be alerted on directly, and therepositoryattribute stays bounded by configured repos.How it moves us toward the northstar
Unattended fleet-scale GitOps needs an error/warn stream where every line is actionable and every safety-relevant metric can carry an alert. This clears the highest-volume warn sources and turns App-ownership drift — a trust-boundary signal — into something operators can alert on directly.
Opened by Claude (Fable 5).