#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2013             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.

# MB warn, crit
#jolokia_metrics_mem_default_levels = (2000, 3000) #

# Number of threads warn, crit
jolokia_metrics_threads_default_levels  = (80, 100)

# Number of sessions low crit, low warn, high warn, high crit
jolokia_metrics_app_sess_default_levels = (-1, -1, 800, 1000)

# Number of requests low crit, low warn, high warn, high crit
jolokia_metrics_serv_req_default_levels = (-1, -1, 5000, 6000)

jolokia_metrics_queue_default_levels = ( 20, 50 )


def jolokia_metrics_parse(info):
    parsed = {}
    for inst, var, value in info:
        app, servlet = None, None
        if ',' in inst:
            parts = inst.split(',')
            if len(parts) == 3:
                inst, app, servlet = parts
            else:
                inst, app = parts

        parsed.setdefault(inst, {})
        if servlet:
            parsed[inst].setdefault('apps', {})
            parsed[inst]['apps'].setdefault(app, {})
            parsed[inst]['apps'][app].setdefault('servlets', {})
            parsed[inst]['apps'][app]['servlets'].setdefault(servlet, {})
            parsed[inst]['apps'][app]['servlets'][servlet][var] = value
        elif app:
            parsed[inst].setdefault('apps', {})
            parsed[inst]['apps'].setdefault(app, {})
            parsed[inst]['apps'][app][var] = value
        else:
            parsed[inst][var] = value
    return parsed


# This bisects the app server and its values
def jolokia_metrics_app(info, (inst, app)):
    parsed = jolokia_metrics_parse(info)
    if not inst in parsed \
       or not app in parsed[inst].get('apps', {}):
        return None
    return parsed[inst]['apps'][app]


# This bisects info from BEA and passes on to jolokia_metrics_app
def jolokia_metrics_serv(info, (inst, app, serv)):
    app = jolokia_metrics_app(info, (inst, app))
    if not app or not serv in app.get('servlets', {}):
        return None
    return app['servlets'][serv]


def inventory_jolokia_metrics(info, what):
    parsed = jolokia_metrics_parse(info)
    levels = None

    if what == 'mem':
        levels = {}
    elif what == 'threads':
        levels = 'jolokia_metrics_threads_default_levels'

    return [ (k, levels) for k in parsed ]


def inventory_jolokia_metrics_apps(info, what):
    inv = []
    parsed = jolokia_metrics_parse(info)

    if what == 'app_sess':
        levels = 'jolokia_metrics_app_sess_default_levels'
        needed_key = [ "Sessions", "activeSessions" ]
    elif what  == 'bea_app_sess':
        levels = 'jolokia_metrics_app_sess_default_levels'
        needed_key = [ "OpenSessionsCurrentCount" ]
    elif what == 'queue':
        needed_key = [ "QueueLength" ]
        levels = "jolokia_metrics_queue_default_levels"
    # Only works on BEA
    elif what == 'bea_requests':
        needed_key = [ "CompletedRequestCount" ]
        levels = None
    elif what == 'requests':
        needed_key = [ "requestCount" ]
        levels = None
    elif what == 'threads':
        needed_key = [ "StandbyThreadCount" ]
        levels = None
    else:
        needed_key = [ "Running", "stateName" ]
        levels = None

    # this handles information from BEA, they stack one level
    # higher than the rest.
    if what == 'bea_app_sess':
        for inst, vals in parsed.iteritems():
            for app, appstate in vals.get('apps', {}).items():
                if 'servlets' in appstate:
                    for nk in needed_key:
                        for servlet in appstate['servlets']:
                            if nk in appstate['servlets'][servlet]:
                                inv.append(('%s %s %s' % (inst, app, servlet), levels))
                                continue
    # This does the same for tomcat
    for inst, vals in parsed.iteritems():
        for app, appstate in vals.get('apps', {}).items():
            for nk in needed_key:
                if nk in appstate:
                    inv.append(('%s %s' % (inst, app), levels))
                    continue
    return inv


def inventory_jolokia_metrics_serv(info, what):
    inv = []
    parsed = jolokia_metrics_parse(info)
    levels = None
    if what == 'serv_req':
        levels = 'jolokia_metrics_serv_req_default_levels'
        needed_key = "Requests"
    for inst, vals in parsed.iteritems():
        for app, val in vals.get('apps', {}).iteritems():
            for serv, servinfo in val.get('servlets', {}).items():
                if needed_key in servinfo:
                    inv.append(('%s %s %s' % (inst, app, serv), levels))
    return inv


