use std::sync::Arc;
use std::sync::RwLock;
use crate::configuration::Configuration;
use crate::event_builder::Event;
pub struct Client {
configuration: Arc<RwLock<Configuration>>,
agent: ureq::Agent,
}
impl Client {
pub fn new(configuration: Arc<RwLock<Configuration>>) -> Self {
let timeout = configuration.read().unwrap().timeout;
let agent = ureq::AgentBuilder::new().timeout(timeout).build();
Client {
configuration,
agent,
}
}
pub fn deliver(&self, event: &Event) -> bool {
let (uri, api_key) = {
let config = self.configuration.read().unwrap();
(config.ingestion_uri(), config.api_key())
};
self.post(uri, api_key, &event.to_json())
}
pub fn deliver_performance_samples(&self, body: &str) -> bool {
let (uri, api_key) = {
let config = self.configuration.read().unwrap();
(config.performance_samples_uri(), config.api_key())
};
self.post(uri, api_key, body)
}
pub fn deliver_spans(&self, body: &str) -> bool {
let (uri, api_key) = {
let config = self.configuration.read().unwrap();
(config.spans_uri(), config.api_key())
};
self.post(uri, api_key, body)
}
pub fn deliver_metrics(&self, body: &str) -> bool {
let (uri, api_key) = {
let config = self.configuration.read().unwrap();
(config.custom_metrics_uri(), config.api_key())
};
self.post(uri, api_key, body)
}
pub fn deliver_infrastructure_metrics(&self, body: &str) -> bool {
let (uri, api_key) = {
let config = self.configuration.read().unwrap();
(config.infrastructure_metrics_uri(), config.api_key())
};
self.post(uri, api_key, body)
}
fn post(&self, uri: Option<String>, api_key: Option<String>, body: &str) -> bool {
let (Some(uri), Some(api_key)) = (uri, api_key) else {
return false;
};
let result = self
.agent
.post(&uri)
.set("Authorization", &format!("Bearer {api_key}"))
.set("Content-Type", "application/json")
.send_string(body);
match result {
Ok(response) => (200..300).contains(&response.status()),
Err(_) => false,
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::mpsc;
use std::time::Duration;
use super::*;
fn test_event() -> Event {
Event {
exception_class: "std::io::Error".to_string(),
message: "boom".to_string(),
backtrace: vec![],
occurred_at: "2024-01-15T10:30:00Z".to_string(),
environment: "production".to_string(),
release: None,
server_name: None,
context: HashMap::new(),
tags: HashMap::new(),
sdk_name: "rust".to_string(),
user: None,
breadcrumbs: vec![],
sql_objects: None,
sql_statement: None,
trace_id: None,
}
}
fn serve_once(status: u16) -> (String, mpsc::Receiver<(String, String)>) {
use std::io::Write;
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let received = crate::test_support::read_full_request(&mut stream);
let text = String::from_utf8_lossy(&received).into_owned();
let mut lines = text.lines();
let request_line = lines.next().unwrap_or("").to_string();
let auth_header = lines
.find(|l| l.to_lowercase().starts_with("authorization:"))
.unwrap_or("")
.to_string();
let _ = tx.send((request_line, auth_header));
let body = "{}";
let response = format!(
"HTTP/1.1 {status} OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
let _ = stream.write_all(response.as_bytes());
});
(format!("http://{addr}/api/v1/events"), rx)
}
#[test]
fn deliver_sends_authorized_request_and_returns_true_on_2xx() {
let (base_uri, rx) = serve_once(200);
let dsn = base_uri.replacen("http://", "http://secret-key@", 1);
let mut config = Configuration::new();
config.dsn = Some(dsn);
config.timeout = Duration::from_secs(2);
let client = Client::new(Arc::new(RwLock::new(config)));
let ok = client.deliver(&test_event());
assert!(ok);
let (request_line, auth_header) = rx
.recv_timeout(Duration::from_secs(2))
.expect("server never received a request");
assert!(
request_line.starts_with("POST "),
"request line = {request_line:?}"
);
assert!(
auth_header.to_lowercase().contains("bearer secret-key"),
"authorization header = {auth_header:?}"
);
}
#[test]
fn deliver_returns_false_on_non_2xx() {
let (base_uri, _rx) = serve_once(500);
let dsn = base_uri.replacen("http://", "http://key@", 1);
let mut config = Configuration::new();
config.dsn = Some(dsn);
config.timeout = Duration::from_secs(2);
let client = Client::new(Arc::new(RwLock::new(config)));
assert!(!client.deliver(&test_event()));
}
#[test]
fn deliver_returns_false_when_unreachable() {
let mut config = Configuration::new();
config.dsn = Some("http://key@127.0.0.1:1/events".to_string());
config.timeout = Duration::from_millis(200);
let client = Client::new(Arc::new(RwLock::new(config)));
assert!(!client.deliver(&test_event()));
}
#[test]
fn deliver_returns_false_with_no_dsn() {
let config = Configuration::new();
let client = Client::new(Arc::new(RwLock::new(config)));
assert!(!client.deliver(&test_event()));
}
}