pvl/rrd/graph.py
author Tero Marttila <terom@paivola.fi>
Sun, 07 Sep 2014 14:21:56 +0300
changeset 424 e77e967d59b0
parent 232 5894c70dc6a8
permissions -rw-r--r--
hgignore: use glob; ignore snmp mibs
from pvl.rrd import api as rrd
import time

from pvl.invoke import merge # XXX

import logging; log = logging.getLogger('pvl.rrd.graph')

"""
    RRDTool graph builders.

    Includes support for Smokeping-inspired interface graphs from MRTG/Collectd.
"""

def timestamp () :
    return time.strftime("%Y/%m/%d %H:%M:%S %Z")

class Graph (object) :
    """
        Render an RRD graph from definitions/options.
        
        Acts as a semi-magical object, with immutable state, and methods that return updated copies of our state.

        >>> Graph()
        Graph()
        >>> Graph('foo')
        Graph('foo')
        >>> Graph(bar='bar')
        Graph(bar='bar')
        >>> Graph('foo', bar='bar')
        Graph('foo', bar='bar')
        >>> Graph('foo')(bar='bar')
        Graph('foo', bar='bar')
    """

    def __init__ (self, *defs, **opts) :
        self.defs = defs
        self.opts = opts

    def __call__ (self, *defs, **opts) :
        return type(self)(*(self.defs + defs), **merge(self.opts, opts))

    def __getitem__ (self, key) :
        return self.opts[key]

    def graph (self, out) :
        """
            Render completed graph using pvl.rrd.api.graph()
        """

        return rrd.graph(out, *self.defs, **self.opts)
    
    def __repr__ (self) :
        return "{type}({args})".format(
                type    = self.__class__.__name__,
                args    = ', '.join([repr(def_) for def_ in self.defs] + [str(opt) + '=' + repr(value) for opt, value in self.opts.iteritems()]),
        )

