You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2021/08/04 18:10:52 UTC

[incubator-tuweni] branch main updated: Add configuration to monitor different upgrades

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git


The following commit(s) were added to refs/heads/main by this push:
     new 0b3cb61  Add configuration to monitor different upgrades
     new 7086a70  Merge pull request #335 from atoulme/add_multiple_upgrade_configs
0b3cb61 is described below

commit 0b3cb6186404e97a313b726cb4734072ba536739
Author: Antoine Toulme <an...@lunar-ocean.com>
AuthorDate: Tue Aug 3 22:44:09 2021 -0700

    Add configuration to monitor different upgrades
---
 .../apache/tuweni/config/PropertyValidator.java    |  21 +++-
 .../org/apache/tuweni/eth/crawler/CrawlerApp.kt    |   2 +-
 .../org/apache/tuweni/eth/crawler/CrawlerConfig.kt |  93 +++++++++++------
 .../tuweni/eth/crawler/RelationalPeerRepository.kt | 116 ++++++++++++++-------
 .../tuweni/eth/crawler/rest/ClientsService.kt      |   9 +-
 5 files changed, 162 insertions(+), 79 deletions(-)

diff --git a/config/src/main/java/org/apache/tuweni/config/PropertyValidator.java b/config/src/main/java/org/apache/tuweni/config/PropertyValidator.java
index 36606a5..c59c01d 100644
--- a/config/src/main/java/org/apache/tuweni/config/PropertyValidator.java
+++ b/config/src/main/java/org/apache/tuweni/config/PropertyValidator.java
@@ -24,6 +24,8 @@ import java.util.List;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
+import com.google.common.base.Strings;
+
 /**
  * A validator associated with a specific configuration property.
  */
