Node.jsで.csv形式でログを取って.tar.gz形式で圧縮する

soluna-eureka.hatenablog.com

の続き

開発背景

電力が貴重なこの時代!学術研究でも計算機の消費電力は無視できない!そもそもCPU温度が下がらない状況は危ない!
ということで常駐で温度を監視してログを取れるやつにまで昇華しました,やってることは基礎的なことのはずなんだけどな…

ちなみに冷却装置の設定によっては室温との相関があるかもしれない,流体の熱伝導を考えればそれはそうという感じはあるが

機能

  • 1秒毎にCPU温度を監視
    • 設定温度を超過するとSlackにログを送信
  • 毎分10n秒でログを.csvに書き込む
    • excelとかで見るのを前提に
  • 毎時0分でSlackにログを送信
    • 動作確認の意味合いも兼ねて
  • 毎日0時5分に前日の.csv.tar.gzに圧縮し元の.csvを削除
    • もちろん最大圧縮

以下,反省と言い訳

csvの操作がクソ難しい

こちらもオブジェクト型や配列型の酷使に慣れていなかったというのもあるが,単純にnpmcsvの各ライブラリ(csv-parseとかcsv-stringifyとか)の仕様がかなり変わっていて導入が面倒だった,ごく最近のいい感じのブログとかもなかったので公式ドキュメントを見ないとなかなか実装できなかった,あっ営利目的でこの記事を参考にしてもいいけど参考にしたらここのリンクを広めといてね♡

async/await多すぎ問題

これってsyncなライブラリだよな〜と思ったらasyncでした〜みたいなことがちょっと多すぎて頭にきますよ〜,じゃけんasync/awaitかけますね(保険的な意味合いで),async functionで定義した関数をawaitなしで呼び出すとpromiseが返ってくるなんて初めて知りましたね…言われてみればそうなんだけど,本当にそうなんだ…

.csvファイルの成形問題

ヘッダの有無くらいしか実装できなかったが,実際の利用においては問題はないと判断したのでこれで行くことにした,調べた限りではヘッダありで.csvをパースしてjsonライクにオブジェクト型でデータを操作する的な場合もあるらしいが,わざわざ各ループの間で状態を渡し続ける実装なんて面倒だし,もう1行目の完全一致だけで判定した方がはるかに早そうなのでなぁ…

その無駄っぽい変数宣言いる?

いる(確信),書いてるうちにわからなくなりかけたので必要だった(確信),ちなみに文字数と修飾語で用途を分けている

地味に温度の精度が悪い

1℃単位の測定しか許容されていなかったり誤差が無駄に大きかったりという噂がOpen Hardware Monitorにはあるらしいが,まぁ気休め程度だと思って使うことにする,実際に測定結果を見ていても面白いから問題なし

コード

// IO plugin
const fs = require('fs-extra');
const tar = require('tar');
const tryCatch = require('try-catch');
const scheduler = require('node-schedule');

// API plugin
const { getJSON } = require('simple-get-json');
const { stringify } = require('csv-stringify/sync');
const { parse } = require('csv-parse/sync');
const csvStringify = stringify;
const csvParsing = parse;

//Util plugin
const datetime = require('date-and-time');
const { IncomingWebhook } = require("@slack/webhook");

// Parameters
const slackURL = "https://hooks.slack.com/services/xxx/yyy/zzz";
const slackWebhook = new IncomingWebhook(slackURL);
const monitorURL = "http://localhost:8085/data.json";
const coreTempLimit = 60;




// API for Open Hardware Monitor
async function accessJSON() {
    try {
        var res = await getJSON(monitorURL);
        var raw = JSON.stringify(res, null, 4);
        var obj = JSON.parse(raw)['Children']; // for Open Hardware Monitor
        var tgt = obj[0]['Children'][1]['Children'][1]['Children']; // for Open Hardware Monitor
        return tgt;
    } catch (err) {
        console.log("something wrong happened with CPU Monitor!")
        tgt = [
            { Text: "0", Value: "0" } // for open hardware monitor
        ];
        return tgt;
    }
}


// Shaper for Open Hardware Monitor
async function makeMessage(tgt) {
    var cpuParam = "";
    var coreName = "";
    var coreTemp = "";
    var heatFlag = false;

    var i = 0;
    var j = 0;
    tgt.forEach(cpuCore => {
        coreName = cpuCore['Text']; // for Open Hardware Monitor
        coreTemp = cpuCore['Value']; // for Open Hardware Monitor
        cpuParam += (coreName + " is " + coreTemp + "\n");
        floatCoreTemp = parseFloat(coreTemp);
        i = i + 1
        if (floatCoreTemp > coreTempLimit) {
            j = j + 1;
        }
    });
    if (i == j) {
        heatFlag = true; // whether need to notice or not
    }

    var txt = cpuParam + datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS') + "\n";
    var flg = heatFlag;
    return [flg, txt];
}


