#!/usr/bin/perl -w
use strict;

# Retrieve the 95'th percentile based on reqtime from the apache access long since the last poll
# period (max 1 hour though)

if ($< != 0) {
	die("This script must be run as root\n");
}

use Time::Piece;
use Time::Seconds;
use IPC::Open2;
use Getopt::Long;
use File::Basename;
use Cwd qw/abs_path/;
use JSON::XS;

my ($journal_mode,$percentile,$instance_name,$site_identifier,$state_file_dir) = validate_options();

my $state_file = $state_file_dir . '/';
if ($instance_name) {
	$state_file .= '_oca_' . $instance_name;
	if ($site_identifier) {
		$state_file .= '_' . $site_identifier;
	}
} elsif ($site_identifier) {
	$state_file .= '_siteid_' . $site_identifier;
} else {
	$state_file .= '_all';
}

my $base_path = abs_path(dirname($0)) . '/';

my @parser_args = (
	\*POUT,
	\*PIN,
	$base_path . 'oca-accesslog-percentile', #TODO - this script probably should belong to oca-server-config
	'--percentile',$percentile,
	'--stat','reqtime',
);

if ($site_identifier) {
	push @parser_args,'--site-identifier',$site_identifier
}

my $parser_pid  = open2(@parser_args);

if ($journal_mode) {
	
	$state_file .= "_journal";

	my @journal_args = (
		\*JNLOUT,
		undef,
		$base_path . 'oca-requesttimes-metric-journalctl', #TODO - replace this with Trav's new version of journalctl
		'@all',
		'--since=-1hr',
	   	'-o', 'cat',
	   	'--identifier=http-access',
		'--bookmark=' . $state_file
	);
	
	my $journal_pid = open2(@journal_args);

	#this step should not be necessary, but I can't figure out how to pass JNLOUT as the input file descriptor for the parser
	while (<JNLOUT>) {
		print PIN $_;
	}
	
} else {

	$state_file .= "_accesslogs";
	
	my $files = get_access_logs($state_file);

	my $min_time = localtime;
	$min_time -= ONE_HOUR;

	foreach my $file(@$files) {

		if (! open FH, '<', $file->{name}) {
			next;
		}

		seek FH, $file->{seek}, 0;

		while (<FH>) {

			#trim the string to the first closing ]. This is presumed to be the timestamp position
			my $up_to_time = substr($_,0,index($_,']')+1);

			if ($site_identifier) {
				#performance improvement, no time parsing required if up to the time doesn't include the site identifier
				next if index($up_to_time,$site_identifier) == -1;
			}

			#this is significantly faster than a regex, but this only works if none of the items before the timestamp
			#have optional spaces
			my @parts = split(' ',$up_to_time);

			if (exists $parts[3] && exists $parts[4]) {

				#Time::Piece::strptime will exit the script if it has bad data
				#If the session cookie contains a space, this occurs
				eval {
					my $line_time = Time::Piece->strptime($parts[3] . ' ' . $parts[4],'[%d/%b/%Y:%H:%M:%S %z]');

					if ($line_time && $line_time >= $min_time) {
						print PIN $_;
					}

					1;
				}
			}
		}

		$file->{seek} = tell FH;

		close FH;
	}

	write_state_file_access_logs($state_file,$files);
}

close PIN;
print <POUT>;

exit(0);

sub write_state_file_access_logs {
	my $state_file = shift;
	my $files = shift;
	
	open FH, '>', $state_file or die("Could not write state file");

	print FH encode_json $files;

	close FH;
}