class Interface (Graph) :
    """
        An RRD graph showing in/out traffic in bits/s on an interface, with style/interval support.
    """

    @classmethod
    def build (cls, title, rrd, style, interval) :
        """
            Return a simple Graph using the given title, RRD and style/interval

                title       - common(title=...)
                rrd         - data(rrd=...)
                style       - style(style=...)
                interval    - interval(interval=...)
        """

        return cls().common(title).source(rrd).style(style).interval(interval)

    # builders
    def common (self, title) :
        """
            Common options for all views
        """

        return self(
            # output
            imgformat           = "PNG",
            #        lazy                = True,

            color               = [
                # disable border
                # border            = 0,
                "SHADEA#ffffff00",
                "SHADEB#ffffff00",

                # keep background transparent
                "BACK#ffffff00",
                "SHADEB#ffffff00",
            ],
             
            # labels
            vertical_label      = "bits/s",
            title               = title,
            units               = "si",

            # use logarithmic scaling?
            #        logarithmic         = True,
            
            # smooth out lines
            slope_mode          = True,
        )
    
    ## data
    # DS names; in/out bytes
    IN = OUT = None

    def source (self, rrd) :
        """
            Abstract: rrd -> in/out
        """

        if not (self.IN and self.OUT) :
            raise TypeError("No IN/OUT DS names for %s" % (self, ))

        log.debug("%s", rrd)

        return self(
            # data sources, bytes/s
            r'DEF:in0={rrd}:{ds_in}:AVERAGE'.format(rrd=rrd, ds_in=self.IN),
            r'DEF:out0={rrd}:{ds_out}:AVERAGE'.format(rrd=rrd, ds_out=self.OUT),

            # data, bits/s
            'CDEF:in=in0,8,*',
            'CDEF:out=out0,8,*',
        )
    
    ## style
    def style_overview (graph) :
        """
            in/out bps -> overview graph
        """

        return graph(
            "CDEF:all=in,out,+",

            "VDEF:max=all,MAXIMUM",
            "VDEF:avg=all,AVERAGE",
            "VDEF:min=all,MINIMUM",

            "LINE1:in#0000FF:In",
            "LINE1:out#00CC00:Out",

            "GPRINT:max:%6.2lf %Sbps max",
            "GPRINT:avg:%6.2lf %Sbps avg",
            "GPRINT:min:%6.2lf %Sbps min\\l",

            # dimensions
            width               = 600,
            height              = 50,
        )

    def style_detail (graph) :
        """
            in/out bps -> detail graph
        """

        return graph(
            # values
            'VDEF:in_max=in,MAXIMUM',
            'VDEF:in_avg=in,AVERAGE',
            'VDEF:in_min=in,MINIMUM',
            'VDEF:in_cur=in,LAST',
            'VDEF:out_max=out,MAXIMUM',
            'VDEF:out_avg=out,AVERAGE',
            'VDEF:out_min=out,MINIMUM',
            'VDEF:out_cur=out,LAST',

            # legend/graph
            "COMMENT:%4s" % "",
            "COMMENT:%11s" % "Maximum",
            "COMMENT:%11s" % "Average",
            "COMMENT:%11s" % "Minimum",
            "COMMENT:%11s\\l" % "Current",

            "LINE1:in#0000FF:%4s" % "In",
            'GPRINT:in_max:%6.2lf %Sbps',
            'GPRINT:in_avg:%6.2lf %Sbps',
            'GPRINT:in_min:%6.2lf %Sbps',
            'GPRINT:in_cur:%6.2lf %Sbps\\l',

            "LINE1:out#00CC00:%4s" % "Out",
            'GPRINT:out_max:%6.2lf %Sbps',
            'GPRINT:out_avg:%6.2lf %Sbps',
            'GPRINT:out_min:%6.2lf %Sbps',
            'GPRINT:out_cur:%6.2lf %Sbps\\l',
            
            # mark
            "COMMENT:Generated {now}\\r".format(now=timestamp().replace(':', '\\:')),

            # dimensions
            width               = 600,
            height              = 200,
        )

    STYLE = {
        'overview': style_overview,
        'detail':   style_detail,
    }

    def style (self, style) :
        return self.STYLE[style](self)
    
    ## interval
    def interval_daily (graph) :
        """
            Common options for the 'daily' view
        """

        return graph(
            # labels
            x_grid              = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
        
            # general info
            title               = "Daily {graph[title]}".format(graph=graph),
        
            # interval
            start               = "-24h",
        )

    def interval_weekly (graph) :
        return graph(
            # labels
            #        x_grid              = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
        
            # general info
            title               = "Weekly {graph[title]}".format(graph=graph),
        
            # interval
            start               = "-7d",
        )

    def interval_yearly (graph) :
        return graph(
            # labels
            #        x_grid              = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
        
            # general info
            title               = "Yearly {graph[title]}".format(graph=graph),
        
            # interval
            start               = "-1y",
        )

    INTERVAL = {
        'daily':    interval_daily,
        'weekly':   interval_weekly,
        'yearly':   interval_yearly,
    }

    def interval (self, interval) :
        return self.INTERVAL[interval](self)

# specific types
class Mrtg (Interface) :
    """
        MRTG -> in/out
    """

    IN = 'ds0'
    OUT = 'ds1'

class CollectdIfOctets (Interface) :
    """
        Collectd if_octets -> in/out
    """

    IN = 'rx'
    OUT = 'tx'

INTERFACE = {
    'mrtg':         Mrtg,
    'collectd':     CollectdIfOctets,
}

def interface_type (type) :
    """
        Lookup Interface -subclass for given type.
    """

    return INTERFACE[type]

# XXX: legacy
def mrtg (style, interval, title, rrd, out) :
    return MrtgInterface.build(title, rrd, style, interval).graph(out)

def collectd_ifoctets (style, interval, title, rrd, out) :
    return CollectdIfOctets.build(title, rrd, style, interval).graph(out)

if __name__ == '__main__':
    import doctest
    doctest.testmod()