30 March 2016

Making charts from iostat output

I've decided to make some charts from the output of the iostat command. Just for fun =)

The idea is simple: a script named `report.sh` runs periodically in background(via `cron`) collecting `iostat` data into $REPORT_DIR/$param/$device files, where

  • $REPORT_DIR is a directory with the reports;
  • $param is the `iostat` parameter name;
  • $device is a device name from the same `iostat` output.

When we need to update the charts, we call `plot.sh`.

Required components

Scripts

util.sh
#!/bin/bash -

# $1 - Optional error message.
function die
{
  [ $# -gt 0 ] && echo >&2 $1
  exit 1
}

function warning
{
  echo >&2 "Warning: $1"
}


function getSafeFilename
{
  r=${1//\//_}
  echo ${r/\%/}
}

#vim: ts=2 sts=2 sw=2 et
report.sh
#!/bin/bash -
# Generates iostat data files for processing with the GNU plot utility.

HEADER=
TIME_FORMAT="%Y/%m/%d/%H:%M"
TIME=$(date "+$TIME_FORMAT")
DIR=$(cd $(dirname "$0"); pwd)
REPORT_DIR="${DIR}/r"


#####################################################################
# Functions

source $DIR/util.sh


# $1 - exit code
function usage
{
  echo "
Generates iostat data files for processing with the GNU plot utility(plot.sh).

Usage: $0 OPTIONS

OPTIONS:
-h, --help            Display help message
-t, --time-format     Time format. Default: ${TIME_FORMAT}.
-i, --report-dir      Directory with reports. Default: ${REPORT_DIR}.
"
  exit $1
}

#####################################################################
# Parsing CLI options

OPTS=$(getopt -o t:i:h -l time-format:,report-dir:,help -- "$@")
[ $? -eq 0 ] || usage 1
eval set -- "$OPTS"

while true
do
  case "$1" in
    -t|--time-format)
      TIME_FORMAT="$2"
      shift 2;;
    -i|--report-dir)
      REPORT_DIR="$2"
      shift 2;;
    -h|--help)
      usage 0
      shift;;
    --)
      shift
      break;;
    ?)
      usage 1;;
    *)
      die "Internal error: failed to parse CLI args"
  esac
done

echo TIME_FORMAT: $TIME_FORMAT REPORT_DIR: $REPORT_DIR

#####################################################################

mkdir -p $REPORT_DIR
[ $? -eq 0 ] || die "Report directory '$REPORT_DIR' is inaccessible"

