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);
}
}
?>