Показать сообщение отдельно
  #2 (permalink)  
Старый 14.04.2014, 19:47
Аватар для Hapson
Кандидат Javascript-наук
Отправить личное сообщение для Hapson Посмотреть профиль Найти все сообщения от Hapson
 
Регистрация: 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);
	}
}
?>
Ответить с цитированием