Rust
Rust SDK
Build ServiceRadar WebAssembly checker plugins in idiomatic Rust with the serviceradar-sdk-rust crate, targeting the same V2 plugin contract as the Go SDK.
serviceradar-sdk-rust is the Rust-native path to the same V2 plugin contract
the Go SDK implements. It targets WebAssembly and hides the low-level
host ABI behind concrete, serde-native domain types like PluginResult,
Metric, Widget, Event, and HttpClient.
Repository: https://code.carverauto.dev/carverauto/serviceradar-sdk-rust
The crate includes:
- Host-provided config loading.
-
Result construction and serialization for
serviceradar.plugin_result.v1. - Host logging.
- Host-proxied HTTP, TCP, UDP, and WebSocket helpers.
-
Policy input parsing and validation for
serviceradar.plugin_inputs.v1. - Device discovery / enrichment payload helpers for inventory plugins.
- Camera/media helpers and RTSP parsing/depacketization utilities.
- Signal schema and display contract references for package-managed logs and events.
The Go SDK remains the behavior reference for parity, but this crate aims for an idiomatic Rust interface rather than a line-for-line port.
Install
cargo add serviceradar-sdk-rust
The crate uses Rust edition 2024 (rust-version = 1.85). Import it under a short
alias:
use serviceradar_sdk_rust as sdk;Your first plugin
A plugin exports a single run_check function. sdk::execute runs your closure,
serializes the returned PluginResult, and submits it to the host.
use serviceradar_sdk_rust as sdk;
#[derive(Debug, serde::Deserialize)]
#[serde(default)]
struct Config {
url: String,
warn_ms: f64,
crit_ms: f64,
}
impl Default for Config {
fn default() -> Self {
Self {
url: "https://example.com/health".to_string(),
warn_ms: 0.0,
crit_ms: 0.0,
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn run_check() {
let _ = sdk::execute(|| {
let cfg = sdk::load_config_or_default::<Config>()?;
let response = sdk::HttpClient::default().get(&cfg.url)?;
let latency_ms = response.duration.as_millis() as f64;
let thresholds = sdk::Thresholds::new(cfg.warn_ms, cfg.crit_ms);
Ok(sdk::PluginResult::new()
.with_summary(format!("http {} in {:.0}ms", response.status, latency_ms))
.with_thresholds(latency_ms, thresholds.warn, thresholds.crit)
.with_metric_spec(
sdk::Metric::new("latency_ms", latency_ms)
.with_unit("ms")
.with_thresholds(&thresholds),
)
.with_widget(sdk::Widget::stat_card(
"Latency",
format!("{latency_ms:.0}ms"),
"success",
)))
});
}Bundled examples
The repository ships http-check, tcp-check, udp-check, and widgets-check
examples under examples/.
Build, test, and publish
# Native build (examples)
cargo build --examples
# WebAssembly build (examples)
rustup target add wasm32-unknown-unknown
cargo build --examples --target wasm32-unknown-unknown
# Tests
cargo test
CI runs fmt, clippy, the test suite, native and wasm example builds, and
cargo publish --dry-run. Releases are automated in Forgejo Actions: bump
version in Cargo.toml, push to main, then push a matching tag such as
v0.1.4. The publish workflow verifies the tag matches the crate version before
running cargo publish.
Result and domain types
PluginResult is the serviceradar.plugin_result.v1 payload. Use the fluent
with_* builders, or construct the domain types (Metric, Widget, Event)
directly.
| API | Purpose |
|---|---|
PluginResult::new() / PluginResult::ok(summary) |
Construct a result |
.with_summary(text) |
Set the summary |
.with_thresholds(value, warn, crit) |
Derive status from a value |
.with_metric_spec(Metric) |
Append a structured metric |
.with_widget(Widget) |
Append a display widget |
.with_device_discovery(DeviceDiscovery) |
Attach discovery records |
.serialize() |
Produce the JSON payload (Result<Vec<u8>, sdk::Error>) |
Metric::new(name, value).with_unit(u).with_thresholds(&t) |
Build a metric |
Widget::stat_card(label, value, tone) |
Build a stat_card widget |
Thresholds::new(warn, crit) |
Warn/crit threshold pair |
Status and Severity enums mirror the Go SDK (OK / WARNING / CRITICAL /
UNKNOWN and Info / Warning / Error / Critical).
Config loading
let cfg = sdk::load_config_or_default::<Config>()?; // falls back to Default
let cfg = sdk::load_config::<Config>()?; // errors if host config is absent
get_config and get_config_bytes give lower-level access when you need the raw
payload.
Host I/O
Host I/O is proxied through the agent runtime, which enforces the domain and port allowlists declared in your plugin manifest.
-
HTTP:
sdk::HttpClientwithdefault(),get, andpost; the request and response types areHttpRequest/HttpResponse. -
TCP:
sdk::tcp_dialreturning aTcpConnection. -
UDP:
sdk::udp_send_to. -
WebSocket:
sdk::websocket_connect,sdk::websocket_connect_with_headers,sdk::websocket_dial,sdk::websocket_dial_with_headers, returning aWebSocketConnection.
WebSocket calls require the manifest capabilities websocket_connect,
websocket_send, websocket_recv, and websocket_close.
Events, telemetry, and signal schemas
Attach a package schema/display reference to an OCSF event:
let event = sdk::Event::log_activity("camera motion", sdk::Severity::Warning)
.with_signal_schema_ref(&sdk::SignalSchemaRef {
producer_id: "axis-camera".to_string(),
producer_version: "0.1.0".to_string(),
schema_id: "com.carverauto.axis_camera.event_log".to_string(),
schema_version: "1.0.0".to_string(),
display_contract_id: "com.carverauto.axis_camera.event_log.display".to_string(),
display_contract_version: "1.0.0".to_string(),
display_contract: "display/event_log_activity.display.json".to_string(),
signal_type: sdk::SIGNAL_SCHEMA_SIGNAL_TYPE_EVENT.to_string(),
payload_kind: sdk::SIGNAL_SCHEMA_PAYLOAD_KIND_OCSF_EVENT.to_string(),
});
For logs or events routed independently of the result payload, declare the
emit_telemetry capability and emit a batch:
let record = sdk::TelemetryRecord::ocsf_event(event)?.with_signal_schema_ref(&schema_ref);
sdk::emit_telemetry(
sdk::TelemetryBatch::new(vec![record])
.with_source(sdk::TelemetrySource::new("axis-camera", "front-door")),
)?;
emit_telemetry serializes the same JSON host ABI payload as the Go SDK. See
Signal display contracts for the package side of the
contract.
Device discovery
use serviceradar_sdk_rust as sdk;
let location = sdk::DeviceLocation::at(29.9844, -95.3414)
.with_site_code("IAH")
.with_site_name("Houston");
let device = sdk::DiscoveredDevice::named("NIAHAP-MDF001-WAP001")
.with_serial("CNC3HN77NW")
.with_device_type("access_point")
.with_location(location)
.with_label("site", "IAH")
.with_metadata("radio_count", 2);
let result = sdk::PluginResult::ok("discovered 1 device").with_device_discovery(
sdk::DeviceDiscovery::new("ual-network-map").with_device(device),
);
The discovery structs are public and serde-native, so collectors can build them
with struct literals or mutate them incrementally with push_device,
add_device_discovery, and Extend while streaming assets. Core ingests these
serviceradar.device_discovery.v1 envelopes and reconciles them into
ocsf_devices.
Policy inputs
Parse and validate the serviceradar.plugin_inputs.v1 payload with
PluginInputsPayload, parse_plugin_inputs_json, and parse_plugin_inputs_map.
The TargetContext, PluginInput, and PluginInputItem types describe the
resolved devices, interfaces, and credential grants for a policy assignment.
Northbound actions
Rust plugins use the same northbound action contract as the Go SDK:
-
plugin.yamldeclares each action underactions. -
ServiceRadar passes an
action_invocationpayload to the plugin at launch. -
The plugin returns
serviceradar.northbound_action_result.v1.
Use action descriptors for external integrations that operate on selected ServiceRadar inventory. Device actions receive device snapshots; interface actions receive both the parent device context and the selected interface context; event actions are reserved for event-handler workflows that call an external system after a ServiceRadar event is created.
Declare enough required_context for the external API call to be unambiguous.
For example, an interface remediation plugin should require device.ip and
interface.name if the target NMS identifies ports by device address and
interface name.
actions:
- action_id: sample.interface.audit
version: 1.0.0
label: Sample Interface Audit
scopes: [interface]
required_context:
- device.ip
- interface.name
safety_classification: standard
requires_confirmation: true
result_schema_version: serviceradar.northbound_action_result.v1
The SDK exposes typed action status, descriptor, invocation, target, and result
structures (ActionDescriptor, ActionInvocation, ActionResult,
submit_action_result) so plugins implement the contract without hand-rolled
JSON maps. Return one target result per selected device or interface, and
include external correlation IDs, URLs, ticket IDs, and operation names where
they help operators audit what happened — but never credentials or secrets, as
ServiceRadar treats action output as operator-visible audit data.
Advanced modules
The crate also exposes higher-level helpers that mirror the Go SDK for camera and streaming plugins:
-
Camera / media:
CameraHttpClient,MediaStream,MediaChunk, and theCameraPluginConfig/CameraStreamingConfigloaders. -
RTSP:
StreamClient,H264Depacketizer,InterleavedFrame, andVideoTrackfor RTSP parsing and depacketization. -
Check descriptors:
CheckDescriptorfor descriptor-aware target results.