Hi all,

On Tuesday we talked a fair amount about NetLogger and logging APIs. I also had a talk with Russell about his ideas for the API and behavior of the logging.

I've written (or re-written) a NetLogger-based implementation of Michelle's API. Right now it's at branches/DC1/src/log/, and it has three components:

logd.py : This reads the policy file, spawns a forwarding daemon that watches the local log file and forwards it to the central logging node, and if this *is* the central node, spawns a listening daemon, too.

LSSTLog.py: My old Python module, which is a wrapper around NetLogger, and which also now includes a function (outside of any class) called log(message, priority=INFO, **extraData). This is compatible with Michelle's API, but also adds the possibility of taking additional parameters. See my suggestions on style below for how to use this.

nlogtopd.py:  A daemon which parses 'top' and writes data to the log file.

(also, logging.policy in this directory controls logging behavior).

The two important changes are the following: logd.py *must* be run if you are running on multiple machines. Without it, data is not forwarded. This is a major point of contention with people - I've talked to Tim and Russell about writing a smarter forwarding daemon which will automatically run when LSSTLog is used, but I've heard several ideas about how to do this (Singleton pattern, grep-ing "ps -ef", talking to a known socket address and asking "are you my daemon?", etc) but Tim and I have decided I need to work on other things right now.

I added some fairly extensive information to LSSTLog.py's docstring, explaining how to use log(...) for best effectiveness.
The long and short of it is this:

log("stageleader.moveData.start", priority=INFO, MPI_RANK='1')

will generate the following:

t DATE: 2006-08-10T23:05:22.836473
s LVL: INFO
s HOST: 141.142.41.82
s MPI_RANK: 1
s EVNT: stageleader.moveData.start

our visualization software can use this to correlate the program running, the machine it's running on, and the time it entered a section of the code called 'moveData'. Instrumenting code with messages like "programName.majorFunction.start" and "programName.majorFunction.end" will help us figure out where code is getting slowed down.

Since logging seems to be important to many people, I'd appreciate if everyone would look at the code and say whether or not they feel strongly about moving to this version before we make our next run on TeraGrid. Since it's API-compatible, I could potentially just change "LSSTLog" to "log" in the source and replace Michelle's code in the SVN tree.

-jmyers
#!/usr/bin/env python

import nlforward
import os
import sys
import apps.fw.Policy.Policy as Policy
import log.LSSTLog as LSSTLog


ForwardInterval=5







def startListener(centralLogFile):
    tmp = os.fork()
    
    #start netlogd.py daemon.
    if tmp == 0:
        try:
            os.execlpe('netlogd.py',
                       'netlogd.py',
                       "-o" + centralLogFile + "",
                       os.environ)
        except OSError:
            sys.stderr.write("nlogtopd: Couldn't find netlogd.py" +
                             " - listening daemon not started!")
    







if __name__ == "__main__":

    # read Policy file to determine where to send log messages
    logPolicy = Policy('./logging.policy')
    logNode = logPolicy.Get ('logNode')
    localLog = logPolicy.Get ('logFile')
    centralLogFile = logPolicy.Get('centralLogFile')

    fwdir, fwpat = os.path.split(localLog)
    if fwdir == '':
        localLog = './' + localLog
        # because nlforward.py demands a directory component,
        # *that's* why!


    #if this *is* the server node, start listening!
    if LSSTLog.nllite.get_host() == logNode:
        startListener(centralLogFile)


    #nlforward.py is not a daemon for some reason, so
    # let's make it one!
    # TO DO:  Don't spawn another forwarding daemon if
    # there is already one shown by ps -ef.
    if os.fork()==0:
        os.setsid()
        sys.stdout=open("/dev/null", 'w')
        sys.stdin=open("/dev/null", 'r')
        if os.fork()==0:
            #start a forwarding daemon for that log
            try:
                os.execlpe('nlforward.py',
                           'nlforward.py',
                           "-s " + str(ForwardInterval), 
                           localLog,
                           "x-netlog://" + logNode,
                           os.environ)
                sys.exit(0)
            except OSError:
                sys.stderr.write("logd: Couldn't find nlforward.py" +
                                 " - forwarding daemon not started!")    
                sys.exit(-1)
