Plugin API (Enterprise)

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> 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);