Mini Shell

Direktori : /opt/sharedrads/
Upload File :
Current File : //opt/sharedrads/recent-cp

#!/usr/bin/perl
# encoding: utf-8
#
# author: Kyle Yetter <kyley@inmotionhosting.com>
# date: November 16, 2011
#

# TODO: Comment/document code more cleanly


=pod


=head1 recent-cp

recent-cp - Summarize cpu usage over the past hour

=head1 SYNOPSIS

recent-cp [-1|-5|-15|-60] [userna5]

=head1 DESCRIPTION

Summarize cpu usage over the past hour.

=head1 OPTIONS

=over 8

=item B<-1>, B<-5>, B<-15>, B<-60>

Extract the top users during the last 1 minute, 5 minutes, 15 minutes, or 60 minutes (respectively).

=item B<-n> I<COUNT>, B<--top>=I<COUNT>

Limit the results to the top I<COUNT> users/commands (Default: 10)

=item B<-p>, B<--percentages>

Show values only as percentages of the total for each interval

=item B<-s>, B<--secs>

Show only the cpu second counts for each interval and do not show the percentage of the total.

=item B<-o> I<HOURS>, B<--hours-ago>=I<HOURS>

Run the sampling starting I<HOURS> ago instead of right now. This can be a decimal number (e.g. 0.5 for a half hour ago).

=item B<-b>, B<--bleach>

Do not use ANSI color code escapes in the output.

=item B<--man>

Show full man page documentation for this program.

=item B<-h>, B<--help>

You know the drill

=item B<-v>, B<--version>

Ditto

=back

=head1 EXAMPLES

=head2 Getting Recent Top CPU Users

    $ recent-cp
      +----------+-----------+-----------+-----------+-----------+
      |   user   |    1m     |    5m     |    15m    |    60m    |
      +----------+-----------+-----------+-----------+-----------+
      | p2fitn5  |     0.36s |     3.49s |    11.22s |    47.85s |
      | gomanu5  |     0.00s |     3.84s |    19.60s |    55.53s |
      | chilet5  |     0.76s |     4.45s |    12.37s |    43.86s |
      | coloni6  |     0.43s |     4.79s |    22.77s |   186.82s |
      | camera11 |     0.91s |     5.86s |     5.86s |    70.36s |
      | staysu5  |     0.00s |     8.98s |    15.52s |    15.83s |
      | battle8  |     1.12s |    10.68s |    17.18s |    46.07s |
      | aztecs6  |     1.25s |    12.33s |    36.80s |   172.05s |
      | root     |     1.17s |    34.56s |    68.39s |  1004.76s |
      | phenom5  |     0.00s |    45.20s |    52.23s |   104.42s |
      | ohhowp5  |     6.78s |    81.60s |   190.70s |   719.87s |
      +----------+-----------+-----------+-----------+-----------+

=head2 Getting CPU Usage By Command Name For a User

    $ recent-cp root
      +-----------------+-----------+-----------+-----------+-----------+
      |     command     |    1m     |    5m     |    15m    |    60m    |
      +-----------------+-----------+-----------+-----------+-----------+
      | top             |     0.00s |     0.11s |     0.34s |     1.40s |
      | pgrep           |     0.03s |     0.13s |     0.39s |     1.54s |
      | dcpumon         |     0.00s |     0.38s |     1.26s |     5.19s |
      | ps              |     0.00s |     0.49s |     1.71s |     7.96s |
      | ls              |     0.14s |     0.69s |     2.09s |     9.08s |
      | python          |     0.14s |     0.90s |     2.76s |    11.35s |
      | couriertls      |     0.07s |     1.11s |     3.91s |    18.62s |
      | sa              |     0.52s |     3.18s |     8.50s |    40.47s |
      | ftp_clamscan.ph |     0.12s |    10.06s |    29.72s |   104.83s |
      | lastcomm        |     0.00s |    10.76s |    10.76s |    20.93s |
      | perl-bin        |     0.01s |    16.81s |    21.80s |   785.29s |
      +-----------------+-----------+-----------+-----------+-----------+

=cut


use Getopt::Long;
use Date::Parse;
use Term::ANSIColor;
use Pod::Usage;
use POSIX qw( strftime );
use constant (
  MAX_OFF_HITS => 200
);

our $VERSION     = 1.1;

# the time at the start of execution of the script
our $now         = time;
# will contain a username if the command distribution of the user is requested
our $target_user = undef;
# seconds ago in the past at which we will start the one hour window
our $offset      = 0;

our $bleach      = !-t STDOUT;

# the characters of the progress bar in sequence
our @progress_glyphs    = qw( | / - \\ | / - \\ );
# tracks the current progress character to display
our $progress_glyph     = 0;
# update the display every 200 steps
our $progress_frequency = 1000;
# increments each update and refreshes the display when @progress_frequency is reached
our $progress_steps     = 0;

