Learning Path and Next Steps
What you'll learn: A structured learning roadmap (weeks 1–2, months 1–3+), recommended books and resources, common pitfalls for C# developers (ownership confusion, fighting the borrow checker), and structured observability with
tracingvsILogger.Difficulty: 🟢 Beginner
Immediate Next Steps (Week 1-2)
-
Set up your environment
- Install Rust via rustup.rs
- Configure VS Code with rust-analyzer extension
- Create your first
cargo new hello_worldproject
-
Master the basics
- Practice ownership with simple exercises
- Write functions with different parameter types (
&str,String,&mut) - Implement basic structs and methods
-
Error handling practice
- Convert C# try-catch code to Result-based patterns
- Practice with
?operator andmatchstatements - Implement custom error types
Intermediate Goals (Month 1-2)
-
Collections and iterators
- Master
Vec<T>,HashMap<K,V>, andHashSet<T> - Learn iterator methods:
map,filter,collect,fold - Practice with
forloops vs iterator chains
- Master
-
Traits and generics
- Implement common traits:
Debug,Clone,PartialEq - Write generic functions and structs
- Understand trait bounds and where clauses
- Implement common traits:
-
Project structure
- Organize code into modules
- Understand
pubvisibility - Work with external crates from crates.io
Advanced Topics (Month 3+)
-
Concurrency
- Learn about
SendandSynctraits - Use
std::threadfor basic parallelism - Explore
tokiofor async programming
- Learn about
-
Memory management
- Understand
Rc<T>andArc<T>for shared ownership - Learn when to use
Box<T>for heap allocation - Master lifetimes for complex scenarios
- Understand
-
Real-world projects
- Build a CLI tool with
clap - Create a web API with
axumorwarp - Write a library and publish to crates.io
- Build a CLI tool with
Recommended Learning Resources
Books
- "The Rust Programming Language" (free online) - The official book
- "Rust by Example" (free online) - Hands-on examples
- "Programming Rust" by Jim Blandy - Deep technical coverage
Online Resources
- Rust Playground - Try code in browser
- Rustlings - Interactive exercises
- Rust by Example - Practical examples
Practice Projects
- Command-line calculator - Practice with enums and pattern matching
- File organizer - Work with filesystem and error handling
- JSON processor - Learn serde and data transformation
- HTTP server - Understand async programming and networking
- Database library - Master traits, generics, and error handling
Common Pitfalls for C# Developers
Ownership Confusion
// DON'T: Trying to use moved values
fn wrong_way() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // ERROR: s was moved
}
// DO: Use references or clone when needed
fn right_way() {
let s = String::from("hello");
borrows_string(&s);
println!("{}", s); // OK: s is still owned here
}
fn takes_ownership(s: String) { /* s is moved here */ }
fn borrows_string(s: &str) { /* s is borrowed here */ }
Fighting the Borrow Checker
// DON'T: Multiple mutable references
fn wrong_borrowing() {
let mut v = vec![1, 2, 3];
let r1 = &mut v;
// let r2 = &mut v; // ERROR: cannot borrow as mutable more than once
}
// DO: Limit scope of mutable borrows
fn right_borrowing() {
let mut v = vec![1, 2, 3];
{
let r1 = &mut v;
r1.push(4);
} // r1 goes out of scope here
let r2 = &mut v; // OK: no other mutable borrows exist
r2.push(5);
}
Expecting Null Values
// DON'T: Expecting null-like behavior
fn no_null_in_rust() {
// let s: String = null; // NO null in Rust!
}
// DO: Use Option<T> explicitly
fn use_option_instead() {
let maybe_string: Option<String> = None;
match maybe_string {
Some(s) => println!("Got string: {}", s),
None => println!("No string available"),
}
}
Final Tips
- Embrace the compiler - Rust's compiler errors are helpful, not hostile
- Start small - Begin with simple programs and gradually add complexity
- Read other people's code - Study popular crates on GitHub
- Ask for help - The Rust community is welcoming and helpful
- Practice regularly - Rust's concepts become natural with practice
Remember: Rust has a learning curve, but it pays off with memory safety, performance, and fearless concurrency. The ownership system that seems restrictive at first becomes a powerful tool for writing correct, efficient programs.
Congratulations! You now have a solid foundation for transitioning from C# to Rust. Start with simple projects, be patient with the learning process, and gradually work your way up to more complex applications. The safety and performance benefits of Rust make the initial learning investment worthwhile.
<!-- ch16.2a: Structured Observability with tracing -->Structured Observability: tracing vs ILogger and Serilog
C# developers are accustomed to structured logging via ILogger, Serilog, or NLog — where log messages carry typed key-value properties. Rust's log crate provides basic leveled logging, but tracing is the production standard for structured observability with spans, async awareness, and distributed tracing support.
Why tracing Over log
| Feature | log crate | tracing crate | C# Equivalent |
|---|---|---|---|
| Leveled messages | ✅ info!(), error!() | ✅ info!(), error!() | ILogger.LogInformation() |
| Structured fields | ❌ String interpolation only | ✅ Typed key-value fields | Serilog Log.Information("{User}", user) |
| Spans (scoped context) | ❌ | ✅ #[instrument], span!() | ILogger.BeginScope() |
| Async-aware | ❌ Loses context across .await | ✅ Spans follow across .await | Activity / DiagnosticSource |
| Distributed tracing | ❌ | ✅ OpenTelemetry integration | System.Diagnostics.Activity |
| Multiple output formats | Basic | JSON, pretty, compact, OTLP | Serilog sinks |
Getting Started
# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
Basic Usage: Structured Logging
// C# Serilog
Log.Information("Processing order {OrderId} for {Customer}, total {Total:C}",
orderId, customer.Name, order.Total);
// Output: Processing order 12345 for Alice, total $99.95
// JSON: {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...}
use tracing::{info, warn, error, debug, instrument};
// Structured fields — typed, not string-interpolated
info!(order_id = 12345, customer = "Alice", total = 99.95,
"Processing order");
// Output: INFO Processing order order_id=12345 customer="Alice" total=99.95
// JSON: {"order_id": 12345, "customer": "Alice", "total": 99.95, ...}
// Dynamic values
let order_id = 12345;
info!(order_id, "Order received"); // field name = variable name shorthand
// Conditional fields
if let Some(promo) = promo_code {
info!(order_id, promo_code = %promo, "Promo applied");
// ^ % means use Display formatting
// ? would use Debug formatting
}
Spans: The Killer Feature for Async Code
Spans are scoped contexts that carry fields across function calls and .await points — like ILogger.BeginScope() but async-safe.
// C# — Activity / BeginScope
using var activity = new Activity("ProcessOrder").Start();
activity.SetTag("order_id", orderId);
using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
{
_logger.LogInformation("Starting processing");
await ProcessPaymentAsync();
_logger.LogInformation("Payment complete"); // OrderId still in scope
}
use tracing::{info, instrument, Instrument};
// #[instrument] automatically creates a span with function args as fields
#[instrument(skip(db), fields(customer_name))]
async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> {
let order = db.get_order(order_id).await?;
// Add a field to the current span dynamically
tracing::Span::current().record("customer_name", &order.customer_name.as_str());
info!("Starting processing");
process_payment(&order).await?; // span context preserved across .await!
info!(items = order.items.len(), "Payment complete");
Ok(())
}
// Every log message inside this function automatically includes:
// order_id=12345 customer_name="Alice"
// Even in nested async calls!
// Manual span creation (like BeginScope)
async fn batch_process(orders: Vec<u64>, db: &Database) {
for order_id in orders {
let span = tracing::info_span!("process_order", order_id);
// .instrument(span) attaches the span to the future
process_order(order_id, db)
.instrument(span)
.await
.unwrap_or_else(|e| error!("Failed: {e}"));
}
}
Subscriber Configuration (Like Serilog Sinks)
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
fn init_tracing() {
// Development: human-readable, colored output
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
.with(fmt::layer().pretty()) // Colored, indented spans
.init();
}
fn init_tracing_production() {
// Production: JSON output for log aggregation (like Serilog JSON sink)
tracing_subscriber::registry()
.with(EnvFilter::new("my_app=info"))
.with(fmt::layer().json()) // Structured JSON
.init();
// Output: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
# Control log levels via environment variable (like Serilog MinimumLevel)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run # everything
Serilog → tracing Migration Cheat Sheet
| Serilog / ILogger | tracing | Notes |
|---|---|---|
Log.Information("{Key}", val) | info!(key = val, "message") | Fields are typed, not interpolated |
Log.ForContext("Key", val) | span.record("key", val) | Add fields to current span |
using BeginScope(...) | #[instrument] or info_span!() | Automatic with #[instrument] |
.WriteTo.Console() | fmt::layer() | Human-readable |
.WriteTo.Seq() / .File() | fmt::layer().json() + file redirect | Or use tracing-appender |
.Enrich.WithProperty() | span!(Level::INFO, "name", key = val) | Span fields |
LogEventLevel.Debug | tracing::Level::DEBUG | Same concept |
{@Object} destructuring | field = ?value (Debug) or %value (Display) | ? = Debug, % = Display |
OpenTelemetry Integration
# For distributed tracing (like System.Diagnostics + OTLP exporter)
[dependencies]
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-otlp = "0.14"
// Add OpenTelemetry layer alongside console output
use tracing_opentelemetry::OpenTelemetryLayer;
fn init_otel() {
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(opentelemetry_otlp::new_exporter().tonic())
.install_batch(opentelemetry_sdk::runtime::Tokio)
.expect("Failed to create OTLP tracer");
tracing_subscriber::registry()
.with(OpenTelemetryLayer::new(tracer)) // Send spans to Jaeger/Tempo
.with(fmt::layer()) // Also print to console
.init();
}
// Now #[instrument] spans automatically become distributed traces!