├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── error.rs ├── error_message.rs ├── message.rs └── panic.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - stable 5 | - nightly 6 | 7 | cache: 8 | cargo: true 9 | 10 | script: 11 | - | 12 | cargo build && 13 | cargo test 14 | 15 | env: 16 | global: 17 | - RUST_BACKTRACE=1 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rollbar" 3 | version = "0.7.0" 4 | authors = ["Giovanni Capuano All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted 4 | provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 10 | conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 14 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 15 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 16 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 17 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 20 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rollbar-rs 2 | [![Build Status](https://travis-ci.org/benashford/rs-es.svg?branch=master)](https://travis-ci.org/benashford/rs-es) 3 | [![](https://meritbadge.herokuapp.com/rollbar)](https://crates.io/crates/rollbar) 4 | [![](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](https://roxasshadow.github.io/rollbar-rs) 5 | 6 | Track and report errors, exceptions and messages from your Rust application to [Rollbar](https://rollbar.com/). 7 | 8 | ## Usage 9 | 10 | ### Automatic logging 11 | `examples/panic.rs` will show you how to set a hook for all the 12 | [panic](https://doc.rust-lang.org/std/panic/fn.set_hook.html)s that your application could raise 13 | so that they will be handled automatically by `rollbar-rs` in order to be tracked on Rollbar. 14 | 15 | You can run it with `$ cargo run --example panic` if you remember to set the correct `access_token`. 16 | 17 | ### Manual logging 18 | Manual logging could be useful when you want to handle errors in your application but also notify Rollbar about them. 19 | 20 | `examples/error.rs` shows how to deal with errors, while `examples/message.rs` is for plain text reports. 21 | 22 | ### Customize the reports 23 | Check the [documentation](https://roxasshadow.github.io/rollbar-rs) to understand how you can add or modify 24 | one or more fields in the reports that will be sent to Rollbar. Generally, all the methods whose names starts 25 | with `with_` or `from_` is what you need. 26 | -------------------------------------------------------------------------------- /examples/error.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rollbar; 3 | extern crate backtrace; 4 | 5 | fn main() { 6 | let client = rollbar::Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 7 | 8 | match "笑".parse::() { 9 | Ok(_) => { println!("lolnope"); }, 10 | Err(e) => { let _ = report_error!(client, e).join(); } 11 | } 12 | 13 | /* // `report_error!` expands to the following code: 14 | * let backtrace = backtrace::Backtrace::new(); 15 | * let line = line!() - 2; 16 | * 17 | * client.build_report() 18 | * .from_error(&e) 19 | * .with_frame(rollbar::FrameBuilder::new() 20 | * .with_line_number(line) 21 | * .with_file_name(file!()) 22 | * .build()) 23 | * .with_backtrace(&backtrace) 24 | * .send(); 25 | * // If you want to customize the report, you might not want to use the macro. 26 | * // Join the thread only for testing purposes. 27 | */ 28 | } 29 | -------------------------------------------------------------------------------- /examples/error_message.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rollbar; 3 | extern crate backtrace; 4 | 5 | fn main() { 6 | let client = rollbar::Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 7 | let _ = report_error_message!(client, "_| ̄|○").join(); 8 | 9 | /* // `report_error_message!` expands to the following code: 10 | * let backtrace = backtrace::Backtrace::new(); 11 | * let line = line!(); 12 | * 13 | * client.build_report() 14 | * .from_error_message("_| ̄|○") 15 | * .with_frame(rollbar::FrameBuilder::new() 16 | * .with_line_number(line) 17 | * .with_file_name(file!()) 18 | * .build()) 19 | * .with_backtrace(&backtrace) 20 | * .send(); 21 | * // If you want to customize the report, you might not want to use the macro. 22 | * // Join the thread only for testing purposes. 23 | */ 24 | } 25 | -------------------------------------------------------------------------------- /examples/message.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rollbar; 3 | 4 | fn main() { 5 | let client = rollbar::Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 6 | let _ = report_message!(client, "hai").join(); 7 | 8 | /* // `report_message!` expands to the following code: 9 | * client.build_report() 10 | * .from_message("hai") 11 | * .with_level(rollbar::Level::INFO) 12 | * .send(); 13 | * // If you want to customize the message, you might not want to use the macro. 14 | * // Join the thread only for testing purposes. 15 | */ 16 | } 17 | -------------------------------------------------------------------------------- /examples/panic.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rollbar; 3 | extern crate backtrace; 4 | 5 | fn main() { 6 | let client = rollbar::Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 7 | report_panics!(client); 8 | 9 | /* // `report_panics!` expands to the following code: 10 | * std::panic::set_hook(Box::new(move |panic_info| { 11 | * let backtrace = backtrace::Backtrace::new(); 12 | * client.build_report() 13 | * .from_panic(panic_info) 14 | * .with_backtrace(&backtrace) 15 | * .send(); 16 | * })); 17 | * // If you want to customize the reports, you might not want to use the macro. 18 | * // Join the thread only for testing purposes. 19 | */ 20 | 21 | let zero = "0".parse::().unwrap(); // let's trick the lint a bit! 22 | let _ = 42/zero; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Track and report errors, exceptions and messages from your Rust application to Rollbar. 2 | 3 | pub extern crate backtrace; 4 | extern crate futures; 5 | extern crate hyper; 6 | extern crate hyper_tls; 7 | extern crate serde; 8 | #[macro_use] 9 | extern crate serde_derive; 10 | #[macro_use] 11 | extern crate serde_json; 12 | extern crate tokio; 13 | 14 | //use std::io::{self, Write}; 15 | use std::borrow::ToOwned; 16 | use std::sync::Arc; 17 | use std::{error, fmt, panic, thread}; 18 | 19 | use backtrace::Backtrace; 20 | //use hyper::client::HttpConnector; 21 | use hyper::rt::Future; 22 | use hyper::{Method, Request}; 23 | use hyper_tls::HttpsConnector; 24 | use tokio::runtime::current_thread; 25 | 26 | /// Report an error. Any type that implements `error::Error` is accepted. 27 | #[macro_export] 28 | macro_rules! report_error { 29 | ($client:ident, $err:ident) => {{ 30 | let backtrace = $crate::backtrace::Backtrace::new(); 31 | let line = line!() - 2; 32 | 33 | $client 34 | .build_report() 35 | .from_error(&$err) 36 | .with_frame( 37 | ::rollbar::FrameBuilder::new() 38 | .with_line_number(line) 39 | .with_file_name(file!()) 40 | .build(), 41 | ) 42 | .with_backtrace(&backtrace) 43 | .send() 44 | }}; 45 | } 46 | 47 | /// Report an error message. Any type that implements `fmt::Display` is accepted. 48 | #[macro_export] 49 | macro_rules! report_error_message { 50 | ($client:ident, $err:expr) => {{ 51 | let backtrace = $crate::backtrace::Backtrace::new(); 52 | let line = line!(); 53 | 54 | $client 55 | .build_report() 56 | .from_error_message(&$err) 57 | .with_frame( 58 | ::rollbar::FrameBuilder::new() 59 | .with_line_number(line) 60 | .with_file_name(file!()) 61 | .build(), 62 | ) 63 | .with_backtrace(&backtrace) 64 | .send() 65 | }}; 66 | } 67 | 68 | /// Set a global hook for the `panic`s your application could raise. 69 | #[macro_export] 70 | macro_rules! report_panics { 71 | ($client:ident) => {{ 72 | ::std::panic::set_hook(::std::boxed::Box::new(move |panic_info| { 73 | let backtrace = $crate::backtrace::Backtrace::new(); 74 | $client 75 | .build_report() 76 | .from_panic(panic_info) 77 | .with_backtrace(&backtrace) 78 | .send(); 79 | })) 80 | }}; 81 | } 82 | 83 | /// Send a plain text message to Rollbar with severity level `INFO`. 84 | #[macro_export] 85 | macro_rules! report_message { 86 | ($client:ident, $message:expr) => {{ 87 | $client 88 | .build_report() 89 | .from_message($message) 90 | .with_level(::rollbar::Level::INFO) 91 | .send() 92 | }}; 93 | } 94 | 95 | macro_rules! add_field { 96 | ($n:ident, $f:ident, $t:ty) => ( 97 | pub fn $n(&mut self, val: $t) -> &mut Self { 98 | self.$f = Some(val); 99 | self 100 | } 101 | ); 102 | } 103 | 104 | macro_rules! add_generic_field { 105 | ($n:ident, $f:ident, $t:path) => ( 106 | pub fn $n(&mut self, val: T) -> &mut Self { 107 | self.$f = Some(val.into()); 108 | self 109 | } 110 | ); 111 | } 112 | 113 | /// Variants for setting the severity level. 114 | /// If not specified, the default value is `ERROR`. 115 | #[derive(Serialize, Clone)] 116 | pub enum Level { 117 | CRITICAL, 118 | ERROR, 119 | WARNING, 120 | INFO, 121 | DEBUG, 122 | } 123 | 124 | impl<'a> From<&'a str> for Level { 125 | fn from(s: &'a str) -> Level { 126 | match s { 127 | "critical" => Level::CRITICAL, 128 | "warning" => Level::WARNING, 129 | "info" => Level::INFO, 130 | "debug" => Level::DEBUG, 131 | _ => Level::ERROR, 132 | } 133 | } 134 | } 135 | 136 | impl ToString for Level { 137 | fn to_string(&self) -> String { 138 | match self { 139 | &Level::CRITICAL => "critical".to_string(), 140 | &Level::ERROR => "error".to_string(), 141 | &Level::WARNING => "warning".to_string(), 142 | &Level::INFO => "info".to_string(), 143 | &Level::DEBUG => "debug".to_string(), 144 | } 145 | } 146 | } 147 | 148 | // https://rollbar.com/docs/api/items_post/ 149 | const URL: &'static str = "https://api.rollbar.com/api/1/item/"; 150 | 151 | /// Builder for a generic request to Rollbar. 152 | pub struct ReportBuilder<'a> { 153 | client: &'a Client, 154 | send_strategy: Option< 155 | Box< 156 | dyn Fn( 157 | Arc>>, 158 | String, 159 | ) -> thread::JoinHandle>, 160 | >, 161 | >, 162 | } 163 | 164 | /// Wrapper for a trace, payload of a single exception. 165 | #[derive(Serialize, Default, Debug)] 166 | struct Trace { 167 | frames: Vec, 168 | exception: Exception, 169 | } 170 | 171 | /// Wrapper for an exception, which describes the occurred error. 172 | #[derive(Serialize, Debug)] 173 | struct Exception { 174 | class: String, 175 | message: String, 176 | description: String, 177 | } 178 | 179 | impl Default for Exception { 180 | fn default() -> Self { 181 | Exception { 182 | class: "Generic".to_string(), 183 | message: String::new(), 184 | description: String::new(), 185 | } 186 | } 187 | } 188 | 189 | /// Builder for a frame. A collection of frames identifies a stack trace. 190 | #[derive(Serialize, Default, Clone, Debug)] 191 | pub struct FrameBuilder { 192 | /// The name of the file in which the error had origin. 193 | #[serde(rename = "filename")] 194 | file_name: String, 195 | 196 | /// The line of code in in which the error had origin. 197 | #[serde(skip_serializing_if = "Option::is_none")] 198 | #[serde(rename = "lineno")] 199 | line_number: Option, 200 | 201 | /// Set the number of the column in which an error occurred. 202 | #[serde(skip_serializing_if = "Option::is_none")] 203 | #[serde(rename = "colno")] 204 | column_number: Option, 205 | 206 | /// The method or the function name which caused caused the error. 207 | #[serde(skip_serializing_if = "Option::is_none")] 208 | #[serde(rename = "method")] 209 | function_name: Option, 210 | } 211 | 212 | impl<'a> FrameBuilder { 213 | /// Create a new FrameBuilder. 214 | pub fn new() -> Self { 215 | FrameBuilder { 216 | file_name: file!().to_owned(), 217 | ..Default::default() 218 | } 219 | } 220 | 221 | /// Tell the origin of the error by adding the file name to the report. 222 | pub fn with_file_name>(&'a mut self, file_name: T) -> &'a mut Self { 223 | self.file_name = file_name.into(); 224 | self 225 | } 226 | 227 | /// Set the number of the line in which an error occurred. 228 | add_field!(with_line_number, line_number, u32); 229 | 230 | /// Set the number of the column in which an error occurred. 231 | add_field!(with_column_number, column_number, u32); 232 | 233 | /// Set the method or the function name which caused caused the error. 234 | add_generic_field!(with_function_name, function_name, Into); 235 | 236 | /// Conclude the creation of the frame. 237 | pub fn build(&self) -> Self { 238 | self.to_owned() 239 | } 240 | } 241 | 242 | /// Builder specialized for reporting errors. 243 | #[derive(Serialize)] 244 | pub struct ReportErrorBuilder<'a> { 245 | #[serde(skip_serializing)] 246 | report_builder: &'a ReportBuilder<'a>, 247 | 248 | /// The trace containing the stack frames. 249 | trace: Trace, 250 | 251 | /// The severity level of the error. `Level::ERROR` is the default value. 252 | #[serde(skip_serializing_if = "Option::is_none")] 253 | level: Option, 254 | 255 | /// The title shown in the dashboard for this report. 256 | #[serde(skip_serializing_if = "Option::is_none")] 257 | title: Option, 258 | } 259 | 260 | impl<'a> ReportErrorBuilder<'a> { 261 | /// Attach a `backtrace::Backtrace` to the `description` of the report. 262 | pub fn with_backtrace(&mut self, backtrace: &'a Backtrace) -> &mut Self { 263 | self.trace.frames.extend( 264 | backtrace 265 | .frames() 266 | .iter() 267 | .flat_map(|frames| frames.symbols()) 268 | .map(|symbol| 269 | // http://alexcrichton.com/backtrace-rs/backtrace/struct.Symbol.html 270 | FrameBuilder { 271 | file_name: symbol.filename() 272 | .map_or_else(|| "".to_owned(), |p| format!("{}", p.display())), 273 | line_number: symbol.lineno(), 274 | function_name: symbol.name() 275 | .map(|s| format!("{}", s)), 276 | ..Default::default() 277 | }) 278 | .collect::>(), 279 | ); 280 | 281 | self 282 | } 283 | 284 | /// Add a new frame to the collection of stack frames. 285 | pub fn with_frame(&mut self, frame_builder: FrameBuilder) -> &mut Self { 286 | self.trace.frames.push(frame_builder); 287 | self 288 | } 289 | 290 | /// Set the security level of the report. `Level::ERROR` is the default value. 291 | add_generic_field!(with_level, level, Into); 292 | 293 | /// Set the title to show in the dashboard for this report. 294 | add_generic_field!(with_title, title, Into); 295 | 296 | /// Send the report to Rollbar. 297 | pub fn send(&mut self) -> thread::JoinHandle> { 298 | let client = self.report_builder.client; 299 | 300 | match self.report_builder.send_strategy { 301 | Some(ref send_strategy) => { 302 | let http_client = client.http_client.to_owned(); 303 | send_strategy(http_client, self.to_string()) 304 | } 305 | None => client.send(self.to_string()), 306 | } 307 | } 308 | } 309 | 310 | impl<'a> ToString for ReportErrorBuilder<'a> { 311 | fn to_string(&self) -> String { 312 | let client = self.report_builder.client; 313 | 314 | json!({ 315 | "access_token": client.access_token, 316 | "data": { 317 | "environment": client.environment, 318 | "body": { 319 | "trace": self.trace, 320 | }, 321 | "level": self.level 322 | .to_owned() 323 | .unwrap_or(Level::ERROR) 324 | .to_string(), 325 | "language": "rust", 326 | "title": self.title 327 | } 328 | }) 329 | .to_string() 330 | } 331 | } 332 | 333 | /// Builder specialized for reporting messages. 334 | pub struct ReportMessageBuilder<'a> { 335 | report_builder: &'a ReportBuilder<'a>, 336 | 337 | /// The message that must be reported. 338 | message: &'a str, 339 | 340 | /// The severity level of the error. `Level::ERROR` is the default value. 341 | level: Option, 342 | } 343 | 344 | impl<'a> ReportMessageBuilder<'a> { 345 | /// Set the security level of the report. `Level::ERROR` is the default value 346 | add_generic_field!(with_level, level, Into); 347 | 348 | /// Send the message to Rollbar. 349 | pub fn send(&mut self) -> thread::JoinHandle> { 350 | let client = self.report_builder.client; 351 | 352 | match self.report_builder.send_strategy { 353 | Some(ref send_strategy) => { 354 | let http_client = client.http_client.to_owned(); 355 | send_strategy(http_client, self.to_string()) 356 | } 357 | None => client.send(self.to_string()), 358 | } 359 | } 360 | } 361 | 362 | impl<'a> ToString for ReportMessageBuilder<'a> { 363 | fn to_string(&self) -> String { 364 | let client = self.report_builder.client; 365 | 366 | json!({ 367 | "access_token": client.access_token, 368 | "data": { 369 | "environment": client.environment, 370 | "body": { 371 | "message": { 372 | "body": self.message 373 | } 374 | }, 375 | "level": self.level 376 | .to_owned() 377 | .unwrap_or(Level::INFO) 378 | .to_string() 379 | } 380 | }) 381 | .to_string() 382 | } 383 | } 384 | 385 | impl<'a> ReportBuilder<'a> { 386 | /// To be used when a panic report must be sent. 387 | pub fn from_panic(&'a mut self, panic_info: &'a panic::PanicInfo) -> ReportErrorBuilder<'a> { 388 | let mut trace = Trace::default(); 389 | 390 | let payload = panic_info.payload(); 391 | let message = match payload.downcast_ref::<&str>() { 392 | Some(s) => *s, 393 | None => match payload.downcast_ref::() { 394 | Some(s) => s, 395 | None => "Box", 396 | }, 397 | }; 398 | trace.exception.class = "".to_owned(); 399 | trace.exception.message = message.to_owned(); 400 | trace.exception.description = trace.exception.message.to_owned(); 401 | 402 | if let Some(location) = panic_info.location() { 403 | trace.frames.push(FrameBuilder { 404 | file_name: location.file().to_owned(), 405 | line_number: Some(location.line()), 406 | ..Default::default() 407 | }); 408 | } 409 | 410 | ReportErrorBuilder { 411 | report_builder: self, 412 | trace: trace, 413 | level: None, 414 | title: Some(message.to_owned()), 415 | } 416 | } 417 | 418 | // TODO: remove self? 419 | /// To be used when an `error::Error` must be reported. 420 | pub fn from_error(&'a mut self, error: &'a E) -> ReportErrorBuilder<'a> { 421 | let mut trace = Trace::default(); 422 | trace.exception.class = std::any::type_name::().to_owned(); 423 | trace.exception.message = error.description().to_owned(); 424 | trace.exception.description = error 425 | .source() 426 | .map_or_else(|| format!("{:?}", error), |c| format!("{:?}", c)); 427 | 428 | ReportErrorBuilder { 429 | report_builder: self, 430 | trace: trace, 431 | level: None, 432 | title: Some(format!("{}", error)), 433 | } 434 | } 435 | 436 | /// To be used when a error message must be reported. 437 | pub fn from_error_message( 438 | &'a mut self, 439 | error_message: &'a T, 440 | ) -> ReportErrorBuilder<'a> { 441 | let message = format!("{}", error_message); 442 | 443 | let mut trace = Trace::default(); 444 | trace.exception.class = std::any::type_name::().to_owned(); 445 | trace.exception.message = message.to_owned(); 446 | trace.exception.description = message.to_owned(); 447 | 448 | ReportErrorBuilder { 449 | report_builder: self, 450 | trace: trace, 451 | level: None, 452 | title: Some(message), 453 | } 454 | } 455 | 456 | /// To be used when a message must be tracked by Rollbar. 457 | pub fn from_message(&'a mut self, message: &'a str) -> ReportMessageBuilder<'a> { 458 | ReportMessageBuilder { 459 | report_builder: self, 460 | message: message, 461 | level: None, 462 | } 463 | } 464 | 465 | /// Use given function to send a request to Rollbar instead of the built-in one. 466 | add_field!( 467 | with_send_strategy, 468 | send_strategy, 469 | Box< 470 | dyn Fn( 471 | Arc>>, 472 | String, 473 | ) -> thread::JoinHandle>, 474 | > 475 | ); 476 | } 477 | 478 | /// The access point to the library. 479 | pub struct Client { 480 | http_client: Arc>>, 481 | access_token: String, 482 | environment: String, 483 | } 484 | 485 | impl Client { 486 | /// Create a new `Client`. 487 | /// 488 | /// Your available `environment`s are listed at 489 | /// . 490 | /// 491 | /// You can get the `access_token` at 492 | /// . 493 | pub fn new>(access_token: T, environment: T) -> Client { 494 | let https = HttpsConnector::new(4).expect("TLS initialization failed"); 495 | let client = hyper::Client::builder().build::<_, hyper::Body>(https); 496 | 497 | Client { 498 | http_client: Arc::new(client), 499 | access_token: access_token.into(), 500 | environment: environment.into(), 501 | } 502 | } 503 | 504 | /// Create a `ReportBuilder` to build a new report for Rollbar. 505 | pub fn build_report(&self) -> ReportBuilder { 506 | ReportBuilder { 507 | client: self, 508 | send_strategy: None, 509 | } 510 | } 511 | 512 | /// Function used internally to send payloads to Rollbar as default `send_strategy`. 513 | fn send(&self, payload: String) -> thread::JoinHandle> { 514 | let body = hyper::Body::from(payload); 515 | let request = Request::builder() 516 | .method(Method::POST) 517 | .uri(URL) 518 | .body(body) 519 | .expect("Cannot build post request!"); 520 | 521 | let job = self 522 | .http_client 523 | .request(request) 524 | .map(|res| Some(ResponseStatus::from(res.status()))) 525 | .map_err(|error| { 526 | println!("Error while sending a report to Rollbar."); 527 | print!("The error returned by Rollbar was: {:?}.\n\n", error); 528 | 529 | None:: 530 | }); 531 | 532 | thread::spawn(move || { 533 | current_thread::Runtime::new() 534 | .unwrap() 535 | .block_on(job) 536 | .unwrap() 537 | }) 538 | } 539 | } 540 | 541 | /// Wrapper for `hyper::StatusCode`. 542 | #[derive(Debug)] 543 | pub struct ResponseStatus(hyper::StatusCode); 544 | 545 | impl From for ResponseStatus { 546 | fn from(status_code: hyper::StatusCode) -> ResponseStatus { 547 | ResponseStatus(status_code) 548 | } 549 | } 550 | 551 | impl ResponseStatus { 552 | /// Return a description provided by Rollbar for the status code returned by each request. 553 | pub fn description(&self) -> &str { 554 | match self.0.as_u16() { 555 | 200 => "The item was accepted for processing.", 556 | 400 => "No JSON payload was found, or it could not be decoded.", 557 | 401 => "No access token was found in the request.", 558 | 403 => "Check that your `access_token` is valid, enabled, and has the correct scope. The response will contain a `message` key explaining the problem.", 559 | 413 => "Max payload size is 128kb. Try removing or truncating unnecessary large data included in the payload, like whole binary files or long strings.", 560 | 422 => "A syntactically valid JSON payload was found, but it had one or more semantic errors. The response will contain a `message` key describing the errors.", 561 | 429 => "Request dropped because the rate limit has been reached for this access token, or the account is on the Free plan and the plan limit has been reached.", 562 | 500 => "There was an error on Rollbar's end", 563 | _ => "An undefined error occurred." 564 | } 565 | } 566 | 567 | /// Return the canonical description for the status code returned by each request. 568 | pub fn canonical_reason(&self) -> String { 569 | format!("{}", self.0) 570 | } 571 | } 572 | 573 | impl fmt::Display for ResponseStatus { 574 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 575 | write!( 576 | f, 577 | "Error {}: {}", 578 | self.canonical_reason(), 579 | self.description() 580 | ) 581 | } 582 | } 583 | 584 | #[cfg(test)] 585 | mod tests { 586 | extern crate backtrace; 587 | extern crate hyper; 588 | extern crate serde_json; 589 | 590 | use std::panic; 591 | use std::sync::mpsc::channel; 592 | use std::sync::{Arc, Mutex}; 593 | 594 | use backtrace::Backtrace; 595 | use serde_json::Value; 596 | 597 | use super::{Client, FrameBuilder, Level}; 598 | 599 | macro_rules! normalize_frames { 600 | ($payload:expr, $expected_payload:expr, $expected_frames:expr) => { 601 | // check the description/backtrace is is not empty and also check 602 | // that it is different from the message and then ignore it from now on 603 | let payload_ = $payload.to_owned(); 604 | let description = payload_ 605 | .get("data") 606 | .unwrap() 607 | .get("body") 608 | .unwrap() 609 | .get("trace") 610 | .unwrap() 611 | .get("exception") 612 | .unwrap() 613 | .get("description") 614 | .unwrap(); 615 | let message = payload_ 616 | .get("data") 617 | .unwrap() 618 | .get("body") 619 | .unwrap() 620 | .get("trace") 621 | .unwrap() 622 | .get("exception") 623 | .unwrap() 624 | .get("message") 625 | .unwrap(); 626 | 627 | match description { 628 | &Value::String(ref s) => assert!(!s.is_empty()), 629 | _ => assert!(false), 630 | } 631 | match message { 632 | &Value::String(ref s) => assert!(!s.is_empty()), 633 | _ => assert!(false), 634 | } 635 | 636 | $payload 637 | .get_mut("data") 638 | .unwrap() 639 | .get_mut("body") 640 | .unwrap() 641 | .get_mut("trace") 642 | .unwrap() 643 | .get_mut("frames") 644 | .unwrap() 645 | .as_array_mut() 646 | .unwrap() 647 | .truncate($expected_frames); 648 | }; 649 | } 650 | 651 | #[test] 652 | fn test_report_panics() { 653 | let (tx, rx) = channel(); 654 | 655 | { 656 | let tx = Arc::new(Mutex::new(tx)); 657 | 658 | let client = Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 659 | panic::set_hook(Box::new(move |panic_info| { 660 | let backtrace = Backtrace::new(); 661 | let payload = client 662 | .build_report() 663 | .from_panic(panic_info) 664 | .with_backtrace(&backtrace) 665 | .with_level("info") 666 | .to_string(); 667 | let payload = Arc::new(Mutex::new(payload)); 668 | tx.lock().unwrap().send(payload).unwrap(); 669 | })); 670 | 671 | let result = panic::catch_unwind(|| { 672 | // just to trick the linter 673 | let zero = "0".parse::().unwrap(); 674 | let _ = 1 / zero; 675 | }); 676 | assert!(result.is_err()); 677 | } 678 | 679 | // remove the hook to avoid double panics 680 | let _ = panic::take_hook(); 681 | 682 | let lock = rx.recv().unwrap(); 683 | let payload = match lock.lock() { 684 | Ok(guard) => guard, 685 | Err(poisoned) => poisoned.into_inner(), 686 | }; 687 | 688 | let mut payload: Value = serde_json::from_str(&*payload).unwrap(); 689 | let mut expected_payload = json!({ 690 | "access_token": "ACCESS_TOKEN", 691 | "data": { 692 | "environment": "ENVIRONMENT", 693 | "body": { 694 | "trace": { 695 | "frames": [{ 696 | "filename": "src/lib.rs", 697 | "lineno": 268 698 | }], 699 | "exception": { 700 | "class": "", 701 | "message": "attempt to divide by zero", 702 | "description": "attempt to divide by zero" 703 | } 704 | } 705 | }, 706 | "level": "info", 707 | "language": "rust", 708 | "title": "attempt to divide by zero" 709 | } 710 | }); 711 | 712 | let payload_ = payload.to_owned(); 713 | let line_number = payload_ 714 | .get("data") 715 | .unwrap() 716 | .get("body") 717 | .unwrap() 718 | .get("trace") 719 | .unwrap() 720 | .get("frames") 721 | .unwrap() 722 | .get(0) 723 | .unwrap() 724 | .get("lineno") 725 | .unwrap(); 726 | 727 | assert!(line_number.as_u64().unwrap() > 0); 728 | 729 | *expected_payload 730 | .get_mut("data") 731 | .unwrap() 732 | .get_mut("body") 733 | .unwrap() 734 | .get_mut("trace") 735 | .unwrap() 736 | .get_mut("frames") 737 | .unwrap() 738 | .get_mut(0) 739 | .unwrap() 740 | .get_mut("lineno") 741 | .unwrap() = line_number.to_owned(); 742 | 743 | normalize_frames!(payload, expected_payload, 1); 744 | assert_eq!(expected_payload.to_string(), payload.to_string()); 745 | } 746 | 747 | #[test] 748 | fn test_report_error() { 749 | let client = Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 750 | 751 | match "笑".parse::() { 752 | Ok(_) => { 753 | assert!(false); 754 | } 755 | Err(e) => { 756 | let payload = client 757 | .build_report() 758 | .from_error_message(&e) 759 | .with_level(Level::WARNING) 760 | .with_frame(FrameBuilder::new().with_column_number(42).build()) 761 | .with_frame(FrameBuilder::new().with_column_number(24).build()) 762 | .with_title("w") 763 | .to_string(); 764 | 765 | let expected_payload = json!({ 766 | "access_token": "ACCESS_TOKEN", 767 | "data": { 768 | "environment": "ENVIRONMENT", 769 | "body": { 770 | "trace": { 771 | "frames": [{ 772 | "filename": "src/lib.rs", 773 | "colno": 42 774 | }, { 775 | "filename": "src/lib.rs", 776 | "colno": 24 777 | }], 778 | "exception": { 779 | "class": "core::num::ParseIntError", 780 | "message": "invalid digit found in string", 781 | "description": "invalid digit found in string" 782 | } 783 | } 784 | }, 785 | "level": "warning", 786 | "language": "rust", 787 | "title": "w" 788 | } 789 | }); 790 | 791 | let mut payload: Value = serde_json::from_str(&*payload).unwrap(); 792 | normalize_frames!(payload, expected_payload, 2); 793 | assert_eq!(expected_payload.to_string(), payload.to_string()); 794 | } 795 | } 796 | } 797 | 798 | #[test] 799 | fn test_report_message() { 800 | let client = Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 801 | 802 | let payload = client 803 | .build_report() 804 | .from_message("hai") 805 | .with_level("warning") 806 | .to_string(); 807 | 808 | let expected_payload = json!({ 809 | "access_token": "ACCESS_TOKEN", 810 | "data": { 811 | "environment": "ENVIRONMENT", 812 | "body": { 813 | "message": { 814 | "body": "hai" 815 | } 816 | }, 817 | "level": "warning" 818 | } 819 | }) 820 | .to_string(); 821 | 822 | assert_eq!(payload, expected_payload); 823 | } 824 | 825 | #[test] 826 | fn test_response() { 827 | let client = Client::new("ACCESS_TOKEN", "ENVIRONMENT"); 828 | 829 | let status_handle = client 830 | .build_report() 831 | .from_message("hai") 832 | .with_level("info") 833 | .send(); 834 | 835 | match status_handle.join().unwrap() { 836 | Some(status) => { 837 | assert_eq!( 838 | status.to_string(), 839 | "Error 401 Unauthorized: No access token was found in the request.".to_owned() 840 | ); 841 | } 842 | None => { 843 | assert!(false); 844 | } 845 | } 846 | } 847 | } 848 | --------------------------------------------------------------------------------