dogstatsd/lib.rs
1//! A Rust client for interacting with Dogstatsd
2//!
3//! Dogstatsd is a custom `StatsD` implementation by `DataDog` for sending metrics and events to their
4//! system. Through this client you can report any type of metric you want, tag it, and enjoy your
5//! custom metrics.
6//!
7//! ## Usage
8//!
9//! Build an options struct and create a client:
10//!
11//! ```
12//! use dogstatsd::{Client, Options, OptionsBuilder};
13//! use std::time::Duration;
14//!
15//! // Binds to a udp socket on an available ephemeral port on 127.0.0.1 for
16//! // transmitting, and sends to 127.0.0.1:8125, the default dogstatsd
17//! // address.
18//! let default_options = Options::default();
19//! let default_client = Client::new(default_options).unwrap();
20//!
21//! // Binds to 127.0.0.1:9000 for transmitting and sends to 10.1.2.3:8125, with a
22//! // namespace of "analytics".
23//! let custom_options = Options::new("127.0.0.1:9000", "10.1.2.3:8125", "analytics", vec!(String::new()), None, None);
24//! let custom_client = Client::new(custom_options).unwrap();
25//!
26//! // You can also use the OptionsBuilder API to avoid needing to specify every option.
27//! let built_options = OptionsBuilder::new().from_addr(String::from("127.0.0.1:9001")).build();
28//! let built_client = Client::new(built_options).unwrap();
29//! ```
30//!
31//! Start sending metrics:
32//!
33//! ```
34//! use dogstatsd::{Client, Options, ServiceCheckOptions, ServiceStatus};
35//!
36//! let client = Client::new(Options::default()).unwrap();
37//! let tags = &["env:production"];
38//!
39//! // Increment a counter
40//! client.incr("my_counter", tags).unwrap();
41//!
42//! // Decrement a counter
43//! client.decr("my_counter", tags).unwrap();
44//!
45//! // Time a block of code (reports in ms)
46//! client.time("my_time", tags, || {
47//! // Some time consuming code
48//! }).unwrap();
49//!
50//! // Report your own timing in ms
51//! client.timing("my_timing", 500, tags).unwrap();
52//!
53//! // Report an arbitrary value (a gauge)
54//! client.gauge("my_gauge", "12345", tags).unwrap();
55//!
56//! // Report a sample of a histogram
57//! client.histogram("my_histogram", "67890", tags).unwrap();
58//!
59//! // Report a sample of a distribution
60//! client.distribution("distribution", "67890", tags).unwrap();
61//!
62//! // Report a member of a set
63//! client.set("my_set", "13579", tags).unwrap();
64//!
65//! // Report a service check
66//! let service_check_options = ServiceCheckOptions {
67//! hostname: Some("my-host.localhost"),
68//! ..Default::default()
69//! };
70//! client.service_check("redis.can_connect", ServiceStatus::OK, tags, Some(service_check_options)).unwrap();
71//!
72//! // Send a custom event
73//! client.event("My Custom Event Title", "My Custom Event Body", tags).unwrap();
74//!
75//! use dogstatsd::{EventOptions, EventPriority, EventAlertType};
76//! let event_options = EventOptions::new()
77//! .with_timestamp(1638480000)
78//! .with_hostname("localhost")
79//! .with_priority(EventPriority::Normal)
80//! .with_alert_type(EventAlertType::Error);
81//! client.event_with_options("My Custom Event Title", "My Custom Event Body", tags, Some(event_options)).unwrap();
82//! ```
83
84#![cfg_attr(feature = "unstable", feature(test))]
85#![deny(
86 warnings,
87 missing_debug_implementations,
88 missing_copy_implementations,
89 missing_docs
90)]
91extern crate chrono;
92
93use chrono::Utc;
94use std::borrow::Cow;
95use std::future::Future;
96use std::net::UdpSocket;
97use std::os::unix::net::UnixDatagram;
98use std::sync::mpsc::Sender;
99use std::sync::{mpsc, Mutex};
100use std::thread;
101use std::time::Duration;
102
103pub use self::error::DogstatsdError;
104use self::metrics::*;
105pub use self::metrics::{EventAlertType, EventPriority, ServiceCheckOptions, ServiceStatus};
106
107mod error;
108mod metrics;
109
110/// A type alias for returning a unit type or an error
111pub type DogstatsdResult = Result<(), DogstatsdError>;
112
113const DEFAULT_FROM_ADDR: &str = "0.0.0.0:0";
114const DEFAULT_TO_ADDR: &str = "127.0.0.1:8125";
115
116/// The struct that represents the options available for the Dogstatsd client.
117#[derive(Debug, PartialEq, Clone, Copy)]
118pub struct BatchingOptions {
119 /// The maximum buffer size in bytes of a batch of events.
120 pub max_buffer_size: usize,
121 /// The maximum time before sending a batch of events.
122 pub max_time: Duration,
123 /// The maximum retry attempts if we fail to flush our buffer
124 pub max_retry_attempts: usize,
125 /// Upon retry, there is an exponential backoff, this value sets the starting value
126 pub initial_retry_delay: u64,
127}
128
129/// The struct that represents the options available for the Dogstatsd client.
130#[derive(Debug, PartialEq)]
131pub struct Options {
132 /// The address of the udp socket we'll bind to for sending.
133 pub from_addr: String,
134 /// The address of the udp socket we'll send metrics and events to.
135 pub to_addr: String,
136 /// A namespace to prefix all metrics with, joined with a '.'.
137 pub namespace: String,
138 /// Default tags to include with every request.
139 pub default_tags: Vec<String>,
140 /// OPTIONAL, if defined, will use UDS instead of UDP and will ignore UDP options
141 pub socket_path: Option<String>,
142 /// OPTIONAL, if defined, will utilize batching for sending metrics
143 pub batching_options: Option<BatchingOptions>,
144}
145
146impl Default for Options {
147 /// Create a new options struct with all the default settings.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// use dogstatsd::Options;
153 ///
154 /// let options = Options::default();
155 ///
156 /// assert_eq!(
157 /// Options {
158 /// from_addr: "0.0.0.0:0".into(),
159 /// to_addr: "127.0.0.1:8125".into(),
160 /// namespace: String::new(),
161 /// default_tags: vec!(),
162 /// socket_path: None,
163 /// batching_options: None,
164 /// },
165 /// options
166 /// )
167 /// ```
168 fn default() -> Self {
169 Options {
170 from_addr: DEFAULT_FROM_ADDR.into(),
171 to_addr: DEFAULT_TO_ADDR.into(),
172 namespace: String::new(),
173 default_tags: vec![],
174 socket_path: None,
175 batching_options: None,
176 }
177 }
178}
179
180impl Options {
181 /// Create a new options struct by supplying values for all fields.
182 ///
183 /// # Examples
184 ///
185 /// ```
186 /// use dogstatsd::Options;
187 ///
188 /// let options = Options::new("127.0.0.1:9000", "127.0.0.1:9001", "", vec!(String::new()), None, None);
189 /// ```
190 pub fn new(
191 from_addr: &str,
192 to_addr: &str,
193 namespace: &str,
194 default_tags: Vec<String>,
195 socket_path: Option<String>,
196 batching_options: Option<BatchingOptions>,
197 ) -> Self {
198 Options {
199 from_addr: from_addr.into(),
200 to_addr: to_addr.into(),
201 namespace: namespace.into(),
202 default_tags,
203 socket_path,
204 batching_options,
205 }
206 }
207
208 fn merge_with_system_tags(default_tags: Vec<String>) -> Vec<String> {
209 let mut merged_tags = default_tags;
210
211 if merged_tags
212 .iter()
213 .find(|tag| tag.starts_with("env:"))
214 .is_none()
215 {
216 if let Ok(env) = std::env::var("DD_ENV") {
217 merged_tags.push(format!("env:{}", env));
218 }
219 }
220 if merged_tags
221 .iter()
222 .find(|tag| tag.starts_with("service:"))
223 .is_none()
224 {
225 if let Ok(service) = std::env::var("DD_SERVICE") {
226 merged_tags.push(format!("service:{}", service));
227 }
228 }
229 if merged_tags
230 .iter()
231 .find(|tag| tag.starts_with("version:"))
232 .is_none()
233 {
234 if let Ok(version) = std::env::var("DD_VERSION") {
235 merged_tags.push(format!("version:{}", version));
236 }
237 }
238
239 merged_tags
240 }
241}
242
243/// Struct that allows build an `Options` for available for the Dogstatsd client.
244#[derive(Default, Debug)]
245pub struct OptionsBuilder {
246 /// The address of the udp socket we'll bind to for sending.
247 from_addr: Option<String>,
248 /// The address of the udp socket we'll send metrics and events to.
249 to_addr: Option<String>,
250 /// A namespace to prefix all metrics with, joined with a '.'.
251 namespace: Option<String>,
252 /// Default tags to include with every request.
253 default_tags: Vec<String>,
254 /// OPTIONAL, if defined, will use UDS instead of UDP and will ignore UDP options
255 socket_path: Option<String>,
256 /// OPTIONAL, if defined, will utilize batching for sending metrics
257 batching_options: Option<BatchingOptions>,
258}
259
260impl OptionsBuilder {
261 /// Create a new `OptionsBuilder` struct.
262 ///
263 /// # Examples
264 ///
265 /// ```
266 /// use dogstatsd::OptionsBuilder;
267 ///
268 /// let options_builder = OptionsBuilder::new();
269 /// ```
270 pub fn new() -> Self {
271 Self::default()
272 }
273
274 /// Will allow the builder to generate an `Options` struct with the provided value.
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use dogstatsd::OptionsBuilder;
280 ///
281 /// let options_builder = OptionsBuilder::new().from_addr(String::from("127.0.0.1:9000"));
282 /// ```
283 pub fn from_addr(&mut self, from_addr: String) -> &mut OptionsBuilder {
284 self.from_addr = Some(from_addr);
285 self
286 }
287
288 /// Will allow the builder to generate an `Options` struct with the provided value.
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// use dogstatsd::OptionsBuilder;
294 ///
295 /// let options_builder = OptionsBuilder::new().to_addr(String::from("127.0.0.1:9001"));
296 /// ```
297 pub fn to_addr(&mut self, to_addr: String) -> &mut OptionsBuilder {
298 self.to_addr = Some(to_addr);
299 self
300 }
301
302 /// Will allow the builder to generate an `Options` struct with the provided value.
303 ///
304 /// # Examples
305 ///
306 /// ```
307 /// use dogstatsd::OptionsBuilder;
308 ///
309 /// let options_builder = OptionsBuilder::new().namespace(String::from("mynamespace"));
310 /// ```
311 pub fn namespace(&mut self, namespace: String) -> &mut OptionsBuilder {
312 self.namespace = Some(namespace);
313 self
314 }
315
316 /// Will allow the builder to generate an `Options` struct with the provided value. Can be called multiple times to add multiple `default_tags` to the `Options`.
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// use dogstatsd::OptionsBuilder;
322 ///
323 /// let options_builder = OptionsBuilder::new().default_tag(String::from("tag1:tav1val")).default_tag(String::from("tag2:tag2val"));
324 /// ```
325 pub fn default_tag(&mut self, default_tag: String) -> &mut OptionsBuilder {
326 self.default_tags.push(default_tag);
327 self
328 }
329
330 /// Will allow the builder to generate an `Options` struct with the provided value.
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// use dogstatsd::OptionsBuilder;
336 ///
337 /// let options_builder = OptionsBuilder::new().default_tag(String::from("tag1:tav1val")).default_tag(String::from("tag2:tav2val"));
338 /// ```
339 pub fn socket_path(&mut self, socket_path: Option<String>) -> &mut OptionsBuilder {
340 self.socket_path = socket_path;
341 self
342 }
343
344 /// Will allow the builder to generate an `Options` struct with the provided value.
345 ///
346 /// # Examples
347 ///
348 /// ```
349 /// use dogstatsd::{ OptionsBuilder, BatchingOptions };
350 /// use std::time::Duration;
351 ///
352 /// let options_builder = OptionsBuilder::new().batching_options(BatchingOptions { max_buffer_size: 8000, max_time: Duration::from_millis(3000), max_retry_attempts: 3, initial_retry_delay: 10 });
353 /// ```
354 pub fn batching_options(&mut self, batching_options: BatchingOptions) -> &mut OptionsBuilder {
355 self.batching_options = Some(batching_options);
356 self
357 }
358
359 /// Will construct an `Options` with all of the provided values and fall back to the default values if they aren't provided.
360 ///
361 /// # Examples
362 ///
363 /// ```
364 /// use dogstatsd::OptionsBuilder;
365 /// use dogstatsd::Options;
366 ///
367 /// let options = OptionsBuilder::new().namespace(String::from("mynamespace")).default_tag(String::from("tag1:tav1val")).build();
368 ///
369 /// assert_eq!(
370 /// Options {
371 /// from_addr: "0.0.0.0:0".into(),
372 /// to_addr: "127.0.0.1:8125".into(),
373 /// namespace: String::from("mynamespace"),
374 /// default_tags: vec!(String::from("tag1:tav1val")),
375 /// socket_path: None,
376 /// batching_options: None,
377 /// },
378 /// options
379 /// )
380 /// ```
381 pub fn build(&self) -> Options {
382 Options::new(
383 self.from_addr
384 .as_ref()
385 .unwrap_or(&String::from(DEFAULT_FROM_ADDR)),
386 self.to_addr
387 .as_ref()
388 .unwrap_or(&String::from(DEFAULT_TO_ADDR)),
389 self.namespace.as_ref().unwrap_or(&String::default()),
390 self.default_tags.to_vec(),
391 self.socket_path.clone(),
392 self.batching_options,
393 )
394 }
395}
396
397#[derive(Debug)]
398enum SocketType {
399 Udp(UdpSocket),
400 Uds(UnixDatagram),
401 BatchableUdp(Mutex<Sender<batch_processor::Message>>),
402 BatchableUds(Mutex<Sender<batch_processor::Message>>),
403}
404
405/// The client struct that handles sending metrics to the Dogstatsd server.
406#[derive(Debug)]
407pub struct Client {
408 socket: SocketType,
409 from_addr: String,
410 to_addr: String,
411 namespace: String,
412 default_tags: Vec<u8>,
413}
414
415impl PartialEq for Client {
416 fn eq(&self, other: &Self) -> bool {
417 // Ignore `socket`, which will never be the same
418 self.from_addr == other.from_addr
419 && self.to_addr == other.to_addr
420 && self.namespace == other.namespace
421 && self.default_tags == other.default_tags
422 }
423}
424
425impl Drop for Client {
426 fn drop(&mut self) {
427 match &self.socket {
428 SocketType::BatchableUdp(tx_channel) | SocketType::BatchableUds(tx_channel) => {
429 // Destructing Client... If fails, ignore and keep going...
430 let _ = tx_channel
431 .lock()
432 .unwrap()
433 .send(batch_processor::Message::Shutdown);
434 }
435 _ => {}
436 }
437 }
438}
439
440impl Client {
441 /// Create a new client from an options struct.
442 ///
443 /// # Examples
444 ///
445 /// ```
446 /// use dogstatsd::{Client, Options};
447 ///
448 /// let client = Client::new(Options::default()).unwrap();
449 /// ```
450 pub fn new(options: Options) -> Result<Self, DogstatsdError> {
451 let fn_create_tx_channel = |socket: SocketType,
452 batching_options: BatchingOptions,
453 to_addr: String,
454 socket_path: Option<String>|
455 -> Mutex<Sender<batch_processor::Message>> {
456 let (tx, rx) = mpsc::channel();
457 thread::spawn(move || {
458 batch_processor::process_events(batching_options, to_addr, socket, socket_path, rx);
459 });
460 Mutex::from(tx)
461 };
462
463 let socket = match options.socket_path {
464 Some(socket_path) => {
465 // The follow scenarios can occur:
466 // - socket does not exist yet: We will call .bind(...) to create one
467 // - socket exists, but no listener: We will retry attempting to connect
468 // however, if no listener subscribes to the socket within retries, we will
469 // failt to initialize
470 // - socket exists, with a listener: Calling .connect(...) will work successfully
471 let mut uds_socket = UnixDatagram::unbound()?;
472 match uds_socket.connect(socket_path.clone()) {
473 Ok(socket) => socket,
474 Err(e) => {
475 println!(
476 "Couldn't connect to uds socket.. attempting to re-create by binding directly: {e:?}"
477 );
478 uds_socket = UnixDatagram::bind(socket_path.clone())?;
479 }
480 };
481 uds_socket.set_nonblocking(true)?;
482
483 let wrapped_socket = SocketType::Uds(uds_socket);
484 if let Some(batching_options) = options.batching_options {
485 SocketType::BatchableUds(fn_create_tx_channel(
486 wrapped_socket,
487 batching_options,
488 options.to_addr.clone(),
489 Some(socket_path),
490 ))
491 } else {
492 wrapped_socket
493 }
494 }
495 None => {
496 let wrapped_socket = SocketType::Udp(UdpSocket::bind(&options.from_addr)?);
497 if let Some(batching_options) = options.batching_options {
498 SocketType::BatchableUdp(fn_create_tx_channel(
499 wrapped_socket,
500 batching_options,
501 options.to_addr.clone(),
502 None,
503 ))
504 } else {
505 wrapped_socket
506 }
507 }
508 };
509
510 let default_tags = Options::merge_with_system_tags(options.default_tags);
511
512 Ok(Client {
513 socket,
514 from_addr: options.from_addr,
515 to_addr: options.to_addr,
516 namespace: options.namespace,
517 default_tags: default_tags.join(",").into_bytes(),
518 })
519 }
520
521 /// Increment a StatsD counter
522 ///
523 /// # Examples
524 ///
525 /// ```
526 /// use dogstatsd::{Client, Options};
527 ///
528 /// let client = Client::new(Options::default()).unwrap();
529 /// client.incr("counter", &["tag:counter"])
530 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
531 /// ```
532 pub fn incr<'a, I, S, T>(&self, stat: S, tags: I) -> DogstatsdResult
533 where
534 I: IntoIterator<Item = T>,
535 S: Into<Cow<'a, str>>,
536 T: AsRef<str>,
537 {
538 self.send(&CountMetric::Incr(stat.into().as_ref(), 1), tags)
539 }
540
541 /// Increment a StatsD counter by the provided amount
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// use dogstatsd::{Client, Options};
547 ///
548 /// let client = Client::new(Options::default()).unwrap();
549 /// client.incr_by_value("counter", 123, &["tag:counter"])
550 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
551 /// ```
552 pub fn incr_by_value<'a, I, S, T>(&self, stat: S, value: i64, tags: I) -> DogstatsdResult
553 where
554 I: IntoIterator<Item = T>,
555 S: Into<Cow<'a, str>>,
556 T: AsRef<str>,
557 {
558 self.send(&CountMetric::Incr(stat.into().as_ref(), value), tags)
559 }
560
561 /// Decrement a StatsD counter
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// use dogstatsd::{Client, Options};
567 ///
568 /// let client = Client::new(Options::default()).unwrap();
569 /// client.decr("counter", &["tag:counter"])
570 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
571 /// ```
572 pub fn decr<'a, I, S, T>(&self, stat: S, tags: I) -> DogstatsdResult
573 where
574 I: IntoIterator<Item = T>,
575 S: Into<Cow<'a, str>>,
576 T: AsRef<str>,
577 {
578 self.send(&CountMetric::Decr(stat.into().as_ref(), 1), tags)
579 }
580
581 /// Decrement a StatsD counter by the provided amount
582 ///
583 /// # Examples
584 ///
585 /// ```
586 /// use dogstatsd::{Client, Options};
587 ///
588 /// let client = Client::new(Options::default()).unwrap();
589 /// client.decr_by_value("counter", 23, &["tag:counter"])
590 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
591 /// ```
592 pub fn decr_by_value<'a, I, S, T>(&self, stat: S, value: i64, tags: I) -> DogstatsdResult
593 where
594 I: IntoIterator<Item = T>,
595 S: Into<Cow<'a, str>>,
596 T: AsRef<str>,
597 {
598 self.send(&CountMetric::Decr(stat.into().as_ref(), value), tags)
599 }
600
601 /// Make an arbitrary change to a StatsD counter
602 ///
603 /// # Examples
604 ///
605 /// ```
606 /// use dogstatsd::{Client, Options};
607 ///
608 /// let client = Client::new(Options::default()).unwrap();
609 /// client.count("counter", 42, &["tag:counter"])
610 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
611 /// ```
612 pub fn count<'a, I, S, T>(&self, stat: S, count: i64, tags: I) -> DogstatsdResult
613 where
614 I: IntoIterator<Item = T>,
615 S: Into<Cow<'a, str>>,
616 T: AsRef<str>,
617 {
618 self.send(&CountMetric::Arbitrary(stat.into().as_ref(), count), tags)
619 }
620
621 /// Time how long it takes for a block of code to execute.
622 ///
623 /// # Examples
624 ///
625 /// ```
626 /// use dogstatsd::{Client, Options};
627 /// use std::thread;
628 /// use std::time::Duration;
629 ///
630 /// let client = Client::new(Options::default()).unwrap();
631 /// client.time("timer", &["tag:time"], || {
632 /// thread::sleep(Duration::from_millis(200))
633 /// }).unwrap_or_else(|(_, e)| println!("Encountered error: {}", e))
634 /// ```
635 pub fn time<'a, F, O, I, S, T>(
636 &self,
637 stat: S,
638 tags: I,
639 block: F,
640 ) -> Result<O, (O, DogstatsdError)>
641 where
642 F: FnOnce() -> O,
643 I: IntoIterator<Item = T>,
644 S: Into<Cow<'a, str>>,
645 T: AsRef<str>,
646 {
647 let start_time = Utc::now();
648 let output = block();
649 let end_time = Utc::now();
650 let stat = stat.into();
651 let metric = TimeMetric::new(stat.as_ref(), &start_time, &end_time);
652 match self.send(&metric, tags) {
653 Ok(()) => Ok(output),
654 Err(error) => Err((output, error)),
655 }
656 }
657
658 /// Time how long it takes for an async block of code to execute.
659 ///
660 /// # Examples
661 ///
662 /// ```
663 /// use dogstatsd::{Client, Options};
664 /// use std::thread;
665 /// use std::time::Duration;
666 ///
667 /// # async fn do_work() {}
668 /// async fn timer() {
669 /// let client = Client::new(Options::default()).unwrap();
670 /// client.async_time("timer", &["tag:time"], do_work)
671 /// .await
672 /// .unwrap_or_else(|(_, e)| println!("Encountered error: {}", e))
673 /// }
674 /// ```
675 pub async fn async_time<'a, Fn, Fut, O, I, S, T>(
676 &self,
677 stat: S,
678 tags: I,
679 block: Fn,
680 ) -> Result<O, (O, DogstatsdError)>
681 where
682 Fn: FnOnce() -> Fut,
683 Fut: Future<Output = O>,
684 I: IntoIterator<Item = T>,
685 S: Into<Cow<'a, str>>,
686 T: AsRef<str>,
687 {
688 let start_time = Utc::now();
689 let output = block().await;
690 let end_time = Utc::now();
691 let stat = stat.into();
692 match self.send(
693 &TimeMetric::new(stat.as_ref(), &start_time, &end_time),
694 tags,
695 ) {
696 Ok(()) => Ok(output),
697 Err(error) => Err((output, error)),
698 }
699 }
700
701 /// Send your own timing metric in milliseconds
702 ///
703 /// # Examples
704 ///
705 /// ```
706 /// use dogstatsd::{Client, Options};
707 ///
708 /// let client = Client::new(Options::default()).unwrap();
709 /// client.timing("timing", 350, &["tag:timing"])
710 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
711 /// ```
712 pub fn timing<'a, I, S, T>(&self, stat: S, ms: i64, tags: I) -> DogstatsdResult
713 where
714 I: IntoIterator<Item = T>,
715 S: Into<Cow<'a, str>>,
716 T: AsRef<str>,
717 {
718 self.send(&TimingMetric::new(stat.into().as_ref(), ms), tags)
719 }
720
721 /// Report an arbitrary value as a gauge
722 ///
723 /// # Examples
724 ///
725 /// ```
726 /// use dogstatsd::{Client, Options};
727 ///
728 /// let client = Client::new(Options::default()).unwrap();
729 /// client.gauge("gauge", "12345", &["tag:gauge"])
730 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
731 /// ```
732 pub fn gauge<'a, I, S, SS, T>(&self, stat: S, val: SS, tags: I) -> DogstatsdResult
733 where
734 I: IntoIterator<Item = T>,
735 S: Into<Cow<'a, str>>,
736 SS: Into<Cow<'a, str>>,
737 T: AsRef<str>,
738 {
739 self.send(
740 &GaugeMetric::new(stat.into().as_ref(), val.into().as_ref()),
741 tags,
742 )
743 }
744
745 /// Report a value in a histogram
746 ///
747 /// # Examples
748 ///
749 /// ```
750 /// use dogstatsd::{Client, Options};
751 ///
752 /// let client = Client::new(Options::default()).unwrap();
753 /// client.histogram("histogram", "67890", &["tag:histogram"])
754 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
755 /// ```
756 pub fn histogram<'a, I, S, SS, T>(&self, stat: S, val: SS, tags: I) -> DogstatsdResult
757 where
758 I: IntoIterator<Item = T>,
759 S: Into<Cow<'a, str>>,
760 SS: Into<Cow<'a, str>>,
761 T: AsRef<str>,
762 {
763 self.send(
764 &HistogramMetric::new(stat.into().as_ref(), val.into().as_ref()),
765 tags,
766 )
767 }
768
769 /// Report a value in a distribution
770 ///
771 /// # Examples
772 ///
773 /// ```
774 /// use dogstatsd::{Client, Options};
775 ///
776 /// let client = Client::new(Options::default()).unwrap();
777 /// client.distribution("distribution", "67890", &["tag:distribution"])
778 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
779 /// ```
780 pub fn distribution<'a, I, S, SS, T>(&self, stat: S, val: SS, tags: I) -> DogstatsdResult
781 where
782 I: IntoIterator<Item = T>,
783 S: Into<Cow<'a, str>>,
784 SS: Into<Cow<'a, str>>,
785 T: AsRef<str>,
786 {
787 self.send(
788 &DistributionMetric::new(stat.into().as_ref(), val.into().as_ref()),
789 tags,
790 )
791 }
792
793 /// Report a value in a set
794 ///
795 /// # Examples
796 ///
797 /// ```
798 /// use dogstatsd::{Client, Options};
799 ///
800 /// let client = Client::new(Options::default()).unwrap();
801 /// client.set("set", "13579", &["tag:set"])
802 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
803 /// ```
804 pub fn set<'a, I, S, SS, T>(&self, stat: S, val: SS, tags: I) -> DogstatsdResult
805 where
806 I: IntoIterator<Item = T>,
807 S: Into<Cow<'a, str>>,
808 SS: Into<Cow<'a, str>>,
809 T: AsRef<str>,
810 {
811 self.send(
812 &SetMetric::new(stat.into().as_ref(), val.into().as_ref()),
813 tags,
814 )
815 }
816
817 /// Report the status of a service
818 ///
819 /// # Examples
820 ///
821 /// ```
822 /// use dogstatsd::{Client, Options, ServiceStatus, ServiceCheckOptions};
823 ///
824 /// let client = Client::new(Options::default()).unwrap();
825 /// client.service_check("redis.can_connect", ServiceStatus::OK, &["tag:service"], None)
826 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
827 ///
828 /// let options = ServiceCheckOptions {
829 /// hostname: Some("my-host.localhost"),
830 /// ..Default::default()
831 /// };
832 /// client.service_check("redis.can_connect", ServiceStatus::OK, &["tag:service"], Some(options))
833 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
834 ///
835 /// let all_options = ServiceCheckOptions {
836 /// hostname: Some("my-host.localhost"),
837 /// timestamp: Some(1510326433),
838 /// message: Some("Message about the check or service")
839 /// };
840 /// client.service_check("redis.can_connect", ServiceStatus::OK, &["tag:service"], Some(all_options))
841 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
842 /// ```
843 pub fn service_check<'a, I, S, T>(
844 &self,
845 stat: S,
846 val: ServiceStatus,
847 tags: I,
848 options: Option<ServiceCheckOptions>,
849 ) -> DogstatsdResult
850 where
851 I: IntoIterator<Item = T>,
852 S: Into<Cow<'a, str>>,
853 T: AsRef<str>,
854 {
855 let unwrapped_options = options.unwrap_or_default();
856 self.send(
857 &ServiceCheck::new(stat.into().as_ref(), val, unwrapped_options),
858 tags,
859 )
860 }
861
862 /// Send a custom event as a title and a body
863 ///
864 /// # Examples
865 ///
866 /// ```
867 /// use dogstatsd::{Client, Options};
868 ///
869 /// let client = Client::new(Options::default()).unwrap();
870 /// client.event("Event Title", "Event Body", &["tag:event"])
871 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
872 ///
873 /// ```
874
875 pub fn event<'a, I, S, SS, T>(&self, title: S, text: SS, tags: I) -> DogstatsdResult
876 where
877 I: IntoIterator<Item = T>,
878 S: Into<Cow<'a, str>>,
879 SS: Into<Cow<'a, str>>,
880 T: AsRef<str>,
881 {
882 self.send(
883 &Event::new(title.into().as_ref(), text.into().as_ref()),
884 tags,
885 )
886 }
887
888 /// Send a custom event as a title and a body
889 ///
890 /// # Examples
891 ///
892 /// ```
893 /// use dogstatsd::{Client, Options, EventOptions, EventAlertType, EventPriority};
894 ///
895 /// let client = Client::new(Options::default()).unwrap();
896 /// let event_options = EventOptions::new()
897 /// .with_timestamp(1638480000)
898 /// .with_hostname("localhost")
899 /// .with_priority(EventPriority::Normal)
900 /// .with_alert_type(EventAlertType::Error);
901 /// client.event_with_options("My Custom Event Title", "My Custom Event Body", &["tag:event"], Some(event_options))
902 /// .unwrap_or_else(|e| println!("Encountered error: {}", e));
903
904 pub fn event_with_options<'a, I, S, SS, T>(
905 &self,
906 title: S,
907 text: SS,
908 tags: I,
909 options: Option<EventOptions<'a>>,
910 ) -> DogstatsdResult
911 where
912 I: IntoIterator<Item = T>,
913 S: Into<Cow<'a, str>>,
914 SS: Into<Cow<'a, str>>,
915 T: AsRef<str>,
916 {
917 let title_owned = title.into();
918 let text_owned = text.into();
919 let mut event = Event::new(title_owned.as_ref(), text_owned.as_ref());
920
921 // Apply additional options if provided
922 if let Some(options) = options {
923 if let Some(timestamp) = options.timestamp {
924 event = event.with_timestamp(timestamp);
925 }
926 if let Some(hostname) = options.hostname {
927 event = event.with_hostname(hostname);
928 }
929 if let Some(aggregation_key) = options.aggregation_key {
930 event = event.with_aggregation_key(aggregation_key);
931 }
932 if let Some(priority) = options.priority {
933 event = event.with_priority(priority);
934 }
935 if let Some(source_type_name) = options.source_type_name {
936 event = event.with_source_type_name(source_type_name);
937 }
938 if let Some(alert_type) = options.alert_type {
939 event = event.with_alert_type(alert_type);
940 }
941 }
942
943 self.send(&event, tags)
944 }
945
946 fn send<I, M, S>(&self, metric: &M, tags: I) -> DogstatsdResult
947 where
948 I: IntoIterator<Item = S>,
949 M: Metric,
950 S: AsRef<str>,
951 {
952 let formatted_metric = format_for_send(metric, &self.namespace, tags, &self.default_tags);
953 match &self.socket {
954 SocketType::Udp(socket) => {
955 socket.send_to(formatted_metric.as_slice(), &self.to_addr)?;
956 }
957 SocketType::Uds(socket) => {
958 socket.send(formatted_metric.as_slice())?;
959 }
960 SocketType::BatchableUdp(tx_channel) | SocketType::BatchableUds(tx_channel) => {
961 tx_channel
962 .lock()
963 .expect("Mutex poisoned...")
964 .send(batch_processor::Message::Data(formatted_metric))
965 .unwrap_or_else(|error| {
966 println!("Exception occurred when writing to channel: {:?}", error);
967 });
968 }
969 }
970 Ok(())
971 }
972}
973
974/// Configuration options for an `Event`.
975///
976/// `EventOptions` provides additional optional metadata that can be attached
977/// to an event, enabling greater flexibility and contextual information
978/// for event handling and monitoring systems.
979///
980/// # Example
981///
982/// ```rust
983/// use dogstatsd::{EventOptions, EventAlertType, EventPriority};
984///
985/// let options = EventOptions {
986/// timestamp: Some(1638480000),
987/// hostname: Some("localhost"),
988/// aggregation_key: Some("service_down"),
989/// priority: Some(EventPriority::Normal),
990/// source_type_name: Some("monitoring"),
991/// alert_type: Some(EventAlertType::Error),
992/// };
993/// ```
994///
995#[derive(Default, Clone, Copy, Debug)]
996pub struct EventOptions<'a> {
997 /// Optional Unix timestamp representing the event time. The default is the current Unix epoch timestamp.
998 pub timestamp: Option<u64>,
999 /// Optional hostname associated with the event.
1000 pub hostname: Option<&'a str>,
1001 /// Optional key for grouping related events.
1002 pub aggregation_key: Option<&'a str>,
1003 /// Optional priority level of the event, e.g., `"low"` or `"normal"`.
1004 pub priority: Option<EventPriority>,
1005 /// Optional source type name of the event, e.g., `"monitoring"`.
1006 pub source_type_name: Option<&'a str>,
1007 /// Optional alert type for the event, e.g., `"error"`, `"warning"`, `"info"`, `"success"`. Default `"info"`.
1008 pub alert_type: Option<EventAlertType>,
1009}
1010
1011impl<'a> EventOptions<'a> {
1012 /// Creates a new `EventOptions` instance with all fields set to `None`.
1013 pub fn new() -> Self {
1014 EventOptions {
1015 timestamp: None,
1016 hostname: None,
1017 aggregation_key: None,
1018 priority: None,
1019 source_type_name: None,
1020 alert_type: None,
1021 }
1022 }
1023 /// Sets the `hostname` for the event.
1024 pub fn with_timestamp(mut self, timestamp: u64) -> Self {
1025 self.timestamp = Some(timestamp);
1026 self
1027 }
1028
1029 /// Sets the `hostname` for the event.
1030 pub fn with_hostname(mut self, hostname: &'a str) -> Self {
1031 self.hostname = Some(hostname);
1032 self
1033 }
1034
1035 /// Sets the `aggregation_key` for the event.
1036 pub fn with_aggregation_key(mut self, aggregation_key: &'a str) -> Self {
1037 self.aggregation_key = Some(aggregation_key);
1038 self
1039 }
1040
1041 /// Sets the `priority` for the event.
1042 pub fn with_priority(mut self, priority: EventPriority) -> Self {
1043 self.priority = Some(priority);
1044 self
1045 }
1046
1047 /// Sets the `source_type_name` for the event.
1048 pub fn with_source_type_name(mut self, source_type_name: &'a str) -> Self {
1049 self.source_type_name = Some(source_type_name);
1050 self
1051 }
1052
1053 /// Sets the `alert_type` for the event.
1054 pub fn with_alert_type(mut self, alert_type: EventAlertType) -> Self {
1055 self.alert_type = Some(alert_type);
1056 self
1057 }
1058}
1059
1060mod batch_processor {
1061 use std::sync::mpsc::Receiver;
1062 use std::time::SystemTime;
1063
1064 use retry::{delay::jitter, delay::Exponential, retry};
1065
1066 use crate::{BatchingOptions, SocketType};
1067
1068 pub(crate) enum Message {
1069 Data(Vec<u8>),
1070 Shutdown,
1071 }
1072
1073 fn send_to_socket_with_retries(
1074 batching_options: &BatchingOptions,
1075 socket: &SocketType,
1076 data: &Vec<u8>,
1077 to_addr: &String,
1078 socket_path: &Option<String>,
1079 ) {
1080 retry(
1081 Exponential::from_millis(batching_options.initial_retry_delay)
1082 .map(jitter)
1083 .take(batching_options.max_retry_attempts),
1084 || {
1085 match socket {
1086 SocketType::Udp(socket) => {
1087 if let Err(error) = socket.send_to(data.as_slice(), to_addr) {
1088 return Err(error);
1089 }
1090 }
1091 SocketType::Uds(socket) => {
1092 if let Err(error) = socket.send(data.as_slice()) {
1093 // Per https://doc.rust-lang.org/stable/std/os/unix/net/struct.UnixDatagram.html#method.send
1094 // If send fails, it is due to a connection issue, so just attempt
1095 // to reconnect
1096 let socket_path_unwrapped = socket_path
1097 .as_ref()
1098 .expect("Only invoked if socket path is defined.");
1099 socket.connect(socket_path_unwrapped)?;
1100
1101 return Err(error);
1102 }
1103 }
1104 SocketType::BatchableUdp(_tx_channel)
1105 | SocketType::BatchableUds(_tx_channel) => {
1106 panic!("Logic Error - socket type should not be batchable.");
1107 }
1108 }
1109
1110 Ok(())
1111 },
1112 )
1113 .unwrap_or_else(|error| {
1114 println!(
1115 "Failed to send within retry policy... Dropping metrics: {:?}",
1116 error
1117 )
1118 });
1119 }
1120
1121 pub(crate) fn process_events(
1122 batching_options: BatchingOptions,
1123 to_addr: String,
1124 socket: SocketType,
1125 socket_path: Option<String>,
1126 rx: Receiver<Message>,
1127 ) {
1128 let mut last_updated = SystemTime::now();
1129 let mut buffer: Vec<u8> = vec![];
1130
1131 loop {
1132 match rx.recv() {
1133 Ok(Message::Data(data)) => {
1134 for ch in data {
1135 buffer.push(ch);
1136 }
1137 buffer.push(b'\n');
1138
1139 let current_time = SystemTime::now();
1140 if buffer.len() >= batching_options.max_buffer_size
1141 || last_updated + batching_options.max_time < current_time
1142 {
1143 send_to_socket_with_retries(
1144 &batching_options,
1145 &socket,
1146 &buffer,
1147 &to_addr,
1148 &socket_path,
1149 );
1150 buffer.clear();
1151 last_updated = current_time;
1152 }
1153 }
1154 Ok(Message::Shutdown) => {
1155 send_to_socket_with_retries(
1156 &batching_options,
1157 &socket,
1158 &buffer,
1159 &to_addr,
1160 &socket_path,
1161 );
1162 buffer.clear();
1163 }
1164 Err(e) => {
1165 println!("Exception occurred when reading from channel: {:?}", e);
1166 break;
1167 }
1168 }
1169 }
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use metrics::GaugeMetric;
1176
1177 use super::*;
1178
1179 #[test]
1180 fn test_options_default() {
1181 let options = Options::default();
1182 let expected_options = Options {
1183 from_addr: DEFAULT_FROM_ADDR.into(),
1184 to_addr: DEFAULT_TO_ADDR.into(),
1185 namespace: String::new(),
1186 ..Default::default()
1187 };
1188
1189 assert_eq!(expected_options, options)
1190 }
1191
1192 #[test]
1193 fn test_options_builder_none() {
1194 let options = OptionsBuilder::new().build();
1195 let expected_options = Options {
1196 from_addr: DEFAULT_FROM_ADDR.into(),
1197 to_addr: DEFAULT_TO_ADDR.into(),
1198 namespace: String::new(),
1199 ..Default::default()
1200 };
1201
1202 assert_eq!(expected_options, options);
1203 }
1204
1205 #[test]
1206 fn teset_options_builder_all() {
1207 let options = OptionsBuilder::new()
1208 .from_addr("127.0.0.2:0".into())
1209 .to_addr("127.0.0.2:8125".into())
1210 .namespace("mynamespace".into())
1211 .default_tag(String::from("tag1:tag1val"))
1212 .build();
1213 let expected_options = Options {
1214 from_addr: "127.0.0.2:0".into(),
1215 to_addr: "127.0.0.2:8125".into(),
1216 namespace: "mynamespace".into(),
1217 default_tags: vec!["tag1:tag1val".into()].to_vec(),
1218 socket_path: None,
1219 batching_options: None,
1220 };
1221
1222 assert_eq!(expected_options, options);
1223 }
1224
1225 #[test]
1226 fn test_new() {
1227 let client = Client::new(Options::default()).unwrap();
1228 let expected_client = Client {
1229 socket: SocketType::Udp(UdpSocket::bind(DEFAULT_FROM_ADDR).unwrap()),
1230 from_addr: DEFAULT_FROM_ADDR.into(),
1231 to_addr: DEFAULT_TO_ADDR.into(),
1232 namespace: String::new(),
1233 default_tags: String::new().into_bytes(),
1234 };
1235
1236 assert_eq!(expected_client, client)
1237 }
1238
1239 #[test]
1240 fn test_new_default_tags() {
1241 let options = Options::new(
1242 DEFAULT_FROM_ADDR,
1243 DEFAULT_TO_ADDR,
1244 "",
1245 vec![String::from("tag1:tag1val")],
1246 None,
1247 None,
1248 );
1249 let client = Client::new(options).unwrap();
1250 let expected_client = Client {
1251 socket: SocketType::Udp(UdpSocket::bind(DEFAULT_FROM_ADDR).unwrap()),
1252 from_addr: DEFAULT_FROM_ADDR.into(),
1253 to_addr: DEFAULT_TO_ADDR.into(),
1254 namespace: String::new(),
1255 default_tags: String::from("tag1:tag1val").into_bytes(),
1256 };
1257
1258 assert_eq!(expected_client, client)
1259 }
1260
1261 #[test]
1262 fn test_system_tags() {
1263 let options = Options::new(
1264 DEFAULT_FROM_ADDR,
1265 DEFAULT_TO_ADDR,
1266 "",
1267 vec![String::from("tag1:tag1val"), String::from("version:0.0.2")],
1268 None,
1269 None,
1270 );
1271
1272 let client = with_default_system_tags(|| Client::new(options).unwrap());
1273
1274 dbg!(String::from_utf8_lossy(client.default_tags.as_ref()));
1275
1276 let expected_client = Client {
1277 socket: SocketType::Udp(UdpSocket::bind(DEFAULT_FROM_ADDR).unwrap()),
1278 from_addr: DEFAULT_FROM_ADDR.into(),
1279 to_addr: DEFAULT_TO_ADDR.into(),
1280 namespace: String::new(),
1281 default_tags: String::from("tag1:tag1val,version:0.0.2,env:production,service:service")
1282 .into_bytes(),
1283 };
1284
1285 assert_eq!(expected_client, client)
1286 }
1287
1288 #[test]
1289 fn test_send() {
1290 let options = Options::new("127.0.0.1:9001", "127.0.0.1:9002", "", vec![], None, None);
1291 let client = Client::new(options).unwrap();
1292 // Shouldn't panic or error
1293 client
1294 .send(
1295 &GaugeMetric::new("gauge".into(), "1234".into()),
1296 &["tag1", "tag2"],
1297 )
1298 .unwrap();
1299 }
1300
1301 fn with_default_system_tags<T, F: FnOnce() -> T>(f: F) -> T {
1302 std::env::set_var("DD_ENV", "production");
1303 std::env::set_var("DD_SERVICE", "service");
1304 std::env::set_var("DD_VERSION", "0.0.1");
1305 let t = f();
1306 std::env::remove_var("DD_ENV");
1307 std::env::remove_var("DD_SERVICE");
1308 std::env::remove_var("DD_VERSION");
1309 t
1310 }
1311}
1312
1313#[cfg(all(feature = "unstable", test))]
1314mod bench {
1315 extern crate test;
1316
1317 use super::*;
1318
1319 use self::test::Bencher;
1320
1321 #[bench]
1322 fn bench_incr(b: &mut Bencher) {
1323 let options = Options::default();
1324 let client = Client::new(options).unwrap();
1325 let tags = &["name1:value1"];
1326 b.iter(|| {
1327 client.incr("bench.incr", tags).unwrap();
1328 })
1329 }
1330
1331 #[bench]
1332 fn bench_decr(b: &mut Bencher) {
1333 let options = Options::default();
1334 let client = Client::new(options).unwrap();
1335 let tags = &["name1:value1"];
1336 b.iter(|| {
1337 client.decr("bench.decr", tags).unwrap();
1338 })
1339 }
1340
1341 #[bench]
1342 fn bench_count(b: &mut Bencher) {
1343 let options = Options::default();
1344 let client = Client::new(options).unwrap();
1345 let tags = &["name1:value1"];
1346 let mut i = 0;
1347 b.iter(|| {
1348 client.count("bench.count", i, tags).unwrap();
1349 i += 1;
1350 })
1351 }
1352
1353 #[bench]
1354 fn bench_timing(b: &mut Bencher) {
1355 let options = Options::default();
1356 let client = Client::new(options).unwrap();
1357 let tags = &["name1:value1"];
1358 let mut i = 0;
1359 b.iter(|| {
1360 client.timing("bench.timing", i, tags).unwrap();
1361 i += 1;
1362 })
1363 }
1364
1365 #[bench]
1366 fn bench_gauge(b: &mut Bencher) {
1367 let options = Options::default();
1368 let client = Client::new(options).unwrap();
1369 let tags = vec!["name1:value1"];
1370 let mut i = 0;
1371 b.iter(|| {
1372 client.gauge("bench.guage", &i.to_string(), &tags).unwrap();
1373 i += 1;
1374 })
1375 }
1376
1377 #[bench]
1378 fn bench_histogram(b: &mut Bencher) {
1379 let options = Options::default();
1380 let client = Client::new(options).unwrap();
1381 let tags = vec!["name1:value1"];
1382 let mut i = 0;
1383 b.iter(|| {
1384 client
1385 .histogram("bench.histogram", &i.to_string(), &tags)
1386 .unwrap();
1387 i += 1;
1388 })
1389 }
1390
1391 #[bench]
1392 fn bench_distribution(b: &mut Bencher) {
1393 let options = Options::default();
1394 let client = Client::new(options).unwrap();
1395 let tags = vec!["name1:value1"];
1396 let mut i = 0;
1397 b.iter(|| {
1398 client
1399 .distribution("bench.distribution", &i.to_string(), &tags)
1400 .unwrap();
1401 i += 1;
1402 })
1403 }
1404
1405 #[bench]
1406 fn bench_set(b: &mut Bencher) {
1407 let options = Options::default();
1408 let client = Client::new(options).unwrap();
1409 let tags = vec!["name1:value1"];
1410 let mut i = 0;
1411 b.iter(|| {
1412 client.set("bench.set", &i.to_string(), &tags).unwrap();
1413 i += 1;
1414 })
1415 }
1416
1417 #[bench]
1418 fn bench_service_check(b: &mut Bencher) {
1419 let options = Options::default();
1420 let client = Client::new(options).unwrap();
1421 let tags = vec!["name1:value1"];
1422 let all_options = ServiceCheckOptions {
1423 hostname: Some("my-host.localhost"),
1424 timestamp: Some(1510326433),
1425 message: Some("Message about the check or service"),
1426 };
1427 b.iter(|| {
1428 client
1429 .service_check(
1430 "bench.service_check",
1431 ServiceStatus::Critical,
1432 &tags,
1433 Some(all_options),
1434 )
1435 .unwrap();
1436 })
1437 }
1438
1439 #[bench]
1440 fn bench_event(b: &mut Bencher) {
1441 let options = Options::default();
1442 let client = Client::new(options).unwrap();
1443 let tags = vec!["name1:value1"];
1444 b.iter(|| {
1445 client
1446 .event("Test Event Title", "Test Event Message", &tags, None)
1447 .unwrap();
1448 })
1449 }
1450
1451 fn bench_event_options(b: &mut Bencher) {
1452 let options = Options::default();
1453 let client = Client::new(options).unwrap();
1454 let tags = vec!["name1:value1"];
1455 let event_options = EventOptions::new()
1456 .with_timestamp(1638480000)
1457 .with_hostname("localhost")
1458 .with_priority(EventPriority::Normal)
1459 .with_alert_type(EventAlertType::Error);
1460
1461 b.iter(|| {
1462 client
1463 .event(
1464 "Test Event Title",
1465 "Test Event Message",
1466 &tags,
1467 Some(event_options),
1468 )
1469 .unwrap();
1470 })
1471 }
1472}