Skip to content

Commit 3cd4538

Browse files
authored
feat(metrics): extend node_info and add node status gauges (#7049)
1 parent 7a07046 commit 3cd4538

12 files changed

Lines changed: 698 additions & 4 deletions

File tree

‎common/src/main/java/org/tron/common/prometheus/MetricKeys.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ public static class Gauge {
4242
public static final String DB_SST_LEVEL = "tron:db_sst_level";
4343
public static final String MANAGER_QUEUE = "tron:manager_queue_size";
4444
public static final String TX_CACHE = "tron:tx_cache";
45+
// per local consensus witness miner; value 1, label is the Base58Check witness address
46+
public static final String WITNESS_ADDRESS = "tron:witness_address";
47+
// numeric backup-election role of this node: 0 = init, 1 = master, 2 = slaver
48+
public static final String NODE_ROLE = "tron:node_role";
49+
// witness flag of this node: 0 = not witness, 1 = witness
50+
public static final String IS_WITNESS = "tron:is_witness";
4551

4652
private Gauge() {
4753
throw new IllegalStateException("Gauge");

‎common/src/main/java/org/tron/common/prometheus/MetricLabels.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public static class Gauge {
6262

6363
public static final String PEERS_VALID = "valid";
6464

65+
// ADDRESS is the label of tron:witness_address (node_role and is_witness carry no label)
66+
public static final String ADDRESS = "address";
67+
6568
private Gauge() {
6669
throw new IllegalStateException("Gauge");
6770
}

‎common/src/main/java/org/tron/common/prometheus/MetricsGauge.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ class MetricsGauge {
1919
init(MetricKeys.Gauge.DB_SIZE_BYTES, "tron db size .", "type", "db", "level");
2020
init(MetricKeys.Gauge.DB_SST_LEVEL, "tron db files .", "type", "db", "level");
2121
init(MetricKeys.Gauge.TX_CACHE, "tron tx cache info.", "type");
22+
init(MetricKeys.Gauge.WITNESS_ADDRESS,
23+
"1 for each consensus witness address this node holds a miner for.",
24+
MetricLabels.Gauge.ADDRESS);
25+
init(MetricKeys.Gauge.IS_WITNESS,
26+
"Whether this node runs in witness mode, encoded as a number: 0 = not witness, "
27+
+ "1 = witness.");
28+
init(MetricKeys.Gauge.NODE_ROLE,
29+
"Backup-election role of this node, encoded as a number: 0 = init, "
30+
+ "1 = master, 2 = slaver.");
2231
}
2332

2433
private MetricsGauge() {

‎common/src/main/java/org/tron/common/prometheus/SRMetrics.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@ private SRMetrics() {
1010
throw new IllegalStateException("SRMetrics");
1111
}
1212

13+
// is_witness: 0 = not witness, 1 = witness
14+
public static final int NOT_WITNESS = 0;
15+
public static final int WITNESS = 1;
16+
17+
// node_role: 0 = init (pre-election / re-election), 1 = master, 2 = slaver
18+
public static final int NODE_ROLE_INIT = 0;
19+
public static final int NODE_ROLE_MASTER = 1;
20+
public static final int NODE_ROLE_SLAVER = 2;
21+
22+
/** Publishes is_witness as one label-less series; 0 = not witness, 1 = witness. */
23+
public static void recordIsWitness(int isWitness) {
24+
if (!Metrics.enabled()) {
25+
return;
26+
}
27+
Metrics.gaugeSet(MetricKeys.Gauge.IS_WITNESS, isWitness);
28+
}
29+
30+
/** Publishes node_role as one label-less series; role changes rewrite it in place. */
31+
public static void recordNodeRole(int role) {
32+
if (!Metrics.enabled()) {
33+
return;
34+
}
35+
Metrics.gaugeSet(MetricKeys.Gauge.NODE_ROLE, role);
36+
}
37+
38+
/** Publishes one tron:witness_address series (value 1) for a local witness address. */
39+
public static void recordWitnessAddress(String address) {
40+
if (!Metrics.enabled()) {
41+
return;
42+
}
43+
Metrics.gaugeSet(MetricKeys.Gauge.WITNESS_ADDRESS, 1, address);
44+
}
45+
1346
public static void recordSrSetChange(List<ByteString> currentWits, List<ByteString> newWits) {
1447
if (!Metrics.enabled()) {
1548
return;

‎docs/metrics-changelog.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ This file tracks Prometheus metric additions, changes, and removals in java-tron
1010
#### Core
1111

1212
- `tron:node_info` (Info, labels `version`, `genesis_block_id`) — static node identity: the running node version string plus the full genesis block hash as the canonical chain identifier, for fleet dashboards and version/chain alert rules. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
13+
- `tron:is_witness` (Gauge, no labels) — whether this node runs in witness mode, encoded as a number: `0` = `not witness` (started without witness mode; witness keys are not loaded and no `tron:witness_address` series is published), `1` = `witness` (started with `-w`). It is a process identity, published once at startup and independent of the backup-election role. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
14+
- `tron:node_role` (Gauge, no labels) — backup-election role of this node, encoded as a number: `0` = `init` (backup election configured but not yet run, and again while a re-election is running after the master lost keep-alive), `1` = `master` (the backup primary; also the standalone default for both node types when backup is not configured), `2` = `slaver` (standing by for a master). The role is a node-level status, independent of `tron:is_witness`, so the metric is a single series: a role change rewrites its value, so no stale series remain. Refreshed at startup and on every backup-status change. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
15+
- `tron:witness_address` (Gauge, label `address`) — value `1` for each consensus witness miner this node holds; the label is the Base58Check-encoded witness account address. The set is assembled once at startup from the finalized consensus miners: one series per configured witness private key, or — when a single private key signs for an explicitly configured witness account address — that witness account address. With several witness keys, each address gets its own series. A plain fullnode without a witness miner publishes no `tron:witness_address` series at all. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
1316
- `tron:block_fetch_armed` (Counter) — incremented when `FetchBlockService` arms a fetch tracking for a block; denominator for secondary-fetch rates. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
1417
- `tron:block_fetch_secondary` (Counter) — incremented when a secondary fetch request is sent to an alternate peer. ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))
1518
- `tron:block_already_known` (Counter) — incremented when a block response matches an outstanding adv request whose exact block ID is already known before that response is processed (best-effort signal; concurrent arrivals may be missed). ([#6923](https://github.com/tronprotocol/java-tron/issues/6923))

‎framework/src/main/java/org/tron/common/backup/BackupManager.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.concurrent.ConcurrentHashMap;
1515
import java.util.concurrent.ScheduledExecutorService;
1616
import java.util.concurrent.TimeUnit;
17+
import javax.annotation.PostConstruct;
1718
import lombok.Getter;
1819
import lombok.Setter;
1920
import lombok.extern.slf4j.Slf4j;
@@ -25,6 +26,8 @@
2526
import org.tron.common.backup.socket.UdpEvent;
2627
import org.tron.common.es.ExecutorServiceManager;
2728
import org.tron.common.parameter.CommonParameter;
29+
import org.tron.common.prometheus.Metrics;
30+
import org.tron.common.prometheus.SRMetrics;
2831
import org.tron.p2p.utils.NetUtil;
2932

3033
@Slf4j(topic = "backup")
@@ -67,6 +70,41 @@ public class BackupManager implements EventHandler {
6770
public void setStatus(BackupStatusEnum status) {
6871
logger.info("Change backup status to {}", status);
6972
this.status = status;
73+
recordNodeRole();
74+
}
75+
76+
/** Publishes tron:node_role once at startup, and again on every backup-status change. */
77+
@PostConstruct
78+
public synchronized void recordNodeRole() {
79+
if (!Metrics.enabled()) {
80+
return;
81+
}
82+
SRMetrics.recordNodeRole(currentRole());
83+
}
84+
85+
private int currentRole() {
86+
// no backup configured: default to master, for either identity
87+
if (!isBackupConfigured()) {
88+
return SRMetrics.NODE_ROLE_MASTER;
89+
}
90+
// status still defaults to MASTER before backup init; not a real election result yet
91+
if (!isInit) {
92+
return SRMetrics.NODE_ROLE_INIT;
93+
}
94+
// post-init INIT covers the pre-keep-alive window and master-loss re-election
95+
switch (status) {
96+
case MASTER:
97+
return SRMetrics.NODE_ROLE_MASTER;
98+
case SLAVER:
99+
return SRMetrics.NODE_ROLE_SLAVER;
100+
default:
101+
return SRMetrics.NODE_ROLE_INIT;
102+
}
103+
}
104+
105+
private boolean isBackupConfigured() {
106+
// same condition BackupServer.initServer() uses to start the election transport
107+
return parameter.getBackupPort() > 0 && !parameter.getBackupMembers().isEmpty();
70108
}
71109

72110
public void init() {

‎framework/src/main/java/org/tron/core/consensus/ConsensusService.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import org.springframework.stereotype.Component;
1212
import org.tron.common.crypto.SignUtils;
1313
import org.tron.common.parameter.CommonParameter;
14+
import org.tron.common.prometheus.Metrics;
15+
import org.tron.common.prometheus.SRMetrics;
16+
import org.tron.common.utils.StringUtil;
1417
import org.tron.consensus.Consensus;
1518
import org.tron.consensus.base.Param;
1619
import org.tron.consensus.base.Param.Miner;
@@ -77,12 +80,47 @@ public void start() {
7780
}
7881

7982
param.setMiners(miners);
83+
recordWitnessAddresses(miners);
84+
recordIsWitness();
8085
param.setBlockHandle(blockHandle);
8186
param.setPbftInterface(pbftBaseImpl);
8287
consensus.start(param);
8388
logger.info("consensus service start success");
8489
}
8590

91+
private void recordWitnessAddresses(List<Miner> miners) {
92+
if (!Metrics.enabled() || miners == null) {
93+
return;
94+
}
95+
for (Miner miner : miners) {
96+
ByteString witnessAddress = miner.getWitnessAddress();
97+
try {
98+
String address = StringUtil.encode58Check(witnessAddress.toByteArray());
99+
SRMetrics.recordWitnessAddress(address);
100+
} catch (Exception e) {
101+
logger.warn("Failed to record witness address metrics.", e);
102+
}
103+
}
104+
}
105+
106+
/**
107+
* Publishes tron:is_witness once at consensus startup; 0 = not witness, 1 = witness. It is
108+
* published independently of the witness-address publication, and even when no miner is
109+
* configured, so the flag series always exists. Catches its own failure so a metrics
110+
* problem can neither skip the witness publication nor abort consensus startup.
111+
*/
112+
private void recordIsWitness() {
113+
if (!Metrics.enabled()) {
114+
return;
115+
}
116+
try {
117+
SRMetrics.recordIsWitness(Args.getInstance().isWitness()
118+
? SRMetrics.WITNESS : SRMetrics.NOT_WITNESS);
119+
} catch (Exception e) {
120+
logger.warn("Failed to record is-witness metrics.", e);
121+
}
122+
}
123+
86124
public void stop() {
87125
logger.info("consensus service closed start.");
88126
consensus.stop();

‎framework/src/test/java/org/tron/common/backup/BackupManagerTest.java‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.tron.common.backup;
22

33
import io.netty.channel.Channel;
4+
import io.prometheus.client.CollectorRegistry;
45
import java.lang.reflect.Field;
56
import java.lang.reflect.Method;
67
import java.lang.reflect.Modifier;
@@ -20,14 +21,19 @@
2021
import org.junit.Rule;
2122
import org.junit.Test;
2223
import org.junit.rules.TemporaryFolder;
24+
import org.mockito.Mockito;
2325
import org.tron.common.TestConstants;
2426
import org.tron.common.backup.BackupManager.BackupStatusEnum;
2527
import org.tron.common.backup.message.KeepAliveMessage;
2628
import org.tron.common.backup.socket.BackupServer;
29+
import org.tron.common.backup.socket.MessageHandler;
2730
import org.tron.common.backup.socket.UdpEvent;
2831
import org.tron.common.parameter.CommonParameter;
32+
import org.tron.common.prometheus.MetricKeys;
33+
import org.tron.common.prometheus.SRMetrics;
2934
import org.tron.common.utils.PublicMethod;
3035
import org.tron.common.utils.ReflectUtils;
36+
import org.tron.consensus.base.Param;
3137
import org.tron.core.config.args.Args;
3238
import org.tron.core.config.args.InetUtil;
3339

@@ -76,6 +82,9 @@ public void tearDown() throws Exception {
7682
});
7783
InetUtil.dnsLookup = previousDnsLookup;
7884
Args.clearParam();
85+
// the consensus miner list is a process-wide singleton: reset it so this class
86+
// cannot leak state into other tests of the same JVM
87+
Param.getInstance().setMiners(new ArrayList<>());
7988
BackupTestUtils.throwIfAnyError(errors);
8089
}
8190

@@ -183,6 +192,64 @@ public void testBackupServerLifecycleDuringKeepAliveInterval() throws Exception
183192
BackupTestUtils.assertExecutorsTerminated(manager, backupServer);
184193
}
185194

195+
/**
196+
* A backup group whose only member is this node's own address is a "single-master" group:
197+
* no peer ever answers a keep-alive, so the node must leave the pre-election INIT window and
198+
* self-promote to MASTER once the keep-alive timeout elapses.
199+
*
200+
* <p>Drives the real {@link BackupManager#init()} and its real scheduled keep-alive task. The
201+
* timeout condition is made deterministic by ageing the last keep-alive time, so the first
202+
* (fixed 1s) scheduler tick is guaranteed to observe an expired keep-alive; recovery is then
203+
* awaited by polling, so there is no fixed sleep. The published {@code tron:node_role} must
204+
* track the transition (0 = init, 1 = master) and the manager executor must terminate.
205+
*/
206+
@Test(timeout = 30000)
207+
public void singleMasterGroupRecoversFromInitToMaster() throws Exception {
208+
CommonParameter parameter = CommonParameter.getInstance();
209+
parameter.setBackupPriority(8);
210+
// the group's only member is ourselves, so init() drops it and no peer can answer
211+
parameter.setBackupMembers(
212+
Collections.singletonList(InetAddress.getLocalHost().getHostAddress()));
213+
214+
boolean previousMetrics = Args.getInstance().isMetricsPrometheusEnable();
215+
Args.getInstance().setMetricsPrometheusEnable(true);
216+
BackupManager single = new BackupManager();
217+
// absorb any keep-alive send in case the local IP lookup behaves differently
218+
single.setMessageHandler(Mockito.mock(MessageHandler.class));
219+
try {
220+
// backup configured but not yet init'ed: the pre-election window reports init (0)
221+
Assert.assertEquals(BackupManager.BackupStatusEnum.MASTER, single.getStatus());
222+
single.recordNodeRole();
223+
Assert.assertEquals(SRMetrics.NODE_ROLE_INIT, nodeRoleGauge(), 0.0);
224+
225+
single.init();
226+
Assert.assertEquals(BackupManager.BackupStatusEnum.INIT, single.getStatus());
227+
Assert.assertEquals(SRMetrics.NODE_ROLE_INIT, nodeRoleGauge(), 0.0);
228+
229+
// no keep-alive has ever arrived, so the next keep-alive tick must self-promote.
230+
// setStatus() flips the volatile status before it republishes the gauge, so wait on
231+
// the published series (the externally visible result), not on the status field alone.
232+
ReflectUtils.setFieldValue(single, "lastKeepAliveTime", 0L);
233+
BackupTestUtils.awaitCondition("lone master to recover from INIT",
234+
() -> single.getStatus() == BackupManager.BackupStatusEnum.MASTER
235+
&& Double.valueOf(SRMetrics.NODE_ROLE_MASTER).equals(nodeRoleGauge()));
236+
Assert.assertEquals(BackupManager.BackupStatusEnum.MASTER, single.getStatus());
237+
Assert.assertEquals(SRMetrics.NODE_ROLE_MASTER, nodeRoleGauge(), 0.0);
238+
} finally {
239+
single.stop();
240+
Args.getInstance().setMetricsPrometheusEnable(previousMetrics);
241+
}
242+
243+
ScheduledExecutorService executorService =
244+
(ScheduledExecutorService) ReflectUtils.getFieldObject(single, "executorService");
245+
Assert.assertTrue("single-master executor must terminate", executorService.isTerminated());
246+
}
247+
248+
private Double nodeRoleGauge() {
249+
return CollectorRegistry.defaultRegistry.getSampleValue(
250+
MetricKeys.Gauge.NODE_ROLE, new String[] {}, new String[] {});
251+
}
252+
186253
// ===== domain-handling tests for init() =====
187254

188255
@Test(timeout = 5000)

0 commit comments

Comments
 (0)