It has been a while now since we published this article about collection modes in some of our plugins. You may have already tried SNMP Collections and SQL Collections (also explained in this additional article and this Youtube video in French). This draft of article acted as a teaser for this long waited tutorial. Here it is, and I hope it is worth the wait!
Foreword
- In this article I’ll try to explain everything I judge necessary, but I’ll assume you have basic understanding of HTTP protocol and a bit of experience in monitoring with our plugins. Having read the other articles linked in the introduction may also be useful preferable.
- I have built my example with Centreon’s WUI and API so that any Centreon user can reproduce my commands on their platform.
- The API checks presented here are not necessarily useful as monitoring checks. The point of this article is to show you how to build a collections to monitor a given API, not to give tips about monitoring a Centreon platform.
- The version of Centreon Web that has been used here is 23.04.9.
- All the commands below have been run on my central server from my central server, hence the localhost IP address.
- You’ll need Centreon’s HTTP plugin (
yum install centreon-plugin-Applications-Protocol-Http
/apt install centreon-plugin-applications-protocol-http
). The current version of the plugin is 20231114. - Never use
root
to test your monitoring commands. Always runsudo su - centreon-engine
before starting ;-) - HTTP Collections do not exist yet as a service template in the HTTP Server Monitoring Connector at the time when this article is being written, it is just a powerful yet user-unfriendly plugin mode.
Before using HTTP Collections
Collections are complex and may be a bit tricky to use. It may not be the only solution to your need. If your need is simple, do not forget that there are easier features in our HTTP Server Monitoring Connector, such as:
- HTTP-Response-Time but it is very limited and won’t be detailed here.
- HTTP-Expected-Content
- HTTP-JSON-Content
HTTP-Expected-Content
If you want to make sure the web page you are monitoring contains a string that gives you the assurance it is working properly, you may use this mode.
For example, to make sure the default landing page when you reach Centreon contains “Centreon - IT & Network Monitoring”, use this command:
/usr/lib/centreon/plugins/centreon_protocol_http.pl --plugin apps::protocols::http::plugin --mode expected-content --hostname 127.0.0.1 --urlpath / --http-backend curl --expected-string='Centreon - IT & Network Monitoring'
If the returned page matches, you’ll get this result:
OK: HTTP test(s) | 'size'=579B;;;0; 'time'=0.071s;;;0;
Else you’ll have:
CRITICAL: Content test [filter: '%{content} !~ /Bad content/mi'] | 'size'=579B;;;0; 'time'=0.066s;;;0;
According to me it is the least you should monitor on every web server, but we can do a lot better. Read on!
HTTP-JSON-Content
This mode will allow you to parse a JSON content looking for an attribute’s value. Here is an example where we’ll check whether there is a pending update of the Centreon Web component.
The information is available at this endpoint without authentication:
curl -s http://127.0.0.1/centreon/api/latest/platform/installation/status
Result:
{
"is_installed": true,
"has_upgrade_available": false
}
Knowing that, we can monitor the has_upgrade_available
attribute with the following command.
NB: The comments will break the command line. To actually test it, copy-paste the second one right after.
/usr/lib/centreon/plugins/centreon_protocol_http.pl \
--plugin apps::protocols::http::plugin \
--mode json-content \
--http-backend=curl \ # cURL library is more complete than LWP
--hostname=127.0.0.1 \
--urlpath='/centreon/api/latest/platform/installation/status' \ # cf https://docs-api.centreon.com/api/centreon-web/#tag/Platform/paths/~1platform~1installation~1status/get
--lookup='$.has_upgrade_available' \ # the info is at the root of the json
--threshold-value=values \ # the default (count) would have worked as well but here we have interest for values
--warning-numeric=0 \ # 0 means false, 1 means true
--format-ok='The Centreon central server is up-to-date' \ # How the the output must be written in case of OK state
--format-warning='An upgrade is pending' \ # How the the output must be written in case of CRITICAL state
--output-ignore-perfdata # If we don't want to store metrics
Command to use:
/usr/lib/centreon/plugins/centreon_protocol_http.pl --plugin apps::protocols::http::plugin --mode json-content --http-backend=curl --hostname=127.0.0.1 --urlpath='/centreon/api/latest/platform/installation/status' --lookup='$.has_upgrade_available' --threshold-value=values --warning-numeric=0 --format-ok='The Centreon central server is up-to-date' --format-warning='An upgrade is pending' --output-ignore-perfdata
The expected result is:
OK: The Centreon central server is up-to-date
Or if you have updated the centreon-web package without processing though the wizard:
WARNING: An upgrade is pending
First Collection: authenticate to Centreon
When the returned JSON is more complex to parse, or when the logic to determine the status is more subtle, another option is available: collections!
Overall JSON structure
The JSON structure of a HTTP collection is similar to other collections. The main difference is the data source, that will be named http
instead of sql
or snmp
for other protocols.
The main items of a HTTP collection are http
, selection
and selection_loop
.
The complete possible structure is:
{
"constants": {}, // to define constants that will be used in several parts
"mapping": {}, // to translate a code (work or number) into a more understandable string
"formatting": {}, // to define the default formatting
"http": {
"requests": [] // HTTP requests that will provide the data
},
"selection": [], // simple selection mode
"selection_loop": [] // loop selection
}
The //comments
above will break the JSON compatibility, so don’t use this snippet as a starter for your collections, besides that it lacks a lot of things.
As a first example, let’s authenticate to Centreon!
Authentication request
It is not an end in itself, but a necessary step to access the full API, and also a good use case to learn how the collection works.
To login to Centreon in command line, you just need this (the endpoint is documented here):
curl -s \
-d '{"security":{"credentials":{"password":"***","login":"admin"}}}' \
'http://127.0.0.1/centreon/api/latest/login'
The returned JSON code will look like:
{
"security" : {
"token" : "Vj77k9p53L+FTXon1UDvaYBZU8P3MnUK2siU7mst3HJ1QcnjTDWVW6TX3RlccjpV"
},
"contact" : {
"name" : "Administrateur_Centreon",
"is_admin" : true,
"email" : "null@localhost.local",
"alias" : "admin",
"id" : 1
}
}
Here is how we do it with a first HTTP request (with comments to explain the role of each section):
{
"_comment": "All keys beginning with '_comment' are comments, they are here to help understand the format of the JSON collection but they can all be removed.",
"constants": {
"_comment": "The default values for variable parameters",
"protocol": "https",
"port": "443",
"_comment_customPath": "The customizable path that comes after https://ip_address/",
"customPath": "centreon"
},
"http": {
"_comment_requests": "List of the HTTP requests",
"requests": [
{
"_comment_name": "The name will be used to refer to the request",
"name": "authenticationRequest",
"_comment_hostname": "The hostname must be provided in the command line (e.g. --constant='hostname=127.0.0.1')",
"hostname": "%(constants.hostname)",
"_comment_proto": "The desired protocol to use (can be 'http' or 'https'). We use the defined constant.",
"proto": "%(constants.protocol)",
"_comment_port": "The TCP port to connect to. We use the defined constant.",
"port": "%(constants.port)",
"_comment_endpoint": "The endpoint where the HTTP request must be sent.",
"endpoint": "/%(constants.customPath)/api/latest/login",
"_comment_method": "The method used to query the endpoint. Usually GET to get data or POST to send data such as credentials. ",
"method": "POST",
"_comment_headers": "If specific headers are required for the request to succeed.",
"headers": [
"Accept:application/json",
"Content-Type:application/json"
],
"timeout": 30,
"_comment_payload": "The payload is the data sent to the server. Its format depends on what the application is expecting. Here is the format expected by Centreon's login endpoint.",
"payload": {
"type": "json",
"value": {
"security": {
"credentials": {
"login": "%(constants.username)",
"password": "%(constants.password)"
}
}
}
},
"_comment_backend": "It is recommended to use the cURL backend rather than LWP.",
"backend": "curl",
"_comment_rtype": "Format of the returned content. Handled formats: json, text, xml.",
"rtype": "json",
"_comment_parse": "List of the parsing rules to extract parts of the data.",
"parse": [
{
"_comment_name": "The name will be used to refer to the parsed data.",
"name": "token",
"_comment_path": "Path where we will look for the entries.",
"path": "$.security.token",
"_comment_entries": "List of the fields we want to store for each entry.",
"entries": [
{
"id": "value"
}
]
}
]
}
]
}
}
NB: All the _comment_
fields can be safely removed, they are here to give in-context information.
Some other comments:
- The constants are defined in the JSON so that you don’t need to redefine them if the default values suit you. You may redefine them in the command line with
--constant=’constant_name=constant_value’
. - The constants’ names are user-defined. I chose their names, and you can choose yours.
- This collection is not complete, it lacks the
selection
orselection_loop
section, but if you run it in debug mode:
/usr/lib/centreon/plugins/centreon_protocol_http.pl --plugin apps::protocols::http::plugin --mode collection --constant='hostname=127.0.0.1' --constant='protocol=http' --constant='port=80' --constant='username=admin' --constant='password=***' --config /etc/centreon-engine/test.json --debug
You’ll get:
OK:
======> request send
[...]
======> response done
[...]
======> variables
http.tables.authenticationRequestToken.[0].value = ztPySmPwHgHrZTJFFWxO94NrDL218eMy6vKoZRThk2O6me8aDHgDVhVdpU7glbe5
Remember the last line as we will use it in our selection
/selection_loop
. The name of the array, authenticationRequestToken
, is built by concatenating the name of the HTTP request
and the name of the parsing rule (with the first letter capitalized).
Control that the authentication succeeded with a selection
At this stage, we won’t use the obtained token, but we’ll make sure that the HTTP response is as expected. We’ll do that with a selection.
{
"selection": [
{
"_comment_name": "The name will be used to refer to the selection.",
"name": "authenticationSelection",
"_comment_critical": "If the given condition is true, the returned status will be CRITICAL.",
"critical": "defined(%(builtin.httpCode.authenticationRequest)) and %(builtin.httpCode.authenticationRequest) != 200",
"_comment_formatting": "Define the output message in printf format.",
"formatting": {
"printf_msg": "Authentication resulted in %s HTTP code",
"printf_var": [
"%(builtin.httpCode.authenticationRequest)"
],
"_comment_display_ok": "Set to true if the formatting rule applies when the resulting state is OK.",
"display_ok": true
}
}
]
}
NB: this is not a collection, it’s just a part of one and it will not work by itself at all.
The builtin.httpCode.authenticationRequestToken
variable contains the HTTP status code. The Centreon API login endpoint returns a 200
code in case of success, so any value other than 200
means it has failed.
Complete collection and results
Here is the complete code of the collection we have built:
{
"constants": {
"protocol": "https",
"port": "443",
"customPath": "centreon"
},
"http": {
"requests": [
{
"name": "authenticationRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/login",
"method": "POST",
"headers": [
"Accept:application/json",
"Content-Type:application/json"
],
"timeout": 30,
"payload": {
"type": "json",
"value": {
"security": {
"credentials": {
"login": "%(constants.username)",
"password": "%(constants.password)"
}
}
}
},
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "token",
"path": "$.security.token",
"entries": [
{
"id": "value"
}
]
}
]
}
]
},
"selection": [
{
"name": "authenticationSelection",
"critical": "defined(%(builtin.httpCode.authenticationRequest)) and %(builtin.httpCode.authenticationRequest) != 200",
"formatting": {
"printf_msg": "Authentication resulted in %s HTTP code",
"printf_var": [
"%(builtin.httpCode.authenticationRequest)"
],
"display_ok": true
}
}
]
}
When used this way:
/usr/lib/centreon/plugins/centreon_protocol_http.pl --plugin apps::protocols::http::plugin --mode collection --constant='hostname=127.0.0.1' --constant='protocol=http' --constant='port=80' --constant='username=admin' --constant='password=***' --config /etc/centreon-engine/check_auth.json
It will return:
OK: Authentication resulted in 200 HTTP code
if it succeeds and:
CRITICAL: Authentication resulted in 401 HTTP code
if it fails.
In this selection
, I used the same formatting rule for all statuses, but I could have written other (formatting_warning
, formatting_critical
and formatting_unknown
) rules.
Second Collection: count down hosts using a selection
Now that we can authenticate to Centreon, let’s use the /monitoring/hosts endpoints.
Building the GET /monitoring/hosts request
This endpoint returns a structure that contains an array of hosts:
{
"meta": {
"search": {},
"total": 1,
"limit": 1000,
"sort_by": {},
"page": 1
},
"result": [
{
"passive_checks": false,
"timezone": "",
"last_check": "2023-11-21T21:21:14+01:00",
"checked": true,
"state": 0,
"last_time_up": "2023-11-21T21:21:18+01:00",
"icon_image": "ppm/applications-monitoring-centreon-central-centreon-128-2.png",
"icon_image_alt": "",
"state_type": 1,
"address_ip": "127.0.0.1",
"criticality": null,
"last_time_down": null,
"scheduled_downtime_depth": 0,
"name": "CENTREON",
"last_time_unreachable": null,
"alias": "Centreon Central Server",
"poller_id": 1,
"last_hard_state_change": "2023-09-07T20:03:53+02:00",
"execution_time": 0.112673,
"last_state_change": "2023-09-07T20:03:53+02:00",
"output": "OK - 127.0.0.1 rta 0.079ms lost 0%\n",
"id": 13,
"max_check_attempts": 3,
"last_update": "2023-11-21T08:57:13+01:00",
"acknowledged": false,
"display_name": "CENTREON",
"check_attempt": 1
}
]
}
The fields that interest us are name
and state
, so we’ll include them in the parsing rule.
To build the parsing rule, I use this site. It works with 3 zones:
JSONPath
: where you write the path you want to look at.Inputs
: where you can copy the JSON code that is returned by the API.Evaluation Results
: where you see the data available at the given JSONpath.
As an example:
- In the
Inputs
field, I put the above JSON code - In the
JSONPath
field, I tested thepath
andentries
written under that form:$.result[*].[name,state]
.
We’ll keep the authentication request and add a new request to get the host monitoring data:
{
"http": {
"request": [
{...},
{
"name": "hostsRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/monitoring/hosts?limit=1000",
"method": "GET",
"headers": [
"X-AUTH-TOKEN: %(http.tables.authenticationRequestToken.[0].value)",
"Accept: text/json"
],
"timeout": 30,
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "entries",
"type": "body",
"path": "$.result[*]",
"entries": [
{
"id": "name"
},
{
"id": "state"
}
]
}
]
}
]
}
}
NB: notice the use of the http.tables.authenticationRequestToken
array we receive from the authentication request as a X-AUTH-TOKEN
HTTP header.
This request will store an array named http.tables.hostsRequestEntries
.
Counting the results with a selection
Now that we have this new request, let’s use it to count the hosts that are not UP.
{
"selection": [
{
"name": "authenticationSelection",
"...": "..."
},
{
"name": "hostsSelection",
"_comment_functions": "Functions can apply one of the supported types of computations and store (save) them in variables.",
"functions": [
{
"_comment_type": "The type of computation can be: map, scale, second2human, date2epoch, epoch2date, count, replace, assign, capture, scientific2number.",
"type": "count",
"_comment_src": "Expression pointing to the data to compute.",
"src": "%(http.tables.hostsRequestEntries)",
"_comment_filter": "Only the data for which the given expression is true will be processed.",
"filter": "%(src.state) != 0",
"_comment_save": "Name of the variable that will store the computed value.",
"save": "%(downCount)"
},
{
"type": "count",
"src": "%(http.tables.hostsRequestEntries)",
"save": "%(hostsCount)"
}
],
"_comment_perfdatas": "Performance data (metrics) definition.",
"perfdatas": [
{
"_comment_nlabel": "Name of the perfdata/metric to return.",
"nlabel": "hostsRequest.down.count",
"_comment_value": "Value of the metric.",
"value": "%(downCount)",
"_comment_warning": "Warning threshold to display in the perfdata.",
"warning": "0",
"_comment_min": "Minimum value to display in the perfdata.",
"min": 0,
"_comment_max": "Maximum value to display in the perfdata.",
"max": "%(hostsCount)"
}
],
"_comment_warning": "If the expression is true, the returned status will be WARNING or worse.",
"warning": "%(downCount) > 0",
"_comment_formatting": "Define the output message in printf format.",
"formatting": {
"printf_msg": "Number of down hosts: %s out of %s",
"printf_var": [
"%(downCount)",
"%(hostsCount)"
],
"_comment_display_ok": "Set to true if the formatting rule applies when the resulting state is OK.",
"display_ok": true
}
}
]
}
Complete collection
The complete collection to perform this check is here:
{
"constants": {
"protocol": "https",
"port": "443",
"customPath": "centreon"
},
"http": {
"requests": [
{
"name": "authenticationRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/login",
"method": "POST",
"headers": [
"Accept:application/json",
"Content-Type:application/json"
],
"timeout": 30,
"payload": {
"type": "json",
"value": {
"security": {
"credentials": {
"login": "%(constants.username)",
"password": "%(constants.password)"
}
}
}
},
"scenario_stopped": "%(builtin.httpCode.authenticationRequest) != 200",
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "token",
"path": "$.security.token",
"entries": [
{
"id": "value"
}
]
}
]
},
{
"name": "hostsRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/monitoring/hosts?limit=1000",
"method": "GET",
"headers": [
"X-AUTH-TOKEN: %(http.tables.authenticationRequestToken.[0].value)",
"Accept: text/json"
],
"timeout": 30,
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "entries",
"type": "body",
"path": "$.result[*]",
"entries": [
{
"id": "name"
},
{
"id": "state"
}
]
}
]
}
]
},
"selection": [
{
"name": "authenticationSelection",
"critical": "defined(%(builtin.httpCode.authenticationRequest)) and %(builtin.httpCode.authenticationRequest) != 200",
"exit": "defined(%(builtin.httpCode.authenticationRequest)) and %(builtin.httpCode.authenticationRequest) != 200",
"formatting": {
"printf_msg": "Authentication resulted in %s HTTP code",
"printf_var": [
"%(builtin.httpCode.authenticationRequest)"
],
"display_ok": false
}
},
{
"name": "hostsSelection",
"functions": [
{
"type": "count",
"src": "%(http.tables.hostsRequestEntries)",
"filter": "%(src.state) != 0",
"save": "%(downCount)"
},
{
"type": "count",
"src": "%(http.tables.hostsRequestEntries)",
"save": "%(hostsCount)"
}
],
"perfdatas": [
{
"nlabel": "hostsRequest.down.count",
"value": "%(downCount)",
"warning": "0",
"min": 0,
"max": "%(hostsCount)"
}
],
"warning": "%(downCount) > 0",
"formatting": {
"printf_msg": "Number of down hosts: %s out of %s",
"printf_var": [
"%(downCount)",
"%(hostsCount)"
],
"display_ok": true
}
}
],
"formatting": {
"custom_message_global": "All hosts are UP",
"separator": "-"
}
}
When used this way:
/usr/lib/centreon/plugins/centreon_protocol_http.pl --plugin apps::protocols::http::plugin --mode collection --constant='hostname=127.0.0.1' --constant='protocol=http' --constant='port=80' --constant='username=admin' --constant='password=***' --config /etc/centreon-engine/check_down_hosts.json
It will return:
OK: All hosts are UP | 'hostsRequest.down.count'=0;0;;0;4
If all hosts are UP, and if some are down:
WARNING: Number of down hosts: 1 out of 5 | 'hostsRequest.down.count'=1;0;;0;5
NB: the request in this example is not optimal since it will retrieve the status of all the monitored hosts (unless you have more than 1000 of them). I don’t recommend using it as a monitored service on a production platform.
Third collection: look for broken commands using a selection_loop
It may have occurred to some readers that after a migration, a plugin was missing, and a bunch of services turned into UNKNOWN state, reading (Execute command failed)
in the Information column.
The next example will aim at tracking them and returning the names of the hosts and services having this issue. Let’s use another endpoint of Centreon's monitoring API: /monitoring/resources.
Building the GET /monitoring/resources request
The /monitoring/resources endpoint returns a structure that contains a meta
section and an array of resources named result
:
{
"meta" : {
"page" : 1,
"sort_by" : {},
"search" : {
"$and" : {
"information" : {
"$eq" : "(Execute command failed)"
}
}
},
"limit" : 1000,
"total" : 1
},
"result" : [
{
"uuid" : "h254-s1616",
"host_id" : 254,
"monitoring_server_name" : "Central",
"status" : {
"name" : "UNKNOWN",
"code" : 3,
"severity_code" : 3
},
"icon" : null,
"alias" : null,
"last_status_change" : "2023-11-22T11:55:30+01:00",
"short_type" : "s",
"name" : "Svc-BadCommand",
"last_check" : "1m 2s",
"duration" : "4h 28m",
"acknowledged" : false,
"in_downtime" : false,
"chart_url" : null,
"tries" : "1/3 (H)",
"information" : "(Execute command failed)",
"performance_data" : null,
"parent" : {
"status" : {
"severity_code" : 1,
"name" : "DOWN",
"code" : 1
},
"host_id" : null,
"uuid" : "h254",
"alias" : "Down",
"icon" : null,
"links" : {
"externals" : {
"action_url" : null,
"notes" : null
},
"uris" : {
"configuration" : null,
"reporting" : null,
"logs" : null
},
"endpoints" : {}
},
"id" : 254,
"fqdn" : "1.2.3.4",
"name" : "FakeHostThatIsDown",
"type" : "host",
"short_type" : "h",
"service_id" : null
},
"links" : {
"externals" : {
"action_url" : "",
"notes" : {
"url" : "",
"label" : ""
}
},
"uris" : {
"logs" : "/centreon/main.php?p=20301&svc=254_1616",
"configuration" : "/centreon/main.php?p=60201&o=c&service_id=1616",
"reporting" : "/centreon/main.php?p=30702&period=yesterday&start=&end=&host_id=254&item=1616"
},
"endpoints" : {
"check" : "/centreon/api/latest/monitoring/hosts/254/services/1616/check",
"acknowledgement" : "/centreon/api/latest/monitoring/hosts/254/services/1616/acknowledgements?limit=1",
"timeline" : "/centreon/api/latest/monitoring/hosts/254/services/1616/timeline",
"performance_graph" : null,
"downtime" : "/centreon/api/latest/monitoring/hosts/254/services/1616/downtimes?search=%7B%22%24and%22:%5B%7B%22start_time%22:%7B%22%24lt%22:1700666614%7D,%22end_time%22:%7B%22%24gt%22:1700666614%7D,%220%22:%7B%22%24or%22:%7B%22is_cancelled%22:%7B%22%24neq%22:1%7D,%22deletion_time%22:%7B%22%24gt%22:1700666614%7D%7D%7D%7D%5D%7D",
"forced_check" : "/centreon/api/latest/monitoring/hosts/254/services/1616/check",
"status_graph" : "/centreon/api/latest/monitoring/hosts/254/services/1616/metrics/status",
"details" : "/centreon/api/latest/monitoring/resources/hosts/254/services/1616"
}
},
"passive_checks" : false,
"notification_enabled" : false,
"service_id" : 1616,
"type" : "service",
"severity" : null,
"fqdn" : null,
"active_checks" : true,
"id" : 1616
}
]
}
- The
meta
section will, among other information, give the total number of results matching the query. - Each item of
result
has at least aname
, atype
and aninformation
attribute, and if thetype
is “service”, it will also have aparent
section, containing information about the parent host of the service.
Here is my request section:
{
"name": "resourcesRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/monitoring/resources?limit=1000&search={\"information\": {\"$eq\": \"(Execute command failed)\"}}",
"method": "GET",
"headers": [
"X-AUTH-TOKEN: %(http.tables.authenticationRequestToken.[0].value)",
"Accept: text/json"
],
"timeout": 30,
"scenario_stopped": "%(builtin.httpCode.authenticationRequest) != 200",
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "meta",
"type": "body",
"path": "$.meta",
"entries": [
{"id": "total"}
]
},
{
"name": "entries",
"type": "body",
"path": "$.result[*]",
"entries": [
{"id": "parent.name"},
{"id": "name"},
{"id": "type"},
{"id": "information"}
]
}
]
}
- As you can see, the
endpoint
attribute includes the search criteria (making it less resource-consuming than the previous example: it will only process through the problematic elements). - We have two parsing rules:
- One named “meta”, that retrieves the total number of items matching tne search.
- One named “entries”, retrieving the following information:
name
,type
,information
andparent.name
(the last one will be available only for services).
A new section: selection_loop
To use the information about the resources that use a plugin that does not exist, we’ll set a selection_loop
instead of a selection
section.
{
"selection_loop": [
{
"name": "HostsLoop",
"source": "%(http.tables.resourcesRequestEntries)",
"filter": "%(resourcesRequestEntries.type) eq 'host'",
"formatting": {
"display_ok": "false",
"printf_msg": "Host %s's output is '%s'",
"printf_var": [
"%(resourcesRequestEntries.name)",
"%(resourcesRequestEntries.information)"
]
},
"warning": "%(resourcesRequestEntries.information) =~ /No output returned from plugin|Execute command failed/"
},
{
"name": "ServicesLoop",
"source": "%(http.tables.resourcesRequestEntries)",
"filter": "%(resourcesRequestEntries.type) eq 'service'",
"formatting": {
"display_ok": "false",
"printf_msg": "Service %s/%s output is '%s'",
"printf_var": [
"%(resourcesRequestEntries.parent.name)",
"%(resourcesRequestEntries.name)",
"%(resourcesRequestEntries.information)"
]
},
"warning": "%(resourcesRequestEntries.information) =~ /No output returned from plugin|Execute command failed/"
}
]
}
Since hosts and services are not structured identically, I have written two separate loops, one for each type.
- HostLoop
- The
filter
attribute makes it apply to hosts only. - The formatting of the output is host-specific.
- The
warning
attribute will set the returned status as soon as there is one entry matching this rule.
- The
- ServiceLoop
- Shares the same data
source
as HostLoop. - The formatting is service-specific. It uses the additional
parent.name
field.
- Shares the same data
And another selection
Complete collection
Here is the complete collection to count and list the broken commands:
{
"constants": {
"protocol": "https",
"port": "443",
"customPath": "centreon"
},
"http": {
"requests": [
{
"name": "authenticationRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/login",
"method": "POST",
"headers": [
"Accept:application/json",
"Content-Type:application/json"
],
"timeout": 30,
"payload": {
"type": "json",
"value": {
"security": {
"credentials": {
"login": "%(constants.username)",
"password": "%(constants.password)"
}
}
}
},
"scenario_stopped": "%(builtin.httpCode.authenticationRequest) != 200",
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "token",
"path": "$.security.token",
"entries": [
{
"id": "value"
}
]
}
]
},
{
"name": "resourcesRequest",
"hostname": "%(constants.hostname)",
"proto": "%(constants.protocol)",
"port": "%(constants.port)",
"endpoint": "/%(constants.customPath)/api/latest/monitoring/resources?limit=1000&search={\"information\": {\"$eq\": \"(Execute command failed)\"}}",
"method": "GET",
"headers": [
"X-AUTH-TOKEN: %(http.tables.authenticationRequestToken.[0].value)",
"Accept: text/json"
],
"timeout": 30,
"scenario_stopped": "%(builtin.httpCode.authenticationRequest) != 200",
"backend": "curl",
"rtype": "json",
"parse": [
{
"name": "meta",
"type": "body",
"path": "$.meta",
"entries": [
{
"id": "total"
}
]
},
{
"name": "entries",
"type": "body",
"path": "$.result[*]",
"entries": [
{
"id": "parent.name"
},
{
"id": "name"
},
{
"id": "type"
},
{
"id": "information"
}
]
}
]
}
]
},
"selection_loop": [
{
"name": "HostsLoop",
"source": "%(http.tables.resourcesRequestEntries)",
"filter": "%(resourcesRequestEntries.type) eq 'host'",
"formatting": {
"display_ok": "false",
"printf_msg": "Host %s's output is '%s'",
"printf_var": [
"%(resourcesRequestEntries.name)",
"%(resourcesRequestEntries.information)"
]
},
"warning": "%(resourcesRequestEntries.information) =~ /No output returned from plugin|Execute command failed/"
},
{
"name": "ServicesLoop",
"source": "%(http.tables.resourcesRequestEntries)",
"filter": "%(resourcesRequestEntries.type) eq 'service'",
"formatting": {
"display_ok": "false",
"printf_msg": "Service %s/%s output is '%s'",
"printf_var": [
"%(resourcesRequestEntries.parent.name)",
"%(resourcesRequestEntries.name)",
"%(resourcesRequestEntries.information)"
]
},
"warning": "%(resourcesRequestEntries.information) =~ /No output returned from plugin|Execute command failed/"
}
],
"selection": [
{
"name": "totalSelection",
"functions": [
{
"type": "assign",
"expression": "%(http.tables.resourcesRequestMeta.[0].total)",
"save": "%(brokenCommandsCount)"
}
],
"perfdatas": [
{
"nlabel": "commands.broken.count",
"value": "%(brokenCommandsCount)",
"warning": "0",
"min": 0
}
],
"exit": "%(brokenCommandsCount) == 0",
"warning": "%(brokenCommandsCount) > 0",
"formatting_warning": {
"printf_msg": "",
"display_ok": false,
"separator": " "
},
"formatting": {
"printf_msg": "All commands are fine",
"display_ok": true,
"separator": ""
}
}
]
}
In the event when no “broken commands” are found, the resourcesRequestEntries
table won’t be created and the selection_loop
will break, resulting in an UNKNOWN state. To prevent this, I added a selection
that works with the meta parsing rule, to force exiting (thanks to the exit
condition) in OK state if the total number of elements found is 0.
When there are no broken commands, the selection
alone is checked and returns the following output:
OK: All commands are fine | 'commands.broken.count'=0;0;;0;
When some commands are broken, the hosts and services are listed in the output:
WARNING: - Host FakeHostThatIsDown's output is '(Execute command failed)' - Service FakeHostThatIsDown/Svc-BadCommand output is '(Execute command failed)' - Service FakeHostThatIsDown/Svc-BadCommand_1 output is '(Execute command failed)' - Service FakeHostThatIsDown/Svc-BadCommand_2 output is '(Execute command failed)' - Service FakeHostThatIsDown/Svc-BadCommand_3 output is '(Execute command failed)' | 'commands.broken.count'=5;0;;0;
Conclusion
I believe that this tutorial only covers 50 to 70% of the range of possibilities this plugin’s mode offers. But I hope it makes them understandable. If not, ask your questions in the Data collection section, and I may edit my post to improve it or add complementary posts under this one in addition to answering your questions.
Not covered in this article: