./hadak

Modlog – a BASH modular logger

by Hans Kokx on Nov.10, 2009, under Bash Scripts, Linux

Introduction

This summer, I had the good fortune to spend several months working at the University of Michigan. One of the tasks assigned to me was devising a method for data collection over time for certain types of data. In particular, we were looking to collect information from /proc/meminfo and out of our lm_sensors output.


The issue that we came across with logging these sets of data was that they were set up for one-time viewing. That is, they looked similar to the following:

Timestamp:  00:00:00
Header 1:     data 1
Header 2:     data 2
Header 3:     data 3

Timestamp: 00:00:01 Header 1: data 4 Header 2: data 5 Header 3: data 6
Timestamp: 00:00:02 Header 1: data 7 Header 2: data 8 Header 3: data 9

and so on. Certainly, we had the ability to set up a cron job and dump the data we sought after into a text file, however that was a very messy solution over long periods of time. Ideally, we would like to pile up our data in a manner such as this:

Timestamp     Header 1     Header 2     Header 3
00:00:00      Data 1       Data 2       Data 3
00:00:01      Data 4       Data 5       Data 6
00:00:02      Data 7       Data 8       Data 9



Over time, it is much easier to read the data, as well as build graphs from the dataset. You will be able to awk out a single column (or multiple columns) and compare certain data over a particular date range. Compared to the original method -- dumping to a text file in the original state -- this is much more usable.

Solution

Modlog, at it's heart, is a modular logging system (thus the name). It uses configuration files to generate output, then compiles the output in an easy to read and easy to store format. Furthermore, modlog will allow you to easily recall that data using regular expressions. Modlog is the brain child of Hans Kokx, with additional code snippets provided by Brian Ward and Ezekiel Hendrickson. There are a few components to modlog. First, there is the main script. This is the heart and soul of modlog, of which all other components are referenced. Second, there are the configuration files, and finally, a perl helper application. I will explain each of these in more detail.

Modlog

The main modlog script is written for BASH. It has been tested throughly, but that does not mean it will work in all circumstances without any hiccups. There are a few variables that can (and should) be set, depending on your configuration. They are as follows:



confdir is where your configuration files can be found.

confdir=/etc/sysconfig/modlog

kernlogger is what logs to your system logs. In this case, it is configured to use syslog-ng. You should check with your distrobution's documentation to find the correct command for logging to your system logs.

kernlogger="logger -t kernlog -p daemon.info $name[$$]: "



Most other variables should be fine with the defaults. Anything that is initialized blank should be left blank.

 
#!/bin/bash
 
# Name          : modlog
# Description   : A modular logging system
# Version       : 1.0.1
# Coded by      : Hans Kokx, Brian Ward, Ezekiel Hendrickson
# Date          : Sept 24, 2009
 
## Variables ##
# Configuration file directory.
confdir=/etc/sysconfig/modlog
 
# Default column length used for writing files and displaying output.
defaultcollength=25
 
# Kernlogger command
kernlogger="logger -t kernlog -p daemon.info $name[$$]: "
 
# Date format used in output file names.
date=`date +%F%n`
 
# Timestamp used as the first column in output files.
time="date +%H:%M:%S"
 
# Define a new line as a variable
nl="
"
 
optdate=$date
optconfig=
optfield=
opt_c=
opt_d=
opt_f=
opt_i=
opt_l=
opt_w=
input=
output=
description=
works=
hrf=tee
read=
write=
verbose=
counter=1
running=
 
# CONJUNCTION JUNCTION WHATS YOUR FUNCTION
 
# Generic function to convert text to lowercase
toLower() {
    echo $1 | tr "[:upper:]" "[:lower:]"
}
 
# Make sure the $collength variable is populated.
colLength()
{
    if [[ -z "${collength}" ]]
    then
        collength=$defaultcollength
    fi
}
 
