libhoney/
client.rs

1/*! Client is the interface to create new builders, events and send the latter somewhere using `Transmission`.
2
3*/
4use std::collections::HashMap;
5
6use crossbeam_channel::Receiver;
7use log::info;
8use serde_json::Value;
9
10use crate::errors::Result;
11use crate::fields::FieldHolder;
12use crate::response::Response;
13use crate::sender::Sender;
14use crate::Event;
15use crate::{Builder, DynamicFieldFunc};
16
17const DEFAULT_API_HOST: &str = "https://api.honeycomb.io";
18const DEFAULT_API_KEY: &str = "";
19const DEFAULT_DATASET: &str = "librust-dataset";
20const DEFAULT_SAMPLE_RATE: usize = 1;
21
22/// Options is a subset of the global libhoney config that focuses on the
23/// configuration of the client itself.
24#[derive(Debug, Clone)]
25pub struct Options {
26    /// api_key is the Honeycomb authentication token. If it is specified during libhoney
27    /// initialization, it will be used as the default API key for all events. If absent,
28    /// API key must be explicitly set on a builder or event. Find your team's API keys at
29    /// https://ui.honeycomb.io/account
30    pub api_key: String,
31
32    /// api_host is the hostname for the Honeycomb API server to which to send this
33    /// event. default: https://api.honeycomb.io/
34    pub api_host: String,
35
36    /// dataset is the name of the Honeycomb dataset to which to send these events.  If it
37    /// is specified during libhoney initialization, it will be used as the default
38    /// dataset for all events. If absent, dataset must be explicitly set on a builder or
39    /// event.
40    pub dataset: String,
41
42    /// sample_rate is the rate at which to sample this event. Default is 1, meaning no
43    /// sampling. If you want to send one event out of every 250 times Send() is called,
44    /// you would specify 250 here.
45    pub sample_rate: usize,
46}
47
48impl Default for Options {
49    fn default() -> Self {
50        Self {
51            api_key: DEFAULT_API_KEY.to_string(),
52            dataset: DEFAULT_DATASET.to_string(),
53            api_host: DEFAULT_API_HOST.to_string(),
54            sample_rate: DEFAULT_SAMPLE_RATE,
55        }
56    }
57}
58
59/// Client represents an object that can create new builders and events and send them
60/// somewhere.
61#[derive(Debug, Clone)]
62pub struct Client<T: Sender> {
63    pub(crate) options: Options,
64    /// transmission mechanism for the client
65    pub transmission: T,
66
67    builder: Builder,
68}
69
70impl<T> Client<T>
71where
72    T: Sender,
73{
74    /// new creates a new Client with the provided Options and initialised
75    /// Transmission.
76    ///
77    /// Once populated, it auto starts the transmission background threads and is ready to
78    /// send events.
79    pub fn new(options: Options, transmission: T) -> Self {
80        info!("Creating honey client");
81
82        let mut c = Self {
83            transmission,
84            options: options.clone(),
85            builder: Builder::new(options),
86        };
87        c.start();
88        c
89    }
90
91    fn start(&mut self) {
92        self.transmission.start();
93    }
94
95    /// add adds its data to the Client's scope. It adds all fields in a struct or all
96    /// keys in a map as individual Fields. These metrics will be inherited by all
97    /// builders and events.
98    pub fn add(&mut self, data: HashMap<String, Value>) {
99        self.builder.add(data);
100    }
101
102    /// add_field adds a Field to the Client's scope. This metric will be inherited by all
103    /// builders and events.
104    pub fn add_field(&mut self, name: &str, value: Value) {
105        self.builder.add_field(name, value);
106    }
107
108    /// add_dynamic_field takes a field name and a function that will generate values for
109    /// that metric. The function is called once every time a new_event() is created and
110    /// added as a field (with name as the key) to the newly created event.
111    pub fn add_dynamic_field(&mut self, name: &str, func: DynamicFieldFunc) {
112        self.builder.add_dynamic_field(name, func);
113    }
114
115    /// close waits for all in-flight messages to be sent. You should call close() before
116    /// app termination.
117    pub fn close(mut self) -> Result<()> {
118        info!("closing libhoney client");
119        self.transmission.stop()
120    }
121
122    /// flush closes and reopens the Transmission, ensuring events are sent without
123    /// waiting on the batch to be sent asyncronously. Generally, it is more efficient to
124    /// rely on asyncronous batches than to call Flush, but certain scenarios may require
125    /// Flush if asynchronous sends are not guaranteed to run (i.e. running in AWS Lambda)
126    /// Flush is not thread safe - use it only when you are sure that no other parts of
127    /// your program are calling Send
128    pub fn flush(&mut self) -> Result<()> {
129        info!("flushing libhoney client");
130        self.transmission.stop()?;
131        self.transmission.start();
132        Ok(())
133    }
134
135    /// new_builder creates a new event builder. The builder inherits any Dynamic or
136    /// Static Fields present in the Client's scope.
137    pub fn new_builder(&self) -> Builder {
138        self.builder.clone()
139    }
140
141    /// new_event creates a new event prepopulated with any Fields present in the Client's
142    /// scope.
143    pub fn new_event(&self) -> Event {
144        self.builder.new_event()
145    }
146
147    /// responses returns a receiver channel with responses
148    pub fn responses(&self) -> Receiver<Response> {
149        self.transmission.responses()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{Client, FieldHolder, Options, Value};
156    use crate::transmission::{self, Transmission};
157
158    #[test]
159    fn test_init() {
160        let client = Client::new(
161            Options::default(),
162            Transmission::new(transmission::Options::default()).unwrap(),
163        );
164        client.close().unwrap();
165    }
166
167    #[test]
168    fn test_flush() {
169        use reqwest::StatusCode;
170        use serde_json::json;
171
172        let api_host = &mockito::server_url();
173        let _m = mockito::mock(
174            "POST",
175            mockito::Matcher::Regex(r"/1/batch/(.*)$".to_string()),
176        )
177        .with_status(200)
178        .with_header("content-type", "application/json")
179        .with_body("[{ \"status\": 202 }]")
180        .create();
181
182        let mut client = Client::new(
183            Options {
184                api_key: "some api key".to_string(),
185                api_host: api_host.to_string(),
186                ..Options::default()
187            },
188            Transmission::new(transmission::Options::default()).unwrap(),
189        );
190
191        let mut event = client.new_event();
192        event.add_field("some_field", Value::String("some_value".to_string()));
193        event.metadata = Some(json!("some metadata in a string"));
194        event.send(&mut client).unwrap();
195
196        let response = client.responses().iter().next().unwrap();
197        assert_eq!(response.status_code, Some(StatusCode::ACCEPTED));
198        assert_eq!(response.metadata, Some(json!("some metadata in a string")));
199
200        client.flush().unwrap();
201
202        event = client.new_event();
203        event.add_field("some_field", Value::String("some_value".to_string()));
204        event.metadata = Some(json!("some metadata in a string"));
205        event.send(&mut client).unwrap();
206
207        let response = client.responses().iter().next().unwrap();
208        assert_eq!(response.status_code, Some(StatusCode::ACCEPTED));
209        assert_eq!(response.metadata, Some(json!("some metadata in a string")));
210
211        client.close().unwrap();
212    }
213
214    #[test]
215    fn test_send_without_api_key() {
216        use serde_json::json;
217
218        use crate::errors::ErrorKind;
219
220        let api_host = &mockito::server_url();
221        let _m = mockito::mock(
222            "POST",
223            mockito::Matcher::Regex(r"/1/batch/(.*)$".to_string()),
224        )
225        .with_status(200)
226        .with_header("content-type", "application/json")
227        .with_body("[{ \"status\": 202 }]")
228        .create();
229
230        let mut client = Client::new(
231            Options {
232                api_host: api_host.to_string(),
233                ..Options::default()
234            },
235            Transmission::new(transmission::Options::default()).unwrap(),
236        );
237
238        let mut event = client.new_event();
239        event.add_field("some_field", Value::String("some_value".to_string()));
240        event.metadata = Some(json!("some metadata in a string"));
241        let err = event.send(&mut client).err().unwrap();
242
243        assert_eq!(err.kind, ErrorKind::MissingOption);
244        assert_eq!(
245            err.message,
246            "missing option 'api_key', can't send to Honeycomb"
247        );
248        client.close().unwrap();
249    }
250}