1. HTTP
    1. Control commands
    2. HTTP API description
  2. Telnet
    1. Control commands
    2. Telnet API description

THIS VERSION OF THE API (v1) IS OUTDATED AND DOES NOT SUPPORT CONCURRENT SESSIONS!

**You can find the most recent version of the HTTP API here.

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

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
msg_get() { 
  wget -qO- "http://$HOST/$*"
}

# GET requests using curl
msg_get() { 
  curl "http://$HOST/$*"
}

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

# POST requests using curl
msg_post() { 
  curl -d@- "http://$HOST/$*"
}

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

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

# --- Examples ---

# PowerGoblin server
export HOST=localhost:8080

# GET examples

msg_get api/v1/session/meters

msg_get api/v1/measurement/start/TestScenario1

# POST examples

export DATA='{ "triggerType":"Run", "unit":"SUT", "message":"foo", "type":"trigger" }'
echo $DATA | msg_post api/v1/trigger

echo foobar | msg_post api/v1/session/rename

msg_post_file collectd.zip api/v1/import/collectd
# Requires Python 3+, python-requests, python-simplejson

import requests                                                                                                                          
import json                                                                                                                          

class GoblinClient:
  def __init__(self, host):
    self.host = "http://" + host
 
  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

meters = c.get("api/v1/session/meters")
print(c.interpret(meters))

meters = c.get("api/v1/measurement/start/TestScenario1")

# POST examples

data = { "triggerType":"Run", "unit":"SUT", "message":"foo", "type":"trigger" }
c.post_json("api/v1/trigger", json = data)

c.post_text("api/v1/session/rename", text = "foobar")

c.post_file("api/v1/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));
  }
  
  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("api/v1/session/meters");
  
  c.get("api/v1/measurement/start/TestScenario1");

  // POST examples
            
  var data = "{ \"triggerType\":\"Run\", \"unit\":\"SUT\", \"message\":\"foo\", \"type\":\"trigger\" }";
  c.post("api/v1/trigger", data);

  c.post("api/v1/session/rename", "foobar");
  
  c.post("api/v1/import/collectd", Path.of("collectd.zip"));
}
const request = require('request');

const host = "localhost:8080";

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

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

// GET examples

get("api/v1/session/meters");

get("api/v1/measurement/start/TestScenario1");

// POST examples
  
var data = { "triggerType" : "Run", "unit" : "SUT", "message": "foo", "type": "trigger" };
post_json("api/v1/trigger", data);
  
post_json("api/v1/session/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")))
  
  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("api/v1/session/meters")

c.get("api/v1/measurement/start/TestScenario1")

// POST examples
        
var data = "{ \"triggerType\":\"Run\", \"unit\":\"SUT\", \"message\":\"foo\", \"type\":\"trigger\" }"
c.post("api/v1/trigger", data)

c.post("api/v1/session/rename", "foobar")

c.post("api/v1/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

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/v1/session GET Query Return a hierarchical structure of the session state
api/v1/session GET Query Return a hierarchical structure of the session state
api/v1/session/meters GET Query Return the list of active meters in the session
api/v1/session/store GET Cmd Store the session data
api/v1/session/close GET Cmd Close the session (also stores the data)
api/v1/session/reset GET Cmd Reset the session data (dangerous, deletes everything)
api/v1/session/reconfigure GET Cmd Add/remove meters after a hot plug event.
api/v1/session/rename/:name GET Event Rename the session to 'name'
api/v1/session/sync/:timestamp GET Event Synchronize the session clocks between the SUT and PowerGoblin instance (*).
api/v1/measurement GET Query Return the status of the current measurement
api/v1/measurement/rename/:name GET Event Rename the measurement to 'name'
api/v1/measurement/start/:unit GET Event Start a new measurement, initiated by 'unit'
api/v1/measurement/stop/:unit GET Event Stop the measurement, initiated by 'unit'
api/v1/run GET Query Return the status of the current run
api/v1/run/start/:unit GET Event Start a new run, initiated by 'unit'
api/v1/run/stop/:unit GET Event Stop the run, initiated by 'unit'
api/v1/meter GET Query Return the status of all the meters in the session
api/v1/meter/:id GET Query Return the status of the 'id'
api/v1/meter/rename/:id/:name GET Event Rename the meter 'id' to 'name'
api/v1/trigger/:name/:unit/:msg GET Event Create a new trigger event name, unit unit, msg msg
api/v1/cmd/exit GET Cmd Shut down the application.
api/v1/cmd/deleteLogs GET Cmd Delete all logs in the log directory.
api/v1/cmd/deletePlots GET Cmd Delete all plots in the plot directory.
api/v1/benchmark/http GET Cmd Start a HTTP benchmark. The perf result can be read from the logs.
api/v1/benchmark/telnet GET Cmd Start a Telnet benchmark. The perf result can be read from the logs.
api/v1/session POST Query Return a hierarchical structure of the session state
api/v1/session/meters POST Query Return the list of active meters in the session
api/v1/session/store POST Cmd Store the session data
api/v1/session/close POST Cmd Close the session (also stores the data)
api/v1/session/reset POST Cmd Reset the session data (dangerous, deletes everything)
api/v1/session/reconfigure POST Cmd Add/remove meters after a hot plug event.
api/v1/session/rename POST Event Rename the session. Body of the request = name.
api/v1/session/sync POST Event Synchronize the session clocks. Body of the request = timestamp
api/v1/measurement POST Query Return the status of the current measurement
api/v1/measurement/rename POST Event Rename the measurement. Body of the request = name.
api/v1/measurement/start POST Event Start a new measurement. Body of the request = initiating unit.
api/v1/measurement/stop POST Event Stop the measurement. Body of the request = initiating unit.
api/v1/run POST Query Return the status of the current run
api/v1/run/start POST Event Start a new run. Body of the request = initiating unit.
api/v1/run/stop POST Event Stop the run. Body of the request = initiating unit.
api/v1/meter POST Query Return the status of all the meters in the session
api/v1/meter/:id POST Query Return the status of the 'id'
api/v1/meter/rename/:id POST Event Rename the meter 'id'. Body of the request = name.
api/v1/trigger POST Event Create a new trigger. Body of the request = serialized event.
api/v1/cmd POST Cmd Execute a command. Body of the request = serialized command.
api/v1/import/collectd POST Event Send
api/v1/benchmark/http POST Cmd Start a HTTP benchmark. The perf result can be read from the logs.
api/v1/benchmark/telnet POST 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.


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 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.