diff options
-rw-r--r-- | notes/rust_libraries.txt | 22 | ||||
-rw-r--r-- | rust/env.example | 3 | ||||
-rw-r--r-- | rust/src/bin/fatcatd.rs | 29 | ||||
-rw-r--r-- | rust/src/endpoints.rs | 55 | ||||
-rw-r--r-- | rust/src/server.rs | 12 |
5 files changed, 101 insertions, 20 deletions
diff --git a/notes/rust_libraries.txt b/notes/rust_libraries.txt index 7e6f33eb..d5c8c18a 100644 --- a/notes/rust_libraries.txt +++ b/notes/rust_libraries.txt @@ -8,6 +8,28 @@ libs: - cadence (emits statsd) - frank_jwt and JWT for (simple?) auth +metrics: +- best would be something with a configurable back-end, like 'log' for logging, + but supporing tags/labels. the prometheus model probably makes most sense by + default (really nice to be able to grab metrics with 'curl'/browser for + individual instances), but statsd seems to be what we run in production. not + spewing out lots of UDP by default seems like a good idea. +- dipstick: has all the good features, and popular, but code quality has smells + ("a32dlkjhw"-style commit messages), and API doesn't seem very clean. Also + prometheus stuff not actually implemented +- cadence: seems stable, somewhat popular, clean API. statsd-only for now, but + has custom backends that could be hooked on to. *super* few dependencies, + nice. +- tic: many deps; doesn't seem stable or under development +- rust-prometheus: developed by pingcap (large company). has push and pull + features. medum-sized deps; has feature flags + +A nice feature of a statsd solution is that collectd is usually running +locally (on linux dev, or in production), and metrics can be sent there by +default, like journald for logging. + +Seems like a decision between cadence (statsd) and rust-prometheus. + similar: - https://github.com/DavidBM/templic-backend - https://github.com/alexanderbanks/rust-api diff --git a/rust/env.example b/rust/env.example index 482f2400..bee653c2 100644 --- a/rust/env.example +++ b/rust/env.example @@ -5,3 +5,6 @@ AUTH_KEY_IDENT="20190101-dev-dummy-key" AUTH_SECRET_KEY="5555555555555555555555555555555555555555xms=" AUTH_ALT_KEYS="20181220-dev:6666666666666666666666666666666666666666xms=,20181210-dev:7777777777777777777777777777777777777777xms=" #SENTRY_DSN= +# should be localhost IP, not 'localhost' +#FATCAT_STATSD_HOST="127.0.0.1" +#FATCAT_STATSD_PORT="8125" diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs index 388c6e61..df5c390e 100644 --- a/rust/src/bin/fatcatd.rs +++ b/rust/src/bin/fatcatd.rs @@ -15,6 +15,9 @@ use iron_slog::{DefaultLogFormatter, LoggerMiddleware}; use sentry::integrations::panic; use slog::{Drain, Logger}; use std::env; +use cadence::{StatsdClient, QueuingMetricSink, BufferedUdpMetricSink}; +use cadence::prelude::*; +use std::net::UdpSocket; // HTTP header middleware header! { (XClacksOverhead, "X-Clacks-Overhead") => [String] } @@ -29,6 +32,7 @@ impl AfterMiddleware for XClacksOverheadMiddleware { } } + /// Create custom server, wire it to the autogenerated router, /// and pass it to the web server. fn main() -> Result<()> { @@ -60,7 +64,30 @@ fn main() -> Result<()> { None }; - let server = create_server()?; + let mut server = create_server()?; + + // metrics reporting + match env::var("FATCAT_STATSD_HOST") { + Err(_) => { + info!(logger, "no metrics recipient configured"); + }, + Ok(host) => { + let port: u16 = match env::var("FATCAT_STATSD_PORT") { + Err(_) => cadence::DEFAULT_PORT, + Ok(var) => var.parse::<u16>()?, // "expect FATCAT_STATSD_PORT to be null or an integer + }; + let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); + socket.set_nonblocking(true).unwrap(); + let udp_sink = BufferedUdpMetricSink::from((host.as_ref(), port), socket).unwrap(); + let queuing_sink = QueuingMetricSink::from(udp_sink); + info!(logger, "sending statsd metrics via UDP to: {}:{}", host, port); + server.metrics = StatsdClient::from_sink("fatcat.api", queuing_sink); + //server.metrics = StatsdClient::from_udp_host("fatcat.api", (host.as_ref(), port))?; + server.metrics.incr("restart").unwrap(); + } + }; + info!(logger, "{:#?}", server.metrics); + info!( logger, "using primary auth key: {}", server.auth_confectionary.identifier, diff --git a/rust/src/endpoints.rs b/rust/src/endpoints.rs index 1bed3c56..027e25bc 100644 --- a/rust/src/endpoints.rs +++ b/rust/src/endpoints.rs @@ -22,6 +22,7 @@ use futures::{self, Future}; use sentry::integrations::failure::capture_fail; use std::str::FromStr; use uuid::{self, Uuid}; +use cadence::prelude::*; // This makes response matching below *much* more terse use crate::errors::FatcatError::*; @@ -121,8 +122,10 @@ macro_rules! wrap_entity_handlers { edit_context.check(&conn)?; entity.db_create(&conn, &edit_context)?.into_model() }).map_err(|e| FatcatError::from(e)) { - Ok(edit) => - $post_resp::CreatedEntity(edit), + Ok(edit) => { + self.metrics.incr("entities.created").ok(); + $post_resp::CreatedEntity(edit) + }, Err(fe) => generic_auth_err_responses!(fe, $post_resp), }; Box::new(futures::done(Ok(ret))) @@ -146,8 +149,14 @@ macro_rules! wrap_entity_handlers { } else { None }; self.$post_batch_handler(&conn, entity_list, autoaccept.unwrap_or(false), auth_context.editor_id, editgroup_id) }).map_err(|e| FatcatError::from(e)) { - Ok(edit) => - $post_batch_resp::CreatedEntities(edit), + Ok(edits) => { + self.metrics.count("entities.created", edits.len() as i64).ok(); + if let Some(true) = autoaccept { + self.metrics.incr("editgroup.created").ok(); + self.metrics.incr("editgroup.accepted").ok(); + }; + $post_batch_resp::CreatedEntities(edits) + }, Err(fe) => generic_auth_err_responses!(fe, $post_batch_resp), }; Box::new(futures::done(Ok(ret))) @@ -171,8 +180,10 @@ macro_rules! wrap_entity_handlers { edit_context.check(&conn)?; entity.db_update(&conn, &edit_context, entity_id)?.into_model() }).map_err(|e| FatcatError::from(e)) { - Ok(edit) => - $update_resp::UpdatedEntity(edit), + Ok(edit) => { + self.metrics.incr("entities.updated").ok(); + $update_resp::UpdatedEntity(edit) + }, Err(fe) => generic_auth_err_responses!(fe, $update_resp), }; Box::new(futures::done(Ok(ret))) @@ -195,8 +206,10 @@ macro_rules! wrap_entity_handlers { edit_context.check(&conn)?; $model::db_delete(&conn, &edit_context, entity_id)?.into_model() }).map_err(|e| FatcatError::from(e)) { - Ok(edit) => - $delete_resp::DeletedEntity(edit), + Ok(edit) => { + self.metrics.incr("entities.deleted").ok(); + $delete_resp::DeletedEntity(edit) + }, Err(fe) => generic_auth_err_responses!(fe, $delete_resp), }; Box::new(futures::done(Ok(ret))) @@ -745,10 +758,13 @@ impl Api for Server { }) .map_err(|e| FatcatError::from(e)) { - Ok(()) => AcceptEditgroupResponse::MergedSuccessfully(Success { - success: true, - message: "horray!".to_string(), - }), + Ok(()) => { + self.metrics.incr("editgroup.accepted").ok(); + AcceptEditgroupResponse::MergedSuccessfully(Success { + success: true, + message: "horray!".to_string(), + }) + }, Err(fe) => generic_auth_err_responses!(fe, AcceptEditgroupResponse), }; Box::new(futures::done(Ok(ret))) @@ -804,7 +820,10 @@ impl Api for Server { }) .map_err(|e| FatcatError::from(e)) { - Ok(eg) => CreateEditgroupResponse::SuccessfullyCreated(eg), + Ok(eg) => { + self.metrics.incr("editgroup.created").ok(); + CreateEditgroupResponse::SuccessfullyCreated(eg) + }, Err(fe) => match fe { NotFound(_, _) => CreateEditgroupResponse::NotFound(fe.into()), DatabaseError(_) | InternalError(_) => { @@ -884,8 +903,14 @@ impl Api for Server { }) .map_err(|e: Error| FatcatError::from(e)) { - Ok((result, true)) => AuthOidcResponse::Created(result), - Ok((result, false)) => AuthOidcResponse::Found(result), + Ok((result, true)) => { + self.metrics.incr("account.signup").ok(); + AuthOidcResponse::Created(result) + }, + Ok((result, false)) => { + self.metrics.incr("account.login").ok(); + AuthOidcResponse::Found(result) + }, Err(fe) => match fe { DatabaseError(_) | InternalError(_) => { error!("{}", fe); diff --git a/rust/src/server.rs b/rust/src/server.rs index 6b389a97..66b215fc 100644 --- a/rust/src/server.rs +++ b/rust/src/server.rs @@ -8,6 +8,7 @@ use diesel::pg::PgConnection; use diesel::r2d2::ConnectionManager; use dotenv::dotenv; use std::env; +use cadence::{StatsdClient, NopMetricSink}; #[cfg(feature = "postgres")] embed_migrations!("../migrations/"); @@ -32,6 +33,7 @@ pub fn database_worker_pool() -> Result<ConnectionPool> { pub struct Server { pub db_pool: ConnectionPool, pub auth_confectionary: AuthConfectionary, + pub metrics: StatsdClient, } /// Instantiate a new API server with a pooled database connection @@ -39,13 +41,15 @@ pub fn create_server() -> Result<Server> { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let manager = ConnectionManager::<PgConnection>::new(database_url); - let pool = diesel::r2d2::Pool::builder() + let db_pool = diesel::r2d2::Pool::builder() .build(manager) .expect("Failed to create database pool."); - let confectionary = env_confectionary()?; + let auth_confectionary = env_confectionary()?; + let metrics = StatsdClient::from_sink("blackhole", NopMetricSink); Ok(Server { - db_pool: pool, - auth_confectionary: confectionary, + db_pool, + auth_confectionary, + metrics, }) } |