4 releases (2 breaking)
Uses new Rust 2024
| new 0.3.0 | Feb 25, 2026 |
|---|---|
| 0.2.0 | Feb 24, 2026 |
| 0.1.1 | Jan 30, 2026 |
| 0.1.0 | Jan 30, 2026 |
#120 in Debugging
Used in fail2ban-rs
125KB
2.5K
SLoC
Tell Rust SDK
Rust SDK for Tell — product analytics and structured logging, 1,000× faster than PostHog and Mixpanel.
- 80 ns per call. Serializes, encodes, and enqueues. Your thread moves on.
- 10M events/sec delivered. Batched, encoded, sent over the wire.
- Fire & forget. Synchronous API, async background worker. Zero I/O blocking.
- Thread-safe.
Clone + Send + Sync. Share across threads viaArc.
Installation
cargo add tell
cargo add tokio --features rt-multi-thread,macros
Quick Start
use tell::{Tell, TellConfig, props};
#[tokio::main]
async fn main() {
let client = Tell::new(
TellConfig::production("feed1e11feed1e11feed1e11feed1e11").unwrap()
).unwrap();
// Track events
client.track("user_123", "Page Viewed", props! {
"url" => "/home",
"referrer" => "google"
});
// Identify users
client.identify("user_123", props! {
"name" => "Jane",
"plan" => "pro"
});
// Revenue
client.revenue("user_123", 49.99, "USD", "order_456", None::<serde_json::Value>);
// Structured logging
client.log_error("DB connection failed", Some("api"), props! {
"host" => "db.internal"
});
client.close().await.ok();
}
Performance
Delivery throughput — batched, encoded, and sent over TCP (Apple M4 Pro):
| Batch size | With payload | No payload |
|---|---|---|
| 10 | 7.8M/s | 14.5M/s |
| 100 | 8.3M/s | 14.3M/s |
| 500 | 9.8M/s | 18.2M/s |
Each event is 200 bytes on the wire — device ID, session ID, timestamp, event name, and user properties, FlatBuffer-encoded with API key and batch headers.
Caller latency — serialize, encode, and enqueue. Wire-ready before your function returns:
| Operation | With properties | No properties |
|---|---|---|
track |
84 ns | 52 ns |
log |
76 ns | 50 ns |
PostHog and Mixpanel send an HTTP request on every track call — ~85 µs on localhost, milliseconds in production. Tell enqueues a wire-ready event in 84 ns — 1,000× less overhead. Your thread never touches the network.
For comparison, FlashLog achieves ~16 ns by copying raw bytes into a ring buffer — serialization and I/O happen later. Tell pays upfront for a wire-ready event.
cargo bench -p tell-bench --bench hot_path # caller latency
cargo bench -p tell-bench --bench comparison # vs flashlog
cargo run -p tell-bench --example throughput --release # delivery throughput
Configuration
use tell::TellConfig;
// Production — collect.tell.rs:50000, batch=100, flush=10s
let config = TellConfig::production("feed1e11feed1e11feed1e11feed1e11").unwrap();
// Development — localhost:50000, batch=10, flush=2s
let config = TellConfig::development("feed1e11feed1e11feed1e11feed1e11").unwrap();
// Custom — see crates/tell/examples/config.rs for all builder options
let config = TellConfig::builder("feed1e11feed1e11feed1e11feed1e11")
.service("my-backend") // stamped on every event and log
.endpoint("collect.internal:50000")
.on_error(|e| eprintln!("[Tell] {e}"))
.build()
.unwrap();
API
Tell is Clone + Send + Sync. Cloning is cheap (internally Arc).
let client = Tell::new(config)?;
// Events — user_id is always the first parameter
client.track(user_id, event_name, properties);
client.identify(user_id, traits);
client.group(user_id, group_id, properties);
client.revenue(user_id, amount, currency, order_id, properties);
client.alias(previous_id, user_id);
// Super properties — merged into every track/group/revenue call
client.register(props!{"app_version" => "2.0"});
client.unregister("app_version");
// Logging
client.log(level, message, service, data);
client.log_info(message, service, data);
client.log_error(message, service, data);
// + log_emergency, log_alert, log_critical, log_warning,
// log_notice, log_debug, log_trace
// Lifecycle
client.reset_session();
client.flush().await?;
client.close().await?;
Properties accept props!, Props::new(), Option<impl Serialize>, or None::<serde_json::Value>:
use tell::{props, Props};
// props! macro — fastest path
client.track("user_123", "Click", props! {
"url" => "/home",
"count" => 42,
"active" => true
});
// Props builder — for dynamic values
let p = Props::new()
.add("url", &request.path)
.add("status", response.status);
client.track("user_123", "Request", p);
// serde_json — works with any Serialize type
client.track("user_123", "Click", Some(json!({"url": "/home"})));
// No properties
client.track("user_123", "Click", None::<serde_json::Value>);
Requirements
- Rust: 2024 edition
- Runtime: Tokio 1.x
License
MIT
Dependencies
~4–18MB
~172K SLoC