├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples └── client.rs └── src ├── client.rs └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | env: 11 | RUSTFLAGS: -Dwarnings 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install Rust Toolchain 21 | run: rustup toolchain install stable --profile minimal --component clippy --component rustfmt --no-self-update 22 | 23 | - uses: swatinem/rust-cache@v2 24 | 25 | - name: Run Rustfmt 26 | run: cargo fmt --all -- --check 27 | 28 | - name: Run Clippy 29 | run: cargo clippy --workspace --all-features --tests -- -D clippy::all 30 | 31 | test: 32 | name: Test (ubuntu) 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # pin@v1 39 | with: 40 | toolchain: stable 41 | profile: minimal 42 | override: true 43 | 44 | - uses: swatinem/rust-cache@81d053bdb0871dcd3f10763c8cc60d0adc41762b # pin@v1 45 | with: 46 | key: ${{ github.job }} 47 | 48 | - name: Run Cargo Tests 49 | uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # pin@v1 50 | with: 51 | command: test 52 | args: --all 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "statsd" 3 | version = "0.16.1" 4 | description = "A basic statsd client for rust." 5 | homepage = "https://github.com/markstory/rust-statsd" 6 | readme = "README.md" 7 | license = "MIT" 8 | authors = ["Mark Story "] 9 | include = [ 10 | "**/*.rs", 11 | "Cargo.toml", 12 | "README.md", 13 | "LICENSE.txt", 14 | ] 15 | edition = "2018" 16 | 17 | [dependencies] 18 | rand = "0.8" 19 | 20 | [dev-dependencies] 21 | itertools = "0.10" 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015, Mark Story 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Statsd 2 | 3 | [![CI status](https://github.com/markstory/rust-statsd/actions/workflows/ci.yml/badge.svg)](https://github.com/markstory/rust-statsd/actions/workflows/ci.yml) 4 | 5 | A StatsD client implementation of statsd in rust. 6 | 7 | ## Using the client library 8 | 9 | Add the `statsd` package as a dependency in your `Cargo.toml` file: 10 | 11 | ```toml 12 | [dependencies] 13 | statsd = "^0.16" 14 | ``` 15 | 16 | You need rustc >= 1.31.0 for statsd to work. 17 | 18 | You can then get a client instance and start tracking metrics: 19 | 20 | ```rust 21 | // Load the crate 22 | extern crate statsd; 23 | 24 | // Import the client object. 25 | use statsd::Client; 26 | 27 | // Get a client with the prefix of `myapp`. The host should be the 28 | // IP:port of your statsd daemon. 29 | let client = Client::new("127.0.0.1:8125", "myapp").unwrap(); 30 | ``` 31 | 32 | ## Tracking Metrics 33 | 34 | Once you've created a client, you can track timers and metrics: 35 | 36 | ```rust 37 | // Increment a counter by 1 38 | client.incr("some.counter"); 39 | 40 | // Decrement a counter by 1 41 | client.decr("some.counter"); 42 | 43 | // Update a gauge 44 | client.gauge("some.value", 12.0); 45 | 46 | // Modify a counter by an arbitrary float. 47 | client.count("some.counter", 511.0); 48 | 49 | // Send a histogram value as a float. 50 | client.histogram("some.histogram", 511.0); 51 | 52 | // Send a key/value. 53 | client.kv("some.data", 15.26); 54 | ``` 55 | 56 | ### Tracking Timers 57 | 58 | Timers can be updated using `timer()` and `time()`: 59 | 60 | ```rust 61 | // Update a timer based on a calculation you've done. 62 | client.timer("operation.duration", 13.4); 63 | 64 | // Time a closure 65 | client.time("operation.duration", || { 66 | // Do something expensive. 67 | }); 68 | ``` 69 | 70 | ### Pipeline 71 | 72 | Multiple metrics can be sent to StatsD once using pipeline: 73 | 74 | ```rust 75 | let mut pipe = client.pipeline(): 76 | 77 | // Increment a counter by 1 78 | pipe.incr("some.counter"); 79 | 80 | // Decrement a counter by 1 81 | pipe.decr("some.counter"); 82 | 83 | // Update a gauge 84 | pipe.gauge("some.value", 12.0); 85 | 86 | // Modify a counter by an arbitrary float. 87 | pipe.count("some.counter", 511.0); 88 | 89 | // Send a histogram value as a float. 90 | pipe.histogram("some.histogram", 511.0); 91 | 92 | // Send a key/value. 93 | pipe.kv("some.data", 15.26); 94 | 95 | // Set max UDP packet size if you wish, default is 512 96 | pipe.set_max_udp_size(128); 97 | 98 | // Send to StatsD 99 | pipe.send(&client); 100 | ``` 101 | 102 | Pipelines are also helpful to make functions simpler to test, as you can 103 | pass a pipeline and be confident that no UDP packets will be sent. 104 | 105 | 106 | ## License 107 | 108 | Licenesed under the [MIT License](LICENSE.txt). 109 | -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | // Load the crate 2 | extern crate statsd; 3 | 4 | // Import the client object. 5 | use statsd::client::Client; 6 | 7 | fn main() { 8 | let client = Client::new("127.0.0.1:8125", "myapp").unwrap(); 9 | client.incr("some.counter"); 10 | println!("Sent a counter!"); 11 | 12 | client.gauge("some.gauge", 124.0); 13 | println!("Set a gauge!"); 14 | 15 | client.timer("timer.duration", 182.1); 16 | println!("Set a timer!"); 17 | 18 | client.time("closure.duration", || { 19 | println!("Timing a closure"); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::error; 3 | use std::fmt; 4 | use std::io::Error; 5 | use std::net::AddrParseError; 6 | use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; 7 | use std::time; 8 | 9 | #[derive(Debug)] 10 | pub enum StatsdError { 11 | IoError(Error), 12 | AddrParseError(String), 13 | } 14 | 15 | impl From for StatsdError { 16 | fn from(_: AddrParseError) -> StatsdError { 17 | StatsdError::AddrParseError("Address parsing error".to_string()) 18 | } 19 | } 20 | 21 | impl From for StatsdError { 22 | fn from(err: Error) -> StatsdError { 23 | StatsdError::IoError(err) 24 | } 25 | } 26 | 27 | impl fmt::Display for StatsdError { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | match *self { 30 | StatsdError::IoError(ref e) => write!(f, "{}", e), 31 | StatsdError::AddrParseError(ref e) => write!(f, "{}", e), 32 | } 33 | } 34 | } 35 | 36 | impl error::Error for StatsdError {} 37 | 38 | /// Client socket for statsd servers. 39 | /// 40 | /// After creating a metric you can use `Client` 41 | /// to send metrics to the configured statsd server 42 | /// 43 | /// # Example 44 | /// 45 | /// Creating a client and sending metrics can be done with: 46 | /// 47 | /// ```ignore 48 | /// use statsd::client::Client; 49 | /// 50 | /// let client = Client::new("127.0.0.1:8125", "myapp"); 51 | /// client.incr("some.metric.completed"); 52 | /// ``` 53 | pub struct Client { 54 | socket: UdpSocket, 55 | server_address: SocketAddr, 56 | prefix: String, 57 | } 58 | 59 | impl Client { 60 | /// Construct a new statsd client given an host/port & prefix 61 | pub fn new(host: T, prefix: &str) -> Result { 62 | let server_address = host 63 | .to_socket_addrs()? 64 | .next() 65 | .ok_or_else(|| StatsdError::AddrParseError("Address parsing error".to_string()))?; 66 | 67 | // Bind to a generic port as we'll only be writing on this 68 | // socket. 69 | let socket = if server_address.is_ipv4() { 70 | UdpSocket::bind("0.0.0.0:0")? 71 | } else { 72 | UdpSocket::bind("[::]:0")? 73 | }; 74 | Ok(Client { 75 | socket, 76 | prefix: prefix.to_string(), 77 | server_address, 78 | }) 79 | } 80 | 81 | /// Increment a metric by 1 82 | /// 83 | /// ```ignore 84 | /// # Increment a given metric by 1. 85 | /// client.incr("metric.completed"); 86 | /// ``` 87 | /// 88 | /// This modifies a counter with an effective sampling 89 | /// rate of 1.0. 90 | pub fn incr(&self, metric: &str) { 91 | self.count(metric, 1.0); 92 | } 93 | 94 | /// Decrement a metric by -1 95 | /// 96 | /// ```ignore 97 | /// # Decrement a given metric by 1 98 | /// client.decr("metric.completed"); 99 | /// ``` 100 | /// 101 | /// This modifies a counter with an effective sampling 102 | /// rate of 1.0. 103 | pub fn decr(&self, metric: &str) { 104 | self.count(metric, -1.0); 105 | } 106 | 107 | /// Modify a counter by `value`. 108 | /// 109 | /// Will increment or decrement a counter by `value` with 110 | /// a sampling rate of 1.0. 111 | /// 112 | /// ```ignore 113 | /// // Increment by 12 114 | /// client.count("metric.completed", 12.0); 115 | /// ``` 116 | pub fn count(&self, metric: &str, value: f64) { 117 | let data = self.prepare(format!("{}:{}|c", metric, value)); 118 | self.send(data); 119 | } 120 | 121 | /// Modify a counter by `value` only x% of the time. 122 | /// 123 | /// Will increment or decrement a counter by `value` with 124 | /// a custom sampling rate. 125 | /// 126 | /// 127 | /// ```ignore 128 | /// // Increment by 4 50% of the time. 129 | /// client.sampled_count("metric.completed", 4, 0.5); 130 | /// ``` 131 | pub fn sampled_count(&self, metric: &str, value: f64, rate: f64) { 132 | if rand::random::() >= rate { 133 | return; 134 | } 135 | let data = self.prepare(format!("{}:{}|c|@{}", metric, value, rate)); 136 | self.send(data); 137 | } 138 | 139 | /// Set a gauge value. 140 | /// 141 | /// ```ignore 142 | /// // set a gauge to 9001 143 | /// client.gauge("power_level.observed", 9001.0); 144 | /// ``` 145 | pub fn gauge(&self, metric: &str, value: f64) { 146 | let data = self.prepare(format!("{}:{}|g", metric, value)); 147 | self.send(data); 148 | } 149 | 150 | /// Send a timer value. 151 | /// 152 | /// The value is expected to be in ms. 153 | /// 154 | /// ```ignore 155 | /// // pass a duration value 156 | /// client.timer("response.duration", 10.123); 157 | /// ``` 158 | pub fn timer(&self, metric: &str, value: f64) { 159 | let data = self.prepare(format!("{}:{}|ms", metric, value)); 160 | self.send(data); 161 | } 162 | 163 | /// Time a block of code. 164 | /// 165 | /// The passed closure will be timed and executed. The block's 166 | /// duration will be sent as a metric. 167 | /// 168 | /// ```ignore 169 | /// // pass a duration value 170 | /// client.time("response.duration", || { 171 | /// // Your code here. 172 | /// }); 173 | /// ``` 174 | pub fn time(&self, metric: &str, callable: F) -> R 175 | where 176 | F: FnOnce() -> R, 177 | { 178 | let start = time::Instant::now(); 179 | let return_val = callable(); 180 | let used = start.elapsed(); 181 | let data = self.prepare(format!("{}:{}|ms", metric, used.as_millis())); 182 | self.send(data); 183 | return_val 184 | } 185 | 186 | fn prepare>(&self, data: T) -> String { 187 | if self.prefix.is_empty() { 188 | data.as_ref().to_string() 189 | } else { 190 | format!("{}.{}", self.prefix, data.as_ref()) 191 | } 192 | } 193 | 194 | /// Send data along the UDP socket. 195 | fn send(&self, data: String) { 196 | let _ = self.socket.send_to(data.as_bytes(), self.server_address); 197 | } 198 | 199 | /// Get a pipeline struct that allows optimizes the number of UDP 200 | /// packets used to send multiple metrics 201 | /// 202 | /// ```ignore 203 | /// let mut pipeline = client.pipeline(); 204 | /// pipeline.incr("some.metric", 1); 205 | /// pipeline.incr("other.metric", 1); 206 | /// pipeline.send(&mut client); 207 | /// ``` 208 | pub fn pipeline(&self) -> Pipeline { 209 | Pipeline::new() 210 | } 211 | 212 | /// Send a histogram value. 213 | /// 214 | /// ```ignore 215 | /// // pass response size value 216 | /// client.histogram("response.size", 128.0); 217 | /// ``` 218 | pub fn histogram(&self, metric: &str, value: f64) { 219 | let data = self.prepare(format!("{}:{}|h", metric, value)); 220 | self.send(data); 221 | } 222 | 223 | /// Send a key/value 224 | /// 225 | /// ```ignore 226 | /// client.kv("key", 1.); 227 | /// ``` 228 | pub fn kv(&self, metric: &str, value: f64) { 229 | let data = self.prepare(format!("{}:{}|kv", metric, value)); 230 | self.send(data); 231 | } 232 | } 233 | 234 | pub struct Pipeline { 235 | stats: VecDeque, 236 | max_udp_size: usize, 237 | } 238 | 239 | impl Pipeline { 240 | pub fn new() -> Pipeline { 241 | Pipeline { 242 | stats: VecDeque::new(), 243 | max_udp_size: 512, 244 | } 245 | } 246 | 247 | /// Set max UDP packet size 248 | /// 249 | /// ``` 250 | /// use statsd::client::Pipeline; 251 | /// 252 | /// let mut pipe = Pipeline::new(); 253 | /// pipe.set_max_udp_size(128); 254 | /// ``` 255 | pub fn set_max_udp_size(&mut self, max_udp_size: usize) { 256 | self.max_udp_size = max_udp_size; 257 | } 258 | 259 | /// Increment a metric by 1 260 | /// 261 | /// ``` 262 | /// use statsd::client::Pipeline; 263 | /// 264 | /// let mut pipe = Pipeline::new(); 265 | /// // Increment a given metric by 1. 266 | /// pipe.incr("metric.completed"); 267 | /// ``` 268 | /// 269 | /// This modifies a counter with an effective sampling 270 | /// rate of 1.0. 271 | pub fn incr(&mut self, metric: &str) { 272 | self.count(metric, 1.0); 273 | } 274 | 275 | /// Decrement a metric by -1 276 | /// 277 | /// ``` 278 | /// use statsd::client::Pipeline; 279 | /// 280 | /// let mut pipe = Pipeline::new(); 281 | /// // Decrement a given metric by 1 282 | /// pipe.decr("metric.completed"); 283 | /// ``` 284 | /// 285 | /// This modifies a counter with an effective sampling 286 | /// rate of 1.0. 287 | pub fn decr(&mut self, metric: &str) { 288 | self.count(metric, -1.0); 289 | } 290 | 291 | /// Modify a counter by `value`. 292 | /// 293 | /// Will increment or decrement a counter by `value` with 294 | /// a sampling rate of 1.0. 295 | /// 296 | /// ``` 297 | /// use statsd::client::Pipeline; 298 | /// 299 | /// let mut pipe = Pipeline::new(); 300 | /// // Increment by 12 301 | /// pipe.count("metric.completed", 12.0); 302 | /// ``` 303 | pub fn count(&mut self, metric: &str, value: f64) { 304 | let data = format!("{}:{}|c", metric, value); 305 | self.stats.push_back(data); 306 | } 307 | 308 | /// Modify a counter by `value` only x% of the time. 309 | /// 310 | /// Will increment or decrement a counter by `value` with 311 | /// a custom sampling rate. 312 | /// 313 | /// ``` 314 | /// use statsd::client::Pipeline; 315 | /// 316 | /// let mut pipe = Pipeline::new(); 317 | /// // Increment by 4 50% of the time. 318 | /// pipe.sampled_count("metric.completed", 4.0, 0.5); 319 | /// ``` 320 | pub fn sampled_count(&mut self, metric: &str, value: f64, rate: f64) { 321 | if rand::random::() >= rate { 322 | return; 323 | } 324 | let data = format!("{}:{}|c|@{}", metric, value, rate); 325 | self.stats.push_back(data); 326 | } 327 | 328 | /// Set a gauge value. 329 | /// 330 | /// ``` 331 | /// use statsd::client::Pipeline; 332 | /// 333 | /// let mut pipe = Pipeline::new(); 334 | /// // set a gauge to 9001 335 | /// pipe.gauge("power_level.observed", 9001.0); 336 | /// ``` 337 | pub fn gauge(&mut self, metric: &str, value: f64) { 338 | let data = format!("{}:{}|g", metric, value); 339 | self.stats.push_back(data); 340 | } 341 | 342 | /// Send a timer value. 343 | /// 344 | /// The value is expected to be in ms. 345 | /// 346 | /// ``` 347 | /// use statsd::client::Pipeline; 348 | /// 349 | /// let mut pipe = Pipeline::new(); 350 | /// // pass a duration value 351 | /// pipe.timer("response.duration", 10.123); 352 | /// ``` 353 | pub fn timer(&mut self, metric: &str, value: f64) { 354 | let data = format!("{}:{}|ms", metric, value); 355 | self.stats.push_back(data); 356 | } 357 | 358 | /// Time a block of code. 359 | /// 360 | /// The passed closure will be timed and executed. The block's 361 | /// duration will be sent as a metric. 362 | /// 363 | /// ``` 364 | /// use statsd::client::Pipeline; 365 | /// 366 | /// let mut pipe = Pipeline::new(); 367 | /// // pass a duration value 368 | /// pipe.time("response.duration", || { 369 | /// // Your code here. 370 | /// }); 371 | /// ``` 372 | pub fn time(&mut self, metric: &str, callable: F) 373 | where 374 | F: FnOnce(), 375 | { 376 | let start = time::Instant::now(); 377 | callable(); 378 | let used = start.elapsed(); 379 | let data = format!("{}:{}|ms", metric, used.as_millis()); 380 | self.stats.push_back(data); 381 | } 382 | 383 | /// Send a histogram value. 384 | /// 385 | /// ``` 386 | /// use statsd::client::Pipeline; 387 | /// 388 | /// let mut pipe = Pipeline::new(); 389 | /// // pass response size value 390 | /// pipe.histogram("response.size", 128.0); 391 | /// ``` 392 | pub fn histogram(&mut self, metric: &str, value: f64) { 393 | let data = format!("{}:{}|h", metric, value); 394 | self.stats.push_back(data); 395 | } 396 | 397 | /// Send a key/value. 398 | /// 399 | /// ``` 400 | /// use statsd::client::Pipeline; 401 | /// 402 | /// let mut pipe = Pipeline::new(); 403 | /// // pass response size value 404 | /// pipe.kv("response.size", 256.); 405 | /// ``` 406 | pub fn kv(&mut self, metric: &str, value: f64) { 407 | let data = format!("{}:{}|kv", metric, value); 408 | self.stats.push_back(data); 409 | } 410 | 411 | /// Send data along the UDP socket. 412 | pub fn send(&mut self, client: &Client) { 413 | let mut _data = String::new(); 414 | if let Some(data) = self.stats.pop_front() { 415 | _data += client.prepare(&data).as_ref(); 416 | while !self.stats.is_empty() { 417 | let stat = client.prepare(self.stats.pop_front().unwrap()); 418 | if _data.len() + stat.len() + 1 > self.max_udp_size { 419 | client.send(_data.clone()); 420 | _data.clear(); 421 | _data += &stat; 422 | } else { 423 | _data += "\n"; 424 | _data += &stat; 425 | } 426 | } 427 | } 428 | if !_data.is_empty() { 429 | client.send(_data); 430 | } 431 | } 432 | } 433 | 434 | impl Default for Pipeline { 435 | fn default() -> Self { 436 | Self::new() 437 | } 438 | } 439 | 440 | #[cfg(test)] 441 | mod test { 442 | use super::*; 443 | 444 | use std::net::{SocketAddr, UdpSocket}; 445 | use std::sync::{ 446 | atomic::{AtomicBool, Ordering}, 447 | mpsc::channel, 448 | Arc, 449 | }; 450 | use std::thread; 451 | use std::time::Duration; 452 | 453 | struct Server { 454 | local_addr: SocketAddr, 455 | sock: UdpSocket, 456 | } 457 | 458 | impl Server { 459 | fn new() -> Self { 460 | let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); 461 | let sock = UdpSocket::bind(addr).unwrap(); 462 | sock.set_read_timeout(Some(Duration::from_millis(100))) 463 | .unwrap(); 464 | let local_addr = sock.local_addr().unwrap(); 465 | Server { local_addr, sock } 466 | } 467 | 468 | fn addr(&self) -> SocketAddr { 469 | self.local_addr 470 | } 471 | 472 | /// Run the given test function while receiving several packets. Return a vector of the 473 | /// packets. 474 | fn run_while_receiving_all(self, func: F) -> Vec 475 | where 476 | F: Fn(), 477 | { 478 | let (serv_tx, serv_rx) = channel(); 479 | let func_ran = Arc::new(AtomicBool::new(false)); 480 | let bg_func_ran = Arc::clone(&func_ran); 481 | let bg = thread::spawn(move || loop { 482 | let mut buf = [0; 1500]; 483 | if let Ok((len, _)) = self.sock.recv_from(&mut buf) { 484 | let bytes = Vec::from(&buf[0..len]); 485 | serv_tx.send(bytes).unwrap(); 486 | } 487 | // go through the loop least once (do...while) 488 | if bg_func_ran.load(Ordering::SeqCst) { 489 | break; 490 | } 491 | }); 492 | func(); 493 | std::thread::sleep(Duration::from_millis(200)); 494 | func_ran.store(true, Ordering::SeqCst); 495 | bg.join().expect("background thread should join"); 496 | serv_rx 497 | .into_iter() 498 | .map(|bytes| String::from_utf8(bytes).unwrap()) 499 | .collect() 500 | } 501 | 502 | /// Run the given test function while receiving several packets. Return the concatenation 503 | /// of the packets. 504 | fn run_while_receiving(self, func: F) -> String 505 | where 506 | F: Fn(), 507 | { 508 | itertools::Itertools::intersperse( 509 | self.run_while_receiving_all(func).into_iter(), 510 | String::from("\n"), 511 | ) 512 | .fold(String::new(), |acc, b| acc + &b) 513 | } 514 | } 515 | 516 | #[test] 517 | fn test_sending_gauge() { 518 | let server = Server::new(); 519 | let client = Client::new(server.addr(), "myapp").unwrap(); 520 | let response = server.run_while_receiving(|| client.gauge("metric", 9.1)); 521 | assert_eq!("myapp.metric:9.1|g", response); 522 | } 523 | 524 | #[test] 525 | fn test_sending_gauge_without_prefix() { 526 | let server = Server::new(); 527 | let client = Client::new(server.addr(), "").unwrap(); 528 | let response = server.run_while_receiving(|| client.gauge("metric", 9.1)); 529 | assert_eq!("metric:9.1|g", response); 530 | } 531 | 532 | #[test] 533 | fn test_sending_incr() { 534 | let server = Server::new(); 535 | let client = Client::new(server.addr(), "myapp").unwrap(); 536 | let response = server.run_while_receiving(|| client.incr("metric")); 537 | assert_eq!("myapp.metric:1|c", response); 538 | } 539 | 540 | #[test] 541 | fn test_sending_decr() { 542 | let server = Server::new(); 543 | let client = Client::new(server.addr(), "myapp").unwrap(); 544 | let response = server.run_while_receiving(|| client.decr("metric")); 545 | assert_eq!("myapp.metric:-1|c", response); 546 | } 547 | 548 | #[test] 549 | fn test_sending_count() { 550 | let server = Server::new(); 551 | let client = Client::new(server.addr(), "myapp").unwrap(); 552 | let response = server.run_while_receiving(|| client.count("metric", 12.2)); 553 | assert_eq!("myapp.metric:12.2|c", response); 554 | } 555 | 556 | #[test] 557 | fn test_sending_timer() { 558 | let server = Server::new(); 559 | let client = Client::new(server.addr(), "myapp").unwrap(); 560 | let response = server.run_while_receiving(|| client.timer("metric", 21.39)); 561 | assert_eq!("myapp.metric:21.39|ms", response); 562 | } 563 | 564 | struct TimeTest { 565 | num: u8, 566 | } 567 | 568 | #[test] 569 | fn test_sending_timed_block() { 570 | let server = Server::new(); 571 | let client = Client::new(server.addr(), "myapp").unwrap(); 572 | let response = server.run_while_receiving(|| { 573 | let mut t = TimeTest { num: 10 }; 574 | let output = client.time("time_block", || { 575 | t.num += 2; 576 | "a string" 577 | }); 578 | assert_eq!(output, "a string"); 579 | assert_eq!(t.num, 12); 580 | }); 581 | assert!(response.contains("myapp.time_block")); 582 | assert!(response.contains("|ms")); 583 | } 584 | 585 | #[test] 586 | fn test_sending_histogram() { 587 | let server = Server::new(); 588 | let client = Client::new(server.addr(), "myapp").unwrap(); 589 | let response = server.run_while_receiving(|| client.histogram("metric", 9.1)); 590 | assert_eq!("myapp.metric:9.1|h", response); 591 | } 592 | 593 | #[test] 594 | fn test_sending_kv() { 595 | let server = Server::new(); 596 | let client = Client::new(server.addr(), "myapp").unwrap(); 597 | let response = server.run_while_receiving(|| client.kv("metric", 15.26)); 598 | assert_eq!("myapp.metric:15.26|kv", response); 599 | } 600 | 601 | #[test] 602 | fn test_pipeline_sending_time_block() { 603 | let server = Server::new(); 604 | let client = Client::new(server.addr(), "myapp").unwrap(); 605 | let response = server.run_while_receiving(|| { 606 | let mut pipeline = client.pipeline(); 607 | pipeline.gauge("metric", 9.1); 608 | 609 | let mut t = TimeTest { num: 10 }; 610 | pipeline.time("time_block", || { 611 | t.num += 2; 612 | }); 613 | pipeline.send(&client); 614 | assert_eq!(t.num, 12); 615 | }); 616 | assert_eq!("myapp.metric:9.1|g\nmyapp.time_block:0|ms", response); 617 | } 618 | 619 | #[test] 620 | fn test_pipeline_sending_gauge() { 621 | let server = Server::new(); 622 | let client = Client::new(server.addr(), "myapp").unwrap(); 623 | let response = server.run_while_receiving(|| { 624 | let mut pipeline = client.pipeline(); 625 | pipeline.gauge("metric", 9.1); 626 | pipeline.send(&client); 627 | }); 628 | assert_eq!("myapp.metric:9.1|g", response); 629 | } 630 | 631 | #[test] 632 | fn test_pipeline_sending_histogram() { 633 | let server = Server::new(); 634 | let client = Client::new(server.addr(), "myapp").unwrap(); 635 | let response = server.run_while_receiving(|| { 636 | let mut pipeline = client.pipeline(); 637 | pipeline.histogram("metric", 9.1); 638 | pipeline.send(&client); 639 | }); 640 | assert_eq!("myapp.metric:9.1|h", response); 641 | } 642 | 643 | #[test] 644 | fn test_pipeline_sending_kv() { 645 | let server = Server::new(); 646 | let client = Client::new(server.addr(), "myapp").unwrap(); 647 | let response = server.run_while_receiving(|| { 648 | let mut pipeline = client.pipeline(); 649 | pipeline.kv("metric", 15.26); 650 | pipeline.send(&client); 651 | }); 652 | assert_eq!("myapp.metric:15.26|kv", response); 653 | } 654 | 655 | #[test] 656 | fn test_pipeline_sending_multiple_data() { 657 | let server = Server::new(); 658 | let client = Client::new(server.addr(), "myapp").unwrap(); 659 | let response = server.run_while_receiving(|| { 660 | let mut pipeline = client.pipeline(); 661 | pipeline.gauge("metric", 9.1); 662 | pipeline.count("metric", 12.2); 663 | pipeline.send(&client); 664 | }); 665 | assert_eq!("myapp.metric:9.1|g\nmyapp.metric:12.2|c", response); 666 | } 667 | 668 | #[test] 669 | fn test_pipeline_set_max_udp_size() { 670 | let server = Server::new(); 671 | let client = Client::new(server.addr(), "myapp").unwrap(); 672 | let response = server.run_while_receiving_all(|| { 673 | let mut pipeline = client.pipeline(); 674 | pipeline.set_max_udp_size(20); 675 | pipeline.gauge("metric", 9.1); 676 | pipeline.count("metric", 12.2); 677 | pipeline.send(&client); 678 | }); 679 | assert_eq!(vec!["myapp.metric:9.1|g", "myapp.metric:12.2|c"], response); 680 | } 681 | 682 | #[test] 683 | fn test_pipeline_set_max_udp_size_chunk() { 684 | let server = Server::new(); 685 | let client = Client::new(server.addr(), "myapp").unwrap(); 686 | let response = server.run_while_receiving_all(|| { 687 | let mut pipeline = client.pipeline(); 688 | pipeline.set_max_udp_size(5); 689 | pipeline.gauge("metric_gauge", 9.1); 690 | pipeline.count("metric_letters", 12.2); 691 | pipeline.send(&client); 692 | }); 693 | assert_eq!( 694 | vec!["myapp.metric_gauge:9.1|g", "myapp.metric_letters:12.2|c"], 695 | response 696 | ); 697 | } 698 | 699 | #[test] 700 | fn test_pipeline_send_metric_after_pipeline() { 701 | let server = Server::new(); 702 | let client = Client::new(server.addr(), "myapp").unwrap(); 703 | let response = server.run_while_receiving_all(|| { 704 | let mut pipeline = client.pipeline(); 705 | 706 | pipeline.gauge("load", 9.0); 707 | pipeline.count("customers", 7.0); 708 | pipeline.send(&client); 709 | 710 | // Should still be able to send metrics 711 | // with the client. 712 | client.count("customers", 6.0); 713 | }); 714 | assert_eq!( 715 | vec!["myapp.load:9|g\nmyapp.customers:7|c", "myapp.customers:6|c"], 716 | response 717 | ); 718 | } 719 | } 720 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust implementation of statsd server & client. 2 | //! 3 | //! The statsd protocol consistents of plain-text single-packet messages sent 4 | //! over UDP, containing not much more than a key and (possibly sampled) value. 5 | //! 6 | //! Due to the inherent design of the system, there is no guarantee that metrics 7 | //! will be received by the server, and there is (by design) no indication of 8 | //! this. 9 | //! 10 | pub mod client; 11 | pub use client::Client; 12 | --------------------------------------------------------------------------------