diff --git a/cmk/base/plugins/agent_based/graylog_alerts.py b/cmk/base/plugins/agent_based/graylog_alerts.py index 42ee419c9b9..b5095b73e45 100644 --- a/cmk/base/plugins/agent_based/graylog_alerts.py +++ b/cmk/base/plugins/agent_based/graylog_alerts.py @@ -3,41 +3,43 @@ # This file is part of Checkmk (https://checkmk.com). It is subject to the terms and # conditions defined in the file COPYING, which is part of this source code package. +""" +Kuhn & Rueß GmbH +Consulting and Development +https://kuhn-ruess.de +""" + + import json from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any +from typing import Any, NamedTuple -from .agent_based_api.v1 import check_levels, register, render, Service +from .agent_based_api.v1 import check_levels, register, Service from .agent_based_api.v1.type_defs import CheckResult, DiscoveryResult, StringTable # <<>> -# {"alerts": {"num_of_alerts": 0, "has_since_argument": false, "alerts_since": null, "num_of_alerts_in_range": 0}} +# {"alerts": {"num_of_events": 947, "num_of_alerts": 174}} # <<>> -# {"alerts": {"num_of_alerts": 5, "has_since_argument": true, "alerts_since": 1800, "num_of_alerts_in_range": 2}} +# {"alerts": {"num_of_events": 543, "num_of_alerts": 0}} -@dataclass -class AlertsInfo: +class AlertsInfo(NamedTuple): + num_of_events: int num_of_alerts: int - has_since_argument: bool - alerts_since: int | None - num_of_alerts_in_range: int def parse_graylog_alerts(string_table: StringTable) -> AlertsInfo | None: alerts_section = json.loads(string_table[0][0]) + if len(alerts_section) != 1: return None alerts_data = alerts_section.get("alerts") return AlertsInfo( + num_of_events=alerts_data.get("num_of_events"), num_of_alerts=alerts_data.get("num_of_alerts"), - has_since_argument=alerts_data.get("has_since_argument"), - alerts_since=alerts_data.get("alerts_since"), - num_of_alerts_in_range=alerts_data.get("num_of_alerts_in_range"), ) @@ -53,21 +55,14 @@ def discover_graylog_alerts(section: AlertsInfo) -> DiscoveryResult: def check_graylog_alerts(params: Mapping[str, Any], section: AlertsInfo) -> CheckResult: - yield from check_levels( - value=section.num_of_alerts, - levels_upper=params.get("alerts_upper", (None, None)), - levels_lower=params.get("alerts_lower", (None, None)), - render_func=lambda x: str(int(x)), - label="Total number of alerts", - ) - - if section.has_since_argument and section.alerts_since: + for which in ["alerts", "events"]: yield from check_levels( - value=section.num_of_alerts_in_range, - levels_upper=params.get("alerts_in_range_upper", (None, None)), - levels_lower=params.get("alerts_in_range_lower", (None, None)), + value=(section._asdict())[f"num_of_{which}"], + levels_upper=params.get(f"{which}_upper", (None, None)), + levels_lower=params.get(f"{which}_lower", (None, None)), + metric_name=f"graylog_{which}", render_func=lambda x: str(int(x)), - label=f"Total number of alerts in the last {render.timespan(section.alerts_since)}", + label=f"Total number of {which}", ) diff --git a/cmk/gui/plugins/metrics/graylog.py b/cmk/gui/plugins/metrics/graylog.py new file mode 100644 index 00000000000..03cda853d40 --- /dev/null +++ b/cmk/gui/plugins/metrics/graylog.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +""" +Kuhn & Rueß GmbH +Consulting and Development +https://kuhn-ruess.de +""" + + +from cmk.gui.i18n import _ +from cmk.gui.plugins.metrics.utils import graph_info, metric_info + +# . +# .--Metrics-------------------------------------------------------------. +# | __ __ _ _ | +# | | \/ | ___| |_ _ __(_) ___ ___ | +# | | |\/| |/ _ \ __| '__| |/ __/ __| | +# | | | | | __/ |_| | | | (__\__ \ | +# | |_| |_|\___|\__|_| |_|\___|___/ | +# | | +# +----------------------------------------------------------------------+ +# | Definitions of metrics | +# '----------------------------------------------------------------------' + +metric_info["graylog_alerts"] = { + "title": "Total amount of alerts", + "unit": "count", + "color": "blue", +} +metric_info["graylog_events"] = { + "title": "Total amount of events", + "unit": "count", + "color": "green", +} + +# . +# .--Graphs--------------------------------------------------------------. +# | ____ _ | +# | / ___|_ __ __ _ _ __ | |__ ___ | +# | | | _| '__/ _` | '_ \| '_ \/ __| | +# | | |_| | | | (_| | |_) | | | \__ \ | +# | \____|_| \__,_| .__/|_| |_|___/ | +# | |_| | +# +----------------------------------------------------------------------+ +# | Definitions of time series graphs | +# '----------------------------------------------------------------------' + +graph_info["graylog_alerts"] = { + "title": _("Graylog alerts and events"), + "metrics": [ + ("graylog_alerts", "line"), + ("graylog_events", "line"), + ], +} diff --git a/cmk/gui/plugins/wato/check_parameters/graylog_alerts.py b/cmk/gui/plugins/wato/check_parameters/graylog_alerts.py index 31d49b60c34..e400165713e 100644 --- a/cmk/gui/plugins/wato/check_parameters/graylog_alerts.py +++ b/cmk/gui/plugins/wato/check_parameters/graylog_alerts.py @@ -3,6 +3,13 @@ # This file is part of Checkmk (https://checkmk.com). It is subject to the terms and # conditions defined in the file COPYING, which is part of this source code package. +""" +Kuhn & Rueß GmbH +Consulting and Development +https://kuhn-ruess.de +""" + + from cmk.gui.i18n import _ from cmk.gui.plugins.wato.utils import ( CheckParameterRulespecWithoutItem, @@ -20,8 +27,8 @@ def _parameter_valuespec_graylog_alerts(): Tuple( title=_("Total alerts count upper levels"), elements=[ - Integer(title=_("Warning at")), - Integer(title=_("Critical at")), + Integer(title=_("Warning at"), unit="alerts"), + Integer(title=_("Critical at"), unit="alerts"), ], ), ), @@ -30,28 +37,28 @@ def _parameter_valuespec_graylog_alerts(): Tuple( title=_("Total alerts count lower levels"), elements=[ - Integer(title=_("Warning below")), - Integer(title=_("Critical below")), + Integer(title=_("Warning below"), unit="alerts"), + Integer(title=_("Critical below"), unit="alerts"), ], ), ), ( - "alerts_in_range_upper", + "events_upper", Tuple( - title=_("Number of alerts in defined timespan upper level"), + title=_("Total events count upper levesl"), elements=[ - Integer(title=_("Warning below"), unit="alerts"), - Integer(title=_("Critical below"), unit="alerts"), + Integer(title=_("Warning at"), unit="events"), + Integer(title=_("Critical at"), unit="events"), ], ), ), ( - "alerts_in_range_lower", + "events_lower", Tuple( - title=_("Number of alerts in defined timespan lower level"), + title=_("Total event count lower levels"), elements=[ - Integer(title=_("Warning at"), unit="alerts"), - Integer(title=_("Critical at"), unit="alerts"), + Integer(title=_("Warning below"), unit="events"), + Integer(title=_("Critical below"), unit="events"), ], ), ), diff --git a/cmk/special_agents/agent_graylog.py b/cmk/special_agents/agent_graylog.py index 2528485ed9d..b677faf04fd 100644 --- a/cmk/special_agents/agent_graylog.py +++ b/cmk/special_agents/agent_graylog.py @@ -3,6 +3,13 @@ # This file is part of Checkmk (https://checkmk.com). It is subject to the terms and # conditions defined in the file COPYING, which is part of this source code package. +""" +Kuhn & Rueß GmbH +Consulting and Development +https://kuhn-ruess.de +""" + + import argparse import json import sys @@ -34,7 +41,7 @@ def main(argv=None): # Add new queries here sections = [ - GraylogSection(name="alerts", uri="/streams/alerts?limit=300"), + GraylogSection(name="alerts", uri="/events/search"), GraylogSection(name="cluster_health", uri="/system/indexer/cluster/health"), GraylogSection(name="cluster_inputstates", uri="/cluster/inputstates"), GraylogSection(name="cluster_stats", uri="/system/cluster/stats"), @@ -70,6 +77,14 @@ def handle_request(args, sections): # pylint: disable=too-many-branches if section.name == "events": value = handle_response(url, args, "POST").json() + + elif section.name == "alerts": + content = { + "filter": {"alerts": "include"}, + "timerange": {"type": "relative", "range": args.alerts_since}, + } + value = handle_response(url, args, "POST", args.alerts_since, content).json() + else: value = handle_response(url, args).json() @@ -157,24 +172,24 @@ def handle_request(args, sections): # pylint: disable=too-many-branches handle_output([events], section.name, args) if section.name == "alerts": - num_of_alerts = value.get("total", 0) - num_of_alerts_in_range = 0 - alerts_since_argument = args.alerts_since - - if alerts_since_argument: - url_alerts_in_range = f"{url}%since={str(alerts_since_argument)}" - num_of_alerts_in_range = ( - handle_response(url_alerts_in_range, args).json().get("total", 0) - ) + num_of_events = value.get("total_events", 0) + + content = { + "filter": {"alerts": "only"}, + "timerange": {"type": "relative", "range": args.alerts_since}, + } + value = handle_response(url, args, "POST", args.alerts_since, content).json() + + num_of_alerts = value.get("total_events", 0) + num_of_events -= num_of_alerts alerts = { "alerts": { + "num_of_events": num_of_events, "num_of_alerts": num_of_alerts, - "has_since_argument": bool(alerts_since_argument), - "alerts_since": alerts_since_argument if alerts_since_argument else None, - "num_of_alerts_in_range": num_of_alerts_in_range, } } + handle_output([alerts], section.name, args) if section.name == "sources": @@ -219,18 +234,29 @@ def handle_request(args, sections): # pylint: disable=too-many-branches handle_output(value, section.name, args) -def handle_response(url, args, method="GET", events_since=86400): +def handle_response(url, args, method="GET", events_since=86400, content=None): if method == "POST": try: - response = requests.post( - url, - auth=(args.user, args.password), - headers={ - "Content-Type": "application/json", - "X-Requested-By": args.user, - }, - json={"timerange": {"type": "relative", "range": events_since}}, - ) + if content: + response = requests.post( + url, + auth=(args.user, args.password), + headers={ + "Content-Type": "application/json", + "X-Requested-By": args.user, + }, + json=content, + ) + else: + response = requests.post( + url, + auth=(args.user, args.password), + headers={ + "Content-Type": "application/json", + "X-Requested-By": args.user, + }, + json={"timerange": {"type": "relative", "range": events_since}}, + ) except requests.exceptions.RequestException as e: sys.stderr.write("Error: %s\n" % e) if args.debug: @@ -330,7 +356,7 @@ def parse_arguments(argv): "-m", "--sections", default=sections, - help="""Comma separated list of data to query. Possible values: %s (default: all)""" + help="""Comma seperated list of data to query. Possible values: %s (default: all)""" % ", ".join(sections), ) parser.add_argument( diff --git a/tests/unit/cmk/base/plugins/agent_based/test_graylog_alerts.py b/tests/unit/cmk/base/plugins/agent_based/test_graylog_alerts.py index d5e59979ffe..fb457d70b4c 100644 --- a/tests/unit/cmk/base/plugins/agent_based/test_graylog_alerts.py +++ b/tests/unit/cmk/base/plugins/agent_based/test_graylog_alerts.py @@ -8,7 +8,7 @@ import pytest from cmk.base.api.agent_based.type_defs import StringTable -from cmk.base.plugins.agent_based.agent_based_api.v1 import Result, State +from cmk.base.plugins.agent_based.agent_based_api.v1 import Metric, Result, State from cmk.base.plugins.agent_based.graylog_alerts import check_graylog_alerts, parse_graylog_alerts @@ -16,28 +16,44 @@ "section, expected_check_result", [ pytest.param( + [['{"alerts": {"num_of_events": 0, "num_of_alerts": 0}}']], [ - [ - '{"alerts": {"num_of_alerts": 0, "has_since_argument": false, "alerts_since": null, "num_of_alerts_in_range": 0}}' - ] + Result(state=State.OK, summary="Total number of alerts: 0"), + Metric("graylog_alerts", 0.0), + Result(state=State.OK, summary="Total number of events: 0"), + Metric("graylog_events", 0.0), ], - [Result(state=State.OK, summary="Total number of alerts: 0")], - id="Timeframe for 'alerts_since' not configured.", + id="No alerts and events.", ), pytest.param( + [['{"alerts": {"num_of_events": 53, "num_of_alerts": 0}}']], [ - [ - '{"alerts": {"num_of_alerts": 5, "has_since_argument": true, "alerts_since": 1800, "num_of_alerts_in_range": 2}}' - ] + Result(state=State.OK, summary="Total number of alerts: 0"), + Metric("graylog_alerts", 0.0), + Result(state=State.OK, summary="Total number of events: 53"), + Metric("graylog_events", 53.0), ], + id="Events exists and no alerts.", + ), + pytest.param( + [['{"alerts": {"num_of_events": 0, "num_of_alerts": 5}}']], [ Result(state=State.OK, summary="Total number of alerts: 5"), - Result( - state=State.OK, - summary="Total number of alerts in the last 30 minutes 0 seconds: 2", - ), + Metric("graylog_alerts", 5.0), + Result(state=State.OK, summary="Total number of events: 0"), + Metric("graylog_events", 0.0), + ], + id="Alerts exists and no events.", + ), + pytest.param( + [['{"alerts": {"num_of_events": 63, "num_of_alerts": 7}}']], + [ + Result(state=State.OK, summary="Total number of alerts: 7"), + Metric("graylog_alerts", 7.0), + Result(state=State.OK, summary="Total number of events: 63"), + Metric("graylog_events", 63.0), ], - id="Timeframe for 'alerts_since' configured. Now the check gives information about the total number of alerts received in the timeframe.", + id="Events and alerts exists.", ), ], ) @@ -59,6 +75,6 @@ def test_check_graylog_alerts( def test_parse_graylog_alerts_empty_alerts_section() -> None: - section = [['{"total": 0, "alerts": []}']] + section = [['{"num_of_events": 0, "num_of_alerts": 0}']] parsed_section = parse_graylog_alerts(section) assert parsed_section is None