Tutorial
This page goes through a tutorial to create a simple plugin which will provide readings
for a single "memory" device. For more examples, see the SDK's examples
directory or
the Synse emulator plugin.
In this tutorial, we will get system memory data using github.com/shirou/gopsutil, which you can get with
go get github.com/shirou/gopsutil
1. Planning¶
Prior to writing any code for a plugin, it is a good idea to figure out what the plugin will do, what data it will provide, and what devices it will have. This will provide concrete definitions and constraints when implementing the plugin.
Goals
- Provide readings for memory usage
- Do not support writing (doesn't make sense for this use case)
- Have the readings updated every 5 seconds
Devices
- One "memory" device which will have reading outputs of:
- total memory (in bytes)
- free memory (in bytes)
- used percentage (percent)
2. Create the plugin skeleton¶
With an idea of what we want the plugin to do, the next step would be to lay down the foundation for the plugin. This tutorial will only go over the bare minimum for what is needed, however additional things may be added to enhance the development flow (Makefile, CI, dependency management, etc).
We know that we will be defining a plugin with a new memory type device, so we should have a device configuration, a plugin configuration, plugin-specific reading outputs, and the plugin source itself:
. ├── config │ └── device │ └── memory.yaml ├── config.yaml ├── handlers.go ├── outputs.go └── plugin.go
Note
A plugin can be structured in different ways. Since this is a simple example plugin, a simple structure is being used. This example does not aim to define a "correct" structure.
3. Write the configurations¶
Once the project structure is in place, we can define the various configurations. It is not necessary to define them before writing the plugin source itself, though it can be helpful so you know exactly what data the plugin will be dealing with when it is implemented.
3a. Plugin configuration¶
The plugin configuration defines how the plugin itself will behave. For a reference on configuration options, see the plugin configuration documentation.
From the goals listed earlier, we know that we will want the plugin to update its readings every 5 seconds. Additionally, we need to choose whether the plugin will run in serial mode or parallel mode. Since reading the memory info we want is not serial-bound, we can run it in parallel mode. The plugin must also run either using unix socket or TCP for the gRPC transport. In general, plugins should choose to use TCP as it is easier to manage.
With this, we can define our simple plugin configuration:
# config.yaml version: 3 debug: false network: type: tcp address: ':5001' settings: mode: parallel read: interval: 5s
Debug logging can be verbose, so it is disabled here. If you wish to see debug logs,
just set debug: true
.
3b. Device configuration¶
The device configuration defines the devices which the plugin will manage and collect data from. For a reference on configuration options, see the device configuration documentation.
From the planning section, we know that we will want a "memory"-type device, of which we will have a single instance which provides multiple readings. All devices need to be associated with a device handler which the plugin implements. Since we have not implemented the plugin yet, that device handler does not exist, but we can still give it a name for when we do implement it -- we'll call it "virtual-memory".
# config/device/memory.yaml version: 3 devices: - type: memory handler: virtual-memory instances: - info: Virtual Memory Usage data: id: 1
Above, we define the "memory" device which uses the "virtual-memory" handler. There is one instance, which defines two fields in this example:
- info: A human readable string which helps us identify the device.
- data: Data associated with the device. Generally this would give the plugin info on how to connect to the device, such as the port number, address, etc. Since this example plugin is so simple, there is no need for that -- the memory usage is just for the system the plugin is running on. We still need to specify something for the data because of Synse's deterministic device IDs. Providing unique data here allows Synse to generate a unique deterministic ID hash for the device.
4. Define reading outputs¶
The SDK provides some built-in outputs, but as per the planning section, this plugin will require some custom outputs for
- total memory
- free memory
- percent memory used
Total memory and free memory are returned as bytes, and the percent memory used is returned
as a percentage, so we can define a bytes
and percent
custom output:
// outputs.go package main import "github.com/vapor-ware/synse-sdk/sdk/output" var ( outputBytes = output.Output{ Name: "bytes", Unit: &output.Unit{ Name: "bytes", Symbol: "B", }, } outputPercent = output.Output{ Name: "percent", Precision: 2, Unit: &output.Unit{ Name: "percent", Symbol: "%", }, } )
These outputs will be registered with the plugin in a later step.
5. Define the device handler¶
If you have read through the SDK documentation, you should know that devices are configured with a device handler, which tells the plugin how to read from/write to the device. As stated in the planning step, this plugin will only support reading. In the device configuration step, we also specified that our memory-type device will use a handler named "virtual-memory".
Below, we define the virtual-memory device using gopsutil to get the memory data we desire.
// handlers.go package main import ( "github.com/shirou/gopsutil/mem" "github.com/vapor-ware/synse-sdk/sdk" "github.com/vapor-ware/synse-sdk/sdk/output" ) var virtualMemoryHandler = sdk.DeviceHandler{ Name: "virtual-memory", Read: func(device *sdk.Device) ([]*output.Reading, error) { vMemStat, err := mem.VirtualMemory() if err != nil { return nil, err } total := outputBytes.MakeReading(vMemStat.Total).WithContext(map[string]string{ "info": "total", }) free := outputBytes.MakeReading(vMemStat.Total).WithContext(map[string]string{ "info": "free", }) pctUsed := outputBytes.MakeReading(vMemStat.Total).WithContext(map[string]string{ "info": "percent memory used", }) return []*output.Reading{ total, free, pctUsed, }, nil }, }
Notice that for each reading, a context was added with "info" key describing what the reading value corresponds to. The data in the context is arbitrary, so additional info does not necessarily have to live under "info". Adding context is useful in cases like this where a single device has two outputs of the same type (bytes) which need to be differentiated.
6. Create the plugin¶
With all the configurations defined, the custom outputs defined, and the plugin's device handler defined, its time to create the plugin itself and register everything with it.
The plugin should be created within the main()
function and will require some metadata
to be defined, namely a plugin name and maintainer. We'll call the plugin "memory tutorial" and
the maintainer will be "vaporio".
// plugin.go package main import ( "log" "github.com/vapor-ware/synse-sdk/sdk" ) func main() { sdk.SetPluginInfo( "memory tutorial", "vaporio", "a tutorial plugin for reading virtual memory", "", ) // Create a new plugin instance. plugin, err := sdk.NewPlugin() if err != nil { log.Fatal(err) } // Register custom output types. err = plugin.RegisterOutputs( &outputBytes, &outputPercent, ) if err != nil { log.Fatal(err) } // Register the plugin's device handler. err = plugin.RegisterDeviceHandlers( &virtualMemoryHandler, ) if err != nil { log.Fatal(err) } // Run the plugin. if err := plugin.Run(); err != nil { log.Fatal(err) } }
7. Build and run the plugin¶
With all the plugin files defined, the plugin can now be built and run.
go build -o plugin
Running the plugin, you should see logs similar to:
INFO[0000] [config] loading configuration ext=yaml loader=plugin name=config paths="[. ./config /etc/synse/plugin/config]" policy=optional INFO[0000] [config] found matching config file=config.yaml loader=plugin path=. policy=optional INFO[0000] Plugin Info: INFO[0000] Tag: vaporio/memory-tutorial INFO[0000] Name: memory tutorial INFO[0000] Maintainer: vaporio INFO[0000] VCS: INFO[0000] Description: a tutorial plugin for reading virtual memory INFO[0000] Version Info: INFO[0000] Plugin Version: - INFO[0000] SDK Version: 3.0.0 INFO[0000] Git Commit: - INFO[0000] Git Tag: - INFO[0000] Build Date: - INFO[0000] Go Version: - INFO[0000] OS/Arch: darwin/amd64 INFO[0000] Plugin Config: INFO[0000] Version: 3 INFO[0000] Debug: false INFO[0000] ID: INFO[0000] UsePluginTag: true INFO[0000] UseMachineID: false INFO[0000] UseEnv: [] INFO[0000] UseCustom: [] INFO[0000] Settings: INFO[0000] Mode: parallel INFO[0000] Listen: INFO[0000] Disable: false INFO[0000] Read: INFO[0000] Disable: false INFO[0000] QueueSize: 128 INFO[0000] Interval: 5s INFO[0000] Delay: 0s INFO[0000] Write: INFO[0000] Disable: false INFO[0000] QueueSize: 128 INFO[0000] BatchSize: 128 INFO[0000] Interval: 1s INFO[0000] Delay: 0s INFO[0000] Transaction: INFO[0000] TTL: 5m0s INFO[0000] Limiter: INFO[0000] Rate: 0 INFO[0000] Burst: 0 INFO[0000] Cache: INFO[0000] Enabled: false INFO[0000] TTL: 3m0s INFO[0000] Network: INFO[0000] Type: tcp INFO[0000] Address: :5001 INFO[0000] TLS: INFO[0000] Key: INFO[0000] Cert: INFO[0000] CACerts: [] INFO[0000] SkipVerify: false INFO[0000] Health: INFO[0000] HealthFile: /etc/synse/plugin/healthy INFO[0000] UpdateInterval: 30s INFO[0000] DynamicRegistration: INFO[0000] Config: [] INFO[0000] [id] generated plugin id namespace id=f61f04ae-6338-5cbd-9bdf-ca6ed6c307db INFO[0000] [plugin] initializing INFO[0000] [device manager] initializing INFO[0000] [config] loading configuration ext=yaml loader=device name= paths="[./config/device /etc/synse/plugin/config/device]" policy=required INFO[0000] [config] found matching config file=memory.yaml loader=device path=./config/device policy=required INFO[0000] [device manager] added new device id=aad3aac1-c1e2-54ac-8809-cb2883daa979 type=memory INFO[0000] [device manager] created devices devices=1 INFO[0000] [server] tls/ssl not configured, using insecure transport INFO[0000] [plugin] executing pre-run actions actions=2 INFO[0000] [health] registered default health check name="read queue health" type=periodic INFO[0000] [health] registered default health check name="write queue health" type=periodic INFO[0000] [plugin] running INFO[0000] [device manager] starting INFO[0000] [state manager] starting INFO[0000] [scheduler] starting INFO[0000] [server] starting INFO[0000] [plugin] will terminate on: [SIGTERM, SIGINT] INFO[0000] [scheduler] listeners will not be scheduled (no listener handlers registered) INFO[0000] [scheduler] starting read scheduling delay=0s interval=5s mode=parallel INFO[0000] [server] serving addr=":5001" mode=tcp INFO[0000] [scheduler] writing will not be scheduled (no write handlers registered)
You can use the Synse CLI to interact with the plugin directly, or continue
on to run it alongside Synse Server and interact with it through the Synse API. You can terminate
the plugin with ^C
.
8. Running with Synse Server¶
The easiest way to run the plugin with Synse Server is to create a docker image for the plugin.
# Dockerfile FROM scratch COPY plugin plugin ENTRYPOINT ["./plugin"]
Since the scratch
image requires a linux/amd64 binary, we should rebuild the plugin for
that architecture:
$ GOOS=linux GOARCH=amd64 go build -o plugin
Then, the Docker image can be built -- we'll tag the image as vaporio/tutorial-plugin
.
docker build -t vaporio/tutorial-plugin .
We now have a docker image for the plugin, but it needs some additional configuration to run, namely mounting the plugin and device configuration we defined into the container. We can create a compose file to mount in the configuration and connect it to a Synse Server instance. See the Synse Server documentation for details on how the server container is configured.
# compose.yaml version: '3' services: synse-server: container_name: synse-server image: vaporio/synse-server ports: - '5000:5000' environment: SYNSE_PLUGIN_TCP: 'tutorial-plugin:5001' links: - tutorial-plugin tutorial-plugin: container_name: tutorial-plugin image: vaporio/tutorial-plugin expose: - 5001 volumes: - ./config.yaml:/etc/synse/plugin/config/config.yaml - ./config/device:/etc/synse/plugin/config/device
Which can be run with:
docker-compose -f compose.yaml up -d
Once running, you should see both containers up
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6556deaf8b0f vaporio/synse-server "/usr/bin/tini -- bi…" 31 seconds ago Up 30 seconds 0.0.0.0:5000->5000/tcp synse-server d110ab9e1ac7 vaporio/tutorial-plugin "./plugin" 32 seconds ago Up 31 seconds 5001/tcp tutorial-plugin
You can now interact with the plugin via the Synse Server API, e.g.
$ curl localhost:5000/v3/read [ { "device":"aad3aac1-c1e2-54ac-8809-cb2883daa979", "timestamp":"2019-05-28T20:09:57Z", "type":"", "device_type":"memory", "unit":{ "name":"bytes", "symbol":"B" }, "value":2095575040, "context":{ "info":"total" } }, { "device":"aad3aac1-c1e2-54ac-8809-cb2883daa979", "timestamp":"2019-05-28T20:09:57Z", "type":"", "device_type":"memory", "unit":{ "name":"bytes", "symbol":"B" }, "value":2095575040, "context":{ "info":"free" } }, { "device":"aad3aac1-c1e2-54ac-8809-cb2883daa979", "timestamp":"2019-05-28T20:09:57Z", "type":"", "device_type":"memory", "unit":{ "name":"bytes", "symbol":"B" }, "value":2095575040, "context":{ "info":"percent memory used" } } ]