def check_jolokia_metrics_mem(item, params, info):
    parsed = jolokia_metrics_parse(info)
    if item not in parsed:
        return (3, "data not found in agent output")


    # convert old parameter version ( warn, crit )
    # represented levels of total heap
    if type(params) == tuple:
        params = {"total": params}

    # rename totalheap to total
    # this block can be removed in the future (today 22.02.13)
    if "totalheap" in params:
        params["total"] = params["totalheap"]
        del params["totalheap"]

    d = parsed[item]
    mb = 1024 * 1024.0
    heap = saveint(d["HeapMemoryUsage"]) / mb
    heapmax = saveint(d.get("HeapMemoryMax",-1)) / mb
    nonheap = saveint(d["NonHeapMemoryUsage"]) / mb
    nonheapmax = saveint(d.get("NonHeapMemoryMax",-1)) / mb
    total = heap + nonheap
    if heapmax > 0 and nonheapmax > 0:
        totalmax = heapmax + nonheapmax
    else:
        totalmax   = ""
        if heapmax < 0:
            heapmax = ""
        if nonheapmax < 0:
            nonheapmax = ""

    state_sign = { 0: "", 1: "(!)", 2: "(!!)" }
    worst_state = 0
    perfdata = []
    info_list = []

    for (what, value, value_max) in [
            ("heap", heap, heapmax),
            ("nonheap",  nonheap, nonheapmax),
            ("total",  total, totalmax),
        ]:
        param_state = 0
        level_info  = ""
        used_info   = ""
        if params.get(what):
            warn_level = ""
            crit_level = ""
            if type(params[what][0]) == int:
                if what != "total":
                    if value_max:
                        warn_level = value_max - params[what][0]
                        crit_level = value_max - params[what][1]
                    perfdata.append((what, value, warn_level, crit_level, "", value_max))
                if not value_max:
                    param_state = 0
                elif value >= crit_level:
                    param_state = 2
                    level_info = "%s(crit at %sMB free)" % (state_sign[2], params[what][1])
                elif value >= warn_level:
                    param_state = 1
                    level_info = "%s(warn at %sMB free)" % (state_sign[1], params[what][0])
            else:
                if what != "total":
                    if value_max:
                        warn_level = value_max * params[what][0] / 100.0
                        crit_level = value_max * params[what][1] / 100.0
                    perfdata.append((what, value, warn_level, crit_level, "", value_max))
                if not value_max:
                    param_state = 0
                elif value >= crit_level:
                    param_state = 2
                    level_info = "%s(crit at %s%%)" % (state_sign[2], params[what][1])
                elif value >= warn_level:
                    param_state = 1
                    level_info = "%s(warn at %s%%)" % (state_sign[1], params[what][0])
        else:
            if what != "total":
                perfdata.append((what, value, "", "", "", value_max))

        if value_max:
            used_info = "/%.1f%% used" % (value / value_max * 100)
        info_list.append("%s: %0.fMB%s%s" % (what.title(), value, used_info, level_info))
        worst_state = max(param_state, worst_state)

    return (worst_state, ', '.join(info_list), perfdata)

def check_jolokia_metrics_threads(item, params, info):
    warn, crit = params
    parsed = jolokia_metrics_parse(info)
    if item not in parsed:
        return (3, "data not found in agent output")
    d = parsed[item]

    this_time = time.time()
    wrapped = False
    perfdata = []
    output   = []
    status   = 0
    for key in [ 'ThreadCount', 'DeamonThreadCount', 'PeakThreadCount', 'TotalStartedThreadCount' ]:
        val = saveint(d[key])
        if key == 'ThreadCount':
            # Thread count might lead to a warn/crit state
            if val >= crit:
                status = 2
            elif val >= warn:
                status = 1

            # Calculate the thread increase rate
            try:
                timedif, rate = get_counter("jolokia_metrics.threads.%s" % item, this_time, val)
                output.append('ThreadRate: %0.2f' % rate)
                perfdata.append(('ThreadRate', rate))
            except MKCounterWrapped:
                wrapped = True

        perfdata.append((key, val))
        output.append('%s: %d' % (key, val))
    # Only process the perfdata when no wrap occured
    if wrapped:
        return (status, ', '.join(output))
    else:
        return (status, ', '.join(output), perfdata)

