Logstash-Java-Input-Plug-in für Apache PLC4X

Keine Kommentare

In diesem Artikel beschreiben wir die Entwicklung, zusammen mit den Herausforderungen, des Logstash-Java-Input-Plug-ins für Apache PLC4X, das im Rahmen des Open-Source-Projekts Apache PLC4X in Absprache mit Elastic entwickelt wurde.

Architektonische Übersicht über das Logstash-Java-Input-Plug-in

Zunächst werfen wir einen Blick auf die grundlegende Architektur und Systemlandschaft und werden im Anschluss die Umsetzung des Plug-ins betrachten. In der nachfolgenden Abbildung ist eine Übersicht über das Einsatzgebiet des Plug-ins zu sehen.

Overall architecture Logstash Java Input Plug-in PLC4X

Wir verwenden im Plug-in das Framework Apache PLC4X, um Daten von IIoT-Geräten (Industrial Internet of Things) mittels Pull-Prinzip abzufragen. Diese Daten werden an Logstash übergeben, welches diese anschließend über ein Output-Plug-in der Pipeline mittels Push-Prinzip weiterleitet. Hier haben wir beispielsweise ElasticSearch gewählt.
Neben der Abfrage von mehreren IIoT-Geräten in einer Pipeline ist es ebenfalls möglich, mehrere Pipelines zu definieren, um die Abfragen auf mehrere Pipelines zu verteilen. Dies ist z. B. bei verschiedenen Gerätetypen oder Standorten sinnvoll.

Die Entwicklung des Plug-ins und der zugehörige Build-Prozess (als Teil von Apache PLC4X)

Für die Entwicklung eines Logstash-Java-Input-Plug-ins gibt es von Elastic eine Anleitung, die hier gefunden werden kann.
Dort wird erklärt, dass als erstes der Quelltext von logstash heruntergeladen und gebaut werden muss. Anschließend wird im Projekt des Java-Logstash-Input-Plug-ins der Pfad zu logstash in der Datei gradle.properties eingetragen. Dieser Schritt ist notwendig, solange die entsprechende JAR-Datei von logstash noch nicht auf dem Maven Central Repository verfügbar ist. Aktuell ist noch unklar, wann dies der Fall ist – es existiert aber bereits ein Issue im entsprechenden GitHub-Projekt von Elastic.

Wird das Logstash-Java-Input-Plug-in anschließend gebaut, verwendet das Skript gradle.build die Datei rubyUtils.gradle von logstash. In diesen Utils wird JRuby heruntergeladen und anschließend verwendet. Für das Herunterladen und Entpacken von JRuby wird der Befehl commandLine ‚./mvnw‘, ‚clean‘, ‚install‘, ‚-Pdist‘, ‚-Pcomplete‘ verwendet. Gradle führt Maven an dieser Stelle über einen Maven-Wrapper aus. Dies ist im Rahmen des Apache-PLC4X-Projekts ungünstig. Denn PLC4X verwendet Maven selbst als Build-Tool. Daher ergäbe sich bei einfacher Einbindung von Gradle aus der Elastic-Anleitung folgender Build-Prozess:

Build process with maven and gradle (plc4x project)

Hier ist eine unnötige Verkettung von Build-Tools zu sehen, da Maven Gradle ausführt und dieses im Anschluss wiederum Maven aufruft. Daher haben wir beschlossen, den Gradle-Teil des Build-Prozesses ebenfalls in Maven umzusetzen, sodass sich folgender Ablauf ergibt:

build process with maven (plc4x project)

Abschließend stellen wir noch die Umsetzung des Build-Prozesses mit Maven und den zugehörigen Plug-ins vor.

build process

groovy-maven-plugin: In diesem Schritt wird die Version ohne SNAPSHOT berechnet und als künstliche Eigenschaft in Maven (current-full-version) gesetzt. JRuby benötigt die Version in diesem Format, da es ansonsten mit einer Fehlermeldung abbricht.
download-maven-plugin: Dieses Plug-in lädt und entpackt JRuby, das zum Erstellen des Artefakts benötigt wird.
maven-resources-plugin: Dieses Plug-in bearbeitet alle Ressourcen (z. B. ersetze ${current-full-version} durch die Version).
maven-shade-plugin: Erstellt eine Fat JAR aus den Klassen und Dateien des Plug-ins sowie allen Abhängigkeiten.
exec-maven-plugin: Mit diesem Plug-in werden JRuby und Gem aufgerufen, die anschließend das Artefakt logstash-input-plc4x-X.X.X.gem erstellen.
build-helper-maven-plugin: Mit diesem Plug-in wird das Artefakt in Maven hinzugefügt, sodass es ebenfalls in der install-Phase von Maven berücksichtigt wird.

