Remove notifier module duplication (#10051)

This commit is contained in:
Michael Vines 2020-05-14 17:32:08 -07:00 committed by GitHub
parent 40b7c11262
commit 9ef9969d29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 114 additions and 160 deletions

13
Cargo.lock generated
View File

@ -4569,6 +4569,15 @@ dependencies = [
"solana-sdk", "solana-sdk",
] ]
[[package]]
name = "solana-notifier"
version = "1.2.0"
dependencies = [
"log 0.4.8",
"reqwest",
"serde_json",
]
[[package]] [[package]]
name = "solana-ownable" name = "solana-ownable"
version = "1.2.0" version = "1.2.0"
@ -4618,6 +4627,7 @@ dependencies = [
"solana-logger", "solana-logger",
"solana-metrics", "solana-metrics",
"solana-net-utils", "solana-net-utils",
"solana-notifier",
"solana-sdk", "solana-sdk",
"solana-stake-program", "solana-stake-program",
"tar", "tar",
@ -4993,14 +5003,13 @@ dependencies = [
"clap", "clap",
"humantime 2.0.0", "humantime 2.0.0",
"log 0.4.8", "log 0.4.8",
"reqwest",
"serde_json",
"solana-clap-utils", "solana-clap-utils",
"solana-cli", "solana-cli",
"solana-cli-config", "solana-cli-config",
"solana-client", "solana-client",
"solana-logger", "solana-logger",
"solana-metrics", "solana-metrics",
"solana-notifier",
"solana-sdk", "solana-sdk",
"solana-transaction-status", "solana-transaction-status",
"solana-version", "solana-version",

View File

@ -31,6 +31,7 @@ members = [
"measure", "measure",
"metrics", "metrics",
"net-shaper", "net-shaper",
"notifier",
"programs/bpf_loader", "programs/bpf_loader",
"programs/budget", "programs/budget",
"programs/btc_spv", "programs/btc_spv",

2
notifier/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
/farf/

20
notifier/Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "solana-notifier"
version = "1.2.0"
description = "Solana Notifier"
authors = ["Solana Maintainers <maintainers@solana.com>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
edition = "2018"
[dependencies]
log = "0.4.8"
reqwest = { version = "0.10.4", default-features = false, features = ["blocking", "rustls-tls", "json"] }
serde_json = "1.0"
[lib]
name = "solana_notifier"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -1,7 +1,26 @@
/// To activate Slack, Discord and/or Telegram notifications, define these environment variables
/// before using the `Notifier`
/// ```bash
/// export SLACK_WEBHOOK=...
/// export DISCORD_WEBHOOK=...
/// ```
///
/// Telegram requires the following two variables:
/// ```bash
/// export TELEGRAM_BOT_TOKEN=...
/// export TELEGRAM_CHAT_ID=...
/// ```
///
/// To receive a Twilio SMS notification on failure, having a Twilio account,
/// and a sending number owned by that account,
/// define environment variable before running `solana-watchtower`:
/// ```bash
/// export TWILIO_CONFIG='ACCOUNT=<account>,TOKEN=<securityToken>,TO=<receivingNumber>,FROM=<sendingNumber>'
/// ```
use log::*; use log::*;
use reqwest::blocking::Client; use reqwest::{blocking::Client, StatusCode};
use serde_json::json; use serde_json::json;
use std::env; use std::{env, thread::sleep, time::Duration};
struct TelegramWebHook { struct TelegramWebHook {
bot_token: String, bot_token: String,
@ -65,11 +84,11 @@ pub struct Notifier {
} }
impl Notifier { impl Notifier {
pub fn new() -> Self { pub fn default() -> Self {
Self::new_with_env_prefix("") Self::new("")
} }
pub fn new_with_env_prefix(env_prefix: &str) -> Self { pub fn new(env_prefix: &str) -> Self {
info!("Initializing {}Notifier", env_prefix); info!("Initializing {}Notifier", env_prefix);
let discord_webhook = env::var(format!("{}DISCORD_WEBHOOK", env_prefix)) let discord_webhook = env::var(format!("{}DISCORD_WEBHOOK", env_prefix))
@ -107,9 +126,30 @@ impl Notifier {
pub fn send(&self, msg: &str) { pub fn send(&self, msg: &str) {
if let Some(webhook) = &self.discord_webhook { if let Some(webhook) = &self.discord_webhook {
let data = json!({ "content": msg }); for line in msg.split('\n') {
if let Err(err) = self.client.post(webhook).json(&data).send() { // Discord rate limiting is aggressive, limit to 1 message a second
warn!("Failed to send Discord message: {:?}", err); sleep(Duration::from_millis(1000));
info!("Sending {}", line);
let data = json!({ "content": line });
loop {
let response = self.client.post(webhook).json(&data).send();
if let Err(err) = response {
warn!("Failed to send Discord message: \"{}\": {:?}", line, err);
break;
} else if let Ok(response) = response {
info!("response status: {}", response.status());
if response.status() == StatusCode::TOO_MANY_REQUESTS {
warn!("rate limited!...");
warn!("response text: {:?}", response.text());
sleep(Duration::from_secs(2));
} else {
break;
}
}
}
} }
} }

View File

@ -21,6 +21,7 @@ solana-client = { path = "../client", version = "1.2.0" }
solana-logger = { path = "../logger", version = "1.2.0" } solana-logger = { path = "../logger", version = "1.2.0" }
solana-metrics = { path = "../metrics", version = "1.2.0" } solana-metrics = { path = "../metrics", version = "1.2.0" }
solana-net-utils = { path = "../net-utils", version = "1.2.0" } solana-net-utils = { path = "../net-utils", version = "1.2.0" }
solana-notifier = { path = "../notifier", version = "1.2.0" }
solana-sdk = { path = "../sdk", version = "1.2.0" } solana-sdk = { path = "../sdk", version = "1.2.0" }
solana-stake-program = { path = "../programs/stake", version = "1.2.0" } solana-stake-program = { path = "../programs/stake", version = "1.2.0" }
tar = "0.4.26" tar = "0.4.26"

View File

@ -1,6 +1,5 @@
//! Ramp up TPS for Tour de SOL until all validators drop out //! Ramp up TPS for Tour de SOL until all validators drop out
mod notifier;
mod results; mod results;
mod stake; mod stake;
mod tps; mod tps;
@ -54,7 +53,7 @@ fn gift_for_round(tps_round: u32, initial_balance: u64) -> u64 {
fn main() { fn main() {
solana_logger::setup_with_default("solana=debug"); solana_logger::setup_with_default("solana=debug");
solana_metrics::set_panic_hook("ramp-tps"); solana_metrics::set_panic_hook("ramp-tps");
let mut notifier = notifier::Notifier::new(); let mut notifier = solana_notifier::Notifier::default();
let matches = App::new(crate_name!()) let matches = App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
@ -199,7 +198,7 @@ fn main() {
let _ = fs::remove_dir_all(&tmp_ledger_path); let _ = fs::remove_dir_all(&tmp_ledger_path);
fs::create_dir_all(&tmp_ledger_path).expect("failed to create temp ledger path"); fs::create_dir_all(&tmp_ledger_path).expect("failed to create temp ledger path");
notifier.notify("Hi!"); notifier.send("Hi!");
datapoint_info!("ramp-tps", ("event", "boot", String),); datapoint_info!("ramp-tps", ("event", "boot", String),);
let entrypoint_str = matches.value_of("entrypoint").unwrap(); let entrypoint_str = matches.value_of("entrypoint").unwrap();
@ -219,7 +218,7 @@ fn main() {
debug!("First normal slot: {}", first_normal_slot); debug!("First normal slot: {}", first_normal_slot);
let sleep_slots = first_normal_slot.saturating_sub(current_slot); let sleep_slots = first_normal_slot.saturating_sub(current_slot);
if sleep_slots > 0 { if sleep_slots > 0 {
notifier.notify(&format!( notifier.send(&format!(
"Waiting for warm-up epochs to complete (epoch {})", "Waiting for warm-up epochs to complete (epoch {})",
epoch_schedule.first_normal_epoch epoch_schedule.first_normal_epoch
)); ));
@ -291,7 +290,7 @@ fn main() {
let mut tps_sampler = tps::Sampler::new(&entrypoint_addr); let mut tps_sampler = tps::Sampler::new(&entrypoint_addr);
loop { loop {
notifier.notify(&format!("Round {}!", tps_round)); notifier.send(&format!("Round {}!", tps_round));
let tx_count = tx_count_for_round(tps_round, tx_count_baseline, tx_count_increment); let tx_count = tx_count_for_round(tps_round, tx_count_baseline, tx_count_increment);
datapoint_info!( datapoint_info!(
"ramp-tps", "ramp-tps",
@ -328,7 +327,7 @@ fn main() {
("validators", starting_validators.len(), i64) ("validators", starting_validators.len(), i64)
); );
notifier.buffer(format!( notifier.send(&format!(
"There are {} validators present:", "There are {} validators present:",
starting_validators.len() starting_validators.len()
)); ));
@ -338,11 +337,10 @@ fn main() {
.map(|node_pubkey| format!("* {}", pubkey_to_keybase(&node_pubkey))) .map(|node_pubkey| format!("* {}", pubkey_to_keybase(&node_pubkey)))
.collect(); .collect();
validators.sort(); validators.sort();
notifier.buffer_vec(validators); notifier.send(&validators.join("\n"));
notifier.flush();
let client_tx_count = tx_count / NUM_BENCH_CLIENTS as u64; let client_tx_count = tx_count / NUM_BENCH_CLIENTS as u64;
notifier.notify(&format!( notifier.send(&format!(
"Starting transactions for {} minutes (batch size={})", "Starting transactions for {} minutes (batch size={})",
round_minutes, tx_count, round_minutes, tx_count,
)); ));
@ -393,7 +391,7 @@ fn main() {
("round", tps_round, i64), ("round", tps_round, i64),
); );
notifier.notify("Transactions stopped"); notifier.send("Transactions stopped");
tps_sampler.report_results(&notifier); tps_sampler.report_results(&notifier);
let remaining_validators = voters::fetch_active_validators(&rpc_client); let remaining_validators = voters::fetch_active_validators(&rpc_client);

View File

@ -1,92 +0,0 @@
use log::*;
use reqwest::{blocking::Client, StatusCode};
use serde_json::json;
use std::{env, thread::sleep, time::Duration};
/// For each notification
/// 1) Log an info level message
/// 2) Notify Slack channel if Slack is configured
/// 3) Notify Discord channel if Discord is configured
pub struct Notifier {
buffer: Vec<String>,
client: Client,
discord_webhook: Option<String>,
slack_webhook: Option<String>,
}
impl Notifier {
pub fn new() -> Self {
let discord_webhook = env::var("DISCORD_WEBHOOK")
.map_err(|_| {
warn!("Discord notifications disabled");
})
.ok();
let slack_webhook = env::var("SLACK_WEBHOOK")
.map_err(|_| {
warn!("Slack notifications disabled");
})
.ok();
Notifier {
buffer: Vec::new(),
client: Client::new(),
discord_webhook,
slack_webhook,
}
}
fn send(&self, msg: &str) {
if let Some(webhook) = &self.discord_webhook {
for line in msg.split('\n') {
// Discord rate limiting is aggressive, limit to 1 message a second to keep
// it from getting mad at us...
sleep(Duration::from_millis(1000));
info!("Sending {}", line);
let data = json!({ "content": line });
loop {
let response = self.client.post(webhook).json(&data).send();
if let Err(err) = response {
warn!("Failed to send Discord message: \"{}\": {:?}", line, err);
break;
} else if let Ok(response) = response {
info!("response status: {}", response.status());
if response.status() == StatusCode::TOO_MANY_REQUESTS {
warn!("rate limited!...");
warn!("response text: {:?}", response.text());
std::thread::sleep(Duration::from_secs(2));
} else {
break;
}
}
}
}
}
if let Some(webhook) = &self.slack_webhook {
let data = json!({ "text": msg });
if let Err(err) = self.client.post(webhook).json(&data).send() {
warn!("Failed to send Slack message: {:?}", err);
}
}
}
pub fn buffer(&mut self, msg: String) {
self.buffer.push(msg);
}
pub fn buffer_vec(&mut self, mut msgs: Vec<String>) {
self.buffer.append(&mut msgs);
}
pub fn flush(&mut self) {
self.notify(&self.buffer.join("\n"));
self.buffer.clear();
}
pub fn notify(&self, msg: &str) {
info!("{}", msg);
self.send(msg);
}
}

View File

@ -1,4 +1,4 @@
use crate::{notifier, utils}; use crate::utils;
use log::*; use log::*;
use solana_client::{rpc_client::RpcClient, rpc_response::RpcEpochInfo}; use solana_client::{rpc_client::RpcClient, rpc_response::RpcEpochInfo};
use solana_sdk::{ use solana_sdk::{
@ -64,11 +64,11 @@ pub fn wait_for_warm_up(
rpc_client: &RpcClient, rpc_client: &RpcClient,
stake_config: &StakeConfig, stake_config: &StakeConfig,
genesis_config: &GenesisConfig, genesis_config: &GenesisConfig,
notifier: &notifier::Notifier, notifier: &solana_notifier::Notifier,
) { ) {
// Sleep until activation_epoch has finished // Sleep until activation_epoch has finished
if epoch_info.epoch <= activation_epoch { if epoch_info.epoch <= activation_epoch {
notifier.notify(&format!( notifier.send(&format!(
"Waiting until epoch {} is finished...", "Waiting until epoch {} is finished...",
activation_epoch activation_epoch
)); ));
@ -105,7 +105,7 @@ pub fn wait_for_warm_up(
let warm_up_epochs = calculate_stake_warmup(stake_entry, stake_config); let warm_up_epochs = calculate_stake_warmup(stake_entry, stake_config);
let stake_warmed_up_epoch = latest_epoch + warm_up_epochs; let stake_warmed_up_epoch = latest_epoch + warm_up_epochs;
if stake_warmed_up_epoch > current_epoch { if stake_warmed_up_epoch > current_epoch {
notifier.notify(&format!( notifier.send(&format!(
"Waiting until epoch {} for stake to warmup (current epoch is {})...", "Waiting until epoch {} for stake to warmup (current epoch is {})...",
stake_warmed_up_epoch, current_epoch stake_warmed_up_epoch, current_epoch
)); ));

View File

@ -1,7 +1,7 @@
use crate::notifier::Notifier;
use log::*; use log::*;
use solana_client::perf_utils::{sample_txs, SampleStats}; use solana_client::perf_utils::{sample_txs, SampleStats};
use solana_client::thin_client::ThinClient; use solana_client::thin_client::ThinClient;
use solana_notifier::Notifier;
use solana_sdk::timing::duration_as_s; use solana_sdk::timing::duration_as_s;
use std::{ use std::{
net::SocketAddr, net::SocketAddr,
@ -64,7 +64,7 @@ impl Sampler {
pub fn report_results(&self, notifier: &Notifier) { pub fn report_results(&self, notifier: &Notifier) {
let SampleStats { tps, elapsed, txs } = self.maxes.read().unwrap()[0].1; let SampleStats { tps, elapsed, txs } = self.maxes.read().unwrap()[0].1;
let avg_tps = txs as f32 / duration_as_s(&elapsed); let avg_tps = txs as f32 / duration_as_s(&elapsed);
notifier.notify(&format!( notifier.send(&format!(
"Highest TPS: {:.0}, Average TPS: {:.0}", "Highest TPS: {:.0}, Average TPS: {:.0}",
tps, avg_tps tps, avg_tps
)); ));

View File

@ -1,8 +1,8 @@
use crate::notifier::Notifier;
use bzip2::bufread::BzDecoder; use bzip2::bufread::BzDecoder;
use log::*; use log::*;
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_net_utils::parse_host; use solana_net_utils::parse_host;
use solana_notifier::Notifier;
use solana_sdk::{ use solana_sdk::{
clock::{Epoch, Slot}, clock::{Epoch, Slot},
genesis_config::GenesisConfig, genesis_config::GenesisConfig,
@ -89,8 +89,8 @@ pub fn is_host(string: String) -> Result<(), String> {
Ok(()) Ok(())
} }
pub fn bail(notifier: &crate::notifier::Notifier, msg: &str) -> ! { pub fn bail(notifier: &Notifier, msg: &str) -> ! {
notifier.notify(msg); notifier.send(msg);
sleep(Duration::from_secs(30)); // Wait for notifications to send sleep(Duration::from_secs(30)); // Wait for notifications to send
std::process::exit(1); std::process::exit(1);
} }

View File

@ -1,7 +1,7 @@
use crate::notifier::Notifier;
use crate::utils; use crate::utils;
use log::*; use log::*;
use solana_client::{client_error::Result as ClientResult, rpc_client::RpcClient}; use solana_client::{client_error::Result as ClientResult, rpc_client::RpcClient};
use solana_notifier::Notifier;
use solana_sdk::{ use solana_sdk::{
clock::Slot, clock::Slot,
epoch_schedule::EpochSchedule, epoch_schedule::EpochSchedule,
@ -183,7 +183,7 @@ pub fn announce_results(
) { ) {
let buffer_records = |keys: Vec<&Pubkey>, notifier: &mut Notifier| { let buffer_records = |keys: Vec<&Pubkey>, notifier: &mut Notifier| {
if keys.is_empty() { if keys.is_empty() {
notifier.buffer("* None".to_string()); notifier.send("* None");
return; return;
} }
@ -199,7 +199,7 @@ pub fn announce_results(
} }
} }
validators.sort(); validators.sort();
notifier.buffer_vec(validators); notifier.send(&validators.join("\n"));
}; };
let healthy: Vec<_> = remaining_validators let healthy: Vec<_> = remaining_validators
@ -217,14 +217,12 @@ pub fn announce_results(
.filter(|k| !remaining_validators.contains_key(k)) .filter(|k| !remaining_validators.contains_key(k))
.collect(); .collect();
notifier.buffer("Healthy Validators:".to_string()); notifier.send("Healthy Validators:");
buffer_records(healthy, notifier); buffer_records(healthy, notifier);
notifier.buffer("Unhealthy Validators:".to_string()); notifier.send("Unhealthy Validators:");
buffer_records(unhealthy, notifier); buffer_records(unhealthy, notifier);
notifier.buffer("Inactive Validators:".to_string()); notifier.send("Inactive Validators:");
buffer_records(inactive, notifier); buffer_records(inactive, notifier);
notifier.flush();
} }
/// Award stake to the surviving validators by delegating stake to their vote account /// Award stake to the surviving validators by delegating stake to their vote account
@ -235,10 +233,12 @@ pub fn award_stake(
sol_gift: u64, sol_gift: u64,
notifier: &mut Notifier, notifier: &mut Notifier,
) { ) {
let mut buffer = vec![];
for (node_pubkey, vote_account_pubkey) in voters { for (node_pubkey, vote_account_pubkey) in voters {
info!("Delegate {} SOL to {}", sol_gift, node_pubkey); info!("Delegate {} SOL to {}", sol_gift, node_pubkey);
delegate_stake(rpc_client, faucet_keypair, vote_account_pubkey, sol_gift); delegate_stake(rpc_client, faucet_keypair, vote_account_pubkey, sol_gift);
notifier.buffer(format!("Delegated {} SOL to {}", sol_gift, node_pubkey)); buffer.push(format!("Delegated {} SOL to {}", sol_gift, node_pubkey));
} }
notifier.flush(); notifier.send(&buffer.join("\n"));
} }

View File

@ -12,14 +12,13 @@ homepage = "https://solana.com/"
clap = "2.33.1" clap = "2.33.1"
log = "0.4.8" log = "0.4.8"
humantime = "2.0.0" humantime = "2.0.0"
reqwest = { version = "0.10.4", default-features = false, features = ["blocking", "rustls-tls", "json"] }
serde_json = "1.0"
solana-clap-utils = { path = "../clap-utils", version = "1.2.0" } solana-clap-utils = { path = "../clap-utils", version = "1.2.0" }
solana-cli-config = { path = "../cli-config", version = "1.2.0" } solana-cli-config = { path = "../cli-config", version = "1.2.0" }
solana-cli = { path = "../cli", version = "1.2.0" } solana-cli = { path = "../cli", version = "1.2.0" }
solana-client = { path = "../client", version = "1.2.0" } solana-client = { path = "../client", version = "1.2.0" }
solana-logger = { path = "../logger", version = "1.2.0" } solana-logger = { path = "../logger", version = "1.2.0" }
solana-metrics = { path = "../metrics", version = "1.2.0" } solana-metrics = { path = "../metrics", version = "1.2.0" }
solana-notifier = { path = "../notifier", version = "1.2.0" }
solana-sdk = { path = "../sdk", version = "1.2.0" } solana-sdk = { path = "../sdk", version = "1.2.0" }
solana-transaction-status = { path = "../transaction-status", version = "1.2.0" } solana-transaction-status = { path = "../transaction-status", version = "1.2.0" }
solana-version = { path = "../version", version = "1.2.0" } solana-version = { path = "../version", version = "1.2.0" }

View File

@ -23,25 +23,3 @@ On failure this data point contains details about the specific test that failed
the following fields: the following fields:
* `test`: name of the sanity test that failed * `test`: name of the sanity test that failed
* `err`: exact sanity failure message * `err`: exact sanity failure message
### Sanity failure push notification
To receive a Slack, Discord and/or Telegram notification on sanity failure,
define environment variables before running `solana-watchtower`:
```
export SLACK_WEBHOOK=...
export DISCORD_WEBHOOK=...
```
Telegram requires the following two variables:
```
export TELEGRAM_BOT_TOKEN=...
export TELEGRAM_CHAT_ID=...
```
To receive a Twilio SMS notification on failure, having a Twilio account,
and a sending number owned by that account,
define environment variable before running `solana-watchtower`:
```
export TWILIO_CONFIG='ACCOUNT=<account>,TOKEN=<securityToken>,TO=<receivingNumber>,FROM=<sendingNumber>'
```

View File

@ -1,8 +1,5 @@
//! A command-line executable for monitoring the health of a cluster //! A command-line executable for monitoring the health of a cluster
mod notifier;
use crate::notifier::Notifier;
use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg}; use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg};
use log::*; use log::*;
use solana_clap_utils::{ use solana_clap_utils::{
@ -13,6 +10,7 @@ use solana_client::{
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_response::RpcVoteAccountStatus, client_error::Result as ClientResult, rpc_client::RpcClient, rpc_response::RpcVoteAccountStatus,
}; };
use solana_metrics::{datapoint_error, datapoint_info}; use solana_metrics::{datapoint_error, datapoint_info};
use solana_notifier::Notifier;
use solana_sdk::{ use solana_sdk::{
clock::Slot, hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize, clock::Slot, hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize,
pubkey::Pubkey, pubkey::Pubkey,
@ -232,7 +230,7 @@ fn load_blocks(
} }
fn transaction_monitor(rpc_client: RpcClient) { fn transaction_monitor(rpc_client: RpcClient) {
let notifier = Notifier::new_with_env_prefix("TRANSACTION_NOTIFIER_"); let notifier = Notifier::new("TRANSACTION_NOTIFIER_");
let mut start_slot = loop { let mut start_slot = loop {
match rpc_client.get_slot() { match rpc_client.get_slot() {
Ok(slot) => break slot, Ok(slot) => break slot,
@ -303,7 +301,7 @@ fn main() -> Result<(), Box<dyn error::Error>> {
}; };
let rpc_client = RpcClient::new(config.json_rpc_url.clone()); let rpc_client = RpcClient::new(config.json_rpc_url.clone());
let notifier = Notifier::new(); let notifier = Notifier::default();
let mut last_transaction_count = 0; let mut last_transaction_count = 0;
let mut last_recent_blockhash = Hash::default(); let mut last_recent_blockhash = Hash::default();
let mut last_notification_msg = "".into(); let mut last_notification_msg = "".into();