summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/peers.rs7
-rw-r--r--src/api/stats.rs4
-rw-r--r--src/api/stats/prometheus.rs35
-rw-r--r--src/main.rs21
-rw-r--r--src/state.rs2
-rw-r--r--src/swarm/members.rs89
6 files changed, 147 insertions, 11 deletions
diff --git a/src/api/peers.rs b/src/api/peers.rs
index 0305647..7caaaf3 100644
--- a/src/api/peers.rs
+++ b/src/api/peers.rs
@@ -48,6 +48,8 @@ impl From<Epoch> for EpochInfo {
#[serde(rename_all = "camelCase")]
pub(crate) struct PeerStatus {
public_key: String,
+ /// The name the peer gave itself in the member list, if any (not unique).
+ alias: Option<String>,
validated_swarm: Option<String>,
status: PeerLivenessStatus,
handshake_complete: bool,
@@ -87,6 +89,8 @@ pub(crate) struct ThroughputInfo {
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PeerListResponse {
+ /// Our own alias (`--alias`).
+ local_alias: Option<String>,
local_epoch: EpochInfo,
/// Number of heads of our document. Several heads are concurrent changes no later
/// change has merged yet.
@@ -125,6 +129,7 @@ pub(crate) async fn list_peers(
let now = Instant::now();
let local_epoch = state.local_epoch();
+ let mut aliases = crate::swarm::members::aliases(&state.doc.read().await.doc);
let mut peers = Vec::with_capacity(snapshot.len());
for (public_key, peer_state_mutex) in snapshot {
@@ -155,6 +160,7 @@ pub(crate) async fn list_peers(
};
peers.push(PeerStatus {
+ alias: aliases.remove(&public_key),
public_key,
validated_swarm: ps.standing.swarm_id().map(str::to_owned),
status,
@@ -178,6 +184,7 @@ pub(crate) async fn list_peers(
peers.sort_by(|a, b| a.public_key.cmp(&b.public_key));
let local_heads = state.doc.write().await.doc.get_heads().len();
Ok(Json(PeerListResponse {
+ local_alias: state.alias.clone(),
local_epoch: local_epoch.into(),
local_heads,
peers,
diff --git a/src/api/stats.rs b/src/api/stats.rs
index b39e054..bc925ff 100644
--- a/src/api/stats.rs
+++ b/src/api/stats.rs
@@ -19,6 +19,8 @@ use crate::transport::traffic::PeerTraffic;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct StatsResponse {
+ public_key: String,
+ alias: Option<String>,
uptime_secs: u64,
ready: bool,
epoch: EpochInfo,
@@ -147,6 +149,8 @@ async fn collect(state: &AppState) -> StatsResponse {
}
StatsResponse {
+ public_key: state.local_id().to_owned(),
+ alias: state.alias.clone(),
uptime_secs: node.uptime.as_secs(),
ready: state.is_ready().await,
epoch: state.local_epoch().into(),
diff --git a/src/api/stats/prometheus.rs b/src/api/stats/prometheus.rs
index 16869f7..d29207c 100644
--- a/src/api/stats/prometheus.rs
+++ b/src/api/stats/prometheus.rs
@@ -68,6 +68,7 @@ fn flag(value: bool) -> f64 {
/// What we show per peer.
struct Peer {
key: String,
+ alias: Option<String>,
neighbour: bool,
dormant: bool,
last_seen_secs: Option<f64>,
@@ -86,6 +87,7 @@ async fn peers(state: &AppState) -> Vec<Peer> {
.iter()
.map(|(key, peer_state)| (key.clone(), peer_state.clone()))
.collect();
+ let mut aliases = crate::swarm::members::aliases(&state.doc.read().await.doc);
let mut peers = Vec::with_capacity(snapshot.len());
for (key, peer_state) in snapshot {
let neighbour = crate::swarm::topology::is_neighbour(state, &key);
@@ -111,6 +113,7 @@ async fn peers(state: &AppState) -> Vec<Peer> {
incidents: peer_state.incidents,
rtt_secs,
throughput,
+ alias: aliases.remove(&key),
key,
});
}
@@ -139,6 +142,22 @@ fn traffic_series(traffic: &PeerTraffic) -> Vec<(&'static str, &'static str, Cou
pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
let mut out = Out::default();
+ // Names for joining (`* on(peer) group_left(alias) continuum_peer_info`), kept out
+ // of the other series so a rename doesn't start new ones.
+ out.family(
+ "info",
+ "gauge",
+ "This node's public key and alias (always 1).",
+ );
+ out.sample(
+ "info",
+ &[
+ ("public_key", &stats.public_key),
+ ("alias", stats.alias.as_deref().unwrap_or("")),
+ ],
+ 1.0,
+ );
+
out.single(
"uptime_seconds",
"gauge",
@@ -342,6 +361,22 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String {
let peers = peers(state).await;
+ out.family(
+ "peer_info",
+ "gauge",
+ "A peer's alias from the member list (always 1).",
+ );
+ for peer in &peers {
+ out.sample(
+ "peer_info",
+ &[
+ ("peer", &peer.key),
+ ("alias", peer.alias.as_deref().unwrap_or("")),
+ ],
+ 1.0,
+ );
+ }
+
out.family("peer_neighbour", "gauge", "1 if the peer is a neighbour.");
for peer in &peers {
out.sample(
diff --git a/src/main.rs b/src/main.rs
index 7261d6a..65134d8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -97,12 +97,32 @@ struct Args {
#[arg(long, value_name = "COUNT", default_value_t = 12)]
neighbours: usize,
+ /// A name for this node, shown next to its public key (/peers, metrics, logs).
+ /// Published in the swarm's member list; not unique, never an identity.
+ #[arg(long, value_name = "NAME", value_parser = parse_alias)]
+ alias: Option<String>,
+
/// Test hook: start on a non-genesis epoch of this generation (with a fixed dummy
/// id), to check that nodes on different epochs refuse to sync.
#[arg(long, hide = true, requires = "debug_api", value_name = "GENERATION")]
debug_epoch: Option<u64>,
}
+/// Up to 64 characters, no control characters, not blank.
+fn parse_alias(value: &str) -> Result<String, String> {
+ let alias = value.trim();
+ if alias.is_empty() {
+ return Err("must not be blank".to_owned());
+ }
+ if alias.chars().count() > 64 {
+ return Err("must be at most 64 characters".to_owned());
+ }
+ if alias.chars().any(char::is_control) {
+ return Err("must not contain control characters".to_owned());
+ }
+ Ok(alias.to_owned())
+}
+
#[tokio::main]
async fn main() -> Result<()> {
logging::init();
@@ -201,6 +221,7 @@ async fn main() -> Result<()> {
swarm_key,
run_id: uuid::Uuid::new_v4().into_bytes(),
neighbours_k: args.neighbours.max(2),
+ alias: args.alias,
neighbours: Default::default(),
pulling: Default::default(),
rtt: Default::default(),
diff --git a/src/state.rs b/src/state.rs
index d8f0bbf..7517b72 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -593,6 +593,8 @@ pub(crate) struct AppState {
/// Target number of neighbours (`--neighbours`); up to this many + 1 members the
/// swarm is a full mesh.
pub(crate) neighbours_k: usize,
+ /// Our alias (`--alias`), published in the member list (`swarm::members`).
+ pub(crate) alias: Option<String>,
/// Our own connectivity record and stability tier (`swarm::stability`).
pub(crate) stability: Arc<std::sync::Mutex<crate::swarm::stability::Stability>>,
/// Round-trip times to peers (`swarm::rtt`).
diff --git a/src/swarm/members.rs b/src/swarm/members.rs
index e64dd20..e02dba4 100644
--- a/src/swarm/members.rs
+++ b/src/swarm/members.rs
@@ -7,6 +7,12 @@
//! while it is alive. An operator's forget (`DELETE /peers/{pk}`) removes the entry
//! together with writing the forget record; a forgotten node that turns out to be alive
//! cancels the record and registers again.
+//!
+//! Besides its tier, an entry holds the alias a node gives itself (`--alias`), a name
+//! for people reading /peers, metrics and logs. Not unique and never an identity: the
+//! public key stays that.
+
+use std::collections::HashMap;
use automerge::AutoCommit;
use serde_json::{Value as JsonValue, json};
@@ -69,27 +75,88 @@ pub(crate) fn set_tier(doc: &mut AutoCommit, pk: &str, tier: Tier) -> Result<(),
store::write_path(doc, &tier_path(pk), &json!(tier.as_str()))
}
-/// Registers this node if it is ready and its entry is missing.
+fn alias_path(pk: &str) -> Vec<String> {
+ let mut path = member_path(pk);
+ path.push("alias".to_owned());
+ path
+}
+
+/// `pk`'s alias, if it gave itself one.
+pub(crate) fn alias(doc: &AutoCommit, pk: &str) -> Option<String> {
+ match store::read_path(doc, &alias_path(pk)) {
+ Ok(Some(JsonValue::String(alias))) => Some(alias),
+ _ => None,
+ }
+}
+
+/// The aliases of all members that have one.
+pub(crate) fn aliases(doc: &AutoCommit) -> HashMap<String, String> {
+ members(doc)
+ .into_iter()
+ .filter_map(|pk| alias(doc, &pk).map(|alias| (pk, alias)))
+ .collect()
+}
+
+fn set_alias(doc: &mut AutoCommit, pk: &str, alias: Option<&str>) -> Result<(), StoreError> {
+ match alias {
+ Some(alias) => store::write_path(doc, &alias_path(pk), &json!(alias)),
+ None => match store::delete_path(doc, &alias_path(pk), false) {
+ Err(StoreError::NotFound) => Ok(()),
+ result => result,
+ },
+ }
+}
+
+/// Registers this node if it is ready and its entry is missing, and keeps its alias
+/// as configured (set, changed or removed after a restart with another `--alias`).
pub(crate) async fn ensure_registered(state: &AppState) {
if !state.is_ready().await {
return;
}
- let own = member_path(state.local_id());
- let registered = store::read_path(&state.doc.read().await.doc, &own)
- .ok()
- .flatten()
- .is_some();
- if registered {
+ let local = state.local_id().to_owned();
+ let own = member_path(&local);
+ let (registered, recorded_alias) = {
+ let replica = state.doc.read().await;
+ let registered = store::read_path(&replica.doc, &own)
+ .ok()
+ .flatten()
+ .is_some();
+ (registered, alias(&replica.doc, &local))
+ };
+ let wanted = state.alias.clone();
+
+ if !registered {
+ let mut entry = json!({ "since": unix_millis() });
+ if let Some(alias) = &wanted {
+ entry["alias"] = json!(alias);
+ }
+ match state
+ .mutate_doc(move |doc| store::write_path(doc, &own, &entry))
+ .await
+ {
+ Ok(()) => info!(
+ alias = wanted.as_deref(),
+ "Registered in the swarm's member list"
+ ),
+ Err(error) => warn!(error = %error, "Failed to register in the member list"),
+ }
return;
}
- let entry = json!({ "since": unix_millis() });
+ if recorded_alias == wanted {
+ return;
+ }
+ let alias = wanted.clone();
match state
- .mutate_doc(move |doc| store::write_path(doc, &own, &entry))
+ .mutate_doc(move |doc| set_alias(doc, &local, alias.as_deref()))
.await
{
- Ok(()) => info!("Registered in the swarm's member list"),
- Err(error) => warn!(error = %error, "Failed to register in the member list"),
+ Ok(()) => info!(
+ alias = wanted.as_deref(),
+ previous = recorded_alias.as_deref(),
+ "Alias updated in the member list"
+ ),
+ Err(error) => warn!(error = %error, "Failed to record our alias"),
}
}