TcLog - Logging für TwinCAT

Logging in TwinCAT mit den Bordmitteln ist beschränkt auf die Ausgabe als ADS-Event. Die hier vorgestellte Bibliothek TcLog ermöglicht ein flexibles Logging in das Dateisystem.

Aktuelle Dokumentation auf GitHub! Die in diesem Artikel beschriebene Version der Bibliothek ist veraltet. Die Dokumentation sowie die Bibliothek selbst werden auf GitHub gepflegt.

Logging in TwinCAT

Von Zeit zu Zeit kommt es vor, dass ich eine Log-Funktion in TwinCAT benötige, um sporadische Fehler zu finden oder Benutzerinteraktionen aufzuzeichnen. TwinCAT stellt eine Logging-Möglichkeit in der Standardbibliothek zur Verfügung: AdsLogStr. Diese Funktion, die es neben STRING auch für die Datentypen LREAL und DINT gibt, ermöglicht die Ausgabe von ADS-Meldungen als Textbox am Bildschirm und an die ADS-Konsole. Über eine Maske, die dem Baustein übergeben wird, lässt sich konfigurieren, welches Log-Level und welches Ziel (Konsole oder Textbox) die Meldung hat.

Damit sind die Hausmittel von TwinCAT auch schon erschöpft.

Daher habe ich das Open Source Projekt TcLog gestartet. TcLog ist ein Logging-Framework, das über NuGet als Bibliothek in TwinCAT eingebunden werden kann. Es ermöglicht eine flexible Konfiguration der Logs sowie die Angabe verschiedener Log-Targets.

Der Quellcode ist auf GitHub verfügbar und die kompilierte Bibliothek kann hier herunter geladen werden.

TcLog: Flexibles Logging-Framework

Um das Rad nicht neu zu erfinden, orientiert sich TcLog an existierenden Logging-Lösungen wie Serilog. Im Unterschied zu Serilog unterstützt TcLog kein strukturiertes Logging. Alle Logmeldungen werden direkt in Strings umgewandelt.

TcLog stellt einen zentralen, statischen Logger TcLogCore zur Verfügung, der über ein fluent interface konfiguriert werden kann:

VAR
	CoreLogger : TcLogCore;
END_VAR

CoreLogger
	.WriteToAds()
	.MinimumLevel(E_LogLevel.Warning)
	.RunLogger();

Verwendet wird er über einen zweiten Baustein TcLog mit dem die Meldungen dann ausgelöst werden.

VAR
	Logger : TcLog;
END_VAR

Logger.Debug('This is a debug message.');	
Logger.Error('This is an error message.');		

Raising an error message

