Parcourir la source

finished exporter app, more or less

Grega Bremec il y a 2 ans
Parent
commit
d57c413b14
21 fichiers modifiés avec 3455 ajouts et 0 suppressions
  1. 5 0
      exporter/.dockerignore
  2. 39 0
      exporter/.gitignore
  3. 3 0
      exporter/README.adoc
  4. 194 0
      exporter/pom.xml
  5. 94 0
      exporter/src/main/docker/Dockerfile.jvm
  6. 90 0
      exporter/src/main/docker/Dockerfile.legacy-jar
  7. 27 0
      exporter/src/main/docker/Dockerfile.native
  8. 30 0
      exporter/src/main/docker/Dockerfile.native-micro
  9. 18 0
      exporter/src/main/java/net/p0f/openshift/metrics/exporter/MetricsResource.java
  10. 155 0
      exporter/src/main/java/net/p0f/openshift/metrics/exporter/ProcessAccountingMetrics.java
  11. 529 0
      exporter/src/main/java/net/p0f/openshift/metrics/exporter/SysstatMetrics.java
  12. 115 0
      exporter/src/main/java/net/p0f/openshift/metrics/model/ProcessAccountingRecord.java
  13. 1775 0
      exporter/src/main/java/net/p0f/openshift/metrics/model/SysstatMeasurement.java
  14. 24 0
      exporter/src/main/java/net/p0f/openshift/metrics/processor/PsacctToCsv.java
  15. 43 0
      exporter/src/main/java/net/p0f/openshift/metrics/routes/PsacctConsumer.java
  16. 15 0
      exporter/src/main/java/net/p0f/openshift/metrics/routes/RegisterProcessAccounting.java
  17. 14 0
      exporter/src/main/java/net/p0f/openshift/metrics/routes/ResetProcessAccounting.java
  18. 41 0
      exporter/src/main/java/net/p0f/openshift/metrics/routes/SysstatConsumer.java
  19. 213 0
      exporter/src/main/resources/META-INF/resources/index.html
  20. 10 0
      exporter/src/main/resources/application.properties
  21. 21 0
      exporter/src/main/resources/net/p0f/openshift/metrics/routes/transformSysstat.json

+ 5 - 0
exporter/.dockerignore

