Note that this is not an exact science, maybe with changes to the log format it can be refined but you will likely need to get a VM as NFO probably will not change that.
Any requests from a unique IP in the last 3 seconds is considered a Concurrent User. Multiple users behind a Natted private address will only show as 1 Concurrent User.
Even so, unless your website markets to large corporations this should be fairly accurate!
This script also has a built in search function allowing you see dig through the connection logs and see whats hitting your site.
(IPs in the image below are all web-crawlers)
You can do alot more with this like reverse-DNSing the IP to see who is hitting your webserver and other stuff so feel free to add to it! Cheers!
Add this concurrent_users.php script to your main directory (inside Public), if you put it somewhere else make sure to point file('../access_log') at the top of the script to the right place. If you don't know PHP ../ mean up 1 directory.
Code: Select all
<?php
$log_file = file('../access_log');
$ip_list = array();
$now = strtotime("NOW");
$concurrent_users = 0;
foreach($log_file as $key => $data){ // for each line in the log
$data = str_replace("\n", "", $data);
$temp = explode(" ", $data); // Array by spaces
$website = $temp[0]; // first non-space chunk is the website accessed
$Connecting_ip = $temp[1]; // IP is next non-space chunk
if(!isset($ip_list[$Connecting_ip])){
$ip_list[$Connecting_ip] = array(); // Builds an array by IP
}
array_push($ip_list[$Connecting_ip], Array()); // Creates a new array for each connection request by this IP.
end($ip_list[$Connecting_ip]); // move the internal pointer to the end of the array
$key = key($ip_list[$Connecting_ip]); // fetches the key of the element pointed to by the internal pointer
$ip_list[$Connecting_ip][$key]['website'] = $website;
preg_match('/\d{1,2}\/\w*\/\d{4}:\d{2}:\d{2}:\d{2}\s-\d+/', $data, $access_time); // DateTime Regex
$ip_list[$Connecting_ip][$key]['access_time'] = $access_time[0];
$string_details = substr($data, (strpos($data, '"')+1)); // The rest of the line minus the starting "
$request_type = explode(" ", $string_details);
$ip_list[$Connecting_ip][$key]['request_type'] = $request_type[0]; // Ok first part of this is the request method
$ip_list[$Connecting_ip][$key]['file_accesses'] = $request_type[1]; // Next is the directory requested
$ip_list[$Connecting_ip][$key]['protocol_used'] = substr($request_type[2], 0, (strlen($request_type[2])-1)); //then the protocol used minus the "
$ip_list[$Connecting_ip][$key]['return_code'] = $request_type[3]; // HTTP return code
$ip_list[$Connecting_ip][$key]['response_size'] = $request_type[4]; // response size
$client_information = ''; // Just lump the rest of the exploded array into a string
for($id=6; $id<=count($request_type); $id++){
$client_information .= $request_type[$id];
}
$ip_list[$Connecting_ip][$key]['client_information'] = str_replace('"', '', $client_information); // The end of the string is client information, we do not want Quotes
if(strlen($access_time[0]) > 0){
$time = ($now - strtotime($access_time[0])); // time between request and now
if($time <= 3 && !isset($ip_list[$Connecting_ip]['active_user'])){// PHP does not track miliseconds so, anything 3 seconds or less ago is considered a "Concurrent user", no not perfect but good enough for general ideas
$concurrent_users++;
$ip_list[$Connecting_ip]['active_user'] = 1;
}
$ip_list[$Connecting_ip][$key]['time_since_connect'] = $time;
}
}
echo '<h1> Concurrent Users: '.$concurrent_users.'</h1>';
// ----------------------------------------------- IF YOU have a very large access_log.txt file, the load times for this script can become quite long, if so i would delete everything from here down and just use the $concurrent_users varable.
$http_codes = array( // https://gist.github.com/henriquemoody/6580488
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', // WebDAV; RFC 2518
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information', // since HTTP/1.1
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status', // WebDAV; RFC 4918
208 => 'Already Reported', // WebDAV; RFC 5842
226 => 'IM Used', // RFC 3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other', // since HTTP/1.1
304 => 'Not Modified',
305 => 'Use Proxy', // since HTTP/1.1
306 => 'Switch Proxy',
307 => 'Temporary Redirect', // since HTTP/1.1
308 => 'Permanent Redirect', // approved as experimental RFC
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC 2324
419 => 'Authentication Timeout', // not in RFC 2616
420 => 'Enhance Your Calm', // Twitter
420 => 'Method Failure', // Spring Framework
422 => 'Unprocessable Entity', // WebDAV; RFC 4918
423 => 'Locked', // WebDAV; RFC 4918
424 => 'Failed Dependency', // WebDAV; RFC 4918
424 => 'Method Failure', // WebDAV)
425 => 'Unordered Collection', // Internet draft
426 => 'Upgrade Required', // RFC 2817
428 => 'Precondition Required', // RFC 6585
429 => 'Too Many Requests', // RFC 6585
431 => 'Request Header Fields Too Large', // RFC 6585
444 => 'No Response', // Nginx
449 => 'Retry With', // Microsoft
450 => 'Blocked by Windows Parental Controls', // Microsoft
451 => 'Redirect', // Microsoft
451 => 'Unavailable For Legal Reasons', // Internet draft
494 => 'Request Header Too Large', // Nginx
495 => 'Cert Error', // Nginx
496 => 'No Cert', // Nginx
497 => 'HTTP to HTTPS', // Nginx
499 => 'Client Closed Request', // Nginx
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates', // RFC 2295
507 => 'Insufficient Storage', // WebDAV; RFC 4918
508 => 'Loop Detected', // WebDAV; RFC 5842
509 => 'Bandwidth Limit Exceeded', // Apache bw/limited extension
510 => 'Not Extended', // RFC 2774
511 => 'Network Authentication Required', // RFC 6585
598 => 'Network read timeout error', // Unknown
599 => 'Network connect timeout error' // Unknown
);
$table_header = '<table border="2" ID="Log_table"><thead bgcolor="gray"><tr><td style="width:10%"><strong><center>Connecting IP</center></strong></td><td style="width:10%"><strong><center>Time Since</center></strong></td><td style="width:30%"><strong><center>File Accessed</center></strong></td><td style="width:10%"><strong><center>Return Code</center></strong></td><td style="width:10%"><strong><center>Protocol Used</center></strong></td><td style="width:10%"><strong><center>Website</center></strong></td><td style="width:10%"><strong><center>Response Size</center></strong></td><td style="width:10%"><strong><center>Client Info</center></strong></td></thead></tr>';
$table_body = '';
function humanTiming ($time) // ulx time to human readable time
{
$time = ($time<1)? 1 : $time;
$tokens = array (
31536000 => 'year',
2592000 => 'month',
604800 => 'week',
86400 => 'day',
3600 => 'hour',
60 => 'minute',
1 => 'second'
);
foreach ($tokens as $unit => $text) {
if ($time < $unit) continue;
$numberOfUnits = floor($time / $unit);
return $numberOfUnits.' '.$text.(($numberOfUnits>1)?'s':'');
}
}
?>
<script type="text/javascript">
function makeTableScroll() {
var maxRows = 4;
var table = document.getElementById('Log_table');
var wrapper = table.parentNode;
var rowsInTable = table.rows.length;
var height = 0;
if (rowsInTable > maxRows) {
for (var i = 0; i < maxRows; i++) {
height += table.rows[i].clientHeight;
}
wrapper.style.height = height + "px";
}
}
</script>
<style>
* {
box-sizing: border-box;
}
#myInput {
background-image: url('');
background-position: 10px 10px;
background-repeat: no-repeat;
width: 100%;
font-size: 16px;
padding: 12px 20px 12px 40px;
border: 1px solid #ddd;
margin-bottom: 12px;
}
</style>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search Access List" autofocus>
<?php
foreach($ip_list as $key => $ip){
foreach($ip as $key2 => $value){
if(is_numeric($key2)){
if($value['file_accesses'] == '/'){
$value['file_accesses'] = 'root'; // saying root is nicer then /
}
$table_body .= '<tr><td>'.$key.'</td><td>'.humanTiming($value['time_since_connect']).' ago</td><td>'.$value['file_accesses'].'</td><td>'.$value['return_code'].' - '.$http_codes[$value['return_code']].'</td><td>'.$value['protocol_used'].'</td><td>'.$value['website'].'</td><td>'.$value['response_size'].'</td><td>'.$value['client_information'].'</td></tr>';
}
}
}
$table_body .= '</table>';
echo $table_header.$table_body;
//echo '<pre>' , var_dump($ip_list), '</pre>'; // uncomment if you want to see all the data.
?>
<script>
function myFunction() {
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("myInput");
filter = input.value.toUpperCase();
table = document.getElementById("Log_table");
tr = table.getElementsByTagName("tr");
for (i = 1; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td");
if (td) {
pass = 0;
for (e = 1; e < td.length; e++) {
txtValue = td[e].textContent || td[e].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
pass = 1;
}
}
if (pass == 1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
</script>