#!/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 is not 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"
}
