はじめに
JavaScriptでconsoleのファイル出力機能を作成しました。
通常、JavaScriptのconsoleオブジェクトを使用すると、PCのWEBブラウザ開発モードからコンソール画面にログが表示されます。
ここでは、consoleオブジェクト使用すると、サーバー側でログファイルを出力するようにしました。処理の流れは下記の通りです。
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オブジェクトでログに関する設定を行います。
/**
* 設定
*/
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オブジェクトの実装は下記の通りです。
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クラスでログに関する設定を行います。
<?php
/**
* 設定クラス
*/
class Config {
const LOGDIR_PATH = './logs/'; // ログファイル出力ディレクトリ
const LOGFILE_NAME = 'console'; // ログファイル名
const LOGFILE_MAXSIZE = 10485760; // ログファイル最大サイズ(Byte)
const LOGFILE_PERIOD = 30; // ログ保存期間(日)
}
?>
APIの実装は下記の通りです。
<?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に公開しています。
以上です。