summaryrefslogtreecommitdiff
path: root/src/iostat2pcp
diff options
context:
space:
mode:
authorIgor Pashev <pashev.igor@gmail.com>2014-10-26 12:33:50 +0400
committerIgor Pashev <pashev.igor@gmail.com>2014-10-26 12:33:50 +0400
commit47e6e7c84f008a53061e661f31ae96629bc694ef (patch)
tree648a07f3b5b9d67ce19b0fd72e8caa1175c98f1a /src/iostat2pcp
downloadpcp-debian.tar.gz
Debian 3.9.10debian/3.9.10debian
Diffstat (limited to 'src/iostat2pcp')
-rw-r--r--src/iostat2pcp/GNUmakefile43
-rw-r--r--src/iostat2pcp/README123
-rwxr-xr-xsrc/iostat2pcp/iostat2pcp859
3 files changed, 1025 insertions, 0 deletions
diff --git a/src/iostat2pcp/GNUmakefile b/src/iostat2pcp/GNUmakefile
new file mode 100644
index 0000000..a79ac5a
--- /dev/null
+++ b/src/iostat2pcp/GNUmakefile
@@ -0,0 +1,43 @@
+#!gmake
+#
+# Copyright (c) 2010 Ken McDonell. All Rights Reserved.
+# Copyright (c) 2009 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+#
+# 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; either version 2 of the License, or (at your
+# option) any later version.
+#
+# 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.
+#
+
+TOPDIR = ../..
+include $(TOPDIR)/src/include/builddefs
+
+SCRIPT = iostat2pcp
+LDIRT = $(MAN_PAGES) $(MAN_PAGES).tmp
+LSRCFILES = $(SCRIPT) README
+
+ifneq ($(POD2MAN),)
+MAN_SECTION = 1
+MAN_PAGES = $(SCRIPT).$(MAN_SECTION)
+MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION)
+endif
+
+default: $(MAN_PAGES)
+
+$(SCRIPT).$(MAN_SECTION): $(SCRIPT)
+ $(POD_MAKERULE)
+
+include $(BUILDRULES)
+
+install: default
+ $(INSTALL) -m 755 $(SCRIPT) $(PCP_BIN_DIR)/$(SCRIPT)
+ @$(INSTALL_MAN)
+
+default_pcp : default
+
+install_pcp : install
diff --git a/src/iostat2pcp/README b/src/iostat2pcp/README
new file mode 100644
index 0000000..6fde585
--- /dev/null
+++ b/src/iostat2pcp/README
@@ -0,0 +1,123 @@
+Converting a iostat output to a PCP archive
+
+This example uses the PCP::LogImport Perl wrapper around the libpcp_import
+library to convert a sadc datafile into a PCP archive.
+
+The version of iostat that is supported here is the one from
+http://freshmeat.net/projects/sysstat and provides the following
+reporting options:
+
+-t Add timestamps like 27/07/10 12:47:34 ahead of each sample.
+ If $S_TIME_FORMAT=ISO in the environment, then the format changes
+ to 2010-07-27T12:46:07+1000
+
+ The default is to not include any timestamps.
+
+ Start date is on first line
+ Linux 2.6.32-23-generic (bozo) 27/07/10 _i686_ (1 CPU)
+
+-z supress activity for idle devices
+
+-k Disk activity in Kilobytes not blocks
+ Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
+
+-m Disk activity in megabytes not blocks
+ Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
+
+-c CPU utilization
+ avg-cpu: %user %nice %system %iowait %steal %idle
+ 75.00 0.00 25.00 0.00 0.00 0.00
+
+-d Disk activity
+ Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn
+ sda 9.27 42.38 135.10 128 408
+ sdb 83.44 182.78 1634.44 552 4936
+ scd0 0.00 0.00 0.00 0 0
+
+-n NFS report
+ Filesystem: rBlk_nor/s wBlk_nor/s rBlk_dir/s wBlk_dir/s rBlk_svr/s wBlk_svr/s rops/s wops/s
+
+-x Extended disk activity
+ Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
+ sda 0.00 10.30 0.00 4.32 0.00 116.94 27.08 0.00 0.31 0.31 0.13
+ sdb 0.00 83.39 0.33 10.63 2.66 754.82 69.09 0.05 4.36 0.85 0.93
+ scd0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
+
+-p like -d but for partitions (example below is with -z)
+ Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn
+ sda 2.99 0.00 77.08 0 232
+ sda1 2.99 0.00 77.08 0 232
+ sdb 20.60 2.66 890.37 8 2680
+ sdb6 20.60 2.66 890.37 8 2680
+
+Usage: iostat2pcp infile archive
+
+The translation currently supports the following PCP metrics:
+ disk.all.read
+ disk.all.read_bytes
+ disk.all.total
+ disk.all.total_bytes
+ disk.all.write
+ disk.all.write_bytes
+ disk.dev.avactive
+ disk.dev.read_bytes
+ disk.dev.total
+ disk.dev.total_bytes
+ disk.dev.write_bytes
+ kernel.all.cpu.idle
+ kernel.all.cpu.intr
+ kernel.all.cpu.nice
+ kernel.all.cpu.sys
+ kernel.all.cpu.user
+ kernel.all.cpu.wait.total
+ kernel.all.intr
+ kernel.all.load
+ kernel.all.pswitch
+ kernel.percpu.cpu.idle
+ kernel.percpu.cpu.intr
+ kernel.percpu.cpu.nice
+ kernel.percpu.cpu.sys
+ kernel.percpu.cpu.user
+ kernel.percpu.cpu.wait.total
+ mem.util.bufmem
+ mem.util.cached
+ mem.util.free
+ mem.util.swapCached
+ mem.util.swapFree
+ mem.util.used
+ mem.vmstat.pgfault
+ mem.vmstat.pgfree
+ mem.vmstat.pgmajfault
+ mem.vmstat.pgpgin
+ mem.vmstat.pgpgout
+ network.interface.collisions
+ network.interface.in.bytes
+ network.interface.in.drops
+ network.interface.in.errors
+ network.interface.in.fifo
+ network.interface.in.frame
+ network.interface.in.packets
+ network.interface.out.bytes
+ network.interface.out.carrier
+ network.interface.out.drops
+ network.interface.out.errors
+ network.interface.out.fifo
+ network.interface.out.packets
+ proc.runq.runnable
+ swap.pagesin
+ swap.pagesout
+ vfs.dentry.count
+ vfs.files.count
+ vfs.inodes.count
+
+This is sufficient to support the following standard pmchart views:
+ CPU
+ Disk
+ Diskbytes
+ Loadavg
+ Memory
+ Netbytes
+ Netpackets
+ Overview (except for a lesser memory stats)
+ Paging
+
diff --git a/src/iostat2pcp/iostat2pcp b/src/iostat2pcp/iostat2pcp
new file mode 100755
index 0000000..91f1f3d
--- /dev/null
+++ b/src/iostat2pcp/iostat2pcp
@@ -0,0 +1,859 @@
+#!/usr/bin/perl
+#
+# Copyright (c) 2010 Ken McDonell. All Rights Reserved.
+#
+# 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; either version 2 of the License, or (at your
+# option) any later version.
+#
+# 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.
+#
+# Assumptions:
+# Except for line suppresion for idle devices with -z, output from
+# iostat is completely deterministic, with the number and order of
+# values identical for each sample interval.
+#
+# Since we don't know what sort of system the data came from
+# there is no point trying to match the PMIDs with those from
+# any particular OS PMDA. For the names we'll use the common
+# names where possible, else the Linux names (as the most likely
+# case).
+#
+
+use strict;
+use warnings;
+
+use Getopt::Std;
+use Date::Parse;
+use Date::Format;
+use PCP::LogImport;
+
+my $line = 0; # input line number
+my $basedate = undef;
+my $basetime = "00:00:00";
+my $stamp_style = 0;
+my $host = undef;
+my $zone = "UTC"; # default timezone
+ # unless -t & $S_TIME_FORMAT=ISO or -Z on command line
+my $first_tag = undef;
+my $sts;
+my %options; # for command line arguments
+my $sample = -1;
+my $interval = undef;
+my $in_dev = 0; # in Device: group
+my $dev_style; # "d" for standard -d Device: data, "x" for -x Device: data
+ # Note -x and -d are mutually exclusive to iostat
+my $in_cpu = 0; # in avg-cpu: group
+my $stamp; # timestamp from -t output lines
+my $now; # faked tv_sec of gettimeofday() for current sample
+my $next_stamp; # ready for next sample interval
+my $next_now;
+my $vflag; # -v for verbosity
+my @handle = (); # pmi* handles, one per metric-instance pair
+my $h = 0; # index into handle[]
+my $putsts = 0; # pmiPutValue() errors are only checked @ end of loop
+my $dev_thru_scale; # scale factor for disk thruput - 0.5 for blocks, 1 for
+ # kB and 1024 for MB
+my $dev_indom = pmInDom_build(PMI_DOMAIN, 0);
+my %dev_first_handle; # for each disk instance, index into handle[] for first
+ # handle, other handles for related metrics for the same
+ # instance follow consecutively in handle[]
+my %dev_prev; # previous values for each instance for disk.dev.* metrics
+my %dev_seen; # tracking disk instances seen in this sample for
+ # handling -z and missing lines for inactive devices
+my %inst_map = (); # key=indom value=last_inst_assigned, and
+ # key=indom.instance value=inst
+
+sub dodate($)
+{
+ # convert datetime formats 27/07/10 12:47:34 or
+ # 2013-07-05 09:17:28 or 2010-07-27T12:46:07+1000
+ # into ISO-8601 dates that Date::Parse
+ # seems to be able to parse correctly ... this would appear to
+ # have to be YYYY-MM-DDTHH:MM:SS.000000 and then pass the timezone
+ # as the second parameter to str2time()
+ #
+ my ($datetime) = @_;
+ my @fields;
+ my $mm;
+ my $yy;
+ my $dd;
+ my $time;
+ @fields = split(/T/, $datetime);
+ if ($#fields == 1) {
+ # ISO format - YYYY-MM-DDTHH:MM:SS[timezone]
+ $time = $fields[1];
+ $time =~ s/[+-].*//;
+ @fields = split(/-/, $fields[0]);
+ $#fields == 2 or die "dodate: bad datetime format: \"$datetime\"\n";
+ $yy = $fields[0];
+ $mm = $fields[1];
+ $dd = $fields[2];
+ }
+ else {
+ @fields = split(/\//, $datetime);
+ if ($#fields == 2) {
+ # DD/MM/YY HH:MM:SS format
+ @fields = split(/ /, $datetime);
+ $#fields == 1 or die "dodate: bad datetime format 1: \"$datetime\"\n";
+ $time = $fields[1];
+ @fields = split(/\//, $fields[0]);
+ $#fields == 2 or die "dodate: bad date format 1: \"$datetime\"\n";
+ $dd = $fields[0];
+ $mm = $fields[1];
+ $yy = $fields[2];
+ }
+ else {
+ @fields = split(/-/, $datetime);
+ if ($#fields == 2) {
+ # YYYY-MM-DD HH:MM:SS format
+ @fields = split(/ /, $datetime);
+ $#fields == 1 or die "dodate: bad datetime format 3: \"$datetime\"\n";
+ $time = $fields[1];
+ @fields = split(/-/, $fields[0]);
+ $#fields == 2 or die "dodate: bad date format 3: \"$datetime\"\n";
+ $yy = $fields[0];
+ $mm = $fields[1];
+ $dd = $fields[2];
+ }
+ }
+ }
+
+ if ($time !~ /\./) { $time .= ".000000" }
+
+ # get into canonical DD, MM and YYYY format
+ if ($dd < 10 && $dd !~ /^0/) { $dd .= "0" }; # add leading zero
+ if ($mm < 10 && $mm !~ /^0/) { $mm .= "0" }; # add leading zero
+ if ($yy < 100) {
+ # terrible Y2K hack ... will stop working in 2080
+ if ($yy <= 80) { $yy += 2000; }
+ else { $yy += 1900; }
+ }
+ return $yy . "-" . $mm . "-" . $dd . "T" . $time;
+}
+
+# for stamp_style 1 or 2 or 3, -S and -t options ignored
+# for stamp_style 2, -Z option ignored
+#
+sub check_opts()
+{
+ return if $stamp_style == 0;
+ if (exists($options{S})) {
+ print "iostat2pcp: Warning: timestamps found, -S $basetime option ignored\n";
+ }
+ if (exists($options{t})) {
+ print "iostat2pcp: Warning: timestamps found, -t $interval option ignored\n";
+ }
+ if ($stamp_style == 2 && exists($options{Z})) {
+ print "iostat2pcp: Warning: ISO timestamps found, -Z $zone option ignored\n";
+ }
+}
+
+# Handle metrics with the a singular value, calling pmiAddMetric() and
+# pmiGetHandle()
+#
+sub def_single($)
+{
+ my ($name) = @_;
+ my $sts;
+ my $units = pmiUnits(0,0,0,0,0,0);
+ my $type = PM_TYPE_FLOAT;
+ if (pmiAddMetric($name, PM_ID_NULL, $type, PM_INDOM_NULL, PM_SEM_INSTANT, $units) < 0) {
+ pmiDump();
+ die "pmiAddMetric($name, ...): " . pmiErrStr(-1) . "\n";
+ }
+ $sts = pmiGetHandle($name, "");
+ if ($sts < 0) {
+ pmiDump();
+ die "pmiGetHandle($name, ...): " . pmiErrStr($sts) . "\n";
+ }
+ push(@handle, $sts);
+}
+
+# Handle metrics with multiple values, calling pmiAddMetric().
+# Defer to pmiGetHandle() to def_metric_inst().
+#
+sub def_multi($$)
+{
+ my ($name,$indom) = @_;
+ my $units = pmiUnits(0,-1,1,0,PM_TIME_SEC,PM_COUNT_ONE);
+ my $type = PM_TYPE_FLOAT;
+ my $sem = PM_SEM_INSTANT;
+ if ($name eq "disk.dev.avactive") {
+ $units = pmiUnits(0,0,0,0,0,0);
+ }
+ elsif ($name =~ /disk\.dev\..*bytes/) {
+ $units = pmiUnits(1,-1,0,PM_SPACE_KBYTE,PM_TIME_SEC,0);
+ }
+ if (pmiAddMetric($name, PM_ID_NULL, $type, $indom, $sem, $units) < 0) {
+ pmiDump();
+ die "pmiAddMetric($name, ...): " . pmiErrStr(-1) . "\n";
+ }
+}
+
+# Deal with metric-instance pairs.
+# If first time this instance has been seen for this indom, add it to
+# the instance domain.
+# Get a handle and add it to handle[].
+#
+sub def_metric_inst($$$)
+{
+ my ($name,$indom,$instance) = @_;
+ my $sts;
+ # inst_map{} holds the last allocated inst number with $indom as the
+ # key, and marks the instance as known with $indom . $instance as the
+ # key
+ if (!exists($inst_map{$indom . $instance})) {
+ my $inst;
+ if (exists($inst_map{$indom})) {
+ $inst_map{$indom}++;
+ $inst = $inst_map{$indom};
+ }
+ else {
+ $inst_map{$indom} = 0;
+ $inst = 0;
+ }
+ if (pmiAddInstance($indom, $instance, $inst) < 0) {
+ pmiDump();
+ die "pmiAddInstance([$name], $instance, $inst): " . pmiErrStr(-1) . "\n";
+ }
+ $inst_map{$indom . $instance} = $inst;
+ }
+ $sts = pmiGetHandle($name, $instance);
+ if ($sts < 0) {
+ pmiDump();
+ die "pmiGetHandle($name, $instance): " . pmiErrStr($sts) . "\n";
+ }
+ push(@handle, $sts);
+}
+
+# wrapper for pmiPutValueHandle(), using @handle
+#
+sub put($)
+{
+ my ($value) = @_;
+ my $sts;
+ if (!exists($handle[$h])) {
+ pmiDump();
+ die <<EOF
+put($value): No handle[] entry for index $h.
+Check Handles in dump above.
+EOF
+ }
+ $sts = pmiPutValueHandle($handle[$h], $value);
+ if ($sts < 0 && $putsts == 0) { $putsts = $sts };
+ $h++;
+}
+
+# flush log record at end of sample interval
+#
+sub sample_done()
+{
+ # if -z is in play, then the Device: stats don't have values for
+ # those instances with no activity ... need to map this onto PCP data
+ # semantics, so if this instance has been seen at all, then for counters
+ # output the previous value (no activity) and for instantaneous/discrete
+ # metrics output zero (no activity).
+ #
+ # All the metrics we have so far instantaneous ...
+ #
+ foreach my $instance (keys %dev_seen) {
+ if ($dev_seen{$instance} == 0) {
+ next if !exists($dev_prev{$instance});
+ if (exists($dev_first_handle{$instance})) {
+ $h = $dev_first_handle{$instance};
+ }
+ else {
+ pmiDump();
+ print "[$line] $_\n";
+ die "Device: no first handle for instance \"" . $instance . "\", check Handles in dump above";
+ }
+ if ($dev_style eq "d") {
+ put(0); # total
+ put(0); # read_bytes
+ put(0); # write_bytes
+ }
+ else {
+ put(0); # read_merge
+ put(0); # write_merge
+ put(0); # read
+ put(0); # write
+ put(0); # read_bytes
+ put(0); # write_bytes
+ put(0); # avactive
+ }
+ }
+ else {
+ $dev_seen{$instance} = 0;
+ }
+ }
+ if ($putsts < 0) {
+ pmiDump();
+ die "pmiPutValue: Failed @ $stamp: " . pmiErrStr($putsts) . "\n";
+ }
+ if ($vflag) {
+ print "End of sample $sample";
+ if (defined($now) && defined($stamp)) { print " @ $now $stamp"; }
+ print "\n";
+ }
+ if (pmiWrite($now, 0) < 0) {
+ pmiDump();
+ die "pmiWrite: @ $stamp: " . pmiErrStr(-1) . "\n";
+ }
+ $h = 0;
+ $putsts = 0;
+}
+
+$sts = getopts('S:t:vZ:', \%options);
+
+if (!defined($sts) || $#ARGV != 1) {
+ print "Usage: iostat2pcp [-v] [-S start] [-t interval] [-Z timezone] infile outfile\n";
+ exit(1);
+}
+
+exists($options{S}) and $basetime = $options{S};
+exists($options{t}) and $interval = $options{t};
+exists($options{v}) and $vflag = 1;
+if (exists($options{Z})) {
+ $zone = $options{Z};
+ if ($zone !~ /^[-+][0-9][0-9][0-9][0-9]$/) {
+ print "iostat2pcp: Illegal -Z value, must be +NNNN or -NNNN\n";
+ exit(1);
+ }
+}
+
+pmiStart($ARGV[1], 0);
+
+open(INFILE, "<" . $ARGV[0])
+ or die "iostat2pcp: Failed to open infile \"$ARGV[0]\"\n";
+
+while (<INFILE>) {
+ my $end_sample = 0;
+ my $header = 0;
+ chomp;
+ $line++;
+ #debug# print "[" . $line . "] {" . $sample . "} $_\n";
+ if ($line == 1) {
+ # first line ... extract baseline date in format YYYY-MM-DD
+ # from something like ...
+ # Linux 2.6.32-23-generic (bozo) 27/07/10 _i686_ (1 CPU)
+ #
+ if (/.*\s+([^\s]+)\s+[0-3][0-9]\/[0-1][0-9]\/[0-9][0-9]\s+/) {
+ my @part;
+ $basedate = $_;
+ $basedate =~ s#.*([0-3][0-9]/[0-1][0-9]/[0-9][0-9]).*#$1#;
+ @part = split(/\//, $basedate);
+ # terrible Y2K hack ... will stop working in 2080
+ if ($part[2] <= 80) { $part[2] += 2000; }
+ else { $part[2] += 1900; }
+ $basedate = $part[2] . "-" . $part[1] . "-" . $part[0];
+ }
+ # or possibly like this when $S_TIME_FORMAT=ISO is set in the
+ # environment (ISO 8601) ...
+ # Linux 2.6.32-23-generic (bozo) 2010-07-27 _i686_ (1 CPU)
+ elsif (/.*\s+([^\s]+)\s+[12][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]\s+/) {
+ $basedate = $_;
+ $basedate =~ s#.*([12][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]).*#$1#;
+ }
+ # or possibly like this ...
+ # Linux 2.6.18-194.3.1.el5 (somehost.somewhere.com) 07/27/2010
+ elsif (/.*\s+([^\s]+)\s+[0-1][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]/) {
+ my @part;
+ $basedate = $_;
+ $basedate =~ s#.*([0-1][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]).*#$1#;
+ @part = split(/\//, $basedate);
+ $basedate = $part[2] . "-" . $part[0] . "-" . $part[1];
+ }
+ else {
+ print "[" . $line . "] $_\n";
+ print "iostat2pcp: First line does not look like iostat ... I give up\n";
+ exit(0);
+ }
+ $host = $_;
+ $host =~ s/[^(]*\(//;
+ $host =~ s/\).*//;
+ next;
+ }
+ elsif ($line == 3 && /[0-3][0-9]\/[0-1][0-9]\/[0-9][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/) {
+ # simple -t option timestamp
+ # 27/07/10 12:47:34
+ #
+ $stamp_style = 1;
+ check_opts();
+ next;
+ }
+ elsif ($line == 3 && /[1-2][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9][-+][0-9]+/) {
+
+ # -t option timestamp and $S_TIME_FORMAT=ISO set in the environment
+ # 2010-07-27T12:46:07+1000
+ #
+ $zone = $_;
+ $zone =~ s/.*([-+][0-9]+).*/$1/;
+ $stamp_style = 2;
+ check_opts();
+ next;
+ }
+ elsif ($line == 3 && /[1-2][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/) {
+
+ # -t option timestamp and ??? not sure ... but visible in
+ # https://bugzilla.redhat.com/show_bug.cgi?id=981545
+ # 2013-07-05 09:17:28
+ #
+ $stamp_style = 3;
+ check_opts();
+ next;
+ }
+ elsif ($line == 3) {
+ # first group tag
+ $first_tag = $_;
+ $first_tag =~ s/([^:]+):.*/$1/;
+ }
+
+ next if /^\s*$/;
+
+ if ($stamp_style == 0 && $line > 3) {
+ my $tag = $_;
+ $tag =~ s/([^:]+):.*/$1/;
+ if ($tag eq $first_tag) {
+ if ($sample > 0) {
+ # for sample #0, time is set below in the end_sample code
+ $next_now = $now + $interval;
+ $next_stamp = ctime($next_now, $zone);
+ chomp $next_stamp;
+ }
+ $end_sample = 1;
+ }
+ }
+ elsif ($stamp_style == 1) {
+ if (/[0-3][0-9]\/[0-1][0-9]\/[0-9][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/) {
+ $next_stamp = dodate($_);
+ $next_now = str2time($next_stamp, $zone);
+ $end_sample = 1;
+ }
+ }
+ elsif ($stamp_style == 2) {
+ if (/[1-2][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9][-+][0-9]+/) {
+ $next_stamp = dodate($_);
+ $next_now = str2time($next_stamp, $zone);
+ $end_sample = 1;
+ }
+ }
+ elsif ($stamp_style == 3) {
+ if (/[1-2][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/) {
+ $next_stamp = dodate($_);
+ $next_now = str2time($next_stamp, $zone);
+ $end_sample = 1;
+ }
+ }
+
+ if ($end_sample) {
+ if ($stamp_style == 0 && $sample == 0) {
+ print "iostat2pcp: Warning: no timestamps, assuming data starts at $basedate $basetime $zone\n";
+ $stamp = dodate($basedate . "T" . $basetime);
+ $now = str2time($stamp, $zone);
+ if (!defined($interval)) {
+ print "iostat2pcp: Warning: cannot determine sample interval, assuming 15 seconds\n";
+ $interval = 15;
+ }
+ $now += $interval;
+ $stamp = ctime($now, $zone);
+ chomp $stamp;
+ $next_now = $now + $interval;
+ $next_stamp = ctime($next_now, $zone);
+ chomp $next_stamp;
+ }
+ if ($vflag) {
+ if ($sample == 0) {
+ print "stamp_style=$stamp_style zone=$zone basedate=$basedate now=$now stamp=$stamp";
+ print " interval=$interval" if defined($interval);
+ print " first_tag=$first_tag" if defined($first_tag);
+ print "\n";
+ }
+ }
+
+ if ($sample > -1) {
+ if ($sample == 0) {
+ # Serious strangeness here ...
+ # the Perl Date::Parse and Date::Format routines appear to
+ # only work with timezones of the format +NNNN or -NNNN
+ # ("UTC" is an exception)
+ #
+ # PCP expects a $TZ style timezone in the archive label, so
+ # we have to make up a PCP-xx:xx timezone ... note this
+ # involves a sign reversal!
+ #
+ my $label_zone = $zone;
+ if ($zone =~ /^[-+][0-9][0-9][0-9][0-9]/) {
+ $label_zone =~ s/^\+/PCP-/;
+ $label_zone =~ s/^-/PCP+/;
+ $label_zone =~ s/(..)$/:$1/;
+ }
+ elsif ($zone ne "UTC") {
+ print "iostat2pcp: Warning: unexpected timezone ($zone), reverting to UTC\n";
+ $zone = "UTC";
+ $label_zone = "UTC";
+ }
+ pmiSetTimezone($label_zone) >= 0
+ or die "pmiSetTimezone($label_zone): " . pmiErrStr(-1) . "\n";
+
+ if (defined($host)) {
+ pmiSetHostname($host) >= 0
+ or die "pmiSetHostname($host): " . pmiErrStr(-1) . "\n";
+ }
+ }
+ sample_done();
+ }
+
+ $sample++;
+ $stamp = $next_stamp;
+ $now = $next_now;
+
+ # if timestamp, get onto real data in following lines
+ #
+ ($stamp_style == 1 || $stamp_style == 2 || $stamp_style == 3) and next;
+ }
+
+ if (/^Device:/) {
+ $in_cpu = 0;
+ $in_dev = 1;
+ $header = 1;
+ }
+ elsif (/^avg-cpu:/) {
+ $in_dev = 0;
+ $in_cpu = 1;
+ $header = 1;
+ }
+
+ if ($sample == -1) {
+ # first time we have the stats since boot, we need to figure
+ # out the meta data, and for $stamp_style == 0, try to compute
+ # the sample interval
+ #
+ if ($in_cpu && $header) {
+ # avg-cpu: %user %nice %system %iowait %steal %idle
+ # if present, always comes first
+ #
+ def_single("kernel.all.cpu.user");
+ def_single("kernel.all.cpu.nice");
+ def_single("kernel.all.cpu.sys");
+ def_single("kernel.all.cpu.wait.total");
+ def_single("kernel.all.cpu.steal");
+ def_single("kernel.all.cpu.idle");
+ }
+ elsif ($in_dev) {
+ if ($header) {
+ # one of ...
+ # Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn
+ # Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
+ # Device: tps MB_read/s MB_wrtn/s MB_read MB_wrtn
+ # Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
+ if (/tps/) {
+ $dev_style = "d"; # -d option
+ if (/Blk_read/) { $dev_thru_scale = 0.5; }
+ elsif (/kB_read/) { $dev_thru_scale = 1; }
+ elsif (/MB_read/) { $dev_thru_scale = 1024; }
+ else {
+ print "[$line] $_\n";
+ die "Device: cannot determine thruput scale";
+ }
+ def_multi("disk.dev.total", $dev_indom);
+ def_multi("disk.dev.read_bytes", $dev_indom);
+ def_multi("disk.dev.write_bytes", $dev_indom);
+ }
+ else {
+ $dev_style = "x"; # assume -x option
+ $dev_thru_scale = 0.5; # assume 512-byte sectors
+ def_multi("disk.dev.read_merge", $dev_indom);
+ def_multi("disk.dev.write_merge", $dev_indom);
+ def_multi("disk.dev.read", $dev_indom);
+ def_multi("disk.dev.write", $dev_indom);
+ def_multi("disk.dev.read_bytes", $dev_indom);
+ def_multi("disk.dev.write_bytes", $dev_indom);
+ def_multi("disk.dev.avactive", $dev_indom);
+ }
+ }
+ # Note: instances are populated as they are found _after_ the
+ # first (from boot time) stats are processed
+ }
+ next;
+ }
+ elsif ($sample >= 0 && $in_dev && $dev_style eq "d" && $header == 0
+ && $stamp_style == 0 && !defined($interval)) {
+ # if basic Device: stats, can compute sample interval from ratio of
+ # totals to rate for reads or writes ... need to avoid divide zero
+ # counts ... must do on the second sample interval and may fail if
+ # no -d or -z and no activity
+ my @part = split(/\s+/, $_);
+ if ($#part != 5) {
+ print "[$line] $_\n";
+ die "Device: number of values? expected 6, found " . ($#part+1) . "\n";
+ }
+ if ($part[4] != 0) {
+ $interval = int(0.5 + $part[4]/$part[2]);
+ }
+ elsif ($part[5] != 0) {
+ $interval = int(0.5 + $part[5]/$part[3]);
+ }
+ if (defined($interval) && $interval <= 0) {
+ # in case we've really screwed up, better to be clueless
+ $interval = undef;
+ }
+ }
+
+ if ($header == 0 && $sample >= 0) {
+ if ($in_cpu) {
+ my @part = split(/\s+/, $_);
+ # $part[0] is empty white space before first value
+ if ($#part != 6) {
+ print "[$line] $_\n";
+ die "avg-cpu: number of values? expected 7, found " . ($#part+1) . "\n";
+ }
+ put($part[1]/100); # user
+ put($part[2]/100); # nice
+ put($part[3]/100); # system
+ put($part[4]/100); # iowait
+ put($part[5]/100); # steal
+ put($part[6]/100); # idle
+ }
+ elsif ($in_dev) {
+ # Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn
+ # sda 3.34 10.70 77.59 32 232
+ # or
+ # Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
+ # sda 0.03 0.76 0.32 0.21 8.90 7.79 31.61 0.01 13.37 2.11 0.11
+ my @part = split(/\s+/, $_);
+ my @thisval;
+ my $instance;
+ if ($#part == 0) {
+ # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=604637
+ # long device name followed by embedded newline ... append the next
+ # input line
+ #
+ $instance = $part[0];
+ $_ = $instance . " " . <INFILE>;
+ chomp;
+ $line++;
+ @part = split(/\s+/, $_);
+ }
+ if ($dev_style eq "d") {
+ if ($#part != 5) {
+ print "[$line] $_\n";
+ die "Device: number of values? expected 6, found " . ($#part+1) . "\n";
+ }
+ $instance = $part[0];
+ }
+ else {
+ if ($#part != 11) {
+ print "[$line] $_\n";
+ die "Device: number of values? expected 12, found " . ($#part+1) . "\n";
+ }
+ $instance = $part[0];
+ }
+ if (exists($dev_first_handle{$instance})) {
+ $h = $dev_first_handle{$instance};
+ }
+ else {
+ # first time we've seen this instance, set up indom and handles
+ #
+ if ($dev_style eq "d") {
+ def_metric_inst("disk.dev.total", $dev_indom, $instance);
+ $dev_first_handle{$instance} = $#handle;
+ def_metric_inst("disk.dev.read_bytes", $dev_indom, $instance);
+ def_metric_inst("disk.dev.write_bytes", $dev_indom, $instance);
+ }
+ else {
+ def_metric_inst("disk.dev.read_merge", $dev_indom, $instance);
+ $dev_first_handle{$instance} = $#handle;
+ def_metric_inst("disk.dev.write_merge", $dev_indom, $instance);
+ def_metric_inst("disk.dev.read", $dev_indom, $instance);
+ def_metric_inst("disk.dev.write", $dev_indom, $instance);
+ def_metric_inst("disk.dev.read_bytes", $dev_indom, $instance);
+ def_metric_inst("disk.dev.write_bytes", $dev_indom, $instance);
+ def_metric_inst("disk.dev.avactive", $dev_indom, $instance);
+ }
+ # populate dev_seen for each instance
+ $dev_seen{$instance} = 0;
+ }
+ if ($dev_style eq "d") {
+ # disk.dev.total
+ # disk.dev.read_bytes
+ # disk.dev.write_bytes
+ @thisval = ($part[1], $part[2] * $dev_thru_scale, $part[3] * $dev_thru_scale);
+ if (!exists($dev_prev{$instance})) {
+ # first time seen for this instance
+ $dev_prev{$instance} = [0,0,0];
+ }
+ for (my $i = 0; $i <= $#thisval; $i++) {
+ # all these are instantaneous
+ $dev_prev{$instance}->[$i] = $thisval[$i];
+ put($dev_prev{$instance}->[$i]);
+ }
+ $dev_seen{$instance} = 1;
+ }
+ else {
+ if (exists($dev_first_handle{$instance})) {
+ $h = $dev_first_handle{$instance};
+ }
+ else {
+ pmiDump();
+ print "[$line] $_\n";
+ die "Device: no first handle for instance \"" . $instance . "\", check Handles in dump above";
+ }
+ # disk.dev.read_merge
+ # disk.dev.write_merge
+ # disk.dev.read
+ # disk.dev.write
+ # disk.dev.read_bytes
+ # disk.dev.write_bytes
+ # disk.dev.avactive
+ @thisval = ($part[1], $part[2], $part[3], $part[4], $part[5] * $dev_thru_scale, $part[6] * $dev_thru_scale, $part[11]);
+ if (!exists($dev_prev{$instance})) {
+ # first time seen for this instance
+ $dev_prev{$instance} = [0,0,0,0,0,0,0];
+ }
+ for (my $i = 0; $i <= $#thisval; $i++) {
+ # all these are instantaneous
+ $dev_prev{$instance}->[$i] = $thisval[$i];
+ put($dev_prev{$instance}->[$i]);
+ }
+ $dev_seen{$instance} = 1;
+ }
+ }
+ }
+}
+
+# flush last sample out ... end of file is the end of the sample
+#
+sample_done();
+
+pmiEnd();
+
+exit(0);
+
+=pod
+
+=head1 NAME
+
+iostat2pcp - Import iostat data and create a PCP archive
+
+=head1 SYNOPSIS
+
+B<iostat2pcp> [B<-v>] [B<-S> I<start>] [B<-t> I<interval>] [B<-Z> I<timezone>] I<infile> I<outfile>
+
+=head1 DESCRIPTION
+
+B<iostat2pcp> reads a text file created with
+B<iostat>(1) (I<infile>) and translates this into a Performance
+Co-Pilot (PCP) archive with the basename I<outfile>.
+If I<infile> is E<quot>-E<quot> then I<iostat2pcp> reads for
+standard input, allowing easy preprocessing of the I<iostat>(1) output
+with I<sed>(1) or similar.
+
+The resultant PCP archive may be used with all the PCP client tools
+to graph subsets of the data using B<pmchart>(1),
+perform data reduction and reporting, filter with
+the PCP inference engine B<pmie>(1), etc.
+
+A series of physical files will be created with the prefix I<outfile>.
+These are I<outfile>B<.0> (the performance data),
+I<outfile>B<.meta> (the metadata that describes the performance data) and
+I<outfile>B<.index> (a temporal index to improve efficiency of replay
+operations for the archive). If any of these files exists already,
+then B<iostat2pcp> will B<not> overwrite them and will exit with an error
+message.
+
+The first output sample from I<iostat>(1) contains a statistical summary
+since boot time and is ignored by I<iostat2pcp>, so the first real data
+set is the second one in the I<iostat>(1) output.
+
+The best results are obtained when I<iostat>(1) was run with its own B<-t>
+flag, so each output sample is prefixed with a timestamp. Even better
+is B<-t> with $B<S_TIME_FORMAT=ISO> set in environment when I<iostat>(1)
+is run, in which case the timestamp includes the timezone.
+
+Note that if $B<S_TIME_FORMAT=ISO> is B<not> used with the B<-t> option
+then I<iostat>(1) may produce a timestamp controlled by B<LC_TIME> from
+the locale that is in a format I<iostat2pcp> cannot parse. The formats
+for the timestamp that I<iostat2pcp> accepts are illustrated by these
+examples:
+
+=over 4
+
+=item B<2013-07-06T21:34:39+1000>
+
+(for the $B<S_TIME_FORMAT=ISO>).
+
+=item B<2013-07-06 21:34:39>
+
+(for some of the European formats, e.g. de_AT,
+de_BE, de_LU and en_DK.utf8).
+
+=item B<06/07/13 21:34:39>
+
+(for all of the $B<LC_TIME> settings for English
+locales outside North America, e.g. en_AU, en_GB, en_IE, en_NZ,
+en_SG and en_ZA, and all the Spanish locales, e.g. es_ES, es_MX and es_AR).
+
+=back
+
+In particular, note that some common North American $B<LC_TIME> settings will
+B<not> work with I<iostat2pcp> (namely, en_US, POSIX and C) because they
+use the MM/DD format which may be incorrectly converted with the
+assumed DD/MM format. This is another reason to recommend setting
+$B<S_TIME_FORMAT=ISO>.
+
+If there are no timestamps in the input stream, I<iostat2pcp> will
+try and deduce the sample interval if basic Disk data (B<-d>
+option for I<iostat>(1)) is found. If this fails, then the B<-t> option may be
+used to specify the sample I<interval> in seconds.
+This option is ignored if timestamps are found in the input stream.
+
+The B<-S> option may be used to specify as start time for the
+first real sample in I<infile>, where I<start> must have the format
+HH:MM:SS.
+This option is ignored if timestamps are found in the input stream.
+
+The B<-Z> option may be used to specify a timezone. It must have the
+format +HHMM (for hours and minutes East of UTC) or -HHMM (for hours
+and minutes West of UTC). Note in particular that B<neither> the B<zoneinfo>
+(aka Olson) format, e.g. Europe/Paris, nor the Posix B<TZ> format, e.g.
+EST+5 is allowed for the B<-Z> option.
+This option is ignored if ISO timestamps are found in the input stream.
+If the timezone is not specified and cannot be deduced, it defaults to
+E<quot>UTCE<quot>.
+
+Some additional diagnostic output is generated with the B<-v> option.
+
+B<iostat2pcp> is a Perl script that uses the PCP::LogImport Perl wrapper
+around the PCP I<libpcp_import>
+library, and as such could be used as an example to develop new
+tools to import other types of performance data and create PCP archives.
+
+=head1 CAVEAT
+
+B<iostat2pcp> requires I<infile> to have been created by the version
+of B<iostat>(1) from
+L<http://freshmeat.net/projects/sysstat>.
+
+B<iostat2pcp> handles the B<-c> (CPU), B<-d> (Disk), B<-x> (eXtended
+Disk) and B<-p> (Partition) report formats (including their B<-k>, B<-m>,
+B<-z> and
+B<ALL> variants), but does not accommodate the B<-n> (Network Filesystem)
+report format from B<iostat>(1); this is a demand-driven limitation rather
+than a technical limitation.
+
+=head1 SEE ALSO
+
+B<Date::Format>(3pm),
+B<Date::Parse>(3pm),
+B<iostat>(1),
+B<LOGIMPORT>(3),
+B<PCP::LogImport>(3pm),
+B<pmchart>(1),
+B<pmie>(1),
+B<pmlogger>(1) and
+B<sed>(1).