#!/usr/bin/env python
"""LSSTLog is a thin wrapper around the NetLogger package
to allow simpler usage of the NetLogger package.

We intend to implement the NetLogger methodology
as described at http://dsd.lbl.gov/NetLogger/methodology.html.


    Before running anything that uses LSSTLog, you *MUST*
    run logd.py.  logd.py spawns the daemons for
    forwarding logs and receiving logs over the network.

    *IF LOGD.PY IS NOT RUN, YOUR LOGS WILL NOT BE RECEIVED.*

    This is the biggest change from Michelle's original
    logging method.  And of course, if you're just debugging,
    you might not want to bother forwarding to a central log-
    instead, you can always just read the local one.

    Also, there is now an extra, optional parameter to log(...),
    but that means that log remains backwards-compatible.

    However, it is strongly recommended that you read style
    suggestions below, as this will allow us to have an easier,
    more useful time visualizing logs and extracting useful info.
    

    log(message, priority=INFO, **extraData)
    ----------------------------------------
    where priority is one of the constants defined in this
    module and extraData are field-value pairs
    (i.e. a variable number of parameters of the form
    key1=value1, key2=value2,...)

    These field-value pairs, along with the message (in NL
    parlance, an 'event' - which has an implicit field-name
    'EVENT') will be used for visualization.


    STYLE SUGGESTONS
    -----------------

    Good message (event) names are ones like:

      'myModule.doBigCalculation.start'
      'myModule.doBigCalculation.end'

    for diagnostics, or:
    
      'myModule.MPI_Comm_World.err'

    for an error.
    

    Good field-value pairs might be:
      MPI_ERROR_CODE=32,
      ERR_MESSAGE='Looks like our MPI Universe is too small for spawning!' }

    for an error log

    or 

      LOOP_ITERATOR_VALUE=i, MY_MPI_RANK=mpiRank

    in an very computationally-intensive loop on variable i
    inside an MPI program.


    Note that all logs will automatically include the calling node's hostname,
    the current system time.  And of course, bear in mind when writing
    an error message, we can filter out any information we want,
    but obviously cannot extract information that is not given!


    code examples:

    import LSSTLog
    ....

    def main(argv):

        LSSTLog.log('myModule.main.start', LSSTLog.INFO)
        ....

        try: 
            for i in range(10):
                LSSTLog.log('myModule.moveHugeDataSet.start',
                            LSSTLog.INFO,
                            HUGE_DATA_SET_INDEX=i)
                moveHugeDataSet(i)
                LSSTLog.log('myModule.moveHugeDataSet.end',
                            LSSTLog.INFO,
                            HUGE_DATA_SET_INDEX=i)
        except e:
            LSSTLog.log('myModule.main.err', LSSTLog.ERR,
                        ERR_MESSAGE=\"\"\"Got an exception
                        while moving huge data sets!\"\"\")
        ...

        ...

        LSSTLog.log('myModule.main.end', LSSTLog.INFO)
        return 0
                         

Added by jmyers.
"""




from gov.lbl.dsd.netlogger import nllite
import os
import sys
import apps.fw.Policy.Policy as Policy

__author__="jmyers"



#For API compatibility with Michelle's version

#global GlobalLog 
GlobalLog = None
#GlobalLogSetFlag = 0

CRIT = nllite.Level.FATAL
ERR = nllite.Level.ERROR
WARNING = nllite.Level.WARN
INFO = nllite.Level.INFO
DEBUG = nllite.Level.DEBUG

DefaultLogLevel = DEBUG