Die erste Meldung wurde mit Log-Level Debug ausgelöst, die minimale Schwelle wurde aber mit Warning eingestellt, daher kommt nur die zweite Meldung zur Anzeige. `TcLog stellt folgende Meldungsstufen zur Verfügung:

  • E_LogLevel.Debug
  • E_LogLevel.Information
  • E_LogLevel.Warning
  • E_LogLevel.Error
  • E_LogLevel.Fatal

Statische Bindung von TcLog an TcLogCore

Alle im Programm vorkommenden Instanzen von TcLog sind statisch an die eine Instanz von TcLogCore gebunden, die die Konfiguration des Loggers bereitstellt. Diese Instanz muss zyklisch aufgerufen werden.

Dieses Verhalten ist als das Singleton Design Pattern bekannt. Mittlerweile wird es eher kritisch gesehen, da es die Testbarkeit von Software einschränkt. Für das Singleton spricht jedoch, dass es einen geringen Overhead besitzt. Ist die Konfiguration des Loggers in MAIN aufgesetzt kann in jedem weiteren Baustein des SPS-Programm durch TcLog ein Logging ausgelöst werden, wobei auf die Konfiguration des zentralen statischen Loggers zurückgegriffen wird. Da Einfachheit oberstes Ziel dieser Bibliothek ist, überwiegen für diesen Anwendungsfall die Vorteile des Singleton.

Variable Gestaltung des Meldungstextes

TcLog implementiert einen StringBuilder und daher lässt sich der Meldungstext flexibel zusammensetzen:

VAR
	myInt : INT := 10;
	myVarInfo : __SYSTEM.VAR_INFO := __VARINFO(myInt);
END_VAR

Logger
	.AppendString('Let´s log some values: ')
	.AppendAny(myInt)
	.AppendString(' - or some symbols: ')
	.AppendVariable(myVarInfo, myInt)
	.Error('');	

Using a StringBuilder to generate the message text

Somit können beliebig viele Informationen an die Nachricht angehängt werden, ohne dass TcLog mit einer Vielzahl an Eingangsparametern implementiert werden muss, da TwinCAT keine optionalen Eingangsparameter zulässt.

Die Verwendung eines fluent interface bringt noch einen weiteren Vorteil mit sich: zukünftige Änderungen an dem Baustein stellen neuen Funktionalität über neue Methoden bereit, nicht über neue Eingangsparameter. Das bedeutet, dass bestehender Code nicht angepasst werden muss.

Aufrufpfad in die Meldung aufnehmen

TcLog bietet mit .IncludeInstancePath() die Möglichkeit, die Stelle, an der die Meldung ausgelöst wurde mit in den Meldungstext zu integrieren:

CoreLogger
	.WriteToAds()
	.IncludeInstancePath()
	.MinimumLevel(E_LogLevel.Warning)
	.RunLogger();
	
Logger.Error('This is an error message.');

Including the instance path

Bedingte Ausführung des Loggings

Die häufigste Anwendung von Logging wird in der Form IF ... THEN log() END_IF zu finden sein. Daher ist diese Abfrage bereits in TcLog integriert:

VAR
	rTrigLog : R_TRIG;
	bLog : BOOL;
END_VAR

rTrigLog(CLK := bLog);
Logger
	.OnCondition(rTrigLog.Q)
	.Error('Only logs when OnCondition evaluates to TRUE.');	

Logging bei Flankenänderung

Da eine Log-Meldung meist bei einer Statusänderung einmalig abgesetzt werden soll, bringt TcLog auch dafür einen Baustein mit: TcLogTRIG. Im Gegensatz zu TcLog muss für diesen Baustein für jede Verwendung eine eigene Instanz erstellt werden, da intern der Flankenzustand gespeichert wird. Dafür kann obige bedingte Ausführung weiter vereinfacht werden:

VAR
	rTrigLogger : TcLogTRIG;
END_VAR

rTrigLogger
	.OnRisingEdge(bLog)
	.Error('rTrig Test');

Analog kann auf fallende Flanken mit OnFallingEdge(cond) das Logging getriggert werden.

Speichern der Logs im Dateisystem

Die bisher gezeigten Features sind zwar ein flexibler Wrapper für AdsLogStr, rechtfertigen aber alleine noch kein neues Framework. TcLog bringt daher die Möglichkeit mit, Logs im Dateisystem in der Form von Textdateien zu speichern. Diese Option kann TcLogCore über die Methode .WriteToFile(Pfad, Dateiname) aufgetragen werden:

fbCoreLogger
	.IncludeInstancePath()
	.MinimumLevel(E_LogLevel.Warning)
	.WriteToFile('c:\logs\', 'test.txt')
	.RunLogger();
	
rTrigLogger
	.OnRisingEdge(bLog)
	.Error('rTrig Test');

Logging to the file system

Dem Dateinamen wird dabei zusätzlich als Präfix das Erstellungsdatum der Logdatei vorangestellt. Das Format des Datums kann mittels eines Format-String beliebig festgelegt werden. Beispiel:

YYMMDD-hh:mm:ss:iii

Wichtig: Die Groß- und Kleinschreibung muss beibehalten werden, weiterhin müssen gleiche Buchstaben immer hintereinander stehen. Blöcke gleicher Buchstaben sind nicht zulässig: YYMMDD-YYYY

Dieses Format wird TcLogCore über die Methode .TimestampFormat('YYMMDD-hh:mm:ss:iii') übergeben.

Da TwinCAT nur in das lokale Dateisystem schreiben kann, gilt diese Einschränkung für TcLog ebenfalls.

Benutzerdefiniertes Trennzeichen

TcLogCore kann mit .SetDelimiter('|') ein beliebiges Trennzeichen zwischen den Bestandteile des Log-Eintrags mitgeteilt werden.

Rolling interval festlegen

Ein rolling interval bezeichnet das Intervall bis zur Erstellung eines neuen Logfiles. TcLog bietet die Möglichkeit, in regelmäßigen Abständen eine neue Logdatei anzulegen. Dieses rolling interval wird TcLogCore über SetRollingInterval(..) vorgegeben:

  • E_RollingInterval.None: Keine neue Logdatei anlegen
  • E_RollingInterval.Hourly: Jede Stunde eine neue Logdatei anlegen
  • E_RollingInterval.Daily: Täglich eine neue Logdatei anlegen
  • E_RollingInterval.Monthly: Monatlich eine neue Logdatei anlegen

Die Logdatei wird nur dann angelegt, wenn eine Meldung ausgelöst wird.

Alte Logdateien löschen

Um keine Schwemme an veralteten Logs zu erzeugen kann eine Lebensdauer von Logs festgelegt werden. Die Methode DeleteLogsAfterDays(days) von TcLogCore konfiguriert diese. Logdateien, deren Lebenszeit überschritten wurde, werden automatisch um Mitternacht gelöscht.

Benutzerdefinierte Anpassung des Loggings

Verwendung mehrere Logger

Auch wenn der Logger in erster Linie als Singleton konzipiert wurde, ist es möglich, mehrere Logger zu verwenden. Beispielswiese können Sensordaten zyklisch erfasst und in einer separaten Logdatei gespeichert werden. Um einen weiteren Logger hinzuzufügen, muss eine Instanz von TcLogCore erstellt werden. Diese wird dann an die gewünschte TcLog-Instanz gebunden:

VAR
	newLogger: TcLogCore;
	Logger : TcLog;
	myInt : INT := 10;
END_VAR

newLogger
	.MinimumLevel(E_LogLevel.Information)
	.SetRollingInterval(E_RollingInterval.Hourly)
	.WriteToFile('c:\logs\', 'sensor_data.txt')
	.DeleteLogFilesAfterDays(7)
	.RunLogger();
	
Logger.SetLogger(newLogger);

Logger
	.AppendString('Sensor xy: ')
	.AppendAny(myInt)
	.Information('');	

Logger berücksichtigt von nun an die Konfiguration des newLogger.

Eigene Logging-Templates

Möchte man anstelle der Standard-Logs beispielsweise Sensordaten aufzeichnen, ist das möglich. Am einfachsten funktioniert es, indem man um TcLog einen Wrapper programmiert, der das spezifische Template forciert.

Beispiel: Logging von Sensordaten

Nehmen wir an, wir möchten Sensordaten im REAL-Format aufzeichnen. Die Daten sollen in einer csv-Datei gespeichert werden, die folgendes Format besitzt:

hh:mm:ss;Betriebsmittelkennzeichen;Wert;Einheit
10:33:15;+CC1-B31;35.1;°C

Wrapper um TcLog

Als Wrapper verwenden wir einen FB, der TcLog kapselt und mithilfe von der Eingänge die Dateneingabe erzwingt. Weiterhin implementiert er die Schnittstelle ILog, mit der die Verknüpfung zwischen Logger und Basis-Logger hergestellt wird.

FUNCTION_BLOCK UserLog IMPLEMENTS ILog
VAR_INPUT
	condition: BOOL;
	identification: STRING;
	value: REAL;
	unit: STRING;
END_VAR
VAR
	GetTimeData: GenerateTimeData;
	timestamp: STRING;
END_VAR
VAR_STAT
	Logger: TcLog;	
END_VAR

GetTimeData();
timestamp := GetTimeData.ToString('hh:mm:ss');

Logger
	.OnCondition(condition)
	.AppendString(timestamp)
	.AppendString(';')
	.AppendString(identification)
	.AppendString(';')
	.AppendAny(value)
	.AppendString(';')
	.AppendString(unit)
	.ToCustomFormat('');

Hier erweist sich die Hilfsfunktion GenerateTimeData als nützlich, die über die Methode .ToString(Format) das aktuelle Datum und Uhrzeit formatiert zurückgibt. Damit erzeugen wir den Zeitstempel der Sensordaten.

Die Methode .ToCustomFormat('') am Ende der Kette bewirkt, dass die Nachricht unverändert geloggt wird. Keine zusätzlichen Informationen wie weitere Zeitstempel oder Aufrufpfad werden angehängt.

Implementation der Schnittstelle ILog

Die Schnittstelle wird implementiert, indem die Logger-Referenz an die TcLog-Instanz weiter gereicht wird:

METHOD SetLogger : BOOL
VAR_INPUT
	Ref2Core : REFERENCE TO TcLogCore;
END_VAR

Logger.SetLogger(Ref2Core);

Aufruf des Wrappers

Beispielsweise in MAIN wird zyklisch TcLogCore aufgerufen. Gibt es mehr als eine Instanz davon, können wir unserem Logger die gewünschte Instanz über .SetLogger(Instanz) mitteilen. Andernfalls wird die Konfiguration des Logger-Singletons verwendet.

VAR
	newLogger: TcLogCore;
	rTrigLog : R_TRIG;
	bLog : BOOL;
	myLog : UserLog;
	myValue: REAL := 1.0;
	myValue2: REAL := 2.0;
END_VAR

newLogger
	.MinimumLevel(E_LogLevel.Information)
	.SetRollingInterval(E_RollingInterval.Hourly)
	.WriteToFile('c:\logs\', 'sensor.csv')
	.DeleteLogFilesAfterDays(1)
	.RunLogger();
	
myLog.SetLogger(newLogger);
rTrigLog(CLK := bLog);

myLog(
	condition := rTrigLog.Q,
	identification := '+CC1-B31',
	value := myValue,
	unit := '°C');
	
myLog(
	condition := rTrigLog.Q,
	identification := '+CC1-B32',
	value := myValue2,
	unit := '°C');

Sobald über bLog das Logging getriggert wird, werden die csv-Datei und die Einträge darin angelegt:

Custom logging

Verwendung eigener Logger

TcLogCore implementiert die Schnittstelle ILogCore, die die Methoden LogCustomFormat und LogStandardFormat definiert. Ein eigener Logger mit beispielsweise anderen Log-Sinks kann auf zwei Arten erstellt werden:

  1. Neuen FB erstellen, der von TcLogCore erbt. Dabei kann der neue FB um zusätzliche Funktionen erweitert werden und bringt gleichzeitig alle Methoden mit, die TcLogCore besitzt.
  2. Neuen FB erstellen, der die Schnittstelle ILogCore implementiert. Dabei kann der Logger von Grund auf neu geschrieben werden. Die Schnittstelle stellt sicher, dass die im Code vorhandenen Instanzen von TcLog weiterhin verwendet werden können.

Fehlermeldungen

TcCoreLog liefert über die Eigenschaft Error Hinweise auf interne Fehlermeldungen.

VAR
  error: ST_Error;
END_VAR

error := newLogger.Error;

ST_Error

Unit- und Integrationstests

Das Projekt auf Github enthält sowohl Unit (TcUnit) als auch Integrationstests (xUnit).

Weitere Logging-Möglichkeiten in TwinCAT

Mit log4TC existiert eine weitere Logging-Möglichkeit für TwinCAT. Diese ermöglicht strukturiertes Logging, es muss allerdings zusätzliche ein Windows-Service installiert werden, der mit der SPS-Bibliothek kommuniziert. TcLog hingegen kommt als reine SPS-Bibliothek. Der Code zu log4TC wurde als Open Source auf GitHub veröffentlicht.