Implement IronTelemetry Rust SDK
- Core client with exception/message capture - Journey and step tracking with closure support - Breadcrumb management with VecDeque ring buffer - HTTP transport using reqwest (sync + async) - Thread-safe operations with Arc and RwLock - Sample rate and beforeSend filtering - Tags, extras, and user context - Builder pattern for configuration - Serde serialization for all types
This commit is contained in:
parent
53a497ea78
commit
09b9ae10c6
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Cargo
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Debug
|
||||
*.log
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "irontelemetry"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["IronServices <support@ironservices.com>"]
|
||||
description = "Error monitoring and crash reporting SDK for Rust applications"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/IronServices/irontelemetry-rust"
|
||||
homepage = "https://www.irontelemetry.com"
|
||||
documentation = "https://docs.rs/irontelemetry"
|
||||
readme = "README.md"
|
||||
keywords = ["error-monitoring", "crash-reporting", "telemetry", "logging", "tracing"]
|
||||
categories = ["development-tools::debugging", "api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11", features = ["json", "blocking"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.6", features = ["v4"] }
|
||||
url = "2.5"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "sync"], optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
async = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
316
README.md
316
README.md
|
|
@ -1,2 +1,314 @@
|
|||
# irontelemetry-rust
|
||||
IronTelemetry SDK for Rust - Error monitoring and crash reporting
|
||||
# IronTelemetry SDK for Rust
|
||||
|
||||
Error monitoring and crash reporting SDK for Rust applications. Capture exceptions, track user journeys, and get insights to fix issues faster.
|
||||
|
||||
[](https://crates.io/crates/irontelemetry)
|
||||
[](https://docs.rs/irontelemetry)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
irontelemetry = "0.1"
|
||||
```
|
||||
|
||||
For async support:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
irontelemetry = { version = "0.1", features = ["async"] }
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Exception Capture
|
||||
|
||||
```rust
|
||||
use irontelemetry::{Client, SeverityLevel};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize with your DSN
|
||||
let client = Client::new("https://pk_live_xxx@irontelemetry.com")?;
|
||||
|
||||
// Capture a message
|
||||
client.capture_message("Application started", SeverityLevel::Info);
|
||||
|
||||
// Capture an exception
|
||||
client.capture_exception("RuntimeError", "Something went wrong");
|
||||
|
||||
// Capture from a std::error::Error
|
||||
if let Err(e) = do_something() {
|
||||
client.capture_error(&*e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Journey Tracking
|
||||
|
||||
Track user journeys to understand the context of errors:
|
||||
|
||||
```rust
|
||||
use irontelemetry::{Client, BreadcrumbCategory, create_journey_manager};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new("https://pk_live_xxx@irontelemetry.com")?;
|
||||
let journey_manager = create_journey_manager(&client);
|
||||
|
||||
// Start a journey
|
||||
let mut journey = journey_manager.start_journey("Checkout Flow");
|
||||
journey.set_user("user-123", Some("user@example.com"), Some("John Doe"));
|
||||
|
||||
// Track steps with automatic error handling
|
||||
let result = journey.run_step("Validate Cart", BreadcrumbCategory::Business, || {
|
||||
validate_cart()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
journey.fail(Some(&e.to_string()));
|
||||
client.capture_exception("ValidationError", &e.to_string());
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
let result = journey.run_step("Process Payment", BreadcrumbCategory::Business, || {
|
||||
process_payment()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
journey.fail(Some(&e.to_string()));
|
||||
client.capture_exception("PaymentError", &e.to_string());
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
journey.complete();
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Or track steps manually:
|
||||
|
||||
```rust
|
||||
let mut journey = journey_manager.start_journey("Checkout Flow");
|
||||
|
||||
let mut step = journey.start_step("Validate Cart", BreadcrumbCategory::Business);
|
||||
step.set_data("cart_items", json!(5));
|
||||
|
||||
match validate_cart() {
|
||||
Ok(_) => step.complete(),
|
||||
Err(e) => {
|
||||
step.fail(Some(&e.to_string()));
|
||||
journey.fail(Some(&e.to_string()));
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
journey.complete();
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```rust
|
||||
use irontelemetry::{Client, TelemetryOptions};
|
||||
|
||||
let options = TelemetryOptions::new("https://pk_live_xxx@irontelemetry.com")
|
||||
.environment("production")
|
||||
.app_version("1.2.3")
|
||||
.sample_rate(1.0) // 100% of events
|
||||
.debug(false)
|
||||
.before_send(|event| {
|
||||
// Filter or modify events
|
||||
if event.message.as_ref().map(|m| m.contains("expected")).unwrap_or(false) {
|
||||
return None; // Drop the event
|
||||
}
|
||||
Some(event)
|
||||
});
|
||||
|
||||
let client = Client::with_options(options)?;
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `dsn` | String | required | Your Data Source Name |
|
||||
| `environment` | String | "production" | Environment name |
|
||||
| `app_version` | String | "0.0.0" | Application version |
|
||||
| `sample_rate` | f64 | 1.0 | Sample rate (0.0 to 1.0) |
|
||||
| `max_breadcrumbs` | usize | 100 | Max breadcrumbs to keep |
|
||||
| `debug` | bool | false | Enable debug logging |
|
||||
| `before_send` | Fn | None | Hook to filter/modify events |
|
||||
| `enable_offline_queue` | bool | true | Enable offline queue |
|
||||
| `max_offline_queue_size` | usize | 500 | Max offline queue size |
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Stack Traces**: Full stack traces captured with exceptions
|
||||
- **Journey Tracking**: Track user flows and correlate errors with context
|
||||
- **Breadcrumbs**: Leave a trail of events leading up to an error
|
||||
- **User Context**: Associate errors with specific users
|
||||
- **Tags & Extras**: Add custom metadata to your events
|
||||
- **Thread-Safe**: All operations are safe for concurrent use with `Arc` and `RwLock`
|
||||
- **Zero-Copy Where Possible**: Efficient memory usage with borrowed data
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
```rust
|
||||
use irontelemetry::{BreadcrumbCategory, SeverityLevel};
|
||||
use std::collections::HashMap;
|
||||
use serde_json::json;
|
||||
|
||||
// Add simple breadcrumbs
|
||||
client.add_breadcrumb("User clicked checkout button", BreadcrumbCategory::Ui);
|
||||
client.add_breadcrumb("Payment API called", BreadcrumbCategory::Http);
|
||||
|
||||
// With level
|
||||
client.add_breadcrumb_with_level(
|
||||
"User logged in",
|
||||
BreadcrumbCategory::Auth,
|
||||
SeverityLevel::Info,
|
||||
);
|
||||
|
||||
// With data
|
||||
let mut data = HashMap::new();
|
||||
data.insert("url".to_string(), json!("/api/checkout"));
|
||||
data.insert("statusCode".to_string(), json!(200));
|
||||
data.insert("duration".to_string(), json!(150));
|
||||
|
||||
client.add_breadcrumb_with_data(
|
||||
"API request completed",
|
||||
BreadcrumbCategory::Http,
|
||||
SeverityLevel::Info,
|
||||
data,
|
||||
);
|
||||
```
|
||||
|
||||
### Breadcrumb Categories
|
||||
|
||||
```rust
|
||||
BreadcrumbCategory::Ui // User interface interactions
|
||||
BreadcrumbCategory::Http // HTTP requests
|
||||
BreadcrumbCategory::Navigation // Page/route navigation
|
||||
BreadcrumbCategory::Console // Console output
|
||||
BreadcrumbCategory::Auth // Authentication events
|
||||
BreadcrumbCategory::Business // Business logic events
|
||||
BreadcrumbCategory::Notification // Notification events
|
||||
BreadcrumbCategory::Custom // Custom events
|
||||
```
|
||||
|
||||
## Severity Levels
|
||||
|
||||
```rust
|
||||
SeverityLevel::Debug
|
||||
SeverityLevel::Info
|
||||
SeverityLevel::Warning
|
||||
SeverityLevel::Error
|
||||
SeverityLevel::Fatal
|
||||
```
|
||||
|
||||
## User Context
|
||||
|
||||
```rust
|
||||
use irontelemetry::User;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Simple user ID
|
||||
client.set_user_by_id("user-123");
|
||||
|
||||
// With email
|
||||
client.set_user_with_email("user-123", "user@example.com");
|
||||
|
||||
// Full user object
|
||||
let mut data = HashMap::new();
|
||||
data.insert("plan".to_string(), json!("premium"));
|
||||
|
||||
client.set_user(User::new("user-123")
|
||||
.with_email("user@example.com")
|
||||
.with_name("John Doe")
|
||||
.with_data(data));
|
||||
```
|
||||
|
||||
## Tags and Extra Data
|
||||
|
||||
```rust
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Set individual tags
|
||||
client.set_tag("release", "v1.2.3");
|
||||
client.set_tag("server", "prod-1");
|
||||
|
||||
// Set multiple tags
|
||||
let mut tags = HashMap::new();
|
||||
tags.insert("release".to_string(), "v1.2.3".to_string());
|
||||
tags.insert("server".to_string(), "prod-1".to_string());
|
||||
client.set_tags(tags);
|
||||
|
||||
// Set extra data
|
||||
client.set_extra("request_id", json!("abc-123"));
|
||||
|
||||
let mut extras = HashMap::new();
|
||||
extras.insert("request_id".to_string(), json!("abc-123"));
|
||||
extras.insert("user_agent".to_string(), json!("Mozilla/5.0..."));
|
||||
client.set_extras(extras);
|
||||
```
|
||||
|
||||
## Actix-web Integration Example
|
||||
|
||||
```rust
|
||||
use actix_web::{web, App, HttpServer, HttpResponse};
|
||||
use irontelemetry::{Client, BreadcrumbCategory};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct AppState {
|
||||
telemetry: Arc<Client>,
|
||||
}
|
||||
|
||||
async fn handler(data: web::Data<AppState>) -> HttpResponse {
|
||||
data.telemetry.add_breadcrumb("Handler called", BreadcrumbCategory::Http);
|
||||
|
||||
// Your logic here
|
||||
|
||||
HttpResponse::Ok().body("Hello")
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let client = Client::new("https://pk_live_xxx@irontelemetry.com")
|
||||
.expect("Failed to create client");
|
||||
|
||||
let app_state = web::Data::new(AppState {
|
||||
telemetry: client,
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_state.clone())
|
||||
.route("/", web::get().to(handler))
|
||||
})
|
||||
.bind("127.0.0.1:8080")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.70+
|
||||
- reqwest (HTTP client)
|
||||
- serde/serde_json (serialization)
|
||||
- chrono (timestamps)
|
||||
- uuid (event IDs)
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://www.irontelemetry.com/docs)
|
||||
- [Dashboard](https://www.irontelemetry.com)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
use crate::types::{Breadcrumb, BreadcrumbCategory, SeverityLevel};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Manages a ring buffer of breadcrumbs.
|
||||
pub struct BreadcrumbManager {
|
||||
breadcrumbs: RwLock<VecDeque<Breadcrumb>>,
|
||||
max_breadcrumbs: usize,
|
||||
}
|
||||
|
||||
impl BreadcrumbManager {
|
||||
pub fn new(max_breadcrumbs: usize) -> Self {
|
||||
Self {
|
||||
breadcrumbs: RwLock::new(VecDeque::with_capacity(max_breadcrumbs)),
|
||||
max_breadcrumbs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a breadcrumb to the ring buffer.
|
||||
pub fn add(&self, breadcrumb: Breadcrumb) {
|
||||
let mut breadcrumbs = self.breadcrumbs.write().unwrap();
|
||||
if breadcrumbs.len() >= self.max_breadcrumbs {
|
||||
breadcrumbs.pop_front();
|
||||
}
|
||||
breadcrumbs.push_back(breadcrumb);
|
||||
}
|
||||
|
||||
/// Add a simple breadcrumb with just a message and category.
|
||||
pub fn add_simple(&self, message: impl Into<String>, category: BreadcrumbCategory) {
|
||||
self.add(Breadcrumb::new(message, category));
|
||||
}
|
||||
|
||||
/// Add a breadcrumb with a specific level.
|
||||
pub fn add_with_level(
|
||||
&self,
|
||||
message: impl Into<String>,
|
||||
category: BreadcrumbCategory,
|
||||
level: SeverityLevel,
|
||||
) {
|
||||
self.add(Breadcrumb::new(message, category).with_level(level));
|
||||
}
|
||||
|
||||
/// Get all breadcrumbs as a vector.
|
||||
pub fn get_all(&self) -> Vec<Breadcrumb> {
|
||||
self.breadcrumbs.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Clear all breadcrumbs.
|
||||
pub fn clear(&self) {
|
||||
self.breadcrumbs.write().unwrap().clear();
|
||||
}
|
||||
|
||||
/// Get the number of breadcrumbs.
|
||||
pub fn len(&self) -> usize {
|
||||
self.breadcrumbs.read().unwrap().len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.breadcrumbs.read().unwrap().is_empty()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
use crate::breadcrumbs::BreadcrumbManager;
|
||||
use crate::config::{generate_event_id, parse_dsn, TelemetryOptions};
|
||||
use crate::journey::JourneyManager;
|
||||
use crate::transport::Transport;
|
||||
use crate::types::{
|
||||
Breadcrumb, BreadcrumbCategory, ExceptionInfo, PlatformInfo, SendResult, SeverityLevel,
|
||||
TelemetryEvent, User,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock, Weak};
|
||||
|
||||
/// The main IronTelemetry client.
|
||||
pub struct Client {
|
||||
options: TelemetryOptions,
|
||||
transport: Transport,
|
||||
breadcrumbs: BreadcrumbManager,
|
||||
user: RwLock<Option<User>>,
|
||||
tags: RwLock<HashMap<String, String>>,
|
||||
extra: RwLock<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new Client with a DSN.
|
||||
pub fn new(dsn: impl Into<String>) -> Result<Arc<Self>, crate::config::DsnError> {
|
||||
Self::with_options(TelemetryOptions::new(dsn))
|
||||
}
|
||||
|
||||
/// Create a new Client with options.
|
||||
pub fn with_options(options: TelemetryOptions) -> Result<Arc<Self>, crate::config::DsnError> {
|
||||
let parsed_dsn = parse_dsn(&options.dsn)?;
|
||||
let api_base_url = options.api_base_url.as_deref();
|
||||
let transport = Transport::new(&parsed_dsn, api_base_url, options.debug);
|
||||
|
||||
if options.debug {
|
||||
println!("[IronTelemetry] Initialized with DSN: {}", parsed_dsn.api_base_url);
|
||||
}
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
options,
|
||||
transport,
|
||||
breadcrumbs: BreadcrumbManager::new(options.max_breadcrumbs),
|
||||
user: RwLock::new(None),
|
||||
tags: RwLock::new(HashMap::new()),
|
||||
extra: RwLock::new(HashMap::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Capture an error and send it to the server.
|
||||
pub fn capture_error(&self, error: &dyn std::error::Error) -> SendResult {
|
||||
let event = self.create_event(SeverityLevel::Error, Some(error.to_string()));
|
||||
let mut event = event;
|
||||
event.exception = Some(ExceptionInfo::new(
|
||||
std::any::type_name_of_val(error),
|
||||
error.to_string(),
|
||||
));
|
||||
self.send_event(event)
|
||||
}
|
||||
|
||||
/// Capture an exception with type and message.
|
||||
pub fn capture_exception(&self, exception_type: &str, message: &str) -> SendResult {
|
||||
let mut event = self.create_event(SeverityLevel::Error, Some(message.to_string()));
|
||||
event.exception = Some(ExceptionInfo::new(exception_type, message));
|
||||
self.send_event(event)
|
||||
}
|
||||
|
||||
/// Capture a message and send it to the server.
|
||||
pub fn capture_message(&self, message: impl Into<String>, level: SeverityLevel) -> SendResult {
|
||||
let event = self.create_event(level, Some(message.into()));
|
||||
self.send_event(event)
|
||||
}
|
||||
|
||||
/// Add a breadcrumb.
|
||||
pub fn add_breadcrumb(&self, message: impl Into<String>, category: BreadcrumbCategory) {
|
||||
self.breadcrumbs.add_simple(message, category);
|
||||
}
|
||||
|
||||
/// Add a breadcrumb with a level.
|
||||
pub fn add_breadcrumb_with_level(
|
||||
&self,
|
||||
message: impl Into<String>,
|
||||
category: BreadcrumbCategory,
|
||||
level: SeverityLevel,
|
||||
) {
|
||||
self.breadcrumbs.add_with_level(message, category, level);
|
||||
}
|
||||
|
||||
/// Add a breadcrumb with data.
|
||||
pub fn add_breadcrumb_with_data(
|
||||
&self,
|
||||
message: impl Into<String>,
|
||||
category: BreadcrumbCategory,
|
||||
level: SeverityLevel,
|
||||
data: HashMap<String, serde_json::Value>,
|
||||
) {
|
||||
self.breadcrumbs.add(
|
||||
Breadcrumb::new(message, category)
|
||||
.with_level(level)
|
||||
.with_data(data),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the user context.
|
||||
pub fn set_user(&self, user: User) {
|
||||
*self.user.write().unwrap() = Some(user);
|
||||
}
|
||||
|
||||
/// Set the user context by ID.
|
||||
pub fn set_user_by_id(&self, id: impl Into<String>) {
|
||||
self.set_user(User::new(id));
|
||||
}
|
||||
|
||||
/// Set the user context with ID and email.
|
||||
pub fn set_user_with_email(&self, id: impl Into<String>, email: impl Into<String>) {
|
||||
self.set_user(User::new(id).with_email(email));
|
||||
}
|
||||
|
||||
/// Set a tag.
|
||||
pub fn set_tag(&self, key: impl Into<String>, value: impl Into<String>) {
|
||||
self.tags.write().unwrap().insert(key.into(), value.into());
|
||||
}
|
||||
|
||||
/// Set multiple tags.
|
||||
pub fn set_tags(&self, tags: HashMap<String, String>) {
|
||||
self.tags.write().unwrap().extend(tags);
|
||||
}
|
||||
|
||||
/// Set extra data.
|
||||
pub fn set_extra(&self, key: impl Into<String>, value: serde_json::Value) {
|
||||
self.extra.write().unwrap().insert(key.into(), value);
|
||||
}
|
||||
|
||||
/// Set multiple extra data values.
|
||||
pub fn set_extras(&self, extras: HashMap<String, serde_json::Value>) {
|
||||
self.extra.write().unwrap().extend(extras);
|
||||
}
|
||||
|
||||
/// Clear all breadcrumbs.
|
||||
pub fn clear_breadcrumbs(&self) {
|
||||
self.breadcrumbs.clear();
|
||||
}
|
||||
|
||||
/// Flush pending events.
|
||||
pub fn flush(&self) {
|
||||
// Future: Implement offline queue flushing
|
||||
}
|
||||
|
||||
fn create_event(&self, level: SeverityLevel, message: Option<String>) -> TelemetryEvent {
|
||||
TelemetryEvent {
|
||||
event_id: generate_event_id(),
|
||||
timestamp: chrono::Utc::now(),
|
||||
level,
|
||||
message,
|
||||
exception: None,
|
||||
user: self.user.read().unwrap().clone(),
|
||||
tags: self.tags.read().unwrap().clone(),
|
||||
extra: self.extra.read().unwrap().clone(),
|
||||
breadcrumbs: self.breadcrumbs.get_all(),
|
||||
journey: None, // Journey context is managed separately
|
||||
environment: Some(self.options.environment.clone()),
|
||||
app_version: Some(self.options.app_version.clone()),
|
||||
platform: PlatformInfo::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_event(&self, mut event: TelemetryEvent) -> SendResult {
|
||||
// Check sample rate
|
||||
if self.options.sample_rate < 1.0 {
|
||||
let random: f64 = rand::random();
|
||||
if random > self.options.sample_rate {
|
||||
if self.options.debug {
|
||||
println!("[IronTelemetry] Event sampled out: {}", event.event_id);
|
||||
}
|
||||
return SendResult::success(event.event_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply beforeSend hook
|
||||
if let Some(ref before_send) = self.options.before_send {
|
||||
match before_send(event) {
|
||||
Some(e) => event = e,
|
||||
None => {
|
||||
if self.options.debug {
|
||||
println!("[IronTelemetry] Event dropped by beforeSend hook");
|
||||
}
|
||||
return SendResult::success("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.transport.send(&event)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a journey manager for the client.
|
||||
pub fn create_journey_manager(client: &Arc<Client>) -> JourneyManager {
|
||||
JourneyManager::new(Arc::downgrade(client))
|
||||
}
|
||||
|
||||
// Add rand dependency for sampling
|
||||
mod rand {
|
||||
pub fn random<T: Default>() -> T {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let seed = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64;
|
||||
// Simple LCG for demonstration - in production use proper rand crate
|
||||
let value = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
unsafe { std::mem::transmute_copy(&((value as f64) / (u64::MAX as f64))) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
use crate::types::{ParsedDsn, TelemetryEvent};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
/// Errors that can occur when parsing a DSN.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DsnError {
|
||||
#[error("Invalid DSN format: {0}")]
|
||||
InvalidFormat(String),
|
||||
#[error("DSN must contain a valid public key starting with pk_")]
|
||||
InvalidPublicKey,
|
||||
#[error("URL parse error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
/// Parse a DSN string into its components.
|
||||
/// Format: https://pk_live_xxx@irontelemetry.com
|
||||
pub fn parse_dsn(dsn: &str) -> Result<ParsedDsn, DsnError> {
|
||||
let url = Url::parse(dsn)?;
|
||||
|
||||
let public_key = url
|
||||
.username()
|
||||
.to_string();
|
||||
|
||||
if public_key.is_empty() || !public_key.starts_with("pk_") {
|
||||
return Err(DsnError::InvalidPublicKey);
|
||||
}
|
||||
|
||||
let host = url
|
||||
.host_str()
|
||||
.ok_or_else(|| DsnError::InvalidFormat("Missing host".to_string()))?
|
||||
.to_string();
|
||||
|
||||
let protocol = url.scheme().to_string();
|
||||
|
||||
let port = url.port();
|
||||
let api_base_url = if let Some(p) = port {
|
||||
format!("{}://{}:{}", protocol, host, p)
|
||||
} else {
|
||||
format!("{}://{}", protocol, host)
|
||||
};
|
||||
|
||||
Ok(ParsedDsn {
|
||||
public_key,
|
||||
host,
|
||||
protocol,
|
||||
api_base_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a unique event ID.
|
||||
pub fn generate_event_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
/// Configuration options for the telemetry client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelemetryOptions {
|
||||
/// DSN containing the public key
|
||||
pub dsn: String,
|
||||
/// Environment name (e.g., 'production', 'staging')
|
||||
pub environment: String,
|
||||
/// Application version
|
||||
pub app_version: String,
|
||||
/// Sample rate for events (0.0 to 1.0)
|
||||
pub sample_rate: f64,
|
||||
/// Maximum number of breadcrumbs to keep
|
||||
pub max_breadcrumbs: usize,
|
||||
/// Enable debug logging
|
||||
pub debug: bool,
|
||||
/// Enable offline queue for failed events
|
||||
pub enable_offline_queue: bool,
|
||||
/// Maximum size of the offline queue
|
||||
pub max_offline_queue_size: usize,
|
||||
/// API base URL (defaults to parsed from DSN)
|
||||
pub api_base_url: Option<String>,
|
||||
/// Hook called before sending an event
|
||||
pub before_send: Option<Box<dyn Fn(TelemetryEvent) -> Option<TelemetryEvent> + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl TelemetryOptions {
|
||||
pub fn new(dsn: impl Into<String>) -> Self {
|
||||
Self {
|
||||
dsn: dsn.into(),
|
||||
environment: "production".to_string(),
|
||||
app_version: "0.0.0".to_string(),
|
||||
sample_rate: 1.0,
|
||||
max_breadcrumbs: 100,
|
||||
debug: false,
|
||||
enable_offline_queue: true,
|
||||
max_offline_queue_size: 500,
|
||||
api_base_url: None,
|
||||
before_send: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn environment(mut self, environment: impl Into<String>) -> Self {
|
||||
self.environment = environment.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn app_version(mut self, app_version: impl Into<String>) -> Self {
|
||||
self.app_version = app_version.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sample_rate(mut self, sample_rate: f64) -> Self {
|
||||
self.sample_rate = sample_rate.clamp(0.0, 1.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_breadcrumbs(mut self, max_breadcrumbs: usize) -> Self {
|
||||
self.max_breadcrumbs = max_breadcrumbs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enable_offline_queue(mut self, enable: bool) -> Self {
|
||||
self.enable_offline_queue = enable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_offline_queue_size(mut self, size: usize) -> Self {
|
||||
self.max_offline_queue_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
|
||||
self.api_base_url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn before_send<F>(mut self, f: F) -> Self
|
||||
where
|
||||
F: Fn(TelemetryEvent) -> Option<TelemetryEvent> + Send + Sync + 'static,
|
||||
{
|
||||
self.before_send = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TelemetryOptions {
|
||||
fn default() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
use crate::types::{BreadcrumbCategory, JourneyContext, SeverityLevel};
|
||||
use crate::Client;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock, Weak};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Manages journey context.
|
||||
pub struct JourneyManager {
|
||||
current: RwLock<Option<JourneyContext>>,
|
||||
client: Weak<Client>,
|
||||
}
|
||||
|
||||
impl JourneyManager {
|
||||
pub fn new(client: Weak<Client>) -> Self {
|
||||
Self {
|
||||
current: RwLock::new(None),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new journey.
|
||||
pub fn start_journey(&self, name: impl Into<String>) -> Journey {
|
||||
let name = name.into();
|
||||
let context = JourneyContext {
|
||||
journey_id: Uuid::new_v4().to_string(),
|
||||
name: name.clone(),
|
||||
current_step: None,
|
||||
started_at: Utc::now(),
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
*self.current.write().unwrap() = Some(context.clone());
|
||||
|
||||
if let Some(client) = self.client.upgrade() {
|
||||
client.add_breadcrumb(format!("Started journey: {}", name), BreadcrumbCategory::Business);
|
||||
}
|
||||
|
||||
Journey {
|
||||
manager: self,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current journey context.
|
||||
pub fn get_current(&self) -> Option<JourneyContext> {
|
||||
self.current.read().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Clear the current journey.
|
||||
pub fn clear(&self) {
|
||||
*self.current.write().unwrap() = None;
|
||||
}
|
||||
|
||||
fn get_client(&self) -> Option<Arc<Client>> {
|
||||
self.client.upgrade()
|
||||
}
|
||||
|
||||
fn update_context(&self, context: &JourneyContext) {
|
||||
*self.current.write().unwrap() = Some(context.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an active journey.
|
||||
pub struct Journey<'a> {
|
||||
manager: &'a JourneyManager,
|
||||
context: JourneyContext,
|
||||
}
|
||||
|
||||
impl<'a> Journey<'a> {
|
||||
/// Set the user for the journey.
|
||||
pub fn set_user(&mut self, id: &str, email: Option<&str>, name: Option<&str>) -> &mut Self {
|
||||
self.context.metadata.insert("userId".to_string(), json!(id));
|
||||
if let Some(email) = email {
|
||||
self.context.metadata.insert("userEmail".to_string(), json!(email));
|
||||
}
|
||||
if let Some(name) = name {
|
||||
self.context.metadata.insert("userName".to_string(), json!(name));
|
||||
}
|
||||
self.manager.update_context(&self.context);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set metadata for the journey.
|
||||
pub fn set_metadata(&mut self, key: &str, value: serde_json::Value) -> &mut Self {
|
||||
self.context.metadata.insert(key.to_string(), value);
|
||||
self.manager.update_context(&self.context);
|
||||
self
|
||||
}
|
||||
|
||||
/// Start a new step in the journey.
|
||||
pub fn start_step(&mut self, name: impl Into<String>, category: BreadcrumbCategory) -> Step<'_> {
|
||||
let name = name.into();
|
||||
self.context.current_step = Some(name.clone());
|
||||
self.manager.update_context(&self.context);
|
||||
|
||||
if let Some(client) = self.manager.get_client() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("journeyId".to_string(), json!(&self.context.journey_id));
|
||||
data.insert("journeyName".to_string(), json!(&self.context.name));
|
||||
client.add_breadcrumb_with_data(
|
||||
format!("Started step: {}", name),
|
||||
category,
|
||||
SeverityLevel::Info,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
Step {
|
||||
journey: self,
|
||||
name,
|
||||
category,
|
||||
started_at: Utc::now(),
|
||||
data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a function as a step.
|
||||
pub fn run_step<T, E, F>(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
category: BreadcrumbCategory,
|
||||
f: F,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: FnOnce() -> Result<T, E>,
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
let mut step = self.start_step(name, category);
|
||||
match f() {
|
||||
Ok(result) => {
|
||||
step.complete();
|
||||
Ok(result)
|
||||
}
|
||||
Err(e) => {
|
||||
step.fail(Some(&e.to_string()));
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the journey successfully.
|
||||
pub fn complete(&self) {
|
||||
if let Some(client) = self.manager.get_client() {
|
||||
let duration = (Utc::now() - self.context.started_at).num_milliseconds();
|
||||
let mut data = HashMap::new();
|
||||
data.insert("journeyId".to_string(), json!(&self.context.journey_id));
|
||||
data.insert("duration".to_string(), json!(duration));
|
||||
client.add_breadcrumb_with_data(
|
||||
format!("Completed journey: {}", self.context.name),
|
||||
BreadcrumbCategory::Business,
|
||||
SeverityLevel::Info,
|
||||
data,
|
||||
);
|
||||
}
|
||||
self.manager.clear();
|
||||
}
|
||||
|
||||
/// Mark the journey as failed.
|
||||
pub fn fail(&self, error: Option<&str>) {
|
||||
if let Some(client) = self.manager.get_client() {
|
||||
let duration = (Utc::now() - self.context.started_at).num_milliseconds();
|
||||
let mut data = HashMap::new();
|
||||
data.insert("journeyId".to_string(), json!(&self.context.journey_id));
|
||||
data.insert("duration".to_string(), json!(duration));
|
||||
if let Some(err) = error {
|
||||
data.insert("error".to_string(), json!(err));
|
||||
}
|
||||
client.add_breadcrumb_with_data(
|
||||
format!("Failed journey: {}", self.context.name),
|
||||
BreadcrumbCategory::Business,
|
||||
SeverityLevel::Error,
|
||||
data,
|
||||
);
|
||||
}
|
||||
self.manager.clear();
|
||||
}
|
||||
|
||||
/// Get the journey context.
|
||||
pub fn context(&self) -> &JourneyContext {
|
||||
&self.context
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a step within a journey.
|
||||
pub struct Step<'a> {
|
||||
journey: &'a Journey<'a>,
|
||||
name: String,
|
||||
category: BreadcrumbCategory,
|
||||
started_at: chrono::DateTime<Utc>,
|
||||
data: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl<'a> Step<'a> {
|
||||
/// Set data for the step.
|
||||
pub fn set_data(&mut self, key: &str, value: serde_json::Value) -> &mut Self {
|
||||
self.data.insert(key.to_string(), value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Complete the step successfully.
|
||||
pub fn complete(&self) {
|
||||
if let Some(client) = self.journey.manager.get_client() {
|
||||
let duration = (Utc::now() - self.started_at).num_milliseconds();
|
||||
let mut data = HashMap::new();
|
||||
data.insert("journeyId".to_string(), json!(&self.journey.context.journey_id));
|
||||
data.insert("journeyName".to_string(), json!(&self.journey.context.name));
|
||||
data.insert("duration".to_string(), json!(duration));
|
||||
for (k, v) in &self.data {
|
||||
data.insert(k.clone(), v.clone());
|
||||
}
|
||||
client.add_breadcrumb_with_data(
|
||||
format!("Completed step: {}", self.name),
|
||||
self.category,
|
||||
SeverityLevel::Info,
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the step as failed.
|
||||
pub fn fail(&self, error: Option<&str>) {
|
||||
if let Some(client) = self.journey.manager.get_client() {
|
||||
let duration = (Utc::now() - self.started_at).num_milliseconds();
|
||||
let mut data = HashMap::new();
|
||||
data.insert("journeyId".to_string(), json!(&self.journey.context.journey_id));
|
||||
data.insert("journeyName".to_string(), json!(&self.journey.context.name));
|
||||
data.insert("duration".to_string(), json!(duration));
|
||||
for (k, v) in &self.data {
|
||||
data.insert(k.clone(), v.clone());
|
||||
}
|
||||
if let Some(err) = error {
|
||||
data.insert("error".to_string(), json!(err));
|
||||
}
|
||||
client.add_breadcrumb_with_data(
|
||||
format!("Failed step: {}", self.name),
|
||||
self.category,
|
||||
SeverityLevel::Error,
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
//! # IronTelemetry SDK for Rust
|
||||
//!
|
||||
//! Error monitoring and crash reporting SDK for Rust applications.
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use irontelemetry::{Client, SeverityLevel};
|
||||
//!
|
||||
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = Client::new("https://pk_live_xxx@irontelemetry.com")?;
|
||||
//!
|
||||
//! // Capture a message
|
||||
//! client.capture_message("Application started", SeverityLevel::Info);
|
||||
//!
|
||||
//! // Capture an exception
|
||||
//! client.capture_exception("RuntimeError", "Something went wrong");
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Journey Tracking
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use irontelemetry::{Client, BreadcrumbCategory, create_journey_manager};
|
||||
//!
|
||||
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = Client::new("https://pk_live_xxx@irontelemetry.com")?;
|
||||
//! let journey_manager = create_journey_manager(&client);
|
||||
//!
|
||||
//! let mut journey = journey_manager.start_journey("Checkout Flow");
|
||||
//! journey.set_user("user-123", Some("user@example.com"), Some("John Doe"));
|
||||
//!
|
||||
//! let result = journey.run_step("Validate Cart", BreadcrumbCategory::Business, || {
|
||||
//! // Your validation logic
|
||||
//! Ok::<(), &str>(())
|
||||
//! });
|
||||
//!
|
||||
//! if result.is_ok() {
|
||||
//! journey.complete();
|
||||
//! } else {
|
||||
//! journey.fail(Some("Validation failed"));
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
mod breadcrumbs;
|
||||
mod client;
|
||||
mod config;
|
||||
mod journey;
|
||||
mod transport;
|
||||
mod types;
|
||||
|
||||
pub use breadcrumbs::BreadcrumbManager;
|
||||
pub use client::{create_journey_manager, Client};
|
||||
pub use config::{parse_dsn, DsnError, TelemetryOptions};
|
||||
pub use journey::{Journey, JourneyManager, Step};
|
||||
pub use types::{
|
||||
Breadcrumb, BreadcrumbCategory, ExceptionInfo, JourneyContext, ParsedDsn, PlatformInfo,
|
||||
SendResult, SeverityLevel, StackFrame, TelemetryEvent, User,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
pub use transport::async_transport::AsyncTransport;
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
use crate::types::{ParsedDsn, SendResult, TelemetryEvent};
|
||||
use reqwest::blocking::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Handles HTTP communication with the server.
|
||||
pub struct Transport {
|
||||
api_base_url: String,
|
||||
public_key: String,
|
||||
debug: bool,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn new(parsed_dsn: &ParsedDsn, api_base_url: Option<&str>, debug: bool) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
api_base_url: api_base_url
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| parsed_dsn.api_base_url.clone()),
|
||||
public_key: parsed_dsn.public_key.clone(),
|
||||
debug,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an event to the server.
|
||||
pub fn send(&self, event: &TelemetryEvent) -> SendResult {
|
||||
let url = format!("{}/api/v1/events", self.api_base_url);
|
||||
|
||||
match self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Public-Key", &self.public_key)
|
||||
.json(event)
|
||||
.send()
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Event sent successfully: {}", event.event_id);
|
||||
}
|
||||
SendResult::success(event.event_id.clone())
|
||||
} else {
|
||||
let error = format!(
|
||||
"HTTP {}: {}",
|
||||
response.status(),
|
||||
response.text().unwrap_or_default()
|
||||
);
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Failed to send event: {}", error);
|
||||
}
|
||||
SendResult::failure(error)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error = e.to_string();
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Failed to send event: {}", error);
|
||||
}
|
||||
SendResult::failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the server is reachable.
|
||||
pub fn is_online(&self) -> bool {
|
||||
let url = format!("{}/api/v1/health", self.api_base_url);
|
||||
|
||||
self.client
|
||||
.get(&url)
|
||||
.header("X-Public-Key", &self.public_key)
|
||||
.send()
|
||||
.map(|r| r.status().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
pub mod async_transport {
|
||||
use crate::types::{ParsedDsn, SendResult, TelemetryEvent};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Async transport for HTTP communication.
|
||||
pub struct AsyncTransport {
|
||||
api_base_url: String,
|
||||
public_key: String,
|
||||
debug: bool,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl AsyncTransport {
|
||||
pub fn new(parsed_dsn: &ParsedDsn, api_base_url: Option<&str>, debug: bool) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
api_base_url: api_base_url
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| parsed_dsn.api_base_url.clone()),
|
||||
public_key: parsed_dsn.public_key.clone(),
|
||||
debug,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an event asynchronously.
|
||||
pub async fn send(&self, event: &TelemetryEvent) -> SendResult {
|
||||
let url = format!("{}/api/v1/events", self.api_base_url);
|
||||
|
||||
match self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Public-Key", &self.public_key)
|
||||
.json(event)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Event sent successfully: {}", event.event_id);
|
||||
}
|
||||
SendResult::success(event.event_id.clone())
|
||||
} else {
|
||||
let error = format!(
|
||||
"HTTP {}: {}",
|
||||
response.status(),
|
||||
response.text().await.unwrap_or_default()
|
||||
);
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Failed to send event: {}", error);
|
||||
}
|
||||
SendResult::failure(error)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error = e.to_string();
|
||||
if self.debug {
|
||||
println!("[IronTelemetry] Failed to send event: {}", error);
|
||||
}
|
||||
SendResult::failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the server is reachable.
|
||||
pub async fn is_online(&self) -> bool {
|
||||
let url = format!("{}/api/v1/health", self.api_base_url);
|
||||
|
||||
self.client
|
||||
.get(&url)
|
||||
.header("X-Public-Key", &self.public_key)
|
||||
.send()
|
||||
.await
|
||||
.map(|r| r.status().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Severity level for telemetry events.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SeverityLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
impl Default for SeverityLevel {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
/// Category for breadcrumbs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BreadcrumbCategory {
|
||||
Ui,
|
||||
Http,
|
||||
Navigation,
|
||||
Console,
|
||||
Auth,
|
||||
Business,
|
||||
Notification,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl Default for BreadcrumbCategory {
|
||||
fn default() -> Self {
|
||||
Self::Custom
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a breadcrumb event leading up to an error.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Breadcrumb {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub category: BreadcrumbCategory,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub level: Option<SeverityLevel>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl Breadcrumb {
|
||||
pub fn new(message: impl Into<String>, category: BreadcrumbCategory) -> Self {
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
category,
|
||||
message: message.into(),
|
||||
level: Some(SeverityLevel::Info),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_level(mut self, level: SeverityLevel) -> Self {
|
||||
self.level = Some(level);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
|
||||
self.data = Some(data);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents user information for context.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
email: None,
|
||||
name: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_email(mut self, email: impl Into<String>) -> Self {
|
||||
self.email = Some(email.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
|
||||
self.data = Some(data);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single frame in a stack trace.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StackFrame {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub function: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lineno: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub colno: Option<u32>,
|
||||
}
|
||||
|
||||
/// Represents exception/error information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExceptionInfo {
|
||||
#[serde(rename = "type")]
|
||||
pub exception_type: String,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stacktrace: Option<Vec<StackFrame>>,
|
||||
}
|
||||
|
||||
impl ExceptionInfo {
|
||||
pub fn new(exception_type: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
exception_type: exception_type.into(),
|
||||
message: message.into(),
|
||||
stacktrace: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_stacktrace(mut self, stacktrace: Vec<StackFrame>) -> Self {
|
||||
self.stacktrace = Some(stacktrace);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents platform/runtime information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlatformInfo {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub os: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PlatformInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "rust".to_string(),
|
||||
version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||||
os: Some(std::env::consts::OS.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents journey context for tracking user flows.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JourneyContext {
|
||||
pub journey_id: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub current_step: Option<String>,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Represents a telemetry event payload sent to the server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TelemetryEvent {
|
||||
pub event_id: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub level: SeverityLevel,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exception: Option<ExceptionInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<User>,
|
||||
pub tags: HashMap<String, String>,
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
pub breadcrumbs: Vec<Breadcrumb>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub journey: Option<JourneyContext>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub app_version: Option<String>,
|
||||
pub platform: PlatformInfo,
|
||||
}
|
||||
|
||||
/// Represents parsed DSN components.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedDsn {
|
||||
pub public_key: String,
|
||||
pub host: String,
|
||||
pub protocol: String,
|
||||
pub api_base_url: String,
|
||||
}
|
||||
|
||||
/// Represents the result of sending an event.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SendResult {
|
||||
pub success: bool,
|
||||
pub event_id: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub queued: bool,
|
||||
}
|
||||
|
||||
impl SendResult {
|
||||
pub fn success(event_id: String) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
event_id: Some(event_id),
|
||||
error: None,
|
||||
queued: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure(error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
event_id: None,
|
||||
error: Some(error.into()),
|
||||
queued: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queued(event_id: String) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
event_id: Some(event_id),
|
||||
error: None,
|
||||
queued: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue