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 tunneled 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 3, Java 21+, JavaScript, Kotlin 2.4+).

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 curl/wget & coreutils

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

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

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

# --- Example ---

# PowerGoblin server
export HOST=localhost:8080

# GET examples

echo|pg_post session
pg_get session/latest/meter
echo Measurement|pg_post session/latest/trigger
echo name|pg_post session/latest/measurement
echo Run|pg_post session/latest/trigger
echo RunStop|pg_post session/latest/trigger
echo MeasurementStop|pg_post session/latest/trigger
pg_post_file collectd.zip session/latest/import/collectd
# Requires 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"]

# --- Example ---

c = GoblinClient("localhost:8080")

c.post_text("session", "")
meters = c.get("session/latest/meter")
print(c.interpret(meters))

c.post_text("session/latest/trigger", text = "Measurement")
c.post_text("session/latest/measurement", json = "name")
c.post_text("session/latest/trigger", json = "Run")
c.post_text("session/latest/trigger", json = "RunStop")
c.post_text("session/latest/trigger", text = "MeasurementStop")
c.post_file("session/latest/import/collectd", "collectd.zip")
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)));
  }
}

// --- Example ---

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

  c.post("session", "");
  c.get("session/latest/meter");

  c.post("session/latest/trigger", "Measurement");
  c.post("session/latest/measurement", "name");
  c.post("session/latest/trigger", "Run");
  c.post("session/latest/trigger", "RunStop");
  c.post("session/latest/trigger", "MeasurementStop");
  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)
    }
  );
}

// --- Example ---

post("session", "")
get("session/latest/meter")

post("session/latest/trigger", "Measurement")
post("session/latest/measurement", "name")
post("session/latest/trigger", "Run")
post("session/latest/trigger", "RunStop")
post("session/latest/trigger", "MeasurementStop")
post_json("session/latest/rename", "foobar");
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)))
}

// --- Example ---

var c = GoblinClient("localhost:8080")

c.post("session", "")
c.get("session/latest/meter")

c.post("session/latest/trigger", "Measurement")
c.post("session/latest/measurement", "name")
c.post("session/latest/trigger", "Run")
c.post("session/latest/trigger", "RunStop")
c.post("session/latest/trigger", "MeasurementStop")
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 query. The following table describes the meaning of the URLs:

Method Endpoint Description Context
POST /api/v2/cmd Execute a command. Payload: serialized Command object global
GET /api/v2/cmd/startSession Starts a new session. (deprecated) global
GET /api/v2/instance Return the state of all instances. global
GET /api/v2/instance/:instance Return the instance state. instance 'instance'
GET /api/v2/instance/:instance/busy Query whether there are active tasks or sessions writing to disk. instance 'instance'
GET /api/v2/instance/:instance/debug/apicalls Return the list of supported API endpoints. instance 'instance'
GET /api/v2/instance/:instance/debug/ipaddr Run 'ipaddr' and return the output. instance 'instance'
GET /api/v2/instance/:instance/debug/lspci Run 'lspci' and return the output. instance 'instance'
GET /api/v2/instance/:instance/debug/lsusb Run 'lsusb' and return the output. instance 'instance'
GET /api/v2/instance/:instance/debug/mqtt Return the list of MQTT message entries. instance 'instance'
GET /api/v2/instance/:instance/log Return the list of all compatible session logs. instance 'instance'
GET /api/v2/instance/:instance/log/busy Return the number of sessions writing / planning to write the state to disk. instance 'instance'
GET /api/v2/instance/:instance/log/count Return the total number of log files. instance 'instance'
GET /api/v2/instance/:instance/log/incompatible Return the list of all incompatible session logs. instance 'instance'
GET /api/v2/instance/:instance/log/past Return the list of all compatible session logs stored in the past. instance 'instance'
GET /api/v2/instance/:instance/log/today Return the list of all compatible session logs stored today. instance 'instance'
GET /api/v2/instance/:instance/meter Return the list of meter states for all meters. instance 'instance'
GET /api/v2/instance/:instance/meter//count Return the number of meters. instance 'instance'
GET /api/v2/instance/:instance/meter//free Return the status of all free meters. instance 'instance'
GET /api/v2/instance/:instance/meter//free-specs Return the specs of all free meters. instance 'instance'
GET /api/v2/instance/:instance/meter//specs Return the specs of all meters. instance 'instance'
GET /api/v2/instance/:instance/meter/:meter Return the state of the meter. instance 'instance', meter 'meter'
GET /api/v2/instance/:instance/meter/:meter/free Return the state of the free meter. instance 'instance', meter 'meter'
GET /api/v2/instance/:instance/meter/:meter/free-specs Return the specs of the free meter. instance 'instance', meter 'meter'
GET /api/v2/instance/:instance/meter/:meter/specs Return the specs of the meter. instance 'instance', meter 'meter'
GET /api/v2/instance/:instance/node Return the list of node entries. instance 'instance'
GET /api/v2/instance/:instance/task Return the list of asynchronous task entries. instance 'instance'
GET /api/v2/instance/local Return the local instance state. instance, local
POST /api/v2/mqtt/:topic Send a MQTT message. Payload: message body topic 'topic'
GET /api/v2/node Return the list of summaries of all nodes. global
POST /api/v2/node Register a node. Payload: serialized NodeSpecs object global
GET /api/v2/node/:node Return the state of the node. node 'node'
POST /api/v2/node/:node/collectd-stream Receive a collectd resource stream (http plugin). Payload: resource stream unit 'node'
GET /api/v2/node/:node/commands Return the list of enqueued node commands. node 'node'
POST /api/v2/node/:node/enqueue Enqueue an agent command. Payload: serialized AgentCommand object node 'node'
POST /api/v2/node/:node/execute Enqueue a agent execute command. Payload: space separated list of command line arguments node 'node'
POST /api/v2/node/:node/message Append a node message. Payload: message content node 'node'
POST /api/v2/node/:node/status Update the agent status. Payload: serialized AgentStatus object node 'node'
POST /api/v2/null A null sink that discards everything (network benchmarks). Payload: stream global
GET /api/v2/session Return the state data for all active and inactive sessions in this instance. global
POST /api/v2/session Start a new session. Payload: optional description global
GET /api/v2/session/:session Return the session state. session 'session'
GET /api/v2/session/:session/author Return the session author. session 'session'
POST /api/v2/session/:session/author Set the session author. Payload: session author instance 'instance'
GET /api/v2/session/:session/close Close the session (stays available). (deprecated) session 'session'
POST /api/v2/session/:session/cmd Validate the session (validation flag). Payload: validate instance 'instance'
POST /api/v2/session/:session/cmd Reset the session (wipes all session data). Payload: reset instance 'instance'
POST /api/v2/session/:session/cmd Remove the session from the instance's memory. Payload: remove instance 'instance'
POST /api/v2/session/:session/cmd Store the session to disk. Payload: store instance 'instance'
POST /api/v2/session/:session/cmd Close the session. Payload: close instance 'instance'
GET /api/v2/session/:session/description Return the session description. session 'session'
POST /api/v2/session/:session/description Set the session description. Payload: session description instance 'instance'
POST /api/v2/session/:session/import/assets Import session assets (zip archive). Payload: asset archive (zip) instance 'instance'
POST /api/v2/session/:session/import/collectd-stream/:unit Receive a collectd resource stream (http plugin), 'unit' override is optional. Payload: resource stream instance 'instance', unit 'unit'
POST /api/v2/session/:session/import/collectd/:unit Import collectd dump (zip archive), 'unit' override is optional. Payload: resource dump (zip) instance 'instance', unit 'unit'
POST /api/v2/session/:session/import/template Import session description. Payload: Description document instance 'instance'
POST /api/v2/session/:session/import/variables Import session variables. Payload: Variables structure instance 'instance'
GET /api/v2/session/:session/log Return the session summary. session 'session'
GET /api/v2/session/:session/log/busy Query whether the session logs are still busy. session 'session'
GET /api/v2/session/:session/log/errors Return the error log for this session or null. session 'session'
GET /api/v2/session/:session/log/files Return the list of relative paths of log files or null. session 'session'
GET /api/v2/session/:session/log/path Return the associated log folder. session 'session'
GET /api/v2/session/:session/log/power/:meter/:channel Return the (power, time) scatter data for the meter & channel & measurement. session 'session'
GET /api/v2/session/:session/log/resource/:unit/:resource Return the resource readings for the unit/resource/measurement for doing scatter plots. session 'session'
GET /api/v2/session/:session/log/statistics Return session statistics. session 'session'
GET /api/v2/session/:session/measurement Return the list of summaries for past measurements. session 'session'
POST /api/v2/session/:session/measurement Set the measurement name. Payload: measurement name instance 'instance'
GET /api/v2/session/:session/measurement/:m Return the summary of a past measurement. session 'session', measurement 'm'
GET /api/v2/session/:session/measurement/active Return the state of the currently active measurement. session 'session', active measurement
GET /api/v2/session/:session/measurement/start Start a new measurement. (deprecated) session 'session'
GET /api/v2/session/:session/measurement/stop Stop the measurement. (deprecated) session 'session'
POST /api/v2/session/:session/merge Merge session with the other, chronologically later session. Payload: other session instance 'instance'
POST /api/v2/session/:session/message/:unit Create a new message event, initiated by 'unit'. Payload: message body instance 'instance'
POST /api/v2/session/:session/meter Set the set of assigned meter(s) for the session. Payload: comma separated list of meter ids instance 'instance'
POST /api/v2/session/:session/meter/add Add meter(s) to the session. Payload: comma separated list of meter ids instance 'instance'
POST /api/v2/session/:session/meter/name/:meter/:channel Rename the meter/channel for the session. Payload: meter name instance 'instance', meter 'meter', channel 'channel'
POST /api/v2/session/:session/meter/remove Remove meter(s) from the session. Payload: comma separated list of meter ids instance 'instance'
POST /api/v2/session/:session/meter/toggle Toggle the session meter(s) on/off. Payload: comma separated list of meter ids instance 'instance'
GET /api/v2/session/:session/name Return the session name. session 'session'
POST /api/v2/session/:session/name Set the session name. Payload: session name instance 'instance'
GET /api/v2/session/:session/remove Removes the session. (deprecated) session 'session'
GET /api/v2/session/:session/resource Return the session resource consumption. session 'session'
POST /api/v2/session/:session/resource/add/:unit/:resource Add a custom resource entry for unit/resource. Payload: numeric resource value instance 'instance'
POST /api/v2/session/:session/resource/exclude Exclude the resource filter(s). Payload: comma separated list of filters instance 'instance'
POST /api/v2/session/:session/resource/include Include the resource filter(s). Payload: comma separated list of filters instance 'instance'
GET /api/v2/session/:session/run Return the list of summaries for past runs. session 'session'
GET /api/v2/session/:session/run/:r Return the summary of a past run. session 'session', run 'r'
GET /api/v2/session/:session/run/active Return the state of the currently active run. session 'session', active run
GET /api/v2/session/:session/run/start Start a new run. (deprecated) session 'session'
GET /api/v2/session/:session/run/stop Stop the run. (deprecated) session 'session'
GET /api/v2/session/:session/sync Return the session synchronization data. (*) session 'session'
POST /api/v2/session/:session/sync/:unit Synchronize the clocks between the unit and the instance. Payload: synchronization timestamps (**) instance 'instance', unit 'unit'
POST /api/v2/session/:session/trigger Create a new trigger event. Payload: trigger type instance 'instance'
GET /api/v2/session/latest Return the session state. session, most recent
POST /api/v2/session/reconstruct Reconstruct a past session. Payload: session path (***) global
POST /api/v2/session/restore Restore a past session. Payload: session path (***) global

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.


Telnet

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 has two backends, a generic socket interface and on Linux also io_uring for higher performance and lower latency.

Control commands

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

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 coreutils & gnu-netcat or busybox

GoblinClient() {
  export HOST=$1
  export PORT=$2
}

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

# --- Example ---

GoblinClient 127.0.0.1 9000

send SESSION latest METER
send SESSION latest MEASUREMENT START algo1
send SESSION latest SYNC $(date +%s%N)
send SESSION latest RUN START
send SESSION latest RUN STOP
send SESSION latest MEASUREMENT STOP
send SESSION latest CLOSE
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("127.0.0.1", 9000)

c.send("SESSION latest METER")
c.send("SESSION latest SYNC " + milli_time())
c.send("SESSION latest MEASUREMENT START algo1")
c.send("SESSION latest RUN START")
c.send("SESSION latest RUN STOP")
c.send("SESSION latest MEASUREMENT STOP")
c.send("SESSION latest CLOSE")
import java.io.PrintWriter;

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

// --- Example ---

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

  c.send("SESSION latest METER");
    c.send("SESSION latest SYNC " + System.currentTimeMillis());
  c.send("SESSION latest MEASUREMENT START algo1");
  c.send("SESSION latest RUN START");
  c.send("SESSION latest RUN STOP");
  c.send("SESSION latest MEASUREMENT STOP");
  c.send("SESSION latest CLOSE");
}
var net = require('net');

const GoblinClient = (host, port) => ({
    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();
        });
    }
});

// --- Example ---

var c = GoblinClient("127.0.0.1", 9000);

c.send("SESSION latest METER");
c.send("SESSION latest SYNC " + Date.now());
c.send("SESSION latest MEASUREMENT START algo1");
c.send("SESSION latest RUN START");
c.send("SESSION latest RUN STOP");
c.send("SESSION latest MEASUREMENT STOP");
c.send("SESSION latest CLOSE");
class GoblinClient(val host: String, val port: Int) {
  fun send(msg: String) =
    java.net.Socket(host, port).use {
      java.io.PrintWriter(it.outputStream).use {
        it.println(msg)
      }
  }
}

// --- Example ---

val c = GoblinClient("127.0.0.1", 9000)

c.send("SESSION latest METER")
c.send("SESSION latest SYNC " + System.currentTimeMillis())
c.send("SESSION latest MEASUREMENT START algo1")
c.send("SESSION latest RUN START")
c.send("SESSION latest RUN STOP")
c.send("SESSION latest MEASUREMENT STOP")
c.send("SESSION latest CLOSE")

The shell scripts assume that the environment variable $HOST points to the host 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 space separated. The following table describes the meaning of the commands:

Command Type Description
START Cmd Start a new session.
RESTORE :id Cmd Restore an old session 'id'.
RECONSTRUCT :id Cmd Reconstruct an old session 'id'.
RECONFIGURE Cmd Reconfigure the meters.
SHUTDOWN Cmd Shut down the application.
BENCH :id Cmd Start a benchmark. The perf result can be read from the logs.
INSTANCE Query Return the specs of the instance.
TASK Query Return the status of the async tasks.
METER Query Return the specs of all meters.
NODE Query Return the specs of all nodes.
SESSION Query Return the entries of all sessions.
SESSION :id Query Return state of the session 'id'.
SESSION :id METER Query Return the specs of the meter(s) in session 'id'.
SESSION :id METER STATUS Query Return the status of meter(s) in session 'id'.
SESSION :id METER ADD :m Cmd Add the meters 'm' (space separated) to session 'id'.
SESSION :id METER REMOVE :m Cmd Remove the meters 'm' (space separated) from session 'id'.
SESSION :id METER SET :m Cmd Set the meters 'm' (space separated) for session 'id'.
SESSION :id METER RENAME :m :c :name Event Rename the session 'id' meter 'm' channel 'c' to 'name'.
SESSION :id STORE Cmd Store the session 'id' data.
SESSION :id CLOSE Cmd Close the session 'id'.
SESSION :id REMOVE Cmd Close the session 'id' & remove from list.
SESSION :id RESET Cmd Reset the session 'id' data (warning: resets everything).
SESSION :id RENAME :name Event Rename the session 'id' to 'name'.
SESSION :id SYNC :ts Event Synchronize the session 'id' clocks ('ts' = space separate timestamps) between SUT & PowerGoblin (*).
SESSION :id MEASUREMENT Query Return the status of the current measurement in session 'id'.
SESSION :id MEASUREMENT START :name Event Start a new measurement in session 'id', 'name' is optional.
SESSION :id MEASUREMENT STOP Event Stop the measurement in session 'id'.
SESSION :id RUN Query Return the status of the current run in session 'id'.
SESSION :id RUN START Event Start a new run in session 'id'.
SESSION :id RUN STOP Event Stop the run in session 'id'.
SESSION :id MESSAGE :msg Event Create a new 'msg' in session 'id'.

(*) 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.

If MQTT has been enabled (see CLI options & global configuration), 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 the topic lab-HOSTNAME/INSTANCE_ID. The instance ID is the same as the one listed via the HTTP API, a 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 StatusEvents:

{
  "type": "instanceStarted",
  "nt": 53143378270195,
  "ts": 1780999152930,
  "id": "37060f723250d1ff74563031d0a33b5657a12998"
}

Each message has the following standard fields

  • type: Type of the message (see the Type column in the table below)
  • nt: Timestamp in nanoseconds
  • ts: Timestamp (Unix) in milliseconds
  • id: Id if the PowerGoblin instance
  • sessionId: Session id if the message is associated with a session

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.