main() {
    outfile=$output/$date.log
    if [ "$verbose" = "1" ]
    then
        echo -en "\033[1m*\033[0m Writing output to $outfile\n"
    fi
 
    if [[ $works = 0 ]]
        then
        mkdir -p $output
        if [[ "$input" != "" ]]
        then
            if [[ ! -s $outfile ]]
            # Print the timestamp header to our output, then write headers, and
            # finally start writing rows of data.
            then
            	if [ "$verbose" = "1" ]
    			then
        		    echo -en "\033[1m*\033[0m $outfile does not exist.  Creating new file, and writing headers.\n"
    			fi
                echo -n "Timestamp " | awk '{ printf "%9s", $1 "  " }'  >> $outfile
                $input | sed 's/:/ /g' | awk 'BEGIN{FS="  +"}NF>1{ printf "%'"$collength"'s", $1 "   " }' >> $outfile
                echo "" >> $outfile
            fi
                $time | awk '{ printf "%9s", $1 "  " }'  >> $outfile
                $input | sed 's/:/ /g' | awk 'BEGIN{FS="  +"}NF>1{ printf "%'"$collength"'s", $2 "  " }' >> $outfile
            echo "" >> $outfile
        fi
    else
        if [[ "$verbose" = "1" ]]
        then
            echo -en "\E[31m\033[1m*\E[31m\033[0m $input will not complete successfully on this machine. Skipping.\n"
        fi
            $kernlogger $input will not complete successfully on this machine. Skipping.
    fi
    works=
}
 
writeData()
{
    if [[ "$optconfig" = "" ]] && [[ "$optfield" = "" ]]
    then
    # Read config files one by one, and run them as commands, dumping output
    # into a file.
   		if [ "$verbose" = "1" ]
   		then
       		echo -en "\033[1m*\033[0m Reading configuration from $FILE\n"
   		fi
        for FILE in $(find $confdir -name "*.conf" -type f); do
            works=
    		if [ "$verbose" = "1" ]
    		then
        		echo -en "\033[1m*\033[0m Executing commands...\n"
    		fi
            . $FILE
            main
        done
    elif [[ ! "$optconfig" = "" ]] && [[ "$optfield" = "" ]] && [[ "$optdate" = "$date" ]]
    then
        . $confdir/$optconfig.conf
        main
    else
        echo >&2 "Error!"
    fi
}
 
showData()
{
    # We have -l input, so loop through arguments and return the matching
    # fields.
	    if [ "$verbose" = "1" ]
	    then
	        echo -en "\033[1m*\033[0m Sourcing $confdir/$optconfig.conf\n"
	    fi
        . $confdir/$optconfig.conf
        colLength
    # The following is all voodoo magic.
    # Loop over each line to see if it matches our regex, and determine whether
    # or not it should be printed.
    counter=
	    if [ "$verbose" = "1" ]
	    then
	        echo -en "\033[1m*\033[0m Determining matching columns...\n"
	    fi
    for re in ${ARGS[@]}; do
        forloop='for(i=1;i<=NF;i++)if(tolower($i)~/'$re'/)f[i]=1;'
        forloops="$forloops $forloop"
        counter=`expr $counter + 1`
    done
    # Start by setting our field separator as at least two whitespaces.
    # Then, we set each line to show, based on whether or not it matches our
    # regex.
    # Finally, we loop through our columns, match all lines that should be
    # visible, and then pipe it to our helper to help display them.
 
    if [ ! "$opt_w" = "1" ]
    then
	    if [ "$verbose" = "1" ]
	    then
	        echo -en "\033[1m*\033[0m Automatic column width adjustments being calculated...\n"
	    fi
        termcols=`stty -a| grep columns|awk '{print $7}'|sed 's/;//g'`
        collength=`expr $termcols / $counter - 65` # set a variable column
    fi                                             # width based on the
                                                   # current terminal width
	if [ "$verbose" = "1" ]
	then
	    echo -en "\033[1m*\033[0m Printing matching fields\n"
	fi
    gawk 'BEGIN{FS="  +"}
        NR==1{
            for(i=1;i<=NF;i++)
                f[i]=0;'"$forloops"'
           f[1]=0;
        }
        {
            printf"%9s",$1;
            for(i=1;i<=NF;i++)
                if(f[i])
                    printf"%'"$collength"'s",$i;
            print""
        }' $output/$optdate.log | $hrf
}
 
info()
{
     if [ -n "$optconfig" ]
     then
         . $confdir/$optconfig.conf
         echo -en "\033[1mKey:\033[0m Header ("'\E[31m'"\033[1mawk record"'\E[31m\033[0m'")\n"
         $input | sed 's/:/ /g' | awk 'BEGIN{FS="  +"}NF>1{
                         sub(/[:]/, "");
                         printf "%25s", $1 " (" "$"NR+1 ")  \n";
                       }'
         opt_i=2
         echo >&2 "REMINDER: The awk delimiter is two or more whitespaces. \(ex. -F \"  +\"\)."
         echo >&2 "The awk record numbers indicated are pre-computed and should be used when trying to pull data directly from the log file."
     else
         echo >&2 "No configuration file selected. Use -l flag to view available configuration files."
     fi
     exit 1
}
 
list()
{
    echo Available configuration files:
    for FILE in $(find $confdir -name "*.conf" -type f); do
        . $FILE
        filename=`basename $FILE .conf`
        echo "  $filename - $description"
    done
    exit 1
}
 
# Subroutine to display usage information
usage()
{
cat<<EOF
Usage: $0 <options>
 
This script can pull columns of data from a specified date on a specified
configuration.
 
OPTIONS:
    -c  Use configuration for data as listed in -i. Case sensitive.
    -d  Use date for data in the format YYYY-MM-DD. Defaults to today.
    -f  Use field given with -i switch for information. Case insensitive.
    -h  Human readable output. Displays headers every 20 lines.
    -i  Lists possible field for a configuration.
    -l  Lists available configurations.
    -R  Read mode, for pulling data from logs.
    -v  Verbose
    -W  Write mode, for writing data to logs.
    -w  Variable column width.
EOF
exit 1
}
 
# If no arguments are given, display usage information
[ $# -eq 0 ] && usage
 
# Parse command line options
while getopts "c:d:f:hilRvWw:" option
do
    case $option in
        c)
            # Set configuration file to use for data here
            optconfig=$OPTARG
            opt_c=1
            ;;
        d)
            # Set date to use for data here
            optdate=$OPTARG
            opt_d=1
            ;;
        f)
            # Set field(s) to use for data here
            optfield=`toLower $OPTARG`
            typeset -a ARGS; n=0
                        ARGS[${n}]=${optfield}
                        n=$(( $n+1 ))
 
            while [[ "${!OPTIND:0:1}" != "-" &&  "${!OPTIND}" != "" ]]; do
                    # echo ${!OPTIND}
                    ARGS[${n}]=${!OPTIND}
                    OPTIND=$(( $OPTIND+1 ))
                    n=$(( $n+1 ))
            done
            opt_f=1
            ;;
        h)
            # Sets the output to human readable (headers every few lines).
            hrf="$confdir/helper.pl"
            ;;
        i)
            # List possible fields within a configuration
            opt_i=1
            ;;
        l)
            # Lists available configuration files
            opt_l=1
            ;;
        R)
            read=1
            ;;
        v)
            verbose=1
            ;;
        W)
            write=1
            ;;
        w)
            # Set the column width
            collength=$OPTARG
            opt_w=1
            ;;
        \?)
            # Displays a handy help message
            usage
            ;;
    esac
done
 
# Verbosity checks
if [ "$opt_c" = 1 ] && [ "$verbose" = 1 ]
then
    echo -en "\033[1m*\033[0m Using $optconfig as config\n"
fi
 
if [ "$opt_d" = 1 ] && [ "$verbose" = 1 ]
then
    echo -en "\033[1m*\033[0m $optdate specified for review\n"
fi
 
if [ "$opt_f" = 1 ] && [ "$verbose" = 1 ]
then
    echo -en "\033[1m*\033[0m Field(s) \033[1m$optfield\033[0m specified for review\n"
fi
 
if [ "$opt_h" = 1 ] && [ "$verbose" = 1 ]
then
    echo -en "\033[1m*\033[0m Using $hrf for human-readable post-processing\n"
fi
 
# Process functions based on input
if [ "$opt_i" = "1" ]
then
    info
fi
 
if [ "$opt_l" = "1" ]
then
    list
fi
 
if [ "$read" = "1" ]
then
    if [ ! "$opt_c" = "1" ]
    then
        echo >&2 "No configuration file specified!"
        exit 1
    else
        if [ "$opt_c" = "1" ] && [ -z "$opt_f" ]
        then
            echo >&2 "No fields specified!"
            exit 1
        else
            showData
        fi
    fi
elif [ "$write" = "1" ]
then
    writeData
elif [ -z "$write" ] || [ -z "$read" ]
then
    echo "You must specify whether you would like to [R]ead or [W]rite data!"
