|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace ShortPixelWeb; |
| 4 | + |
| 5 | +class PhpTail { |
| 6 | + |
| 7 | + /** |
| 8 | + * Location of the log file we're tailing |
| 9 | + * @var string |
| 10 | + */ |
| 11 | + private $log = ""; |
| 12 | + /** |
| 13 | + * The time between AJAX requests to the server. |
| 14 | + * |
| 15 | + * Setting this value too high with an extremly fast-filling log will cause your PHP application to hang. |
| 16 | + * @var integer |
| 17 | + */ |
| 18 | + private $updateTime; |
| 19 | + /** |
| 20 | + * This variable holds the maximum amount of bytes this application can load into memory (in bytes). |
| 21 | + * @var string |
| 22 | + */ |
| 23 | + private $maxSizeToLoad; |
| 24 | + /** |
| 25 | + * |
| 26 | + * PHPTail constructor |
| 27 | + * @param string $log the location of the log file |
| 28 | + * @param integer $defaultUpdateTime The time between AJAX requests to the server. |
| 29 | + * @param integer $maxSizeToLoad This variable holds the maximum amount of bytes this application can load into memory (in bytes). Default is 2 Megabyte = 2097152 byte |
| 30 | + */ |
| 31 | + public function __construct($log, $defaultUpdateTime = 2000, $maxSizeToLoad = 2097152) { |
| 32 | + $this->log = is_array($log) ? $log : array($log); |
| 33 | + $this->updateTime = $defaultUpdateTime; |
| 34 | + $this->maxSizeToLoad = $maxSizeToLoad; |
| 35 | + } |
| 36 | + /** |
| 37 | + * This function is in charge of retrieving the latest lines from the log file |
| 38 | + * @param string $lastFetchedSize The size of the file when we lasted tailed it. |
| 39 | + * @param string $grepKeyword The grep keyword. This will only return rows that contain this word |
| 40 | + * @return Returns the JSON representation of the latest file size and appended lines. |
| 41 | + */ |
| 42 | + public function getNewLines($file, $lastFetchedSize, $grepKeyword, $invert) { |
| 43 | + |
| 44 | + /** |
| 45 | + * Clear the stat cache to get the latest results |
| 46 | + */ |
| 47 | + clearstatcache(); |
| 48 | + /** |
| 49 | + * Define how much we should load from the log file |
| 50 | + * @var |
| 51 | + */ |
| 52 | + if(empty($file)) { |
| 53 | + $file = key(array_slice($this->log, 0, 1, true)); |
| 54 | + } |
| 55 | + |
| 56 | + $fsize = file_exists($this->log[$file]) ? filesize($this->log[$file]) : -1; |
| 57 | + $maxLength = ($fsize - $lastFetchedSize); |
| 58 | + /** |
| 59 | + * Verify that we don't load more data then allowed. |
| 60 | + */ |
| 61 | + if($maxLength > $this->maxSizeToLoad) { |
| 62 | + $maxLength = ($this->maxSizeToLoad / 2); |
| 63 | + } |
| 64 | + /** |
| 65 | + * Actually load the data |
| 66 | + */ |
| 67 | + $data = array(); |
| 68 | + if($maxLength > 0) { |
| 69 | + |
| 70 | + $fp = fopen($this->log[$file], 'r'); |
| 71 | + fseek($fp, -$maxLength , SEEK_END); |
| 72 | + $data = explode("\n", fread($fp, $maxLength)); |
| 73 | + |
| 74 | + } |
| 75 | + /** |
| 76 | + * Run the grep function to return only the lines we're interested in. |
| 77 | + */ |
| 78 | + if($invert == 0) { |
| 79 | + $data = preg_grep("/$grepKeyword/",$data); |
| 80 | + } |
| 81 | + else { |
| 82 | + $data = preg_grep("/$grepKeyword/",$data, PREG_GREP_INVERT); |
| 83 | + } |
| 84 | + /** |
| 85 | + * If the last entry in the array is an empty string lets remove it. |
| 86 | + */ |
| 87 | + if(end($data) == "") { |
| 88 | + array_pop($data); |
| 89 | + } |
| 90 | + return json_encode(array("size" => $fsize, "file" => $this->log[$file], "data" => $data)); |
| 91 | + } |
| 92 | + /** |
| 93 | + * This function will print out the required HTML/CSS/JS |
| 94 | + */ |
| 95 | + public function generateGUI() { |
| 96 | + ?> |
| 97 | + <!DOCTYPE html> |
| 98 | + <html lang="en"> |
| 99 | + <head> |
| 100 | + <meta charset="utf-8"> |
| 101 | + <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| 102 | + <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 103 | + <title>Bootstrap 101 Template</title> |
| 104 | + |
| 105 | + <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> |
| 106 | + <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> |
| 107 | + <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/themes/smoothness/jquery-ui.css" /> |
| 108 | + |
| 109 | + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> |
| 110 | + <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> |
| 111 | + <!--[if lt IE 9]> |
| 112 | + <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> |
| 113 | + <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> |
| 114 | + <![endif]--> |
| 115 | + |
| 116 | + <style type="text/css"> |
| 117 | + #grepKeyword, #settings { |
| 118 | + font-size: 80%; |
| 119 | + } |
| 120 | + .float { |
| 121 | + background: white; |
| 122 | + border-bottom: 1px solid black; |
| 123 | + padding: 10px 0 10px 0; |
| 124 | + margin: 0px; |
| 125 | + height: 30px; |
| 126 | + width: 100%; |
| 127 | + text-align: left; |
| 128 | + } |
| 129 | + .contents { |
| 130 | + margin-top: 30px; |
| 131 | + } |
| 132 | + .results { |
| 133 | + padding: 40px 10px 20px; |
| 134 | + font-family: monospace; |
| 135 | + font-size: small; |
| 136 | + white-space: pre; |
| 137 | + } |
| 138 | + </style> |
| 139 | + |
| 140 | + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> |
| 141 | + <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script> |
| 142 | + |
| 143 | + <script type="text/javascript"> |
| 144 | + /* <![CDATA[ */ |
| 145 | + //Last know size of the file |
| 146 | + lastSize = 0; |
| 147 | + //Grep keyword |
| 148 | + grep = ""; |
| 149 | + //Should the Grep be inverted? |
| 150 | + invert = 0; |
| 151 | + //Last known document height |
| 152 | + documentHeight = 0; |
| 153 | + //Last known scroll position |
| 154 | + scrollPosition = 0; |
| 155 | + //Should we scroll to the bottom? |
| 156 | + scroll = true; |
| 157 | + lastFile = window.location.hash != "" ? window.location.hash.substr(1) : ""; |
| 158 | + console.log(lastFile); |
| 159 | + $(document).ready(function() { |
| 160 | + // Setup the settings dialog |
| 161 | + $("#settings").dialog({ |
| 162 | + modal : true, |
| 163 | + resizable : false, |
| 164 | + draggable : false, |
| 165 | + autoOpen : false, |
| 166 | + width : 590, |
| 167 | + height : 270, |
| 168 | + buttons : { |
| 169 | + Close : function() { |
| 170 | + $(this).dialog("close"); |
| 171 | + } |
| 172 | + }, |
| 173 | + open : function(event, ui) { |
| 174 | + scrollToBottom(); |
| 175 | + }, |
| 176 | + close : function(event, ui) { |
| 177 | + grep = $("#grep").val(); |
| 178 | + invert = $('#invert input:radio:checked').val(); |
| 179 | + $("#results").text(""); |
| 180 | + lastSize = 0; |
| 181 | + $("#grepspan").html("Grep keyword: \"" + grep + "\""); |
| 182 | + $("#invertspan").html("Inverted: " + (invert == 1 ? 'true' : 'false')); |
| 183 | + } |
| 184 | + }); |
| 185 | + //Close the settings dialog after a user hits enter in the textarea |
| 186 | + $('#grep').keyup(function(e) { |
| 187 | + if (e.keyCode == 13) { |
| 188 | + $("#settings").dialog('close'); |
| 189 | + } |
| 190 | + }); |
| 191 | + //Focus on the textarea |
| 192 | + $("#grep").focus(); |
| 193 | + //Settings button into a nice looking button with a theme |
| 194 | + //Settings button opens the settings dialog |
| 195 | + $("#grepKeyword").click(function() { |
| 196 | + $("#settings").dialog('open'); |
| 197 | + $("#grepKeyword").removeClass('ui-state-focus'); |
| 198 | + }); |
| 199 | + $(".file").click(function(e) { |
| 200 | + $("#results").text(""); |
| 201 | + lastSize = 0; |
| 202 | + console.log(e); |
| 203 | + lastFile = $(e.target).attr('href').substr(1); |
| 204 | + }); |
| 205 | + |
| 206 | + //Set up an interval for updating the log. Change updateTime in the PHPTail contstructor to change this |
| 207 | + setInterval("updateLog()", <?php echo $this->updateTime; ?>); |
| 208 | + //Some window scroll event to keep the menu at the top |
| 209 | + $(window).scroll(function(e) { |
| 210 | + if ($(window).scrollTop() > 0) { |
| 211 | + $('.float').css({ |
| 212 | + position : 'fixed', |
| 213 | + top : '0', |
| 214 | + left : 'auto' |
| 215 | + }); |
| 216 | + } else { |
| 217 | + $('.float').css({ |
| 218 | + position : 'static' |
| 219 | + }); |
| 220 | + } |
| 221 | + }); |
| 222 | + //If window is resized should we scroll to the bottom? |
| 223 | + $(window).resize(function() { |
| 224 | + if (scroll) { |
| 225 | + scrollToBottom(); |
| 226 | + } |
| 227 | + }); |
| 228 | + //Handle if the window should be scrolled down or not |
| 229 | + $(window).scroll(function() { |
| 230 | + documentHeight = $(document).height(); |
| 231 | + scrollPosition = $(window).height() + $(window).scrollTop(); |
| 232 | + if (documentHeight <= scrollPosition) { |
| 233 | + scroll = true; |
| 234 | + } else { |
| 235 | + scroll = false; |
| 236 | + } |
| 237 | + }); |
| 238 | + scrollToBottom(); |
| 239 | + |
| 240 | + }); |
| 241 | + //This function scrolls to the bottom |
| 242 | + function scrollToBottom() { |
| 243 | + $("html, body").animate({scrollTop: $(document).height()}, "fast"); |
| 244 | + } |
| 245 | + //This function queries the server for updates. |
| 246 | + function updateLog() { |
| 247 | + $.getJSON('?ajax=1&file=' + lastFile + '&lastsize=' + lastSize + '&grep=' + grep + '&invert=' + invert, function(data) { |
| 248 | + lastSize = data.size; |
| 249 | + $("#current").text(data.file); |
| 250 | + $.each(data.data, function(key, value) { |
| 251 | + $("#results").append('' + value + '<br/>'); |
| 252 | + }); |
| 253 | + if (scroll) { |
| 254 | + scrollToBottom(); |
| 255 | + } |
| 256 | + }); |
| 257 | + } |
| 258 | + /* ]]> */ |
| 259 | + </script> |
| 260 | + </head> |
| 261 | + <body> |
| 262 | + <div class="navbar navbar-default navbar-fixed-top" role="navigation"> |
| 263 | + <div class="container"> |
| 264 | + <div class="navbar-header"> |
| 265 | + <a class="navbar-brand" href="#">PHP Tail</a> |
| 266 | + </div> |
| 267 | + <div class="collapse navbar-collapse"> |
| 268 | + <ul class="nav navbar-nav"> |
| 269 | + <li class="dropdown"> |
| 270 | + <a href="#" class="dropdown-toggle" data-toggle="dropdown">Files<span class="caret"></span></a> |
| 271 | + <ul class="dropdown-menu" role="menu"> |
| 272 | + <?php foreach ($this->log as $title => $f): ?> |
| 273 | + <li><a class="file" href="#<?php echo $f;?>"><?php echo $title;?></a></li> |
| 274 | + <?php endforeach;?> |
| 275 | + </ul> |
| 276 | + </li> |
| 277 | + <li><a href="#" id="grepKeyword">Settings</a></li> |
| 278 | + <li><span class="navbar-text" id="grepspan"></span></li> |
| 279 | + <li><span class="navbar-text" id="invertspan"></span></li> |
| 280 | + </ul> |
| 281 | + <p class="navbar-text navbar-right" id="current"></p> |
| 282 | + </div> |
| 283 | + </div> |
| 284 | + </div> |
| 285 | + <div class="contents"> |
| 286 | + <div id="results" class="results"></div> |
| 287 | + <div id="settings" title="PHPTail settings"> |
| 288 | + <p>Grep keyword (return results that contain this keyword)</p> |
| 289 | + <input id="grep" type="text" value="" /> |
| 290 | + <p>Should the grep keyword be inverted? (Return results that do NOT contain the keyword)</p> |
| 291 | + <div id="invert"> |
| 292 | + <input type="radio" value="1" id="invert1" name="invert" /><label for="invert1">Yes</label> |
| 293 | + <input type="radio" value="0" id="invert2" name="invert" checked="checked" /><label for="invert2">No</label> |
| 294 | + </div> |
| 295 | + </div> |
| 296 | + </div> |
| 297 | + <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> |
| 298 | + </body> |
| 299 | + </html> |
| 300 | + <?php |
| 301 | + } |
| 302 | +} |
| 303 | + |
| 304 | + |
0 commit comments