LoginSignup
7
13

More than 3 years have passed since last update.

[JavaScript] consoleでログファイルを出力する

Last updated at Posted at 2020-04-09

はじめに

JavaScriptでconsoleのファイル出力機能を作成しました。

通常、JavaScriptのconsoleオブジェクトを使用すると、PCのWEBブラウザ開発モードからコンソール画面にログが表示されます。

ここでは、consoleオブジェクト使用すると、サーバー側でログファイルを出力するようにしました。処理の流れは下記の通りです。

url.png

consoleオブジェクトの関数(例えば、console.log())を実行すると、ログメッセージがキューイングされます。10秒間隔て定期的にサーバー(php)にログメッセージがPOSTされます。そして、サーバー側でメッセージをファイルに出力しています。

この仕組みのメリットは、開発モードが使えないブラウザで有効であると思います。例えば、IOTデバイスにブラウザが組み込まれているような場合は、開発モードが有効になっていないことが多いです。

機能としては下記の通りです。

  • ログレベル
  • ログローテート
  • 出力先指定

実行環境

今回実行した環境は下記の通りです。

実行環境
JavaScript: ECMAScript6

使い方

使い方は下記の通りです。

console.error('log: error.');
console.warn('log: warn.');
console.info('log: info.');
console.debug('log: debug.');
console.log({a:"aaa",b:1,c:{d:2},d:[3,4,5]});

出力されるログは下記の通りです。

[2020-03-27 14:41:45.283][ERROR][::1]  log: error.
[2020-03-27 14:41:45.283][WARN][::1]  log: warn.
[2020-03-27 14:41:45.283][INFO][::1]  log: info.
[2020-03-27 14:41:45.283][DEBUG][::1]  log: debug.
[2020-03-27 14:41:45.284][DEBUG][::1]  {a:"aaa",b:1,c:{d:2},d:[3,4,5]}

ログファイル出力の実装

JavaScriptの実装

configオブジェクトでログに関する設定を行います。

config.js
/**
 * 設定
 */
var config = {
    IS_LOGFILE: true, // ログフラグ true=ログファイル出力あり/false=なし
    LOG_LEVEL: 3, // ログレベル 0=ERROR/1=WARN/2=INFO/3=DEBUG
    API_LOGOUT: 'http://localhost/console_api.php', // ログ出力API 
};

logオブジェクトの実装は下記の通りです。

log.js
var log = log || {}; {
    // ログレベル
    let logLevel = {
        ERROR: 0,
        WARN: 1,
        INFO: 2,
        DEBUG: 3,
    };

    let msgList = [];

    /**
     * 初期化
     */
    log.init = () => {
        // console関数の上書き
        overrideConsole();

        // 定期的にログをPOSTする
        let run = () => {
            if (0 < msgList.length) {
                let msg = msgList.join('\r\n');
                msgList = [];
                postMsg({
                    msg: msg
                });
            }
            setTimeout(run, 10000);
        }
        run();
    };

    /**
     * console関数を上書きする
     */
    let overrideConsole = () => {
        console = {};
        console.error = (...args) => {
            if (logLevel.ERROR <= config.LOG_LEVEL) {
                out('ERROR', args);
            }
        }
        console.warn = (...args) => {
            if (logLevel.WARN <= config.LOG_LEVEL) {
                out('WARN', args);
            }
        }
        console.info = (...args) => {
            if (logLevel.INFO <= config.LOG_LEVEL) {
                out('INFO', args);
            }
        }
        console.debug = (...args) => {
            if (logLevel.DEBUG <= config.LOG_LEVEL) {
                out('DEBUG', args);
            }
        }
        console.log = (...args) => {
            if (logLevel.DEBUG <= config.LOG_LEVEL) {
                out('DEBUG', args);
            }
        }
        console.ws = null;
    }

    /**
     * ログを出力する
     * @param {String} level ログレベル
     * @param {String} msg メッセージ
     */
    let out = (level, msg) => {
        msg = `[${getTimestamp(new Date())}][${level}][{ipAddress}] ${convertMsg(msg)}`;
        msgList.push(msg);
    }

    /**
     * タイムスタンプを取得する
     * @param {Date} dt 
     */
    let getTimestamp = (dt) => {
        let yyyy = dt.getFullYear();
        let MM = ('00' + (dt.getMonth() + 1)).slice(-2);
        let DD = ('00' + dt.getDate()).slice(-2);
        let hh = ('00' + dt.getHours()).slice(-2);
        let mm = ('00' + dt.getMinutes()).slice(-2);
        let ss = ('00' + dt.getSeconds()).slice(-2);
        let dd = ('000' + dt.getMilliseconds()).slice(-3)
        return `${yyyy}-${MM}-${DD} ${hh}:${mm}:${ss}.${dd}`;
    }

    /**
     * ログメッセージを変換する
     * @param {Array} arg メッセージ
     */
    let convertMsg = (arg) => {
        let msg = '';
        for (let i = 0; i < arg.length; i++) {
            msg += ' ' + dumpObject(arg[i]);
        }
        return msg;
    }

    /**
     * オブジェクトを出力する
     * @param {Object} obj 
     */
    let dumpObject = (obj) => {
        let v = '';
        let t = typeof obj;
        switch (t) {
            case 'number':
            case 'boolean':
                v = obj;
                break;
            case 'string':
                v = '"' + obj + '"';
                break;
            case 'object':
                if (isArray(obj)) {
                    v = '[';
                    let count = 0;
                    for (let value of obj) {
                        v += dumpObject(value);
                        count++;
                        if (count < obj.length) {
                            v += ',';
                        }
                    }
                    v += ']';
                } else if (isObject(obj) || isError(obj)) {
                    v = '{';
                    let count = 0;
                    let nameList = Object.getOwnPropertyNames(obj);
                    for (let key of nameList) {
                        let ret = dumpObject(obj[key]);
                        if (ret) {
                            v += key + ':' + ret;
                            count++;
                            if (count < nameList.length) {
                                v += ',';
                            }
                        }
                    }
                    v += '}';
                } else if (isString(obj) || isDate(obj)) {
                    v = '"' + obj.toString() + '"';
                } else if (isNumber(obj) || isBoolean(obj)) {
                    v = obj.valueOf();
                } else if (isFunction(obj)) {
                    v = '<fuction>';
                } else if (isNull(obj)) {
                    v = '<null>';
                } else if (isUndefined(obj)) {
                    v = '<undefined>';
                }
                break;
            case 'function':
                v = '<function>';
                break;
            case 'symbol':
                v = '<symbol>';
                break;
            case 'undefined':
                v = '<undefined>';
                break;
        }
        return v;
    }

    let isArray = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Array]';
    }

    let isBoolean = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Boolean]';
    }

    let isDate = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Date]';
    }

    let isError = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Error]';
    }

    let isNumber = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Number]';
    }

    let isObject = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Object]';
    }

    let isString = (obj) => {
        return Object.prototype.toString.call(obj) === '[object String]';
    }

    let isFunction = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Function]';
    }

    let isNull = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Null]';
    }

    let isUndefined = (obj) => {
        return Object.prototype.toString.call(obj) === '[object Undefined]';
    }

    /**
     * ログメッセージをPOSTする
     * @param {Object} data リクエストパラメータ
     */
    let postMsg = (data) => {
        return new Promise((resolve, reject) => {

            let req = new XMLHttpRequest();
            req.open('POST', config.API_LOGOUT, true);
            req.onload = () => {
                if (req.responseText && req.status === 200) {
                    resolve(req.responseText);
                } else {
                    reject({
                        message: `API response invalid (http status:${req.status})`
                    });
                }
            };
            req.onerror = () => {
                reject({
                    message: `API request error.`
                });
            };
            req.ontimeout = () => {
                reject({
                    message: 'API request timeout.'
                });
            };
            req.onabort = () => {
                reject({
                    message: 'API request abort.'
                });
            };
            req.timeout = 60 * 1000;
            req.setRequestHeader('Content-Type', 'application/json');
            req.send(JSON.stringify(data));
        });
    };
}

