Imported from briananderson1222/mqtt-recorder (
AGENTS.md). Install upstream withnpx skills add briananderson1222/mqtt-recorder. Copyright stays with the author.
AI Assistant Guidance for mqtt-recorder
This document provides guidance for AI assistants (such as GitHub Copilot, Claude, ChatGPT, etc.) when working with this repository.
About This File
This file serves as persistent context/memory for AI agents. Update it when:
- Adding new modules, features, or architectural changes
- The user repeatedly corrects the same behavior (capture the lesson here)
- Important patterns or conventions are established
- You learn something unique about this repository through trial and error
Do not reference this file in user-facing documentation (README, doc comments, etc.) unless explicitly requested.
Project Overview
mqtt-recorder is a Rust CLI tool for recording and replaying MQTT messages. It provides four operational modes:
- Record: Subscribe to MQTT topics and save messages to CSV
- Replay: Read messages from CSV and publish to a broker with timing preservation
- Mirror: Subscribe to an external broker and republish to an embedded broker
- Standalone Broker: Run an embedded MQTT broker
Additionally, it provides utility modes for CSV file management:
- Validate: Check CSV files for format correctness and data integrity
- Fix: Repair corrupted CSV files by re-encoding binary payloads
Architecture
src/
├── main.rs # Application entry point, mode dispatch, signal handling
├── lib.rs # Library exports
├── cli.rs # CLI argument parsing (clap)
├── mqtt.rs # MQTT client wrapper (rumqttc v4/v5)
├── broker.rs # Embedded broker (rumqttd)
├── csv_handler/ # CSV reading/writing, binary detection, auto-encoding
│ ├── mod.rs # Module exports
│ ├── reader.rs # CSV reading and decoding
│ ├── writer.rs # CSV writing and encoding
│ ├── record.rs # MessageRecord struct
│ └── encoding.rs # Binary detection and base64 encoding
├── tui/ # Interactive terminal UI (ratatui)
│ ├── mod.rs # Module exports
│ ├── state.rs # TUI state management
│ ├── render.rs # UI rendering
│ ├── input.rs # Keyboard input handling
│ └── types.rs # TUI type definitions
├── validator.rs # CSV validation logic
├── fixer.rs # CSV repair logic
├── topics.rs # Topic filter and JSON parsing
├── recorder.rs # Record mode handler
├── replayer.rs # Replay mode handler
├── mirror.rs # Mirror mode handler
├── util.rs # Shared utilities
└── error.rs # Custom error types (thiserror)
Key Dependencies
- clap: CLI argument parsing with derive macros
- rumqttc: MQTT client library
- rumqttd: Embedded MQTT broker
- tokio: Async runtime
- ratatui/crossterm: Terminal UI rendering and input
- serde/serde_json: JSON serialization
- csv: CSV file handling
- base64: Payload encoding
- chrono: Timestamp handling
- thiserror: Error handling (single error enum in src/error.rs; no anyhow)
Code Conventions
Commit Messages
Use Conventional Commits format:
feat:new featuresfix:bug fixesdocs:documentation changestest:adding/updating testsrefactor:code changes that neither fix bugs nor add featureschore:maintenance tasks
Error Handling
- Use
thiserrorfor defining error types insrc/error.rs - Return
Result<T, MqttRecorderError>from library functions - Provide descriptive error messages with context
Async Patterns
- Use
tokioruntime for all async operations - Use
tokio::sync::broadcastfor shutdown signaling - Handle graceful shutdown via signal handlers
Testing
- Unit tests are co-located in source files (
#[cfg(test)]modules) - Property-based tests use
proptestcrate intests/property/ - Integration tests in
tests/integration/(run against the embedded rumqttd broker on dynamic localhost ports; no Docker needed)
Documentation
- All public APIs have doc comments
- Use
///for item documentation - Use
//!for module-level documentation - Include examples in doc comments where helpful
Common Tasks
Adding a New CLI Argument
- Add the field to
Argsstruct insrc/cli.rs - Add validation logic in
Args::validate()if needed - Update the mode handlers to use the new argument
- Add tests for the new argument
- Update README.md CLI reference
Adding a New Mode
- Create a new mode handler in
src/<mode>.rs - Add the mode variant to
Modeenum insrc/cli.rs - Add validation rules in
Args::validate() - Add dispatch logic in
src/main.rs - Add tests and documentation
Modifying CSV Format
- Update
MessageRecordstruct insrc/csv_handler/record.rs - Update
CsvWriterandCsvReaderimplementations - Update property tests in
tests/property/csv_props.rs - Update README.md CSV format documentation
Testing Guidelines
Development Approach: Test-Driven Development (TDD)
This project follows strict TDD practices with property-based testing at its core:
- Write properties first: Before implementing a feature, define the properties it must satisfy
- Add property tests: Create property-based tests in
tests/property/that encode these properties - Implement the feature: Write the minimal code to make properties pass
- Add integration tests: For features involving external systems (MQTT broker, file I/O)
- Refactor: Clean up while keeping all tests green
Property-Based Testing (Required)
Property tests are the primary verification mechanism. When adding new functionality:
- Identify invariants and properties the code must maintain
- Look at existing property tests for patterns (e.g.,
tests/property/csv_props.rs) - Use
proptestcrate with appropriate strategies - Test edge cases through property generation, not manual enumeration
Example workflow for a new CSV feature:
// 1. Define the property
proptest! {
#[test]
fn roundtrip_preserves_data(record in any_message_record()) {
// Write then read should return identical data
let written = write_record(&record);
let read = read_record(&written);
prop_assert_eq!(record, read);
}
}
// 2. Then implement the feature to satisfy this property
Running Tests
# All tests
cargo test
# Unit tests only
cargo test --lib
# Property tests only
cargo test --test '*_props'
# Specific test
cargo test test_name
Property-Based Tests
Property tests verify correctness properties from the design document:
- Property 1-3: CLI argument parsing (tests/property/cli_props.rs)
- Property 4-6: Topic filtering (tests/property/topics_props.rs)
- Property 7-11: CSV handling (tests/property/csv_props.rs)
- Property 1-7: CSV validation and binary handling (tests/property/csv_validation_props.rs)
When modifying core logic, ensure property tests still pass.
Important Files
| File | Purpose |
|---|---|
Cargo.toml |
Dependencies and project configuration |
src/cli.rs |
All CLI arguments and validation |
src/error.rs |
Error type definitions |
src/csv_handler/ |
CSV format, binary detection, auto-encoding |
src/validator.rs |
CSV validation logic |
src/fixer.rs |
CSV repair logic |
.github/workflows/ci.yml |
CI pipeline configuration |
.github/workflows/release.yml |
Release automation |
Design Decisions
Why rumqttc + rumqttd?
rumqttcis a mature, async MQTT client for Rustrumqttdprovides an embedded broker for mirror/replay modes- Both are from the same project, ensuring compatibility
MQTT v4/v5 Split
- The embedded broker runs an MQTT v5 listener (rumqttd v5 config)
- Internal clients connecting to the embedded broker use
MqttClientV5(rumqttc v5 module) - External-facing clients (record mode, mirror source) use
MqttClient(rumqttc v4) for maximum compatibility with third-party brokers AnyMqttClientenum wraps either client type for components that connect to both (Replayer, Recorder, Mirror source)MqttIncomingprovides a unified event type so consumers don't depend on v4/v5 packet types directly- QoS types differ between v4 and v5 in rumqttc;
MqttClientV5converts internally
Why CSV for Storage?
- Human-readable and editable
- Compatible with spreadsheet tools
- Simple to parse and generate
- Supports streaming writes (no need to buffer entire file)
Why Base64 Encoding Option?
- MQTT payloads can be binary
- CSV doesn't handle binary data well
- Base64 ensures safe storage of any payload
Automatic Binary Detection
- When
--encode-b64is not set, payloads are automatically analyzed - Binary payloads (non-UTF8 or control characters) are auto-encoded with
b64:prefix - Text payloads are stored as-is for human readability
- The
b64:prefix allows distinguishing auto-encoded from literal content
Troubleshooting
Common Issues
- Connection refused: Check broker host/port, ensure broker is running
- TLS errors: Verify certificate paths and permissions
- CSV parsing errors: Check for proper escaping, verify encoding matches
- Binary payload corruption: Use
--validateto check file,--fixto repair
Debug Tips
- Use
RUST_LOG=debugfor verbose logging - Check exit codes for error category
- Verify argument combinations with
--help
Contact
For questions about this codebase, refer to:
- README.md for usage documentation
- CONTRIBUTING.md for contribution guidelines
Lessons Learned
Capture corrections and unique patterns discovered while working in this repo:
TUI and Logging
- When TUI is active (
--servewithout--no-interactive), suppress alleprintln!logging - Use
tui_state.is_some()or atui_activeboolean to conditionally log - Logging interferes with ratatui terminal rendering and corrupts the display
- Keep logging enabled for
--no-interactiveand CI modes
Verify Mode (--verify on mirror)
--verifyspawns an independent MqttClientV5 subscriber on the embedded broker that subscribes to#- Compares raw
(topic, payload_bytes)from the source against what actually arrives on the embedded broker — completely bypasses CSV - Three mismatch categories: PAYLOAD MISMATCH (same topic, different bytes), UNEXPECTED (message on broker not in expected queue), MISSING (expected but never received)
- All mismatches log the actual payload content (text or hex) to the audit log for debugging
- Use
--verifyas the primary debugging tool when investigating data integrity issues between source and embedded broker — if verify shows clean matches but CSV output differs, the bug is in the CSV layer - The verify subscriber is registered as an internal client so it doesn't inflate the broker connection count
TUI State as Source of Truth
- TUI state (
TuiState) should NEVER be the source of truth for initialization data — resolve config values (file paths, flags) from CLI args before creating TuiState - TUI state IS the correct source of truth for runtime user intent (toggle states set via keyboard input)
- Counters (
received_count,recorded_count,verify_matched, etc.) and audit log are currently coupled toTuiState, which only exists when--serveis active. Non-serve modes use local variables andinfo!()for final summaries. - Design debt: Extract a
SessionStatsstruct (counters + audit) that lives independently of TUI.TuiStatewould hold a reference to it. All mode handlers would receiveSessionStatsregardless of--serve. Do this when adding--json-summary, metrics endpoints, or daemon mode.
Unified Serve Mode
- When
--serve+--hostare both present, all modes route through the mirror event loop (run_mirror_mode). The--modeflag only sets initial toggle states (record ON/OFF, mirror ON/OFF, playback ON/OFF). run_record_modeonly runs without--serve(simple CLI recording, no TUI)run_replay_modeonly runs for--servewithout--host(playback to embedded broker, no source)- This ensures all TUI toggles (record/mirror/playback) actually work regardless of which
--modewas specified