#!/usr/bin/perl -w
# 
# ***** BEGIN LICENSE BLOCK *****
# Zimbra Collaboration Suite Server
# Copyright (C) 2007, 2008, 2009, 2010, 2013, 2014, 2015, 2016 Synacor, Inc.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software Foundation,
# version 2 of the License.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>.
# ***** END LICENSE BLOCK *****
# 

# Periodically print CPU stats obtained from /proc/stat.
# The first line of /proc/stat output, "cpu", gives total CPU times since boot.
# The ensuing "cpuN" lines report the same info per CPU.
# Seven numbers are reported on each CPU line:
#
#   user
#   nice
#   system
#   idle
#   iowait
#   irq
#   softirq
#
# This script converts these into %util since last time slice.
#
# On the Mac, only user, system and idle are reported, using the output of the
# iostat program.
#

use strict;
use Getopt::Long;
use lib "/opt/zimbra/common/lib/perl5";
use Zimbra::Mon::Zmstat;
use Zimbra::Mon::Logger;

zmstatInit();

my $STAT = '/proc/stat';
my @FIELDS = ('user', 'nice', 'sys', 'idle', 'iowait', 'irq', 'softirq');
my $NUM_CPUS;
my @PREV_VALS;
my $isMac = isMac();

my ($CONSOLE, $LOGFH, $LOGFILE, $HEADING, $ROTATE_DEFER, $ROTATE_NOW);

sub getHeading() {
    my $heading = 'timestamp';
    if ($isMac) {
        return "$heading, cpu:user, cpu:sys, cpu:idle";
    }
    my $i;
    for ($i = 0; $i <= $NUM_CPUS; $i++) {
        my $cpu = $i == 0 ? 'cpu' : sprintf("cpu%d", $i - 1);
        my $j;
        for ($j = 0; $j < scalar(@FIELDS); $j++) {
            my $col = "$cpu:" . $FIELDS[$j];
            $heading .= ", $col";
        }
    }
    return $heading;
}

sub init() {
    if ($isMac) {
        $NUM_CPUS = 1;
        @PREV_VALS = ();
        return;
    }
    my $cpus = 0;
    my $line;
    open(STAT, "< $STAT") or die "Can't open $STAT: $!";
    while (defined($line = <STAT>)) {
        chomp($line);
        last if ($line !~ /^cpu/);
        my @fields = split(/\s+/, $line);
        if ($fields[0] =~ /cpu(\d+)/) {
            $cpus++;
        }
    }
    close(STAT);
    $NUM_CPUS = $cpus;
    @PREV_VALS = ();
    my $i;
    for ($i = 0; $i <= $NUM_CPUS; $i++) {
        # total, user, nice, system, idle, iowait, irq, softirq
        push(@PREV_VALS, 0, 0, 0, 0, 0, 0, 0, 0);
    }
}

sub getCpuStat() {
    my @utils;
    my $line;
    my $cpu = 0;
    open(STAT, "< $STAT") or die "Can't open $STAT: $!";
    while (defined($line = <STAT>)) {
        chomp($line);
        last if ($line !~ /^cpu/);
        my @fields = split(/\s+/, $line);
        my $total = 0;
        my $i;
        for ($i = 1; $i <= 7; $i++) {
            $total += $fields[$i];
        }
        my $total_delta = $total - $PREV_VALS[$cpu];
        $PREV_VALS[$cpu] = $total;
        for ($i = 1; $i <= 7; $i++) {
            my $delta = $fields[$i] - $PREV_VALS[$cpu + $i];
            my $pct = percent($delta, $total_delta);
            push(@utils, $pct);
            $PREV_VALS[$cpu + $i] = $fields[$i];
        }
        $cpu += 8;
    }
    close(STAT);
    return join(', ', @utils);
}

sub runIOSTATMac($) {
    my $interval = shift;
    my $fh = new FileHandle;
    my $cmd = "/usr/sbin/iostat -d -C -K -w $interval";
    open($fh, "$cmd |") || die "Unable to execute command \"$cmd\": $!";
    # Skip the first 2 lines.
    readLine($fh, 1);  # device list
    readLine($fh, 1);  # iostat heading
    return $fh;
}