if (config.IS_LOGFILE) {
    log.init();
}

PHPの実装

configクラスでログに関する設定を行います。

config.php
<?php
/**
 * 設定クラス
 */
class Config {
    const LOGDIR_PATH = './logs/'; // ログファイル出力ディレクトリ
    const LOGFILE_NAME = 'console'; // ログファイル名
    const LOGFILE_MAXSIZE = 10485760; // ログファイル最大サイズ(Byte)
    const LOGFILE_PERIOD = 30; // ログ保存期間(日)
}

?>

APIの実装は下記の通りです。

console_api.php
<?php
/**
 * ログ出力API
 */
if($_SERVER['REQUEST_METHOD'] === 'POST') {

    require_once("./config.php");

    $json = file_get_contents('php://input');
    $data = json_decode($json, true);
    $msg = $data['msg'];

    if(isset($msg)) {

        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
        } else {
            $ipAddress = $_SERVER['REMOTE_ADDR'];
        }

        $logMessage = str_replace('{ipAddress}', $ipAddress, $msg) . "\n";
        $logFilePath = Config::LOGDIR_PATH . Config::LOGFILE_NAME . '.log';

        $result = file_put_contents($logFilePath, $logMessage, FILE_APPEND | LOCK_EX);
        if(!$result) {
            error_log('LogUtil::out error_log ERROR', 0);
        }

        if(Config::LOGFILE_MAXSIZE < filesize($logFilePath)) {
            // ファイルサイズを超えた場合、リネームしてgz圧縮する
            $oldPath = Config::LOGDIR_PATH . Config::LOGFILE_NAME . '_' . date('YmdHis');
            $oldLogFilePath = $oldPath . '.log';
            rename($logFilePath, $oldLogFilePath);
            $gz = gzopen($oldPath . '.gz', 'w9');
            if($gz) {
                gzwrite($gz, file_get_contents($oldLogFilePath));
                $isClose = gzclose($gz);
                if($isClose) {
                    unlink($oldLogFilePath);
                } else {
                    error_log("gzclose ERROR.", 0);
                }
            } else {
                error_log("gzopen ERROR.", 0);
            }

            // 古いログファイルを削除する
            $retentionDate = new DateTime();
            $retentionDate->modify('-' . Config::LOGFILE_PERIOD . ' day');
            if ($dh = opendir(Config::LOGDIR_PATH)) {
                while (($fileName = readdir($dh)) !== false) {
                    $pm = preg_match("/" . preg_quote(Config::LOGFILE_NAME) . "_(\d{14}).*\.gz/", $fileName, $matches);
                    if($pm === 1) {
                        $logCreatedDate = DateTime::createFromFormat('YmdHis', $matches[1]);
                        if($logCreatedDate < $retentionDate) {
                            unlink(Config::LOGDIR_PATH . '/' . $fileName);
                        }
                    }
                }
                closedir($dh);
            }
        }
        echo 'OK.';
    }
}

?>

さいごに

ソースコードをGitHubに公開しています。

ソースファイルはこちら

以上です。

7
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
13