# the number of top users / commands to display in the output table
our $number_of_entries = 10;

# display percentage of CPU for each interval
our $show_percentage = 1;
# display cpu seconds for each interval
our $show_secs       = 1;

# tracks the user/command totals over the last one minute
our %last_1;
# tracks the user/command totals over the last five minutes
our %last_5;
# tracks the user/command totals over the last fifteen minutes
our %last_15;
# tracks the user/command totals over the last hour
our %last_60;
our @maps = ( \%last_1, \%last_5, \%last_15, \%last_60 );

# the number of the data set to use to determine the top users to display
#   five minutes by default
our $sort_index = 1;

our $lastcomm_options = '';

our %special_users = (
  root => 1,
  nobody => 1,
  mailnull => 1,
  mysql => 1
);

####################################################################################################
################################## Parse Options / Setup Variables #################################
####################################################################################################

GetOptions(
  '1'       => sub { $sort_index = 0; },
  '5'       => sub { $sort_index = 1; },
  '15'      => sub { $sort_index = 2; },
  '60'      => sub { $sort_index = 3; },

  'n|top=i' => \$number_of_entries,

  'h|help'  => sub { pod2usage( 0 ); },

  'man' => sub {
    pod2usage({
      -exitval => 0,
      -verbose => 2,
      -noperldoc => 1
    });
  },

  'b|bleach' => \$bleach,

  'p|percentages' => sub {
    $show_percentage = 1;
    $show_secs = 0;
  },

  's|secs' => sub {
    $show_percentage = 0;
    $show_secs = 1;
  },

  'o|hours-ago=f' => \$offset,

  'v|version' => sub {
    print( "$VERSION\n" );
    exit( 0 );
  }
);


$offset *= 3600;

our $thresh_1    = $now - 60   - $offset;
our $thresh_5    = $now - 300  - $offset;
our $thresh_15   = $now - 900  - $offset;
our $thresh_60   = $now - 3600 - $offset;
our $start       = $now - $offset;


#
# contains the daily summary details extract from sa
#
our %sa_data;

sub window_size {
  my @size = ( $ENV{'COLUMNS'} || 80, $ENV{'LINES'} || 22 );
  my $tiocgwinsz = 0x5413;

  eval {
    my $data = '';
    if ( ioctl( STDERR, $tiocgwinsz, $data ) >= 0 ) {
      my ( $height, $width ) = unpack( "SSSS", $data );
      $size[ 1 ] = $height if $height >= 0;
      $size[ 0 ] = $width  if $width  >= 0;
    }
  };
  return @size;
}

our ( $screen_width, undef ) = window_size();


sub shell_escape( $ ) {
  my $token = shift;
  if ( length( $token ) == 0 ) { return "''"; }
  $token =~ s/([^A-Za-z0-9_\-\.,:\/@\n])/\\$1/g;
  $token =~ s/\n/'\n'/g;
  return $token;
}

sub c($$) {
  my ( $str, $style ) = @_;
  $str = colored( $str, $style ) unless $bleach;
  return $str;
}

