Tori

Log to file(s) v.1.1

Jun 23rd, 2011
116
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 10.20 KB | None | 0 0
  1. <?php
  2. /**
  3.  * Doplněk - používané výjimky (které nejsou součástí SPL).
  4.  */
  5. if (!class_exists('NotImplementedException'))   {
  6.     class NotImplementedException extends LogicException { }
  7. }
  8. if (!class_exists('IOException'))   {
  9.     class IOException extends RuntimeException { }
  10. }
  11. if (!class_exists('FileNotFoundException')) {
  12.     class FileNotFoundException extends IOException { }
  13. }
  14.  
  15.  
  16. /**
  17.  * <b>Log: Logování do souboru</b>
  18.  * 
  19.  * <i>Použití:</i>
  20.  * $log = Log::getInstance();
  21.  * $log->add('Zpráva do logu', 'Volitelný ladící výpis,který se k ní připojí');
  22.  * $log->add(. ....
  23.  * $log->write();
  24.  *
  25.  * <i>Nastavení:</i>
  26.  * $log->setLogDir('../log'); //cesta k adresáři s logy. Výchozí je aktuální adresář, musí se zadat *absolutní* cesta.
  27.  * $log->setMaxLogSize('2MB'); // max.velikost logu. Buď v bytech, nebo 'k/kb', 'M/MB' (case-insens.). Výchozí je 0 = bez limitu
  28.  * $log->setLogName('myLog.<date>.log'); // maska pro názvy souboru. Musí obs. "<date>" - u aktuálního se nahradí "current", u
  29.  *  archivovaných logů datumem archivace. Výchozí je "<date>.log". Není možné zadávat i cestu k souboru.
  30.  * - metody setLogDir, setMaxLogSize, setLogName a add vracejí opět instanci příslušného logu, je možné je řetězit.
  31.  *  
  32.  * <i>Několik různých logů:</i>
  33.  * Log::getInstance('app')->setLogDir('../log')->setLogName('appLog.<date>');
  34.  * Log::getInstance('db')->setLogDir('../log')->setLogName('db.<date>');
  35.  * Log::getInstance()->setLogDir('../log')->setLogName('blizeNeurcenyLog.<date>.log');
  36.  *
  37.  * Log::getInstance('db')->add('Chyba databáze.');
  38.  * Log::getInstance('app')->add('Chyba aplikace')->add('Další chyba aplikace');
  39.  * Log::getInstance()->add('Toto jde do nepojmenovaného logu.');
  40.  *
  41.  * Log::getInstance('db')->write(); // zapíše jen databázový log
  42.  * Log::writeAll();  // zapíše všechny existující logy
  43.  * - Metody write() / writeAll() je praktické volat z funkce registrované jako shutdown handler.
  44.  *
  45.  * @author Viktorie Halasů <[email protected]>
  46.  * @version 1.1 (lastmod 2012-05-03)
  47.  * @license MIT <http://www.opensource.org/licenses/mit-license.php>
  48.   */
  49.  
  50. final class Log {
  51.    
  52.     /** @var string Název souboru zámku. */
  53.     private $lockName = '.lock';
  54.    
  55.     /** @var string Cesta k adresáři s logy. Výchozí = adresář s touto třídou. */
  56.     private $path = '';
  57.    
  58.     /** @var string Maska pro název souboru logu (musí obsahovat řetězec '<date>'). */
  59.     private $logNameMask = '<date>.log';
  60.    
  61.     /** @var int Limit velikosti logu (v bytech). Pokud 0, tak se nepoužije. */
  62.     private $limit = 0;
  63.    
  64.     /** @var array Záznamy, které se mají zapsat do logu.*/
  65.     private $entries = array();
  66.    
  67.     private static $instances = array();
  68.    
  69.     /**
  70.      * Soukromý konstruktor.
  71.      */
  72.     private function __construct()  {
  73.         $this->path = dirname(__FILE__);
  74.     }
  75.  
  76.    
  77.     /**
  78.      * Vrací instanci Logu, v případě potřeby ji vytvoří.
  79.      * @param string $logName Volitelný identifikátor logu (pokud se jich používá víc).
  80.      * @return Log
  81.      */
  82.     public static function getInstance($logName = 0)        {
  83.         if (!isset(self::$instances[$logName]))
  84.             self::$instances[$logName] = new self;
  85.         return self::$instances[$logName];
  86.     }
  87.    
  88.    
  89.     /**
  90.      * Přidá zprávu do fronty logu.
  91.      * @param string $zprava Text zprávy k zapsání.
  92.      * @param string $debug='' Ladící výpis
  93.      * @return Log fluent interface
  94.      */
  95.     public function add($zprava, $debug='') {
  96.         $this->entries[] = $this->format($zprava, $debug);
  97.         return $this;
  98.     }
  99.    
  100.    
  101.     /**
  102.      * Zapíše do souboru logu všechny zprávy ve frontě.
  103.      * @throws IOException Pokud se nepodařilo zapsat data.
  104.      */
  105.     public function write() {
  106.         if (empty($this->entries))
  107.             return;
  108.         $this->tryBackup();
  109.         if (!file_put_contents($this->getFilename(), implode("\n", $this->entries)."\n", FILE_APPEND | LOCK_EX))
  110.             throw new IOException('Log: Cannot write to log file.');
  111.         $this->entries = array();
  112.     }
  113.    
  114.    
  115.     /**
  116.      * Pokusí se zapsat všechny existující logy.
  117.      */
  118.     static public function writeAll()   {
  119.         foreach (self::$instances as $log)
  120.             $log->write();
  121.     }
  122.    
  123.    
  124.     /**
  125.      * Nastaví cestu k adresáři s logy. Ověří, jestli je adresářem a zapisovatelný.
  126.      * @param string $path *Absolutní cesta* k adresáři.
  127.      * @throws FileNotFoundException Pokud název nezadán, nebo není adresář.
  128.      * @throws IOException Pokud adresář není zapisovatelný.
  129.      * @return Log fluent interface
  130.      */
  131.     public function setLogDir($path)    {
  132.         if (empty($path) || !is_dir($path))
  133.             throw new FileNotFoundException('Bad filename given to Log::setLogDir (not a directory or empty string).');
  134.         if (!is_writable($path))
  135.             throw new IOException('Log::setLogDir: given directory is not writable.');
  136.            
  137.         $this->path = rtrim($path,'/');
  138.         return $this;
  139.     }
  140.    
  141.    
  142.     /**
  143.      * Nastaví limit pro velikost logu.
  144.      * @param string $size Buď přesná velikost textově (1k, 5M, 3MB,...), nebo 0 (=bez limitu).
  145.      * @throws InvalidArgumentException Pokud byla zadaná neplatná hodnota.
  146.      * @return Log fluent interface.
  147.      */
  148.     public function setMaxLogSize($size)    {
  149.         if ($size == 0)
  150.             return $this;
  151.         if (!preg_match('~^(?P<digit>\d+)\s*(?P<unit>\w*)$~', trim($size), $matches))
  152.             throw new InvalidArgumentException('Bad max filesize given to Log::setMaxLogSize. Proper format is a decimal number optionally followed by "k","kb","m" or "mb" (case insensitive).');
  153.            
  154.         switch (strtolower($matches['unit']))   {
  155.             case 'k':
  156.             case 'kb':
  157.                 $this->limit = $matches['digit'] * pow(2,10);
  158.                 break;
  159.             case 'm':
  160.             case 'mb':
  161.                 $this->limit = $matches['digit'] * pow(2,20);
  162.                 break;
  163.             default:
  164.                 $this->limit = $matches['digit'];
  165.         }
  166.         return $this;
  167.     }
  168.    
  169.    
  170.     /**
  171.      * Nastavuje masku pro jméno souboru logu, kde "<date>" bude nahrazeno datumem archivace.
  172.      *  Výchozí je <date>.log. Pro aktivní (současný) soubor logu se <date> nahradí "current".
  173.      * @param string $mask Smí obsahovat pouze alfanum.znaky, pomlčku, podtržítko, tečku a vlnovku.
  174.      * @throws InvalidArgumentException Pokud maska pro název souboru obsahuje nepovolené znaky.
  175.      * @return Log fluent interface
  176.      */
  177.     public function setLogName($mask)   {
  178.         if (empty($mask))
  179.             return $this;
  180.         if (!preg_match('/^[a-z0-9_.~-]*<date>[a-z0-9_.~-]*$/i', $mask))
  181.             throw new InvalidArgumentException('Bad filename given to Log::setLogName. Filename mask must contain "<date>" and only "a-z0-9_.~-" are allowed.');
  182.         $this->logNameMask = $mask;
  183.         return $this;
  184.     }
  185.  
  186.    
  187.     /**
  188.      * Zformátuje zprávu do podoby používané v logu.
  189.      * @param string $msg Zpráva
  190.      * @param string $debug='' Volitelný ladící výpis
  191.      * @return string
  192.      *
  193.      * TBD Vyhazovat CR/LF z ladícího výpisu nebo ne?
  194.      */
  195.     private function format($msg, $debug='')    {
  196.         /* $debug = empty($debug) ? '' : preg_replace("~[\r\n]+~", ';', $debug); */
  197.         $line = date('Y-m-d H:i:s')."\t$msg\t$debug";
  198.         return $line;
  199.     }
  200.    
  201.    
  202.     /**
  203.      * Zjistí aktuální velikost souboru logu (v bytech).
  204.      * @return int
  205.      */
  206.     private function getLogSize() {
  207.         clearstatcache();
  208.         if (file_exists($this->getFilename()))
  209.             return filesize($this->getFilename());
  210.         else
  211.             return 0;
  212.     }
  213.    
  214.    
  215.     /**
  216.      * Zjistí, jestli jiné vláknu neprovádí archivaci logu (= existuje soubor zámku).
  217.      * Automaticky maže zámky starší než 1 hodina.
  218.      * @return bool
  219.      */
  220.     private function isLocked() {
  221.         clearstatcache();
  222.         $this->removeOldLocks();
  223.         return file_exists($this->path . '/' . $this->lockName);
  224.     }
  225.    
  226.    
  227.     /**
  228.      * Nastaví zámek
  229.      */
  230.     private function lock() {
  231.         file_put_contents($this->path . '/' . $this->lockName, "\n");
  232.     }
  233.    
  234.    
  235.     /**
  236.      * Zruší zámek
  237.      * @throws IOException Pokud nebylo možné smazat zámek.
  238.      */
  239.     private function unlock() {
  240.         $filename = $this->path . '/' . $this->lockName;
  241.         if (file_exists($filename))     {
  242.             if (!unlink($filename)) {
  243.                 $error = error_get_last();
  244.                 throw new IOException('Log: deleting current lock failed. Error: '.$error['message']);
  245.             }
  246.         }
  247.     }
  248.    
  249.    
  250.     /**
  251.      * Zruší staré soubory zámku (starší než 1hod)
  252.      * @throws IOException Pokud nebylo možné smazat starý zámek.
  253.      */
  254.     private function removeOldLocks()   {
  255.         $filename = $this->path . '/' . $this->lockName;
  256.         if (file_exists($filename))     {
  257.             if (filemtime($filename) < time()-3600) {  
  258.                 if (!unlink($filename)) {
  259.                     $error = error_get_last();
  260.                     throw new IOException('Log: deleting old lock failed. Error: '.$error['message']);
  261.                 }
  262.             }
  263.         }
  264.     }
  265.    
  266.    
  267.     /**
  268.      * Zjistí, jestli není potřeba archivovat log.
  269.      * @return bool
  270.      */
  271.     private function logLimitReached() {
  272.         if ($this->limit == 0)
  273.             return false;
  274.         return ($this->getLogSize() > $this->limit);
  275.     }
  276.    
  277.    
  278.     /**
  279.      * Pokud je to potřeba, zálohuje aktuální log pod aktuálním datumem, a current.log zkrátí na "\n".
  280.      * Pokud právě probíhá záloha na jiném vlákně, ostatní vlákna čekají na uvolnění.
  281.      * @throws IOException Pokud se nedá vytvořit záloha logu.
  282.      */
  283.     private function tryBackup() {
  284.         if ($this->logLimitReached())   {
  285.             if ($this->isLocked())  {
  286.                 $maxWaitTime = 5; /* jak dlouho nejdele smi cekat na odblokování (sec) */
  287.                 for ($stopTime = time() + $maxWaitTime; !$this->isLocked() || time() < $stopTime; usleep(300))  {   }
  288.             }
  289.             else {
  290.                 $this->lock();
  291.                 if (!copy($this->getFilename(), $this->makeBackupFilename()))   {
  292.                     $this->unlock();
  293.                     throw new IOException('Log: Backup of current log failed.');
  294.                 }
  295.                 file_put_contents($this->getFilename(), "\n");
  296.                 $this->unlock();
  297.             }
  298.         }
  299.     }
  300.    
  301.    
  302.     /**
  303.      * Vytvoří cestu k záložnímu souboru logu podle masky.
  304.      * Pokud už existuje, vytvoří nový s náhodným sufixem.
  305.      * @return string
  306.      */
  307.     private function makeBackupFilename()   {
  308.         $filename = $this->path . '/' . str_replace('<date>', date('Y-m-d_H-i-s'), $this->logNameMask);
  309.         clearstatcache();
  310.         if (file_exists($filename))
  311.             $filename = $this->path . '/' . str_replace('<date>', date('Y-m-d_H-i-s').'_'.rand(1000,9999), $this->logNameMask);
  312.         return $filename;
  313.     }
  314.    
  315.    
  316.     /**
  317.      * Vytvoří cestu k aktuálnímu souboru logu podle masky.
  318.      * @return string
  319.      */
  320.     private function getFilename()  {
  321.         return $this->path . '/' . str_replace('<date>', 'current', $this->logNameMask);
  322.     }
  323.    
  324.    
  325. }
Advertisement
Add Comment
Please, Sign In to add comment