extern crate backtrace;
extern crate time;
use std::thread;
use std::sync::mpsc::{channel, Sender, Receiver};
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::fmt::Debug;
use std::time::Duration;
use std::io::Read;
use std::env;
#[macro_use]
extern crate hyper;
use hyper::Client;
use hyper::header::{Headers, ContentType};
extern crate chrono;
use chrono::offset::utc::UTC;
struct ThreadState<'a> {
alive: &'a mut Arc<AtomicBool>,
}
impl<'a> ThreadState<'a> {
fn set_alive(&self) {
self.alive.store(true, Ordering::Relaxed);
}
}
impl<'a> Drop for ThreadState<'a> {
fn drop(&mut self) {
self.alive.store(false, Ordering::Relaxed);
}
}
pub trait WorkerClosure<T, P>: Fn(&P, T) -> () + Send + Sync {}
impl<T, F, P> WorkerClosure<T, P> for F where F: Fn(&P, T) -> () + Send + Sync {}
pub struct SingleWorker<T: 'static + Send, P: Clone + Send> {
parameters: P,
f: Arc<Box<WorkerClosure<T, P, Output = ()>>>,
receiver: Arc<Mutex<Receiver<T>>>,
sender: Mutex<Sender<T>>,
alive: Arc<AtomicBool>,
}
impl<T: 'static + Debug + Send, P: 'static + Clone + Send> SingleWorker<T, P> {
pub fn new(parameters: P, f: Box<WorkerClosure<T, P, Output = ()>>) -> SingleWorker<T, P> {
let (sender, receiver) = channel::<T>();
let worker = SingleWorker {
parameters: parameters,
f: Arc::new(f),
receiver: Arc::new(Mutex::new(receiver)),
sender: Mutex::new(sender),
alive: Arc::new(AtomicBool::new(true)),
};
SingleWorker::spawn_thread(&worker);
worker
}
fn is_alive(&self) -> bool {
self.alive.clone().load(Ordering::Relaxed)
}
fn spawn_thread(worker: &SingleWorker<T, P>) {
let mut alive = worker.alive.clone();
let f = worker.f.clone();
let receiver = worker.receiver.clone();
let parameters = worker.parameters.clone();
thread::spawn(move || {
let state = ThreadState { alive: &mut alive };
state.set_alive();
let lock = match receiver.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
loop {
match lock.recv() {
Ok(value) => f(¶meters, value),
Err(_) => {
thread::yield_now();
}
};
}
});
while !worker.is_alive() {
thread::yield_now();
}
}
pub fn work_with(&self, msg: T) {
let alive = self.is_alive();
if !alive {
SingleWorker::spawn_thread(self);
}
let lock = match self.sender.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let _ = lock.send(msg);
}
}
pub trait ToJsonString {
fn to_json_string(&self) -> String;
}
impl ToJsonString for String {
fn to_json_string(&self) -> String {
let mut s = String::new();
s.push_str("\"");
let mut start = 0;
for (i, byte) in self.bytes().enumerate() {
let escaped = match byte {
b'"' => "\\\"",
b'\\' => "\\\\",
b'\x00' => "\\u0000",
b'\x01' => "\\u0001",
b'\x02' => "\\u0002",
b'\x03' => "\\u0003",
b'\x04' => "\\u0004",
b'\x05' => "\\u0005",
b'\x06' => "\\u0006",
b'\x07' => "\\u0007",
b'\x08' => "\\b",
b'\t' => "\\t",
b'\n' => "\\n",
b'\x0b' => "\\u000b",
b'\x0c' => "\\f",
b'\r' => "\\r",
b'\x0e' => "\\u000e",
b'\x0f' => "\\u000f",
b'\x10' => "\\u0010",
b'\x11' => "\\u0011",
b'\x12' => "\\u0012",
b'\x13' => "\\u0013",
b'\x14' => "\\u0014",
b'\x15' => "\\u0015",
b'\x16' => "\\u0016",
b'\x17' => "\\u0017",
b'\x18' => "\\u0018",
b'\x19' => "\\u0019",
b'\x1a' => "\\u001a",
b'\x1b' => "\\u001b",
b'\x1c' => "\\u001c",
b'\x1d' => "\\u001d",
b'\x1e' => "\\u001e",
b'\x1f' => "\\u001f",
b'\x7f' => "\\u007f",
_ => {
continue;
}
};
if start < i {
s.push_str(&self[start..i]);
}
s.push_str(escaped);
start = i + 1;
}
if start != self.len() {
s.push_str(&self[start..]);
}
s.push_str("\"");
s
}
}
#[derive(Debug,Clone)]
pub struct StackFrame {
filename: String,
function: String,
lineno: u32,
}
#[derive(Debug,Clone)]
pub struct Event {
event_id: String, message: String, timestamp: String, level: String, logger: String, platform: String, sdk: SDK,
device: Device,
culprit: Option<String>, server_name: Option<String>, stack_trace: Option<Vec<StackFrame>>, release: Option<String>, tags: Vec<(String, String)>, environment: Option<String>, modules: Vec<(String, String)>, extra: Vec<(String, String)>, fingerprint: Vec<String>, }
impl Event {
pub fn new(logger: &str,
level: &str,
message: &str,
culprit: Option<&str>,
fingerprint: Option<Vec<String>>,
server_name: Option<&str>,
stack_trace: Option<Vec<StackFrame>>,
release: Option<&str>,
environment: Option<&str>)
-> Event {
Event {
event_id: "".to_string(),
message: message.to_owned(),
timestamp: UTC::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
level: level.to_owned(),
logger: logger.to_owned(),
platform: "other".to_string(),
sdk: SDK {
name: "rust-sentry".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
device: Device {
name: env::var_os("OSTYPE")
.and_then(|cs| cs.into_string().ok())
.unwrap_or("".to_string()),
version: "".to_string(),
build: "".to_string(),
},
culprit: culprit.map(|c| c.to_owned()),
server_name: server_name.map(|c| c.to_owned()),
stack_trace: stack_trace,
release: release.map(|c| c.to_owned()),
tags: vec![],
environment: environment.map(|c| c.to_owned()),
modules: vec![],
extra: vec![],
fingerprint: fingerprint.unwrap_or(vec![]),
}
}
pub fn push_tag(&mut self, key: String, value: String) {
self.tags.push((key, value));
}
}
impl ToJsonString for Event {
fn to_json_string(&self) -> String {
let mut s = String::new();
s.push_str("{");
s.push_str(&format!("\"event_id\":{},", self.event_id.to_json_string()));
s.push_str(&format!("\"message\":{},", self.message.to_json_string()));
s.push_str(&format!("\"timestamp\":\"{}\",", self.timestamp));
s.push_str(&format!("\"level\":\"{}\",", self.level));
s.push_str(&format!("\"logger\":\"{}\",", self.logger));
s.push_str(&format!("\"platform\":\"{}\",", self.platform));
s.push_str(&format!("\"sdk\": {},", self.sdk.to_json_string()));
s.push_str(&format!("\"device\": {}", self.device.to_json_string()));
if let Some(ref culprit) = self.culprit {
s.push_str(&format!(",\"culprit\": {}", culprit.to_json_string()));
}
if let Some(ref server_name) = self.server_name {
s.push_str(&format!(",\"server_name\":\"{}\"", server_name));
}
if let Some(ref release) = self.release {
s.push_str(&format!(",\"release\":\"{}\"", release));
}
if self.tags.len() > 0 {
let last_index = self.tags.len() - 1;
s.push_str(",\"tags\":{");
for (index, tag) in self.tags.iter().enumerate() {
s.push_str(&format!("\"{}\":\"{}\"", tag.0, tag.1));
if index != last_index {
s.push_str(",");
}
}
s.push_str("}");
}
if let Some(ref environment) = self.environment {
s.push_str(&format!(",\"environment\":\"{}\"", environment));
}
if self.modules.len() > 0 {
s.push_str(",\"modules\":\"{");
for module in self.modules.iter() {
s.push_str(&format!("\"{}\":\"{}\"", module.0, module.1));
}
s.push_str("}");
}
if self.extra.len() > 0 {
s.push_str(",\"extra\":\"{");
for extra in self.extra.iter() {
s.push_str(&format!("\"{}\":\"{}\"", extra.0, extra.1));
}
s.push_str("}");
}
if let Some(ref stack_trace) = self.stack_trace {
s.push_str(",\"stacktrace\":{\"frames\":[");
let mut is_first = true;
for stack_frame in stack_trace.iter().rev() {
if !is_first {
s.push_str(",");
} else {
is_first = false;
}
s.push_str(&format!("{{\"filename\":\"{}\",\"function\":\"{}\",\"lineno\":{}}}",
stack_frame.filename,
stack_frame.function,
stack_frame.lineno));
}
s.push_str("]}");
}
if self.fingerprint.len() > 0 {
s.push_str(",\"fingerprint\":[");
let mut comma = false;
for fingerprint in self.fingerprint.iter() {
if comma {
s.push_str(",");
}
s.push_str(&fingerprint.to_json_string());
comma = true;
}
s.push_str("]");
}
s.push_str("}");
s
}
}
#[derive(Debug,Clone)]
pub struct SDK {
name: String,
version: String,
}
impl ToJsonString for SDK {
fn to_json_string(&self) -> String {
format!("{{\"name\":\"{}\",\"version\":\"{}\"}}",
self.name,
self.version)
}
}
#[derive(Debug,Clone)]
pub struct Device {
name: String,
version: String,
build: String,
}
impl ToJsonString for Device {
fn to_json_string(&self) -> String {
format!("{{\"name\":\"{}\",\"version\":\"{}\",\"build\":\"{}\"}}",
self.name,
self.version,
self.build)
}
}
#[derive(Debug,Clone)]
pub struct SentryCredential {
pub key: String,
pub secret: String,
pub host: String,
pub project_id: String,
}
pub struct Sentry {
server_name: String,
release: String,
environment: String,
worker: Arc<SingleWorker<Event, SentryCredential>>,
}
header! { (XSentryAuth, "X-Sentry-Auth") => [String] }
impl Sentry {
pub fn new(server_name: String,
release: String,
environment: String,
credential: SentryCredential)
-> Sentry {
let worker = SingleWorker::new(credential,
Box::new(move |credential, e| {
Sentry::post(credential, &e);
}));
Sentry {
server_name: server_name,
release: release,
environment: environment,
worker: Arc::new(worker),
}
}
fn post(credential: &SentryCredential, e: &Event) {
let mut headers = Headers::new();
let timestamp = time::get_time().sec.to_string();
let xsentryauth = format!("Sentry sentry_version=7,sentry_client=rust-sentry/{},\
sentry_timestamp={},sentry_key={},sentry_secret={}",
env!("CARGO_PKG_VERSION"),
timestamp,
credential.key,
credential.secret);
headers.set(XSentryAuth(xsentryauth));
headers.set(ContentType::json());
let body = e.to_json_string();
println!("Sentry body {}", body);
let mut client = Client::new();
client.set_read_timeout(Some(Duration::new(5, 0)));
client.set_write_timeout(Some(Duration::new(5, 0)));
let url = format!("https://{}:{}@{}/api/{}/store/",
credential.key,
credential.secret,
credential.host,
credential.project_id);
let mut res = client.post(&url)
.headers(headers)
.body(&body)
.send()
.unwrap();
let mut body = String::new();
res.read_to_string(&mut body).unwrap();
println!("Sentry Response {}", body);
}
pub fn log_event(&self, e: Event) {
self.worker.work_with(e);
}
pub fn register_panic_handler<F>(&self, maybe_f: Option<F>)
where F: Fn(&std::panic::PanicInfo) + 'static + Sync + Send
{
let server_name = self.server_name.clone();
let release = self.release.clone();
let environment = self.environment.clone();
let worker = self.worker.clone();
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo| {
let location = info.location()
.map(|l| format!("{}: {}", l.file(), l.line()))
.unwrap_or("NA".to_string());
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => {
match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "Box<Any>",
}
}
};
let mut frames = vec![];
backtrace::trace(|frame: &backtrace::Frame| {
backtrace::resolve(frame.ip(), |symbol| {
let name = symbol.name()
.map_or("unresolved symbol".to_string(), |name| name.to_string());
let filename = symbol.filename()
.map_or("".to_string(), |sym| format!("{:?}", sym));
let lineno = symbol.lineno().unwrap_or(0);
frames.push(StackFrame {
filename: filename,
function: name,
lineno: lineno,
});
});
true });
let e = Event::new("panic",
"fatal",
msg,
Some(&location),
None,
Some(&server_name),
Some(frames),
Some(&release),
Some(&environment));
let _ = worker.work_with(e.clone());
if let Some(ref f) = maybe_f {
f(info);
}
}));
}
pub fn unregister_panic_handler(&self) {
let _ = std::panic::take_hook();
}
pub fn fatal(&self, logger: &str, message: &str, culprit: Option<&str>) {
self.log(logger, "fatal", message, culprit, None);
}
pub fn error(&self, logger: &str, message: &str, culprit: Option<&str>) {
self.log(logger, "error", message, culprit, None);
}
pub fn warning(&self, logger: &str, message: &str, culprit: Option<&str>) {
self.log(logger, "warning", message, culprit, None);
}
pub fn info(&self, logger: &str, message: &str, culprit: Option<&str>) {
self.log(logger, "info", message, culprit, None);
}
pub fn debug(&self, logger: &str, message: &str, culprit: Option<&str>) {
self.log(logger, "debug", message, culprit, None);
}
fn log(&self,
logger: &str,
level: &str,
message: &str,
culprit: Option<&str>,
fingerprint: Option<Vec<String>>) {
let fpr = match fingerprint {
Some(f) => f,
None => {
vec![logger.to_string(),
level.to_string(),
culprit.map(|c| c.to_string()).unwrap_or("".to_string())]
}
};
self.worker.work_with(Event::new(logger,
level,
message,
culprit,
Some(fpr),
Some(&self.server_name),
None,
Some(&self.release),
Some(&self.environment)));
}
}
#[cfg(test)]
mod tests {
use super::SingleWorker;
use super::Sentry;
use super::ToJsonString;
use super::SentryCredential;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::channel;
use std::thread;
use std::panic::PanicInfo;
#[test]
fn test_to_json_string_for_string() {
let uut1 = "".to_owned();
assert_eq!(uut1.to_json_string(), "\"\"");
let uut2 = "plain_string".to_owned();
assert_eq!(uut2.to_json_string(), "\"plain_string\"");
let uut3 = "string\"with escapes\"".to_owned();
assert_eq!(uut3.to_json_string(), "\"string\\\"with escapes\\\"\"");
let uut4 = "string with null\x00".to_owned();
assert_eq!(uut4.to_json_string(), "\"string with null\\u0000\"");
}
#[test]
fn it_should_pass_value_to_worker_thread() {
let (sender, receiver) = channel();
let s = Mutex::new(sender);
let worker = SingleWorker::new("",
Box::new(move |_, v| {
let _ = s.lock().unwrap().send(v);
}));
let v = "Value";
worker.work_with(v);
let recv_v = receiver.recv().ok();
assert!(recv_v == Some(v));
}
#[test]
fn it_should_pass_value_event_after_thread_panic() {
let (sender, receiver) = channel();
let s = Mutex::new(sender);
let i = AtomicUsize::new(0);
let worker = SingleWorker::new("",
Box::new(move |_, v| {
let lock = match s.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let _ = lock.send(v);
i.fetch_add(1, Ordering::SeqCst);
if i.load(Ordering::Relaxed) == 2 {
panic!("PanicTesting");
}
}));
let v0 = "Value0";
let v1 = "Value1";
let v2 = "Value2";
let v3 = "Value3";
worker.work_with(v0);
worker.work_with(v1);
let recv_v0 = receiver.recv().ok();
let recv_v1 = receiver.recv().ok();
while worker.is_alive() {
thread::yield_now();
}
worker.work_with(v2);
worker.work_with(v3);
let recv_v2 = receiver.recv().ok();
let recv_v3 = receiver.recv().ok();
assert!(recv_v0 == Some(v0));
assert!(recv_v1 == Some(v1));
assert!(recv_v2 == Some(v2));
assert!(recv_v3 == Some(v3));
}
#[test]
fn it_registrer_panic_handler() {
let sentry = Sentry::new("Server Name".to_string(),
"release".to_string(),
"test_env".to_string(),
SentryCredential {
key: "xx".to_string(),
secret: "xx".to_string(),
host: "app.getsentry.com".to_string(),
project_id: "xx".to_string(),
});
let (sender, receiver) = channel();
let s = Mutex::new(sender);
sentry.register_panic_handler(Some(move |_: &PanicInfo| -> () {
let lock = match s.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let _ = lock.send(true);
}));
let t1 = thread::spawn(|| {
panic!("Panic Handler Testing");
});
let _ = t1.join();
assert_eq!(receiver.recv().unwrap(), true);
sentry.unregister_panic_handler();
}
#[test]
fn it_share_sentry_accross_threads() {
let sentry = Arc::new(Sentry::new("Server Name".to_string(),
"release".to_string(),
"test_env".to_string(),
SentryCredential {
key: "xx".to_string(),
secret: "xx".to_string(),
host: "app.getsentry.com".to_string(),
project_id: "xx".to_string(),
}));
let sentry1 = sentry.clone();
let t1 = thread::spawn(move || sentry1.server_name.clone());
let sentry2 = sentry.clone();
let t2 = thread::spawn(move || sentry2.server_name.clone());
let r1 = t1.join().unwrap();
let r2 = t2.join().unwrap();
assert!(r1 == sentry.server_name);
assert!(r2 == sentry.server_name);
}
}