Skip to main content

MCAP Logging

Cerulion Core supports MCAP (Modular Container Format) logging, similar to ROS2 bag recording. You can enable logging on individual publishers/subscribers or use a standalone recorder.

What is MCAP Logging?

MCAP is a container format for storing timestamped message data. It’s similar to ROS2 bag files but with better performance and cross-language support. Cerulion Core’s MCAP logging lets you record messages for later playback, analysis, or debugging.
MCAP logging is opt-in. You choose which publishers or subscribers should log their messages. This gives you fine-grained control over what gets recorded.

Features

Automatic Schema Registration

Schemas are automatically registered when channels are created

Thread-Safe

Uses background thread for async writes to avoid blocking

Compression Support

Optional LZ4 or Zstandard compression

Timestamp Preservation

Messages are logged with nanosecond-precision timestamps

Creating an MCAP Recorder

Basic Recorder

use cerulion_core::prelude::*;
use std::sync::Arc;

// Create MCAP recorder
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, None)?
);
Parameters:
  • filename: Path to the MCAP file (e.g., "log.mcap")
  • compression: Optional compression (None = no compression, Some(Compression::Lz4), Some(Compression::Zstandard))
  • schema_registry: Optional schema registry for ROS2 message types
The recorder is wrapped in Arc so it can be shared across multiple publishers/subscribers. This ensures all messages are written to the same file.

Opt-in Logging on Publisher

Enable logging on a publisher to record all sent messages:
use cerulion_core::prelude::*;
use std::sync::Arc;

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
    temperature: f32,
    timestamp: u64,
}

// Create MCAP recorder
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, None)?
);

// Create publisher
let mut publisher = Publisher::<SensorData>::create("sensors/data")?;

// Enable MCAP logging
publisher.enable_mcap_logging(recorder.clone());

// Messages will now be logged automatically when sent
let data = SensorData {
    temperature: 23.5,
    timestamp: 1234567890,
};
publisher.send(data)?;  // Automatically logged to MCAP
Once logging is enabled, all messages sent via publisher.send() are automatically logged to the MCAP file. No additional code needed.

Opt-in Logging on Subscriber

Enable logging on a subscriber to record all received messages:
use cerulion_core::prelude::*;
use std::sync::Arc;

// Create MCAP recorder
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, None)?
);

// Create subscriber
let mut subscriber = Subscriber::<SensorData>::create("sensors/data", None)?;

// Enable MCAP logging
subscriber.enable_mcap_logging(recorder.clone());

// Messages will be logged automatically when received
if let Ok(Some(data)) = subscriber.receive() {
    // Message is automatically logged to MCAP
    println!("Received: {:?}", data);
}
Subscriber logging is useful when you want to record messages from external sources (e.g., ROS2 nodes via RCL Hooks) or when you want to log only specific topics.

Standalone Recorder

Use a standalone recorder to record messages for a specific duration:
use cerulion_core::prelude::*;
use std::sync::Arc;
use std::time::{Duration, Instant};

// Create standalone recorder
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, None)?
);

// Create subscriber
let subscriber = Subscriber::<SensorData>::create("sensors/data", None)?;

// Record messages for a duration
let start = Instant::now();
recorder.record_from_subscriber(&subscriber, "sensors/data", || {
    start.elapsed() < Duration::from_secs(10)  // Record for 10 seconds
})?;
The standalone recorder pattern is useful for time-limited recording or when you want to record messages from a subscriber without modifying the subscriber itself.

Compression

MCAP supports optional compression to reduce file size:
use cerulion_core::prelude::*;

// No compression (faster, larger files)
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, None)?
);

// LZ4 compression (good balance)
let recorder = Arc::new(
    McapRecorder::create("log.mcap", Some(Compression::Lz4), None)?
);

// Zstandard compression (better compression, slower)
let recorder = Arc::new(
    McapRecorder::create("log.mcap", Some(Compression::Zstandard), None)?
);
For real-time logging, use no compression or LZ4. For archival storage, use Zstandard for better compression ratios.

Schema Registry Integration