def log(message, priority=DefaultLogLevel, source="", **extraData):
    """log:  Log a message. (with NetLogger!)


    Please read LSSTLog.py's docstring for more info
    on using this utility! You should really run logd.py first!


    Inputs:
      -message: an event which has occurred or error message.
      -priority:  message priority, one of:
        - CRITICAL
        - ERROR
        - WARNING
        - INFO
        - DEBUG
        The meanings are the same as for the syslog unix utility
      -source: Optional, the name of your program.
      -extraData:  A Python Dictionary of field-value pairs
        which should be used for specific data you would like to record.
        Please see the LSSTLog module's docstring for much better
        style and usage information on this one, it's very important!

    -jmyers
    """
    global GlobalLog
    if GlobalLog == None:
        # set up the global log
        logPolicy = Policy('./logging.policy')
        localLog = logPolicy.Get ('logFile')
        GlobalLog = LSSTLog(localLog)
    ed = extraData
    if source != "":
        ed['SOURCE'] = source
    GlobalLog.write(message, extraData=ed, msgLevel=priority)




def withoutWhiteSpace(input):
    returnVal = input
    if isinstance(input, str):
        returnVal = input.replace(' ', '_')
    return returnVal




#### actual LSSTLog class

class LSSTLog:
    """LSSTLog is a thin wrapper around the NetLogger package
    to allow (slightly) simpler, non-buffered logging for Python.

    The LSSTLog class basically does the following for you:
    -when instantiated:
      -create an nllite (NetLogger) LogOutputStream
      -sets the local logfile for the LogOutputStream
      -sets the LogOutputStream loglevel to Debug3 (highest)
      -if second parametre, centralLogNodeAddress is given to
         __init__:
         starts a nlforward daemon for the local logfile,
         which watches the local log for activity and periodically
         forwards data to the central log node.  It is
         suggested, however, that a single nlforward daemon is
         and a single log file are used for the entire application.
         
    -when writing, it also:
       -always adds, if not present, a HOST='hostname' field/value
       -flushes the buffer every time a message is written

    Added by jmyers.
    """



    
    def __init__(self, localLogfile):
        """Params:
           localLogFile: temporary log file on local machine."""
        global DefaultLogLevel
        self.nlLOS = nllite.LogOutputStream(localLogfile)
        self.nlLOS.setLevel(DefaultLogLevel) #report all logs



    def getLogFile():
        return self.nlLOS._logfile



    def write(self,eventString,extraData={},msgLevel=INFO):
        """Writes the eventString to the log associated with this 
        object. Automatically includes the timestamp and the name
        of the host in the log.

        Optionally adds other FIELD=DATA pairs from the
        extraData parameter.


        Params:
            eventString:
              name of event, e.g. function foo should have
              foo.start and foo.end as events written
              at the start and end of the function,
              and an error might have BAD_FILE_DESCRIPTOR
              as an eventString.
            msgLevel:  
              INFO by default, take one from the Level class
              in the LSSTData module (FATAL, ERROR, WARN,
              INFO, DEBUG, DEBUG1, DEBUG2, DEBUG3)
            extraData:
              A Python dictionary  of field/value pairs
              to be included in the log for analysis
              purposes.  E.G.:
                {'ITERATION':'304', 'CURRENTFILE':'/tmp/3016'}
                or
                {'ERRNUM':'501', 'COREDUMPLOCATION':'~/doom'}"""
        if 'HOST' not in extraData.keys():
            extraData['HOST'] = nllite.get_host()
        #remove spaces, NLV doesn't like them!
        noSpaceEventString = withoutWhiteSpace(eventString)
            
        noSpaceDict = {}
        for i in extraData:
            noSpaceDict[withoutWhiteSpace(i)] = withoutWhiteSpace(
                extraData[i])
        
        self.nlLOS.write(noSpaceEventString, noSpaceDict, level=msgLevel)
        self.nlLOS.flush()


#!/usr/bin/env python
"""nlogtopd.py:

This program is intended to be run in the background.
It will manage most of the NetLogger-ly behavior, such as
reading the policy file, setting the local logfile accordingly,
setting up a forwarding daemon for the local file, and setting up
our logging utility to write to that file every time we call
log(message).  Thus, it needs to be running on every machine which
does logging.

However, if it is not run, logs will all be written to stderr, and
a warning to this effect will be written to stdout when a log
is first written.

In addition, if nlogtopd is run, it will write handy top-style
system information to the log so we can monitor processes on
the local machine.

TODO:  Make this actually use the policy file, not just constants!

Added by jmyers."""