sub get_access_logs {

	my $state_file = shift;

	my $files = [];

	my $now = localtime;
	my $add_current = 1;

	my $log_dir = '/var/log/httpd';
	my $main_log = $log_dir . '/access_log';

	if (-e $state_file) {

		#do a stat of every file in /var/log/httpd
		my $possibles = {};

		opendir(my $dh,$log_dir);
		while (readdir $dh) {
			next if m/\.gz$/ or ! m/^access_log/g;

			my @stat = stat($log_dir . '/' . $_);

			$possibles->{$stat[0] . '-' . $stat[1]} = [$log_dir . '/' . $_,@stat];
		}

		if (open FH, '<', $state_file) {
			my $json = '';
			while (<FH>) {
				$json .= $_;
			}
			close FH;

			my $stored_state = decode_json $json;

			foreach my $stored_file(@$stored_state) {

				my $key = $stored_file->{dev} . '-' . $stored_file->{inode};

				next unless exists($possibles->{$key});

				if ($possibles->{$key}->[0] eq $main_log) {
					$add_current = 0;
				}

				#Note, that all the 'stat' indexes are increased by 1 so this IS referring to the size of the possible file
				if ($stored_file->{seek} > $possibles->{$key}->[8]) {
					#file has been truncated, set the seek back to 0
					$stored_file->{seek} = 0;
				}
				
				#the file might have been renamed due to log rotation
				$stored_file->{name} = $possibles->{$key}->[0];

				push @$files, $stored_file;
			}

		}
	}

	if ($add_current) {

		my $file = {
			name => $main_log,
			seek => 0
		};

		#TODO - should this print the 'zabbix not supported' indicator?'
		if (! -e $file->{name}) {
			exit(1);
		}

		my @stat = stat($file->{name});

		$file->{dev}   = $stat[0];
		$file->{inode} = $stat[1];

		push @$files,$file;
	}

	return $files;

}

sub validate_options {

	my $instance_name;
	my $percentile = 95;
	my $def_state_file_dir = '/var/lib/zabbix-agent/oca-requesttimes-metric/';
	my $state_file_dir = $def_state_file_dir;
	my $journal_mode;
	my $site_identifier;
	my $show_help;


	if (
		not GetOptions('oca:s' => \$instance_name, 'site-identifier=s' => \$site_identifier, 'percentile=i' => \$percentile, "state-file-dir:s" => \$state_file_dir, 'journal-mode=i' => \$journal_mode, 'help' => \$show_help)
		or $show_help
	) {

		my $FH;

		if ($show_help) {
			$FH = *STDOUT;
		} else {
			$FH = *STDERR;
		}

		print $FH "\n\n";
		print $FH "Usage\n";
		print $FH "$0 --oca 'oca name' [--state_file_dir /path/to/store/state/files]\n";
		print $FH "--oca (string)             : The OCA instance to report for, eg oca_im1. This can be not specified or set to an empty string in which case all request times are analyzed\n";
		print $FH "--site-identifier (string) : The raw site identifier to report. This defaults to the value for oca with oca_ stripped off\n";
		print $FH "--percentile (int)         : The percentile to report. Defaults to 95\n";
		print $FH "--state_file_dir (string)  : The path to store the state files in. Defaults to $def_state_file_dir\n";
		print $FH "--journal-mode 1|0         : Wether to use journal mode or not. If not specified, it sets to true if /var/log/journal/remote exists\n";
		print $FH "\n\n";

		exit($show_help ? 0 : 1);
	}

	#Don't check the user exists. It is possible when in journal mode, that the
	#journals may be being pushed to a server that does not have the account users
	#eg a db only server
	if ($instance_name && $instance_name !~ /^oca_[-_\w]+$/) {
			die("Invalid instance name passed for --oca");
	}

	if ($percentile < 1 || $percentile > 99) {
		die("percentile must be between 1 and 99 (inclusive)");
	}

	if ($site_identifier && $site_identifier !~ /^[\w_-]+$/) {
		die("Invalid characters for --site-identifier");
	}

	#remove trailing /
	$state_file_dir =~ s/\/$//;

	if (! -d $state_file_dir || ! -W $state_file_dir) {
		die("$state_file_dir either does not exist or is not writeable\n");
	}

	if (! $site_identifier && $instance_name) {
		$site_identifier = $instance_name;
		$site_identifier =~ s/^oca_//;
	}

	if ((not defined $journal_mode) && -d '/var/log/journal/remote') {
		$journal_mode = 1;
	}

	return ($journal_mode,$percentile,$instance_name,$site_identifier,$state_file_dir);
}
