14.04.2014, 19:46
|
|
Кандидат Javascript-наук
|
|
Регистрация: 23.07.2013
Сообщений: 122
|
|
Серверное логирование 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
});
|
|
14.04.2014, 19:47
|
|
Кандидат Javascript-наук
|
|
Регистрация: 23.07.2013
Сообщений: 122
|
|
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);
}
}
?>
|
|
14.04.2014, 19:48
|
|
Кандидат Javascript-наук
|
|
Регистрация: 23.07.2013
Сообщений: 122
|
|
Кому интересно - посмотрите, попробуйте. Может найдутся баги
|
|
|
|