ributes control schema mapping.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryPacket {
#[serde(rename = "device_id")]
pub identifier: u64,
pub payload: Vec<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<String>,
#[serde(default)]
pub is_encrypted: bool,
#[serde(skip)]
pub internal_checksum: u32,
}
Architecture Rationale:
rename aligns Rust's snake_case conventions with external API requirements without polluting internal code.
skip_serializing_if reduces payload size by omitting None values, critical for network transmission.
default ensures backward compatibility when deserializing older payloads missing new fields.
skip prevents internal state from leaking into external representations.
Step 2: Typed Error Routing with thiserror
Libraries must expose structured errors so consumers can handle specific failure modes programmatically.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PipelineError {
#[error("invalid telemetry format: {reason}")]
ParseFailure { reason: String },
#[error("device {device_id} rejected: {cause}")]
ValidationFailed { device_id: u64, cause: String },
#[error("storage backend unavailable")]
IoError(#[from] std::io::Error),
}
Architecture Rationale:
#[from] automatically implements From<std::io::Error> for PipelineError, enabling the ? operator to propagate I/O failures seamlessly.
- Structured variants allow downstream code to match on specific conditions rather than parsing error strings.
Step 3: Application-Level Error Handling with anyhow
Application binaries should prioritize developer ergonomics and context accumulation over strict type matching.
use anyhow::{Context, Result, bail};
use std::fs;
pub fn load_telemetry_config(path: &str) -> Result<String> {
let raw = fs::read_to_string(path)
.with_context(|| format!("unable to access configuration at {path}"))?;
if raw.trim().is_empty() {
bail!("configuration file contains no data");
}
Ok(raw)
}
pub fn run_pipeline() -> Result<()> {
let config = load_telemetry_config("pipeline.toml")
.context("initialization phase failed")?;
println!("Loaded {} bytes of pipeline configuration", config.len());
Ok(())
}
Architecture Rationale:
anyhow::Result acts as a universal error container, eliminating the need to define a top-level error enum for the binary.
.context() chains debugging information without modifying the underlying error type.
bail! provides early exits with formatted messages, replacing verbose return Err(...) patterns.
Step 4: Statistical Benchmarking with Criterion
Performance validation requires controlled execution environments and statistical analysis.
// benches/pipeline_bench.rs
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use telemetry_pipeline::TelemetryPacket;
fn transform_packet(packet: &TelemetryPacket) -> Vec<f32> {
packet.payload.iter().map(|v| v * 1.5).collect()
}
fn bench_transform(c: &mut Criterion) {
let mut group = c.benchmark_group("transform");
for size in [100, 1_000, 10_000] {
let sample = TelemetryPacket {
identifier: 1,
payload: vec![1.0; size],
metadata: None,
is_encrypted: false,
internal_checksum: 0,
};
group.bench_with_input(
BenchmarkId::new("scale", size),
&sample,
|b, pkt| b.iter(|| transform_packet(black_box(pkt))),
);
}
group.finish();
}
criterion_group!(benches, bench_transform);
criterion_main!(benches);
Architecture Rationale:
black_box prevents the compiler from optimizing away the benchmarked computation by treating the input as externally observable.
bench_with_input scales tests across multiple data sizes, revealing algorithmic complexity trends.
- Criterion runs iterations in a controlled environment, measures variance, and generates HTML reports with regression detection.
Pitfall Guide
1. Library Error Leakage
Explanation: Using anyhow::Error in a library crate forces consumers to handle opaque errors, preventing them from matching on specific failure conditions.
Fix: Reserve anyhow exclusively for binary crates. Libraries must define typed error enums using thiserror to maintain API contract clarity.
2. Benchmark Optimization Artifacts
Explanation: Forgetting black_box allows LLVM to constant-fold or eliminate the benchmarked code path, reporting near-zero execution times.
Fix: Always wrap inputs and outputs in black_box() within b.iter() closures. Verify benchmarks by temporarily adding a std::thread::sleep and confirming the reported time increases.
3. Serde Schema Drift
Explanation: Adding new fields to a struct without #[serde(default)] breaks deserialization of legacy payloads, causing runtime panics in production.
Fix: Apply #[serde(default)] to all new optional or boolean fields. Maintain a versioned migration strategy for breaking schema changes.
4. Workspace Dependency Fragmentation
Explanation: Multi-crate workspaces often specify different versions of the same dependency across Cargo.toml files, causing duplicate compilation and type incompatibility.
Fix: Centralize version constraints in the workspace root [workspace.dependencies] section. Reference crates using workspace = true in member manifests.
5. Criterion Harness Conflicts
Explanation: Omitting harness = false in Cargo.toml causes Cargo to run Criterion benchmarks alongside standard #[test] functions, resulting in compilation errors or skipped measurements.
Fix: Always include [[bench]] sections with harness = false for Criterion files. Keep unit tests in src/ and benchmarks in benches/.
6. Enum Tagging Collisions
Explanation: Using #[serde(tag = "type")] with enum variants that contain a field named type causes serialization conflicts and malformed JSON.
Fix: Choose tag names that do not overlap with variant field names, or switch to #[serde(untagged)] if external schema constraints allow it.
7. Over-Engineering Dynamic JSON
Explanation: Defaulting to serde_json::Value for all JSON handling sacrifices compile-time safety and performance for flexibility that is rarely needed.
Fix: Use strongly-typed structs with #[derive(Deserialize)] for known schemas. Reserve Value only for truly dynamic payloads, such as webhook extensions or user-defined metadata.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal library API | thiserror + typed enums | Enables precise error matching and stable contracts | Low (initial setup) |
| Application binary | anyhow + .context() | Reduces boilerplate and improves debugging velocity | Low (negligible overhead) |
| Known external schema | #[derive(Deserialize)] structs | Compile-time validation and optimal memory layout | Low |
| Unknown/dynamic schema | serde_json::Value | Flexibility for unstructured payloads | Medium (runtime parsing cost) |
| Performance validation | criterion + black_box | Statistical rigor and regression detection | Low (CI integration) |
| Quick timing checks | std::time::Instant | Minimal setup for non-critical measurements | None (but unreliable) |
Configuration Template
# Cargo.toml (Workspace Root)
[workspace]
members = ["crates/telemetry-lib", "crates/telemetry-cli"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
anyhow = "1.0"
criterion = { version = "0.5", features = ["html_reports"] }
# crates/telemetry-lib/Cargo.toml
[package]
name = "telemetry-lib"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
# crates/telemetry-cli/Cargo.toml
[package]
name = "telemetry-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
telemetry-lib = { path = "../telemetry-lib" }
anyhow = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
[[bench]]
name = "pipeline_bench"
harness = false
Quick Start Guide
- Initialize Workspace: Run
cargo new --lib telemetry-lib and cargo new telemetry-cli, then create a root Cargo.toml with the workspace configuration template above.
- Add Dependencies: Populate each crate's
Cargo.toml using workspace = true references to ensure version alignment.
- Define Models & Errors: Implement your data structures with Serde derives in the library crate, and define
thiserror enums for domain-specific failures.
- Wire Application Logic: In the CLI crate, use
anyhow::Result for main() and chain .context() on fallible operations.
- Validate Performance: Create
benches/ directory, add Criterion configuration, and run cargo bench to generate statistical reports before merging performance-sensitive changes.