Skip to main content

Plugin API

Enterprise Feature

The Plugin API is available in HDDS Viewer Enterprise Edition.

The Plugin API allows you to extend hdds_viewer with custom analyzers, exporters, and UI panels. Plugins are loaded as dynamic libraries (.so/.dll/.dylib) or WebAssembly modules.

Overview

Plugins can:

  • Analyze frames in real-time (on_frame callback)
  • Detect custom patterns and anomalies
  • Add custom QoS rules specific to your domain
  • Export data to external systems
  • Render custom UI panels

Quick Start

1. Create a Plugin Project

cargo new --lib my-hdds-plugin
cd my-hdds-plugin

2. Configure Cargo.toml

[package]
name = "my-hdds-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
viewer-plugin = "0.1"
log = "0.4"

3. Implement the Plugin

use viewer_plugin::prelude::*;

pub struct MyPlugin {
threshold: usize,
alert_count: usize,
}

impl Default for MyPlugin {
fn default() -> Self {
Self {
threshold: 10_000,
alert_count: 0,
}
}
}

impl ViewerPlugin for MyPlugin {
fn name(&self) -> &str { "my-custom-analyzer" }
fn version(&self) -> &str { "1.0.0" }
fn description(&self) -> &str { "Custom analysis for my domain" }
fn author(&self) -> &str { "My Company <dev@example.com>" }

fn initialize(&mut self, ctx: &PluginContext) -> Result<()> {
// Load configuration
if let Some(t) = ctx.get_config_i64("threshold") {
self.threshold = t as usize;
}
log::info!("MyPlugin initialized with threshold={}", self.threshold);
Ok(())
}

fn on_frame(&mut self, frame: &PluginFrame) -> Result<PluginAction> {
// Analyze each frame
if frame.payload_size > self.threshold {
self.alert_count += 1;
return Ok(PluginAction::Alert {
severity: Severity::Warning,
message: format!(
"Large payload on {}: {} bytes",
frame.topic_name, frame.payload_size
),
});
}
Ok(PluginAction::Continue)
}

fn on_session_stop(&mut self) -> Result<()> {
log::info!("Session ended. Total alerts: {}", self.alert_count);
Ok(())
}
}

// Export the plugin
viewer_plugin::export_plugin!(MyPlugin);

4. Build the Plugin

cargo build --release
# Output: target/release/libmy_hdds_plugin.so

5. Configure the Plugin

Create ~/.config/hdds-viewer/plugins/my-plugin.toml:

[plugin]
name = "my-custom-analyzer"
path = "/path/to/libmy_hdds_plugin.so"
enabled = true

[settings]
threshold = "5000"

6. Launch hdds_viewer

The plugin will be loaded automatically on startup.

Plugin Trait Reference

pub trait ViewerPlugin: Send + Sync {
// Required: Metadata
fn name(&self) -> &str;
fn version(&self) -> &str;
fn description(&self) -> &str;
fn author(&self) -> &str;

// Required: Initialization
fn initialize(&mut self, ctx: &PluginContext) -> Result<()>;

// Optional: Session lifecycle
fn on_session_start(&mut self, mode: SessionMode) -> Result<()>;
fn on_session_stop(&mut self) -> Result<()>;

// Optional: Frame processing (hot path!)
fn on_frame(&mut self, frame: &PluginFrame) -> Result<PluginAction>;

// Optional: Topology analysis
fn analyze_topology(&self, topology: &PluginTopology) -> Vec<RuleDetection>;

// Optional: Shutdown
fn shutdown(&mut self) -> Result<()>;
}

Types

PluginFrame

Read-only view of a captured frame:

pub struct PluginFrame {
pub timestamp_nanos: u64,
pub topic_id: u16,
pub topic_name: String,
pub type_id: u16,
pub type_name: String,
pub qos_hash: u32,
pub payload_size: usize,
pub payload: Vec<u8>, // CDR2 encoded
}

PluginAction

Return value from on_frame():

pub enum PluginAction {
Continue, // Normal processing
Drop, // Filter out this frame
Alert { severity, message }, // Trigger notification
Export { destination, data }, // Export to external system
Detection(RuleDetection), // Report a rule detection
}

RuleDetection

Custom rule detection result:

pub struct RuleDetection {
pub rule_id: String, // e.g., "AEROSPACE_TIMING_001"
pub description: String,
pub severity: Severity,
pub topic: Option<String>,
pub confidence: f64, // 0.0 to 1.0
pub suggested_fix: Option<String>,
pub timestamp_nanos: u64,
}

Severity

pub enum Severity {
Info, // Informational
Warning, // Potential issue
Error, // Definite problem
Critical, // Requires immediate attention
}

Configuration

Plugin Context

The PluginContext provides access to configuration and shared state:

impl PluginContext {
pub fn get_config(&self, key: &str) -> Option<&str>;
pub fn get_config_i64(&self, key: &str) -> Option<i64>;
pub fn get_config_f64(&self, key: &str) -> Option<f64>;
pub fn get_config_bool(&self, key: &str) -> Option<bool>;
}

Configuration File

[plugin]
name = "my-plugin"
path = "/opt/hdds-plugins/libmy_plugin.so"
enabled = true
priority = 10 # Lower = loaded first

[settings]
# Plugin-specific settings (string key-value)
threshold = "1000"
export_url = "https://monitoring.example.com/api"
debug_mode = "false"

Plugin Directories

Plugins are scanned from these directories:

PlatformSystem DirectoryUser Directory
Linux/usr/share/hdds-viewer/plugins/~/.config/hdds-viewer/plugins/
macOS/Library/Application Support/HDDS Viewer/Plugins/~/Library/Application Support/HDDS Viewer/plugins/
Windows%PROGRAMDATA%\HDDS Viewer\Plugins\%APPDATA%\HDDS Viewer\plugins\

Security

Signature Verification

In production, plugins must be signed with Ed25519:

# Sign a plugin (build tool)
hdds-plugin-sign --key private.key libmy_plugin.so

# Output: libmy_plugin.so.sig

The viewer verifies the signature before loading.

Dev Mode

For development, signature verification can be skipped:

HDDS_PLUGIN_DEV_MODE=1 hdds-viewer
warning

Never use dev mode in production!

Best Practices

Performance

The on_frame() method is called for every frame (potentially millions). Keep it fast:

fn on_frame(&mut self, frame: &PluginFrame) -> Result<PluginAction> {
// Good: O(1) operations
if frame.payload_size > self.threshold {
return Ok(PluginAction::Alert { ... });
}

// Bad: Heavy computation on every frame
// let analysis = expensive_ml_inference(&frame.payload);

Ok(PluginAction::Continue)
}

For heavy computation, use analyze_topology() which is called periodically.

Thread Safety

Plugins must be Send + Sync. Use Arc<RwLock<...>> for shared state:

use std::sync::Arc;
use parking_lot::RwLock;

pub struct MyPlugin {
shared_state: Arc<RwLock<SharedState>>,
}

Error Handling

Never panic in plugin code. Return Err(...) instead:

fn on_frame(&mut self, frame: &PluginFrame) -> Result<PluginAction> {
let data = parse_payload(&frame.payload)
.map_err(|e| PluginError::other(format!("Parse failed: {}", e)))?;
Ok(PluginAction::Continue)
}

Example: Aerospace Timing Analyzer

Complete example for DO-178C timing compliance:

use viewer_plugin::prelude::*;
use std::collections::HashMap;

pub struct AerospaceTimingPlugin {
max_latency_ms: f64,
topic_latencies: HashMap<String, Vec<f64>>,
violations: Vec<RuleDetection>,
}

impl Default for AerospaceTimingPlugin {
fn default() -> Self {
Self {
max_latency_ms: 10.0, // DO-178C requirement
topic_latencies: HashMap::new(),
violations: Vec::new(),
}
}
}

impl ViewerPlugin for AerospaceTimingPlugin {
fn name(&self) -> &str { "aerospace-timing" }
fn version(&self) -> &str { "1.0.0" }
fn description(&self) -> &str { "DO-178C timing compliance checker" }
fn author(&self) -> &str { "Aerospace Corp" }

fn initialize(&mut self, ctx: &PluginContext) -> Result<()> {
if let Some(max) = ctx.get_config_f64("max_latency_ms") {
self.max_latency_ms = max;
}
Ok(())
}

fn on_session_start(&mut self, _mode: SessionMode) -> Result<()> {
self.topic_latencies.clear();
self.violations.clear();
Ok(())
}

fn analyze_topology(&self, _topology: &PluginTopology) -> Vec<RuleDetection> {
let mut detections = Vec::new();

for (topic, latencies) in &self.topic_latencies {
if latencies.is_empty() {
continue;
}

let max = latencies.iter().cloned().fold(0.0_f64, f64::max);
let p99_idx = (latencies.len() as f64 * 0.99) as usize;
let mut sorted = latencies.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p99 = sorted.get(p99_idx).copied().unwrap_or(max);

if p99 > self.max_latency_ms {
detections.push(RuleDetection {
rule_id: "AEROSPACE_TIMING_001".to_string(),
description: format!(
"Topic '{}' P99 latency {:.2}ms exceeds {:.2}ms limit",
topic, p99, self.max_latency_ms
),
severity: Severity::Critical,
topic: Some(topic.clone()),
confidence: 0.95,
suggested_fix: Some(
"Review network path and QoS settings for this topic".to_string()
),
timestamp_nanos: 0,
});
}
}

detections
}

fn get_detections(&self) -> Vec<RuleDetection> {
self.violations.clone()
}
}

viewer_plugin::export_plugin!(AerospaceTimingPlugin);

See Also