More work on packages, plugins and tests.

This commit is contained in:
Mark Nellemann 2021-05-11 15:37:23 +02:00
parent 088d49c90c
commit bc43d687a0
22 changed files with 330 additions and 58 deletions

View File

@ -1,15 +1,14 @@
plugins {
id 'application'
id "com.github.johnrengelman.shadow" version "6.1.0"
id "com.github.johnrengelman.shadow" version "7.0.0"
id "net.nemerosa.versioning" version "2.14.0"
id "nebula.ospackage" version "8.4.1"
id "nebula.ospackage" version "8.5.6"
}
dependencies {
testImplementation project(':shared')
implementation project(':shared')
implementation project(':plugins')
annotationProcessor(group: 'org.pf4j', name: 'pf4j', version: "${pf4jVersion}")
implementation group: 'org.pf4j', name: 'pf4j', version: "${pf4jVersion}"
@ -48,7 +47,7 @@ ospackage {
user = 'root'
packager = "Mark Nellemann <mark.nellemann@gmail.com>"
into '/opt/sysmon-client'
into '/opt/sysmon/client'
from(shadowJar.outputs.files) {
into 'lib'
@ -90,3 +89,10 @@ jar {
)
}
}
shadowJar {
archiveBaseName.set('sysmon-client')
archiveClassifier.set('')
archiveVersion.set('')
mergeServiceFiles() // Tell plugin to merge duplicate service files
}

View File

@ -8,10 +8,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Properties;
import java.util.concurrent.Callable;
@CommandLine.Command(name = "sysmon-client", mixinStandardHelpOptions = true)
@ -22,9 +24,11 @@ public class Application implements Callable<Integer> {
@CommandLine.Option(names = { "-s", "--server-url" }, description = "Server URL (default: ${DEFAULT-VALUE}).", defaultValue = "http://127.0.0.1:9925/metrics", paramLabel = "<url>")
private URL serverUrl;
@CommandLine.Option(names = { "-n", "--hostname" }, description = "Client hostname.", paramLabel = "<name>")
@CommandLine.Option(names = { "-n", "--hostname" }, description = "Client hostname (default: <hostname>).", paramLabel = "<name>")
private String hostname;
@CommandLine.Option(names = { "-p", "--plugins" }, description = "Plugin jar path (default: ${DEFAULT-VALUE}).", paramLabel = "<path>", defaultValue = "/opt/sysmon/plugins")
private File plugins;
public static void main(String... args) {
int exitCode = new CommandLine(new Application()).execute(args);

View File

@ -11,7 +11,11 @@ import org.slf4j.LoggerFactory;
import org.sysmon.shared.MetricExtension;
import org.sysmon.shared.MetricResult;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class ClientRouteBuilder extends RouteBuilder {
@ -22,17 +26,29 @@ public class ClientRouteBuilder extends RouteBuilder {
Registry registry = getContext().getRegistry();
PluginManager pluginManager = new JarPluginManager();
Path[] pluginpaths = { new File("/opt/sysmon/plugins").toPath() };
PluginManager pluginManager = new JarPluginManager(pluginpaths);
pluginManager.loadPlugins();
pluginManager.startPlugins();
List<String> providers = new ArrayList<>();
List<MetricExtension> metricExtensions = pluginManager.getExtensions(MetricExtension.class);
for (MetricExtension ext : metricExtensions) {
if(ext.isSupported()) {
String provides = ext.getProvides();
if(providers.contains(provides)) {
log.warn("Skipping extension (already provided): " + ext.getName());
continue;
}
log.info(">>> Enabling extension: " + ext.getDescription());
providers.add(provides);
// Setup Camel route for this extension
from("timer:collect?period=30000")
from("timer:collect?fixedRate=true&period=30s")
.bean(ext, "getMetrics")
//.doTry()
.process(new MetricEnrichProcessor(registry))

View File

@ -1,4 +1,7 @@
group=org.sysmon
version=0.0.1-SNAPSHOT
pf4jVersion=3.6.0
slf4jVersion=1.7.30
camelVersion=3.7.3
picocliVersion=4.6.1
camelVersion=3.7.4
picocliVersion=4.6.1

View File

@ -1,3 +1,7 @@
plugins {
id "nebula.ospackage" version "8.5.6"
}
subprojects {
apply plugin: 'java'
apply plugin: 'groovy'
@ -49,4 +53,29 @@ task customCleanUp(type:Delete) {
delete "output"
}
tasks.clean.dependsOn(tasks.customCleanUp)
tasks.clean.dependsOn(tasks.customCleanUp)
apply plugin: 'nebula.ospackage'
ospackage {
packageName = 'sysmon-plugins'
release = '1'
user = 'root'
packager = "Mark Nellemann <mark.nellemann@gmail.com>"
into '/opt/sysmon/plugins'
from('output/') {
into ''
}
}
buildRpm {
dependsOn assemble
os = "LINUX"
}
buildDeb {
dependsOn assemble
}

View File

@ -17,6 +17,11 @@ public class AixDiskExtension implements MetricExtension {
return "aix-disk";
}
@Override
public String getProvides() {
return "disk";
}
@Override
public String getDescription() {
return "AIX Disk Metrics (TODO)";

View File

@ -8,7 +8,7 @@ import org.sysmon.shared.MetricExtension;
import org.sysmon.shared.MetricResult;
import org.sysmon.shared.PluginHelper;
import java.util.HashMap;
import java.io.File;
import java.util.List;
import java.util.Map;
@ -19,7 +19,19 @@ public class AixProcessorExtension implements MetricExtension {
@Override
public boolean isSupported() {
return System.getProperty("os.name").toLowerCase().contains("aix");
String osArch = System.getProperty("os.arch").toLowerCase();
if(!osArch.startsWith("ppc64")) {
log.warn("Wrong os arch: " + osArch);
return false;
}
if(!PluginHelper.canExecute("lparstat")) {
log.warn("No lparstat command found.");
return false;
}
return true;
}
@Override
@ -27,6 +39,11 @@ public class AixProcessorExtension implements MetricExtension {
return "aix-processor";
}
@Override
public String getProvides() {
return "processor";
}
@Override
public String getDescription() {
return "AIX Processor Metrics";
@ -35,7 +52,7 @@ public class AixProcessorExtension implements MetricExtension {
@Override
public MetricResult getMetrics() {
List<String> vmstat = PluginHelper.executeCommand("/usr/bin/lparstat 1 1");
List<String> vmstat = PluginHelper.executeCommand("lparstat 1 1");
AixProcessorStat processorStat = processCommandOutput(vmstat);
Map<String, String> tagsMap = processorStat.getTags();

View File

@ -13,7 +13,12 @@ public class AixProcessorStat {
private static final Logger log = LoggerFactory.getLogger(AixProcessorStat.class);
private final Pattern pattern = Pattern.compile("^System configuration: type=(\\S+) mode=(\\S+) smt=(\\d+) lcpu=(\\d+) mem=(\\d+)MB psize=(\\d+) ent=(\\d+\\.?\\d*)");
// System configuration: type=Shared mode=Uncapped smt=8 lcpu=8 mem=4096MB psize=19 ent=0.50
private final Pattern patternAix = Pattern.compile("^System configuration: type=(\\S+) mode=(\\S+) smt=(\\d+) lcpu=(\\d+) mem=(\\d+)MB psize=(\\d+) ent=(\\d+\\.?\\d*)");
// type=Shared mode=Uncapped smt=8 lcpu=4 mem=4101120 kB cpus=24 ent=4.00
private final Pattern patternLinux = Pattern.compile("^type=(\\S+) mode=(\\S+) smt=(\\d+) lcpu=(\\d+) mem=(\\d+) kB cpus=(\\d+) ent=(\\d+\\.?\\d*)");
private String type;
private String mode;
@ -31,34 +36,44 @@ public class AixProcessorStat {
private final Float lbusy; // Indicates the percentage of logical processor(s) utilization that occurred while executing at the user and system level.
/*
AixProcessorStat(List<String> lines) {
System configuration: type=Shared mode=Uncapped smt=8 lcpu=8 mem=4096MB psize=19 ent=0.50
Pattern p;
for (String line : lines) {
%user %sys %wait %idle physc %entc lbusy vcsw phint %nsp %utcyc
----- ----- ------ ------ ----- ----- ------ ----- ----- ----- ------
0.1 0.0 0.0 99.9 0.00 0.2 1.9 37441986 316 149 33.06
*/
AixProcessorStat(List<String> vmstatLines) {
for(String line : vmstatLines) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 7) {
type=matcher.group(1);
mode=matcher.group(2);
smt = Integer.parseInt(matcher.group(3));
lcpu = Integer.parseInt(matcher.group(4));
psize = Integer.parseInt(matcher.group(5));
ent = Float.parseFloat(matcher.group(7));
break;
if (line.startsWith("System configuration:")) {
p = patternAix;
Matcher matcher = patternAix.matcher(line);
if (matcher.find() && matcher.groupCount() == 7) {
type = matcher.group(1);
mode = matcher.group(2);
smt = Integer.parseInt(matcher.group(3));
lcpu = Integer.parseInt(matcher.group(4));
psize = Integer.parseInt(matcher.group(5));
ent = Float.parseFloat(matcher.group(7));
}
}
if (line.startsWith("type=")) {
//type=Shared mode=Uncapped smt=8 lcpu=4 mem=4101120 kB cpus=24 ent=4.00
Matcher matcher = patternLinux.matcher(line);
if (matcher.find() && matcher.groupCount() == 7) {
type = matcher.group(1);
mode = matcher.group(2);
smt = Integer.parseInt(matcher.group(3));
lcpu = Integer.parseInt(matcher.group(4));
psize = Integer.parseInt(matcher.group(6));
ent = Float.parseFloat(matcher.group(7));
}
}
}
String vmstat = vmstatLines.get(vmstatLines.size() -1);
String[] splitStr = vmstat.trim().split("\\s+");
if(splitStr.length != 11) {
throw new UnsupportedOperationException("vmstat string error: " + splitStr.length);
String lparstat = lines.get(lines.size() -1);
String[] splitStr = lparstat.trim().split("\\s+");
if(splitStr.length < 9) {
throw new UnsupportedOperationException("lparstat string error: " + lparstat);
}
this.user = Float.parseFloat(splitStr[0]);

View File

@ -4,10 +4,10 @@ import spock.lang.Specification
class AixProcessorTest extends Specification {
void "test lparstat output processing"() {
void "test AIX lparstat output processing"() {
setup:
def testFile = new File(getClass().getResource('/lparstat.txt').toURI())
def testFile = new File(getClass().getResource('/lparstat-aix.txt').toURI())
List<String> lines = testFile.readLines("UTF-8")
when:
@ -24,4 +24,25 @@ class AixProcessorTest extends Specification {
}
void "test Linux lparstat output processing"() {
setup:
def testFile = new File(getClass().getResource('/lparstat-linux.txt').toURI())
List<String> lines = testFile.readLines("UTF-8")
when:
AixProcessorExtension extension = new AixProcessorExtension()
AixProcessorStat stats = extension.processCommandOutput(lines)
then:
stats.getUser() == 0.03f
stats.getSys() == 0.0f
stats.getWait() == 0.0f
stats.getIdle() == 99.97f
stats.getFields().get("ent") == 4.00f
stats.getTags().get("mode") == "Uncapped"
stats.getTags().get("type") == "Shared"
}
}

View File

@ -0,0 +1,7 @@
System Configuration
type=Shared mode=Uncapped smt=8 lcpu=4 mem=4101120 kB cpus=24 ent=4.00
%user %sys %wait %idle physc %entc lbusy vcsw phint
----- ----- ----- ----- ----- ----- ----- ----- -----
0.03 0.00 0.00 99.97 0.000000 0.000000 0.03 445478301 18863

View File

@ -27,7 +27,8 @@ public class LinuxDiskExtension implements MetricExtension {
@Override
public boolean isSupported() {
return System.getProperty("os.name").toLowerCase().contains("linux");
//return System.getProperty("os.name").toLowerCase().contains("linux");
return false; // TODO: Not ready yet.
}
@Override
@ -35,6 +36,11 @@ public class LinuxDiskExtension implements MetricExtension {
return "linux-disk";
}
@Override
public String getProvides() {
return "disk";
}
@Override
public String getDescription() {
return "Linux Disk Metrics";

View File

@ -6,6 +6,8 @@ import org.sysmon.shared.MetricExtension;
import org.sysmon.shared.MetricResult;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
@ -31,6 +33,11 @@ public class LinuxMemoryExtension implements MetricExtension {
return "linux-memory";
}
@Override
public String getProvides() {
return "memory";
}
@Override
public String getDescription() {
return "Linux Memory Metrics";
@ -42,7 +49,7 @@ public class LinuxMemoryExtension implements MetricExtension {
MetricResult result = new MetricResult("memory");
try {
result.setMeasurement(readProcFile());
result.setMeasurement(processProcFile(readProcFile()));
} catch (IOException e) {
e.printStackTrace();
}
@ -51,27 +58,51 @@ public class LinuxMemoryExtension implements MetricExtension {
}
private Measurement readProcFile() throws IOException {
protected List<String> readProcFile() throws IOException {
List<String> allLines = Files.readAllLines(Paths.get("/proc/meminfo"), StandardCharsets.UTF_8);
return allLines;
}
protected Measurement processProcFile(List<String> lines) {
Map<String, String> tagsMap = new HashMap<>();
Map<String, Object> fieldsMap = new HashMap<>();
List<String> allLines = Files.readAllLines(Paths.get("/proc/meminfo"), StandardCharsets.UTF_8);
for (String line : allLines) {
Long total = null;
Long available = null;
for (String line : lines) {
if (line.startsWith("Mem")) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 2) {
String key = matcher.group(1).substring(3).toLowerCase(); // remove "Mem" and lowercase
Object value = matcher.group(2);
fieldsMap.put(key, value);
String value = matcher.group(2);
switch (key) {
case "total":
total = Long.parseLong(value);
fieldsMap.put(key, total);
break;
case "available":
available = Long.parseLong(value);
fieldsMap.put(key, available);
break;
}
}
}
}
if(total != null && available != null) {
BigDecimal usage = BigDecimal.valueOf(((float)(total - available) / total) * 100);
fieldsMap.put("usage", usage.setScale(2, RoundingMode.HALF_EVEN));
}
return new Measurement(tagsMap, fieldsMap);
}
}

View File

@ -35,6 +35,11 @@ public class LinuxProcessorExtension implements MetricExtension {
return "linux-processor";
}
@Override
public String getProvides() {
return "processor";
}
@Override
public String getDescription() {
return "Linux Processor Metrics";

View File

@ -0,0 +1,23 @@
import org.sysmon.plugins.sysmon_linux.LinuxMemoryExtension
import org.sysmon.shared.Measurement
import spock.lang.Specification
class LinuxMemoryTest extends Specification {
void "test proc file processing"() {
setup:
def testFile = new File(getClass().getResource('/meminfo.txt').toURI())
List<String> lines = testFile.readLines("UTF-8")
when:
LinuxMemoryExtension extension = new LinuxMemoryExtension()
Measurement m = extension.processProcFile(lines);
then:
m.getFields().get("total") == 16069616
m.getFields().get("available") == 7968744
m.getFields().get("usage") == 50.41
}
}

View File

@ -0,0 +1,51 @@
MemTotal: 16069616 kB
MemFree: 1587092 kB
MemAvailable: 7968744 kB
Buffers: 463364 kB
Cached: 6808540 kB
SwapCached: 2156 kB
Active: 9179808 kB
Inactive: 4369880 kB
Active(anon): 6366532 kB
Inactive(anon): 761912 kB
Active(file): 2813276 kB
Inactive(file): 3607968 kB
Unevictable: 270200 kB
Mlocked: 48 kB
SwapTotal: 3985404 kB
SwapFree: 3974652 kB
Dirty: 9708 kB
Writeback: 0 kB
AnonPages: 6545944 kB
Mapped: 1948448 kB
Shmem: 852772 kB
KReclaimable: 302640 kB
Slab: 502784 kB
SReclaimable: 302640 kB
SUnreclaim: 200144 kB
KernelStack: 21376 kB
PageTables: 52856 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 12020212 kB
Committed_AS: 14750600 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 42828 kB
VmallocChunk: 0 kB
Percpu: 8320 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 410796 kB
DirectMap2M: 10795008 kB
DirectMap1G: 6291456 kB

View File

@ -1,7 +1,7 @@
plugins {
id 'application'
id "com.github.johnrengelman.shadow" version "6.1.0"
id "com.github.johnrengelman.shadow" version "7.0.0"
id "net.nemerosa.versioning" version "2.14.0"
id "nebula.ospackage" version "8.5.6"
}
@ -39,7 +39,7 @@ ospackage {
user = 'root'
packager = "Mark Nellemann <mark.nellemann@gmail.com>"
into '/opt/sysmon-server'
into '/opt/sysmon/server'
from(shadowJar.outputs.files) {
into 'lib'
@ -69,7 +69,8 @@ buildDeb {
dependsOn startShadowScripts
}
task aixRpm(type: Rpm) {
task buildRpmAix(type: Rpm) {
dependsOn startShadowScripts
os "AIX"
}
@ -83,6 +84,14 @@ jar {
'Build-Version' : versioning.info.tag ?: (versioning.info.branch + "-" + versioning.info.build),
'Build-Revision' : versioning.info.commit,
'Build-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ss.SSSZ").toString(),
'Add-Opens' : 'java.base/java.lang.invoke' // To ignore "Illegal reflective access by retrofit2.Platform" warnings
)
}
}
shadowJar {
archiveBaseName.set('sysmon-server')
archiveClassifier.set('')
archiveVersion.set('')
mergeServiceFiles() // Tell plugin to merge duplicate service files
}

View File

@ -1,12 +1,16 @@
package org.sysmon.server;
import org.apache.camel.CamelContext;
import org.apache.camel.main.Main;
import org.apache.camel.support.DefaultRegistry;
import org.apache.camel.support.SimpleRegistry;
import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
import picocli.CommandLine;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import java.util.concurrent.Callable;
@CommandLine.Command(name = "sysmon-server", mixinStandardHelpOptions = true)
@ -21,8 +25,11 @@ public class Application implements Callable<Integer> {
@CommandLine.Option(names = { "-p", "--influxdb-pass" }, description = "InfluxDB Password (default: ${DEFAULT-VALUE}).", defaultValue = "", paramLabel = "<pass>")
private String influxPass;
@CommandLine.Option(names = { "-s", "--server-port" }, description = "Server port (default: ${DEFAULT-VALUE}).", defaultValue = "9925", paramLabel = "<port>")
private String listenPort;
@CommandLine.Option(names = { "-H", "--server-host" }, description = "Server listening address (default: ${DEFAULT-VALUE}).", paramLabel = "<addr>")
private String listenHost = "0.0.0.0";
@CommandLine.Option(names = { "-P", "--server-port" }, description = "Server listening port (default: ${DEFAULT-VALUE}).", paramLabel = "<port>")
private Integer listenPort = 9925;
public static void main(String... args) {
@ -34,14 +41,19 @@ public class Application implements Callable<Integer> {
@Override
public Integer call() throws IOException {
Properties properties = new Properties();
properties.put("http.host", listenHost);
properties.put("http.port", listenPort);
InfluxDB influxConnectionBean = InfluxDBFactory.connect(influxUrl.toString(), influxUser, influxPass);
Main main = new Main();
main.bind("myInfluxConnection", influxConnectionBean);
main.bind("myListenPort", Integer.parseInt(listenPort));
main.bind("http.host", listenHost);
main.bind("http.port", listenPort);
main.bind("properties", properties);
main.configure().addRoutesBuilder(ServerRouteBuilder.class);
// now keep the application running until the JVM is terminated (ctrl + c or sigterm)
try {
main.run();

View File

@ -5,6 +5,8 @@ import org.apache.camel.model.rest.RestBindingMode;
import org.apache.camel.spi.Registry;
import org.sysmon.shared.MetricResult;
import java.util.Properties;
public class ServerRouteBuilder extends RouteBuilder {
@Override
@ -14,8 +16,8 @@ public class ServerRouteBuilder extends RouteBuilder {
restConfiguration().component("jetty")
.bindingMode(RestBindingMode.auto)
.host("127.0.0.1")
.port((Integer) registry.lookupByName("myListenPort"));
.host(registry.lookupByNameAndType("http.host", String.class))
.port(registry.lookupByNameAndType("http.port", Integer.class));
rest()
.get("/")
@ -39,7 +41,7 @@ public class ServerRouteBuilder extends RouteBuilder {
.log(">>> metric: ${header.hostname} - ${body}")
.doTry()
.process(new MetricResultToPointProcessor())
.to("influxdb://myInfluxConnection?databaseName=sysmon&retentionPolicy=autogen")
.to("influxdb://ref.myInfluxConnection?databaseName=sysmon&retentionPolicy=autogen")
.doCatch(Exception.class)
.log("Error storing metric to InfluxDB: ${exception}")
.end();

View File

@ -27,7 +27,7 @@ camel.main.name = sysmon-server
#camel.main.beanIntrospectionLoggingLevel=INFO
# run in lightweight mode to be tiny as possible
camel.main.lightweight = true
#camel.main.lightweight = true
# and eager load classes
#camel.main.eager-classloading = true

View File

@ -7,6 +7,7 @@ public interface MetricExtension extends ExtensionPoint {
boolean isSupported();
String getName();
String getProvides();
String getDescription();
MetricResult getMetrics();

View File

@ -7,8 +7,12 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class PluginHelper {
@ -58,5 +62,10 @@ public class PluginHelper {
}
public static boolean canExecute(String cmd) {
return Stream.of(System.getenv("PATH").split(Pattern.quote(File.pathSeparator)))
.map(Paths::get)
.anyMatch(path -> Files.exists(path.resolve(cmd)));
}
}