def check_jolokia_metrics_uptime(item, _unused, info):
    parsed = jolokia_metrics_parse(info)
    if item not in parsed:
        return (3, "data not found in agent output")
    uptime = saveint(parsed[item]['Uptime']) / 1000

    seconds = uptime % 60
    rem = uptime / 60
    minutes = rem % 60
    hours = (rem % 1440) / 60
    days = rem / 1440
    now = int(time.time())
    since = time.strftime("%c", time.localtime(now - uptime))
    return (0, "up since %s (%dd %02d:%02d:%02d)" % (since, days, hours, minutes, seconds), [ ("uptime", uptime) ])


def check_jolokia_metrics_app_state(item, _unused, info):
    app_state=3
    app = jolokia_metrics_app(info, item.split())

    #print app
    # FIXME: this could be nicer.
    if   app and "Running" in app:
        if app['Running']   == '1':
            app_state = 0
        else:
            app_state = 2
    # wenn in app statename steht
    elif app and "stateName" in app:
        if app['stateName'] == 'STARTED':
            app_state = 0
        else:
            app_state = 2
    if   app_state == 3:
        return (3, "data not found in agent output")
    elif app_state == 0:
        return (0, 'application is running')
    elif app_state == 2:
        return (2, 'application is not running (Running: %s)')


    return (3, 'error in agent output')



def check_jolokia_metrics_app_sess(item, params, info):
    lo_crit, lo_warn, hi_warn, hi_crit = params
    if   len(item.split()) == 3:
        app = jolokia_metrics_serv(info, item.split())
    elif len (item.split()) == 2:
        app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")
    sessions = app.get('Sessions', app.get('activeSessions', app.get('OpenSessionsCurrentCount')))
    if sessions == None:
        return (3, "data not found in agent output")
    sess = saveint(sessions)
    maxActive = saveint(app.get('Sessions', app.get('maxActiveSessions', app.get('OpenSessionsCurrentCount'))))

    status = 0
    status_txt = ''
    if lo_crit is not None and sess <= lo_crit:
        status = 2
        status_txt = ' (Below or equal %d)' % lo_crit
    elif lo_warn is not None and sess <= lo_warn:
        status = 1
        status_txt = ' (Below or equal %d)' % lo_warn
    elif hi_crit is not None and sess >= hi_crit:
        status = 2
        status_txt = ' (Above or equal %d)' % lo_warn
    elif hi_warn is not None and sess >= hi_warn:
        status = 1
        status_txt = ' (Above or equal %d)' % lo_crit

    if maxActive and maxActive > 0:
        status_txt += " (max active sessions: %d)" % maxActive

    return (status, '%d Sessions%s' % (sess, status_txt),
            [('sessions', sess, hi_warn, hi_crit)])


def check_jolokia_metrics_serv_req(item, params, info):
    lo_crit, lo_warn, hi_warn, hi_crit = params
    serv = jolokia_metrics_serv(info, item.split())
    if not serv or not 'Requests' in serv:
        return (3, "data not found in agent output")
    req = saveint(serv['Requests'])

    status    = 0
    status_txt = ''
    if lo_crit is not None and req <= lo_crit:
        status = 2
        status_txt = ' (Below or equal %d)' % lo_crit
    elif lo_warn is not None and req <= lo_warn:
        status = 1
        status_txt = ' (Below or equal %d)' % lo_warn
    elif hi_crit is not None and req >= hi_crit:
        status = 2
        status_txt = ' (Above or equal %d)' % lo_warn
    elif hi_warn is not None and req >= hi_warn:
        status = 1
        status_txt = ' (Above or equal %d)' % lo_crit

    output    = ['Requests: %d%s' % (req, status_txt)]
    perfdata  = [('Requests', req, hi_warn, hi_crit)]
    wrapped   = False
    this_time = time.time()
    try:
        timedif, rate = get_counter("jolokia_metrics.serv_req.%s" % item, this_time, req)
        output.append('RequestRate: %0.2f' % rate)
        perfdata.append(('RequestRate', rate))
    except MKCounterWrapped:
        wrapped = True

    if wrapped:
        return (status, ', '.join(output))
    else:
        return (status, ', '.join(output), perfdata)