sub bleach($) {
  my ( $colored ) = @_;
  $colored =~ s(\033\[.*?m)()g;
  return $colored;
}

sub clen($) {
  return length( bleach( $_[ 0 ] ) );
}

sub ljust($$) {
  my ( $string, $width ) = @_;
  my $len = clen( $string );
  return $string if $width <= $len;

  my $padding = $width - $len;

  return( $string . ( ' ' x $padding ) );
}

sub center($$) {
  my ( $string, $width ) = @_;
  my $len = clen( $string );
  return $string if $width <= $len;

  my $padding = $width - $len;
  my $left = int( $padding / 2 );
  my $right = $left + $padding % 2;

  return( ( ' ' x $left ) . $string . ( ' ' x $right ) );
}

sub print_table {
  local $\ = "\n";

  my @rows = @_;
  my @arrays;
  my @titles;
  my @header_row;
  my $title = undef;

  for my $item ( @rows ) {
    if ( ref( $item ) eq 'ARRAY' ) {
      push( @arrays, $item );
    } elsif ( ref( \$item ) eq "SCALAR" ) {
      push( @titles, $item );
    }
  }

  unless ( @arrays ) {
    $@ = "print_table: no rows provided";
    return;
  }

  my $ncols = scalar( @{$arrays[ 0 ]} );
  my @widths;

  for my $c ( 0 .. ( $ncols - 1 ) ) {
    my $w = 0;
    for my $row ( @arrays ) {
      my $col = $row->[$c];
      my $col_width = clen( $col );
      if ( $w < $col_width ) { $w = $col_width; }
    }
    $widths[ $c ] = $w;
  }

  my $inner_width = ( $ncols - 1 ) * 3;
  $inner_width += $_ for @widths;

  if ( @titles ) {
    my $title_width = 0;
    for my $t ( @titles ) {
      my $l = clen( $t );
      $title_width = $l if $l > $title_width;
    }

    if ( $title_width > $inner_width ) {
      $widths[-1] += $title_width - $inner_width;
      $inner_width = $title_width;
    }
  }

  if ( ref( \$rows[0] ) eq "SCALAR" ) {
    $title = shift( @rows );

    print( '+-' . ( '-' x $inner_width ) . '-+' );
    print( '| ' . center( $title, $inner_width ) . ' |' );
  }

  my $mask    = '| ' . join( " | ", map { "%-${_}s" } @widths ) . " |";
  my $border  = '+-' . join( "-+-", map { '-' x $_  } @widths ) . "-+";
  my $outline = '+-' . ( '-' x $inner_width ) . '-+';

  my $after_title = 1;


  print( $border );
  my @title_row = @{ shift( @rows ) };
  print( '| ' . join( ' | ', map { center( $title_row[ $_ ], $widths[ $_ ] ) } ( 0 ... $#title_row ) ) . ' |' );
  print( $border );
  $after_title = 0;

  for my $row ( @rows ) {
    if ( ref( $row ) eq 'ARRAY' ) {
      print( $border ) if $after_title;
      print( '| ' . ( join( ' | ', map { ljust( $row->[ $_ ], $widths[ $_ ] ); } (0 ... $#widths) ) ) . ' |' );
      #print( sprintf( $mask, @{ $row } ) );
      $after_title = 0;
    } elsif ( ref( \$row ) eq 'SCALAR' ) {
      if ( $row eq '-' ) {
        print( $after_title ? $outline : $border );
        $after_title = 0;
      } else {
        print( $after_title ? $outline : $border );
        print( '| ' . center( $row, $inner_width ) . ' |' );
        $after_title = 1;
      }
    }
  }

  print( $after_title ? $outline : $border );
}

sub update_progress( $ ) {
  local $| = 1;
  if ( $progress_steps == 0 ) {
    my $label = shift;
    my $line  = sprintf( "%s   %s", $progress_glyphs[ $progress_glyph ], $label );

    my $n = length( $line );
    if ( $n > $screen_width ) {
      $line = substr( $line, 0, $screen_width - 4 ) . '...';
    }

    print STDERR "\r\e[2K";   # clears the line
    print STDERR $line;
  }

  $progress_steps = ( $progress_steps + 1 ) % $progress_frequency;
  $progress_glyph = ( $progress_glyph + 1 ) % $#progress_glyphs;
}



####################################################################################################
########################################### Script Action ##########################################
####################################################################################################

if ( $target_user = shift( @ARGV ) ) {
  $lastcomm_options = "--user " . shell_escape( $target_user );
}


#
# [1] Pull the lastcomm output for the timeframe of interest. Stop and close the pipe
#     once it seems that we have confidently left the timeframe, i.e. when $off_hits > 200.
#

open( LC, "lastcomm $lastcomm_options |" );

my $off_hits = 0;
while ( <LC> ) {
  chomp;
  update_progress( $_ );

  if ( /^\s*(\S+).*?([\w\-]+)\s+\S+\s+(\d+(?:\.\d+)?)\s+secs\s+(.+)$/ ) {
    my ( $name, $user, $secs, $time ) = ( $1, $2, $3, str2time( $4 ) );

    if ( $time > $start ) { next; }

    if ( $time > $thresh_60 ) {
      $off_hits = 0;
    } else {
      $off_hits++;
      if ( $off_hits > MAX_OFF_HITS ) { last; }
    }

    if ( $secs == 0.0 ) { $secs += 0.001; }

    my $key = $target_user ? $name : $user;

    if ( $time > $thresh_1  ) { $last_1{ $key  } += $secs; }
    if ( $time > $thresh_5  ) { $last_5{ $key  } += $secs; }
    if ( $time > $thresh_15 ) { $last_15{ $key } += $secs; }
    if ( $time > $thresh_60 ) { $last_60{ $key } += $secs; }
  }
}
close( LC );

# clear the progress line display to prepare for printing the table
{
  local $| = 1;
  print "\r\e[2K";
}

#
# calculate the total cp secs for each interval and store it in the
#   @totals array
#

for my $map ( @maps ) {
  my $total = 0;
  $total += $_ for values( %$map );
  push @totals, $total;
}

#
# Extract the top N users from the time range selected by $sort_index
#
our %sort_table   = %{ $maps[ $sort_index ] };
our @top_listings;

if ( $target_user ) {
  @top_listings = keys( %last_60 );
} else {
  @top_listings = keys( %sort_table );
}

@top_listings   = ( sort { ($sort_table{ $a } || 0) <=> ($sort_table{ $b } || 0) } @top_listings );
my $start_index = $#top_listings - $number_of_entries;
$start_index < 0 and $start_index = 0;
@top_listings  = @top_listings[ $start_index ... $#top_listings ];


#
# Harvest daily summary details from sa for the selected top entries
#

if ( $target_user ) {
  our @sa_users = ( $target_user );
} else {
  our @sa_users = @top_listings;
}

{
  my $target_rx = join( "|", map { lc( quotemeta ) } @sa_users );
  $target_rx    = qr<^($target_rx)\s+>i;

  open( SA, "sa -acm |" );
  # skip over the "total sum" line
  my ( $rank, $matched ) = ( 0, 0 );

READ_SA:
  while ( <SA> ) {
    chomp;

    if ( /^\s*\d+/ ) {
      $rank = 0;
      my $line_user = ':total';
      my @fields = split;
      $sa_data{$line_user} = {
        rank       => $rank,
        cpu        => $fields[ 4 ],
        percentage => $fields[ 5 ]
      };

    } elsif ( /$target_rx/ ) {
      $matched++;

      my $line_user = $1;
      my @fields = split;

      my $cpu = 0.0;
      $fields[ 5 ] =~ /(\d*(?:\.\d*)?)cp/i and $cpu += $1;

      $sa_data{$line_user} = {
        rank       => $rank,
        cpu        => $cpu,
        percentage => $fields[ 6 ]
      };
      if ( @sa_users == $matched ) { last READ_SA; }
    }

    $rank++;
  }

  close( SA );

};



my @stat_table;
if ( $target_user ) {
  @stat_table = ( [ qw( command 1m 5m 15m 60m ) ] );
} else {
  @stat_table = ( [ qw( user 1m 5m 15m 60m today  ) ] );
}

my $sort_column = $stat_table[0]->[ $sort_index + 1 ];
$stat_table[ 0 ]->[ $sort_index + 1 ] = "\e[4m$sort_column\e[0m";

our $make_cell;
if ( $show_percentage && $show_secs ) {
  $make_cell = sub {
    my ( $usage, $total ) = @_;
    my $percent = ( $total > 0 ) ? ( 100.0 * $usage / $total ) : 0;
    return sprintf( '%8.2fs %5.1f%%', $usage, $percent );
  };
} elsif ( $show_percentage ) {
  $make_cell = sub {
    my ( $usage, $total ) = @_;
    my $percent = ( $total > 0 ) ? ( 100.0 * $usage / $total ) : 0;
    return sprintf( '%5.1f%%', $percent );
  };
} elsif ( $show_secs ) {
  $make_cell = sub {
    my ( $usage, $total ) = @_;
    return sprintf( '%8.2fs', $usage );
  };
}

#
# This is where the table is constructed
#
for my $top_listing ( @top_listings ) {
  my @row = (
    $top_listing,
    map { $make_cell->( $maps[ $_ ]->{$top_listing}, $totals[ $_ ] ); } ( 0 ... $#maps )
  );

  unless ( $target_user ) {
    my $sa  = $sa_data{$top_listing};
    my $cpu = $sa->{cpu};

    push( @row, sprintf( '%8.1fcp %7s',  $sa->{cpu}, $sa->{percentage} ) );

    my $color = undef;
    if ( $special_users{ $top_listing } ) {
      $color = 'cyan';
    } elsif ( $cpu > 40 ) {
      $color = 'red';
    } elsif ( $cpu > 30 ) {
      $color = 'magenta';
    } elsif ( $cpu > 20 ) {
      $color = 'yellow';
    }

    if ( $cpu ) {
      @row = map { c( $_, $color ) } @row;
    }
  }

  push( @stat_table, \@row );
}

push @stat_table, '-';

my @total_row = ( "total", map { $make_cell->( $_, $_ ); } @totals );
unless( $target_user ) {
  my $sa_total = $sa_data{':total'};
  push( @total_row, sprintf( '%10s %7s',  $sa_total->{cpu}, $sa_total->{percentage} ) );
}
push @stat_table, \@total_row;

print_table( @stat_table );
print( "s = processs user time in cpu seconds, cp = user time + system time in cpu minutes\n" );
unless ( $target_user || $bleach ) {
  print( c( "user within 20cp-30cp for the day, ", 'yellow' ) );
  print( c( "user within 30cp-40cp for the day", 'magenta' ) . "\n" );
  print( c( "user over 40cp for the day, ", 'red' ) );
  print( c( "special users (root, mailnull, mysql)", 'cyan' ) . "\n" );
}

Zerion Mini Shell 1.0