-
Notifications
You must be signed in to change notification settings - Fork 20
feat(blocklistener): Block height metrics for canonical chain vs target node #216
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
e1eb2e8
6384c74
fc14bb1
1f8edd5
ae80654
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,92 @@ | ||||||
| // Copyright © 2026 Kaleido, Inc. | ||||||
| // | ||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||
| // | ||||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| // 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. | ||||||
|
|
||||||
| package ethblocklistener | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
|
|
||||||
| "github.com/hyperledger-firefly/common/pkg/i18n" | ||||||
| "github.com/hyperledger-firefly/common/pkg/log" | ||||||
| "github.com/hyperledger-firefly/common/pkg/metric" | ||||||
| "github.com/hyperledger-firefly/evmconnect/internal/msgs" | ||||||
| ) | ||||||
|
|
||||||
| const ( | ||||||
| metricsSubsystem = "blocklistener" | ||||||
|
|
||||||
| // metricTargetBlockHeight is the block height the endpoint we are connected to reports via eth_blockNumber. | ||||||
| // Emitted from queryBlockHeightFromRPC, so it is always the value we last received from the node. | ||||||
| metricTargetBlockHeight = "target_block_height" | ||||||
| // metricCanonicalBlockHeight is the head of the chain this listener is tracking - in full chain tracking | ||||||
| // mode the head of the in-memory canonical chain built from the block filter / newHeads subscription, | ||||||
| // and in light mode the head we dispatch to consumers. It should track the target height very closely. | ||||||
| metricCanonicalBlockHeight = "canonical_block_height" | ||||||
| // metricPollFailures counts the JSON/RPC polls the listen loop makes that failed, labelled by method. | ||||||
| metricPollFailures = "poll_failures_total" | ||||||
| metricLabelPollFailures = "method" | ||||||
| ) | ||||||
|
|
||||||
| // InitMetrics registers the block listener metrics against the supplied registry. | ||||||
| func (bl *blockListener) InitMetrics(ctx context.Context, registry metric.MetricsRegistry) error { | ||||||
| mm, err := registry.NewMetricsManagerForSubsystem(ctx, metricsSubsystem) | ||||||
| if err != nil { | ||||||
| return i18n.WrapError(ctx, err, msgs.MsgMetricsInitFail, metricsSubsystem) | ||||||
| } | ||||||
| mm.NewGaugeMetric(ctx, metricTargetBlockHeight, "The block height reported by the connected node via eth_blockNumber", false) | ||||||
| mm.NewGaugeMetric(ctx, metricCanonicalBlockHeight, "The block height of the head of the chain tracked by the block listener", false) | ||||||
| mm.NewCounterMetricWithLabels(ctx, metricPollFailures, "The number of block listener JSON/RPC polls that have failed, by method", []string{metricLabelPollFailures}, false) | ||||||
|
|
||||||
| bl.metricsLock.Lock() | ||||||
| defer bl.metricsLock.Unlock() | ||||||
|
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. The lock is needed because the lifecycle of a metrics registry is tied to a transaction manager, but the block listener is not initiated inside the transaction manager. In order to avoid a lock, a standalone metrics registry needs to be created first and then passed into both the listener and the transaction manager. That approach will reflect the fact that transaction manager no longer controls the metric prefix for components that emit custom metrics into the metrics that are linked to its metrics endpoint. |
||||||
| bl.metrics = mm | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (bl *blockListener) getMetrics() metric.MetricsManager { | ||||||
| bl.metricsLock.RLock() | ||||||
| defer bl.metricsLock.RUnlock() | ||||||
| return bl.metrics | ||||||
| } | ||||||
|
|
||||||
| func (bl *blockListener) setBlockHeightMetric(metricName string, blockHeight uint64) { | ||||||
| mm := bl.getMetrics() | ||||||
| if mm == nil { | ||||||
| return | ||||||
| } | ||||||
| mm.SetGaugeMetric(bl.ctx, metricName, float64(blockHeight), nil) | ||||||
| } | ||||||
|
|
||||||
| func (bl *blockListener) incPollFailureMetric(method string) { | ||||||
| mm := bl.getMetrics() | ||||||
| if mm == nil { | ||||||
| return | ||||||
| } | ||||||
| mm.IncCounterMetricWithLabels(bl.ctx, metricPollFailures, map[string]string{metricLabelPollFailures: method}, nil) | ||||||
| } | ||||||
|
|
||||||
| // refreshTargetBlockHeightMetric queries the node for the height it reports, purely so the target gauge | ||||||
| // stays current. Only needed in full chain tracking mode. | ||||||
| func (bl *blockListener) refreshTargetBlockHeightMetric() { | ||||||
| if bl.getMetrics() == nil { | ||||||
| return // never drive any query of the node when metrics are not enabled | ||||||
|
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.
Suggested change
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. This highlights a problem I overlooked as part of hyperledger-firefly/transaction-manager#160 Since the raw MetricsRegistry interface from ff-common was exposed, it didn't include information about whether the metrics are enabled in the config. The control was wrapped in the internal Metrics interface: https://github.com/hyperledger-firefly/transaction-manager/blob/162a945b953601c8ea0811dc701a4d4e06d348da/internal/metrics/metrics.go#L90. But feels like a separate PR to sort this out, as there is a feature gap in the metrics registry
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. |
||||||
| } | ||||||
| if _, err := bl.queryBlockHeightFromRPC(); err != nil { | ||||||
| // Diagnostic only - the failure is recorded on the query failure counter, and the listen loop | ||||||
| // has its own error handling for the chain state | ||||||
| log.L(bl.ctx).Warnf("Failed to refresh target block height: %s", err) | ||||||
| } | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.