@@ -0,0 +1,5 @@
+*
+!target/*-runner
+!target/*-runner.jar
+!target/lib/*
+!target/quarkus-app/*

+ 39 - 0
exporter/.gitignore

@@ -0,0 +1,39 @@
+#Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+release.properties
+
+# Eclipse
+.project
+.classpath
+.settings/
+bin/
+
+# IntelliJ
+.idea
+*.ipr
+*.iml
+*.iws
+
+# NetBeans
+nb-configuration.xml
+
+# Visual Studio Code
+.vscode
+.factorypath
+
+# OSX
+.DS_Store
+
+# Vim
+*.swp
+*.swo
+
+# patch
+*.orig
+*.rej
+
+# Local environment
+.env

+ 3 - 0
exporter/README.adoc

@@ -0,0 +1,3 @@
+= metrics-exporter Project =
+
+TBD

+ 194 - 0
exporter/pom.xml

@@ -0,0 +1,194 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>net.p0f.openshift</groupId>
+  <artifactId>metrics-exporter</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <properties>
+    <compiler-plugin.version>3.8.1</compiler-plugin.version>
+    <failsafe.useModulePath>false</failsafe.useModulePath>
+    <maven.compiler.release>11</maven.compiler.release>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
+    <camel.platform.artifact-id>quarkus-camel-bom</camel.platform.artifact-id>
+    <quarkus.platform.group-id>com.redhat.quarkus.platform</quarkus.platform.group-id>
+    <quarkus.platform.version>2.7.6.Final-redhat-00009</quarkus.platform.version>
+    <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
+  </properties>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>${quarkus.platform.artifact-id}</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>${camel.platform.artifact-id}</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <dependencies>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-arc</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-resteasy</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-file</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-bindy</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-bean</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-direct</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-seda</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-jackson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-jolt</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.quarkus</groupId>
+      <artifactId>camel-quarkus-jsonpath</artifactId>
+    </dependency>
+    <!-- XXX TESTING XXX -->
+    <dependency>
+      <groupId>io.quarkus</groupId>
+      <artifactId>quarkus-junit5</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>rest-assured</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <!-- XXX REPOSITORIES XXX -->
+  <repositories>
+    <repository>
+      <releases>
+        <enabled>true</enabled>
+      </releases>
+      <snapshots>
+        <enabled>false</enabled>
+      </snapshots>
+      <id>redhat</id>
+      <url>https://maven.repository.redhat.com/ga</url>
+    </repository>
+  </repositories>
+  <pluginRepositories>
+    <pluginRepository>
+      <releases>
+        <enabled>true</enabled>
+      </releases>
+      <snapshots>
+        <enabled>false</enabled>
+      </snapshots>
+      <id>redhat</id>
+      <url>https://maven.repository.redhat.com/ga</url>
+    </pluginRepository>
+  </pluginRepositories>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>${quarkus.platform.group-id}</groupId>
+        <artifactId>quarkus-maven-plugin</artifactId>
+        <version>${quarkus.platform.version}</version>
+        <extensions>true</extensions>
+        <executions>
+          <execution>
+            <goals>
+              <goal>build</goal>
+              <goal>generate-code</goal>
+              <goal>generate-code-tests</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>${compiler-plugin.version}</version>
+        <configuration>
+          <compilerArgs>
+            <arg>-parameters</arg>
+          </compilerArgs>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire-plugin.version}</version>
+        <configuration>
+          <systemPropertyVariables>
+            <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+            <maven.home>${maven.home}</maven.home>
+          </systemPropertyVariables>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <profiles>
+    <profile>
+      <id>native</id>
+      <activation>
+        <property>
+          <name>native</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${surefire-plugin.version}</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+                <configuration>
+                  <systemPropertyVariables>
+                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
+                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+                    <maven.home>${maven.home}</maven.home>
+                  </systemPropertyVariables>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <properties>
+        <quarkus.package.type>native</quarkus.package.type>
+      </properties>
+    </profile>
+  </profiles>
+</project>

+ 94 - 0
exporter/src/main/docker/Dockerfile.jvm

@@ -0,0 +1,94 @@
+####
+# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
+#
+# Before building the container image run:
+#
+# ./mvnw package
+#
+# Then, build the image with:
+#
+# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/metrics-exporter-jvm .
+#
+# Then run the container using:
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter-jvm
+#
+# If you want to include the debug port into your docker image
+# you will have to expose the debug port (default 5005) like this :  EXPOSE 8080 5005
+#
+# Then run the container using :
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter-jvm
+#
+# This image uses the `run-java.sh` script to run the application.
+# This scripts computes the command line to execute your Java application, and
+# includes memory/GC tuning.
+# You can configure the behavior using the following environment properties:
+# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
+# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
+#   in JAVA_OPTS (example: "-Dsome.property=foo")
+# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
+#   used to calculate a default maximal heap memory based on a containers restriction.
+#   If used in a container without any memory constraints for the container then this
+#   option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
+#   of the container available memory as set here. The default is `50` which means 50%
+#   of the available memory is used as an upper boundary. You can skip this mechanism by
+#   setting this value to `0` in which case no `-Xmx` option is added.
+# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
+#   is used to calculate a default initial heap memory based on the maximum heap memory.
+#   If used in a container without any memory constraints for the container then this
+#   option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
+#   of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
+#   is used as the initial heap size. You can skip this mechanism by setting this value
+#   to `0` in which case no `-Xms` option is added (example: "25")
+# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
+#   This is used to calculate the maximum value of the initial heap memory. If used in
+#   a container without any memory constraints for the container then this option has
+#   no effect. If there is a memory constraint then `-Xms` is limited to the value set
+#   here. The default is 4096MB which means the calculated value of `-Xms` never will
+#   be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
+# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
+#   when things are happening. This option, if set to true, will set
+#  `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
+# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
+#    true").
+# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
+# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
+#   https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
+# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
+# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
+#   (example: "20")
+# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
+#   (example: "40")
+# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
+#   (example: "4")
+# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
+#   previous GC times. (example: "90")
+# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
+# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
+# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
+#   contain the necessary JRE command-line options to specify the required GC, which
+#   will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
+# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
+# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
+# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
+#   accessed directly. (example: "foo.example.com,bar.example.com")
+#
+###
+FROM registry.access.redhat.com/ubi8/openjdk-11:1.11
+
+ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
+
+
+# We make four distinct layers so if there are application changes the library layers can be re-used
+COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
+COPY --chown=185 target/quarkus-app/*.jar /deployments/
+COPY --chown=185 target/quarkus-app/app/ /deployments/app/
+COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
+
+EXPOSE 8080
+USER 185
+ENV AB_JOLOKIA_OFF=""
+ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
+ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
+

+ 90 - 0
exporter/src/main/docker/Dockerfile.legacy-jar

@@ -0,0 +1,90 @@
+####
+# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
+#
+# Before building the container image run:
+#
+# ./mvnw package -Dquarkus.package.type=legacy-jar
+#
+# Then, build the image with:
+#
+# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/metrics-exporter-legacy-jar .
+#
+# Then run the container using:
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter-legacy-jar
+#
+# If you want to include the debug port into your docker image
+# you will have to expose the debug port (default 5005) like this :  EXPOSE 8080 5005
+#
+# Then run the container using :
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter-legacy-jar
+#
+# This image uses the `run-java.sh` script to run the application.
+# This scripts computes the command line to execute your Java application, and
+# includes memory/GC tuning.
+# You can configure the behavior using the following environment properties:
+# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class")
+# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
+#   in JAVA_OPTS (example: "-Dsome.property=foo")
+# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
+#   used to calculate a default maximal heap memory based on a containers restriction.
+#   If used in a container without any memory constraints for the container then this
+#   option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
+#   of the container available memory as set here. The default is `50` which means 50%
+#   of the available memory is used as an upper boundary. You can skip this mechanism by
+#   setting this value to `0` in which case no `-Xmx` option is added.
+# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
+#   is used to calculate a default initial heap memory based on the maximum heap memory.
+#   If used in a container without any memory constraints for the container then this
+#   option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
+#   of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
+#   is used as the initial heap size. You can skip this mechanism by setting this value
+#   to `0` in which case no `-Xms` option is added (example: "25")
+# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
+#   This is used to calculate the maximum value of the initial heap memory. If used in
+#   a container without any memory constraints for the container then this option has
+#   no effect. If there is a memory constraint then `-Xms` is limited to the value set
+#   here. The default is 4096MB which means the calculated value of `-Xms` never will
+#   be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
+# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
+#   when things are happening. This option, if set to true, will set
+#  `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
+# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
+#    true").
+# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
+# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
+#   https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
+# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
+# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
+#   (example: "20")
+# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
+#   (example: "40")
+# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
+#   (example: "4")
+# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
+#   previous GC times. (example: "90")
+# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
+# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
+# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
+#   contain the necessary JRE command-line options to specify the required GC, which
+#   will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
+# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
+# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
+# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
+#   accessed directly. (example: "foo.example.com,bar.example.com")
+#
+###
+FROM registry.access.redhat.com/ubi8/openjdk-11:1.11
+
+ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
+
+
+COPY target/lib/* /deployments/lib/
+COPY target/*-runner.jar /deployments/quarkus-run.jar
+
+EXPOSE 8080
+USER 185
+ENV AB_JOLOKIA_OFF=""
+ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
+ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"

+ 27 - 0
exporter/src/main/docker/Dockerfile.native

@@ -0,0 +1,27 @@
+####
+# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
+#
+# Before building the container image run:
+#
+# ./mvnw package -Pnative
+#
+# Then, build the image with:
+#
+# docker build -f src/main/docker/Dockerfile.native -t quarkus/metrics-exporter .
+#
+# Then run the container using:
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter
+#
+###
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5
+WORKDIR /work/
+RUN chown 1001 /work \
+    && chmod "g+rwX" /work \
+    && chown 1001:root /work
+COPY --chown=1001:root target/*-runner /work/application
+
+EXPOSE 8080
+USER 1001
+
+CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

+ 30 - 0
exporter/src/main/docker/Dockerfile.native-micro

@@ -0,0 +1,30 @@
+####
+# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
+# It uses a micro base image, tuned for Quarkus native executables.
+# It reduces the size of the resulting container image.
+# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
+#
+# Before building the container image run:
+#
+# ./mvnw package -Pnative
+#
+# Then, build the image with:
+#
+# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/metrics-exporter .
+#
+# Then run the container using:
+#
+# docker run -i --rm -p 8080:8080 quarkus/metrics-exporter
+#
+###
+FROM quay.io/quarkus/quarkus-micro-image:1.0
+WORKDIR /work/
+RUN chown 1001 /work \
+    && chmod "g+rwX" /work \
+    && chown 1001:root /work
+COPY --chown=1001:root target/*-runner /work/application
+
+EXPOSE 8080
+USER 1001
+
+CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

+ 18 - 0
exporter/src/main/java/net/p0f/openshift/metrics/exporter/MetricsResource.java

@@ -0,0 +1,18 @@
+package net.p0f.openshift.metrics.exporter;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@ApplicationScoped
+@Path("/metrics")
+public class MetricsResource {
+    @GET
+    @Path("/version")
+    @Produces(MediaType.TEXT_PLAIN)
+    public String hello() {
+        return "MetricsExporter v1.0.0";
+    }
+}

+ 155 - 0
exporter/src/main/java/net/p0f/openshift/metrics/exporter/ProcessAccountingMetrics.java

@@ -0,0 +1,155 @@
+package net.p0f.openshift.metrics.exporter;
+
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tags;
+import net.p0f.openshift.metrics.model.ProcessAccountingRecord;
+
+@ApplicationScoped
+@Named("processAccountingMetrics")
+public class ProcessAccountingMetrics {
+    static final Logger LOG = Logger.getLogger(ProcessAccountingMetrics.class.getName());
+
+    @Inject
+    MeterRegistry mr;
+
+    HashMap<String, Counter> totalInvocationCount = new HashMap<>();
+    HashMap<String, AtomicInteger> invocationCount = new HashMap<>();
+    HashMap<String, AtomicInteger> totalTime = new HashMap<>();
+    HashMap<String, AtomicInteger> userTime = new HashMap<>();
+    HashMap<String, AtomicInteger> systemTime = new HashMap<>();
+    HashMap<String, AtomicInteger> majFaults = new HashMap<>();
+    HashMap<String, AtomicInteger> minFaults = new HashMap<>();
+    HashMap<String, AtomicInteger> swapEvents = new HashMap<>();
+
+    public void registerRecord(ProcessAccountingRecord par) {
+        LOG.fine("Got record: " + par);
+        String hn = par.getHostName();
+        String pn = par.getProcessName();
+        String key = pn + "@" + hn;
+
+        Tags tags = Tags.of("host", hn, "process", pn);
+
+        if (!this.totalInvocationCount.containsKey(key)) {
+            LOG.fine("Registering psacct.invocation.total for " + key);
+            this.totalInvocationCount.put(key,
+                    Counter.builder("psacct.invocation.total")
+                    .tags(tags)
+                    .register(this.mr));
+        }
+        this.totalInvocationCount.get(key).increment(par.getNumCalls());
+
+        if (!this.invocationCount.containsKey(key)) {
+            LOG.fine("Registering psacct.invocation.count for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.invocationCount.put(key, g);
+            this.mr.gauge("psacct.invocation.count", tags, g);
+        }
+        this.invocationCount.get(key).set(par.getNumCalls());
+
+        if (!this.totalTime.containsKey(key)) {
+            LOG.fine("Registering psacct.time.elapsed for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.totalTime.put(key, g);
+            this.mr.gauge("psacct.time.elapsed", tags, g);
+        }
+        // Needs to be in ms because there's no AtomicFloat.
+        this.totalTime.get(key).set(Float.valueOf(par.getElapsedTime() * 1000).intValue());
+
+        if (!this.userTime.containsKey(key)) {
+            LOG.fine("Registering psacct.time.user for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.userTime.put(key, g);
+            this.mr.gauge("psacct.time.user", tags, g);
+        }
+        // Needs to be in ms because there's no AtomicFloat.
+        this.userTime.get(key).set(Float.valueOf(par.getUserTime() * 1000).intValue());
+
+        if (!this.systemTime.containsKey(key)) {
+            LOG.fine("Registering psacct.time.system for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.systemTime.put(key, g);
+            this.mr.gauge("psacct.time.system", tags, g);
+        }
+        // Needs to be in ms because there's no AtomicFloat.
+        this.systemTime.get(key).set(Float.valueOf(par.getSystemTime() * 1000).intValue());
+
+        if (!this.majFaults.containsKey(key)) {
+            LOG.fine("Registering psacct.vm.fault.major for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.majFaults.put(key, g);
+            this.mr.gauge("psacct.vm.fault.major", tags, g);
+        }
+        this.majFaults.get(key).set(par.getMajFaults());
+
+        if (!this.minFaults.containsKey(key)) {
+            LOG.fine("Registering psacct.vm.fault.minor for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.minFaults.put(key, g);
+            this.mr.gauge("psacct.vm.fault.minor", tags, g);
+        }
+        this.minFaults.get(key).set(par.getMinFaults());
+
+        if (!this.swapEvents.containsKey(key)) {
+            LOG.fine("Registering psacct.vm.swap.events for " + key);
+            AtomicInteger g = new AtomicInteger(0);
+            this.swapEvents.put(key, g);
+            this.mr.gauge("psacct.vm.swap.events", tags, g);
+        }
+        this.swapEvents.get(key).set(par.getSwapEvents());
+    }
+
+    public void resetGauges() {
+        LOG.fine("Resetting all gauges for a new metric.");
+
+        LOG.finer("Resetting psacct.invocation.count...");
+        for (String x : this.invocationCount.keySet()) {
+            LOG.finest("Resetting gauge psacct.invocation.count " + x);
+            this.invocationCount.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.time.elapsed...");
+        for (String x : this.totalTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.time.elapsed " + x);
+            this.totalTime.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.time.user...");
+        for (String x : this.userTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.time.user " + x);
+            this.userTime.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.time.system...");
+        for (String x : this.systemTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.time.system " + x);
+            this.systemTime.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.vm.fault.major...");
+        for (String x : this.systemTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.vm.fault.major " + x);
+            this.majFaults.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.vm.fault.minor...");
+        for (String x : this.systemTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.vm.fault.minor " + x);
+            this.minFaults.get(x).set(0);
+        }
+
+        LOG.finer("Resetting psacct.vm.swap.events...");
+        for (String x : this.systemTime.keySet()) {
+            LOG.finest("Resetting gauge psacct.vm.swap.events " + x);
+            this.swapEvents.get(x).set(0);
+        }
+    }
+}

+ 529 - 0
exporter/src/main/java/net/p0f/openshift/metrics/exporter/SysstatMetrics.java

@@ -0,0 +1,529 @@
+package net.p0f.openshift.metrics.exporter;
+
+import java.util.logging.Logger;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tags;
+import net.p0f.openshift.metrics.model.SysstatMeasurement;
+
+@ApplicationScoped
+@Named("sysstatMetrics")
+public class SysstatMetrics {
+    static final Logger LOG = Logger.getLogger(SysstatMeasurement.class.getName());
+
+    @Inject
+    MeterRegistry mr;
+
+    SysstatMeasurement lastMeasurement = null;
+
+    public void processMetricRecord(SysstatMeasurement sm) {
+        LOG.fine("Updating sysstat metrics records...");
+
+        if (this.lastMeasurement == null) {
+            LOG.fine("Initialising sysstat metrics for " + sm.getHostname());
+
+            this.lastMeasurement = sm;
+            Tags tags = Tags.of("host", this.lastMeasurement.getHostname());
+
+            for (SysstatMeasurement.CpuLoad cl : this.lastMeasurement.getCpuLoad()) {
+                LOG.fine("Registering CPU metrics for CPU-" + cl.getCpu());
+                Tags ctags = Tags.of("host", this.lastMeasurement.getHostname(), "cpu", cl.getCpu());
+    
+                Gauge.builder("sysstat.cpu.usr", cl::getUsr)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.sys", cl::getSys)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.nice", cl::getNice)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.iowait", cl::getIowait)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.steal", cl::getSteal)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.irq", cl::getIrq)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.soft", cl::getSoft)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.guest", cl::getGuest)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.gnice", cl::getGnice)
+                        .tags(ctags)
+                        .register(this.mr);
+                Gauge.builder("sysstat.cpu.idle", cl::getIdle)
+                            .tags(ctags)
+                            .register(this.mr);
+            }
+
+            LOG.fine("Registering process and context switch metrics");
+            SysstatMeasurement.ProcessAndContextSwitch pcs =
+                        this.lastMeasurement.getProcessAndContextSwitch();
+            Gauge.builder("sysstat.sched.ctxswitch", pcs::getCswch)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.newproc", pcs::getProc)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering swap metrics");
+            SysstatMeasurement.SwapPages sp = this.lastMeasurement.getSwapPages();
+            Gauge.builder("sysstat.vm.paging.pg.in", sp::getPswpin)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.paging.pg.out", sp::getPswpout)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering paging metrics");
+            SysstatMeasurement.Paging pg = this.lastMeasurement.getPaging();
+            Gauge.builder("sysstat.vm.paging.kb.in", pg::getPgpgin)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.paging.kb.out", pg::getPgpgout)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.fault.total", pg::getFault)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.fault.major", pg::getMajflt)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.pg.freed", pg::getPgfree)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.pg.scan.kswapd", pg::getPgscank)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.pg.scan.direct", pg::getPgscand)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.pg.cache.reclaimed", pg::getPgsteal)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.vm.efficiency.pct", pg::getVmeffPercent)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering I/O metrics");
+            SysstatMeasurement.Io io = this.lastMeasurement.getIo();
+            Gauge.builder("sysstat.io.tps.total", io::getTps)
+                        .tags(tags)
+                        .register(this.mr);
+            SysstatMeasurement.Io.IoReads ir = io.getIoReads();
+            Gauge.builder("sysstat.io.tps.read", ir::getRtps)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.io.blk.read", ir::getBread)
+                        .tags(tags)
+                        .register(this.mr);
+            SysstatMeasurement.Io.IoWrites iw = io.getIoWrites();
+            Gauge.builder("sysstat.io.tps.write", iw::getWtps)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.io.blk.write", iw::getBwrtn)
+                        .tags(tags)
+                        .register(this.mr);
+            SysstatMeasurement.Io.IoDiscard id = io.getIoDiscard();
+            Gauge.builder("sysstat.io.tps.discard", id::getDtps)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.io.blk.discard", id::getBdscd)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering memory metrics");
+            SysstatMeasurement.Memory m = this.lastMeasurement.getMemory();
+            Gauge.builder("sysstat.mem.kb.free", m::getMemfree)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.avail", m::getAvail)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.used", m::getMemused)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.pct.used", m::getMemusedPercent)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.buf", m::getBuffers)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.cache", m::getCached)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.commit", m::getCommit)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.pct.commit", m::getCommitPercent)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.active", m::getActive)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.inactive", m::getInactive)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.dirty", m::getDirty)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.anonpg", m::getAnonpg)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.slab", m::getSlab)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.kstack", m::getKstack)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.pgtbl", m::getPgtbl)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.kb.vmused", m::getVmused)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.swap.kb.free", m::getSwpfree)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.swap.kb.used", m::getSwpused)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.swap.pct.used", m::getSwpusedPercent)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.swap.kb.cached", m::getSwpcad)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.swap.pct.cached", m::getSwpcadPercent)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering hugepage metrics");
+            SysstatMeasurement.Hugepages hp = this.lastMeasurement.getHugepages();
+            Gauge.builder("sysstat.mem.huge.kb.free", hp::getHugfree)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.huge.kb.used", hp::getHugused)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.huge.pct.used", hp::getHugusedPercent)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.huge.kb.reserved", hp::getHugrsvd)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.mem.huge.kb.surplus", hp::getHugsurp)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering kernel metrics");
+            SysstatMeasurement.Kernel k = this.lastMeasurement.getKernel();
+            Gauge.builder("sysstat.kernel.dentry.unused", k::getDentunusd)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.kernel.nr.file", k::getFileNr)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.kernel.nr.inode", k::getInodeNr)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.kernel.nr.pty", k::getPtyNr)
+                        .tags(tags)
+                        .register(this.mr);
+
+            LOG.fine("Registering scheduler metrics");
+            SysstatMeasurement.Queue q = this.lastMeasurement.getQueue();
+            Gauge.builder("sysstat.sched.sz.runq", q::getRunqSz)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.sz.plist", q::getPlistSz)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.load.1", q::getLdavg1)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.load.5", q::getLdavg5)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.load.15", q::getLdavg15)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.sched.sz.blocked", q::getBlocked)
+                        .tags(tags)
+                        .register(this.mr);
+
+            for (SysstatMeasurement.Disk dsk : this.lastMeasurement.getDisk()) {
+                LOG.fine("Registering blkdev metrics for " + dsk.getDiskDevice());
+                Tags dtags = Tags.of("host", this.lastMeasurement.getHostname(), "blkdev", dsk.getDiskDevice());
+                Gauge.builder("sysstat.io.dev.tps", dsk::getTps)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.read", dsk::getRkB)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.write", dsk::getWkB)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.discard", dsk::getDkB)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.queue.kb", dsk::getAreqSz)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.queue.req", dsk::getAquSz)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.queue.wait", dsk::getAwait)
+                            .tags(dtags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.io.dev.saturation", dsk::getUtilPercent)
+                            .tags(dtags)
+                            .register(this.mr);
+            }
+
+            LOG.fine("Registering network metrics");
+            SysstatMeasurement.Network n = this.lastMeasurement.getNetwork();
+
+            for (SysstatMeasurement.Network.NetDev nd : this.lastMeasurement.getNetwork().getNetDev()) {
+                LOG.fine("Registering netdev metrics for interface " + nd.getIface());
+                Tags ntags = Tags.of("host", this.lastMeasurement.getHostname(), "iface", nd.getIface());
+
+                Gauge.builder("sysstat.net.if.pkt.recv", nd::getRxpck)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.pkt.xmit", nd::getTxpck)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.kb.recv", nd::getRxkB)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.kb.xmit", nd::getTxkB)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.pkt.compressed.recv", nd::getRxcmp)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.pkt.compressed.xmit", nd::getTxcmp)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.pkt.multicast.recv", nd::getRxmcst)
+                            .tags(ntags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.saturation", nd::getIfutilPercent)
+                            .tags(ntags)
+                            .register(this.mr);
+            }
+
+            for (SysstatMeasurement.Network.NetEDev ne : this.lastMeasurement.getNetwork().getNetEDev()) {
+                LOG.fine("Registering error metrics for interface " + ne.getIface());
+                Tags etags = Tags.of("host", this.lastMeasurement.getHostname(), "iface", ne.getIface());
+                Gauge.builder("sysstat.net.if.err.recv.total", ne::getRxerr)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.xmit.total", ne::getTxerr)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.xmit.collision", ne::getColl)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.recv.drop", ne::getRxdrop)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.xmit.drop", ne::getTxdrop)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.xmit.carrier", ne::getTxcarr)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.recv.framing", ne::getRxfram)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.recv.fifo", ne::getRxfifo)
+                            .tags(etags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.if.err.xmit.fifo", ne::getTxfifo)
+                            .tags(etags)
+                            .register(this.mr);
+            }
+
+            SysstatMeasurement.Network.NetNfs nn = n.getNetNfs();
+            Gauge.builder("sysstat.net.nfs.total", nn::getCall)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfs.retrans", nn::getRetrans)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfs.read", nn::getRead)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfs.write", nn::getWrite)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfs.access", nn::getAccess)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfs.getattr", nn::getGetatt)
+                        .tags(tags)
+                        .register(this.mr);
+
+            SysstatMeasurement.Network.NetNfsd nd = n.getNetNfsd();
+            Gauge.builder("sysstat.net.nfsd.total", nd::getScall)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.error", nd::getBadcall)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.packets.total", nd::getPacket)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.packets.udp", nd::getUdp)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.packets.tcp", nd::getTcp)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.cache.hit", nd::getHit)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.cache.miss", nd::getMiss)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.op.read", nd::getSread)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.op.write", nd::getSwrite)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.op.access", nd::getSaccess)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.nfsd.op.getattr", nd::getSgetatt)
+                        .tags(tags)
+                        .register(this.mr);
+
+            SysstatMeasurement.Network.NetSock ns = n.getNetSock();
+            Gauge.builder("sysstat.net.sock.total", ns::getTotsck)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.sock.tcp", ns::getTcpsck)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.sock.udp", ns::getUdpsck)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.sock.raw", ns::getRawsck)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.ip.fragments", ns::getIpFrag)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.net.sock.timewait", ns::getTcpTw)
+                        .tags(tags)
+                        .register(this.mr);
+
+            for (SysstatMeasurement.Network.Softnet sn : this.lastMeasurement.getNetwork().getSoftnet()) {
+                LOG.fine("Registering network interrupts for CPU " + sn.getCpu());
+                Tags stags = Tags.of("host", this.lastMeasurement.getHostname(), "cpu", sn.getCpu());
+                Gauge.builder("sysstat.net.frames.total", sn::getTotal)
+                            .tags(stags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.frames.drop", sn::getDropd)
+                            .tags(stags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.irq.squeeze", sn::getSqueezd)
+                            .tags(stags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.irq.recv", sn::getRxRps)
+                            .tags(stags)
+                            .register(this.mr);
+                Gauge.builder("sysstat.net.irq.flowlimit", sn::getFlwLim)
+                            .tags(stags)
+                            .register(this.mr);
+            }
+
+            LOG.fine("Registering pressure-stall metrics");
+            SysstatMeasurement.Psi psi = this.lastMeasurement.getPsi();
+
+            SysstatMeasurement.Psi.PsiCpu pc = psi.getPsiCpu();
+            Gauge.builder("sysstat.pressure.cpu.some.10", pc::getSomeAvg10)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.cpu.some.60", pc::getSomeAvg60)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.cpu.some.300", pc::getSomeAvg300)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.cpu.some.fromlast", pc::getSomeAvg)
+                        .tags(tags)
+                        .register(this.mr);
+
+            SysstatMeasurement.Psi.PsiIoAndMem pi = psi.getPsiIo();
+            Gauge.builder("sysstat.pressure.io.some.10", pi::getSomeAvg10)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.some.60", pi::getSomeAvg60)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.some.300", pi::getSomeAvg300)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.some.fromlast", pi::getSomeAvg)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.all.10", pi::getFullAvg10)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.all.60", pi::getFullAvg60)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.all.300", pi::getFullAvg300)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.io.all.fromlast", pi::getFullAvg)
+                        .tags(tags)
+                        .register(this.mr);
+
+            SysstatMeasurement.Psi.PsiIoAndMem pm = psi.getPsiMem();
+            Gauge.builder("sysstat.pressure.mem.some.10", pm::getSomeAvg10)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.some.60", pm::getSomeAvg60)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.some.300", pm::getSomeAvg300)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.some.fromlast", pm::getSomeAvg)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.all.10", pm::getFullAvg10)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.all.60", pm::getFullAvg60)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.all.300", pm::getFullAvg300)
+                        .tags(tags)
+                        .register(this.mr);
+            Gauge.builder("sysstat.pressure.mem.all.fromlast", pm::getFullAvg)
+                        .tags(tags)
+                        .register(this.mr);
+        } else {
+            // clone sm into lastMeasurement
+            LOG.fine("Cloning new metric into existing one...");
+            this.lastMeasurement.clone(sm);
+        }
+    }
+
+}

+ 115 - 0
exporter/src/main/java/net/p0f/openshift/metrics/model/ProcessAccountingRecord.java

@@ -0,0 +1,115 @@
+package net.p0f.openshift.metrics.model;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
+import org.apache.camel.dataformat.bindy.annotation.DataField;
+
+@CsvRecord(separator = ",")
+public class ProcessAccountingRecord {
+    // Expected input source from "sa -ajlp", meaning:
+    //   -a = all records
+    //   -j = use seconds instead of minutes
+    //   -l = separate columns for system & user time
+    //   -p = show paging information (faults & swaps)
+    // Reporting time slice defaults to 10s but can be configured.
+    //
+    // Typical record:
+    //   1  0.01re  0.00u  0.01s  231min  0maj  0swp  sadc
+
+    // Assigned.
+    String hostName;
+    @DataField(pos = 8)
+    String processName;
+    @DataField(pos = 1)
+    int numCalls;
+    @DataField(pos = 2)
+    float elapsedTime;
+    @DataField(pos = 3)
+    float userTime;
+    @DataField(pos = 4)
+    float systemTime;
+    @DataField(pos = 5)
+    int minFaults;
+    @DataField(pos = 6)
+    int majFaults;
+    @DataField(pos = 7)
+    int swapEvents;
+
+    public ProcessAccountingRecord() {
+        try {
+            this.hostName = InetAddress.getLocalHost().getCanonicalHostName();
+        } catch (UnknownHostException uhe) {
+            this.hostName = "UNRESOLVABLE";
+        }
+    }
+    public String getHostName() {
+        return hostName;
+    }
+    public void setHostName(String hn) {
+        this.hostName = hn;
+    }
+    public String getProcessName() {
+        return processName;
+    }
+    public void setProcessName(String pn) {
+        this.processName = pn;
+    }
+    public int getNumCalls() {
+        return numCalls;
+    }
+    public void setNumCalls(int numCalls) {
+        this.numCalls = numCalls;
+    }
+    public float getElapsedTime() {
+        return elapsedTime;
+    }
+    public void setElapsedTime(float elapsedTime) {
+        this.elapsedTime = elapsedTime;
+    }
+    public float getUserTime() {
+        return userTime;
+    }
+    public void setUserTime(float userTime) {
+        this.userTime = userTime;
+    }
+    public float getSystemTime() {
+        return systemTime;
+    }
+    public void setSystemTime(float systemTime) {
+        this.systemTime = systemTime;
+    }
+    public int getMinFaults() {
+        return minFaults;
+    }
+    public void setMinFaults(int minFaults) {
+        this.minFaults = minFaults;
+    }
+    public int getMajFaults() {
+        return majFaults;
+    }
+    public void setMajFaults(int majFaults) {
+        this.majFaults = majFaults;
+    }
+    public int getSwapEvents() {
+        return swapEvents;
+    }
+    public void setSwapEvents(int swapEvents) {
+        this.swapEvents = swapEvents;
+    }
+    @Override
+    public String toString() {
+        return "ProcessAccountingRecord ["
+                + "hostName=" + hostName
+                + ", processName=" + processName
+                + ", numCalls=" + numCalls
+                + ", elapsedTime=" + elapsedTime
+                + ", systemTime=" + systemTime
+                + ", userTime=" + userTime
+                + ", majFaults=" + majFaults
+                + ", minFaults=" + minFaults
+                + ", swapEvents=" + swapEvents
+                + "]";
+    }
+}

+ 1775 - 0
exporter/src/main/java/net/p0f/openshift/metrics/model/SysstatMeasurement.java

@@ -0,0 +1,1775 @@
+package net.p0f.openshift.metrics.model;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class SysstatMeasurement implements Serializable {
+    // Getting data from "sadf -s t0 -e t0+td -j -- -A" where
+    //   t0 = time of last loop start (start of reporting period)
+    //   td = report snapshot duration (defaults to 10s; end of reporting period)
+    //   -j = produce JSON report
+    //   -A = sar -A (aka "-bBdFHSvwWy -I SUM -m ALL -n ALL -q ALL -r ALL -u ALL"), where
+    //      GLOBAL STATS
+    //      -b = I/O and xfrate (tps, rtps, wtps, dtps /discard/, bread/s, bwrtn/s, bdscd/s)
+    //      -B = paging statistics (pgin/s, pgout/s, fault/s, Mflt/s, free/s, scank/s, scand/s, steal/s, vmeff%)
+    //      -d = activity per blkdev (tps, rkB/s, wkB/s, dkB/s, areq-sz, aqu-sz, await, %util)
+    //      -F = mount stats (MBfree, MBused, %used, %unpriv-used, Ifree, Iused, %Iused)
+    //      -H = hugepage stats (kbfree, kbused, %used, kbrsrvd, kbsurplus)
+    //      -S = swap stats (kbfree, kbused, %used, kbcached, %cached)
+    //      -v = kernel table stats (dentry-unused, file-nr, inode-nr, pty-nr)
+    //      -w = task creation & switching (proc/s, csw/s)
+    //      -W = swapping stats (swpin/s, swpout/s)
+    //      -y = ttydev stats (rcvintr/s, xmtintr/s, framerr/s, prtyerr/s, brk/s, ovrun/s)
+    //      PER-ITEM STATS
+    //      -I SUM = all interrupts (NOT COLLECTED BY sadc)
+    //      -m ALL = power management stats (NOT COLLECTED BY sadc)
+    //      -n ALL = network interface stats (ALL meaning DEV, EDEV, FC, ICMP, EICMP, ICMP6, EICMP6,
+    //                  IP, EIP, IP6, EIP6, NFS, NFSD, SOCK, SOCK6, SOFT, TCP, ETCP, UDP and UDP6)
+    //      -q ALL = load and pressure stats (%scpu + 10/60/300wnd /stalled/, %sio + wnd /lost to IO/,
+    //                  %fio + wnd /stalled to IO/, runq-sz, plist-sz, ldavg-1/5/15, blocked,
+    //                  %smem + wnd /stalled to mem/, %fmem + wnd /waiting for mem/)
+    //      -r ALL = memory utilisation (kbfree, kbavail, %used, kbbuf, kbcach, kbcommit, %commit,
+    //                  kbact, kbinact, kbdirty, kbanonpg, kbslab, kbkstack, kbpgtbl, kbvmused)
+    //      -u ALL = cpu utilisation (%user, %usr w/o vproc, %nice, %system, %sys w/o intr, %iowait,
+    //                  %steal, %irq, %soft, %guest, %gnice, %idle)
+    // Reporting time slice defaults to 10s but can be changed.
+    //
+    // Typical record is too long to show here but look at annotations.
+    // Also look at SysstatConsumer to see the necessary transformations.
+    private static final Logger LOG = Logger.getLogger(SysstatMeasurement.class.getName());
+
+    @JsonProperty("hostname")
+    String hostname;
+    @JsonProperty("num-cpus")
+    int numCpus;
+
+    @JsonProperty("cpu-load")
+    List<CpuLoad> cpuLoad;
+    @JsonProperty("process-and-context-switch")
+    ProcessAndContextSwitch processAndContextSwitch;
+    @JsonProperty("swap-pages")
+    SwapPages swapPages;
+    @JsonProperty("paging")
+    Paging paging;
+    @JsonProperty("io")
+    Io io;
+    @JsonProperty("memory")
+    Memory memory;
+    @JsonProperty("hugepages")
+    Hugepages hugepages;
+    @JsonProperty("kernel")
+    Kernel kernel;
+    @JsonProperty("queue")
+    Queue queue;
+    @JsonProperty("disk")
+    List<Disk> disk;
+    @JsonProperty("network")
+    Network network;
+    @JsonProperty("psi")
+    Psi psi;
+
+    public String getHostname() {
+        return hostname;
+    }
+    public void setHostname(String hostname) {
+        this.hostname = hostname;
+    }
+    public int getNumCpus() {
+        return numCpus;
+    }
+    public void setNumCpus(int numCpus) {
+        this.numCpus = numCpus;
+    }
+    public List<CpuLoad> getCpuLoad() {
+        return cpuLoad;
+    }
+    public void setCpuLoad(List<CpuLoad> cpuLoad) {
+        this.cpuLoad = cpuLoad;
+    }
+    public ProcessAndContextSwitch getProcessAndContextSwitch() {
+        return processAndContextSwitch;
+    }
+    public void setProcessAndContextSwitch(ProcessAndContextSwitch processAndContextSwitch) {
+        this.processAndContextSwitch = processAndContextSwitch;
+    }
+    public SwapPages getSwapPages() {
+        return swapPages;
+    }
+    public void setSwapPages(SwapPages swapPages) {
+        this.swapPages = swapPages;
+    }
+    public Paging getPaging() {
+        return paging;
+    }
+    public void setPaging(Paging paging) {
+        this.paging = paging;
+    }
+    public Io getIo() {
+        return io;
+    }
+    public void setIo(Io io) {
+        this.io = io;
+    }
+    public Memory getMemory() {
+        return memory;
+    }
+    public void setMemory(Memory memory) {
+        this.memory = memory;
+    }
+    public Hugepages getHugepages() {
+        return hugepages;
+    }
+    public void setHugepages(Hugepages hugepages) {
+        this.hugepages = hugepages;
+    }
+    public Kernel getKernel() {
+        return kernel;
+    }
+    public void setKernel(Kernel kernel) {
+        this.kernel = kernel;
+    }
+    public Queue getQueue() {
+        return queue;
+    }
+    public void setQueue(Queue queue) {
+        this.queue = queue;
+    }
+    public List<Disk> getDisk() {
+        return disk;
+    }
+    public void setDisk(List<Disk> disk) {
+        this.disk = disk;
+    }
+    public Network getNetwork() {
+        return network;
+    }
+    public void setNetwork(Network network) {
+        this.network = network;
+    }
+    public Psi getPsi() {
+        return psi;
+    }
+    public void setPsi(Psi psi) {
+        this.psi = psi;
+    }
+    @Override
+    public String toString() {
+        return "SysstatMeasurement [cpuLoad=" + cpuLoad + ", disk=" + disk + ", hostname=" + hostname + ", hugepages="
+                + hugepages + ", io=" + io + ", kernel=" + kernel + ", memory=" + memory + ", network=" + network
+                + ", numCpus=" + numCpus + ", paging=" + paging + ", processAndContextSwitch=" + processAndContextSwitch
+                + ", psi=" + psi + ", queue=" + queue + ", swapPages=" + swapPages + "]";
+    }
+    public void clone(SysstatMeasurement sm) {
+        this.hostname = sm.hostname;
+        this.numCpus = sm.numCpus;
+        for (int x = 0; x < sm.cpuLoad.size(); x++) {
+            if (this.cpuLoad.size() <= x) {
+                // should not happen - handle metrics registration in this case!
+                LOG.warning("Number of CPUs different between cloned object and myself! " +
+                            "Might need to update metrics trackers?");
+                this.cpuLoad.set(x, new CpuLoad());
+            }
+            this.cpuLoad.get(x).clone(sm.cpuLoad.get(x));
+        }
+        this.processAndContextSwitch.clone(sm.processAndContextSwitch);
+        this.swapPages.clone(sm.swapPages);
+        this.paging.clone(sm.paging);
+        this.io.clone(sm.io);
+        this.memory.clone(sm.memory);
+        this.hugepages.clone(sm.hugepages);
+        this.kernel.clone(sm.kernel);
+        this.queue.clone(sm.queue);
+        for (int x = 0; x < sm.disk.size(); x++) {
+            if (this.disk.size() <= x) {
+                // should not happen - handle metrics registration in this case!
+                LOG.warning("Number of disk devices different between cloned object and myself! " +
+                            "Might need to update metrics trackers?");
+                this.disk.set(x, new Disk());
+            }
+            this.disk.get(x).clone(sm.disk.get(x));
+        }
+        this.network.clone(sm.network);
+        this.psi.clone(sm.psi);
+    }
+
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class CpuLoad {
+        // NOTE: not including "user", which is "usr + guest"
+        // NOTE: not including "system", which is "sys + irq + soft"
+        @JsonProperty
+        String cpu;
+        @JsonProperty
+        float usr;
+        @JsonProperty
+        float nice;
+        @JsonProperty
+        float sys;
+        @JsonProperty
+        float iowait;
+        @JsonProperty
+        float steal;
+        @JsonProperty
+        float irq;
+        @JsonProperty
+        float soft;
+        @JsonProperty
+        float guest;
+        @JsonProperty
+        float gnice;
+        @JsonProperty
+        float idle;
+        public String getCpu() {
+            return cpu;
+        }
+        public void setCpu(String cpu) {
+            this.cpu = cpu;
+        }
+        public float getUsr() {
+            return usr;
+        }
+        public void setUsr(float usr) {
+            this.usr = usr;
+        }
+        public float getNice() {
+            return nice;
+        }
+        public void setNice(float nice) {
+            this.nice = nice;
+        }
+        public float getSys() {
+            return sys;
+        }
+        public void setSys(float sys) {
+            this.sys = sys;
+        }
+        public float getIowait() {
+            return iowait;
+        }
+        public void setIowait(float iowait) {
+            this.iowait = iowait;
+        }
+        public float getSteal() {
+            return steal;
+        }
+        public void setSteal(float steal) {
+            this.steal = steal;
+        }
+        public float getIrq() {
+            return irq;
+        }
+        public void setIrq(float irq) {
+            this.irq = irq;
+        }
+        public float getSoft() {
+            return soft;
+        }
+        public void setSoft(float soft) {
+            this.soft = soft;
+        }
+        public float getGuest() {
+            return guest;
+        }
+        public void setGuest(float guest) {
+            this.guest = guest;
+        }
+        public float getGnice() {
+            return gnice;
+        }
+        public void setGnice(float gnice) {
+            this.gnice = gnice;
+        }
+        public float getIdle() {
+            return idle;
+        }
+        public void setIdle(float idle) {
+            this.idle = idle;
+        }
+        @Override
+        public String toString() {
+            return "CpuLoad [cpu=" + cpu + ", gnice=" + gnice + ", guest=" + guest + ", idle=" + idle + ", iowait="
+                    + iowait + ", irq=" + irq + ", nice=" + nice + ", soft=" + soft + ", steal=" + steal + ", sys="
+                    + sys + ", usr=" + usr + "]";
+        }
+        public void clone(CpuLoad cl) {
+            this.cpu = cl.cpu;
+            this.usr = cl.usr;
+            this.nice = cl.nice;
+            this.sys = cl.sys;
+            this.iowait = cl.iowait;
+            this.steal = cl.steal;
+            this.irq = cl.irq;
+            this.soft = cl.soft;
+            this.guest = cl.guest;
+            this.gnice = cl.gnice;
+            this.idle = cl.idle;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class ProcessAndContextSwitch {
+        @JsonProperty
+        float proc;
+        @JsonProperty
+        float cswch;
+        public float getProc() {
+            return proc;
+        }
+        public void setProc(float proc) {
+            this.proc = proc;
+        }
+        public float getCswch() {
+            return cswch;
+        }
+        public void setCswch(float cswch) {
+            this.cswch = cswch;
+        }
+        @Override
+        public String toString() {
+            return "ProcessAndContextSwitch [cswch=" + cswch + ", proc=" + proc + "]";
+        }
+        public void clone(ProcessAndContextSwitch pc) {
+            this.proc = pc.proc;
+            this.cswch = pc.cswch;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class SwapPages {
+        @JsonProperty
+        float pswpin;
+        @JsonProperty
+        float pswpout;
+        public float getPswpin() {
+            return pswpin;
+        }
+        public void setPswpin(float pswpin) {
+            this.pswpin = pswpin;
+        }
+        public float getPswpout() {
+            return pswpout;
+        }
+        public void setPswpout(float pswpout) {
+            this.pswpout = pswpout;
+        }
+        @Override
+        public String toString() {
+            return "SwapPages [pswpin=" + pswpin + ", pswpout=" + pswpout + "]";
+        }
+        public void clone(SwapPages sp) {
+            this.pswpin = sp.pswpin;
+            this.pswpout = sp.pswpout;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Paging {
+        @JsonProperty
+        float pgpgin;
+        @JsonProperty
+        float pgpgout;
+        @JsonProperty
+        float fault;
+        @JsonProperty
+        float majflt;
+        @JsonProperty
+        float pgfree;
+        @JsonProperty
+        float pgscank;
+        @JsonProperty
+        float pgscand;
+        @JsonProperty
+        float pgsteal;
+        @JsonProperty("vmeff-percent")
+        float vmeffPercent;
+        public float getPgpgin() {
+            return pgpgin;
+        }
+        public void setPgpgin(float pgpgin) {
+            this.pgpgin = pgpgin;
+        }
+        public float getPgpgout() {
+            return pgpgout;
+        }
+        public void setPgpgout(float pgpgout) {
+            this.pgpgout = pgpgout;
+        }
+        public float getFault() {
+            return fault;
+        }
+        public void setFault(float fault) {
+            this.fault = fault;
+        }
+        public float getMajflt() {
+            return majflt;
+        }
+        public void setMajflt(float majflt) {
+            this.majflt = majflt;
+        }
+        public float getPgfree() {
+            return pgfree;
+        }
+        public void setPgfree(float pgfree) {
+            this.pgfree = pgfree;
+        }
+        public float getPgscank() {
+            return pgscank;
+        }
+        public void setPgscank(float pgscank) {
+            this.pgscank = pgscank;
+        }
+        public float getPgscand() {
+            return pgscand;
+        }
+        public void setPgscand(float pgscand) {
+            this.pgscand = pgscand;
+        }
+        public float getPgsteal() {
+            return pgsteal;
+        }
+        public void setPgsteal(float pgsteal) {
+            this.pgsteal = pgsteal;
+        }
+        public float getVmeffPercent() {
+            return vmeffPercent;
+        }
+        public void setVmeffPercent(float vmeffPercent) {
+            this.vmeffPercent = vmeffPercent;
+        }
+        @Override
+        public String toString() {
+            return "Paging [fault=" + fault + ", majflt=" + majflt + ", pgfree=" + pgfree + ", pgpgin=" + pgpgin
+                    + ", pgpgout=" + pgpgout + ", pgscand=" + pgscand + ", pgscank=" + pgscank + ", pgsteal=" + pgsteal
+                    + ", vmeffPercent=" + vmeffPercent + "]";
+        }
+        public void clone(Paging pg) {
+            this.pgpgin = pg.pgpgin;
+            this.pgpgout = pg.pgpgout;
+            this.fault = pg.fault;
+            this.majflt = pg.majflt;
+            this.pgfree = pg.pgfree;
+            this.pgscank = pg.pgscank;
+            this.pgscand = pg.pgscand;
+            this.pgsteal = pg.pgsteal;
+            this.vmeffPercent = pg.vmeffPercent;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Io {
+        @JsonProperty
+        float tps;
+        @JsonProperty("io-reads")
+        IoReads ioReads;
+        @JsonProperty("io-writes")
+        IoWrites ioWrites;
+        @JsonProperty("io-discard")
+        IoDiscard ioDiscard;
+        public float getTps() {
+            return tps;
+        }
+        public void setTps(float tps) {
+            this.tps = tps;
+        }
+        public IoReads getIoReads() {
+            return ioReads;
+        }
+        public void setIoReads(IoReads ioReads) {
+            this.ioReads = ioReads;
+        }
+        public IoWrites getIoWrites() {
+            return ioWrites;
+        }
+        public void setIoWrites(IoWrites ioWrites) {
+            this.ioWrites = ioWrites;
+        }
+        public IoDiscard getIoDiscard() {
+            return ioDiscard;
+        }
+        public void setIoDiscard(IoDiscard ioDiscard) {
+            this.ioDiscard = ioDiscard;
+        }
+        @Override
+        public String toString() {
+            return "Io [ioDiscard=" + ioDiscard + ", ioReads=" + ioReads + ", ioWrites=" + ioWrites + ", tps=" + tps
+                    + "]";
+        }
+        public void clone(Io io) {
+            this.tps = io.tps;
+            this.ioReads.clone(io.ioReads);
+            this.ioWrites.clone(io.ioWrites);
+            this.ioDiscard.clone(io.ioDiscard);
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class IoReads {
+            @JsonProperty
+            float rtps;
+            @JsonProperty
+            float bread;
+            public float getRtps() {
+                return rtps;
+            }
+            public void setRtps(float rtps) {
+                this.rtps = rtps;
+            }
+            public float getBread() {
+                return bread;
+            }
+            public void setBread(float bread) {
+                this.bread = bread;
+            }
+            @Override
+            public String toString() {
+                return "IoReads [bread=" + bread + ", rtps=" + rtps + "]";
+            }
+            public void clone(IoReads ir) {
+                this.rtps = ir.rtps;
+                this.bread = ir.bread;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class IoWrites {
+            @JsonProperty
+            float wtps;
+            @JsonProperty
+            float bwrtn;
+            public float getWtps() {
+                return wtps;
+            }
+            public void setWtps(float wtps) {
+                this.wtps = wtps;
+            }
+            public float getBwrtn() {
+                return bwrtn;
+            }
+            public void setBwrtn(float bwrtn) {
+                this.bwrtn = bwrtn;
+            }
+            @Override
+            public String toString() {
+                return "IoWrites [bwrtn=" + bwrtn + ", wtps=" + wtps + "]";
+            }
+            public void clone(IoWrites iw) {
+                this.wtps = iw.wtps;
+                this.bwrtn = iw.bwrtn;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class IoDiscard {
+            @JsonProperty
+            float dtps;
+            @JsonProperty
+            float bdscd;
+            public float getDtps() {
+                return dtps;
+            }
+            public void setDtps(float dtps) {
+                this.dtps = dtps;
+            }
+            public float getBdscd() {
+                return bdscd;
+            }
+            public void setBdscd(float bdscd) {
+                this.bdscd = bdscd;
+            }
+            @Override
+            public String toString() {
+                return "IoDiscard [bdscd=" + bdscd + ", dtps=" + dtps + "]";
+            }
+            public void clone(IoDiscard id) {
+                this.dtps = id.dtps;
+                this.bdscd = id.bdscd;
+            }
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Memory {
+        @JsonProperty
+        int memfree;
+        @JsonProperty
+        int avail;
+        @JsonProperty
+        int memused;
+        @JsonProperty("memused-percent")
+        float memusedPercent;
+        @JsonProperty
+        int buffers;
+        @JsonProperty
+        int cached;
+        @JsonProperty
+        int commit;
+        @JsonProperty("commit-percent")
+        float commitPercent;
+        @JsonProperty
+        int active;
+        @JsonProperty
+        int inactive;
+        @JsonProperty
+        int dirty;
+        @JsonProperty
+        int anonpg;
+        @JsonProperty
+        int slab;
+        @JsonProperty
+        int kstack;
+        @JsonProperty
+        int pgtbl;
+        @JsonProperty
+        int vmused;
+        @JsonProperty
+        int swpfree;
+        @JsonProperty
+        int swpused;
+        @JsonProperty("swpused-percent")
+        float swpusedPercent;
+        @JsonProperty
+        int swpcad;
+        @JsonProperty("swpcad-percent")
+        float swpcadPercent;
+        public int getMemfree() {
+            return memfree;
+        }
+        public void setMemfree(int memfree) {
+            this.memfree = memfree;
+        }
+        public int getAvail() {
+            return avail;
+        }
+        public void setAvail(int avail) {
+            this.avail = avail;
+        }
+        public int getMemused() {
+            return memused;
+        }
+        public void setMemused(int memused) {
+            this.memused = memused;
+        }
+        public float getMemusedPercent() {
+            return memusedPercent;
+        }
+        public void setMemusedPercent(float memusedPercent) {
+            this.memusedPercent = memusedPercent;
+        }
+        public int getBuffers() {
+            return buffers;
+        }
+        public void setBuffers(int buffers) {
+            this.buffers = buffers;
+        }
+        public int getCached() {
+            return cached;
+        }
+        public void setCached(int cached) {
+            this.cached = cached;
+        }
+        public int getCommit() {
+            return commit;
+        }
+        public void setCommit(int commit) {
+            this.commit = commit;
+        }
+        public float getCommitPercent() {
+            return commitPercent;
+        }
+        public void setCommitPercent(float commitPercent) {
+            this.commitPercent = commitPercent;
+        }
+        public int getActive() {
+            return active;
+        }
+        public void setActive(int active) {
+            this.active = active;
+        }
+        public int getInactive() {
+            return inactive;
+        }
+        public void setInactive(int inactive) {
+            this.inactive = inactive;
+        }
+        public int getDirty() {
+            return dirty;
+        }
+        public void setDirty(int dirty) {
+            this.dirty = dirty;
+        }
+        public int getAnonpg() {
+            return anonpg;
+        }
+        public void setAnonpg(int anonpg) {
+            this.anonpg = anonpg;
+        }
+        public int getSlab() {
+            return slab;
+        }
+        public void setSlab(int slab) {
+            this.slab = slab;
+        }
+        public int getKstack() {
+            return kstack;
+        }
+        public void setKstack(int kstack) {
+            this.kstack = kstack;
+        }
+        public int getPgtbl() {
+            return pgtbl;
+        }
+        public void setPgtbl(int pgtbl) {
+            this.pgtbl = pgtbl;
+        }
+        public int getVmused() {
+            return vmused;
+        }
+        public void setVmused(int vmused) {
+            this.vmused = vmused;
+        }
+        public int getSwpfree() {
+            return swpfree;
+        }
+        public void setSwpfree(int swpfree) {
+            this.swpfree = swpfree;
+        }
+        public int getSwpused() {
+            return swpused;
+        }
+        public void setSwpused(int swpused) {
+            this.swpused = swpused;
+        }
+        public float getSwpusedPercent() {
+            return swpusedPercent;
+        }
+        public void setSwpusedPercent(float swpusedPercent) {
+            this.swpusedPercent = swpusedPercent;
+        }
+        public int getSwpcad() {
+            return swpcad;
+        }
+        public void setSwpcad(int swpcad) {
+            this.swpcad = swpcad;
+        }
+        public float getSwpcadPercent() {
+            return swpcadPercent;
+        }
+        public void setSwpcadPercent(float swpcadPercent) {
+            this.swpcadPercent = swpcadPercent;
+        }
+        @Override
+        public String toString() {
+            return "Memory [active=" + active + ", anonpg=" + anonpg + ", avail=" + avail + ", buffers=" + buffers
+                    + ", cached=" + cached + ", commit=" + commit + ", commitPercent=" + commitPercent + ", dirty="
+                    + dirty + ", inactive=" + inactive + ", kstack=" + kstack + ", memfree=" + memfree + ", memused="
+                    + memused + ", memusedPercent=" + memusedPercent + ", pgtbl=" + pgtbl + ", slab=" + slab
+                    + ", swpcad=" + swpcad + ", swpcadPercent=" + swpcadPercent + ", swpfree=" + swpfree + ", swpused="
+                    + swpused + ", swpusedPercent=" + swpusedPercent + ", vmused=" + vmused + "]";
+        }
+        public void clone(Memory m) {
+            this.memfree = m.memfree;
+            this.avail = m.avail;
+            this.memused = m.memused;
+            this.memusedPercent = m.memusedPercent;
+            this.buffers = m.buffers;
+            this.cached = m.cached;
+            this.commit = m.commit;
+            this.commitPercent = m.commitPercent;
+            this.active = m.active;
+            this.inactive = m.inactive;
+            this.dirty = m.dirty;
+            this.anonpg = m.anonpg;
+            this.slab = m.slab;
+            this.kstack = m.kstack;
+            this.pgtbl = m.pgtbl;
+            this.vmused = m.vmused;
+            this.swpfree = m.swpfree;
+            this.swpused = m.swpused;
+            this.swpusedPercent = m.swpusedPercent;
+            this.swpcad = m.swpcad;
+            this.swpcadPercent = m.swpcadPercent;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Hugepages {
+        @JsonProperty
+        int hugfree;
+        @JsonProperty
+        int hugused;
+        @JsonProperty("hugused-percent")
+        float hugusedPercent;
+        @JsonProperty
+        int hugrsvd;
+        @JsonProperty
+        int hugsurp;
+        public int getHugfree() {
+            return hugfree;
+        }
+        public void setHugfree(int hugfree) {
+            this.hugfree = hugfree;
+        }
+        public int getHugused() {
+            return hugused;
+        }
+        public void setHugused(int hugused) {
+            this.hugused = hugused;
+        }
+        public float getHugusedPercent() {
+            return hugusedPercent;
+        }
+        public void setHugusedPercent(float hugusedPercent) {
+            this.hugusedPercent = hugusedPercent;
+        }
+        public int getHugrsvd() {
+            return hugrsvd;
+        }
+        public void setHugrsvd(int hugrsvd) {
+            this.hugrsvd = hugrsvd;
+        }
+        public int getHugsurp() {
+            return hugsurp;
+        }
+        public void setHugsurp(int hugsurp) {
+            this.hugsurp = hugsurp;
+        }
+        @Override
+        public String toString() {
+            return "Hugepages [hugfree=" + hugfree + ", hugrsvd=" + hugrsvd + ", hugsurp=" + hugsurp + ", hugused="
+                    + hugused + ", hugusedPercent=" + hugusedPercent + "]";
+        }
+        public void clone(Hugepages h) {
+            this.hugfree = h.hugfree;
+            this.hugused = h.hugused;
+            this.hugusedPercent = h.hugusedPercent;
+            this.hugrsvd = h.hugrsvd;
+            this.hugsurp = h.hugsurp;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Kernel {
+        @JsonProperty
+        int dentunusd;
+        @JsonProperty("file-nr")
+        int fileNr;
+        @JsonProperty("inode-nr")
+        int inodeNr;
+        @JsonProperty("pty-nr")
+        int ptyNr;
+        public int getDentunusd() {
+            return dentunusd;
+        }
+        public void setDentunusd(int dentunusd) {
+            this.dentunusd = dentunusd;
+        }
+        public int getFileNr() {
+            return fileNr;
+        }
+        public void setFileNr(int fileNr) {
+            this.fileNr = fileNr;
+        }
+        public int getInodeNr() {
+            return inodeNr;
+        }
+        public void setInodeNr(int inodeNr) {
+            this.inodeNr = inodeNr;
+        }
+        public int getPtyNr() {
+            return ptyNr;
+        }
+        public void setPtyNr(int ptyNr) {
+            this.ptyNr = ptyNr;
+        }
+        @Override
+        public String toString() {
+            return "Kernel [dentunusd=" + dentunusd + ", fileNr=" + fileNr + ", inodeNr=" + inodeNr + ", ptyNr=" + ptyNr
+                    + "]";
+        }
+        public void clone(Kernel k) {
+            this.dentunusd = k.dentunusd;
+            this.fileNr = k.fileNr;
+            this.inodeNr = k.inodeNr;
+            this.ptyNr = k.ptyNr;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Queue {
+        @JsonProperty("runq-sz")
+        int runqSz;
+        @JsonProperty("plist-sz")
+        int plistSz;
+        @JsonProperty("ldavg-1")
+        float ldavg1;
+        @JsonProperty("ldavg-5")
+        float ldavg5;
+        @JsonProperty("ldavg-15")
+        float ldavg15;
+        @JsonProperty
+        int blocked;
+        public int getRunqSz() {
+            return runqSz;
+        }
+        public void setRunqSz(int runqSz) {
+            this.runqSz = runqSz;
+        }
+        public int getPlistSz() {
+            return plistSz;
+        }
+        public void setPlistSz(int plistSz) {
+            this.plistSz = plistSz;
+        }
+        public float getLdavg1() {
+            return ldavg1;
+        }
+        public void setLdavg1(float ldavg1) {
+            this.ldavg1 = ldavg1;
+        }
+        public float getLdavg5() {
+            return ldavg5;
+        }
+        public void setLdavg5(float ldavg5) {
+            this.ldavg5 = ldavg5;
+        }
+        public float getLdavg15() {
+            return ldavg15;
+        }
+        public void setLdavg15(float ldavg15) {
+            this.ldavg15 = ldavg15;
+        }
+        public int getBlocked() {
+            return blocked;
+        }
+        public void setBlocked(int blocked) {
+            this.blocked = blocked;
+        }
+        @Override
+        public String toString() {
+            return "Queue [blocked=" + blocked + ", ldavg1=" + ldavg1 + ", ldavg15=" + ldavg15 + ", ldavg5=" + ldavg5
+                    + ", plistSz=" + plistSz + ", runqSz=" + runqSz + "]";
+        }
+        public void clone(Queue q) {
+            this.runqSz = q.runqSz;
+            this.plistSz = q.plistSz;
+            this.ldavg1 = q.ldavg1;
+            this.ldavg5 = q.ldavg5;
+            this.ldavg15 = q.ldavg15;
+            this.blocked = q.blocked;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Disk {
+        @JsonProperty("disk-device")
+        String diskDevice;
+        @JsonProperty
+        float tps;
+        // This is in sectors. Nobody uses sectors.
+        // @JsonProperty("rd_sec")
+        // float rdSec;
+        // @JsonProperty("wr_sec")
+        // float wrSec;
+        // @JsonProperty("dc_sec")
+        // float dcSec;
+        @JsonProperty
+        float rkB;
+        @JsonProperty
+        float wkB;
+        @JsonProperty
+        float dkB;
+        // This is in sectors. Nobody uses sectors.
+        // @JsonProperty("avgrq-sz")
+        // float avgrqSz;
+        @JsonProperty("areq-sz")
+        float areqSz;
+        // This is in sectors. Nobody uses sectors.
+        // @JsonProperty("avgqu-sz")
+        // float avgquSz;
+        @JsonProperty("aqu-sz")
+        float aquSz;
+        @JsonProperty
+        float await;
+        @JsonProperty("util-percent")
+        float utilPercent;
+        public String getDiskDevice() {
+            return diskDevice;
+        }
+        public void setDiskDevice(String diskDevice) {
+            this.diskDevice = diskDevice;
+        }
+        public float getTps() {
+            return tps;
+        }
+        public void setTps(float tps) {
+            this.tps = tps;
+        }
+        public float getRkB() {
+            return rkB;
+        }
+        public void setRkB(float rkB) {
+            this.rkB = rkB;
+        }
+        public float getWkB() {
+            return wkB;
+        }
+        public void setWkB(float wkB) {
+            this.wkB = wkB;
+        }
+        public float getDkB() {
+            return dkB;
+        }
+        public void setDkB(float dkB) {
+            this.dkB = dkB;
+        }
+        public float getAreqSz() {
+            return areqSz;
+        }
+        public void setAreqSz(float areqSz) {
+            this.areqSz = areqSz;
+        }
+        public float getAquSz() {
+            return aquSz;
+        }
+        public void setAquSz(float aquSz) {
+            this.aquSz = aquSz;
+        }
+        public float getAwait() {
+            return await;
+        }
+        public void setAwait(float await) {
+            this.await = await;
+        }
+        public float getUtilPercent() {
+            return utilPercent;
+        }
+        public void setUtilPercent(float utilPercent) {
+            this.utilPercent = utilPercent;
+        }
+        @Override
+        public String toString() {
+            return "Disk [diskDevice=" + diskDevice + ", aquSz=" + aquSz + ", areqSz=" + areqSz
+                    + ", await=" + await + ", tps=" + tps + ", utilPercent=" + utilPercent
+                    + ", dkB=" + dkB + ", rkB=" + rkB + ", wkB=" + wkB + "]";
+        }
+        public void clone(Disk d) {
+            this.diskDevice = d.diskDevice;
+            this.tps = d.tps;
+            this.rkB = d.rkB;
+            this.wkB = d.wkB;
+            this.dkB = d.dkB;
+            this.areqSz = d.areqSz;
+            this.aquSz = d.aquSz;
+            this.await = d.await;
+            this.utilPercent = d.utilPercent;
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Network {
+        @JsonProperty("net-dev")
+        List<NetDev> netDev;
+        @JsonProperty("net-edev")
+        List<NetEDev> netEDev;
+        @JsonProperty("net-nfs")
+        NetNfs netNfs;
+        @JsonProperty("net-nfsd")
+        NetNfsd netNfsd;
+        @JsonProperty("net-sock")
+        NetSock netSock;
+        @JsonProperty
+        List<Softnet> softnet;
+        public List<NetDev> getNetDev() {
+            return netDev;
+        }
+        public void setNetDev(List<NetDev> netDev) {
+            this.netDev = netDev;
+        }
+        public List<NetEDev> getNetEDev() {
+            return netEDev;
+        }
+        public void setNetEDev(List<NetEDev> netEDev) {
+            this.netEDev = netEDev;
+        }
+        public NetNfs getNetNfs() {
+            return netNfs;
+        }
+        public void setNetNfs(NetNfs netNfs) {
+            this.netNfs = netNfs;
+        }
+        public NetNfsd getNetNfsd() {
+            return netNfsd;
+        }
+        public void setNetNfsd(NetNfsd netNfsd) {
+            this.netNfsd = netNfsd;
+        }
+        public NetSock getNetSock() {
+            return netSock;
+        }
+        public void setNetSock(NetSock netSock) {
+            this.netSock = netSock;
+        }
+        public List<Softnet> getSoftnet() {
+            return softnet;
+        }
+        public void setSoftnet(List<Softnet> softnet) {
+            this.softnet = softnet;
+        }
+        @Override
+        public String toString() {
+            return "Network [netDev=" + netDev + ", netEDev=" + netEDev + ", netNfs=" + netNfs + ", netNfsd=" + netNfsd
+                    + ", netSock=" + netSock + ", softnet=" + softnet + "]";
+        }
+        public void clone(Network n) {
+            for (int x = 0; x < n.netDev.size(); x++) {
+                if (this.netDev.size() <= x) {
+                    // should not happen - handle metrics registration in this case!
+                    LOG.warning("Number of network devices different between cloned object and myself! " +
+                                "Might need to update metrics trackers?");
+                    this.netDev.set(x, new NetDev());
+                }
+                this.netDev.get(x).clone(n.netDev.get(x));
+            }   
+            for (int x = 0; x < n.netEDev.size(); x++) {
+                if (this.netEDev.size() <= x) {
+                    // should not happen - handle metrics registration in this case!
+                    LOG.warning("Number of network devices different between cloned object and myself! " +
+                                "Might need to update metrics trackers?");
+                    this.netEDev.set(x, new NetEDev());
+                }
+                this.netEDev.get(x).clone(n.netEDev.get(x));
+            }   
+            this.netNfs.clone(n.netNfs);
+            this.netNfsd.clone(n.netNfsd);
+            this.netSock.clone(n.netSock);
+            for (int x = 0; x < n.softnet.size(); x++) {
+                if (this.softnet.size() <= x) {
+                    // should not happen - handle metrics registration in this case!
+                    LOG.warning("Number of CPUs different between cloned object and myself! " +
+                                "Might need to update metrics trackers?");
+                    this.softnet.set(x, new Softnet());
+                }
+                this.softnet.get(x).clone(n.softnet.get(x));
+            }   
+        }
+
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class NetDev {
+            @JsonProperty
+            String iface;
+            @JsonProperty
+            float rxpck;
+            @JsonProperty
+            float txpck;
+            @JsonProperty
+            float rxkB;
+            @JsonProperty
+            float txkB;
+            @JsonProperty
+            float rxcmp;
+            @JsonProperty
+            float txcmp;
+            @JsonProperty
+            float rxmcst;
+            @JsonProperty("ifutil-percent")
+            float ifutilPercent;
+            public String getIface() {
+                return iface;
+            }
+            public void setIface(String iface) {
+                this.iface = iface;
+            }
+            public float getRxpck() {
+                return rxpck;
+            }
+            public void setRxpck(float rxpck) {
+                this.rxpck = rxpck;
+            }
+            public float getTxpck() {
+                return txpck;
+            }
+            public void setTxpck(float txpck) {
+                this.txpck = txpck;
+            }
+            public float getRxkB() {
+                return rxkB;
+            }
+            public void setRxkB(float rxkB) {
+                this.rxkB = rxkB;
+            }
+            public float getTxkB() {
+                return txkB;
+            }
+            public void setTxkB(float txkB) {
+                this.txkB = txkB;
+            }
+            public float getRxcmp() {
+                return rxcmp;
+            }
+            public void setRxcmp(float rxcmp) {
+                this.rxcmp = rxcmp;
+            }
+            public float getTxcmp() {
+                return txcmp;
+            }
+            public void setTxcmp(float txcmp) {
+                this.txcmp = txcmp;
+            }
+            public float getRxmcst() {
+                return rxmcst;
+            }
+            public void setRxmcst(float rxmcst) {
+                this.rxmcst = rxmcst;
+            }
+            public float getIfutilPercent() {
+                return ifutilPercent;
+            }
+            public void setIfutilPercent(float ifutilPercent) {
+                this.ifutilPercent = ifutilPercent;
+            }
+            @Override
+            public String toString() {
+                return "NetDev [iface=" + iface + ", ifutilPercent=" + ifutilPercent + ", rxcmp=" + rxcmp + ", rxkB="
+                        + rxkB + ", rxmcst=" + rxmcst + ", rxpck=" + rxpck + ", txcmp=" + txcmp + ", txkB=" + txkB
+                        + ", txpck=" + txpck + "]";
+            }
+            public void clone(NetDev nd) {
+                this.iface = nd.iface;
+                this.rxpck = nd.rxpck;
+                this.txpck = nd.txpck;
+                this.rxkB = nd.rxkB;
+                this.txkB = nd.txkB;
+                this.rxcmp = nd.rxcmp;
+                this.txcmp = nd.txcmp;
+                this.rxmcst = nd.rxmcst;
+                this.ifutilPercent = nd.ifutilPercent;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class NetEDev {
+            @JsonProperty
+            String iface;
+            @JsonProperty
+            float rxerr;
+            @JsonProperty
+            float txerr;
+            @JsonProperty
+            float coll;
+            @JsonProperty
+            float rxdrop;
+            @JsonProperty
+            float txdrop;
+            @JsonProperty
+            float txcarr;
+            @JsonProperty
+            float rxfram;
+            @JsonProperty
+            float rxfifo;
+            @JsonProperty
+            float txfifo;
+            public String getIface() {
+                return iface;
+            }
+            public void setIface(String iface) {
+                this.iface = iface;
+            }
+            public float getRxerr() {
+                return rxerr;
+            }
+            public void setRxerr(float rxerr) {
+                this.rxerr = rxerr;
+            }
+            public float getTxerr() {
+                return txerr;
+            }
+            public void setTxerr(float txerr) {
+                this.txerr = txerr;
+            }
+            public float getColl() {
+                return coll;
+            }
+            public void setColl(float coll) {
+                this.coll = coll;
+            }
+            public float getRxdrop() {
+                return rxdrop;
+            }
+            public void setRxdrop(float rxdrop) {
+                this.rxdrop = rxdrop;
+            }
+            public float getTxdrop() {
+                return txdrop;
+            }
+            public void setTxdrop(float txdrop) {
+                this.txdrop = txdrop;
+            }
+            public float getTxcarr() {
+                return txcarr;
+            }
+            public void setTxcarr(float txcarr) {
+                this.txcarr = txcarr;
+            }
+            public float getRxfram() {
+                return rxfram;
+            }
+            public void setRxfram(float rxfram) {
+                this.rxfram = rxfram;
+            }
+            public float getRxfifo() {
+                return rxfifo;
+            }
+            public void setRxfifo(float rxfifo) {
+                this.rxfifo = rxfifo;
+            }
+            public float getTxfifo() {
+                return txfifo;
+            }
+            public void setTxfifo(float txfifo) {
+                this.txfifo = txfifo;
+            }
+            @Override
+            public String toString() {
+                return "NetEDev [coll=" + coll + ", iface=" + iface + ", rxdrop=" + rxdrop + ", rxerr=" + rxerr
+                        + ", rxfifo=" + rxfifo + ", rxfram=" + rxfram + ", txcarr=" + txcarr + ", txdrop=" + txdrop
+                        + ", txerr=" + txerr + ", txfifo=" + txfifo + "]";
+            }
+            public void clone(NetEDev ne) {
+                this.iface = ne.iface;
+                this.rxerr = ne.rxerr;
+                this.txerr = ne.txerr;
+                this.coll = ne.coll;
+                this.rxdrop = ne.rxdrop;
+                this.txdrop = ne.txdrop;
+                this.txcarr = ne.txcarr;
+                this.rxfram = ne.rxfram;
+                this.rxfifo = ne.rxfifo;
+                this.txfifo = ne.txfifo;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class NetNfs {
+            @JsonProperty
+            float call;
+            @JsonProperty
+            float retrans;
+            @JsonProperty
+            float read;
+            @JsonProperty
+            float write;
+            @JsonProperty
+            float access;
+            @JsonProperty
+            float getatt;
+            public float getCall() {
+                return call;
+            }
+            public void setCall(float call) {
+                this.call = call;
+            }
+            public float getRetrans() {
+                return retrans;
+            }
+            public void setRetrans(float retrans) {
+                this.retrans = retrans;
+            }
+            public float getRead() {
+                return read;
+            }
+            public void setRead(float read) {
+                this.read = read;
+            }
+            public float getWrite() {
+                return write;
+            }
+            public void setWrite(float write) {
+                this.write = write;
+            }
+            public float getAccess() {
+                return access;
+            }
+            public void setAccess(float access) {
+                this.access = access;
+            }
+            public float getGetatt() {
+                return getatt;
+            }
+            public void setGetatt(float getatt) {
+                this.getatt = getatt;
+            }
+            @Override
+            public String toString() {
+                return "NetNfs [access=" + access + ", call=" + call + ", getatt=" + getatt + ", read=" + read
+                        + ", retrans=" + retrans + ", write=" + write + "]";
+            }
+            public void clone(NetNfs nn) {
+                this.call = nn.call;
+                this.retrans = nn.retrans;
+                this.read = nn.read;
+                this.write = nn.write;
+                this.access = nn.access;
+                this.getatt = nn.getatt;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class NetNfsd {
+            @JsonProperty
+            float scall;
+            @JsonProperty
+            float badcall;
+            @JsonProperty
+            float packet;
+            @JsonProperty
+            float udp;
+            @JsonProperty
+            float tcp;
+            @JsonProperty
+            float hit;
+            @JsonProperty
+            float miss;
+            @JsonProperty
+            float sread;
+            @JsonProperty
+            float swrite;
+            @JsonProperty
+            float saccess;
+            @JsonProperty
+            float sgetatt;
+            public float getScall() {
+                return scall;
+            }
+            public void setScall(float scall) {
+                this.scall = scall;
+            }
+            public float getBadcall() {
+                return badcall;
+            }
+            public void setBadcall(float badcall) {
+                this.badcall = badcall;
+            }
+            public float getPacket() {
+                return packet;
+            }
+            public void setPacket(float packet) {
+                this.packet = packet;
+            }
+            public float getUdp() {
+                return udp;
+            }
+            public void setUdp(float udp) {
+                this.udp = udp;
+            }
+            public float getTcp() {
+                return tcp;
+            }
+            public void setTcp(float tcp) {
+                this.tcp = tcp;
+            }
+            public float getHit() {
+                return hit;
+            }
+            public void setHit(float hit) {
+                this.hit = hit;
+            }
+            public float getMiss() {
+                return miss;
+            }
+            public void setMiss(float miss) {
+                this.miss = miss;
+            }
+            public float getSread() {
+                return sread;
+            }
+            public void setSread(float sread) {
+                this.sread = sread;
+            }
+            public float getSwrite() {
+                return swrite;
+            }
+            public void setSwrite(float swrite) {
+                this.swrite = swrite;
+            }
+            public float getSaccess() {
+                return saccess;
+            }
+            public void setSaccess(float saccess) {
+                this.saccess = saccess;
+            }
+            public float getSgetatt() {
+                return sgetatt;
+            }
+            public void setSgetatt(float sgetatt) {
+                this.sgetatt = sgetatt;
+            }
+            @Override
+            public String toString() {
+                return "NetNfsd [badcall=" + badcall + ", hit=" + hit + ", miss=" + miss + ", packet=" + packet
+                        + ", saccess=" + saccess + ", scall=" + scall + ", sgetatt=" + sgetatt + ", sread=" + sread
+                        + ", swrite=" + swrite + ", tcp=" + tcp + ", udp=" + udp + "]";
+            }
+            public void clone(NetNfsd nnd) {
+                this.scall = nnd.scall;
+                this.badcall = nnd.badcall;
+                this.packet = nnd.packet;
+                this.udp = nnd.udp;
+                this.tcp = nnd.tcp;
+                this.hit = nnd.hit;
+                this.miss = nnd.miss;
+                this.sread = nnd.sread;
+                this.swrite = nnd.swrite;
+                this.saccess = nnd.saccess;
+                this.sgetatt = nnd.sgetatt;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class NetSock {
+            @JsonProperty
+            int totsck;
+            @JsonProperty
+            int tcpsck;
+            @JsonProperty
+            int udpsck;
+            @JsonProperty
+            int rawsck;
+            @JsonProperty("ip-frag")
+            int ipFrag;
+            @JsonProperty("tcp-tw")
+            int tcpTw;
+            public int getTotsck() {
+                return totsck;
+            }
+            public void setTotsck(int totsck) {
+                this.totsck = totsck;
+            }
+            public int getTcpsck() {
+                return tcpsck;
+            }
+            public void setTcpsck(int tcpsck) {
+                this.tcpsck = tcpsck;
+            }
+            public int getUdpsck() {
+                return udpsck;
+            }
+            public void setUdpsck(int udpsck) {
+                this.udpsck = udpsck;
+            }
+            public int getRawsck() {
+                return rawsck;
+            }
+            public void setRawsck(int rawsck) {
+                this.rawsck = rawsck;
+            }
+            public int getIpFrag() {
+                return ipFrag;
+            }
+            public void setIpFrag(int ipFrag) {
+                this.ipFrag = ipFrag;
+            }
+            public int getTcpTw() {
+                return tcpTw;
+            }
+            public void setTcpTw(int tcpTw) {
+                this.tcpTw = tcpTw;
+            }
+            @Override
+            public String toString() {
+                return "NetSock [ipFrag=" + ipFrag + ", rawsck=" + rawsck + ", tcpTw=" + tcpTw + ", tcpsck=" + tcpsck
+                        + ", totsck=" + totsck + ", udpsck=" + udpsck + "]";
+            }
+            public void clone(NetSock ns) {
+                this.totsck = ns.totsck;
+                this.tcpsck = ns.tcpsck;
+                this.udpsck = ns.udpsck;
+                this.rawsck = ns.rawsck;
+                this.ipFrag = ns.ipFrag;
+                this.tcpTw = ns.tcpTw;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class Softnet {
+            @JsonProperty
+            String cpu;
+            @JsonProperty
+            float total;
+            @JsonProperty
+            float dropd;
+            @JsonProperty
+            float squeezd;
+            @JsonProperty("rx_rps")
+            float rxRps;
+            @JsonProperty("flw_lim")
+            float flwLim;
+            public String getCpu() {
+                return cpu;
+            }
+            public void setCpu(String cpu) {
+                this.cpu = cpu;
+            }
+            public float getTotal() {
+                return total;
+            }
+            public void setTotal(float total) {
+                this.total = total;
+            }
+            public float getDropd() {
+                return dropd;
+            }
+            public void setDropd(float dropd) {
+                this.dropd = dropd;
+            }
+            public float getSqueezd() {
+                return squeezd;
+            }
+            public void setSqueezd(float squeezd) {
+                this.squeezd = squeezd;
+            }
+            public float getRxRps() {
+                return rxRps;
+            }
+            public void setRxRps(float rxRps) {
+                this.rxRps = rxRps;
+            }
+            public float getFlwLim() {
+                return flwLim;
+            }
+            public void setFlwLim(float flwLim) {
+                this.flwLim = flwLim;
+            }
+            @Override
+            public String toString() {
+                return "Softnet [cpu=" + cpu + ", dropd=" + dropd + ", flwLim=" + flwLim + ", rxRps=" + rxRps
+                        + ", squeezd=" + squeezd + ", total=" + total + "]";
+            }
+            public void clone(Softnet s) {
+                this.cpu = s.cpu;
+                this.total = s.total;
+                this.dropd = s.dropd;
+                this.squeezd = s.squeezd;
+                this.rxRps = s.rxRps;
+                this.flwLim = s.flwLim;
+            }
+        }
+    }
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Psi {
+        @JsonProperty("psi-cpu")
+        PsiCpu psiCpu;
+        @JsonProperty("psi-io")
+        PsiIoAndMem psiIo;
+        @JsonProperty("psi-mem")
+        PsiIoAndMem psiMem;
+        public PsiCpu getPsiCpu() {
+            return psiCpu;
+        }
+        public void setPsiCpu(PsiCpu psiCpu) {
+            this.psiCpu = psiCpu;
+        }
+        public PsiIoAndMem getPsiIo() {
+            return psiIo;
+        }
+        public void setPsiIo(PsiIoAndMem psiIo) {
+            this.psiIo = psiIo;
+        }
+        public PsiIoAndMem getPsiMem() {
+            return psiMem;
+        }
+        public void setPsiMem(PsiIoAndMem psiMem) {
+            this.psiMem = psiMem;
+        }
+        @Override
+        public String toString() {
+            return "Psi [psiCpu=" + psiCpu + ", psiIo=" + psiIo + ", psiMem=" + psiMem + "]";
+        }
+        public void clone(Psi p) {
+            this.psiCpu.clone(p.psiCpu);
+            this.psiIo.clone(p.psiIo);
+            this.psiMem.clone(p.psiMem);
+        }
+
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class PsiCpu {
+            @JsonProperty("some_avg10")
+            float someAvg10;
+            @JsonProperty("some_avg60")
+            float someAvg60;
+            @JsonProperty("some_avg300")
+            float someAvg300;
+            @JsonProperty("some_avg")
+            float someAvg;
+            public float getSomeAvg10() {
+                return someAvg10;
+            }
+            public void setSomeAvg10(float someAvg10) {
+                this.someAvg10 = someAvg10;
+            }
+            public float getSomeAvg60() {
+                return someAvg60;
+            }
+            public void setSomeAvg60(float someAvg60) {
+                this.someAvg60 = someAvg60;
+            }
+            public float getSomeAvg300() {
+                return someAvg300;
+            }
+            public void setSomeAvg300(float someAvg300) {
+                this.someAvg300 = someAvg300;
+            }
+            public float getSomeAvg() {
+                return someAvg;
+            }
+            public void setSomeAvg(float someAvg) {
+                this.someAvg = someAvg;
+            }
+            @Override
+            public String toString() {
+                return "PsiCpu [someAvg=" + someAvg + ", someAvg10=" + someAvg10 + ", someAvg300=" + someAvg300
+                        + ", someAvg60=" + someAvg60 + "]";
+            }
+            public void clone(PsiCpu p) {
+                this.someAvg10 = p.someAvg10;
+                this.someAvg60 = p.someAvg60;
+                this.someAvg300 = p.someAvg300;
+                this.someAvg = p.someAvg;
+            }
+        }
+        @JsonIgnoreProperties(ignoreUnknown = true)
+        public static class PsiIoAndMem extends PsiCpu {
+            @JsonProperty("full_avg10")
+            float fullAvg10;
+            @JsonProperty("full_avg60")
+            float fullAvg60;
+            @JsonProperty("full_avg300")
+            float fullAvg300;
+            @JsonProperty("full_avg")
+            float fullAvg;
+            public float getFullAvg10() {
+                return fullAvg10;
+            }
+            public void setFullAvg10(float fullAvg10) {
+                this.fullAvg10 = fullAvg10;
+            }
+            public float getFullAvg60() {
+                return fullAvg60;
+            }
+            public void setFullAvg60(float fullAvg60) {
+                this.fullAvg60 = fullAvg60;
+            }
+            public float getFullAvg300() {
+                return fullAvg300;
+            }
+            public void setFullAvg300(float fullAvg300) {
+                this.fullAvg300 = fullAvg300;
+            }
+            public float getFullAvg() {
+                return fullAvg;
+            }
+            public void setFullAvg(float fullAvg) {
+                this.fullAvg = fullAvg;
+            }
+            @Override
+            public String toString() {
+                return "PsiIoAndMem ["
+                        + "someAvg=" + someAvg + ", someAvg10=" + someAvg10
+                        + ", someAvg300=" + someAvg300 + ", someAvg60=" + someAvg60
+                        + ", fullAvg=" + fullAvg + ", fullAvg10=" + fullAvg10
+                        + ", fullAvg300=" + fullAvg300 + ", fullAvg60=" + fullAvg60 + "]";
+            }
+            public void clone(PsiIoAndMem p) {
+                super.clone(p);
+                this.fullAvg10 = p.fullAvg10;
+                this.fullAvg60 = p.fullAvg60;
+                this.fullAvg300 = p.fullAvg300;
+                this.fullAvg = p.fullAvg;
+            }
+        }
+    }
+}

+ 24 - 0
exporter/src/main/java/net/p0f/openshift/metrics/processor/PsacctToCsv.java

@@ -0,0 +1,24 @@
+package net.p0f.openshift.metrics.processor;
+
+import java.util.StringTokenizer;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+
+public class PsacctToCsv implements Processor {
+    @Override
+    public void process(Exchange exchange) throws Exception {
+        exchange.getMessage().setBody(
+            exchange.getIn().getBody(String.class)
+                    .replaceAll(" +", ",")
+                    .replaceFirst("^,", "")
+                    .replaceFirst(",$", ",ANONYMOUS")
+                    .replaceAll("(re|u|s|min|maj|swp),", ",")
+        );
+    }
+
+    public static boolean isRecordValid(String body) {
+        StringTokenizer st = new StringTokenizer(body, ",");
+        return st.countTokens() == 8;
+    }
+}

+ 43 - 0
exporter/src/main/java/net/p0f/openshift/metrics/routes/PsacctConsumer.java

@@ -0,0 +1,43 @@
+package net.p0f.openshift.metrics.routes;
+
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.dataformat.bindy.csv.BindyCsvDataFormat;
+import org.apache.camel.spi.DataFormat;
+
+import net.p0f.openshift.metrics.model.ProcessAccountingRecord;
+import net.p0f.openshift.metrics.processor.PsacctToCsv;
+
+public class PsacctConsumer extends RouteBuilder {
+
+    @Override
+    public void configure() throws Exception {
+        PsacctToCsv toCsv = new PsacctToCsv();
+        DataFormat fromCsv = new BindyCsvDataFormat(ProcessAccountingRecord.class);
+        from("file:/metrics?" +
+                "fileName=psacct-dump-all&" +
+                "readLock=changed&" +
+                "readLockCheckInterval=250&" +
+                "move=done/${date:now:yyyyMMdd}/psacct-${date:now:yyyyMMdd-HHmmss}")
+            .routeId("psacct-reader")
+            .log(LoggingLevel.TRACE, "Original Psacct Payload: ${body}")
+            // Reset current gauge status upon receiving a new message.
+            .wireTap("direct:resetPsacct")
+            // Split, unmarshall, and account for, all new records.
+            .split().tokenize("\n").parallelProcessing()
+            .log(LoggingLevel.TRACE, "Split Psacct Record: ${body}")
+            // TODO: See into error-handler ways of avoiding this choice.
+            .choice()
+                .when(bodyAs(String.class).isEqualTo(""))
+                    .log(LoggingLevel.DEBUG, "Skipping empty record.")
+                .when(method(PsacctToCsv.class, "isRecordValid").not())
+                    .log(LoggingLevel.WARN, "Illegal record: ${body}")
+                .otherwise()
+                    .process(toCsv)
+                    .log(LoggingLevel.TRACE, "Transformed Psacct CSV: ${body}")
+                    .unmarshal(fromCsv)
+                    .log(LoggingLevel.DEBUG, "Unmarshaled Psacct: ${body}")
+                    .to("seda:psacct")
+            .endChoice();
+    }
+}

+ 15 - 0
exporter/src/main/java/net/p0f/openshift/metrics/routes/RegisterProcessAccounting.java

@@ -0,0 +1,15 @@
+package net.p0f.openshift.metrics.routes;
+
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+
+public class RegisterProcessAccounting extends RouteBuilder {
+
+    @Override
+    public void configure() throws Exception {
+        from("seda:psacct")
+            .routeId("psacct-dispatch")
+            .log(LoggingLevel.DEBUG, "Sending ${body.processName}@${body.hostName} for processing.")
+            .to("bean:processAccountingMetrics?method=registerRecord&scope=Request");
+    }
+}

+ 14 - 0
exporter/src/main/java/net/p0f/openshift/metrics/routes/ResetProcessAccounting.java

@@ -0,0 +1,14 @@
+package net.p0f.openshift.metrics.routes;
+
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+
+public class ResetProcessAccounting extends RouteBuilder {
+    @Override
+    public void configure() throws Exception {
+        from("direct:resetPsacct")
+            .routeId("psacct-reset")
+            .log(LoggingLevel.DEBUG, "Resetting psacct gauges for new snapshot.")
+            .to("bean:processAccountingMetrics?method=resetGauges&scope=Request");
+    }
+}

+ 41 - 0
exporter/src/main/java/net/p0f/openshift/metrics/routes/SysstatConsumer.java

@@ -0,0 +1,41 @@
+package net.p0f.openshift.metrics.routes;
+
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.jackson.JacksonDataFormat;
+
+import net.p0f.openshift.metrics.model.SysstatMeasurement;
+
+public class SysstatConsumer extends RouteBuilder {
+
+    @Override
+    public void configure() throws Exception {
+        from("file:/metrics?" +
+                "fileName=sysstat-dump.json&" +
+                "readLock=changed&" +
+                "readLockCheckInterval=250&" +
+                "move=done/${date:now:yyyyMMdd}/sysstat-${date:now:yyyyMMdd-HHmmss}")
+            .routeId("sysstat-reader")
+            .log(LoggingLevel.TRACE, "Original Sysstat Payload: ${body}")
+            /*
+             * Need to transform:
+             * - sysstat.hosts[0].nodename -> sysstat.hosts[0].statistics[0].hostname
+             * - sysstat.hosts[0].number-of-cpus -> sysstat.hosts[0].statistics[0].num-cpus
+             * 
+             * Need to drop:
+             * - everything outside statistics[0]
+             * 
+             * See:
+             *   https://github.com/bazaarvoice/jolt
+             *   https://github.com/apache/camel/blob/main/components/camel-jolt/src/test/resources/org/apache/camel/component/jolt/firstSample/spec.json
+             */
+            .to("jolt:net/p0f/openshift/metrics/routes/transformSysstat.json" +
+                                    "?inputType=JsonString" +
+                                    "&outputType=JsonString" +
+                                    "&transformDsl=Chainr")
+            .log(LoggingLevel.TRACE, "Transformed Sysstat Json: ${body}")
+            .unmarshal(new JacksonDataFormat(SysstatMeasurement.class))
+            .log(LoggingLevel.DEBUG, "Unmarshaled Sysstat: ${body}")
+            .to("bean:sysstatMetrics?method=processMetricRecord&scope=Request");
+    }
+}

