Javascript-форум (https://javascript.ru/forum/)
-   Ваши сайты и скрипты (https://javascript.ru/forum/project/)
-   -   Серверное логирование javascript ошибок (https://javascript.ru/forum/project/46550-servernoe-logirovanie-javascript-oshibok.html)

Hapson 14.04.2014 19:46

Серверное логирование javascript ошибок
 
Знаю, знаю... да - написал велосипед :)
Вобщем вот:

JS часть
/**
 * Модуль отправки Javascript ошибок на сервер
 *
 * @author Hapson <hapson5703@gmail.com>
 * @copyright 2014 Hapson
 * @version 1.0.0
 */

var ErrorHandler = (function(win){

var EH = {}, oldH;

var SG = {
	url: undefined,
	debug: true,
	log: true
};

EH.setting = function(set){
	/**
	 * Установка настроек и запуск модуля логирования ошибок
	 * 
	 * @set {object} - массив настроек
	 * set.url {string} - URL, на который отсылать сообщения
	 * set.debug {boolean} - true: стандартный вывод ошибок
	 *                       false: попытка отключить стандартный вывод ошибок
	 * set.log {boolean} - true: отправлять ошибки на сервер
	 *                     false: не отправлять
	 */
	if(Object.prototype.toString.call(set) !== "[object Object]"){
		throw new TypeError("No configuration or incorrect ErrorHandler");
	}
	oldH = win.onerror; win.onerror = handler;
	SG.url = typeof set.url === "string" ? set.url : undefined;
	SG.debug = !!set.debug ? true : false;
	if(typeof win.opera !== "undefined"){SG.debug = !SG.debug;}
	SG.log = !!set.log ? true : false;
};

EH.log = function(error){
	/**
	 * Отправляет ошибку на сервер
	 *
	 * @error {string|object Error} - строка ошибки или объект класса Error
	 *    для получения более информативного стека, рекомендуется передавать объект класса Error
	 */
	if(Object.prototype.toString.call(error) !== "[object Error]"){
		try{throw new Error(""+ error);}catch(ex){error = ex;}
	}
	var ms, sk;
	ms = error.message || "noMessage";
	ms = (error.name || "noName") +": "+ ms +" at ";
	ms += (error.fileName || win.location.href) +":";
	ms += error.lineNumber || error.line || "noLine";
	sk = error.stacktrace || error.stack || getStack(EH.log) || "noStack";
	sendLog({type: "JS_EXCEPTION", message: ms, stack:sk});
};

function handler(message, file, line, col, error){
	/**
	 * Обработчик ошибок
	 *
	 * @message {string} - сообщение об ошибке
	 * @file {string} - файл, в котором произошла ошибка
	 * @line {number} - номер строки
	 * @col {number} - номер символа
	 * @error {object Error} - объект класса Error
	 */
	if(SG.log && SG.url){
		var res = prepareWinError(""+ message, file, line, col, error);
		if(res){sendLog(res);}
	}
	if(oldH){return oldH;}
	return SG.debug ? false : true;
}

function prepareWinError(message, file, line, col, error){
	/**
	 * Форматирует ошибку
	 *
	 * @return {object Object}
	 */
	if(message == "" || (/script *error/i.test(message) && line == 0)){return false;}
	col = col || "noColumn";
	var ms = message +" at "+ file +":"+ line +":"+ col;
	var sk = (error && (error.stacktrace || error.stack)) ? error.stacktrace || error.stack : getStack(ErrorHandler) || "noStack";
	return {type: "JS_ERROR", "message": ms, "stack": sk};
}

function sendLog(arr){
	/**
	 * Отправляет ошибку на сервер
	 * При наличии объекта XmlHttpRequest, отправляет ошибку методом POST
	 * В противном случае отправка производится методом GET
	 *
	 * @arr {object Object} - массив с полями type, message и stack
	 */
	var xhr = getXHR();
	var param = "type="+ arr.type +"&"+
		"message="+ encode(arr.message) +"&"+
		"stack="+ encode(arr.stack) +"&"+
		"platform="+ encode(navigator.platform) +"&"+
		"url="+ encode(win.location.href);
	if(!xhr){
		var url = SG.url + (/\?/.test(SG.url) ? "&" : "?") + param;
		try{new Image().src = url;}catch(ex){} return;
	}
	xhr.open("POST", SG.url, true);
	xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
	xhr.setRequestHeader("HTTP_X_FORWARDED_FOR", "XmlHttpRequest");
	xhr.send(param);
}

function getXHR(){
	try{return new XMLHttpRequest();}catch(e){}
	try{return new ActiveXObject("Msxml2.XMLHTTP.3.0");}catch(er){return false;}
}

function getStack(fn){
	/**
	 * Формирует псевдо-стек. Работает преимущественно в IE
	 */
	if(!fn.caller){return false;}
	var stack = [], name;
	while(fn = fn.caller){
		name = fn.toString().replace(/\{[\s\S]*\}/gm, '');
		stack.push(name);
	}
	return stack.reverse().join(' -> ');
}

function encode(str){return encodeURIComponent(str);}

return EH;

}(window));

// настраиваем и включаем

ErrorHandler.setting({
	url: window.location.protocol +"//"+ window.location.hostname +"/js_log_handler.php",
	debug: false,
	log: true
});

Hapson 14.04.2014 19:47

PHP часть. Пишет в файл
<?php
/**
 * Класс логирования ошибок клиентской стороны (Javascript)
 *
 * @author Hapson <hapson5703@gmail.com>
 * @copyright 2014 Hapson
 * @version 1.0.0
 */

class JSError{

protected $serviseDir = null; // директория для служебных файлов
protected $logDir = null; // директория файлов лога
protected $urlMessages = ''; // URL с которого должны поступать сообщения
protected $messagesLimit = 10; // лимит сообщений в секунду от одного IP
protected $overwriteOldLog = true; // перезаписывать старые лог файлы. false - переименовывать и сохранять

/* Servise vars */
protected $blockRes = false;

public function __construct($dir = false){
	/**
	 * Устанавливает рабочую директорию.
	 *
	 * @description - в случае вызова без параметров или указания несуществующей директории,
	 *    рабочая директория будет создана в текущей директории класса
	 *    В рабочей директории будут созданы каталоги:
	 *    "/mod_js_error_log/service" - служебные файлы
	 *    "/mod_js_error_log/log" - каталог логов Javascript ошибок
	 * @dir {string} - путь к реальной директории
	 * @return {Exception} - выбрасывает исключение в случае проблем с созданием директорий
	 */
	$sdir = is_dir($dir) ? $dir : __DIR__;
	$this->serviseDir = $sdir ."/mod_js_error_log/service";
	$this->logDir = $sdir ."/mod_js_error_log/log";
	if(!is_dir($this->serviseDir)){mkdir($this->serviseDir, 0644, true);}
	if(!is_dir($this->logDir)){mkdir($this->logDir, 0644, true);}
	if(!is_dir($this->serviseDir) || !is_dir($this->logDir)){
		throw new Exception("Модуль записи Javascript ошибок остановлен. Вероятно имеются проблемы с рабочими директориями");
	}
}

public function setLimit($limit = false){
	/**
	 * Устанавливает лимит сообщений в секунду
	 *
	 * @limit {int} - количество сообщений в секунду
	 */
	$this->messagesLimit = gettype($limit) == "integer" ? $limit : 10;
}

public function overwriteLog($flag = true){
	/**
	 * Устанавливает действие, что делать с файлом лога, когда его размер превысит 1024кБ
	 *
	 * @flag {boolean} - true: перезаписывать
	 *                   false: переименовывать
	 */
	$this->overwriteOldLog = $flag === false ? false : true;
}

public function setUrl($url = false){
	/**
	 * Устанавдивает URL, с которого нужно принимать сообщения
	 * Сообщения с иных URL игнорируются и генерируется исключение
	 *
	 * @url {string} - URL, с которого должны приходить сообщения
	 */
	$this->urlMessages = gettype("url") == "string" ? $url : "";
}

public function log($error){
	/**
	 * Проверка URL ошибки, лимита и передача ошибки на запись
	 *
	 * @error {array} - массив с полями для записи в лог
	 * @return {true|Exception} - в случае возникновения ошибок выбрасывает исключение класса Exception
	 *                            в случае успеха возвращает true
	 */
	$url = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
	if(!preg_match("#^{$this->urlMessages}.*#ui", $url)){
		throw new Exception("Javascript ошибка с чужого URL: $url");
		return;
	}
	try{
		$this->block();
		if($this->checkLimit()){$this->writeToFile($error);}
		$this->unblock();
	}catch(Exception $e){
		throw $e;
	}
	return true;
}

protected function writeToFile($error){
	/**
	 * Пишет ошибку в файл
	 *
	 * @error {array} - массив с полями для записи в лог
	 * @return {true|Exception} - в случае ошибки выбрасывает исключение класса Exception
	 *                            в случае успеха возвращает true
	 */
	$fname = $this->logDir ."/js_error.log";
	if(is_file($fname) && filesize($fname) >= 1024 * 1024){
		if($this->overwriteOldLog){
			if(file_put_contents($fname, null, LOCK_EX) === false){
				throw new Exception("Ошибка очистки файла $fname");
			}
		}else{
			$new = rtrim($fname, "log") . date("d_m_Y_H_i_s") .".log";
			if(!rename($fname, $new)){
				throw new Exception("Не удалось переименовать файл $fname -> $new");
			}
		}
	}
	if(is_file($fname)){
		$res = @fopen($fname, "a+b");
		if(!$res){throw new Exception("Ошибка при открытии файла $fname");}
	}else{
		$res = @fopen($fname, "x+b");
		if(!$res){throw new Exception("Ошибка при создании файла $fname");}
	}
	if(!flock($res, LOCK_EX|LOCK_NB)){
		fclose($res);
		throw new Exception("Ошибка при блокировке файла $fname");
	}
	// на всякий случай...
	foreach($error as &$v){$v = (string)$v;}
	$str = date("d.m.Y H:i:s") ."\n";
	foreach($error as $k => $v){
		$str .= "\t". ucfirst($k) .": ". $v ."\n";
	}
	if(fwrite($res, $str) === false){
		throw new Exception("Ошибка при записи в файл $fname");
	}
	@flock($res, LOCK_UN); @fclose($res);
	return true;
}

protected function checkLimit(){
	/**
	 * Проверяет, не достигнут ли лимит с данного IP адреса
	 * Пишет время, IP, UserAgent и пояснительное сообщение в служебный лог при каждом вызове
	 * Пояснительные сообщения:
	 *    "OK" - лимит не достигнут
	 *    "Error read data" - ошибка чтения файла лимитов IP. Файл перезаписан
	 *    "Limit message" - IP достиг лимита
	 * 
	 * @return {boolean}
	 */
	$ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string)$_SERVER['HTTP_USER_AGENT'] : '';
	$ip = isset($_SERVER['REMOTE_ADDR']) ? (string)$_SERVER['REMOTE_ADDR'] : '';
	$fname = $this->serviseDir ."/limit.dat";
	if(!is_file($fname)){
		$data = array($ip => array("time" => time(), "count" => 1));
		file_put_contents($fname, serialize($data));
		$this->serviseLog(time(), $ip, $ua, "OK");
		return true;
	}
	$data = file($fname);
	$data = is_array($data) ? @unserialize($data[0]) : false;
	if(!$data){
		$data = array($ip => array("time" => time(), "count" => 1));
		file_put_contents($fname, serialize($data));
		$this->serviseLog(time(), $ip, $ua, "Error read data");
		return true;
	}
	$data = $this->cleanOldIP($data);
	if(isset($data[$ip]) && isset($data[$ip]['time']) && isset($data[$ip]['count'])){
		if($data[$ip]['time'] === time() && $data[$ip]['count'] >= $this->messagesLimit){
			$this->serviseLog(time(), $ip, $ua, "Limit message");
			return false;
		}else if($data[$ip]['time'] === time() && $data[$ip]['count'] < $this->messagesLimit){
			$data[$ip]['count'] += 1;
		}else{
			$data[$ip] = array("time" => time(), "count" => 1);
		}
	}else{
		$data[$ip] = array("time" => time(), "count" => 1);
	}
	$this->serviseLog(time(), $ip, $ua, "OK");
	file_put_contents($fname, serialize($data));
	return true;
}

protected function serviseLog($time, $ip, $ua, $message){
	/**
	 * Каждое обращение к функции JSError::log() фиксирует в служебном логе
	 * Кроме запросов с неразрешенных URL
	 * 
	 * @time {int} - текущая временная метка
	 * @ip {string} - IP
	 * @ua {string} - UserAgent
	 * @message {string} - короткое сообщение статуса ошибки
	 */
	$fname = $this->serviseDir ."/serviseLog.dat";
	$str = date("d.m.Y H:i:s", $time) ." -> ". $message ." -> ". $ip ." -> ". $ua ."\n";
	$mode = is_file($fname) && filesize($fname) < 102400 ? FILE_APPEND | LOCK_EX : LOCK_EX;
	file_put_contents($fname, $str, $mode);
}

protected function cleanOldIP($data){
	/**
	 * Очищает файл лимитов от старых IP
	 *
	 * @data {array} - массив лимитов
	 */
	foreach($data as $k => $v){
		if($v['time'] + 2 < time()){
			unset($data[$k]);
		}
	}
	return $data;
}

protected function block(){
	/**
	 * Пытается заблокировать служебный блокирующий файл
	 * 
	 * @description - в процессе логирования ошибки происходит работа с несколькими текстовыми файлами.
	 *    Для упрощения работы и повышения надежности процедур чтения/записи производится блокировка
	 *    одного служебного блокирующего файла
	 * @return {Exception} - в случае неудачной попытки получить блокировку выбрасывается исключение класса Exception
	 */
	$block = $this->serviseDir ."/block.dat";
	for($lock = false, $z = 0; $z < 10; $z++){
		if($lock === false){$lock = @fopen($block, "w");}
		if(!($lock === false) && @flock($lock, LOCK_EX | LOCK_NB)){
			$this->blockRes = $lock;
			return true;
		}
		usleep(100000);
	}
	throw new Exception("Не удалось получить блокировку для записи Javascript ошибки в лог");
}

protected function unblock(){
	/**
	 * Снимает блокировку со служебного блокирующего файла
	 */
	@flock($this->blockRes, LOCK_UN);
}

}

// пример использования

function JSErrorLog(){
	$arr = array_merge($_POST, $_GET);
	$keys = array('type', 'message', 'stack', 'url', 'platform');
	$data = array();
	foreach($keys as $v){
		if(isset($arr[$v])){
			$data[$v] = $arr[$v];
		}else{
			return false;
		}
	}
	try{
		$logger = new JSError($_SERVER['DOCUMENT_ROOT'] ."/jslog");
		$logger->log($data);
	}catch(Exception $e){
		print_r($e);
	}
}
?>

Hapson 14.04.2014 19:48

Кому интересно - посмотрите, попробуйте. Может найдутся баги


Часовой пояс GMT +3, время: 12:36.