How-to

HTTP Collections tutorial - How to monitor an API

  • 23 November 2023
  • 1 reply
  • 1303 views

Userlevel 4
Badge +12

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 run sudo 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 or selection_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 the path and entries 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 a name, a type and an information attribute, and if the type is “service”, it will also have a parent 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: nametypeinformation and parent.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.
  • ServiceLoop
    • Shares the same data source as HostLoop.
    • The formatting is service-specific. It uses the additional parent.name field.

 

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:

  • mapping but you may find examples in the SQL and SNMP collections tutorials.
  • Other formats (rtype) than JSON: XML and text.
  • formatting (the global section) same remark as above.
  • all the other types of functions

This topic has been closed for comments

1 reply

Badge +5

If you understand french, you may find additional explanations in this video.