Go
Go SDK
Build, package, and ship ServiceRadar WebAssembly checker plugins in Go (TinyGo) with the official serviceradar-sdk-go module.
serviceradar-sdk-go is the supported way to write ServiceRadar checker
plugins in Go and compile them to a sandboxed WebAssembly artifact with TinyGo.
The SDK owns the low-level host ABI so your plugin code stays focused on the
check itself.
Repository: https://code.carverauto.dev/carverauto/serviceradar-sdk-go
The SDK handles:
-
Config decoding from the host (
get_config). -
A result builder for the
serviceradar.plugin_result.v1payload. - A logging bridge to the host.
- Host-proxied HTTP, TCP, UDP, and WebSocket I/O.
- Device discovery envelopes for inventory-producing plugins.
- OCSF event emission, alert-promotion hints, and first-class telemetry.
- Signal schema and display contract references for package-managed logs and events.
Install
go get code.carverauto.dev/carverauto/serviceradar-sdk-go
The module targets go 1.25 and is compiled with TinyGo for the wasi target.
Your first plugin
A plugin exports a single run_check function. sdk.Execute wraps your check,
submits the result to the host, and converts any returned error into a critical
result automatically.
//go:build tinygo
package main
import (
"fmt"
"code.carverauto.dev/carverauto/serviceradar-sdk-go/sdk"
)
type Config struct {
URL string `json:"url"`
WarnMS float64 `json:"warn_ms"`
CritMS float64 `json:"crit_ms"`
}
//export run_check
func run_check() {
_ = sdk.Execute(func() (*sdk.Result, error) {
cfg := Config{URL: "https://example.com/health"}
if err := sdk.LoadConfig(&cfg); err != nil {
return nil, err
}
resp, err := sdk.HTTP.Get(cfg.URL)
if err != nil {
return nil, fmt.Errorf("http request failed: %w", err)
}
latency := float64(resp.Duration.Milliseconds())
thresholds := sdk.Thresholds(cfg.WarnMS, cfg.CritMS)
return sdk.NewResult().
WithSummary(fmt.Sprintf("http %d in %.0fms", resp.Status, latency)).
WithThresholds(latency, thresholds.Warn, thresholds.Crit).
WithMetric("latency_ms", latency, "ms", thresholds).
WithStatCard("Latency", fmt.Sprintf("%.0fms", latency), "success"), nil
})
}
func main() {}
The main function stays empty: the host calls the exported run_check symbol
directly.
Bundled examples
The repository ships runnable examples under examples/:
-
http-check— HTTP latency check with thresholds and events. -
tcp-check— TCP connectivity check with optional write/read. -
udp-check— UDP send check with a bytes-sent metric. -
widgets-check— stat card, table, sparkline, and markdown widgets. -
sample-northbound— northbound result example.
Build
# Requires TinyGo
cd examples/http-check
tinygo build -o plugin.wasm -target=wasi ./
The result is a single plugin.wasm you package alongside a manifest and config
schema. See Plugin packages for the package layout an operator
installs.
Result builder
Result is the serviceradar.plugin_result.v1 payload. Every field has both a
conventional setter (SetSummary, AddMetric, …) and a fluent builder
(WithSummary, WithMetric, …) so you can pick a style.
Constructors and status
sdk.NewResult() // status defaults to UNKNOWN
sdk.Ok("all good") // StatusOK
sdk.Warning("degraded") // StatusWarning
sdk.Critical("down") // StatusCritical
sdk.Unknown("no data") // StatusUnknown
Status values are sdk.StatusOK, sdk.StatusWarning, sdk.StatusCritical, and
sdk.StatusUnknown (serialized as OK, WARNING, CRITICAL, UNKNOWN).
Defaults and zero values
Defaults are applied at the edge — right before serialization — so Serialize
never mutates your object. A bare var r sdk.Result still serializes to a valid
payload:
-
SchemaVersiondefaults to1. -
Statusdefaults toUNKNOWN. -
Summarydefaults to the status string. -
ObservedAtdefaults to “now” in RFC3339Nano.
Builders
res := sdk.NewResult().
WithSummary("all good").
WithDetails("extended diagnostics").
WithMetric("cpu", 10, "%", nil).
WithLabel("version", "1.2.3")| Builder | Purpose |
|---|---|
WithStatus / WithSummary / WithDetails |
Core status fields |
WithMetric(name, value, unit, thresholds) |
Append a structured metric |
WithLabel(key, value) |
Attach a label |
WithThresholds(value, warn, crit) |
Derive status from a value |
WithStatCard(label, value, tone) |
stat_card display widget |
WithTable(data, layout) |
table display widget |
WithSparkline(label, points, tone) |
sparkline display widget |
WithMarkdown(markdown) |
markdown display widget |
WithEvent / WithOCSFEvent |
Attach OCSF events |
WithImmediateAlert(conditionID) |
Request immediate alert promotion |
ForTarget(ctx) |
Scope the result to a descriptor target |
Thresholds
Thresholds(warn, crit) builds a *ThresholdSpec without helper boilerplate;
each bound is set only when greater than zero.
thresholds := sdk.Thresholds(50, 100)
res.WithMetric("latency_ms", 10, "ms", thresholds)
res.WithThresholds(10, thresholds.Warn, thresholds.Crit)Error handling
Execute accepts a func() (*sdk.Result, error) and returns an error. If your
function returns a non-nil error, the SDK auto-generates a critical result (or
upgrades your result to critical) and records the error details in the payload —
so the happy path stays concise while failures still surface.
err := sdk.Execute(func() (*sdk.Result, error) {
return sdk.Ok("ok"), nil
})
if err != nil {
// Optional: submit/serialize errors (logging is already handled by the SDK).
}Host I/O
Host I/O is proxied through the agent runtime, which enforces the domain and
port allowlists declared in your plugin manifest. Context-aware variants exist to
match Go expectations (they check ctx.Err() before the host call; TinyGo/WASM
is synchronous today, but the API is stable for future cancellation support).
-
HTTP:
sdk.HTTP.Get,sdk.HTTP.Post,sdk.HTTP.Do, plusGetContext/PostContext/DoContext. -
TCP:
sdk.TCPDialContext,(*TCPConn).ReadContext,(*TCPConn).WriteContext. -
UDP:
sdk.UDPSendToContext. -
WebSocket:
sdk.WebSocketDialContext,sdk.WebSocketConnectWithHeaders,(*WebSocketConn).SendContext/RecvContext.
conn, err := sdk.WebSocketDialContext(ctx, "ws://localhost:8080/ws", 10*time.Second)
if err != nil {
return nil, fmt.Errorf("websocket dial failed: %w", err)
}
defer conn.Close()
if err := conn.SendContext(ctx, []byte(`{"method":"getInfo"}`), 10*time.Second); err != nil {
return nil, err
}
buf := make([]byte, 4096)
n, err := conn.RecvContext(ctx, buf, 10*time.Second)
WebSocket connections require the manifest capabilities websocket_connect,
websocket_send, websocket_recv, and websocket_close. Use
WebSocketConnectWithHeaders to send headers (for example Authorization) on the
initial handshake.
Events and alert hints
The result payload carries optional fields that drive event promotion. They are ignored safely by older control-plane builds.
-
events— a list of OCSF Event Log Activity objects. -
alert_hint— a boolean requesting immediate promotion. -
condition_id— a key used for de-duplication and auto-clear.
res := sdk.Critical("HTTP request failed")
res.EmitEvent(sdk.SeverityCritical, "HTTP request failed", "http_request_failed")
res.RequestImmediateAlert("http_request_failed")
Severities are sdk.SeverityInfo, sdk.SeverityWarning, sdk.SeverityError,
and sdk.SeverityCritical. Build a standalone event with
sdk.NewOCSFEventLogActivity(message, severity).
First-class telemetry
Use result-attached events for check-scoped annotations. For logs or events
that should be ingested independently of the check result, declare the
emit_telemetry capability and send a telemetry batch:
event := sdk.NewOCSFEventLogActivity("camera motion", sdk.SeverityWarning)
record := sdk.NewOCSFTelemetryRecord(event).WithSignalSchemaRef(sdk.SignalSchemaRef{
ProducerID: "axis-camera",
ProducerVersion: "0.1.0",
SchemaID: "com.carverauto.axis_camera.event_log",
SchemaVersion: "1.0.0",
DisplayContractID: "com.carverauto.axis_camera.event_log.display",
DisplayContractVersion: "1.0.0",
DisplayContract: "display/event_log_activity.display.json",
SignalType: sdk.SignalSchemaSignalTypeEvent,
PayloadKind: sdk.SignalSchemaPayloadKindOCSFEvent,
})
err := sdk.EmitTelemetry(sdk.TelemetryBatch{
Source: sdk.TelemetrySource{SourceType: "axis-camera", SourceInstance: "front-door"},
Records: []sdk.TelemetryRecord{record},
})
AttachSignalSchemaRef writes the ServiceRadar extension metadata under
metadata.service_radar.signal_schema. See
Signal display contracts for the package side of
this contract.
Device discovery
Inventory-producing plugins can emit serviceradar.device_discovery.v1
envelopes inside the normal result payload (the DeviceDiscovery field). Core
ingests these records and reconciles them into ocsf_devices.
Policy inputs
For policy-driven plugin assignments, decode and validate the typed
serviceradar.plugin_inputs.v1 payload:
var payload sdk.PluginInputsPayload
if err := sdk.LoadConfig(&payload); err != nil {
return nil, err
}
if err := payload.Validate(); err != nil {
return nil, err
}
devices := payload.ItemsByEntity("devices")
err := payload.EachItem(func(item sdk.PluginInputItem) error {
// item.Entity: "devices" | "interfaces" | ...
// item.Item: resolved fields (uid/ip/if_name/...)
return nil
})
Helpers include sdk.ParsePluginInputsJSON, sdk.ParsePluginInputsMap, and the
payload methods FlattenItems, ItemsByEntity, and ItemsByName.
Northbound actions
Northbound action plugins let ServiceRadar select a device, interface, or event and call an external tool from a WASM plugin — NMS lookups, configuration management actions, remediation previews, or ticket-driven external API calls.
Declare each exported action in plugin.yaml under actions:
actions:
- action_id: sample.device.lookup
version: 1.0.0
label: Sample Device Lookup
scopes: [device]
required_context:
- device.uid
- device.ip
safety_classification: read_only
requires_confirmation: false
result_schema_version: serviceradar.northbound_action_result.v1
input_schema:
type: object
additionalProperties: false
properties:
query_mode:
type: string
enum: [summary, full]
default: summary
At runtime ServiceRadar merges the plugin assignment config with an
action_invocation envelope. Load it with sdk.LoadActionConfig(), inspect
ActionInvocation.ActionID, read selected target snapshots from
ActionInvocation.Targets, and return a structured ActionResult:
hostConfig, err := sdk.LoadActionConfig()
if err != nil {
_ = sdk.SubmitActionResult(sdk.ActionFailed("config_error", err.Error()))
return
}
invocation := hostConfig.ActionInvocation
result := sdk.ActionSucceeded("external lookup completed")
for _, target := range invocation.Targets {
result.AddTargetResult(sdk.ActionTargetResult{
DeviceUID: target.DeviceUID,
Status: sdk.ActionStatusSucceeded,
Result: map[string]any{
"device_ip": target.Address(),
"api_query": "GET /devices/" + target.Address(),
},
})
}
_ = sdk.SubmitActionResult(result)
Device actions normally require device.uid and device.ip; interface actions
should require the device address plus a stable interface identifier or name (for
example device.ip and interface.name). Keep operator input in the descriptor
input_schema — ServiceRadar supplies the submitted values in
ActionInvocation.InputValues. Action output is operator-visible audit data, so
include external correlation IDs, URLs, and operation names, but never
credentials or secrets.
The examples/sample-northbound example simulates both a device-scoped lookup
and an interface-scoped audit/remediation preview — use it as the reference shape
for manifests, action result payloads, and tests.
Host ABI
The SDK imports host functions from the env module and exports alloc /
dealloc so the host can write into plugin memory. The imported functions are:
get_config log submit_result emit_telemetry
http_request tcp_connect tcp_read tcp_write tcp_close
udp_sendto websocket_connect websocket_send websocket_recv websocket_close
camera_media_open camera_media_write camera_media_heartbeat camera_media_close
Each capability you call must be declared in the plugin manifest. Payloads are
bounded: results are capped at sdk.MaxPayloadBytes (2 MiB) and HTTP responses
at sdk.MaxHTTPResponseBytes (4 MiB).
CI and versioning
The repository’s .forgejo/workflows/ci.yml runs formatting, vet, lint, and
tests on every change. Consumers pin a tagged module version with go get. The
Rust SDK targets the same V2 plugin contract for teams that prefer
Rust-native tooling.