python-collector.txt 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. # Develop a custom data collector in Python
  2. The Netdata Agent uses [data collectors](/src/collectors/README.md) to
  3. fetch metrics from hundreds of system, container, and service endpoints. While the Netdata team and community has built
  4. [powerful collectors](/src/collectors/COLLECTORS.md) for most system, container,
  5. and service/application endpoints, some custom applications can't be monitored by default.
  6. In this tutorial, you'll learn how to leverage the [Python programming language](https://www.python.org/) to build a
  7. custom data collector for the Netdata Agent. Follow along with your own dataset, using the techniques and best practices
  8. covered here, or use the included examples for collecting and organizing either random or weather data.
  9. ## Disclaimer
  10. If you're comfortable with Golang, consider instead writing a module for the [go.d.plugin](https://github.com/netdata/go.d.plugin).
  11. Golang is more performant, easier to maintain, and simpler for users since it doesn't require a particular runtime on the node to
  12. execute. Python plugins require Python on the machine to be executed. Netdata uses Go as the platform of choice for
  13. production-grade collectors.
  14. We generally do not accept contributions of Python modules to the GitHub project netdata/netdata. If you write a Python collector and
  15. want to make it available for other users, you should create the pull request in <https://github.com/netdata/community>.
  16. ## What you need to get started
  17. - A physical or virtual Linux system, which we'll call a _node_.
  18. - A working [installation of Netdata](/packaging/installer/README.md) monitoring agent.
  19. ### Quick start
  20. For a quick start, you can look at the
  21. [example plugin](https://raw.githubusercontent.com/netdata/netdata/master/src/collectors/python.d.plugin/example/example.chart.py).
  22. **Note**: If you are working 'locally' on a new collector and would like to run it in an already installed and running
  23. Netdata (as opposed to having to install Netdata from source again with your new changes) you can copy over the relevant
  24. file to where Netdata expects it and then either `sudo systemctl restart netdata` to have it be picked up and used by
  25. Netdata or you can just run the updated collector in debug mode by following a process like below (this assumes you have
  26. [installed Netdata from a GitHub fork](/packaging/installer/methods/manual.md) you
  27. have made to do your development on).
  28. ```bash
  29. # clone your fork (done once at the start but shown here for clarity)
  30. #git clone --branch my-example-collector https://github.com/mygithubusername/netdata.git --depth=100 --recursive
  31. # go into your netdata source folder
  32. cd netdata
  33. # git pull your latest changes (assuming you built from a fork you are using to develop on)
  34. git pull
  35. # instead of running the installer we can just copy over the updated collector files
  36. #sudo ./netdata-installer.sh --dont-wait
  37. # copy over the file you have updated locally (pretending we are working on the 'example' collector)
  38. sudo cp collectors/python.d.plugin/example/example.chart.py /usr/libexec/netdata/python.d/
  39. # become user netdata
  40. sudo su -s /bin/bash netdata
  41. # run your updated collector in debug mode to see if it works without having to reinstall netdata
  42. /usr/libexec/netdata/plugins.d/python.d.plugin example debug trace nolock
  43. ```
  44. ## Jobs and elements of a Python collector
  45. A Python collector for Netdata is a Python script that gathers data from an external source and transforms these data
  46. into charts to be displayed by Netdata dashboard. The basic jobs of the plugin are:
  47. - Gather the data from the service/application.
  48. - Create the required charts.
  49. - Parse the data to extract or create the actual data to be represented.
  50. - Assign the correct values to the charts
  51. - Set the order for the charts to be displayed.
  52. - Give the charts data to Netdata for visualization.
  53. The basic elements of a Netdata collector are:
  54. - `ORDER[]`: A list containing the charts to be displayed.
  55. - `CHARTS{}`: A dictionary containing the details for the charts to be displayed.
  56. - `data{}`: A dictionary containing the values to be displayed.
  57. - `get_data()`: The basic function of the plugin which will return to Netdata the correct values.
  58. **Note**: All names are better explained in the
  59. [External Plugins Documentation](/src/plugins.d/README.md).
  60. Parameters like `priority` and `update_every` mentioned in that documentation are handled by the `python.d.plugin`,
  61. not by each collection module.
  62. Let's walk through these jobs and elements as independent elements first, then apply them to example Python code.
  63. ### Determine how to gather metrics data
  64. Netdata can collect data from any program that can print to stdout. Common input sources for collectors can be log files,
  65. HTTP requests, executables, and more. While this tutorial will offer some example inputs, your custom application will
  66. have different inputs and metrics.
  67. A great deal of the work in developing a Netdata collector is investigating the target application and understanding
  68. which metrics it exposes and how to
  69. ### Create charts
  70. For the data to be represented in the Netdata dashboard, you need to create charts. Charts (in general) are defined by
  71. several characteristics: title, legend, units, type, and presented values. Each chart is represented as a dictionary
  72. entry:
  73. ```python
  74. chart= {
  75. "chart_name":
  76. {
  77. "options": [option_list],
  78. "lines": [
  79. [dimension_list]
  80. ]
  81. }
  82. }
  83. ```
  84. Use the `options` field to set the chart's options, which is a list in the form `options: [name, title, units, family,
  85. context, charttype]`, where:
  86. - `name`: The name of the chart.
  87. - `title` : The title to be displayed in the chart.
  88. - `units` : The units for this chart.
  89. - `family`: An identifier used to group charts together (can be null).
  90. - `context`: An identifier used to group contextually similar charts together. The best practice is to provide a context
  91. that is `A.B`, with `A` being the name of the collector, and `B` being the name of the specific metric.
  92. - `charttype`: Either `line`, `area`, `stacked` or `heatmap`. If null line is the default value.
  93. You can read more about `family` and `context` in the [Netdata Charts](/docs/dashboards-and-charts/netdata-charts.md) doc.
  94. Once the chart has been defined, you should define the dimensions of the chart. Dimensions are basically the metrics to
  95. be represented in this chart and each chart can have more than one dimension. In order to define the dimensions, the
  96. "lines" list should be filled in with the required dimensions. Each dimension is a list:
  97. `dimension: [id, name, algorithm, multiplier, divisor]`
  98. - `id` : The id of the dimension. Mandatory unique field (string) required in order to set a value.
  99. - `name`: The name to be presented in the chart. If null id will be used.
  100. - `algorithm`: Can be absolute or incremental. If null absolute is used. Incremental shows the difference from the
  101. previous value.
  102. - `multiplier`: an integer value to divide the collected value, if null, 1 is used
  103. - `divisor`: an integer value to divide the collected value, if null, 1 is used
  104. The multiplier/divisor fields are used in cases where the value to be displayed should be decimal since Netdata only
  105. gathers integer values.
  106. ### Parse the data to extract or create the actual data to be represented
  107. Once the data is received, your collector should process it in order to get the values required. If, for example, the
  108. received data is a JSON string, you should parse the data to get the required data to be used for the charts.
  109. ### Assign the correct values to the charts
  110. Once you have process your data and get the required values, you need to assign those values to the charts you created.
  111. This is done using the `data` dictionary, which is in the form:
  112. `"data": {dimension_id: value }`, where:
  113. - `dimension_id`: The id of a defined dimension in a created chart.
  114. - `value`: The numerical value to associate with this dimension.
  115. ### Set the order for the charts to be displayed
  116. Next, set the order of chart appearance with the `ORDER` list, which is in the form:
  117. `"ORDER": [chart_name_1,chart_name_2, …., chart_name_X]`, where:
  118. - `chart_name_x`: is the chart name to be shown in X order.
  119. ### Give the charts data to Netdata for visualization
  120. Our plugin should just rerun the data dictionary. If everything is set correctly the charts should be updated with the
  121. correct values.
  122. ## Framework classes
  123. Every module needs to implement its own `Service` class. This class should inherit from one of the framework classes:
  124. - `SimpleService`
  125. - `UrlService`
  126. - `SocketService`
  127. - `LogService`
  128. - `ExecutableService`
  129. Also it needs to invoke the parent class constructor in a specific way as well as assign global variables to class variables.
  130. For example, the snippet below is from the
  131. [RabbitMQ collector](https://github.com/netdata/netdata/blob/91f3268e9615edd393bd43de4ad8068111024cc9/collectors/python.d.plugin/rabbitmq/rabbitmq.chart.py#L273).
  132. This collector uses an HTTP endpoint and uses the `UrlService` framework class, which only needs to define an HTTP
  133. endpoint for data collection.
  134. ```python
  135. class Service(UrlService):
  136. def __init__(self, configuration=None, name=None):
  137. UrlService.__init__(self, configuration=configuration, name=name)
  138. self.order = ORDER
  139. self.definitions = CHARTS
  140. self.url = '{0}://{1}:{2}'.format(
  141. configuration.get('scheme', 'http'),
  142. configuration.get('host', '127.0.0.1'),
  143. configuration.get('port', 15672),
  144. )
  145. self.node_name = str()
  146. self.vhost = VhostStatsBuilder()
  147. self.collected_vhosts = set()
  148. self.collect_queues_metrics = configuration.get('collect_queues_metrics', False)
  149. self.debug("collect_queues_metrics is {0}".format("enabled" if self.collect_queues_metrics else "disabled"))
  150. if self.collect_queues_metrics:
  151. self.queue = QueueStatsBuilder()
  152. self.collected_queues = set()
  153. ```
  154. In our use-case, we use the `SimpleService` framework, since there is no framework class that suits our needs.
  155. You can find below the [framework class reference](#framework-class-reference).
  156. ## An example collector using weather station data
  157. Let's build a custom Python collector for visualizing data from a weather monitoring station.
  158. ### Determine how to gather metrics data
  159. This example assumes you can gather metrics data through HTTP requests to a web server, and that the data provided are
  160. numeric values for temperature, humidity and pressure. It also assumes you can get the `min`, `max`, and `average`
  161. values for these metrics.
  162. ### Chart creation
  163. First, create a single chart that shows the latest temperature metric:
  164. ```python
  165. CHARTS = {
  166. "temp_current": {
  167. "options": ["my_temp", "Temperature", "Celsius", "TEMP", "weather_station.temperature", "line"],
  168. "lines": [
  169. ["current_temp_id","current_temperature"]
  170. ]
  171. }
  172. }
  173. ```
  174. ## Parse the data to extract or create the actual data to be represented
  175. Every collector must implement `_get_data`. This method should grab raw data from `_get_raw_data`,
  176. parse it, and return a dictionary where keys are unique dimension names, or `None` if no data is collected.
  177. For example:
  178. ```py
  179. def _get_data(self):
  180. try:
  181. raw = self._get_raw_data().split(" ")
  182. return {'active': int(raw[2])}
  183. except (ValueError, AttributeError):
  184. return None
  185. ```
  186. In our weather data collector we declare `_get_data` as follows:
  187. ```python
  188. def get_data(self):
  189. #The data dict is basically all the values to be represented
  190. # The entries are in the format: { "dimension": value}
  191. #And each "dimension" should belong to a chart.
  192. data = dict()
  193. self.populate_data()
  194. data['current_temperature'] = self.weather_data["temp"]
  195. return data
  196. ```
  197. A standard practice would be to either get the data on JSON format or transform them to JSON format. We use a dictionary
  198. to give this format and issue random values to simulate received data.
  199. The following code iterates through the names of the expected values and creates a dictionary with the name of the value
  200. as `key`, and a random value as `value`.
  201. ```python
  202. weather_data=dict()
  203. weather_metrics=[
  204. "temp","av_temp","min_temp","max_temp",
  205. "humid","av_humid","min_humid","max_humid",
  206. "pressure","av_pressure","min_pressure","max_pressure",
  207. ]
  208. def populate_data(self):
  209. for metric in self.weather_metrics:
  210. self.weather_data[metric]=random.randint(0,100)
  211. ```
  212. ### Assign the correct values to the charts
  213. Our chart has a dimension called `current_temp_id`, which should have the temperature value received.
  214. ```python
  215. data['current_temp_id'] = self.weather_data["temp"]
  216. ```
  217. ### Set the order for the charts to be displayed
  218. ```python
  219. ORDER = [
  220. "temp_current"
  221. ]
  222. ```
  223. ### Give the charts data to Netdata for visualization
  224. ```python
  225. return data
  226. ```
  227. A snapshot of the chart created by this plugin:
  228. ![A snapshot of the chart created by this plugin](https://i.imgur.com/2tR9KvF.png)
  229. Here's the current source code for the data collector:
  230. ```python
  231. # -*- coding: utf-8 -*-
  232. # Description: howto weather station netdata python.d module
  233. # Author: Panagiotis Papaioannou (papajohn-uop)
  234. # SPDX-License-Identifier: GPL-3.0-or-later
  235. from bases.FrameworkServices.SimpleService import SimpleService
  236. import random
  237. NETDATA_UPDATE_EVERY=1
  238. priority = 90000
  239. ORDER = [
  240. "temp_current"
  241. ]
  242. CHARTS = {
  243. "temp_current": {
  244. "options": ["my_temp", "Temperature", "Celsius", "TEMP", "weather_station.temperature", "line"],
  245. "lines": [
  246. ["current_temperature"]
  247. ]
  248. }
  249. }
  250. class Service(SimpleService):
  251. def __init__(self, configuration=None, name=None):
  252. SimpleService.__init__(self, configuration=configuration, name=name)
  253. self.order = ORDER
  254. self.definitions = CHARTS
  255. #values to show at graphs
  256. self.values=dict()
  257. @staticmethod
  258. def check():
  259. return True
  260. weather_data=dict()
  261. weather_metrics=[
  262. "temp","av_temp","min_temp","max_temp",
  263. "humid","av_humid","min_humid","max_humid",
  264. "pressure","av_pressure","min_pressure","max_pressure",
  265. ]
  266. def logMe(self,msg):
  267. self.debug(msg)
  268. def populate_data(self):
  269. for metric in self.weather_metrics:
  270. self.weather_data[metric]=random.randint(0,100)
  271. def get_data(self):
  272. #The data dict is basically all the values to be represented
  273. # The entries are in the format: { "dimension": value}
  274. #And each "dimension" should belong to a chart.
  275. data = dict()
  276. self.populate_data()
  277. data['current_temperature'] = self.weather_data["temp"]
  278. return data
  279. ```
  280. ## Add more charts to the existing weather station collector
  281. To enrich the example, add another chart the collector which to present the humidity metric.
  282. Add a new entry in the `CHARTS` dictionary with the definition for the new chart.
  283. ```python
  284. CHARTS = {
  285. 'temp_current': {
  286. 'options': ['my_temp', 'Temperature', 'Celsius', 'TEMP', 'weather_station.temperature', 'line'],
  287. 'lines': [
  288. ['current_temperature']
  289. ]
  290. },
  291. 'humid_current': {
  292. 'options': ['my_humid', 'Humidity', '%', 'HUMIDITY', 'weather_station.humidity', 'line'],
  293. 'lines': [
  294. ['current_humidity']
  295. ]
  296. }
  297. }
  298. ```
  299. The data has already been created and parsed by the `weather_data=dict()` function, so you only need to populate the
  300. `current_humidity` dimension `self.weather_data["humid"]`.
  301. ```python
  302. data['current_temperature'] = self.weather_data["temp"]
  303. data['current_humidity'] = self.weather_data["humid"]
  304. ```
  305. Next, put the new `humid_current` chart into the `ORDER` list:
  306. ```python
  307. ORDER = [
  308. 'temp_current',
  309. 'humid_current'
  310. ]
  311. ```
  312. [Restart Netdata](/docs/netdata-agent/start-stop-restart.md) to see the new humidity
  313. chart:
  314. ![A snapshot of the modified chart](https://i.imgur.com/XOeCBmg.png)
  315. Next, time to add one more chart that visualizes the average, minimum, and maximum temperature values.
  316. Add a new entry in the `CHARTS` dictionary with the definition for the new chart. Since you want three values
  317. represented in this this chart, add three dimensions. You should also use the same `FAMILY` value in the charts (`TEMP`)
  318. so that those two charts are grouped together.
  319. ```python
  320. CHARTS = {
  321. 'temp_current': {
  322. 'options': ['my_temp', 'Temperature', 'Celsius', 'TEMP', 'weather_station.temperature', 'line'],
  323. 'lines': [
  324. ['current_temperature']
  325. ]
  326. },
  327. 'temp_stats': {
  328. 'options': ['stats_temp', 'Temperature', 'Celsius', 'TEMP', 'weather_station.temperature_stats', 'line'],
  329. 'lines': [
  330. ['min_temperature'],
  331. ['max_temperature'],
  332. ['avg_temperature']
  333. ]
  334. },
  335. 'humid_current': {
  336. 'options': ['my_humid', 'Humidity', '%', 'HUMIDITY', 'weather_station.humidity', 'line'],
  337. 'lines': [
  338. ['current_humidity']
  339. ]
  340. }
  341. }
  342. ```
  343. As before, initiate new dimensions and add data to them:
  344. ```python
  345. data['current_temperature'] = self.weather_data["temp"]
  346. data['min_temperature'] = self.weather_data["min_temp"]
  347. data['max_temperature'] = self.weather_data["max_temp"]
  348. data['avg_temperature`'] = self.weather_data["av_temp"]
  349. data['current_humidity'] = self.weather_data["humid"]
  350. ```
  351. Finally, set the order for the `temp_stats` chart:
  352. ```python
  353. ORDER = [
  354. 'temp_current',
  355. ‘temp_stats’
  356. 'humid_current'
  357. ]
  358. ```
  359. [Restart Netdata](/docs/netdata-agent/start-stop-restart.md) to see the new min/max/average temperature chart with multiple dimensions:
  360. ![A snapshot of the modified chart](https://i.imgur.com/g7E8lnG.png)
  361. ## Add a configuration file
  362. The last piece of the puzzle to create a fully robust Python collector is the configuration file. Python.d uses
  363. configuration in [YAML](https://www.tutorialspoint.com/yaml/yaml_basics.htm) format and is used as follows:
  364. - Create a configuration file in the same directory as the `<plugin_name>.chart.py`. Name it `<plugin_name>.conf`.
  365. - Define a `job`, which is an instance of the collector. It is useful when you want to collect data from different
  366. sources with different attributes. For example, we could gather data from 2 different weather stations, which use
  367. different temperature measures: Fahrenheit and Celsius.
  368. - You can define many different jobs with the same name, but with different attributes. Netdata will try each job
  369. serially and will stop at the first job that returns data. If multiple jobs have the same name, only one of them can
  370. run. This enables you to define different "ways" to fetch data from a particular data source so that the collector has
  371. more chances to work out-of-the-box. For example, if the data source supports both `HTTP` and `linux socket`, you can
  372. define 2 jobs named `local`, with each using a different method.
  373. - Check the `example` collector configuration file on
  374. [GitHub](https://github.com/netdata/netdata/blob/master/src/collectors/python.d.plugin/example/example.conf) to get a
  375. sense of the structure.
  376. ```yaml
  377. weather_station_1:
  378. name: 'Greece'
  379. endpoint: 'https://endpoint_1.com'
  380. port: 67
  381. type: 'celsius'
  382. weather_station_2:
  383. name: 'Florida USA'
  384. endpoint: 'https://endpoint_2.com'
  385. port: 67
  386. type: 'fahrenheit'
  387. ```
  388. Next, access the above configuration variables in the `__init__` function:
  389. ```python
  390. def __init__(self, configuration=None, name=None):
  391. SimpleService.__init__(self, configuration=configuration, name=name)
  392. self.endpoint = self.configuration.get('endpoint', <default_endpoint>)
  393. ```
  394. Because you initiate the `framework class` (e.g `SimpleService.__init__`), the configuration will be available
  395. throughout the whole `Service` class of your module, as `self.configuration`. Finally, note that the `configuration.get`
  396. function takes 2 arguments, one with the name of the configuration field and one with a default value in case it doesn't
  397. find the configuration field. This allows you to define sane defaults for your collector.
  398. Moreover, when creating the configuration file, create a large comment section that describes the configuration
  399. variables and inform the user about the defaults. For example, take a look at the `example` collector on
  400. [GitHub](https://github.com/netdata/netdata/blob/master/src/collectors/python.d.plugin/example/example.conf).
  401. You can read more about the configuration file on the [`python.d.plugin`
  402. documentation](/src/collectors/python.d.plugin/README.md).
  403. You can find the source code for the above examples on [GitHub](https://github.com/papajohn-uop/netdata).
  404. ## Pull Request Checklist for Python Plugins
  405. Pull requests should be created in <https://github.com/netdata/community>.
  406. This is a generic checklist for submitting a new Python plugin for Netdata. It is by no means comprehensive.
  407. At minimum, to be buildable and testable, the PR needs to include:
  408. - The module itself, following proper naming conventions: `collectors/python.d.plugin/<module_dir>/<module_name>.chart.py`
  409. - A README.md file for the plugin under `collectors/python.d.plugin/<module_dir>`.
  410. - The configuration file for the module: `collectors/python.d.plugin/<module_dir>/<module_name>.conf`. Python config files are in YAML format, and should include comments describing what options are present. The instructions are also needed in the configuration section of the README.md
  411. - A basic configuration for the plugin in the appropriate global config file: `collectors/python.d.plugin/python.d.conf`, which is also in YAML format. Either add a line that reads `# <module_name>: yes` if the module is to be enabled by default, or one that reads `<module_name>: no` if it is to be disabled by default.
  412. - A makefile for the plugin at `collectors/python.d.plugin/<module_dir>/Makefile.inc`. Check an existing plugin for what this should look like.
  413. - A line in `collectors/python.d.plugin/Makefile.am` including the above-mentioned makefile. Place it with the other plugin includes (please keep the includes sorted alphabetically).
  414. - Optionally, chart information in `src/web/gui/dashboard_info.js`. This generally involves specifying a name and icon for the section, and may include descriptions for the section or individual charts.
  415. - Optionally, some default alert configurations for your collector in `health/health.d/<module_name>.conf` and a line adding `<module_name>.conf` in `health/Makefile.am`.
  416. ## Framework class reference
  417. Every framework class has some user-configurable variables which are specific to this particular class. Those variables should have default values initialized in the child class constructor.
  418. If module needs some additional user-configurable variable, it can be accessed from the `self.configuration` list and assigned in constructor or custom `check` method. Example:
  419. ```py
  420. def __init__(self, configuration=None, name=None):
  421. UrlService.__init__(self, configuration=configuration, name=name)
  422. try:
  423. self.baseurl = str(self.configuration['baseurl'])
  424. except (KeyError, TypeError):
  425. self.baseurl = "http://localhost:5001"
  426. ```
  427. Classes implement `_get_raw_data` which should be used to grab raw data. This method usually returns a list of strings.
  428. ### `SimpleService`
  429. This is last resort class, if a new module cannot be written by using other framework class this one can be used.
  430. Example: `ceph`, `sensors`
  431. It is the lowest-level class which implements most of module logic, like:
  432. - threading
  433. - handling run times
  434. - chart formatting
  435. - logging
  436. - chart creation and updating
  437. ### `LogService`
  438. Examples: `apache_cache`, `nginx_log`_
  439. Variable from config file: `log_path`.
  440. Object created from this class reads new lines from file specified in `log_path` variable. It will check if file exists and is readable. Also `_get_raw_data` returns list of strings where each string is one line from file specified in `log_path`.
  441. ### `ExecutableService`
  442. Examples: `exim`, `postfix`_
  443. Variable from config file: `command`.
  444. This allows to execute a shell command in a secure way. It will check for invalid characters in `command` variable and won't proceed if there is one of:
  445. - '&'
  446. - '|'
  447. - ';'
  448. - '>'
  449. - '\<'
  450. For additional security it uses python `subprocess.Popen` (without `shell=True` option) to execute command. Command can be specified with absolute or relative name. When using relative name, it will try to find `command` in `PATH` environment variable as well as in `/sbin` and `/usr/sbin`.
  451. `_get_raw_data` returns list of decoded lines returned by `command`.
  452. ### UrlService
  453. Examples: `apache`, `nginx`, `tomcat`_
  454. Variables from config file: `url`, `user`, `pass`.
  455. If data is grabbed by accessing service via HTTP protocol, this class can be used. It can handle HTTP Basic Auth when specified with `user` and `pass` credentials.
  456. Please note that the config file can use different variables according to the specification of each module.
  457. `_get_raw_data` returns list of utf-8 decoded strings (lines).
  458. ### SocketService
  459. Examples: `dovecot`, `redis`
  460. Variables from config file: `unix_socket`, `host`, `port`, `request`.
  461. Object will try execute `request` using either `unix_socket` or TCP/IP socket with combination of `host` and `port`. This can access unix sockets with SOCK_STREAM or SOCK_DGRAM protocols and TCP/IP sockets in version 4 and 6 with SOCK_STREAM setting.
  462. Sockets are accessed in non-blocking mode with 15 second timeout.
  463. After every execution of `_get_raw_data` socket is closed, to prevent this module needs to set `_keep_alive` variable to `True` and implement custom `_check_raw_data` method.
  464. `_check_raw_data` should take raw data and return `True` if all data is received otherwise it should return `False`. Also it should do it in fast and efficient way.