All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Repeated suppression summaries: Active summary emission now reports only newly suppressed events instead of re-emitting the same suppression counts on every interval.
- Redis summary state: Redis-backed storage now persists reported-summary cursors so active emission does not repeat summaries after state reloads.
- Emission retry behavior: Claimed summaries are rolled back if emission panics, allowing them to be retried instead of lost.
- Redis TTL preservation: Active-emission scans no longer refresh Redis TTLs when retained state is unchanged.
- Off-by-one in suppression count:
SuppressionCounter::new()was initializingsuppressed_countto1instead of0, causing the first suppressed event to be reported as "2 suppressed" instead of "1 suppressed". - Recursive summary throttling: The default summary formatter emitted via
tracing::warn!()which went through the same throttle layer, causing infinite nesting. Summaries are now emitted with a dedicated internal target (tracing_throttle::summary) that is statically exempted from throttling.
Zero-Copy Field Extraction
Implemented zero-copy field name extraction using Cow<'static, str>, reducing allocations by 50% for typical logging patterns.
Changes:
- Field names from tracing macros are now borrowed (
&'static str) instead of cloned - Reduces allocations from 6 to 3 per event with 3 fields
- No measurable performance regression in signature computation (~40ns)
- Real-world benefit: reduced allocator pressure under high-volume logging
Impact: Transparent optimization - normal user API unchanged.
EventSignature::new() signature changed
The EventSignature::new() method now accepts BTreeMap<Cow<'static, str>, Cow<'static, str>> instead of BTreeMap<String, String>.
Who's affected: Only advanced users who directly construct EventSignature objects. The builder API and normal logging usage are completely unchanged.
Migration:
// Before (v0.4.0)
let fields = BTreeMap::from([
("user".to_string(), "alice".to_string()),
]);
let sig = EventSignature::new("INFO", "test", &fields, None);
// After (v0.4.1) - use Cow
use std::borrow::Cow;
let fields = BTreeMap::from([
(Cow::Borrowed("user"), Cow::Borrowed("alice")),
]);
let sig = EventSignature::new("INFO", "test", &fields, None);
// Or: Use the unchanged simple() method
let sig = EventSignature::simple("INFO", "test");Technical details:
FieldVisitornow usesCow::Borrowed(field.name())for zero-copyEventMetadatafields updated to use Cow type- All extraction methods in
TracingRateLimitLayerupdated - 236 tests passing, including 9 new Cow-specific tests
- @LifeMoroz for reporting the field inclusion confusion in #1.
Field Inclusion Logic Inverted
Event field values are now included in signatures by default (previously excluded by default).
This breaking change addresses a fundamental UX issue: the old behavior was unintuitive and dangerous. When users wrote info!(user_id = 123, "User login"), they expected user_id=123 and user_id=456 to be treated as different events - because they ARE semantically different events. The old behavior silently ignored field values, making logs appear broken.
Before (v0.3.x):
// Fields EXCLUDED by default - counter-intuitive!
let layer = TracingRateLimitLayer::builder()
.with_policy(Policy::count_based(5).unwrap())
.build()
.unwrap();
info!(user_id = 123, "User login"); // Same signature
info!(user_id = 456, "User login"); // Same signature (WRONG - throttled together!)
// Had to opt-in to include fields:
let layer = TracingRateLimitLayer::builder()
.with_event_fields(vec!["user_id".to_string()]) // Explicit inclusion
.build()
.unwrap();After (v0.4.0):
// Fields INCLUDED by default - intuitive and correct!
let layer = TracingRateLimitLayer::builder()
.with_policy(Policy::count_based(5).unwrap())
.build()
.unwrap();
info!(user_id = 123, "User login"); // Signature includes user_id=123
info!(user_id = 456, "User login"); // Different signature (CORRECT!)
// Opt-out for high-cardinality fields:
let layer = TracingRateLimitLayer::builder()
.with_excluded_fields(vec!["request_id".to_string(), "trace_id".to_string()])
.build()
.unwrap();Migration Guide:
-
If you were using default configuration (no
.with_event_fields()):- Signatures now include ALL field values
- Identify high-cardinality fields (request_id, trace_id, timestamps, UUIDs)
- Exclude them explicitly:
.with_excluded_fields(vec![ "request_id".to_string(), "trace_id".to_string(), "span_id".to_string(), ])
-
If you were using
.with_event_fields():- Remove
.with_event_fields()calls (method no longer exists) - Invert the logic: exclude all OTHER fields instead:
// Before: .with_event_fields(vec!["user_id".to_string()]) // After - exclude everything EXCEPT user_id: .with_excluded_fields(vec![ "request_id".to_string(), "timestamp".to_string(), // ... list all OTHER fields ])
- Note: If you were including many fields, it's now simpler to exclude the few high-cardinality ones
- Remove
-
Memory Management Considerations:
- Monitor signature count with
.snapshot().signature_count() - Set appropriate
.with_max_signatures()limit based on cardinality - See updated documentation for cardinality analysis
- Monitor signature count with
.with_event_fields()- Replaced by.with_excluded_fields()(inverted logic)
.with_excluded_fields()- Opt-out from including specific fields in signatures- Use for high-cardinality fields (request_id, trace_id, UUIDs, timestamps)
- Prevents signature explosion
- Example:
.with_excluded_fields(vec!["request_id".to_string()])
- Default signature computation - Now includes
(level, target, message, ALL field values)- Previously:
(level, target, message)only - Field values define semantic meaning, so they belong in signatures
- More intuitive:
user_id=123anduser_id=456are different events
- Previously:
- lib.rs module documentation - Updated to reflect new field inclusion behavior
- Quick Start example - Added
.with_excluded_fields()demonstration - Memory Management section - Rewrote cardinality analysis for new behavior
- Cardinality table - Updated examples showing exclusion patterns
- All code examples - Consistently use
.with_excluded_fields()API - examples/basic.rs - Updated to use
.with_excluded_fields() - examples/policies.rs - Updated to use
.with_excluded_fields()
- Unintuitive field handling - Field values now correctly included by default
- Silent field value ignoring - Users no longer surprised by fields being excluded
- Documentation consistency - All docs now consistently describe v0.4.0 behavior
This breaking change was necessary because:
-
Principle of Least Surprise: When users write
info!(user_id = 123, "Login"), they expect theuser_idvalue to matter. Ignoring it by default violated this principle. -
Semantic Correctness: Field values define the semantic meaning of events.
user_id=123anduser_id=456are fundamentally different events and should be throttled independently. -
Safer Defaults: The old behavior could silently cause incorrect throttling. The new behavior is correct by default, with explicit opt-out for performance tuning.
-
User Feedback: Real-world testing revealed confusion about why fields were being ignored, leading to perceived bugs in log output.
-
Rust Idioms: Explicit exclusion follows Rust's philosophy: be explicit about what you DON'T want, rather than what you DO want.
- Comprehensive Test Coverage (51 new tests, +2,482 lines)
- Circuit breaker: race conditions, boundary conditions, clone behavior
- Emitter: panic recovery, callback ordering, error handling
- Limiter: fail-open behavior, policy application, concurrent access
- Storage: memory tracking accuracy, eviction sampling edge cases
- Redis storage: serialization, TTL, concurrent access, prefix isolation
- Total test count: 276 tests (225 unit + 39 integration + 12 Redis)
- Circuit breaker test flakiness due to timing sensitivity
- Clone behavior tests now correctly validate independent atomic state
- Removed unnecessary
.clone()calls on Copy types (clippy warnings) - Fixed Policy API usage in tests (time_window and exponential_backoff signatures)
- Removed JSON serialization tests (Redis uses bincode, not JSON)
- Simplified policy serialization tests to match actual implementation
-
Advanced Eviction Strategies (4 strategies total)
- LRU eviction (default) - evicts least recently used signatures
- Priority-based eviction - custom function determines importance
- Memory-based eviction - enforces byte limits with lock-free tracking
- Combined priority+memory - uses both constraints simultaneously
- New
.with_eviction_strategy()builder method - Sampling-based eviction (5-20 samples) for O(1) amortized performance
- Conservative memory estimation (~200 bytes per signature)
-
Human-Readable Suppression Summaries
- Event details (level, target, message) included in suppression summaries
- Makes it easier to identify which events were suppressed
- Helpful for quick diagnostics without needing to look up signature hashes
- Documentation Restructure
- Added comprehensive eviction examples to lib.rs API docs
- Organized features into categories (policies, eviction, other)
- Simplified README eviction section, refer to docs for details
- Updated "Why tracing-throttle?" to mention eviction strategies
- Refocused v1.0.0 roadmap on stability and production readiness
- Performance: Updated benchmarks showing 15M+ ops/sec with advanced eviction
- Memory tracking: Atomic memory accounting for lock-free operations
- Testing: 9 new integration tests for eviction strategies (42 total tests)
- Redis Storage Backend (behind
redis-storagefeature flag)- Distributed rate limiting across multiple application instances
- Automatic TTL-based cleanup of inactive signatures
- Connection pooling via
redis::aio::ConnectionManager - Fail-safe operation (continues if Redis unavailable)
- Custom serialization for Policy types containing Instant fields
- Complete Redis example with Docker Compose setup
- See
examples/redis.rsandexamples/redis/README.md
- Documentation Improvements
- Clarified that event field values are NOT included in signatures by default
- Added new "Event Signatures" section with clear examples
- Updated signature cardinality documentation with field behavior examples
- Added table showing memory impact of
.with_event_fields()configuration - References
tests/event_fields.rsfor working examples - Fixes confusion reported in #1
-
Active Suppression Summary Emission (requires
asyncfeature)- New
.with_active_emission(bool)builder method to enable automatic emission of suppression summaries - Summaries emitted as structured WARN-level tracing events at configurable intervals
- Background task managed via
EmitterHandlein spawned tokio task - Disabled by default (opt-in to prevent surprise behavior)
- Graceful shutdown via
.shutdown().awaitmethod
- New
-
Configurable Summary Formatting
- New
SummaryFormattertype:Arc<dyn Fn(&SuppressionSummary) + Send + Sync + 'static> - New
.with_summary_formatter()builder method for full control over emission format - Customize log level, message format, and structured fields
- Default formatter preserves existing behavior (WARN level with signature/count fields)
- Completely optional and backward compatible
- New
-
Token Bucket Rate Limiting Policy
- New default policy:
Policy::token_bucket(capacity, refill_rate) - Provides burst tolerance with natural recovery over time
- Replaces count-based policy as the recommended default
- Default configuration: 50 burst capacity, 1 token/sec (60/min sustained)
- Handles edge cases: time going backwards, fractional token accumulation
- 16 comprehensive tests including critical regression tests
- New default policy:
-
Metrics Integration Examples
- Prometheus integration pattern documented in
metricsmodule - OpenTelemetry integration pattern documented in
metricsmodule - Examples show periodic export using
snapshot()method - No additional dependencies required (examples use
ignoreattribute)
- Prometheus integration pattern documented in
-
Breaking: Default rate limiting policy changed from
count_based(100)totoken_bucket(50.0, 1.0)- Provides better behavior for intermittent issues (natural recovery)
- Users relying on count-based behavior must explicitly configure it
- Migration: Use
.with_policy(Policy::count_based(100).unwrap())to restore old behavior
-
Builder Structure: Removed
Debugderive fromTracingRateLimitLayerBuilder- Required to support function pointer field (
summary_formatter) - Does not affect normal usage (builders are rarely debugged)
- Required to support function pointer field (
-
Documentation: Moved implementation details from README to API documentation
- README is now ~170 lines shorter and more scannable
- Rate Limiting Policies section condensed (all policies in same format)
- Observability & Metrics section simplified
- Fail-Safe Operation reduced to one paragraph
- Memory Management reduced to two sentences with link to docs
- Added links to docs.rs for detailed information
-
Code Quality
- All 160 tests passing (123 unit + 9 integration + 4 shutdown + 24 doc + 2 ignored)
- Zero clippy warnings
- Comprehensive test coverage for new features
-
EmitterHandle: New handle type for controlling background emitter tasks
shutdown().await: Graceful shutdown with default 10-second timeoutshutdown_with_timeout(): Custom timeout support for flexible deadline controlis_running(): Check if emitter task is still active- Explicit shutdown requirement (no Drop implementation to prevent race conditions)
-
Structured Error Handling
ShutdownErrorenum with clear error types:TaskPanicked: Emitter task panicked during shutdownTaskCancelled: Task was cancelled before completionTimeout: Shutdown exceeded specified timeoutSignalFailed: Failed to send shutdown signal
- All errors properly surfaced to callers (no silent failures in production)
-
Shutdown Features
- Final emission support on shutdown (configurable via
emit_finalparameter) - Biased shutdown signal prioritization for fast, deterministic shutdown
- Panic safety with proper resource cleanup
- Comprehensive cancellation safety documentation
- Final emission support on shutdown (configurable via
-
Breaking:
EmitterHandle::shutdown()now returnsResult<(), ShutdownError>instead of()- Users must handle the Result (e.g.,
.await?or.await.expect("shutdown failed")) - Enables proper error handling in production applications
- Users must handle the Result (e.g.,
-
Breaking:
SummaryEmitter::start()signature changed to includeemit_finalparameter- Old:
start(emit_fn) -> EmitterHandle - New:
start(emit_fn, emit_final: bool) -> EmitterHandle
- Old:
-
Breaking: Removed
Dropimplementation fromEmitterHandleto prevent race conditions- Users must explicitly call
shutdown().awaitto stop emitter tasks - Prevents resource leaks and undefined behavior when tasks outlive handles
- Users must explicitly call
- Error Handling: Production builds now properly surface all errors instead of only logging in debug mode
- Shutdown Reliability:
- Biased
tokio::select!ensures shutdown signal is checked first - Prevents non-deterministic delays (up to 30 seconds) during shutdown
- Fast shutdown even under heavy load
- Biased
- Documentation:
- Added cancellation safety guarantees for spawned tasks
- Documented panic handling and resource cleanup semantics
- Clear examples showing proper shutdown patterns
- Type parameter documentation explaining
Send + 'staticrequirements
- Memory Safety: Added comments explaining Rust's drop semantics ensure no memory leaks even during panics
- Critical (P0): Shutdown race condition where Drop could signal shutdown without waiting for task completion
- Critical (P0): Non-deterministic shutdown delays by prioritizing shutdown signal in select! loop
- Critical (P0): Missing cancellation safety documentation
- Important (P1): Potential resource leaks if emitter task outlived the handle
- Important (P1): Errors swallowed in production builds
- Important (P1): No timeout support for hanging emit functions
- Added 12 dedicated shutdown tests (now 133 total tests: 102 unit + 9 rate limiting + 4 shutdown + 18 doc)
- Comprehensive edge case coverage:
- Panic recovery in emit functions (task continues after panic)
- Custom timeout behavior
- Concurrent shutdown safety (multiple emitters)
- Shutdown during active emission
- Final emission on shutdown
- Explicit shutdown requirement
- All tests pass with zero clippy warnings
- Updated to latest stable versions:
tracing0.1 → 0.1.41tracing-subscriber0.3 → 0.3.20ahash0.8 → 0.8.12dashmap6.0 → 6.1tokio1 → 1.48
This release focuses on production hardening with robust shutdown semantics. All P0 (critical) and P1 (important) issues from code review have been addressed. The crate is now battle-tested and ready for production use.
Migration Guide (from v0.1.0):
// Before (v0.1.0) - if you were using the async emitter
let handle = emitter.start(|summaries| {
// emit logic
});
drop(handle); // Shutdown via Drop (unsafe)
// After (v0.1.1) - explicit shutdown with error handling
let handle = emitter.start(|summaries| {
// emit logic
}, false); // false = don't emit final summaries
// Proper shutdown
handle.shutdown().await?; // Returns Result
// Or with custom timeout
handle.shutdown_with_timeout(Duration::from_secs(5)).await?;Note: Most users are not affected by breaking changes, as the async emitter functionality was added in v0.1.0 but not fully exposed or documented. The TracingRateLimitLayer API remains unchanged.
0.1.0 - 2025-11-25
-
Rate Limiting Policies
- Count-based policy: Allow N events then suppress
- Time-window policy: Allow K events per time period
- Exponential backoff policy: Emit at exponentially increasing intervals (1st, 2nd, 4th, 8th...)
- Custom policy support via
RateLimitPolicytrait
-
Event Signature System
- Compute signatures from (level, message, fields)
- Per-signature throttling for independent rate limiting
- Hash-based deduplication using ahash
-
Memory Management
- LRU eviction with configurable signature limits (default: 10,000)
- Approximate LRU using sampling for performance
- Support for unlimited signatures (with warnings)
- Memory usage: ~150-250 bytes per signature
-
Observability & Metrics
- Track events allowed, suppressed, and evicted
MetricsSnapshotfor point-in-time analysis- Suppression rate calculation
- Signature count monitoring
- Thread-safe atomic counters
-
Fail-Safe Circuit Breaker
- Three states: Closed, Open, HalfOpen
- Fail-open strategy to preserve observability
- Configurable failure threshold (default: 5)
- Automatic recovery after timeout (default: 30s)
- Panic protection using
catch_unwind
-
tracing Integration
TracingRateLimitLayerimplementingtracing::LayerFiltertrait implementation for layer composition- Builder pattern for configuration
- Input validation for all parameters
-
Hexagonal Architecture
- Clean separation: Domain → Application → Infrastructure
- Port & adapter pattern for Clock and Storage
- MockClock for deterministic testing
-
Concurrency
- Sharded storage using DashMap (16 shards)
- Lock-free atomic operations
- Thread-safe across all components
- Scales to 44M ops/sec with 8 threads
-
Testing
- 105 comprehensive tests (94 unit + 11 doc)
- Integration tests for circuit breaker
- Concurrent access stress tests
- Edge case coverage
-
README.md
- Quick start guide
- Feature overview
- Policy examples
- Memory management summary
- Performance benchmarks
- Observability guide
- Circuit breaker documentation
-
API Documentation (lib.rs)
- Comprehensive memory usage breakdown
- Signature cardinality analysis
- Configuration guidelines
- Production monitoring examples
- Memory profiling integration
-
Examples
basic.rs: Simple usage examplepolicies.rs: Different policy demonstrations
-
Benchmarks
- Signature computation benchmarks
- Single-threaded throughput tests
- Concurrent throughput tests
- Signature diversity scenarios
- Registry scaling tests
- GitHub Actions Workflows
test.yml: Multi-OS (Ubuntu, macOS, Windows) and multi-channel (stable, beta) testinglint.yml: Format checking, clippy, and documentation validationpublish.yml: Automated crates.io publishing on tags
-
Throughput
- 20M rate limiting decisions/sec (single-threaded)
- 44M ops/sec with 8 threads
- Excellent scaling with concurrent access
-
Latency
- Signature computation: 13-37ns (simple), 200ns (20 fields)
- Rate limit decision: ~50ns per operation
-
Memory
- Zero allocations in hot path
- Lock-free operations where possible
- Efficient sharded storage
tracing0.1 - Core tracing supporttracing-subscriber0.3 - Layer implementationahash0.8 - Fast non-cryptographic hashingdashmap6.0 - Concurrent hash maptokio1.0 (optional) - Async runtime for future features
This is the initial release of tracing-throttle, providing a production-ready foundation for log deduplication and rate limiting in Rust applications using the tracing ecosystem.
Breaking Changes: N/A (initial release)
Deprecations: None
Known Limitations:
- Field extraction from events is not yet implemented (signatures currently use empty fields)
- Suppression summaries planned for v0.2
- Graceful shutdown for async emitter planned for v0.2