+ 213 - 0
exporter/src/main/resources/META-INF/resources/index.html

@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>metrics-exporter - 1.0.0-SNAPSHOT</title>
+        <style>
+            body {
+                margin: 0;
+                font-family: Roboto, "Helvetica Neue", Arial, sans-serif;
+            }
+            code {
+                font-family: "Courier New", monospace;
+                color: #e83e8c;
+                word-break: break-word;
+            }
+            h1, h2, h3, h4, h5, h6 {
+                margin-bottom: 0.5rem;
+                font-weight: 400;
+                line-height: 1.5;
+            }
+            h1 { font-size: 2.25rem; }
+            h2 { font-size: 1.6rem }
+            h3 { font-size: 1.5rem }
+            h4 { font-size: 1.4rem }
+            h5 { font-size: 1.25rem }
+            h6 { font-size: 1rem }
+            p {
+                padding: .15rem 0;
+                margin: 0;
+                line-height: 1.3rem;
+            }
+            a {
+                text-decoration: underline;
+                color: #1259A5;
+            }
+            a:hover, a:active { color: #c00; }
+            .grid {
+                display: grid;
+                grid-template-columns: repeat(12, 1fr);
+                clear: both;
+            }
+            .header-wrapper {
+                padding: 0 13rem;
+                background: black;
+            }
+            .content-wrapper {
+                padding: 0 13rem;
+                background: white;
+            }
+            .header {
+                background-position: 0 1rem;
+                min-height: 60px;
+                grid-column: span 12;
+            }
+            #quarkus-logo-svg {
+                width: 200px;
+                height: 30px;
+                padding-top: 1rem;
+            }
+            .banner {
+                grid-column: span 12;
+                padding: 2rem 0;
+            }
+            .callout {
+                color: white;
+                font-size: 3.5rem;
+                margin: 0;
+                padding-top: 5rem;
+                grid-column: span 8;
+            }
+            .app-details {
+                margin: 1rem 0;
+                border: 1px solid lightgrey;
+                padding: 1rem;
+                background: white;
+                font-size: .75rem;
+                font-weight: 300;
+                grid-column: span 4;
+            }
+            .app-details code { font-size: 87.5%; }
+            .app-details ul {
+                list-style-type: none;
+                padding: 0;
+                margin: 0;
+            }
+            .app-details ul li { margin-bottom: 0.2rem; }
+            .app-details h5 {
+                margin: 0;
+                padding: 0;
+            }
+            .left-column {
+                grid-column: span 8;
+                padding-right: 3rem;
+            }
+            .left-column p {
+                padding: .15rem 0;
+                margin: 0;
+                font-weight: 300;
+            }
+            .left-column ul {
+                margin: 0;
+                padding: 10px 0 0 25px;
+                list-style-type: "\1F680";
+                font-weight: 300;
+            }
+            .left-column li { padding-left: 10px; }
+            .left-column .cta-button {
+                margin: 20px 0;
+                display: inline-block;
+                padding: 1rem 2rem;
+                background-color: #4695EB;
+                font-weight: 600;
+                color: white;
+                text-decoration: none;
+                text-transform: uppercase;
+            }
+            .right-column {
+                grid-column: span 4;
+                margin-top: 1rem;
+                font-weight: 200;
+            }
+            .right-column h5 {
+                padding: 1rem 0 .25rem 0;
+                margin: 0;
+                font-weight: 300;
+            }
+            .right-column h3 { font-weight: 400;}
+            .right-column ul {
+                margin: 0;
+                list-style-type: circle;
+            }
+            .right-column li {
+                padding: .5rem 0 0 0;
+            }
+            .provided-code {
+                line-height: 20px;
+            }
+            .provided-code b {
+                font-weight: 400;
+                margin-bottom: 15px;
+            }
+            @media screen and (max-width: 1023px) {
+                .header-wrapper {
+                    padding: 0 1rem;
+                    background: black;
+                }
+                .banner-wrapper { padding: 0 1rem; }
+                .content-wrapper {
+                    padding: 0 1rem;
+                    background: white;
+                }
+                .left-column {
+                    grid-column: span 12;
+                }
+                .right-column {
+                    grid-column: span 12;
+                    margin: 3rem 0;
+                }
+                .callout {
+                    color: white;
+                    font-size: 3.5rem;
+                    margin: 0;
+                    padding: 1rem 0;
+                    grid-column: span 12;
+                }
+                .app-details {
+                    margin: 1rem 0;
+                    border: 1px solid lightgrey;
+                    padding: 1rem;
+                    background: white;
+                    font-size: 1rem;
+                    font-weight: 300;
+                    grid-column: span 12;
+                }
+            }
+        </style>
+    </head>
+    <body>
+        <div class="banner-wrapper">
+            <div class="grid">
+                <div class="banner">
+                    <div class="grid">
+                        <div class="app-details">
+                            <h5>Application</h5>
+                            <ul>
+                                <li>GroupId: <code>net.p0f.openshift</code></li>
+                                <li>ArtifactId: <code>metrics-exporter</code></li>
+                                <li>Version: <code>1.0.0-SNAPSHOT</code></li>
+                                <li>Quarkus Version: <code>2.7.6.Final-redhat-00006</code></li>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="content-wrapper">
+            <div class="grid">
+                <div class="left-column">
+                    <p>This page: <code>/index.html</code></p>
+                    <p>Version endpoint: <code>/metrics/version</code></p>
+                    <p>Exposed metrics: <code>/q/metrics</code></p>
+                </div>
+                <div class="right-column">
+                    <h5><a href="https://github.com/benko/linux-metrics-exporter/">Project Page on GitHub</a></h5>
+                    <h5>Pre-Built Container Images</h5>
+                    <p><a href="https://quay.io/benko/ocp-collector-sysstat">Sysstat Collector</a></p>
+                    <p><a href="https://quay.io/benko/ocp-collector-psacct">Psacct Collector</a></p>
+                    <p><a href="https://quay.io/benko/ocp-metrics-exporter">Metrics Exporter</a></p>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>

+ 10 - 0
exporter/src/main/resources/application.properties

@@ -0,0 +1,10 @@
+quarkus.http.host = 0.0.0.0
+
+quarkus.log.level = INFO
+quarkus.log.category."psacct-reader".level = DEBUG
+quarkus.log.category."psacct-dispatch".level = DEBUG
+quarkus.log.category."psacct-reset".level = DEBUG
+quarkus.log.category."sysstat-reader".level = DEBUG
+quarkus.log.category."net.p0f.openshift.metrics".level = DEBUG
+
+quarkus.micrometer.export.json.enabled = true

+ 21 - 0
exporter/src/main/resources/net/p0f/openshift/metrics/routes/transformSysstat.json

@@ -0,0 +1,21 @@
+[
+    {
+        "operation": "shift",
+        "spec": {
+            "sysstat": {
+                "hosts": {
+                    "0": {
+                        "nodename": "hostname",
+                        "number-of-cpus": "num-cpus",
+                        "statistics": {
+                            "0": {
+                                "*": "&0"
+                            }
+                        }
+                    }   
+                }
+            }
+        
+        }
+    }
+]