• Jetzt anmelden. Es dauert nur 2 Minuten und ist kostenlos!

Frage Laufzeit aus Log berechnen

LudwigM

Mitglied
Hallo,
ich habe eine Logdatei, die den ON und OFF Status einer Maschine protokolliert. Beispiel:
Code:
01:04:35  Raum[2] Status:ON
01:06:31  Raum[1] Status:OFF
01:10:40  Raum[2] Status:OFF
01:23:04  Raum[2] Status:ON
01:28:43  Raum[2] Status:OFF
01:34:05  Raum[1] Status:ON
Es gibt dazwischen noch andere Ereignisse in der selben Struktur.
Nun möchte ich ermitteln wie lange ein Raum pro Tag (0-24h) insgesamt den ON Status hatte. Außerdem sollen die Anläufe gezählt werden. Dazu habe ich bisher:
Code:
fetch('13.log')
          .then(response => response.text())
          .then(data => {
            var count = new Array();
            count[0] = 0;
            count[1]=0;
            count[2] =0;
            const lines = data.split('\n');
            for (let line of lines) {
                for(let i=1; i<=3; i++) {
                    if (line.includes('Raum[' + i +'] Status:ON')) {
                        count[i-1]++;
                    }
                }

            }

Dann soll noch die durchschnittliche Zeit im ON Status je Raum ermittelt werden.

Wie geht man am geschicktesten vor?
 

Aaron3219

Senior HTML'ler
Gibt es auch ein Datum zu den jeweiligen Uhrzeiten oder setzt es sich täglich zurück?

Edit: Ich hätte mir deinen Beitrag besser durchlesen sollen...
Nun möchte ich ermitteln wie lange ein Raum pro Tag (0-24h)

Was mich aber noch trotzdem interessieren würde, ist wie groß diese Log-Files werden und ob sie lokal bei dir gespeichert sind oder du über eine URL darauf zugreifen musst.

Wie geht man am geschicktesten vor?
For the sake of Lesbarkeit solltest du wahrscheinlich einen Transform-and-Conquer-Ansatz wählen. Sprich: Du transformierst die Daten erstmal so, dass du damit arbeiten kannst. Also raus aus dem String-Format, hin zu lesbaren Formaten.

Zum Beispiel deinen Zeitstempel in ein ordentliches Date-Format:
Javascript:
new Date(`1970-01-01 ${item.time}`).getTime();

Oder deinen On-Off-Status in einen Boolean (wobei das jetzt nicht zwingend notwendig wäre, aber den Code lesbarer macht):
Javascript:
let statusOn = item.status === "Status:ON";

Damit du so etwas wie item.status oder item.time überhaupt nutzen kannst, musst du wiederum andere Transformationen am String vornehmen.

Auf diese Weise kommst du nach und nach zu einer Datenstruktur, mit der sich ordentlich arbeiten lässt. Ich habe den Code dafür geschrieben, möchte ihn dir aber noch nicht vollständig geben.
Ein kleiner "Anschubser" um dich auf die Wege zu bringen gibt es trotzdem:
Javascript:
(async () => {
  try {
    //const response = await fetch("yourfile.txt");
    //const data = await response.text();

    const data = `01:04:35  Raum[2] Status:ON
01:06:31  Raum[1] Status:OFF
01:10:40  Raum[2] Status:OFF
01:23:04  Raum[2] Status:ON
01:28:43  Raum[2] Status:OFF
01:34:05  Raum[1] Status:ON`;

    // Extract the time, room and the status
    const transformed = data.split("\n").map((line) => {
      const [time, _, room, status] = line.split(" ");
      return {time, room, status};
    });
    console.log(transformed);
  } catch (e) {
    console.error(e);
  }
})();

Versuch damit erstmal zu arbeiten. Für Rückfragen stehe ich natürlich bereit.
 
Zuletzt bearbeitet:

Sempervivum

Senior HTML'ler
Ich empfehle, die einzelnen Zeilen mit regex und match zu parsen, dann stehen dir die einzelnen Werte getrennt zur Verfügung und Du kannst damit arbeiten:
Javascript:
        fetch('13.log')
            .then(response => response.text())
            .then(data => {
                const lines = data.split('\n');
                for (let line of lines) {
                    const
                        regex = /(\d{2}):(\d{2}):(\d{2})\s+Raum\[(\d)\]\sStatus:(ON|OFF)/,
                        result = line.match(regex);
                    if (result) {
                        const
                            secs = result[1] * 3600 + result[2] * 60 + result[3],
                            room = result[4],
                            status = result[5];
                        console.log(secs);
                        console.log(room);
                        console.log(status);
                    }
                }
            });
 
Zuletzt bearbeitet:

LudwigM

Mitglied
Gibt es auch ein Datum zu den jeweiligen Uhrzeiten oder setzt es sich täglich zurück?
Es wird pro Tag 1-31 eine Datei erstellt, dann nach einem Monat überschrieben. Das Datum steht ganz am Anfang als DD.MM.YYYY
Was mich aber noch trotzdem interessieren würde, ist wie groß diese Log-Files werden und ob sie lokal bei dir gespeichert sind oder du über eine URL darauf zugreifen musst.
Der Plan ist diese Dateien mindestens einmal pro Monat per SSH auf den lokalen PC zu kopieren. Lokal soll dann auch eine Historie vorhanden sein. Vermutlich in Ordnern der Struktur /Jahr/Monat/ und in jedem dieser Ordner dann pro Tag eine Logfile.
Für ein anderes Vorgehen bin ich aber offen, da noch nichts fertig ist.

Wie steht ihr zu der Idee, den Status mit Zeitstempel in einer DB zu speichern? Historische Statistiken sind dann vermutlich einfacher umzusetzen. Es würde auch das Problem bei Takten die am Tag x beginnen und am Tag x+1 enden, vereinfachen. Dann müssen nur regelmäßig seit dem letzten Mal die Logdateien eingelesen und die Db aktualisiert werden
 
Zuletzt bearbeitet:

Aaron3219

Senior HTML'ler
Wie steht ihr zu der Idee, den Status mit Zeitstempel in einer DB zu speichern? Historische Statistiken sind dann vermutlich einfacher umzusetzen.
Mit Datenbanken bei so einer Problemstellung zu arbeiten ist vermutlich deutlich einfacher und auch für zukünftige komplexere Vorhaben geeignet. Die Logik, wie man von der Historie zu einem Ergebnis kommt, ist aber ziemlich gleich.

Ich frage mich allerdings, warum man den Zeitstempel aus den Logs auslesen sollte (oder aus der Datenbank), anstatt einfach einen Akkumulator zu verwenden, der die Zeiten bei jedem Statuswechsel hochzählt. Dann spart man sich den ganzen Aufwand im Nachhinein und die Komplexität des Problems sinkt.
 

LudwigM

Mitglied
Ich frage mich allerdings, warum man den Zeitstempel aus den Logs auslesen sollte (oder aus der Datenbank), anstatt einfach einen Akkumulator zu verwenden, der die Zeiten bei jedem Statuswechsel hochzählt. Dann spart man sich den ganzen Aufwand im Nachhinein und die Komplexität des Problems sinkt.
Zum einen komm ich nicht an die Software die die Logs erstellt, und kann also gar nichts ändern.
Zum anderen würde bei einem Akkumulator die Information der Taktlänge fehlen.
Im besten Fall hätte ich Zugriff auf die Software, und würde dort nach meinen Wünschen z.B. direkt in eine DB schreiben...

Die Abfragen in der DB werden etwas komplizierter sein, falls sich mal jmd versuchen möchte hier ein Fiddle mit Beispieldaten. Als Ergebnis sollte folgendes herauskommen:
Maschine 1: Takte: 3, Taktlänge: 13.3 min, Laufzeit: 40 min/24h
Maschine 2: Takte: 1, Taktlänge: 30 min, Laufzeit: 30 min
Maschine 3: Takte: 0, Taktlänge: 0 min, Laufzeit: 0 min
 
Zuletzt bearbeitet:

Aaron3219

Senior HTML'ler
Zum anderen würde bei einem Akkumulator die Information der Taktlänge fehlen.
Nein, denn dafür hast du einen zweiten Akkumulator, der jedes mal einen hochzählt, wenn der Status wechselt. Dann sind deine Takte ja einfach der Akkumulator/2 abgerundet.

Bezüglich dieses Problems:
Zum einen komm ich nicht an die Software die die Logs erstellt, und kann also gar nichts ändern.
Ich gehe also mal davon aus, dass die logs von der Software selbst geschrieben werden und du keine eigenen Logs schreiben kannst. Aber wie willst du dann eine Datenbank nutzen? Willst du die LogFiles erst auslesen und in eine Datenbank überführen um sie auszuwerten?

Die Abfragen in der DB werden etwas komplizierter sein, falls sich mal jmd versuchen möchte hier ein Fiddle mit Beispieldaten.
Ich würde keine Query empfehlen, die alles auf einmal macht und dir fertige Ergebnisse liefert. Nutze die Datenbank, um deine Daten durch Grouping o. Ä. in eine leicht nutzbare Form zu bringen. Dann nutzt du Skripte um den Rest zu machen.

====================

Also wenn ich jetzt mal davon ausgehe, dass du nur Log-Dateien (txt) hast, wäre mein Ansatz folgender:
Du nutzt vermutlich einen Server auf dem du root-Zugriff hast. Führe ein Background shell-Skript aus, welches mittels tail jede neu gespeicherte Zeile ausliest und in die Datenbank speichert. Die Datenbank nutzt trigger, um die Werte entsprechend zu updaten. Du musst natürlich keine DB benutzen, du kannst es wie du willst speichern, aber DB ergeben für zukünftige Auswertungen schon Sinn. Auf diese Weise hättest du sogar eine real-time solution.

Edit: Ich sehe gerade, dass es tail sogar als npm package gibt. Ist vielleicht sogar besser als shell skripte, da JS-File meiner Meinung nach um einiges übersichtlicher sind.
 
Zuletzt bearbeitet:

LudwigM

Mitglied
Ich gehe also mal davon aus, dass die logs von der Software selbst geschrieben werden und du keine eigenen Logs schreiben kannst.
Richtig
Willst du die LogFiles erst auslesen und in eine Datenbank überführen um sie auszuwerten?
(Leider) ja
Du nutzt vermutlich einen Server auf dem du root-Zugriff hast.
Ja, wobei ich sehr vorsichtig bin dort etwas laufen zu lassen, da die Steuerung der Maschine dort läuft und ich nichts beeinflussen möchte. Real-Time wäre top, aber nicht zwingend notwendig

Sollte ich zeilenweise per JS (Ajax) in die DB speichern, oder besser mehrere Zeilen auf einmal, z.B. einen Tag bzw eine Logfile?
 

Aaron3219

Senior HTML'ler
Welchen Ansatz verfolgst du denn jetzt? Den mit dem tail npm package?

Prinzipiell kannst du das Zeile für Zeile machen, wenn nicht 1000 neue Zeilen pro Sekunde dazukommen.

Wenn du einen anderen Ansatz verfolgst und die Log-Files eines Tages in die Datenbank speicherst, solltest du einen großen Chunk pro Ajax-Abfrage nehmen oder gleich alles.
 
Zuletzt bearbeitet:

LudwigM

Mitglied
Welchen Ansatz verfolgst du denn jetzt? Den mit dem tail npm package?
Die Version von @Sempervivum
Code:
        let linesArray = [];
        fetch("13.log")
            .then(response => response.text())
            .then(data => {
                const lines = data.split('\n');
                let datum;
                for (let i = 0; i < lines.length; i++) {
                    let line = lines[i];
                    if(i === 0){
                        datum = convertDateFormat(line);
                    }else{              
                        const
                            regex = /(\d{2}):(\d{2}):(\d{2})\s+Raum\[(\d)\]\sStatus:(.+)/,
                            result = line.match(regex);
                        if (result) {
                            const
                                secs = result[1] * 360 + result[2] * 60 + result[3],
                                room = parseInt(result[4]),
                                status = result[5];
                            zeitstempel = datum + " " + result[1] + ":" + result[2] + ":" + result[3];
                            // Push the data to the array
                            linesArray.push({zeitstempel, status, raum: room + 5});
                        }
                    }
                }
            })
            .catch(error => console.log(error));


            $.ajax({
                type: "POST",
                url: "inc/insert.inc.php",
               
                data: {
                    mode: "1",
                    linesArray: linesArray
                },          
               
                success:function(data) {
                    console.log('success: ' + data);
                },
                error: function(data) {
                        console.log("ajax error, data: " + data);
                },
                complete: function(settings) {                    
                    console.log('ajax complete. ');      
                }
            });
Leider bekomme ich den Fehler "Unknown: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini." Wenn ich es Zeile für Zeile absende, dauert es ewig
 

Sempervivum

Senior HTML'ler
POST-Parameter sind nicht dafür geeignet, große Datenmengen zu übertragen, damit wirst Du jetzt konfrontiert.

Offensichtlich willst Du die Daten zum Server zurück schicken, um sie in die Datenbank einzutragen. Ich bezweifle, dass das eine gute Idee ist: Wenn die Seite nicht aufgerufen wird, unterbleiben auch die Einträge. Oder wenn sie mehrfach aufgerufen wird, finden sie auch mehrfach statt, was man abfangen müsste.

Was ist das denn überhaupt für ein Rechner, der die CSV-Datei bereit stellt? Ein "gewöhnlicher" Computer oder etwas in der Art SPS, da er ja offensichtlich die Maschinen steuert. Kann/soll darauf auch die Datenbank laufen? Offenbar ja, da Du beim Zurückschicken der Daten eine relative URL verwendest.
 

LudwigM

Mitglied
POST-Parameter sind nicht dafür geeignet,
Wie geht es besser?

Was ist das denn überhaupt für ein Rechner, der die CSV-Datei bereit stellt? Ein "gewöhnlicher" Computer oder etwas in der Art SPS
Ein kleiner Linux Rechner, der aber auch die SPS steuert. Ganz genau weiß ich es nicht. Die DB soll da nicht drauf laufen. Höchstens ein kleines Skript dass die Logdateien z.b. einmal pro Tag an einen Server schickt. Derzeit kopiere ich mir die Logdateien lokal auf den Heim-PC wo auch die DB und der Webserver läuft.

Oder wenn sie mehrfach aufgerufen wird, finden sie auch mehrfach statt, was man abfangen müsste.
Das verhindert die DB über den Primärschlüssel
 

Sempervivum

Senior HTML'ler
Ich hatte da früher mit fetch eine Lösung ausgearbeitet wo die Daten JSON-kodiert übertragen werden. Damit kann ich ohne Probleme 100000 Datensätze hoch laden. Mein Testcode:
HTML:
    <span id="out"></span>
    <script>
Javascript:
const
            params = [],
            param = {
                par1: 'some-param',
                par2: 'some-other-param',
                par3: [1, 2, 3, 4, 5]
            };
        for (let i = 0; i < 100000; i++) {
            params.push(param);
        }
        fetch('_demos/fetch/fetch-api-json-request.php', {
            method: 'post',
            headers: {
                'Accept': 'application/json, text/plain, */*',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(params)
        }).then(res => {
            return res.text();
        }).then(res => {
            console.log(res);
            document.getElementById('out').innerHTML = res;
        });
HTML:
</script>
Auslesen in PHP dann so:
PHP:
$params = json_decode(file_get_contents("php://input"));

die Logdateien z.b. einmal pro Tag an einen Server schickt. Derzeit kopiere ich mir die Logdateien lokal auf den Heim-PC wo auch die DB und der Webserver läuft.
Verstehe ich das richtig, dass der Heim-PC keine Dauerlösung sein soll?
Die Daten ein Mal am Tag herüber zu ziehen wäre sicher eine robuste und einfache Lösung.
 

LudwigM

Mitglied
Mit deinem Code @Sempervivum klappt das, vielen Dank!
Verstehe ich das richtig, dass der Heim-PC keine Dauerlösung sein soll?
Im besten Fall nicht. Dann kann man die Auswertungen auch mobil und standortunabhängig einsehen. Erstmal ist die Priorität die Logfiles in die DB zu parsen und die Abfragen zur Auswertung zu schreiben.
Die Daten ein Mal am Tag herüber zu ziehen wäre sicher eine robuste und einfache Lösung.
Hast du einen Vorschlag wie? Ein kleines Skript auf dem Rechner?
Eine realtime Lösung wäre natürlich toll, aber einmal pro Tag reicht auch...
 

Sempervivum

Senior HTML'ler
Ja, normaler Weise gibt es eine Möglichkeit, einen Cronjob einzurichten, der die Daten zu einer bestimmten Zeit z. B. in PHP mit file_get_contents oder curl lädt, dann kannst Du sie weiter verarbeiten und in die DB eintragen.

Was Echtzeit betrifft bin ich leider im Moment überfragt. @Aaron3219 hat da eine Lösung mit tail angedeutet.

PS: Weil mir Python näher steht als solche Shell-Sachen habe ich mal gesucht, ob es ein Äquivalent zu tail gibt und dies gefunden:

Dies funktioniert einwandfrei:
Python:
import time

def watch(fn):
    fp = open(fn, 'r')
    while True:
        new = fp.readline()
        # Once all lines are read this just returns ''
        # until the file changes and a new line appears

        if new:
            print(new)
        else:
            time.sleep(0.5)
watch(r'pfad\test.log')
Könnte man noch optimieren, indem man auf Änderungen an der Datei horcht:
 
Zuletzt bearbeitet:

Aaron3219

Senior HTML'ler
PS: Weil mir Python näher steht als solche Shell-Sachen habe ich mal gesucht, ob es ein Äquivalent zu tail gibt und dies gefunden:
https://stackoverflow.com/questions/1703640/how-to-implement-a-pythonic-equivalent-of-tail
Python ist keine schlechte Wahl. In einem meiner Beiträge habe ich auch ein npm package verlinkt, welches die Funktionalität von tail bereitstellt. Ich stimme dir zu, dass shell skripte teilweise etwas holprig zu schreiben sind.

Ich habe mal ein kleines JS-Skript mit dem npm package geschrieben. Meiner Ansicht nach sieht das npm package deutlich solide aus, als der Polling-Ansatz in Python und bietet auch diverser anderer Parameter und Optionen an.

Javascript:
Tail = require('tail').Tail;
tail = new Tail("./test.txt");

tail.on("line", (line) => {
    let regexResult = line.match(/^(\d{2}:\d{2}:\d{2})\s+Raum\[(\d)\]\sStatus:(ON|OFF)/);
    if (!regexResult) return;

    let time = new Date(`1970-01-01 ${regexResult[1]}`).getTime();
    let room = parseInt(regexResult[2]);
    let status = regexResult[3] === "ON";

    console.log(time, room, status)
});

tail.on("error", function (error) {
    console.log('ERROR: ', error);
});

[EDIT] Also, mir war sehr, sehr langweilig... und was soll ich sagen, ich habe ein wenig rumgebastelt.

Javascript:
const Tail = require('tail').Tail;
const tail = new Tail("./test.txt", { /*fromBeginning: true*/ });

const mariadb = require('mariadb');
const pool = mariadb.createPool({ host: '127.0.0.1', user: 'root', password: '', port: 3307, database: 'test' });
const tableName = "test";

(async () => {
    let conn;
    try {
        conn = await pool.getConnection();

        tail.on("line", async (line) => {
            let regexResult = line.match(/^(\d{2}:\d{2}:\d{2})\s+Raum\[(\d)\]\sStatus:(ON|OFF)/);
            if (!regexResult) return;

            let time = new Date(`1970-01-01T${regexResult[1]}.000Z`).getTime();
            let room = parseInt(regexResult[2]);
            let status = regexResult[3] === "ON";
            // fr-CA wird als Date-Format verwendet, da es die Form YYYY-MM-DD hat, die in der Datenbank verwendet wird
            let currentDate = new Date().toLocaleDateString('fr-CA', { timeZone: 'Europe/Berlin' });

            const insertRes = await conn.query(`INSERT INTO ${tableName} (date, room, status_changes, time_on, currently_on, last_on)
            VALUES(?, ?, ?, IF(?=0, ?, 1-1), ?, IF(?=1, ?, 1-1)) ON DUPLICATE KEY
            UPDATE status_changes=status_changes + 1, time_on = IF(?=0, time_on + (? - last_on), time_on), currently_on = ?, last_on = IF(?=1, ?, 1-1)`,
                [currentDate, room, 1, status, time, status, status, time, status, time, status, status, time]);

            if (insertRes) {
                let [result] = await conn.query(`SELECT * FROM ${tableName} WHERE room = ? AND date = ?`, [room, currentDate]);
            }
        });

        tail.on("error", (error) => {
            console.log('ERROR: ', error);
        });

    } catch (err) {
        console.error(err);
    } finally {
        if (conn) conn.release(); //release to pool
    }
})();

Benötigt die npm-packages mariadb und tail. Du kannst oben die Option fromBeginning ausklammern, wenn die Datei Zeile für Zeile in die Datenbank soll. Ansonsten werden nur Zeilen übernommen, die ab sofort neu dazukommen.

Deine MySQL/MariaDB-Tabelle kannst du hiermit aufsetzen:
SQL:
CREATE TABLE IF NOT EXISTS `test` (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `date` date NOT NULL,
  `room` int(10) UNSIGNED NOT NULL,
  `status_changes` int(10) UNSIGNED NOT NULL,
  `time_on` int(11) UNSIGNED NOT NULL,
  `currently_on` tinyint(1) NOT NULL,
  `last_on` int(10) UNSIGNED NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `date` (`date`,`room`)
) ENGINE=MyISAM AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

Zum testen kannst du ja mal ein File erstellen und mit dem folgenden Shell-Command Zeilen an das File hinten anfügen:
Code:
echo 01:04:35  Raum[4] Status:ON >> ./test.txt

Wie du siehst, nutze ich ausschließlich Akkumulatoren und so lässt sich für jeden Tag für jeden Raum ein Eintrag erstellen, welche dann bequem nach Monat oder was auch immer gruppiert werden können.

Bitte versuche den Code zu verstehen und stelle ggf. Fragen, sofern du den Ansatz nutzen möchtest.
 
Zuletzt bearbeitet:

Sempervivum

Senior HTML'ler
Weil ich es eine interessante Aufgabe finde, bin ich dem Verfahren mit dem Watchdog mal nachgegangen. Ergebnis ist, dass man die Logfiles mit wenigen Zeilen ohne Polling abfragen kann:
Python:
import os,time
from watchdog.observers import Observer
path=r'D:\Gemeinsame Dateien\Webentwicklung\_python\tail'
positions = {}
class Handler():
    @staticmethod
    def dispatch(event):
        print(getattr(event, 'event_type'))
        path = getattr(event, 'src_path')
        print(path)
        fp = open(path, 'r')
        size = os.fstat(fp.fileno()).st_size
        try:
            savedPos = positions[path]
        except:
            savedPos = 0
        if size < savedPos:
            savedPos = 0
        fp.seek(savedPos)
        new = fp.readline()
        while new:
            print(new)
            new = fp.readline()
        positions[path] = fp.tell()
        fp.close()
eventHandler = Handler()
observer = Observer()
observer.schedule(eventHandler, path, recursive=False)
observer.start()
try:
    while True:
        time.sleep(1)
finally:
    observer.stop()
    observer.join()
Das Vorgehen, die Datei jedes Mal zu öffnen und wieder zu schließen erfordert, die letzte Position zu sichern und mit seek wieder aufzusuchen. Ich habe das gewählt, weil ich unter Windows fest gestellt habe, dass die Datei für ein Löschen gesperrt ist, so lange sie offen ist. Je nachdem wie das Programm beim Schreiben der Dateien vorgeht, könnte das zu Problemen führen.
 
Zuletzt bearbeitet:
Oben