1. HTTP
    1. Control commands
    2. HTTP API description
    3. Notes
  2. Telnet
    1. Control commands
    2. Telnet API description
  3. MQTT
    1. Subscribing to MQTT status events

HTTP

This section documents the "REST" (*) API that can be used for initiating communication with the PowerGoblin from the SUT (system under test) or any other system. The goal of the API is to provide an easy-to-use interface for scripts to control the measurement process.

By default, the program does not offer any modern security features. For example, the interface communication is not encrypted, which means that the connection should only be used in a controlled environment or properly firewalled and tunnelled using encrypted connections. On the other hand, this has an advantage because encryption would increase the latency of the communication and could even affect the measurement's results.

However, the scope of the program's operations is limited to the output file directories defined for the program. If necessary, a more restrictive sandbox can be built for the program, for example with container technologies.

(*) JSON/RPC would probably better describe how the interface works.

Control commands

The following examples demonstrate the use of the PowerGoblin HTTP API in common programming languages (Shell, Python, Java, JavaScript, Kotlin).

Shell scripting is probably most useful if the measurement is coordinated by external scripts that are not part of the software to be measured. This way the software can be seen as a black box. Such scripts should be compatible with most systems that support Unix shell scripts. Note that shell scripts might be problematic for high performance measurement because of the latency introduced by the script interpreters and invocation of operating system processes.

Python scripting is most useful for measurements triggered by a front-end Selenium script because the trigger points can be selected more freely and for example the initialization of the browser instance can be discarded.

Java and JavaScript examples are provided for other possible scenarios. For instance, a backend server based on Spring Boot or Node.js could be the source of trigger events.

# Requires sh, curl/wget & coreutils

# GET requests using wget / curl
msg_get() {
  wget -qO- "http://$HOST/api/v2/$*"
}
msg_get() {
  curl "http://$HOST/api/v2/$*"
}

# POST requests using wget/curl (stdin stored to a file)
msg_post() {
  FILE="$(mktemp)"
  cat > "$FILE"
  wget -qO- --post-file "$FILE" "http://$HOST/api/v2/$*"
  rm "$FILE"
}
msg_post() {
  curl -d@- "http://$HOST/api/v2/$*"
}

# POST request using wget/curl (send a file)
msg_post_file() {
  wget -qO- --post-file "$1" http://$HOST/api/v2/$2
}
msg_post_file() {
  curl --data-binary "@$1" http://$HOST/api/v2/$2
}

# --- Examples ---

# PowerGoblin server
export HOST=localhost:8080

# GET examples

msg_get cmd/startSession
msg_get session/latest/meter
msg_get session/latest/measurement/start

# POST examples

export DATA='{ "triggerType":"Run", "unit":"SUT", "message":"foo", "type":"trigger" }'

echo $DATA | msg_post session/latest/trigger
echo foobar | msg_post session/latest/measurement/rename
msg_post_file collectd.zip session/latest/import/collectd
# Requires Python 3+, python-requests, python-simplejson

import requests
import json

class GoblinClient:
  def __init__(self, host):
    self.host = "http://" + host + "/api/v2/"

  def get(self, url):
    return requests.get(self.host + url)

  def post_json(self, url, json):
    return requests.post(self.host + url, json = json)

  def post_text(self, url, text):
    h = {'Content-Type': 'text/plain'}
    return requests.post(self.host + url, data = text, headers=h)

  def post_file(self, url, file):
    with open(file, 'rb') as p:
      h = {'content-type': 'application/x-zip'}
      return requests.post(self.host + url, data=p, verify=False, headers=h)

  def interpret(self, result):
    return json.loads(result.content)["result"]

# --- Examples ---

# PowerGoblin server
c = GoblinClient("localhost:8080")

# GET examples

c.get("cmd/startSession")
meters = c.get("session/latest/meter")
print(c.interpret(meters))

c.get("session/latest/measurement/start")

# POST examples

data = { "triggerType":"Run", "unit":"SUT", "message":"foo", "type":"trigger" }

c.post_json("session/latest/trigger", json = data)
c.post_text("session/latest/measurement/rename", text = "foobar")
c.post_file("session/latest/import/collectd", "collectd.zip")
// Requires Java 21+

import java.io.IOException;
import java.nio.file.*;
import java.net.*;
import java.net.http.*;
import java.util.zip.*;

record GoblinClient(String host) {
  private HttpResponse<String> run(HttpRequest.Builder b) throws Exception {
    try (var client = HttpClient.newHttpClient()) {
      return client.send(
          b.build(),
          HttpResponse.BodyHandlers.ofString()
      );
    }
  }

  private HttpRequest.Builder api(String api) {
      return HttpRequest.newBuilder(URI.create("http://"+host+"/api/v2/"+api));
  }

  HttpResponse<String> get(String api) throws Exception {
    return run(api(api));
  }

  HttpResponse<String> post(String api, String msg) throws Exception {
    return run(api(api).POST(HttpRequest.BodyPublishers.ofString(msg)));
  }

  HttpResponse<String> post(String api, Path path) throws Exception {
    return run(api(api).POST(HttpRequest.BodyPublishers.ofFile(path)));
  }
}

// --- Examples ---

void main() {
  var c = new GoblinClient("localhost:8080");

  // GET examples

  c.get("cmd/startSession");
  c.get("session/latest/meter");
  c.get("session/latest/measurement/start");

  // POST examples

  var data = """
    { "triggerType": "Run", "unit": "SUT", "message": "foo", "type": "trigger" }
  """;
  
  c.post("session/latest/trigger", data);
  c.post("session/latest/rename", "foobar");
  c.post("session/latest/import/collectd", Path.of("collectd.zip"));
}
const request = require('request');

const host = "localhost:8080";

function get(api) {
  request(
    "http://" + host + "/api/v2/" + api,
    (e, r, body) => {
      if (!e && r.statusCode == 200)
        console.log(body);
    }
  );
}

function post_json(api, data) {
  request.post(
    {
      url: "http://" + host + "/api/v2/" + api,
      json: data,
    },
    (e, r, body) => {
      if (!e && r.statusCode == 200)
        console.log(body)
    }
  );
}

// GET examples

get("cmd/startSession");
get("session/latest/meter");
get("session/latest/measurement/start");

// POST examples

var data = { "triggerType" : "Run", "unit" : "SUT", "message": "foo", "type": "trigger" };

post_json("session/latest/trigger", data);
post_json("session/latest/rename", "foobar");
// Requires Java 21+

import java.nio.file.*
import java.net.*
import java.net.http.*
import java.util.zip.*

class GoblinClient(val host: String) {
  private fun run(b: HttpRequest.Builder) =
    HttpClient.newHttpClient().use {
      it.send(
        b.build(),
        HttpResponse.BodyHandlers.ofString()
      )
    }

  private fun api(api: String) =
    HttpRequest.newBuilder(URI.create("http://$host/api/v2/$api"))

  fun get(api: String) = run(api(api))

  fun post(api: String, msg: String) =
    run(api(api).POST(HttpRequest.BodyPublishers.ofString(msg)))

  fun post(api: String, path: Path) =
    run(api(api).POST(HttpRequest.BodyPublishers.ofFile(path)))
}

// --- Examples ---

var c = GoblinClient("localhost:8080")

// GET examples

c.get("cmd/startSession")
c.get("session/latest/meter")
c.get("session/latest/measurement/start")

// POST examples

var data = """
  { "triggerType": "Run", "unit": "SUT", "message": "foo", "type": "trigger" }
"""

c.post("session/latest/trigger", data)
c.post("session/latest/rename", "foobar")
c.post("session/latest/import/collectd", Path.of("collectd.zip"))

The shell scripts assume that the environment variable $HOST points to the host and port running PowerGoblin. The Python and Java versions use object-oriented approach where the server's address and port are provided for the constructor. Further processing of the HTTP reply data requires a JSON parser. We have included one in the Python example.

The default port for the web interface is 8080. The SUT is supposed to be a separate system to minimize the risk of interference, but the commands can be executed on any system, including the one running PowerGoblin, as well. This flexibility allows setting up the measurement in a multitude of ways.

The start & stop run / measurement commands are also available via the web UI. The commands for storing the data is there as well.

HTTP API description

THIS REPRESENTS THE NEW (v2) HTTP API! You can find the documentation for the old version (v1) here.

The following control commands encode the command's name and possible parameters as part of the HTTP POST query. The following table describes the meaning of the URLs:

Command Type Description
api/v2/session GET Query Return the state data for all active and inactive sessions in this instance.
api/v2/session/latest GET Query Return the state data for the most recently created session (i.e. session with the highest id value)
api/v2/session/:id GET Query Return the state data for the session 'id'.
api/v2/session/:id/restore/:path GET Cmd Restores a past session from logs/path (***), note: 'id' is ignored.
api/v2/session/:id/remove GET Cmd Removes the session 'id' from this instance.
api/v2/session/:id/close GET Cmd Closes the session 'id' (may autosave depending on the settings).
api/v2/session/:id/store GET Cmd Stores the session 'id' to disk (in the log directory).
api/v2/session/:id/reset GET Cmd Resets the session 'id' (wipes all session data).
api/v2/session/:id/rename/:name GET Event Rename the session 'id' to 'name' (****)
api/v2/session/:id/sync/:timestamp/:unit GET Event Synchronize the clocks between the 'unit' (optional) and the instance (*).
api/v2/session/:id/resource GET Query Return a summary of resource consumption
api/v2/session/:id/resource/:resources/include GET Event Add the resource filters 'resources' to session 'id'
api/v2/session/:id/resource/:resources/exclude GET Event Remove the resource filters 'resources' from session 'id'
api/v2/session/:id/resource/:resource/add/:value/:unit GET Event Add a custom resource entry for unit 'unit' and resource 'resource' in session 'id'. The value can be of any type.
api/v2/session/:id/logs/power/:measurement/:meter/:channel GET Query Returns the power readings for the meter & channel & measurement in the session 'id', for doing scatter plots.
api/v2/session/:id/logs/resource/:measurement/:unit/:resource GET Query Returns the resource readings for the unit & resource & measurement in the session 'id', for doing scatter plots.
api/v2/session/:id/logs/session GET Query Returns a summary of the session state.
api/v2/session/:id/logs/busy GET Query Returns a boolean value indicating whether the session logs are still in busy state.
api/v2/session/:id/logs/path GET Query Returns a string value representing the session's directory in the log directory.
api/v2/session/:id/meter GET Query Return the meter states for all meters in session 'id'
api/v2/session/:id/meter/:meter GET Query Return the meter states for meter 'meter' in session 'id'
api/v2/session/:id/meter/:meters/toggle GET Cmd Toggle the meter 'id' on or off (all the drivers may not have support for this).
api/v2/session/:id/meter/:meters/add GET Cmd Assign the free meters 'meters' as new assigned meters to session 'id'
api/v2/session/:id/meter/:meters/remove GET Cmd Unassign the assigned meters 'meters' from session 'id'
api/v2/session/:id/meter/:meters/set GET Cmd Assign meters 'meters' to session 'id' and do not include any other meters in the session
api/v2/session/:id/meter/:meter/rename/:channel/:name GET Event Rename the meter 'meter' & channel 'channel' as 'name' in session 'id'
api/v2/session/:id/measurement GET Query Return the state of the past measurements
api/v2/session/:id/measurement/:id GET Query Return the state of the past measurement with id 'id'
api/v2/session/:id/measurement/active GET Query Return the state of the currently active measurement
api/v2/session/:id/measurement/start/:unit/:msg GET Event Start a new measurement, initiated by 'unit', in session 'id'. Optional 'msg'
api/v2/session/:id/measurement/stop/:unit/:msg GET Event Stop the measurement, initiated by 'unit', in session 'id'. Optional 'msg'
api/v2/session/:id/measurement/rename/:name GET Event Rename the active measurement to 'name' in session 'id'
api/v2/session/:id/run GET Query Return the status of the past runs.
api/v2/session/:id/run/:id GET Query Return the status of the past run with id 'id'.
api/v2/session/:id/run/active GET Query Return the status of the currently active run.
api/v2/session/:id/run/start/:unit/:msg GET Event Start a new run, initiated by 'unit', in session 'id'. Optional 'msg'
api/v2/session/:id/run/stop/:unit/:msg GET Event Stop the run, initiated by 'unit', in session 'id'. Optional 'msg'
api/v2/session/:id/trigger/:name/:unit/:msg GET Event Create a new trigger event name, initiated by 'unit', in session 'id'. Optional 'msg'
api/v2/session/:id/cmd/:type GET Cmd Executes a session command of the type 'type', associated with session 'id'
api/v2/cmd/:type GET Cmd Executes a non-session command of the type 'type'
api/v2/meter GET Query Return the status of all meters managed by the instance
api/v2/meter/:id GET Query Return the status of the meter 'id'
api/v2/meter/:id/toggle GET Cmd Toggle the meter 'id' on or off (all the drivers may not have support for this).
api/v2/mqtt/:topic/:message GET Cmd Send MQTT message 'message' using topic 'topic'.
api/v2/instance GET Query Return a state of the instance
api/v2/instance/logs GET Query Return the number of log files available in the instance
api/v2/reconfigure GET Cmd Reconfigure the meters in the instance, e.g. for hot-plugging new meters.
api/v2/benchmark/http GET Cmd Start a HTTP benchmark. The perf result can be read from the logs.
api/v2/benchmark/telnet GET Cmd Start a Telnet benchmark. The perf result can be read from the logs.
api/v2/session/:id/restore POST Cmd Restores a past session given as POST data, 'id' is ignored
api/v2/session/:id/rename POST Event Rename the session 'id' to 'name' given as POST data.
api/v2/session/:id/sync POST Event Synchronize the clocks between the unit 'sut' and the instance (*), time stamp (**) given as POST data.
api/v2/session/:id/resource/:resources/include POST Event Add the resource filters 'resources' to session 'id'
api/v2/session/:id/resource/:resources/exclude POST Event Remove the resource filters 'resources' from session 'id'
api/v2/session/:id/import/collectd POST Event Import collectd data, the data is provided as an attached zip file.
api/v2/session/:id/meter/:meters/toggle POST Cmd Toggle the meter 'id' on or off (all the drivers may not have support for this).
api/v2/session/:id/meter/:meters/add POST Cmd Assign the free meters 'meters' as new assigned meters to session 'id'
api/v2/session/:id/meter/:meters/remove POST Cmd Unassign the assigned meters 'meters' from session 'id'
api/v2/session/:id/meter/:meters/set POST Cmd Assign meters 'meters' to session 'id' and do not include any other meters in the session
api/v2/session/:id/meter/:meter/rename/:channel POST Event Rename the meter 'meter' & channel 'channel' as 'name' given as POST data in session 'id'
api/v2/session/:id/measurement/start/:unit POST Event Start a new measurement, initiated by 'unit', in session 'id'. Optional 'msg' given as POST data.
api/v2/session/:id/measurement/stop/:unit POST Event Stop the measurement, initiated by 'unit', in session 'id'. Optional 'msg' given as POST data.
api/v2/session/:id/measurement/rename POST Event Rename the active measurement to 'name' given as POST data in session 'id'
api/v2/session/:id/run/start/:unit POST Event Start a new run, initiated by 'unit', in session 'id'. Optional 'msg' given as POST data.
api/v2/session/:id/run/stop/:unit POST Event Stop the run, initiated by 'unit', in session 'id'. Optional 'msg' given as POST data.
api/v2/session/:id/trigger POST Event Create a new trigger event, JSON serialized trigger data given as POST data.
api/v2/cmd POST Cmd Executes a command, JSON serialized command data given as POST data.
api/v2/mqtt/:topic POST Cmd Send MQTT message using topic 'topic'. The meesage is given as POST data.

Notes

(*) Currently only a single (latest) synchronization point is supported per unit. Measuring of clock drift is not supported. In the future, support for multiple points and linear interpolation of clock drift may be added. In such a case, setting up only one synchronization point will operate in the same way as before.

(**) The supported timestamp format looks like date +%s%N, using the date utility from GNU coreutils:

$ date +%s%N
1744729470653712269

(***) For security reasons, relative paths or absolute paths pointing outside the log directory will be discarded.

(***) Renaming the session will switch to a new the log directory. All events that were logged before the rename operation remain in the old directory.


Telnet

TODO: The Telnet API is still mimicking the sessionless version of the v1 HTTP API!

In addition to the HTTP API, PowerGoblin also provides a simple Telnet style interface for communication. Using this interface is potentially more straightforward and produces lower latency due to simpler command sending and parsing. The API is currently limited to Linux as it uses the io_uring backend for even higher performance and lower latency.

Control commands

The following examples demonstrate the use of the PowerGoblin Telnet API in common programming languages (Shell, Python, Java, JavaScript).

Shell scripting is probably most useful if the measurement is coordinated by external scripts that are not part of the software to be measured. This way the software can be seen as a black box. Such scripts should be compatible with most systems that support Unix shell scripts. Note that shell scripts might be problematic for high performance measurement because of the latency introduced by the script interpreters and invocation of operating system processes.

Python scripting is most useful for measurements triggered by a front-end Selenium script because the trigger points can be selected more freely and for example the initialization of the browser instance can be discarded.

Java and JavaScript examples are provided for other possible scenarios. For instance, a backend server based on Spring Boot or Node.js could be the source of trigger events.

# Requires sh, coreutils & gnu-netcat or busybox

msg() {
  echo $* | nc $HOST $PORT
}

# --- Examples ---

# PowerGoblin server
export HOST=localhost
export PORT=9000

msg SESSION METERS
msg MEASUREMENT START starting
msg SESSION SYNC $(date +%s%N)
msg TRIGGER Run,SUT,start
msg SESSION RENAME foobar
# Requires Python 3+

import socket
import time

class GoblinClient:
  def __init__(self, host, port):
    self.host = host
    self.port = port

  def send(self, msg):
    self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.socket.connect((self.host, self.port))
    self.socket.sendall(bytes(msg, "utf-8"))
    self.socket.close()

def milli_time():
  return str(round(time.time() * 1000))

# --- Examples ---

c = GoblinClient("localhost", 9000)

c.send("SESSION METERS")
c.send("MEASUREMENT START starting")
c.send("SESSION SYNC " + milli_time())
c.send("TRIGGER Run,SUT,start")
c.send("SESSION RENAME foobar")
// requires Java 21+
import java.net.*;
import java.io.*;

public record GoblinClient(String host, int port) {
  void send(String msg) throws Exception {
    try (
      var s = new Socket(host, port);
      var w = new PrintWriter(s.getOutputStream())
    ) {
      w.println(msg);
    }
  }
}

// --- Examples ---

void main() {
  var c = new GoblinClient("localhost", 9000);

  c.send("SESSION METERS");
  c.send("MEASUREMENT START starting");
  c.send("SESSION SYNC " + System.currentTimeMillis());
  c.send("TRIGGER Run,SUT,start");
  c.send("SESSION RENAME foobar");
}
var net = require('net');

const host = "127.0.0.1";
const port = 9000;

function send(msg) {
  var client = new net.Socket();
  client.connect(port, host, function() {
    client.write(msg);
  });

  client.on('data', function(data) {
    console.log("RESULT: "+data);
    client.destroy();
  });
}

send("SESSION METERS");
send("MEASUREMENT START starting");
send("SESSION SYNC " + System.currentTimeMillis());
send("TRIGGER Run,SUT,start");
send("SESSION RENAME foobar");
// requires Java 21+
import java.net.*
import java.io.*