@@ -208,9 +210,9 @@ public interface PropertyValidator<T> {
   }
 
   /**
-   * A validator that ensures a property, if present, is a well-formed port number.
+   * A validator that ensures a property, if present, is a valid port number.
    *
-   * @return A validator that ensures a property, if present, is a well-formed number.
+   * @return A validator that ensures a property, if present, is a valid port number.
    */
   static PropertyValidator<Integer> isValidPort() {
     return (key, position, value) -> {
@@ -222,6 +224,21 @@ public interface PropertyValidator<T> {
   }
 
   /**
+   * A validator that ensures a property, if present, is not blank.
+   *
+   * @return A validator that ensures a property, if present, is not blank.
+   */
+  static PropertyValidator<String> isNotBlank() {
+    return (key, position, value) -> {
+      if (value != null && !Strings.isNullOrEmpty(value)) {
+        return singleError(position, "Value of property '" + key + "' is blank");
+      }
+      return noErrors();
+    };
+  }
+
+
+  /**
    * Validate a configuration property.
    *
    * @param key The configuration property key.
diff --git a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerApp.kt b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerApp.kt
index a143225..e0ed78f 100644
--- a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerApp.kt
+++ b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerApp.kt
@@ -118,7 +118,7 @@ class CrawlerApplication(
       .load()
     flyway.migrate()
     val crawlerMeter = metricsService.meterSdkProvider["crawler"]
-    val repo = RelationalPeerRepository(ds, config.peerCacheExpiration(), config.clientIdsInterval(), config.clientsStatsDelay(), crawlerMeter, createCoroutineContext())
+    val repo = RelationalPeerRepository(ds, config.peerCacheExpiration(), config.clientIdsInterval(), config.clientsStatsDelay(), crawlerMeter, config.upgradesVersions(), createCoroutineContext())
 
     logger.info("Initial bootnodes: ${config.bootNodes()}")
     val scraper = Scraper(
diff --git a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerConfig.kt b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerConfig.kt
index 3be433d..18591bd 100644
--- a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerConfig.kt
+++ b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/CrawlerConfig.kt
@@ -18,6 +18,7 @@ package org.apache.tuweni.eth.crawler
 
 import org.apache.tuweni.config.Configuration
 import org.apache.tuweni.config.PropertyValidator
+import org.apache.tuweni.config.Schema
 import org.apache.tuweni.config.SchemaBuilder
 import java.net.URI
 import java.nio.file.Path
@@ -48,38 +49,46 @@ class CrawlerConfig(val filePath: Path) {
 
     val mainnetDiscoveryDNS = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.mainnet.ethdisco.net"
 
-    fun schema() = SchemaBuilder.create()
-      .addInteger("discoveryPort", 11000, "Discovery service port", PropertyValidator.isValidPort())
-      .addString("discoveryNetworkInterface", "127.0.0.1", "Discovery network interface", null)
-      .addInteger("rlpxPort", 11000, "RLPx service port", PropertyValidator.inRange(1, 65536))
-      .addString("rlpxNetworkInterface", "127.0.0.1", "RLPx network interface", null)
-      .addListOfString("bootnodes", mainnetEthereumBootnodes, "Bootnodes to discover other peers from", null)
-      .addString("discoveryDNS", mainnetDiscoveryDNS, "DNS discovery crawler", null)
-      .addLong("discoveryDNSPollingPeriod", 60 * 1000L, "DNS Discovery Polling Period in milliseconds", null)
-      .addString(
-        "jdbcUrl", System.getProperty("DATABASE_URL", System.getenv("DATABASE_URL")),
-        "JDBC URL of the form jdbc:posgresql://localhost:5432", PropertyValidator.isPresent()
-      )
-      .addInteger("jdbcConnections", 25, "Number of JDBC connections for the connections pool", null)
-      .addString("network", "mainnet", "Network to use instead of providing a genesis file.", null)
-      .addString("genesisFile", "", "Genesis file to use in hello", null)
-      .addInteger("restPort", 1337, "REST port", null)
-      .addString("restNetworkInterface", "0.0.0.0", "REST network interface", null)
-      .addString("ethstatsNetworkInterface", "0.0.0.0", "Ethstats network interface", null)
-      .addInteger("ethstatsPort", 1338, "Ethstats port", null)
-      .addString("ethstatsSecret", "changeme", "Ethstats shared secret", null)
-      .addLong("peerCacheExpiration", 5 * 60 * 1000L, "Peer data cache expiration", null)
-      .addLong("clientIdsInterval", 24 * 60 * 60 * 1000 * 2L, "Client IDs Interval - number of milliseconds to go back in time", null)
-      .addLong("clientsStatsDelay", 30 * 1000, "Delay between client stats calculations", null)
-      .addLong("rlpxDisconnectionDelay", 10 * 1000L, "RLPx connections disconnection delay", null)
-      .addInteger("maxRequestsPerSec", 30, "Number of requests per second over HTTP", null)
-      .addInteger("numberOfThreads", 10, "Number of Threads for each thread pool", null)
-      .addInteger("metricsPort", 9090, "Metric service port", PropertyValidator.isValidPort())
-      .addString("metricsNetworkInterface", "localhost", "Metric service network interface", null)
-      .addBoolean("metricsGrpcPushEnabled", false, "Enable pushing metrics to gRPC service", null)
-      .addBoolean("metricsPrometheusEnabled", false, "Enable exposing metrics on the Prometheus endpoint", null)
-      .addString("corsAllowedOrigins", "*", "CORS allowed domains filter for REST service", null)
-      .toSchema()
+    fun schema(): Schema {
+      val schema = SchemaBuilder.create()
+        .addInteger("discoveryPort", 11000, "Discovery service port", PropertyValidator.isValidPort())
+        .addString("discoveryNetworkInterface", "127.0.0.1", "Discovery network interface", null)
+        .addInteger("rlpxPort", 11000, "RLPx service port", PropertyValidator.inRange(1, 65536))
+        .addString("rlpxNetworkInterface", "127.0.0.1", "RLPx network interface", null)
+        .addListOfString("bootnodes", mainnetEthereumBootnodes, "Bootnodes to discover other peers from", null)
+        .addString("discoveryDNS", mainnetDiscoveryDNS, "DNS discovery crawler", null)
+        .addLong("discoveryDNSPollingPeriod", 60 * 1000L, "DNS Discovery Polling Period in milliseconds", null)
+        .addString(
+          "jdbcUrl", System.getProperty("DATABASE_URL", System.getenv("DATABASE_URL")),
+          "JDBC URL of the form jdbc:posgresql://localhost:5432", PropertyValidator.isPresent()
+        )
+        .addInteger("jdbcConnections", 25, "Number of JDBC connections for the connections pool", null)
+        .addString("network", "mainnet", "Network to use instead of providing a genesis file.", null)
+        .addString("genesisFile", "", "Genesis file to use in hello", null)
+        .addInteger("restPort", 1337, "REST port", null)
+        .addString("restNetworkInterface", "0.0.0.0", "REST network interface", null)
+        .addString("ethstatsNetworkInterface", "0.0.0.0", "Ethstats network interface", null)
+        .addInteger("ethstatsPort", 1338, "Ethstats port", null)
+        .addString("ethstatsSecret", "changeme", "Ethstats shared secret", null)
+        .addLong("peerCacheExpiration", 5 * 60 * 1000L, "Peer data cache expiration", null)
+        .addLong("clientIdsInterval", 24 * 60 * 60 * 1000 * 2L, "Client IDs Interval - number of milliseconds to go back in time", null)
+        .addLong("clientsStatsDelay", 30 * 1000, "Delay between client stats calculations", null)
+        .addLong("rlpxDisconnectionDelay", 10 * 1000L, "RLPx connections disconnection delay", null)
+        .addInteger("maxRequestsPerSec", 30, "Number of requests per second over HTTP", null)
+        .addInteger("numberOfThreads", 10, "Number of Threads for each thread pool", null)
+        .addInteger("metricsPort", 9090, "Metric service port", PropertyValidator.isValidPort())
+        .addString("metricsNetworkInterface", "localhost", "Metric service network interface", null)
+        .addBoolean("metricsGrpcPushEnabled", false, "Enable pushing metrics to gRPC service", null)
+        .addBoolean("metricsPrometheusEnabled", false, "Enable exposing metrics on the Prometheus endpoint", null)
+        .addString("corsAllowedOrigins", "*", "CORS allowed domains filter for REST service", null)
+
+      val upgradesSection = SchemaBuilder.create()
+        .addString("name", null, "Upgrade name, eg London or Magneto", PropertyValidator.isNotBlank())
+        .addListOfMap("versions", listOf(), "List of minimum version mappings for the upgrade", null)
+        .toSchema()
+      schema.addSection("upgrades", upgradesSection)
+      return schema.toSchema()
+    }
   }
 
   val config = Configuration.fromToml(filePath, schema())
@@ -118,4 +127,24 @@ class CrawlerConfig(val filePath: Path) {
   fun metricsGrpcPushEnabled() = config.getBoolean("metricsGrpcPushEnabled")
   fun metricsPrometheusEnabled() = config.getBoolean("metricsPrometheusEnabled")
   fun corsAllowedOrigins() = config.getString("corsAllowedOrigins")
+
+  fun upgradesVersions(): List<UpgradeConfig> {
+    val upgrades = config.sections("upgrades")
+    val result = mutableListOf<UpgradeConfig>()
+    for (upgrade in upgrades) {
+      val section = config.getConfigurationSection(upgrade)
+      val versions = mutableMapOf<String, String>()
+      for (map in section.getListOfMap("versions")) {
+        for (entry in map.entries) {
+          versions.put(entry.key.toLowerCase(), entry.value.toString())
+        }
+      }
+
+      val upgradeConfig = UpgradeConfig(section.getString("name"), versions)
+      result.add(upgradeConfig)
+    }
+    return result
+  }
 }
+
+data class UpgradeConfig(val name: String, val versions: Map<String, String>)
diff --git a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/RelationalPeerRepository.kt b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/RelationalPeerRepository.kt
index 1c769df..ed5b59b 100644
--- a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/RelationalPeerRepository.kt
+++ b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/RelationalPeerRepository.kt
@@ -48,28 +48,21 @@ open class RelationalPeerRepository(
   private val clientIdsInterval: Long = 24 * 60 * 60 * 1000 * 2,
   private val clientsStatsDelay: Long = 30 * 1000,
   private val meter: Meter,
+  private val upgradeConfigs: List<UpgradeConfig> = listOf(),
   override val coroutineContext: CoroutineContext = Dispatchers.Default,
 ) : CoroutineScope, PeerRepository {
 
   companion object {
     internal val logger = LoggerFactory.getLogger(RelationalPeerRepository::class.java)
-
-    // TODO make this configurable
-    val londonClientVersions = mutableMapOf(
-      Pair("geth", "v1.10.4"),
-      Pair("nethermind", "v1.10.73"),
-      Pair("turbogeth", "v2021.06.04-alpha"),
-      Pair("turbo-geth", "v2021.06.04-alpha"),
-      Pair("erigon", "v2021.06.04-alpha"),
-      Pair("besu", "v21.7.0-RC1"),
-      Pair("openethereum", "v3.3.0-rc2"),
-      Pair("ethereumjs", "v5.4.1")
-    )
   }
+
   private val listeners = mutableListOf<(Peer) -> Unit>()
   private val peerCache = ExpiringMap<SECP256K1.PublicKey, String>()
-  private val totalClientsGauge = meter.longValueRecorderBuilder("totalClients").setDescription("Number of nodes used to compute client stats").build()
-  private val clientCalculationsCounter = meter.longCounterBuilder("clients").setDescription("Number of times clients were computed").build()
+  private val totalClientsGauge =
+    meter.longValueRecorderBuilder("totalClients").setDescription("Number of nodes used to compute client stats")
+      .build()
+  private val clientCalculationsCounter =
+    meter.longCounterBuilder("clients").setDescription("Number of times clients were computed").build()
 
   override fun addListener(listener: (Peer) -> Unit) {
     listeners.add(listener)
@@ -121,7 +114,10 @@ open class RelationalPeerRepository(
 
   fun recordInfo(wireConnection: WireConnection, status: Status?) {
     dataSource.connection.use { conn ->
-      val peer = get(wireConnection.peerPublicKey(), Endpoint(wireConnection.peerHost(), wireConnection.peerPort())) as RepositoryPeer
+      val peer = get(
+        wireConnection.peerPublicKey(),
+        Endpoint(wireConnection.peerHost(), wireConnection.peerPort())
+      ) as RepositoryPeer
       val stmt =
         conn.prepareStatement(
           "insert into nodeInfo(id, createdAt, host, port, publickey, p2pVersion, clientId, capabilities, genesisHash, bestHash, totalDifficulty, identity, disconnectReason) values(?,?,?,?,?,?,?,?,?,?,?,?,?)"
@@ -173,11 +169,16 @@ open class RelationalPeerRepository(
     }
   }
 
-  internal fun getPeersWithInfo(infoCollected: Long, from: Int? = null, limit: Int? = null): List<PeerConnectionInfoDetails> {
+  internal fun getPeersWithInfo(
+    infoCollected: Long,
+    from: Int? = null,
+    limit: Int? = null,
+  ): List<PeerConnectionInfoDetails> {
     dataSource.connection.use { conn ->
-      var query = "select distinct nodeinfo.createdAt, nodeinfo.publickey, nodeinfo.p2pversion, nodeinfo.clientId, nodeinfo.capabilities, nodeinfo.genesisHash, nodeinfo.besthash, nodeinfo.totalDifficulty from nodeinfo " +
-        "  inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen " +
-        "  on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt where createdAt < ? order by nodeInfo.createdAt desc"
+      var query =
+        "select distinct nodeinfo.createdAt, nodeinfo.publickey, nodeinfo.p2pversion, nodeinfo.clientId, nodeinfo.capabilities, nodeinfo.genesisHash, nodeinfo.besthash, nodeinfo.totalDifficulty from nodeinfo " +
+          "  inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen " +
+          "  on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt where createdAt < ? order by nodeInfo.createdAt desc"
       if (from != null && limit != null) {
         query += " limit $limit offset $from"
       }
@@ -197,7 +198,18 @@ open class RelationalPeerRepository(
           val genesisHash = rs.getString(6) ?: ""
           val bestHash = rs.getString(7) ?: ""
           val totalDifficulty = rs.getString(8) ?: ""
-          result.add(PeerConnectionInfoDetails(createdAt, pubkey, p2pVersion, clientId, capabilities, genesisHash, bestHash, totalDifficulty))
+          result.add(
+            PeerConnectionInfoDetails(
+              createdAt,
+              pubkey,
+              p2pVersion,
+              clientId,
+              capabilities,
+              genesisHash,
+              bestHash,
+              totalDifficulty
+            )
+          )
         }
         return result
       }
@@ -227,8 +239,8 @@ open class RelationalPeerRepository(
   }
 
   private var clientIds: List<ClientInfo>? = null
-  private var londonStats: ClientReadyStats? = null
   private var clientsStats: Map<String, Map<String, Long>>? = null
+  private var upgradeStats: MutableMap<String, ClientReadyStats> = mutableMapOf()
   private val started = AtomicBoolean(false)
 
   fun start() {
@@ -240,23 +252,28 @@ open class RelationalPeerRepository(
         val newClientIds = getClientIdsInternal()
         logger.info("Found client ids ${newClientIds.size}")
         clientIds = newClientIds
-        var londonReady = 0
-        var total = 0
         val newClientsStats = mutableMapOf<String, MutableMap<String, Long>>()
+        val total = newClientIds.stream().mapToInt { it.count }.sum()
+
         newClientIds.forEach { newClientCount ->
-          total += newClientCount.count
-          val clientVersion = ClientIdInfo(newClientCount.clientId)
-          val versionStats = newClientsStats.computeIfAbsent(clientVersion.name) { mutableMapOf() }
-          val statsCount = versionStats[clientVersion.version] ?: 0
-          versionStats[clientVersion.version] = statsCount + newClientCount.count
-          londonClientVersions[clientVersion.name().toLowerCase()]?.let { londonVersion ->
-            if (clientVersion >= londonVersion) {
-              londonReady += newClientCount.count
+          val clientIdInfo = ClientIdInfo(newClientCount.clientId)
+          val versionStats = newClientsStats.computeIfAbsent(clientIdInfo.name) { mutableMapOf() }
+          val statsCount = versionStats[clientIdInfo.version] ?: 0
+          versionStats[clientIdInfo.version] = statsCount + newClientCount.count
+        }
+        for (upgradeConfig in upgradeConfigs) {
+          var upgradeReady = 0
+          newClientIds.forEach { newClientCount ->
+            val clientIdInfo = ClientIdInfo(newClientCount.clientId)
+            upgradeConfig.versions.get(clientIdInfo.name().toLowerCase())?.let { upgradeVersion ->
+              if (clientIdInfo >= upgradeVersion) {
+                upgradeReady += newClientCount.count
+              }
             }
           }
+          upgradeStats.put(upgradeConfig.name, ClientReadyStats(total, upgradeReady))
         }
         clientsStats = newClientsStats
-        londonStats = ClientReadyStats(total, londonReady)
         totalClientsGauge.record(total.toLong())
         clientCalculationsCounter.add(1)
 
@@ -269,7 +286,7 @@ open class RelationalPeerRepository(
     started.set(false)
   }
 
-  internal fun getLondonStats() = londonStats
+  internal fun getUpgradeStats() = upgradeStats
 
   internal fun getClientIds(): List<ClientInfo> = clientIds ?: listOf()
 
@@ -277,7 +294,8 @@ open class RelationalPeerRepository(
 
   internal fun getClientIdsInternal(): List<ClientInfo> {
     dataSource.connection.use { conn ->
-      val sql = "select clients.clientId, count(clients.clientId) from (select nodeinfo.clientId, nodeInfo.createdAt from nodeinfo inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt) as clients where clients.createdAt > ? group by clients.clientId"
+      val sql =
+        "select clients.clientId, count(clients.clientId) from (select nodeinfo.clientId, nodeInfo.createdAt from nodeinfo inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt) as clients where clients.createdAt > ? group by clients.clientId"
       val stmt =
         conn.prepareStatement(sql)
       stmt.use {
@@ -299,9 +317,10 @@ open class RelationalPeerRepository(
 
   internal fun getPeerWithInfo(infoCollected: Long, publicKey: String): PeerConnectionInfoDetails? {
     dataSource.connection.use { conn ->
-      var query = "select distinct nodeinfo.createdAt, nodeinfo.publickey, nodeinfo.p2pversion, nodeinfo.clientId, nodeinfo.capabilities, nodeinfo.genesisHash, nodeinfo.besthash, nodeinfo.totalDifficulty from nodeinfo " +
-        "  inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen " +
-        "  on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt where createdAt < ? and nodeinfo.publickey = ? order by nodeInfo.createdAt desc"
+      var query =
+        "select distinct nodeinfo.createdAt, nodeinfo.publickey, nodeinfo.p2pversion, nodeinfo.clientId, nodeinfo.capabilities, nodeinfo.genesisHash, nodeinfo.besthash, nodeinfo.totalDifficulty from nodeinfo " +
+          "  inner join (select identity, max(createdAt) as maxCreatedAt from nodeinfo group by identity) maxSeen " +
+          "  on nodeinfo.identity = maxSeen.identity and nodeinfo.createdAt = maxSeen.maxCreatedAt where createdAt < ? and nodeinfo.publickey = ? order by nodeInfo.createdAt desc"
       val stmt =
         conn.prepareStatement(query)
       stmt.use {
@@ -318,7 +337,16 @@ open class RelationalPeerRepository(
           val genesisHash = rs.getString(6) ?: ""
           val bestHash = rs.getString(7) ?: ""
           val totalDifficulty = rs.getString(8) ?: ""
-          return PeerConnectionInfoDetails(createdAt, pubkey, p2pVersion, clientId, capabilities, genesisHash, bestHash, totalDifficulty)
+          return PeerConnectionInfoDetails(
+            createdAt,
+            pubkey,
+            p2pVersion,
+            clientId,
+            capabilities,
+            genesisHash,
+            bestHash,
+            totalDifficulty
+          )
         } else {
           return null
         }
@@ -326,10 +354,20 @@ open class RelationalPeerRepository(
     }
   }
 }
+
 internal data class ClientReadyStats(val total: Int, val ready: Int)
 internal data class ClientInfo(val clientId: String, val count: Int)
 internal data class PeerConnectionInfo(val nodeId: SECP256K1.PublicKey, val host: String, val port: Int)
-internal data class PeerConnectionInfoDetails(val createdAt: Long, val nodeId: SECP256K1.PublicKey, val p2pVersion: Int, val clientId: String, val capabilities: String, val genesisHash: String, val bestHash: String, val totalDifficulty: String)
+internal data class PeerConnectionInfoDetails(
+  val createdAt: Long,
+  val nodeId: SECP256K1.PublicKey,
+  val p2pVersion: Int,
+  val clientId: String,
+  val capabilities: String,
+  val genesisHash: String,
+  val bestHash: String,
+  val totalDifficulty: String,
+)
 
 internal class RepositoryPeer(
   override val nodeId: SECP256K1.PublicKey,
diff --git a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/rest/ClientsService.kt b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/rest/ClientsService.kt
index 076d04a..c7e048f 100644
--- a/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/rest/ClientsService.kt
+++ b/eth-crawler/src/main/kotlin/org/apache/tuweni/eth/crawler/rest/ClientsService.kt
@@ -23,6 +23,7 @@ import org.apache.tuweni.eth.crawler.RelationalPeerRepository
 import javax.servlet.ServletContext
 import javax.ws.rs.GET
 import javax.ws.rs.Path
+import javax.ws.rs.PathParam
 import javax.ws.rs.Produces
 import javax.ws.rs.core.MediaType
 
@@ -51,12 +52,10 @@ class ClientsService {
     return result
   }
 
-  @GET
-  @Produces(MediaType.APPLICATION_JSON)
-  @Path("london/stats")
-  fun getLondonStats(): String {
+  @Path("{upgrade}/stats")
+  fun getClientStats(@PathParam("upgrade") upgrade: String): String {
     val repo = context!!.getAttribute("repo") as RelationalPeerRepository
-    val peers = repo.getLondonStats()
+    val peers = repo.getUpgradeStats()[upgrade]
     val result = mapper.writeValueAsString(peers)
     return result
   }

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org