async function makeCSVtext(tgt) {
    var cpuParam = [];
    var coreName = "";
    var coreNames = ["date"];
    var coreTemp = "";
    var coreTemps = [datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS')];
    var heatFlag = false;

    var i = 0;
    var j = 0;
    tgt.forEach(cpuCore => {
        coreName = cpuCore['Text']; // for Open Hardware Monitor
        coreNames.push(coreName);
        coreTemp = cpuCore['Value']; // for Open Hardware Monitor
        floatCoreTemp = parseFloat(coreTemp);
        coreTemps.push(floatCoreTemp);
        i = i + 1
        if (floatCoreTemp > coreTempLimit) {
            j = j + 1;
        }
    });
    if (i == j) {
        heatFlag = true; // judge whether to notice or not
    }

    cpuParam = [coreNames, coreTemps];
    var txt = csvStringify(cpuParam, { header: false }, { delimiter: "," });
    var flg = heatFlag;
    return [flg, txt];
}



// csv(array)_txt(string) Parse&Build interface
async function parseCSVdata(txt) {
    var csv = await csvParsing(txt, { columns: false, delimiter: "," });
    return csv;
}

async function parseCSVdataHead(txt) {
    var csv = await csvParsing(txt, { columns: false, delimiter: ",", to_line: 1 }); // get only header line
    return csv;
}

async function buildCSVdata(csv) {
    var txt = await csvStringify(csv, { header: false, delimiter: "," });
    return txt;
}



// API for Slack
async function sendMessage(flg, txt) {
    if (flg) { // grasp whether to notice or not
        try {
            await slackWebhook.send({
                text: `${txt}`
            });
        } catch (err) {
            var text = "temp flag is " + flg + ", but something error happened with slack!";
            console.log(text);
        }
    }
}

async function sendLogging(flg, txt) {
    try {
        await slackWebhook.send({
            text: `${txt}`
        });
    } catch (err) {
        var text = "something error happened with slack!";
        console.log(text);
    }
}



// commonly file system utils
async function createFolder(path) {
    try {
        await fs.ensureDir(path);
    } catch (err) {
        var text = path + " (folder) can not be created!";
        console.log(text);
    };
}

async function createFile(path) {
    try {
        await fs.ensureFile(path);
    } catch (err) {
        var text = path + " (file) can not be created!";
        console.log(text);
    };
}

async function writeupFile(path, txt) {
    await createFile(path);
    try {
        await fs.appendFile(path, txt);
    } catch (err) {
        var text = path + " (file) can not be written!";
        console.log(text);
    };
}

async function writeupCSVdata(path, txt) {
    var preTxt;
    var preCsv;
    var csv;
    try {
        preTxt = await fs.readFile(path);
    } catch (err) {
        var text = path + " (file) can not be read!";
        console.log(text);
        preTxt = "";
    };
    preCsv = await parseCSVdataHead(preTxt);
    csv = await parseCSVdataHead(txt);
    if (JSON.stringify(preCsv) == JSON.stringify(csv)) { // check a header, need to developed
        csv = await parseCSVdata(txt);
        csv.shift();
        txt = await buildCSVdata(csv);
    }
    await writeupFile(path, txt);
}



async function createArchive(filePath, archivePath) {
    try {
        tar.create({
            sync: true,
            file: archivePath,
            gzip: '-k -9'
        }, [
            filePath
        ])
    } catch (err) {
        var text = archivePath + " (archive) can not be created!";
        console.log(text);
    };
}


async function removeFileFolder(path) {
    try {
        fs.removeSync(path);
    } catch (err) {
        var text = path + " (file) can not be removed!";
        console.log(text);
    };
}




// initialize
(async() => {
    createFolder(".", "/Logs");
})();




// routine definition
const scheduleSendMessage = scheduler.scheduleJob("0-59 * * * * *", function() {
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeMessage(tgt);
        await sendMessage(flg, txt);
    })();
});



const scheduleSendLogging = scheduler.scheduleJob("0 0 */1 * * * ", function() {
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeMessage(tgt);
        await sendLogging(flg, txt);
    })();
});



const scheduleLogging = scheduler.scheduleJob("0,10,20,30,40,50 * * * * *", function() {
    var date = datetime.format(new Date(), 'YYYY_MM_DD');
    var path = "./Logs/" + date + ".csv"
    var tgt;
    var txt;
    var flg;
    (async() => {
        tgt = await accessJSON();
        [flg, txt] = await makeCSVtext(tgt);
        await writeupCSVdata(path, txt);
    })();
});



const manageLogfiles = scheduler.scheduleJob("0 5 0 */1 * *", function() {
    var cur = new Date();
    var old = datetime.addDays(cur, -1);
    var curDate = datetime.format(cur, 'YYYY_MM_DD');
    var oldDate = datetime.format(old, 'YYYY_MM_DD');
    var curPath = "./Logs/" + curDate + ".csv";
    var oldPath = "./Logs/" + oldDate + ".csv";
    var curGzip = "./Logs/" + curDate + ".tar.gz";
    var oldGzip = "./Logs/" + oldDate + ".tar.gz";
    (async() => {
        await createFile(curPath);
        await createFile(curGzip);
        await createArchive(oldPath, oldGzip);
        await removeFileFolder(oldPath);
    })();
    console.log(datetime.format(new Date(), 'YYYY/MM/DD HH:mm:ss:SSS'));
    var text = "logging files are regularLy updated!";
    console.log(text);
});