sub usage() {
    print STDERR <<_USAGE_;
Usage: zmstat-cpu [options]
Monitor CPU activity
-i, --interval: output a line every N seconds
-l, --log:      log file (default is /opt/zimbra/zmstat/cpu.csv)
-c, --console:  output to stdout

If logging to a file, rotation occurs when HUP signal is sent or when
date changes.  Current log is renamed to <dir>/YYYY-MM-DD/cpu.csv
and a new file is created.
_USAGE_
    exit(1);
}

sub sighup {
    if (!$CONSOLE) {
        if (!$ROTATE_DEFER) {
            $LOGFH = rotateLogFile($LOGFH, $LOGFILE, $HEADING);
        } else {
            $ROTATE_NOW = 1;
        }
    }
}



#
# main
#

$| = 1; # Flush immediately

my $interval = getZmstatInterval();
my $opts_good = GetOptions(
    'interval=i' => \$interval,
    'log=s' => \$LOGFILE,
    'console' => \$CONSOLE
    );
if (!$opts_good) {
    print STDERR "\n";
    usage();
}

if (!defined($LOGFILE) || $LOGFILE eq '') {
    $LOGFILE = getLogFilePath('cpu.csv');
} elsif ($LOGFILE eq '-') {
    $CONSOLE = 1;
}
if ($CONSOLE) {
    $LOGFILE = '-';
}

createPidFile('zmstat-cpu.pid');

$SIG{HUP} = \&sighup;

init();
$HEADING = getHeading();
$LOGFH = openLogFile($LOGFILE, $HEADING);

my $date = getDate();
waitUntilNiceRoundSecond($interval);

if (!$isMac) {
    while (1) {
        my $cpuinfo = getCpuStat();

        my $tstamp = getTstamp();
        my $currDate = getDate();
        if ($currDate ne $date) {
            $LOGFH = rotateLogFile($LOGFH, $LOGFILE, $HEADING, $date);
            $date = $currDate;
        }

        # Don't allow rotation in signal handler while we're writing.
        $ROTATE_DEFER = 1;
        $LOGFH->print("$tstamp, $cpuinfo\n");
        Zimbra::Mon::Logger::LogStats( "info", "zmstat cpu.csv: ${HEADING}:: $tstamp, $cpuinfo"); 
        $LOGFH->flush();
        $ROTATE_DEFER = 0;
        if ($ROTATE_NOW) {
            # Signal handler delegated rotation to main.
            $ROTATE_NOW = 0;
            $LOGFH = rotateLogFile($LOGFH, $LOGFILE, $HEADING);
        }

        sleep($interval);
    }
} else {
    my $iostatFH = runIOSTATMac($interval);
    while (1) {
        my $line;
        while (!defined($line = readLine($iostatFH, 1))) {
            # Restart iostat if it got killed for some reason.
            waitUntilNiceRoundSecond($interval);
            close($iostatFH);
            $iostatFH = runIOSTATMac($interval);
        }
        # Skip the two-line heading Mac iostat prints every minute.
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        while ($line !~ /^\d/) {
            $line = readLine($iostatFH, 1);
            $line =~ s/^\s+//;
            $line =~ s/\s+$//;
        }

        my $tstamp = getTstamp();
        my $currDate = getDate();
        if ($currDate ne $date) {
            $LOGFH = rotateLogFile($LOGFH, $LOGFILE, $HEADING, $date);
            $date = $currDate;
        }

        my @cols = split(/\s+/, $line);
        my $len = scalar(@cols);
        if ($len < 3) {
            next;
        }
        # Last 3 elements are user/sys/idle CPU.
        my @vals = ($cols[$len - 3] / 100,
                    $cols[$len - 2] / 100,
                    $cols[$len - 1] / 100);

        # Don't allow rotation in signal handler while we're writing.
        $ROTATE_DEFER = 1;
        my $values = join(', ', @vals);
        $LOGFH->print("$tstamp, $values\n");
        Zimbra::Mon::Logger::LogStats( "info", "zmstat-cpu: ${HEADING}:: $tstamp, $values"); 
        $LOGFH->flush();
        $ROTATE_DEFER = 0;
        if ($ROTATE_NOW) {
            # Signal handler delegated rotation to main.
            $ROTATE_NOW = 0;
            $LOGFH = rotateLogFile($LOGFH, $LOGFILE, $HEADING);
        }
    }
    close($iostatFH);
}
close($LOGFH);