fi
 

Configuration

The configuration directory is located in modlog as a variable, near the top of the script. All configuration files are specified as .conf files within the directory. Modlog loops over each configuration file, performing the commands specified, and then goes on to the next. There are some limitations with modlog because of the setup of the configuration files. For example, the command that will be run must be encapsulated in quotes. For some more advanced commands, this has proven to be problematic. Your milage may vary. If, however, you run into this problem and decide to fix it, I would ask that the changes be merged into this document, and all credit for the fix will go to the author of that fix.

The following sample configuration will collect data from /proc/meminfo. Note the output directory; this is where the data will be logged to, and may be changed per configuration.

 
# Name          : meminfo.conf
# Description   : Configuration file for modlog. This config file pulls meminfo.
# Version       : 0.1
# Coded by      : Hans Kokx, Brian Ward
# Date          : May 21, 2009
# Prerequisite	: none
 
# Command that is run to gather data
input="cat /proc/meminfo"
 
# Where the outputted data is stored
output=/var/log/archive/modlog/meminfo
 
# Short description of this configuration file
description="Memory usage information"
 
# A test to make sure that our config file wont fail outright.
# $works should be 0 if all is well, and 1 if the command will fail.
 
if [ ! "$input" == "" ]
	then
	works=1 # fail, unless otherwise specified
 		if [ -e "/proc/meminfo"  ]
		then
			works=0
		fi
fi
 

Again, here is a second example configuration. This one is used to collect data from lm-sensors. NOTE: lm-sensors has a small bug in it, where beep is outputted as beep:enabled. Since there is no whitespace, modlog sees this as a header, and therefore does not properly log this data. If this is of concern to you, the source code is easily modified to fix the bug (in lm-sensors).

 
# Name          : lmsensors.conf
# Description   : Configuration file for modlog. This config file pulls lm-		#				  sensors data.
# Version       : 0.1
# Coded by      : Hans Kokx, Brian Ward
# Date          : May 21, 2009
# Prerequisite	: lm-sensors, `cd /dev && mknod i2c c 89 0`
 
# Command that is run to gather data
input="/usr/bin/sensors"
 
# Where the outputted data is stored
output=/var/log/archive/modlog/sensors
 
# Short description of this configuration file
description="lm-sensors data"
 
# A test to make sure that our config file wont fail outright.
# $works should be 0 if all is well, and 1 if the command will fail.
 
if [ -z "$input" ]
	then
		works=1 # fail, unless otherwise specified
 		if [ ! -s "$input"  ]
		then
			if [ -s "/etc/sensors3.conf"-a -c "/dev/i2c" ]
			then
				works=0
			fi
		fi
fi
 

helper.pl

The final piece of the puzzle is a small perl script. When recalling data from modlog: if -h is invoked, every 20 or so lines (a variable that can be set in the main script), the data column headers will be printed. This bit of magic is accomplished through the following perl script, provided primarilly by Ezekiel Hendrickson:

 
#!/usr/bin/perl
 
# Name          : helper.pl
# Description   : Helper script for formatting the output of modlog for display.
# Version       : .1
# Coded by      : Hans Kokx, Brian Ward, Ezekiel Hendrickson
# Date          : July 2, 2009
 
use strict;
use warnings;
 
# Declare variables
my $i = 0;
my $firstline;
 
# Print a blank line, then the column header every 20 lines.
foreach(<STDIN>) {
    $firstline = $_ if ($i == 0);
    print;
    print "\n",$firstline if ($i > 0 && ($i % 20) == 0);
    $i++;
}
 

Conclusion

Modlog allows for easy, modular logging of a set of data that is intended to be viewed and then discarded. Furthermore, it extends the functionality by easily allowing you to pull data back out of the logs, using regular expressions, for easy comparison. Modlog solved a vexing issue for the University of Michigan, and I am proud to serve it up to all who need it. As with all things, if you find anything that should be changed, fixed, or amended, feel free to leave it in the comments.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • email
  • Print
  • StumbleUpon
  • Technorati
  • TwitThis

Leave a Reply

Looking for something?

Use the form below to search the site:

Still not finding what you're looking for? Drop a comment on a post or contact us so we can take care of it!

Visit our friends!

A few highly recommended friends...