class GoblinClient(val host: String, val port: int) {
  fun send(msg: String) {
      Socket(host, port).use {
          PrintWriter(it.outputStream).use {
              it.println(msg)
          }
  }
}

// --- Examples ---

var c = GoblinClient("localhost", 9000)

c.send("SESSION METERS")
c.send("MEASUREMENT START starting")
c.send("SESSION SYNC " + System.currentTimeMillis())
c.send("TRIGGER Run,SUT,start")
c.send("SESSION RENAME foobar")

The shell scripts assume that the environment variable $HOST points to the host and and $PORT to the port running PowerGoblin. The Python and Java versions use object-oriented approach where the server's address and port are provided for the constructor. Further processing of the reply data requires a JSON parser.

The default port for the Telnet interface is 9000. The SUT is supposed to be a separate system to minimize the risk of interference, but the commands can be executed on any system, including the one running PowerGoblin, as well. This flexibility allows setting up the measurement in a multitude of ways.

Telnet API description

The following control commands encode the command's name and possible parameters as part a plain text query. Multiple parameters are comma separated. The following table describes the meaning of the commands:

Command Type Description
SESSION Query Return a hierarchical structure of the session state
SESSION METERS Query Return the list of active meters in the session
SESSION STORE Cmd Store the session data
SESSION CLOSE Cmd Close the session (also stores the data)
SESSION RESET Cmd Reset the session data (dangerous, deletes everything)
SESSION RECONFIGURE Cmd Add/remove meters after a hot plug event.
SESSION RENAME :name Event Rename the session to 'name'
SESSION SYNC :timestamp Event Synchronize the session clocks between the SUT and PowerGoblin instance (*).
MEASUREMENT Query Return the status of the current measurement
MEASUREMENT RENAME :name Event Rename the measurement to 'name'
MEASUREMENT START :unit Event Start a new measurement, initiated by 'unit'
MEASUREMENT STOP :unit Event Stop the measurement, initiated by 'unit'
RUN Query Return the status of the current run
RUN START :unit Event Start a new run, initiated by 'unit'
RUN STOP :unit Event Stop the run, initiated by 'unit'
METER Query Return the status of all the meters in the session
METER :id Query Return the status of the 'id'
METER RENAME :id,:name Event Rename the meter 'id' to 'name'
TRIGGER :name,:unit,:msg Event Create a new trigger event name, unit unit, msg msg
CMD exit Cmd Shut down the application.
CMD deleteLogs Cmd Delete all logs in the log directory.
CMD deletePlots Cmd Delete all plots in the plot directory.
BENCH HTTP Cmd Start a HTTP benchmark. The perf result can be read from the logs.
BENCH TELNET Cmd Start a Telnet benchmark. The perf result can be read from the logs.

(*) Currently only a single (latest) sync point is supported and the measuring of clock drift is not supported. In the future multiple points may be supported.

The supported timestamp format looks like date +%s%N, using the date utility from GNU coreutils:

$ date +%s%N
1744729470653712269

MQTT

PowerGoblin also provides an interface for monitoring PowerGoblin instances, their sessions and measurement using the MQTT protocol.

Since MQTT messages are routed through the MQTT broker, the protocol does not allow for as low-latency communication as direct connection via HTTP or Telnet protocols. In fact, latency can be very high with MQTT. The first version of PowerGoblin supported coordinating measurements using MQTT, but we found that the latency affected the measurement results so much that the feature has now been removed in PowerGoblin 2. We now use MQTT communication only for non-time-critical remote monitoring.

Currently, when MQTT has been enabled (--mqtt command line switch), the PowerGoblin instance first discovers the hostname of the host system, and subscribes to the topic lab-HOSTNAME/. JSON encoded status events are submitted to lab-HOSTNAME/INSTANCE_ID. The instance ID is the SHA-1 hash of the JSON encoded instance specification structure. Since the launch time affects this structure, it is likely that even two instances running on the same host have different ID values.

The encoding of the messages is defined by the instance of fi.utu.tech.powergoblin.model.StatusEvent. Currently, the following status events are supported:

Name Type Description
InstanceStartedEvent instanceStarted The PowerGoblin instance was started.
InstanceStoppedEvent instanceStopped The PowerGoblin instance was stopped.
InstanceSessionStartedEvent instanceSessionStarted A new measurement session was started on this instance.
InstanceSessionClosedEvent instanceSessionClosed A measurement session was closed on this instance.
InstanceReconfigureEvent instanceReconfigure The PowerGoblin instance was reconfigured (discovery of new meters).
InstanceBenchmarkEvent instanceBenchmark The PowerGoblin instance executed a benchmark.
InstanceSessionEvent instanceSession A session related event was executed (measurement / run was started / stopped, meter set on / off).

Subscribing to MQTT status events

First, check the hostname on the machine running PowerGoblin with:

$ hostname
MYHOST

Next, on any system in the same LAN, execute:

$ mosquitto_sub -h BROKER -t lab-MYHOST/#

Here, the BROKER is the IP / hostname of the machine running the MQTT broker / server (e.g. mosquitto).

You can stop the subscription by pressing Ctrl-C.