iostat -dpxk | while read -r line
do
  if [[ -z $HEADER && $line == Device* ]]; then
    HEADER=( $line )
  elif [[ -n $HEADER ]]; then
    [[ -z $line ]] && continue

    columns=( $line )

    [[ ${#HEADER[@]} != ${#columns[@]} ]] && continue


    i=1
    for h in ${HEADER[@]:1}
    do
      param_report_dir="${REPORT_DIR}/"$(getSafeFilename $h)
      mkdir -p $param_report_dir
      [ $? -eq 0 ] || die "Parameter directory '$param_report_dir' is inaccessible"

      # $REPORT_DIR/param/device
      echo $TIME$'\t'${columns[$i]} >> $param_report_dir'/'${columns[0]}
      (( ++i ))
    done
  fi
done

echo 'Done'
plot.sh
#!/bin/bash -
# Creates visual representation of data files generated by report.sh

TIME_FORMAT="%Y/%m/%d/%H:%M"
# Report files location $REPORT_DIR/param/device
DIR=$(cd $(dirname "$0"); pwd)
REPORT_DIR="${DIR}/r"
OUTPUT_DIR="${DIR}/o"
XRANGE=
PLOT_TERMINAL="png"

#####################################################################
# Functions

source $DIR/util.sh

function getPlots
{
  param_path="$1"

  find $param_path/ -maxdepth 1 -mindepth 1 -type f | while read -r device_path; do
    # $device_path = /*/*/.../param/device
    device=$(basename $device_path)

    printf "'%s' using 1:2 t '%s' with lp pt 5, " \
      "$device_path" $device
    printf "'%s' using 1:2 notitle with impulses, " \
      "$device_path"
  done
}


# Returns default value for GNU plot xrange
function getXRange
{
  y=$(date +%Y)
  m=$(date +%m)

  (( m -= 1 ))

  if [ $m -lt 1 ]; then
    m=1
    (( y -= 1 ))
  fi

  d=$(date "+$TIME_FORMAT")

  echo "[ '$y/$m/1/00:00' : '$d' ]"
}


# $1 - exit code
function usage
{
  echo "
Creates charts from files generated by report.sh

Usage: $0 OPTIONS

OPTIONS:
-h, --help            Display help message
-x, --xrange          GNU Plot xrange value. Default: $(getXRange).
-t, --time-format     Time format. Default: ${TIME_FORMAT}.
-i, --report-dir      Directory with reports. Default: ${REPORT_DIR}.
-o, --output-dir      Output directory. Default: ${OUTPUT_DIR}.
-f, --term            GNU Plot terminal. Default: ${PLOT_TERMINAL}. See 'gnuplot set terminal'.
"
  exit $1
}

#####################################################################
# Parsing CLI options

OPTS=$(getopt -o x:t:i:o:f:h -l xrange:,time-format:,report-dir:,output-dir:,term:,help -- "$@")
[ $? -eq 0 ] || usage 1
eval set -- "$OPTS"

while true
do
  case "$1" in
    -x|--xrange)
        XRANGE="$2"
        shift 2;;
    -t|--time-format)
      TIME_FORMAT="$2"
      shift 2;;
    -i|--report-dir)
      REPORT_DIR="$2"
      shift 2;;
    -o|--output-dir)
      OUTPUT_DIR="$2"
      shift 2;;
    -f|--term)
      PLOT_TERMINAL="$2"
      shift 2;;
    -h|--help)
      usage 0
      shift;;
    --)
      shift
      break;;
    ?)
      usage 1;;
    *)
      die "Internal error: failed to parse CLI args"
  esac
done

echo XRANGE: $XRANGE TIME_FORMAT: $TIME_FORMAT OUTPUT_DIR: $OUTPUT_DIR REPORT_DIR: $REPORT_DIR

#####################################################################

[ -d $REPORT_DIR ] || die "$REPORT_DIR doesn't exist"

find $REPORT_DIR/ -maxdepth 1 -mindepth 1 -type d | while read -r param_path
do
  # $param_path = /*/*/.../param
  param=$(basename $param_path)
  plots=$(getPlots "$param_path")
  plots=${plots/%, /}

  mkdir -p $OUTPUT_DIR
  [ $? -eq 0 ] || die "Output directory '$OUTPUT_DIR' is inaccessible"

  outfile="${OUTPUT_DIR}/${param}.${PLOT_TERMINAL}"

  [[ -z $XRANGE ]] && XRANGE="$(getXRange)"

  gnuplot <<EOS
TIME_FORMAT = "$TIME_FORMAT"
set terminal $PLOT_TERMINAL
set grid nopolar
set xtics rotate
set xlabel "Date/Time"
set ylabel "Value"
set yrange [ 0 : ]
set format x TIME_FORMAT timedate
set timefmt TIME_FORMAT
set xdata time
set xrange $XRANGE
set output "$outfile"

plot $plots
EOS
done

echo 'Done'

Sample chart

Gnuplot Produced by GNUPLOT 5.0 patchlevel 1 (Gentoo revision r1) 0 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 2016/03/30/16:00 2016/03/30/16:30 2016/03/30/17:00 2016/03/30/17:30 2016/03/30/18:00 2016/03/30/18:30 Value Date/Time sda4 sda4 gnuplot_plot_2 sda3 sda3 gnuplot_plot_4 sda1 sda1 gnuplot_plot_6 loop1 loop1 gnuplot_plot_8 loop0 loop0 gnuplot_plot_10 sda sda gnuplot_plot_12 sda2 sda2 gnuplot_plot_14 loop2 loop2 gnuplot_plot_16