beholder: handle partial-delivery errors via metric and log suppression#2266
Conversation
83e688e to
204caa8
Compare
204caa8 to
8aa3f31
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the Beholder chip-ingress batch emitter to classify drop failures into consistent metric/log dimensions, reducing log noise for partial delivery while preserving chip_ingress.events_dropped as the primary operational signal.
Changes:
- Use
batch.ClassifyDropFailureto populatechip_ingress.events_droppedattributes (error_type, optionalerror_code) and enrich error logs for non-partial failures. - Suppress error-level logging for partial-delivery drops (metric-only for that class of failure).
- Add/adjust tests to validate metric dimensions for partial delivery and RPC failures; bump root dependency on
pkg/chipingress.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/beholder/batch_emitter_service.go | Classifies drop failures and adjusts metric/log behavior (no error log for partial delivery). |
| pkg/beholder/batch_emitter_service_test.go | Adds test coverage for partial-delivery classification and RPC error classification in metrics/log fields. |
| go.mod | Bumps github.com/smartcontractkit/chainlink-common/pkg/chipingress dependency version. |
| go.sum | Updates checksums for the bumped pkg/chipingress dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
8aa3f31 to
74cdc15
Compare
📊 API Diff Results
|
| // dropMetricAttrsFor returns a measurement option for the eventsDropped counter. | ||
| // Not cached — drop paths are not on the hot path. | ||
| // | ||
| // error_reason is deliberately excluded: it is free-form text from the server/gRPC | ||
| // stack (e.g. validation messages, status details) and is not a bounded value, so using | ||
| // it as a metric attribute would create unbounded cardinality. domain/entity/error_type/ |
| errorCode := batch.ErrorCodeFor(sendErr) | ||
| errorType := errorTypeFromCode(errorCode) | ||
| e.metrics.eventsDropped.Add(ctx, 1, e.dropMetricAttrsFor(domain, entity, errorType, errorCode)) |
| switch { | ||
| case code == "": | ||
| return "" | ||
| case strings.HasPrefix(code, chipingress.PublishErrorCode(0).String()) || strings.HasPrefix(code, "PUBLISH_ERROR_CODE_"): | ||
| return batch.ErrorTypePartialDelivery | ||
| case code == batch.ErrMessageBufferFull.Error(): | ||
| return batch.ErrorTypeBufferFull | ||
| case code == batch.ErrClientShutdown.Error() || code == batch.ErrorTypeClientError: | ||
| return batch.ErrorTypeClientError | ||
| default: | ||
| return batch.ErrorTypeRPCError | ||
| } |
thomaska
left a comment
There was a problem hiding this comment.
👍 💯 Added a small comment, but overall sgtm!
|
|
||
| var pubErr *PublishError | ||
| if errors.As(err, &pubErr) { | ||
| return pubErr.Code.String() |
There was a problem hiding this comment.
This can be problematic for ErrCodeResultsMismatch that is chipingress.PublishErrorCode(-1) ie. not in the 0-4 range of the proto enum, so this will return a cryptic "-1"
Maybe we could do explicit handling here:
var pubErr *PublishError
if errors.As(err, &pubErr) {
if pubErr.Code == ErrCodeResultsMismatch {
return "results_mismatch"
}
return pubErr.Code.String()
}…elivery-cl-nodes-beholder Resolves go.mod/go.sum and pkg/beholder/batch_emitter_service_test.go conflicts. Addresses PR review comment: ErrorCodeFor returns 'results_mismatch' for the synthetic ErrCodeResultsMismatch (-1) instead of the cryptic proto String() value.
✅ API Diff Results -
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
pkg/chipingress/batch/client.go:81
- ErrorCodeFor currently uses ErrMessageBufferFull.Error() / ErrClientShutdown.Error() as the returned error_code. That couples metric attribute values to human-readable error messages (including spaces), so a future wording change would silently change the metric dimension values. Prefer returning stable, explicitly-named codes (e.g. "message_buffer_full" / "client_shutdown") and keeping the human-readable text only in logs.
if errors.Is(err, ErrMessageBufferFull) {
return ErrMessageBufferFull.Error()
}
if errors.Is(err, ErrClientShutdown) {
return ErrClientShutdown.Error()
}
| // Partial delivery is not logged: it is a per-event, often persistent | ||
| // server-side condition (e.g. missing schema) that would otherwise log on | ||
| // every dropped event. chip_ingress.events_dropped (error_code) already | ||
| // captures that it's happening and roughly why; the full reason isn't | ||
| // needed at fleet-wide log volume. | ||
| var pubErr *batch.PublishError | ||
| if !errors.As(sendErr, &pubErr) { | ||
| e.eng.Errorw("failed to emit to chip ingress", | ||
| "error", sendErr, | ||
| "error_code", errorCode, | ||
| "error_reason", sendErr.Error(), | ||
| "domain", domain, | ||
| "entity", entity, | ||
| ) | ||
| } |
Copilot review feedback: preserve the client_name attribute on chip_ingress.events_dropped when adding error_code, so existing dashboards/alerts keep working.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
pkg/chipingress/batch/client.go:27
- These exported sentinel errors don’t have doc comments. If the repo’s linters enforce comments on exported identifiers (common in Go), this will fail lint and also makes it less clear to consumers that these are intended for errors.Is matching from QueueMessage / batching operations.
var (
ErrMessageBufferFull = errors.New("message buffer is full")
ErrClientShutdown = errors.New("client is shutdown")
)
pkg/beholder/batch_emitter_service.go:242
- dropMetricAttrsFor intentionally isn’t cached, but it currently builds an attribute.Set (attribute.NewSet + WithAttributeSet) on every drop. Creating a Set typically does extra work (dedup/sort) and allocations; since partial-delivery drops can be high-volume, prefer WithAttributes here to avoid the Set construction cost.
if errorCode != "" {
attrs = append(attrs, attribute.String("error_code", errorCode))
}
return otelmetric.WithAttributeSet(attribute.NewSet(attrs...))
}
Updates the root go.mod/go.sum to use the merged main commit of the chipingress submodule.\nThis resolves the dependency validation failure that required chipingress pseudo-versions to exist on main.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
pkg/beholder/batch_emitter_service.go:176
- This log-suppression condition matches any
*batch.PublishError. However, the batch client also usesPublishErrorfor the syntheticErrCodeResultsMismatchcase (pkg/chipingress/batch/client.go:405-409), which is a client/server contract issue rather than a persistent per-event validation failure. As written, results-mismatch errors will be silently dropped from logs too; consider still logging those (perhaps at ERROR) while continuing to suppress normal per-event PublishErrors.
// Partial delivery is not logged: it is a per-event, often persistent
// server-side condition (e.g. missing schema) that would otherwise log on
// every dropped event. chip_ingress.events_dropped (error_code) already
// captures that it's happening and roughly why; the full reason isn't
// needed at fleet-wide log volume.
var pubErr *batch.PublishError
if !errors.As(sendErr, &pubErr) {
e.eng.Errorw("failed to emit to chip ingress",
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| assert.Equal(t, tt.want, ErrorCodeFor(tt.err)) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
pkg/beholder/batch_emitter_service.go:183
- The log-suppression condition currently suppresses all
batch.PublishErrorinstances. This also includes the syntheticErrCodeResultsMismatchcase (server returned an unexpected number of results), which is a client/server-contract issue and is likely worth logging (it’s not the high-volume, persistent per-event validation case the suppression is intended for). Consider only suppressing logs for “real” per-event publish errors, while still logging results-mismatch (and any other non-persistent) failures.
var pubErr *batch.PublishError
if !errors.As(sendErr, &pubErr) {
e.eng.Errorw("failed to emit to chip ingress",
"error", sendErr,
"error_code", errorCode,
"error_reason", sendErr.Error(),
"domain", domain,
"entity", entity,
)
}
With partial delivery enabled, per-event
PublishErrorfailures are now returned in the callback. This PR:batch.ErrorCodeFortopkg/chipingress/batch/client.go, classifying a send/queue error into a boundederror_codestring viaerrors.As/errors.Is/status.FromError.PublishErrorfailures — detected directly viaerrors.As(sendErr, &pubErr)in beholder — since they're already captured by thechip_ingress.events_droppedmetric.error_codeanderror_reason(sendErr.Error()).error_codeattribute to thechip_ingress.events_droppedmetric.Self-contained: the chipingress-side classification helper (previously split out as its own PR, #2210, closed) now lives in this PR alongside its beholder consumer, so there's no cross-PR
go.moddependency to coordinate.Jira: INFOPLAT-13901