Beispielkonfiguration des Plug-ins

Voraussetzung für die Verwendung des Plug-ins ist die Installation in Logstash. Anschließend kann das Plug-in wie folgt initialisiert und konfiguriert werden.

## logstash pipeline config - input
input {
	## use plc4x plugin (logstash-input-plc4x)
	plc4x {
		## define sources
		sources => {
			source1 => "opcua:tcp://opcua-server:4840/"
			source2 => "opcua:tcp://opcua-server1:4840/"
			source3 => "..."
		}
		## define jobs
		jobs => {
			job1 => {
				# pull rate in milliseconds 
				rate => 2
				# sources queried by job1
				sources => ["source1", ...]
				# defined queries [logstash_internal_fieldname => "IIoT query"]
				queries =>  {
					PreStage => "ns=2;i=3"
					MidStage => "ns=2;i=4"
					PostStage => "ns=2;i=5"
					Motor => "ns=2;i=6"
					ConvoyerBeltTimestamp => "ns=2;i=7"
					RobotArmTimestamp => "ns=2;i=8"
				}
			}
			job2 => { .... }
		}
	}
}
 
## logstash pipeline config - filter
filter {
	...
}
 
## logstash pipeline config - output
output {
	...
}

Im Abschnitt input wird das Plug-in durch plc4x initialisiert. Innerhalb des plc4x-Abschnitts können sources und jobs definiert werden. Sources sind die Connection-Strings für die jeweiligen PLCs (hier beispielsweise OPC UA Server).
Im jobs-Abschnitt können einzelne jobs definiert werden. Diese enthalten queries (Abfragen) für bestimmte sources. Die Abfragen sind in der Form X => „Y“ angegeben. X ist der interne Feldname für Logstash. Y ist die Abfrage, die zum PLC gesendet wird.
Für jeden Job wird eine Rate angegeben, die die Zeitspanne zwischen den einzelnen Abfragen definiert. Im Beispiel werden die fünf definierten Felder alle 300 Millisekunden von source1 abgefragt.

Quelltext des Java-Logstash-Plug-ins für PLC4X

Das Plug-in besteht aus einer Klasse, die wir für diesen Blogbeitrag in drei Abschnitte aufgeteilt haben.

Im ersten Teil des Plug-ins werden die sources eingelesen und anschließend dem Builder des Scrapers von PLC4X hinzugefügt. Der Scraper ist ein Teil von PLC4X, um Daten regelmäßig von beliebigen PLCs abzufragen.

// parse sources
for (String sourceName : sources.keySet()) {
	Object o = sources.get(sourceName);
	if(o instanceof String) {
		String source = (String)o;
		builder.addSource(sourceName, source);
	} else {
		logger.severe("URL of source " + sourceName + "has the wrong typ!");
	}
}

Der zweite Abschnitt des Quellcodes ist für das Einlesen und Erstellen der jobs zuständig.
In der äußeren for-Schleife wird über alle definierten jobs iteriert. Für jeden einzelnen Job werden anschließend die Rate, die sources als auch die Abfragen eingelesen und dem jobBuilder (ebenfalls Teil des Scrapers) von PLC4X zugewiesen.

// parse jobs
for (String jobName : jobs.keySet()) {
	Object o = jobs.get(jobName);
	if (o instanceof  Map) {
		Map job = (Map<String, Object>) o;
		JobConfigurationTriggeredImplBuilder jobBuilder = builder.job(
			jobName, String.format("(SCHEDULED,%s)", job.get("rate")));
		for (String source : ((List<String>) job.get("sources"))) {
			jobBuilder.source(source);
		}
		Map<String, Object> queries = (Map<String, Object>) job.get("queries");
		for (String queryName : queries.keySet()) {
 
			String fieldAlias = queryName;
			String fieldAddress = (String) queries.get(queryName);
			jobBuilder.field(fieldAlias, fieldAddress);
		}
		jobBuilder.build();
	} else {
		logger.severe("Jobs of wrong Type!");
	}
}

Nachdem nun alle sources und jobs eingelesen sind, können wir den Scraper ausführen. Bei der Initialisierung des Scrapers übergeben wir eine Lambda-Funktion, die nach erfolgreicher Abfrage der Felder aufgerufen wird. In dieser Funktion werden die erhaltenen Werte geloggt und die Ergebnisse anschließend vom consumer akzeptiert (der consumer ist Teil des Logstash-API und nimmt die Daten entgegen).

// start scraper
ScraperConfigurationTriggeredImpl scraperConfig = builder.build();
try {
	plcDriverManager = new PooledPlcDriverManager();
	triggerCollector = new TriggerCollectorImpl(plcDriverManager);
	scraper = new TriggeredScraperImpl(scraperConfig, (jobName, sourceName, results) -> {
		HashMap<String, Object> resultMap = new HashMap<String, Object>();
		resultMap.put("jobName", jobName);
		resultMap.put("sourceName", sourceName);
		resultMap.put("values", results);
 
		//TODO: add guard for debug mode, so only in debug mode its run
		for (Map.Entry<String, Object> result : results.entrySet()) {
			// Get field-name and -value from the results.
			String fieldName = result.getKey();
			Object fieldValue = result.getValue();
			logger.finest("fieldName: " + fieldName);
			logger.finest("fieldValue: " + fieldValue);
		}
		consumer.accept(resultMap);
	}, triggerCollector);
	scraper.start();
	triggerCollector.start();
} catch (ScraperException e) {
	logger.severe("Error starting the scraper: "+ e);
}

Fazit und Lessons Learned

  • Benennung von Dateien und Klassen
    Die Logstash-Java-Input-Plug-ins sind bei der Benennung der Dateien und Konfigurationen sehr sensitiv. Die Namen der Gemspec-, JAR- sowie Gem-Dateien müssen alle derselben Benennung folgen (z. B. logstash-input-plc4x). Ebenfalls müssen der Klassenname und einige Konfigurationen (beispielsweise Inhalte der Ruby-Konfiguration, also der .rb-Dateien) bezüglich der Benennung übereinstimmen. Zu Beginn der Entwicklung hatten wir uns überlegt, die aritfact-id des Projekts zu verwenden. Dabei hatten wir das Problem, dass wir diese nicht überall durch das maven-resource-plugin ersetzen lassen konnten. Dadurch stimmten die Benennungen teilweise nicht überein und es kam zu Problemen bei Installation und Ausführung des Plug-ins. Nachdem wir uns für eine statische Benennung des Plug-ins entschieden und alle relevanten Stellen identifiziert hatten, ließ sich das Plug-in in Logstash installieren und ausführen. Diese Lösung hat auch den Vorteil, dass die artifact-id plc4j-logstash-plugin mit dem Namensschema von PLC4X und der Plug-in Name logstash-input-plc4x mit dem Namensschema für Plug-ins in Logstash übereinstimmt.
  • Installationsdauer des Plug-ins
    Die Installation des Plug-ins in Logstash benötigt meist ~2 Minuten. Dies ist im Fehlerfall (wie z. B. bei der Problematik mit der Benennung) sehr unglücklich, da so zwischen Änderung und Test dieser einige Minuten vergehen.
  • Build-Tools: Maven, Gradle, JRuby, …
    Ein weiteres Learning dieses Projekts besteht darin, dass wir in zukünftigen Projekten die Verwendung mehrerer Build-Tools, genau wie hier, hinterfragen werden. Ansonsten entstehen gegebenenfalls ungünstige Verkettungen von Build-Tools wie oben beschrieben (Maven und Gradle). An dieser Stelle möchten wir uns auch bei Christofer Dutz bedanken, der uns bei dieser Problematik unterstützt hat.

Der Quellcode des gesamten Plug-ins ist hier im PLC4X-Projekt zu finden.

Till Voß

Till Voß ist Werkstudent bei der codecentric AG. Im Rahmen seines Studiums und während seiner (Neben-)Jobs in ganz unterschiedlichen Unternehmen – vom Startup bis zum Großkonzern – hat er in den letzten Jahren mit diversen Technologien gearbeitet: Backend, Frontend, ESB, bahnspezifische Protokolle, Security-Themen etc. Aktuell beschäftigt er sich mit Keycloak, Vue.JS und der Integration von PLC4X als Java-Plugin in Logstash.

Stefan Herrmann

Stefan ist IT Consultant bei der codecentric AG in Frankfurt. Er ist in der Java-Welt Zuhause. Seine Interessen gelten einer gelebten Agilen Praxis und er hat eine Leidenschaft für Information Retrieval und Machine Learning.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.