use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Level {
Debug,
Info,
Warning,
Error,
Fatal,
}
impl Default for Level {
fn default() -> Self {
Self::Error
}
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Debug => write!(f, "debug"),
Self::Info => write!(f, "info"),
Self::Warning => write!(f, "warning"),
Self::Error => write!(f, "error"),
Self::Fatal => write!(f, "fatal"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackFrame {
pub filename: String,
pub function: String,
pub lineno: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub colno: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_line: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_context: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_context: Option<Vec<String>>,
#[serde(default = "default_true")]
pub in_app: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub module: Option<String>,
}
fn default_true() -> bool {
true
}
impl StackFrame {
pub fn new(filename: impl Into<String>, function: impl Into<String>, lineno: u32) -> Self {
Self {
filename: filename.into(),
function: function.into(),
lineno,
colno: None,
context_line: None,
pre_context: None,
post_context: None,
in_app: true,
module: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExceptionInfo {
#[serde(rename = "type")]
pub error_type: String,
pub value: String,
#[serde(default)]
pub stacktrace: Vec<StackFrame>,
#[serde(skip_serializing_if = "Option::is_none")]
pub module: Option<String>,
}
impl ExceptionInfo {
pub fn new(error_type: impl Into<String>, value: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
value: value.into(),
stacktrace: Vec::new(),
module: None,
}
}
pub fn with_stacktrace(mut self, stacktrace: Vec<StackFrame>) -> Self {
self.stacktrace = stacktrace;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Breadcrumb {
pub category: String,
pub message: String,
#[serde(default)]
pub level: Level,
pub timestamp: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<HashMap<String, serde_json::Value>>,
}
impl Breadcrumb {
pub fn new(category: impl Into<String>, message: impl Into<String>) -> Self {
Self {
category: category.into(),
message: message.into(),
level: Level::Info,
timestamp: Utc::now(),
data: None,
}
}
pub fn with_level(mut self, level: Level) -> Self {
self.level = level;
self
}
pub fn with_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
self.data = Some(data);
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UserContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<HashMap<String, serde_json::Value>>,
}
impl UserContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
pub fn with_username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RequestContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_ip: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorEvent {
pub event_id: String,
pub timestamp: DateTime<Utc>,
pub level: Level,
#[serde(skip_serializing_if = "Option::is_none")]
pub exception: Option<ExceptionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default = "default_platform")]
pub platform: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sdk: Option<SdkInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<RuntimeInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<RequestContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<UserContext>,
#[serde(default)]
pub tags: HashMap<String, String>,
#[serde(default)]
pub extra: HashMap<String, serde_json::Value>,
#[serde(default)]
pub breadcrumbs: Vec<Breadcrumb>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub release: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
}
fn default_platform() -> String {
"rust".to_string()
}
impl ErrorEvent {
pub fn new(event_id: impl Into<String>, level: Level) -> Self {
Self {
event_id: event_id.into(),
timestamp: Utc::now(),
level,
exception: None,
message: None,
platform: "rust".to_string(),
sdk: Some(SdkInfo {
name: "bugwatch-rust".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
}),
runtime: Some(RuntimeInfo {
name: "rust".to_string(),
version: rustc_version(),
}),
request: None,
user: None,
tags: HashMap::new(),
extra: HashMap::new(),
breadcrumbs: Vec::new(),
fingerprint: None,
environment: None,
release: None,
server_name: None,
}
}
}
fn rustc_version() -> String {
option_env!("RUSTC_VERSION")
.unwrap_or("unknown")
.to_string()
}
#[derive(Debug, Clone)]
pub struct BugwatchOptions {
pub api_key: String,
pub endpoint: String,
pub environment: Option<String>,
pub release: Option<String>,
pub server_name: Option<String>,
pub debug: bool,
pub max_breadcrumbs: usize,
pub sample_rate: f64,
pub attach_stacktrace: bool,
}
impl BugwatchOptions {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
endpoint: "https://api.bugwatch.dev".to_string(),
environment: None,
release: None,
server_name: None,
debug: false,
max_breadcrumbs: 100,
sample_rate: 1.0,
attach_stacktrace: true,
}
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = endpoint.into();
self
}
pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
self.environment = Some(environment.into());
self
}
pub fn with_release(mut self, release: impl Into<String>) -> Self {
self.release = Some(release.into());
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
pub fn with_sample_rate(mut self, sample_rate: f64) -> Self {
self.sample_rate = sample_rate.clamp(0.0, 1.0);
self
}
}
impl Default for BugwatchOptions {
fn default() -> Self {
Self::new("")
}
}