MCAP can use an existing schema registry for ROS2 message types:
use cerulion_core::prelude::*;

// Create schema registry (if you have ROS2 message definitions)
let schema_registry = Some(/* your schema registry */);

// Create recorder with schema registry
let recorder = Arc::new(
    McapRecorder::create("log.mcap", None, schema_registry)?
);
Schema registry integration is useful when logging ROS2 messages via RCL Hooks. It ensures proper schema registration in the MCAP file for ROS2 message types.

Complete Example

Here’s a complete example with publisher logging:
use cerulion_core::prelude::*;
use std::sync::Arc;
use std::time::Duration;

#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct SensorData {
    temperature: f32,
    pressure: f32,
    timestamp: u64,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create MCAP recorder
    let recorder = Arc::new(
        McapRecorder::create("sensor_log.mcap", Some(Compression::Lz4), None)?
    );
    
    // Create publisher
    let mut publisher = Publisher::<SensorData>::create("sensors/data")?;
    
    // Enable MCAP logging
    publisher.enable_mcap_logging(recorder.clone());
    
    // Send messages (automatically logged)
    for i in 0..100 {
        let data = SensorData {
            temperature: 20.0 + (i as f32) * 0.1,
            pressure: 1013.25 + (i as f32) * 0.01,
            timestamp: i as u64,
        };
        publisher.send(data)?;
        
        std::thread::sleep(Duration::from_millis(100));
    }
    
    // Recorder automatically closes when dropped
    // File is ready for playback/analysis
    
    Ok(())
}

Reading MCAP Files

MCAP files can be read using the MCAP library or tools:
# List topics in MCAP file
mcap info log.mcap

# Extract messages to JSON
mcap extract log.mcap --output messages.json

# Playback messages
mcap playback log.mcap
MCAP files are self-contained and include schemas, metadata, and message data. They can be read by any MCAP-compatible tool or library.

Best Practices

1. Use Arc for Sharing

// ✅ Good: Share recorder across multiple publishers/subscribers
let recorder = Arc::new(McapRecorder::create("log.mcap", None, None)?);
publisher1.enable_mcap_logging(recorder.clone());
publisher2.enable_mcap_logging(recorder.clone());

2. Choose Appropriate Compression

// ✅ Good: LZ4 for real-time logging
let recorder = Arc::new(
    McapRecorder::create("log.mcap", Some(Compression::Lz4), None)?
);

// ✅ Good: Zstandard for archival
let recorder = Arc::new(
    McapRecorder::create("archive.mcap", Some(Compression::Zstandard), None)?
);

3. Log Only What You Need

// ✅ Good: Log only specific topics
let recorder = Arc::new(McapRecorder::create("log.mcap", None, None)?);
important_publisher.enable_mcap_logging(recorder.clone());
// Don't log debug/verbose topics

4. Handle File Errors

// ✅ Good: Handle file creation errors
match McapRecorder::create("log.mcap", None, None) {
    Ok(recorder) => {
        publisher.enable_mcap_logging(Arc::new(recorder));
    }
    Err(e) => {
        eprintln!("Failed to create MCAP recorder: {}", e);
        // Continue without logging
    }
}

Troubleshooting

”Failed to create MCAP file”

Problem: File path is invalid or permissions issue. Solution: Check file path and permissions:
// ✅ Good: Use absolute path or check current directory
let recorder = McapRecorder::create("/absolute/path/log.mcap", None, None)?;

“MCAP file is empty”

Problem: Recorder dropped before messages were sent. Solution: Keep recorder alive:
// ✅ Good: Keep recorder in scope
let recorder = Arc::new(McapRecorder::create("log.mcap", None, None)?);
publisher.enable_mcap_logging(recorder.clone());
// ... send messages ...
// Recorder closes when dropped (at end of scope)

“High CPU usage during logging”

Problem: Compression is too slow for high-frequency messages. Solution: Use faster compression or disable it:
// ✅ Good: Use LZ4 or no compression for high-frequency data
let recorder = Arc::new(
    McapRecorder::create("log.mcap", Some(Compression::Lz4), None)?
);

Next Steps