From 6ad4983709fb1ef74e2d0984a794a376770a5118 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Tue, 10 Feb 2026 11:03:34 +0100 Subject: [PATCH] Add graylog_input_metrics ported forward to CheckMK 2.4. --- .../2.4/graylog_input_metrics-1.0.0.mkp | Bin 0 -> 6317 bytes .../agent_based/graylog_input_metrics.py | 214 ++++++++++++++++++ .../graphing/graylog_input_metrics.py | 46 ++++ .../libexec/agent_graylog_input_metrics | 135 +++++++++++ .../rulesets/graylog_input_metrics_agent.py | 103 +++++++++ .../rulesets/graylog_input_metrics_checks.py | 81 +++++++ .../graylog_input_metrics.py | 30 +++ 7 files changed, 609 insertions(+) create mode 100755 graylog-metrics/2.4/graylog_input_metrics-1.0.0.mkp create mode 100644 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/agent_based/graylog_input_metrics.py create mode 100644 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/graphing/graylog_input_metrics.py create mode 100755 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/libexec/agent_graylog_input_metrics create mode 100644 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_agent.py create mode 100644 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_checks.py create mode 100644 graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/server_side_calls/graylog_input_metrics.py diff --git a/graylog-metrics/2.4/graylog_input_metrics-1.0.0.mkp b/graylog-metrics/2.4/graylog_input_metrics-1.0.0.mkp new file mode 100755 index 0000000000000000000000000000000000000000..d31a007bc323c1e54af0fce533f6620d23014bda GIT binary patch literal 6317 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 literal 0 HcmV?d00001 diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/agent_based/graylog_input_metrics.py b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/agent_based/graylog_input_metrics.py new file mode 100644 index 0000000..04a0f93 --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/agent_based/graylog_input_metrics.py @@ -0,0 +1,214 @@ +# Copyright (C) 2026 Spearhead Systems SRL +# Copyright (C) 2026 Checkmk GmbH + +#<<>> +# {"641e88d05d447a677efde199": {"input_state": "FAILED", "input_name": "kafka_cef_test", +# "input_type": "CEF Kafka", "input_port": null, "im_m1_rate": 0.0, "im_m5_rate": 0.0, +# "im_m15_rate": 0.0, "rs_m1_rate": 0.0, "rs_m5_rate": 0.0, "rs_m15_rate": 0.0}, +# "641e32885d447a677efd2dbf": {"input_state": "RUNNING", "input_name": "UDP-test", +# "input_type": "Syslog UDP", "input_port": 1514, "im_m1_rate": 1.0846244336700077, +# "im_m5_rate": 1.3700826278955827, "im_m15_rate": 1.254406787430692, "rs_m1_rate": 145.45579305762527, +# "rs_m5_rate": 180.6486220431909, "rs_m15_rate": 165.26666376319292}, +# "641e32795d447a677efd2d9e": {"input_state": "RUNNING", "input_name": "testTCP", "input_type": "Syslog TCP", +# "input_port": 1515, "im_m1_rate": 1.057872514816615, "im_m5_rate": 1.364957693749168, +# "im_m15_rate": 1.2528742858546844, "rs_m1_rate": 140.4719944116262, "rs_m5_rate": 178.57816158901215, +# "rs_m15_rate": 163.80530659055356}} + +import json +from cmk.agent_based.v2 import ( + Result, + Service, + Metric, + State, + CheckPlugin, + AgentSection, + render, +) + + + + +def parse_graylog_input_metrics(section): + if not section: + return {} + try: + return json.loads(section[0][0]) + except (json.JSONDecodeError, IndexError): + return {} + + +def discover_graylog_input_metrics(section): + for input_id, input_info in section.items(): + input_name = input_info["input_name"] + yield Service(item=f"{input_name} ({input_id})") + + +def render_msgs(num_msgs): + return f"{num_msgs:.2f} msgs" + + +check_configs = [ + ("im_m1_rate", render_msgs, "Incoming messages/sec 1m"), + ("im_m5_rate", render_msgs, "5m"), + ("im_m15_rate", render_msgs, "15m"), + ("rs_m1_rate", render.bytes, "Incoming bytes/sec 1m"), + ("rs_m5_rate", render.bytes, "5m"), + ("rs_m15_rate", render.bytes, "15m"), +] + + +def check_graylog_input_metrics(item, params, section): + item_id = item.split()[-1][1:-1] + input_info = section.get(item_id) + if not input_info: + return + + input_state = input_info["input_state"] + + state = State.WARN + if input_state == "RUNNING": state = State.OK + elif input_state == "FAILED": state = State.CRIT + + yield Result(state=state, summary=f"State: {input_state}") + yield Result(state=State.OK, summary="Type: %s" % input_info["input_type"]) + + if input_info["input_port"]: + yield Result(state=State.OK, summary="Port: %s" % input_info["input_port"]) + + for metric_name, render_func, label in check_configs: + value = input_info.get(metric_name) + if value is None: + continue + + levels_upper = params.get(metric_name, {}).get("upper") + levels_lower = params.get(metric_name, {}).get("lower") + + yield from check_levels( + value, + levels_upper = levels_upper, + levels_lower = levels_lower, + metric_name = metric_name, + render_func = render_func, + label = label, + ) + + +# A customer wanted us to support <= for "below", not <. This meant copying +# check_levels() and child functions wholesale out of +# lib/python3.12/site-packages/cmk/agent_based/v2/_check_levels.py just to +# change < to <= in _check_fixed_levels(). +# +# To avoid copying even more code, we're importing some child functions from +# _check_levels, but this comes with some brittleness: if Tribe29 makes +# substantial changes in these files, this plugin might break, and further +# surgery will be needed here. :( +# +# Rock and a hard place. Sometimes a sledgehammer is what it takes... D: +# Start copy =================================================================== +from dataclasses import replace +from cmk.agent_based.v2._check_levels import ( + CheckLevelsResult, + Type, + Direction, + _make_prediction_metric, + _summarize_predictions, + _levels_text, +) + + +def _check_fixed_levels(value, levels, levels_direction, render_func): + warn_level, crit_level = levels + levels_text = _levels_text(levels, levels_direction, render_func) + + if levels_direction == Direction.UPPER: + if value >= crit_level: + return CheckLevelsResult(Type.FIXED, State.CRIT, levels, levels_text) + if value >= warn_level: + return CheckLevelsResult(Type.FIXED, State.WARN, levels, levels_text) + else: + if value <= crit_level: + return CheckLevelsResult(Type.FIXED, State.CRIT, levels, levels_text) + if value <= warn_level: + return CheckLevelsResult(Type.FIXED, State.WARN, levels, levels_text) + + return CheckLevelsResult(Type.FIXED, State.OK, levels) + + +def _check_predictive_levels(value, metric_name, predicted_value, levels, levels_direction, render_func): + if levels is None: + return CheckLevelsResult( + type=Type.PREDICTIVE, + state=State.OK, + levels=None, + levels_text="", + prediction=_make_prediction_metric(metric_name, predicted_value, levels_direction), + ) + + return replace( + _check_fixed_levels(value, levels, levels_direction, render_func), + type=Type.PREDICTIVE, + prediction=_make_prediction_metric(metric_name, predicted_value, levels_direction), + ) + + +def _check_levels(value, levels, levels_direction, render_func): + match levels: + case None | ("no_levels", None): + return CheckLevelsResult(Type.NO_LEVELS, State.OK) + + case "fixed", (warn, crit): + assert isinstance(warn, (float, int)) and isinstance(crit, (float, int)) + return _check_fixed_levels(value, (warn, crit), levels_direction, render_func) + + case "predictive", (metric, prediction, p_levels): + assert isinstance(metric, str) + assert prediction is None or isinstance(prediction, (float, int)) + assert p_levels is None or isinstance(p_levels, tuple) + return _check_predictive_levels( + value, metric, prediction, p_levels, levels_direction, render_func + ) + + case other: + raise TypeError(f"Incorrect level parameters: {other!r}") + + +def check_levels(value, *, levels_upper, levels_lower, metric_name, render_func, label): + value_string = render_func(value) + info_text = f"{label}: {value_string}" if label else value_string + + result_upper = _check_levels(value, levels_upper, Direction.UPPER, render_func) + result_lower = _check_levels(value, levels_lower, Direction.LOWER, render_func) + + state = State.worst(result_upper.state, result_lower.state) + prediction_metrics, prediction_text = _summarize_predictions( + result_upper, result_lower, render_func + ) + + messages = [info_text, prediction_text, result_upper.levels_text, result_lower.levels_text] + summary = " ".join(m for m in messages if m) + yield Result(state=state, summary=summary) + + if metric_name: + yield Metric( + metric_name, + value, + levels=result_upper.levels, + boundaries=None, + ) + yield from prediction_metrics +# End copy ==================================================================== + + +agent_section_graylog_input_metrics = AgentSection( + name = "graylog_input_metrics", + parse_function = parse_graylog_input_metrics +) + +check_plugin_graylog_input_metrics = CheckPlugin( + name = "graylog_input_metrics", + check_ruleset_name = "graylog_input_metrics", + service_name = "Graylog Input %s", + discovery_function = discover_graylog_input_metrics, + check_function = check_graylog_input_metrics, + check_default_parameters = {}, +) diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/graphing/graylog_input_metrics.py b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/graphing/graylog_input_metrics.py new file mode 100644 index 0000000..c00edba --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/graphing/graylog_input_metrics.py @@ -0,0 +1,46 @@ +# Copyright (C) 2026 Spearhead Systems SRL + +from cmk.graphing.v1 import metrics, Title + + +UNIT_BYTES = metrics.Unit(metrics.IECNotation("bytes/sec")) +UNIT_MSGS = metrics.Unit(metrics.IECNotation("msgs/sec")) + + +metric_graylog_input_metrics_im_m1_rate = metrics.Metric( + title = Title("Incoming messages/sec (1 min)"), + name = "im_m1_rate", + unit = UNIT_BYTES, + color = metrics.Color.LIGHT_GREEN, +) +metric_graylog_input_metrics_im_m5_rate = metrics.Metric( + title = Title("Incoming messages/sec (5 min)"), + name = "im_m5_rate", + unit = UNIT_BYTES, + color = metrics.Color.GREEN, +) +metric_graylog_input_metrics_im_m15_rate = metrics.Metric( + title = Title("Incoming messages/sec (15 min)"), + name = "im_m15_rate", + unit = UNIT_BYTES, + color = metrics.Color.DARK_GREEN, +) + +metric_graylog_input_metrics_rs_m1_rate = metrics.Metric( + title = Title("Incoming bytes/sec (1 min)"), + name = "rs_m1_rate", + unit = UNIT_MSGS, + color = metrics.Color.LIGHT_BLUE, +) +metric_graylog_input_metrics_rs_m5_rate = metrics.Metric( + title = Title("Incoming bytes/sec (5 min)"), + name = "rs_m5_rate", + unit = UNIT_MSGS, + color = metrics.Color.BLUE, +) +metric_graylog_input_metrics_rs_m15_rate = metrics.Metric( + title = Title("Incoming bytes/sec (15 min)"), + name = "rs_m15_rate", + unit = UNIT_MSGS, + color = metrics.Color.DARK_BLUE, +) diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/libexec/agent_graylog_input_metrics b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/libexec/agent_graylog_input_metrics new file mode 100755 index 0000000..eace478 --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/libexec/agent_graylog_input_metrics @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Spearhead Systems SRL +# Copyright (C) 2019 Checkmk GmbH + +import argparse +import json +import sys + +import requests +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def main(argv=None): + args = parse_arguments(argv) + if args.demo: + return print_demo() + handle_request(args) + + +def handle_request(args): # pylint: disable=too-many-branches + url_base = f"{args.proto}://{args.hostname}:{args.port}/api" + url = url_base + "/system/metrics" + value = handle_response(url, args).json() + + # Handle the input_metrics section. We need to merge information from + # both inputstates and input_metrics, and we do that by extracting the + # ids returned by both calls. Once merged, we return a single dictionary + # with state, name, type, port, and the various rates (either raw or + # incomingMessages). + url_inputs_data = url_base + "/cluster/inputstates" + inputs_data = handle_response(url_inputs_data, args).json() + inputs_data = tuple(inputs_data.values())[0] + metrics_data = value.get("meters") + + if inputs_data is None or metrics_data is None: + return + + # Create a dictionary, containing all metrics with substrings + # "incomingMessages" and "rawSize". + # All rates should exist, created and with values as low as "0.0". + metrics_dict = {} + for metric, metric_rate in metrics_data.items(): + metric_id = metric.split(".")[-2] + metric_type = None + if "incomingMessages" in metric: + metric_type = "im" + elif "rawSize" in metric: + metric_type = "rs" + if metric_type: + metric_key = metrics_dict.setdefault(metric_id, {}) + metric_key[f"{metric_type}_m1_rate" ] = metric_rate["m1_rate"] + metric_key[f"{metric_type}_m5_rate" ] = metric_rate["m5_rate"] + metric_key[f"{metric_type}_m15_rate"] = metric_rate["m15_rate"] + + # Create a dictionary with all inputs and add the rates from + # the previous dictionary, metrics_dict. This is passed as output. + # Some inputs don't have a "port", so we handle this with .get("port"). + inputs_dict = {} + for inputs in inputs_data: + message_input = inputs["message_input"] + input_id = inputs["id"] + input_state = inputs["state"] + input_name = message_input["title"] + input_type = message_input["name"] + input_port = message_input["attributes"].get("port") + input_rate = metrics_dict[input_id] + + inputs_dict[input_id] = { + "input_state": input_state, + "input_name": input_name, + "input_type": input_type, + "input_port": input_port, + "im_m1_rate": input_rate["im_m1_rate"], + "im_m5_rate": input_rate["im_m5_rate"], + "im_m15_rate": input_rate["im_m15_rate"], + "rs_m1_rate": input_rate["rs_m1_rate"], + "rs_m5_rate": input_rate["rs_m5_rate"], + "rs_m15_rate": input_rate["rs_m15_rate"] + } + + if inputs_dict: + handle_output(inputs_dict) + + +def handle_response(url, args): + try: + return requests.get(url, auth=(args.user, args.password), verify=not args.no_cert_check) + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: {e}") + + +def handle_output(value): + print("<<>>") + print(json.dumps(value)) + + +def print_demo(): + print(""" +<<>> +{"641e88d05d447a677efde199": {"input_state": "FAILED", "input_name": "kafka_cef_test", "input_type": "CEF Kafka", "input_port": null, "im_m1_rate": 0.0, "im_m5_rate": 0.0, "im_m15_rate": 0.0, "rs_m1_rate": 0.0, "rs_m5_rate": 0.0, "rs_m15_rate": 0.0}, "641e32885d447a677efd2dbf": {"input_state": "RUNNING", "input_name": "UDP-test", "input_type": "Syslog UDP", "input_port": 1514, "im_m1_rate": 1.0846244336700077, "im_m5_rate": 1.3700826278955827, "im_m15_rate": 1.254406787430692, "rs_m1_rate": 145.45579305762527, "rs_m5_rate": 180.6486220431909, "rs_m15_rate": 165.26666376319292}, "641e32795d447a677efd2d9e": {"input_state": "RUNNING", "input_name": "testTCP", "input_type": "Syslog TCP", "input_port": 1515, "im_m1_rate": 1.057872514816615, "im_m5_rate": 1.364957693749168, "im_m15_rate": 1.2528742858546844, "rs_m1_rate": 140.4719944116262, "rs_m5_rate": 178.57816158901215, "rs_m15_rate": 163.80530659055356}} + """.strip()) + + +def parse_arguments(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument("-u", "--user", default=None, help="Username for graylog login") + parser.add_argument("-s", "--password", default=None, help="Password for graylog login") + parser.add_argument( + "-P", + "--proto", + default="https", + help="Use 'http' or 'https' for connection to graylog (default=https)", + ) + parser.add_argument( + "-p", "--port", default=443, type=int, help="Use alternative port (default: 443)" + ) + parser.add_argument( + "--no-cert-check", action="store_true", help="Disable SSL certificate validation" + ) + parser.add_argument( + "-d", "--demo", action="store_true", help="Return demo data" + ) + + parser.add_argument( + "hostname", metavar="HOSTNAME", help="Name of the graylog instance to query." + ) + + return parser.parse_args(argv) + + +if __name__ == "__main__": + main() diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_agent.py b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_agent.py new file mode 100644 index 0000000..8199d8e --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_agent.py @@ -0,0 +1,103 @@ +# Copyright (C) 2026 Spearhead Systems SRL + +from cmk.rulesets.v1.form_specs.validators import LengthInRange, NumberInRange +from cmk.rulesets.v1.form_specs import ( + Dictionary, + DictElement, + String, + Integer, + Password, + BooleanChoice, + DefaultValue, + SingleChoice, + SingleChoiceElement, +) +from cmk.rulesets.v1.rule_specs import ( + SpecialAgent, + Topic, + Title, + Help, +) + + +def _formspec_graylog_input_metrics(): + return Dictionary( + title = Title("Graylog Input Metrics"), + help_text = Help("Requests input metrics data from a Graylog instance."), + elements = { + "instance": DictElement( + required = True, + parameter_form = String( + title = Title("Hostname"), + help_text = Help( + "Use this option to set which Graylog instance should " + "be checked by the special agent. Please add the " + "hostname here, e.g. my_graylog.com." + ), + custom_validate = (LengthInRange(min_value=1),), + ), + ), + "user": DictElement( + required = True, + parameter_form = String( + title = Title("Username"), + help_text = Help( + "The username that should be used for accessing the " + "Graylog API. Has to have read permissions at least." + ), + custom_validate = (LengthInRange(min_value=1),), + ), + ), + "password": DictElement( + required = True, + parameter_form = Password( + title = Title("Password"), + ), + ), + "protocol": DictElement( + required = True, + parameter_form = SingleChoice( + title = Title("Protocol"), + elements = [ + SingleChoiceElement( + name = "http", + title = Title("HTTP") + ), + SingleChoiceElement( + name = "https", + title = Title("HTTPS") + ), + ], + prefill=DefaultValue("https"), + ), + ), + "port": DictElement( + required = True, + parameter_form = Integer( + title = Title("Port"), + help_text = Help( + "Use this option to query a port which is different " + "from standard port 443." + ), + prefill = DefaultValue(443), + custom_validate = (NumberInRange(min_value=1, max_value=65535),), + ), + ), + "no_cert_check": DictElement( + required = True, + parameter_form = BooleanChoice( + title = Title("Insecure"), + help_text = Help("Disable SSL certificate validation"), + prefill = DefaultValue(False), + ), + ), + }, + ) + + +rule_spec_agent_graylog_input_metrics = SpecialAgent( + title = Title("Graylog Input Metrics Agent"), + name = "graylog_input_metrics", + topic = Topic.APPLICATIONS, + parameter_form = _formspec_graylog_input_metrics, +) diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_checks.py b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_checks.py new file mode 100644 index 0000000..3241407 --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/rulesets/graylog_input_metrics_checks.py @@ -0,0 +1,81 @@ +# Copyright (C) 2026 Spearhead Systems SRL + +from cmk.rulesets.v1.form_specs import ( + Dictionary, + DictElement, + Float, + DefaultValue, + LevelDirection, + SimpleLevels, +) +from cmk.rulesets.v1.rule_specs import ( + CheckParameters, + HostAndItemCondition, + Topic, + Title, + Help, +) + + +default_levels = { + LevelDirection.UPPER: 999999.0, + LevelDirection.LOWER: 0.0, +} +titles = { + "im": "messages", + "rs": "bytes", + LevelDirection.UPPER: "above or equal", + LevelDirection.LOWER: "below or equal", +} + + +def _rate(metric, minutes, level): + unit_name = titles[metric] + def_value = default_levels[level] + + return DictElement( + parameter_form = SimpleLevels( + title = Title(f"{level.value.capitalize()} level"), + level_direction = level, + form_spec_template = Float( + title = Title(f"{unit_name}/{minutes}min"), + ), + prefill_fixed_levels = DefaultValue(value=(def_value, def_value)) + ), + ) + + +def _parameter_valuespec_graylog_input_metrics(): + elements = {} + + for metric in ["im", "rs"]: + for minutes in [1, 5, 15]: + elements[f"{metric}_m{minutes}_rate"] = DictElement( + parameter_form = Dictionary( + title = Title(f"Incoming {titles[metric]} for past {minutes} minute(s)"), + elements = { + "upper": _rate(metric, minutes, LevelDirection.UPPER), + "lower": _rate(metric, minutes, LevelDirection.LOWER) + } + ) + ) + + return Dictionary( + title = Title("Message and data rates"), + help_text = Help( + "These rates are queried directly from the Graylog instance. " + "Upper and lower levels can be specified for individual metric." + ), + elements = elements, + ) + + +rule_spec_graylog_input_metrics = CheckParameters( + title = Title("Graylog Input Metrics Checks"), + name = "graylog_input_metrics", + topic = Topic.APPLICATIONS, + parameter_form = _parameter_valuespec_graylog_input_metrics, + condition = HostAndItemCondition( + item_title = Title("Graylog input name") + ), +) diff --git a/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/server_side_calls/graylog_input_metrics.py b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/server_side_calls/graylog_input_metrics.py new file mode 100644 index 0000000..9a94db0 --- /dev/null +++ b/graylog-metrics/2.4/local/lib/python3/cmk_addons/plugins/graylog_input_metrics/server_side_calls/graylog_input_metrics.py @@ -0,0 +1,30 @@ +# Copyright (C) 2026 Spearhead Systems SRL + +from cmk.server_side_calls.v1 import noop_parser, SpecialAgentConfig, SpecialAgentCommand + + +def agent_graylog_input_metrics_arguments(params, host_config): + p = params["password"] + if not isinstance(p, str): + p = p.unsafe() + + args = [ + "-P", params["protocol"], + "-p", str(params["port"]), + "-u", params["user"], + "-s", p, + ] + + if params.get("no_cert_check"): + args += ["--no-cert-check"] + + args += [params["instance"]] + + yield SpecialAgentCommand(command_arguments=args) + + +special_agent_graylog_input_metrics = SpecialAgentConfig( + name = "graylog_input_metrics", + parameter_parser = noop_parser, + commands_function = agent_graylog_input_metrics_arguments, +)