def check_jolokia_metrics_bea_queue(item, params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")
    if "QueueLength" not in app:
        return (3, "data not found in agent output")
    queuelength = int(app['QueueLength'])

    status = 0
    warn, crit = params
    if queuelength >= crit:
        status = 2
    elif queuelength >= warn:
        status = 1
    return (status, 'queue length is %d' % queuelength,
             [("length", queuelength, warn, crit)])


# FIXME: This check could work with any JVM
# It has no levels
# A candidate for 1.2.1 overhaul
def check_jolokia_metrics_bea_requests(item, _no_params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")

    for nk in [ "CompletedRequestCount", "requestCount" ]:
       if nk in app:
           requests = int(app[nk])
           timedif, rate = get_counter("j4p.bea.requests.%s" % item, time.time(), requests)
           return (0, "%.2f requests/sec" % rate, [("rate", rate)])

    return (3, "data not found in agent output")


def check_jolokia_metrics_bea_threads(item, _no_params, info):
    app = jolokia_metrics_app(info, item.split())
    if not app:
        return (3, "application not found")

    perfdata = []
    infos = []
    for varname, title in [
        ( "ExecuteThreadTotalCount", "total" ),
        ( "ExecuteThreadIdleCount", "idle" ),
        ( "StandbyThreadCount", "standby" ),
        ( "HoggingThreadCount", "hogging" ) ]:
        value = int(app[varname])
        perfdata.append((varname, value))
        infos.append("%s: %d" % (title, value))

    return (0, ", ".join(infos), perfdata)



# General JVM checks
check_info["jolokia_metrics.mem"] = {
    "service_description"     : "JVM %s Memory",
    "check_function"          : check_jolokia_metrics_mem,
    "inventory_function"      : lambda i: inventory_jolokia_metrics(i, "mem"),
    "has_perfdata"            : True,
    "group"                   : "jvm_memory",
    "default_levels_variable" : "jolokia_metrics_mem_default_levels"
}

check_info["jolokia_metrics.threads"] = {
    "service_description" : "JVM %s Threads",
    "check_function"      : check_jolokia_metrics_threads,
    "inventory_function"  : lambda i: inventory_jolokia_metrics(i, "threads"),
    "has_perfdata"        : True,
}

check_info["jolokia_metrics.uptime"] = {
    "service_description" : "JVM %s Uptime",
    "check_function"      : check_jolokia_metrics_uptime,
    "inventory_function"  : lambda i: inventory_jolokia_metrics(i, "uptime"),
    "has_perfdata"        : True,
}

# Application specific checks
# Note: we could simply pass in "nk" as "what".
# Not changing since there might be something smarter.
check_info["jolokia_metrics.app_state"] = {
    "service_description" : "JVM %s State",
    "check_function"      : check_jolokia_metrics_app_state,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "app_state"),
    "has_perfdata"        : False,
}

check_info["jolokia_metrics.app_sess"] = {
    "service_description" : "JVM %s Sessions",
    "check_function"      : check_jolokia_metrics_app_sess,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "app_sess"),
    "has_perfdata"        : True,
}

check_info["jolokia_metrics.requests"] = {
    "service_description" : "JVM %s Requests",
    "check_function"      : check_jolokia_metrics_bea_requests,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "requests"),
    "has_perfdata"        : True,
}

# Servlet specific checks
check_info["jolokia_metrics.serv_req"] = {
    "service_description" : "JVM %s Requests",
    "check_function"      : check_jolokia_metrics_serv_req,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_serv(i, "serv_req"),
    "has_perfdata"        : True,
}

# Stuff found on BEA Weblogic
check_info["jolokia_metrics.bea_queue"] = {
    "service_description" : "JVM %s Queue",
    "check_function"      : check_jolokia_metrics_bea_queue,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "queue"),
    "has_perfdata"        : True,
}

check_info["jolokia_metrics.bea_requests"] = {
    "service_description" : "JVM %s Requests",
    "check_function"      : check_jolokia_metrics_bea_requests,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "bea_requests"),
    "has_perfdata"        : True,
}

check_info["jolokia_metrics.bea_threads"] = {
    "service_description" : "JVM %s Threads",
    "check_function"      : check_jolokia_metrics_bea_threads,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "threads"),
    "has_perfdata"        : True,
}

check_info["jolokia_metrics.bea_sess"] = {
    "service_description" : "JVM %s Sessions",
    "check_function"      : check_jolokia_metrics_app_sess,
    "inventory_function"  : lambda i: inventory_jolokia_metrics_apps(i, "bea_app_sess"),
    "has_perfdata"        : True,
}

