ServiceRadar plugin SDK for Go (TinyGo/WASM).
This SDK lets you write ServiceRadar plugin checkers in Go without handling low-level WASM host calls. It handles:
- Config decoding from the host
- Result builder for
serviceradar.plugin_result.v1 - Logging bridge
- HTTP/TCP/UDP proxy wrappers
- Support for Websockets
- Device discovery envelopes for inventory-producing plugins
- Event emission + alert promotion hints
- Signal schema/display contract references for package-managed logs and events
go get code.carverauto.dev/carverauto/serviceradar-sdk-gopackage main
import (
"context"
"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) {
var cfg Config
if err := sdk.LoadConfig(&cfg); err != nil {
return nil, err
}
resp, err := sdk.HTTP.GetContext(context.Background(), 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() {}examples/http-check: HTTP latency check with thresholds and eventsexamples/tcp-check: TCP connectivity check with optional write/readexamples/udp-check: UDP send check with bytes-sent metricexamples/widgets-check: HTTP check demonstrating stat card, table, sparkline, and markdown widgets
Execute accepts a function that returns (*Result, error) and itself returns an error:
err := sdk.Execute(func() (*sdk.Result, error) {
// ...
return sdk.Ok("ok"), nil
})
if err != nil {
// Optional: handle submit/serialize errors (logging is already done by the SDK)
}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. This keeps the happy path concise while still surfacing failures.
Defaults are applied at the edge (right before serialization) so Serialize does not mutate the original object:
SchemaVersiondefaults to1Statusdefaults toUNKNOWNSummarydefaults to the status stringObservedAtdefaults to “now” in RFC3339Nano
This means var r sdk.Result is safe; serialization produces a valid payload without altering r.
Result has both conventional setters (SetSummary, AddMetric, etc.) and fluent builders (WithSummary, WithMetric, etc.) so you can choose style:
return sdk.NewResult().
WithSummary("all good").
WithMetric("cpu", 10, "%", nil).
WithLabel("version", "1.2.3"), nilUse ThresholdSpec for warning/critical thresholds and Thresholds(warn, crit) to build one without helper functions:
thresholds := sdk.Thresholds(50, 100)
res.WithMetric("latency_ms", 10, "ms", thresholds)
res.WithThresholds(10, thresholds.Warn, thresholds.Crit)When a plugin emits OCSF events or OTEL-style logs that are described by a package manifest, attach the package schema/display reference through the SDK:
event := sdk.NewOCSFEventLogActivity("camera motion", sdk.SeverityWarning)
sdk.AttachSignalSchemaRef(&event, 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,
})The helper writes the ServiceRadar extension metadata under metadata.service_radar.signal_schema.
For first-class telemetry 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},
})Use result-attached events for check-scoped annotations. Use EmitTelemetry for
standalone or streaming plugin logs/events.
Context variants exist for host I/O to match Go expectations:
- HTTP:
HTTP.DoContext,HTTP.GetContext,HTTP.PostContext - TCP:
TCPDialContext,(*TCPConn).ReadContext,(*TCPConn).WriteContext - UDP:
UDPSendToContext - WebSocket:
WebSocketDialContext,(*WebSocketConn).SendContext,(*WebSocketConn).RecvContext
These currently check ctx.Err() before the host call (TinyGo/Wasm is synchronous), but give you a stable API if cancellation support is added later.
The SDK provides WebSocket client capabilities for plugins that need to communicate with WebSocket servers:
// Dial a WebSocket endpoint
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()
// Send a message
if err := conn.SendContext(ctx, []byte(`{"method": "getInfo"}`), 10*time.Second); err != nil {
return nil, fmt.Errorf("websocket send failed: %w", err)
}
// Read response
buf := make([]byte, 4096)
n, err := conn.RecvContext(ctx, buf, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("websocket recv failed: %w", err)
}
data := buf[:n]WebSocket connections are mediated by the host runtime, which enforces:
- Domain allowlists: Only permitted domains can be connected to
- Port restrictions: Only allowed ports can be accessed
- Connection limits: Maximum concurrent connections per plugin
The plugin must have the following capabilities in its manifest:
websocket_connect: Permission to establish WebSocket connectionswebsocket_send: Permission to send messageswebsocket_recv: Permission to receive messageswebsocket_close: Permission to close connections
To include headers (for example Authorization) on the initial WebSocket handshake:
headers := map[string]string{
"Authorization": "Basic <base64-user-pass>",
}
conn, err := sdk.WebSocketConnectWithHeaders("wss://camera.local/vapix/ws-data-stream?sources=events", headers, 10*time.Second)LoadConfig is an alias of GetConfig for more idiomatic naming in user code:
if err := sdk.LoadConfig(&cfg); err != nil {
return nil, err
}For policy-driven plugin assignments, decode and validate the typed input payload:
var payload sdk.PluginInputsPayload
if err := sdk.LoadConfig(&payload); err != nil {
return nil, err
}
if err := payload.Validate(); err != nil {
return nil, err
}
// Iterate all resolved items (devices/interfaces/etc.)
err := payload.EachItem(func(item sdk.PluginInputItem) error {
// item.Entity: "devices" | "interfaces" | ...
// item.Item: map with resolved fields (uid/ip/if_name/etc.)
return nil
})
if err != nil {
return nil, err
}
devices := payload.ItemsByEntity("devices")
_ = devicesHelpers also include:
sdk.ParsePluginInputsJSON([]byte)sdk.ParsePluginInputsMap(map[string]any)(*PluginInputsPayload).FlattenItems()(*PluginInputsPayload).ItemsByEntity(string)(*PluginInputsPayload).ItemsByName(string)
# Requires TinyGo
cd examples/http-check
tinygo build -o plugin.wasm -target=wasi ./The agent imports host functions from the env module:
get_configlogsubmit_resultemit_telemetryhttp_requesttcp_connect/tcp_read/tcp_write/tcp_closeudp_sendtowebsocket_connect/websocket_send/websocket_recv/websocket_closecamera_media_open/camera_media_write/camera_media_heartbeat/camera_media_close
The SDK wraps these functions and exports alloc/dealloc for host memory access.
The SDK emits optional fields in the result payload to support event promotion:
events: list of OCSF Event Log Activity objectsalert_hint: boolean flag for immediate promotioncondition_id: string used for de-duplication and auto-clear logic
These fields are ignored safely by older control-plane builds.