#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2016             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk 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 in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.


# <<<memcached>>>
# [localhost:11211]
#          accepting_conns           1
#                auth_cmds           0
#              auth_errors           0
#                    bytes           0
#               bytes_read          66
#    ...


memcached_aggregates = [
    ('bytes_percent',  lambda readings:
                                float(readings['bytes']) / float(readings['limit_maxbytes'])),
    ('cache_hit_rate', lambda readings: float(readings['cmd_get']) > 0 and
                                (float(readings['get_hits']) / float(readings['cmd_get'])) or 1.0)
]


class Uptime(int):
    pass


memcached_traits = [
    ("System Information", {
        'pid':                   {'name': "PID", 'type': int},
        'pointer_size':          {'name': "Architecture", 'fixed': 64},
        'uptime':                {'name': "Uptime", 'type': Uptime},
        'version':               {'name': "Version", 'type': str, 'lower_bounds': ("1.4.15", "1.4.15")},
        'rusage_system':         {'name': "CPU usage system", 'upper_bounds': None},
        'rusage_user':           {'name': "CPU usage user", 'upper_bounds': None},
        'threads':               {'name': "Threads", 'upper_bounds': None},
    }),
    ("Operational", {
        'accepting_conns':       {'name': "Accepting Connections", 'type': int, 'fixed': 1},
    }),
    ("Authentification", {
        'auth_cmds':             {'name': "Authentifications", 'upper_bounds': None},
        'auth_errors':           {'name': "Failed Authentifications", 'upper_bounds': None},
    }),
    ("Cache Data", {
        'bytes_percent':         {'name': "Cache usage", 'upper_bounds': (0.8,      0.9)},
        'bytes_read':            {'name': "Bytes read", 'upper_bounds': None},
        'bytes_written':         {'name': "Bytes written", 'upper_bounds': None},
        'curr_items':            {'name': "Cached items", 'upper_bounds': None},
        'evictions':             {'name': "Evictions", 'upper_bounds': (100,      200)},
        'get_hits':              {'name': "GET hits", 'upper_bounds': None},
        'get_misses':            {'name': "GET misses", 'upper_bounds': None},
        'total_connections':     {'name': "Connections", 'upper_bounds': None},
        'total_items':           {'name': "Items", 'upper_bounds': None},
        'cache_hit_rate':        {'name': "Hit rate", 'lower_bounds': (0.9,      0.8)},
    }),
    ("CAS Data", {
        'cas_badval':            {'name': "CAS bad value", 'upper_bounds': (5,        10)},
        'cas_hits':              {'name': "CAS hits", 'upper_bounds': None},
        'cas_misses':            {'name': "CAS misses", 'upper_bounds': None},
    }),
    ("Commands", {
        'cmd_flush':             {'name': "FLUSH commands", 'upper_bounds': (1,        5)},
        'cmd_get':               {'name': "GET commands", 'upper_bounds': None},
        'cmd_set':               {'name': "SET commands", 'upper_bounds': None},
    }),
    ("Connections", {
        'connection_structures': {'name': "Connection Structures", 'upper_bounds': None},
        'curr_connections':      {'name': "open connections", 'upper_bounds': None},
        'listen_disabled_num':   {'name': "Times listen disabled", 'upper_bounds': (5,        10)},
    }),
    ("Connection Overflow", {
        'conn_yields':           {'name': "Connection yields", 'upper_bounds': (1,        5)},
    }),
    ("Increase/Decrease", {
        'decr_hits':             {'name': "Decrease hits", 'upper_bounds': None},
        'decr_misses':           {'name': "Decrease misses", 'upper_bounds': None},
        'incr_hits':             {'name': "Increase hits", 'upper_bounds': None},
        'incr_misses':           {'name': "Increase misses", 'upper_bounds': None},
    }),
    ("Deletions", {
        'delete_hits':           {'name': "Delete hits", 'upper_bounds': None},
        'delete_misses':         {'name': "Delete misses", 'upper_bounds': (1000,     2000)},
    }),
    ("Reclaim", {
        'reclaimed':             {'name': "Reclaimed", 'upper_bounds': None}
    })
]


memcached_factory_settings = {}
for group, values in memcached_traits:
    for key, traits in values.iteritems():
        bounds = [trait for trait_key, trait in traits.iteritems()
                  if trait_key in ['fixed', 'upper_bounds', 'lower_bounds']]
        if bounds and bounds[0] is not None:
            memcached_factory_settings[key] = bounds[0]
factory_settings['memcached_default_levels'] = memcached_factory_settings


def parse_memcached(info):
    instances = {}
    current_instance = None
    for line in info:
        if not line:
            continue

        if line[0].startswith("["):
            current_instance = line[0].strip("[]")
            instances[current_instance] = {}
        elif current_instance is None:
            raise Exception("expected instance name")
        else:
            instances[current_instance][line[0]] = line[1]
    return instances


def inventory_memcached(parsed):
    # one item per memcached instance
    return [(key, {}) for key in parsed.keys()]


def check_memcached(item, params, parsed):
    def expect_order(*args):
        arglist = filter(lambda x: x != None, args)
        sorted_by_val = sorted(enumerate(arglist), key=lambda x: x[1])
        return max([abs(x[0] - x[1][0]) for x in enumerate(sorted_by_val)])

    def format_value(val):
        if isinstance(val, float):
            return "%.1f" % val
        elif isinstance(val, Uptime):
            days, val = divmod(val, 79800)
            hours, val = divmod(val, 3600)
            minutes = val / 60
            return "%dd %dh %dm" % (days, hours, minutes)
        else:
            return "%s" % val

    if item in parsed:
        status = []
        readings = parsed[item]
        # calculate aggregates
        for aggregate, func in memcached_aggregates:
            try:
                readings[aggregate] = func(readings)
            except KeyError:
                # stat missing from output
                pass

        for group, checks in memcached_traits:
            fails = False
            count = 0
            for key, traits in checks.iteritems():
                if key not in readings:
                    # stat missing in output
                    continue
                count += 1
                reading = traits.get('type', float)(readings[key])
                if 'upper_bounds' in traits:
                    warn, crit = params.get(key, (None, None))
                    status = expect_order(reading, warn, crit)
                    if status != 0:
                        fails = True
                        yield status, "%s = %s (warn/crit at %s/%s)" %\
                            (traits['name'], format_value(reading), warn, crit)
                    if type(reading) in [int, float]:
                        yield 0, None, [(key, reading, warn, crit)]

                elif 'lower_bounds' in traits:
                    warn, crit = params.get(key, (None, None))
                    status = expect_order(crit, warn, reading)
                    if status != 0:
                        fails = True
                        yield status, "%s = %s (warn/crit below %s/%s)" %\
                            (traits['name'], format_value(reading), warn, crit)
                    if type(reading) in [int, float]:
                        yield 0, None, [(key, reading)]

                elif 'fixed' in traits:
                    if reading != params.get(key, reading):
                        fails = True
                        yield 2, "%s = %s" % (traits['name'], format_value(reading))

                else:
                    yield 0, "%s = %s" % (traits['name'], format_value(reading))

            if not fails:
                if count > 0:
                    yield 0, "%s OK" % group
                else:
                    yield 1, "%s No Stats" % group


check_info['memcached'] = {
    'inventory_function'      : inventory_memcached,
    'check_function'          : check_memcached,
    'parse_function'          : parse_memcached,
    'has_perfdata'            : True,
    'service_description'     : "Memcached %s",
    'default_levels_variable' : "memcached_default_levels",
    'group'                   : "memcached"
}