import os
import time #for sleeping!
import sys
import commands
import log.LSSTLog as LSSTLog
import apps.fw.Policy.Policy as Policy



DEBUG=False
#parameters for handy future usage
TopFrequency=5
ForwardFrequency=5

__author__="jmyers"





def getCpuUsage(vmstat="/usr/bin/vmstat"):
    """Requires vmstat.  Pretty much stolen from
    nlcpu.py in the NetLogger 3 package.

    Notably, vmstat on NCSA seems to constantly report
    the same value.  That's pretty odd.

    """
    f = os.popen(vmstat)
    f.readline()
    f.readline()
    line = f.readline()
    vals = map(int,line.split()[-4:-1])
    return vals[0] + vals[1] + vals[2]




def topProcessesDict(top="/usr/bin/top"):
    """topProcessesDict:

    calls the program top, then parses the heck out of its 
    output to return a dictionary for logging.  The dictionary
    is of field/value pairs and includes things like
    {
    'PROCESS1_CMD':'foo'
    'PROCESS1_%CPU':'85'
    'PROCESS1_%MEM':'0.1'
    'PROCESS1_USR':'jmyers'
    'PROCESS2_CMD':'bar'
    ...}
    
    Optional parameter:
       top=/path/to/top
    """
    returnDict = {}
    f = commands.getoutput(top + " -b -n 1")
    #-b = batch mode, noninteractive
    #-n 1 = run just once, then exit

    linesFromTop = f.split('\n') #split by newline

    keyLineNum = -1
    for i in range(20):
        #look for a line that contains words like PID and COMMAND
        lineFromTop = linesFromTop[i].split(None) #None = any whitespace
        if "PID" in lineFromTop or "pid" in lineFromTop:
            keyLineNum = i
            break

    if keyLineNum == -1: #no such line found! Panic!
        sys.stderr.write("Error: The utility " + top + " on this machine was"
                         + " very different from expected!  Please inform jmyers.")
        #this should probalby be logged, too! TODO
        exit(-1)
        
    key = linesFromTop[keyLineNum].split(None)
    for j in range(3):
        lineFromTop = linesFromTop[keyLineNum + j].split(None)
        for k in range(len(key)):
            if key[k] in ("%CPU", "COMMAND", "CMD", "USER", "USR", "%MEM",
                          "%cpu", "command", "user", "%mem"):
                returnDict['PROCESS' + str(j) + '_' +
                           key[k]] = lineFromTop[k]

    returnDict['TOTAL_%CPU'] = getCpuUsage()
            
    return returnDict






def main():    
    #we might want to get this from the policy file eventually
    global TopFreqency
    
    # read Policy file to determine where to send log messages
    logPolicy = Policy('./logging.policy')
    localLog = logPolicy.Get ('logFile')
    
    #create a new log object for us
    myLogger = LSSTLog.LSSTLog(localLog)

    #write to the log
    while 1:
        # get top processes, cpu usage,
        # maybe even disk i/o stats
        # using top and vmstat...
        # then write them to the local log
        # then sleep for interval seconds
        dataToReport = topProcessesDict()
        myLogger.write('TOP',
                       msgLevel=LSSTLog.Level.DEBUG,
                       extraData=dataToReport)
        time.sleep(TopFrequency)




if __name__ == '__main__':
    #daemonize!
    if os.fork()==0:
        os.setsid()
        sys.stdout=open("/dev/null", 'w')
        sys.stdin=open("/dev/null", 'r')
        if os.fork()==0:
            main()
            exit(0)
_______________________________________________
LSST-data mailing list
[email protected]
http://www.lsstmail.org/mailman/listinfo/lsst-data

Reply via email to