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.');
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('');
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.');
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');
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 anlegenE_RollingInterval.Hourly
: Jede Stunde eine neue Logdatei anlegenE_RollingInterval.Daily
: Täglich eine neue Logdatei anlegenE_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:
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:
- Neuen FB erstellen, der von
TcLogCore
erbt. Dabei kann der neue FB um zusätzliche Funktionen erweitert werden und bringt gleichzeitig alle Methoden mit, dieTcLogCore
besitzt. - 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 vonTcLog
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;
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.