Rewrite of portcheck plugin. Each error code has its own dimension, which simplifies alarms.
Chris committed
Feb 24, 2018 at 21:40 UTC
a4327d68d09b03881faf546025bae727222209aa
4 files changed
+60
-70
conf.d/python.d/portcheck.conf
+3
-5
@@ -52,11 +52,9 @@
52
# update_every: 1 # [optional] the JOB's data collection frequency
53
# priority: 60000 # [optional] the JOB's order on the dashboard
54
# retries: 60 # [optional] the JOB's number of restoration attempts
55
-# timeout: 3 # [optional] the socket timeout when connecting
56
-# host: 'dns or ip' # [required] the remote host address in either IPv4, IPv6 or as DNS name
57
-# ports: # [required] the port number(s) to check (list is checked sequentially)
58
-# - 22 # at least one port number is required (do not specify well-known names e.g. 'ssh')
59
-# - 8080 # multiple ports are supported. they will be grouped in one chart.
55
+# timeout: 1 # [optional] the socket timeout when connecting
56
+# host: 'dns or ip' # [required] the remote host address in either IPv4, IPv6 or as DNS name.
57
+# port: 22 # [required] the port number to check. Specify an integer, not service name.
58
59
# You just have been warned about possible portscan blocking. The portcheck plugin is meant for simple use cases.
60
# For monitoring large networks, consider a real service monitoring tool.
python.d/README.md
+8
-10
@@ -1553,16 +1553,14 @@ Module monitors one or more remote ports per host.
1553
Following charts are drawn per host:
1554
1555
1. **Latency** ms
1556
- * For each port: TCP socket opening and closing time when connecting.
1557
- Displays latency in 0.1 ms resolution and 0.1ms will be the minimum.
1558
- If it is 0.0ms, the connection failed with error code >0 (useful for API)
1559
-
1560
-2. **Error code** int
1561
- * For each port: One of:
1562
- - 0: Connection successful
1563
- - 1: Could not create socket (dns name not resolved?)
1564
- - 2: Connection refused (port not listening or blocked)
1565
- - 3: Connection timed out (host unreachable?)
1556
+ * TCP socket opening and closing time when connecting.
1557
+ Displays latency in 0.1 ms resolution. If the connection failed, the value is missing.
1558
+
1559
+2. **Error** int
1560
+ * Connection successful: value = 1 otherwise 0
1561
+ * Could not create socket: possible DNS problems (value = 1, otherwise 0)
1562
+ * Connection refused: port not listening or blocked (value = 1, otherwise 0)
1563
+ * Connection timed out: host or port unreachable (value = 1, otherwise 0)
1564
1565
1566
### configuration
python.d/portcheck.chart.py
+45
-51
@@ -10,30 +10,30 @@ from bases.FrameworkServices.SimpleService import SimpleService
10
priority = 60000
11
retries = 60
12
13
+PORT_CONNECT = 'connect'
14
+
15
+PORT_SUCCESS = 'success'
16
+PORT_TIMEOUT = 'timeout'
17
+PORT_FAILED = 'failed'
18
+
19
ORDER = ['latency', 'error']
20
21
CHARTS = {
22
'latency': {
23
'options': [None, 'TCP connect latency', 'ms', 'latency', 'portcheck.latency', 'line'],
18
- 'lines': []
24
+ 'lines': [
25
+ [PORT_CONNECT, 'connect', 'absolute', 100, 1000]
26
+ ]
27
},
28
'error': {
29
'options': [None, 'Portcheck error code', 'code', 'error', 'portcheck.error', 'line'],
30
'lines': [
23
-
31
+ [PORT_SUCCESS, 'success', 'absolute'],
32
+ [PORT_TIMEOUT, 'timeout', 'absolute'],
33
+ [PORT_FAILED, 'failed', 'absolute']
34
]}
35
}
36
27
-# The higher the error code, the "more severe" it is.
28
-# We use steps of 5, which allows future fine-grained error codes, should we need it (fill the blanks where
29
-# appropriate).
30
-CONNECTION_FAILED = 5
31
-CONNECTION_TIMED_OUT = 10
32
-SOCKET_FAILED = 15
33
-
34
-TCP_DIMENSION_CONNECT_PREFIX = 'tcp_connect_'
35
-TCP_DIMENSION_ERROR_PREFIX = 'tcp_error_'
36
-
37
38
class Service(SimpleService):
39
def __init__(self, configuration=None, name=None):
@@ -42,7 +42,7 @@ class Service(SimpleService):
42
self.definitions = CHARTS
43
self.chart_name = ""
44
self.host = self.configuration.get('host', None)
45
- self.ports = self.configuration.get('ports', None)
45
+ self.port = self.configuration.get('port', None)
46
self.timeout = self.configuration.get('timeout', 1)
47
48
def check(self):
@@ -50,24 +50,16 @@ class Service(SimpleService):
50
Parse configuration, check if configuration is available, and dynamically create chart lines data
51
:return: boolean
52
"""
53
- if self.host is None or self.ports is None:
54
- self.error("Host and/or ports missing")
53
+ if self.host is None or self.port is None:
54
+ self.error("Host or port missing")
55
return False
56
- if not isinstance(self.ports, list):
57
- self.error('"ports" is not defined as a list. Specify a PyYaml compatible list.')
56
+ if not isinstance(self.port, int):
57
+ self.error('"port" is not an integer. Specify a numerical value, not service name.')
58
return False
59
60
- for port in self.ports:
61
- if not isinstance(port, int):
62
- self.error("{port} is not an integer. Disabling plugin.".format(port=port))
63
- return False
64
- self.debug("Enabled portcheck: {host}:{port}, update every {update}s, timeout: {timeout}s".format(
65
- host=self.host, port=port, update=self.update_every, timeout=self.timeout
66
- ))
67
- self.definitions['latency']['lines'].append(
68
- [TCP_DIMENSION_CONNECT_PREFIX + str(port), port, 'absolute', 100, 1000])
69
- self.definitions['error']['lines'].append(
70
- [TCP_DIMENSION_ERROR_PREFIX + str(port), port, 'absolute'])
60
+ self.info("Enabled portcheck: {host}:{port}, update every {update}s, timeout: {timeout}s".format(
61
+ host=self.host, port=self.port, update=self.update_every, timeout=self.timeout
62
+ ))
63
# We will accept any (valid-ish) configuration, even if initial connection fails (a service might be down from
64
# the beginning)
65
return True
@@ -79,25 +71,28 @@ class Service(SimpleService):
71
"""
72
data = dict()
73
82
- for port in self.ports:
83
- success = False
84
- try:
85
- for socket_config in socket.getaddrinfo(self.host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
86
- # use first working socket
87
- sock = self._create_socket(socket_config)
88
- if sock is not None:
89
- self._connect2socket(data, socket_config, sock)
90
- self._disconnect(sock)
91
- success = True
92
- break
93
- except socket.gaierror:
94
- success = False
95
- pass
74
+ data[PORT_SUCCESS] = 0
75
+ data[PORT_TIMEOUT] = 0
76
+ data[PORT_FAILED] = 0
77
+
78
+ success = False
79
+ try:
80
+ for socket_config in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
81
+ # use first working socket
82
+ sock = self._create_socket(socket_config)
83
+ if sock is not None:
84
+ self._connect2socket(data, socket_config, sock)
85
+ self._disconnect(sock)
86
+ success = True
87
+ break
88
+ except socket.gaierror as error:
89
+ self.debug('Failed to connect to "{host}:{port}", error: {error}'.format(
90
+ host=self.host, port=self.port, error=error
91
+ ))
92
97
- # We could not connect
98
- if not success:
99
- data[TCP_DIMENSION_CONNECT_PREFIX + str(port)] = 0
100
- data[TCP_DIMENSION_ERROR_PREFIX + str(port)] = SOCKET_FAILED
93
+ # We could not connect
94
+ if not success:
95
+ data[PORT_FAILED] = 1
96
97
return data
98
@@ -122,8 +117,6 @@ class Service(SimpleService):
117
118
af, sock_type, proto, canon_name, sa = socket_config
119
port = str(sa[1])
125
- data[TCP_DIMENSION_CONNECT_PREFIX + port] = 0
126
- data[TCP_DIMENSION_ERROR_PREFIX + port] = 0
120
try:
121
self.debug('Connecting socket to "{address}", port {port}'.format(address=sa[0], port=port))
122
start = time.time()
@@ -133,19 +126,20 @@ class Service(SimpleService):
126
address=sa[0], port=port, latency=diff
127
))
128
# we will set it at least 0.1 ms. 0.0 would mean failed connection (handy for 3rd-party-APIs)
136
- data[TCP_DIMENSION_CONNECT_PREFIX + port] = max(round(diff * 10000), 1)
129
+ data[PORT_CONNECT] = max(round(diff * 10000), 1)
130
+ data[PORT_SUCCESS] = 1
131
132
except socket.timeout as error:
133
self.debug('Socket timed out on "{address}", port {port}, error: {error}'.format(
134
address=sa[0], port=port, error=error
135
))
142
- data[TCP_DIMENSION_ERROR_PREFIX + port] = CONNECTION_TIMED_OUT
136
+ data[PORT_TIMEOUT] = 1
137
138
except socket.error as error:
139
self.debug('Failed to connect to "{address}", port {port}, error: {error}'.format(
140
address=sa[0], port=port, error=error
141
))
148
- data[TCP_DIMENSION_ERROR_PREFIX + port] = CONNECTION_FAILED
142
+ data[PORT_FAILED] = 1
143
144
def _disconnect(self, sock):
145
"""
web/dashboard_info.js
+4
-4
@@ -1733,10 +1733,10 @@ netdataDashboard.context = {
1733
},
1734
1735
'portcheck.error': {
1736
- valueRange: "[0, 3]",
1737
- info: 'The <code>Error</code> is returned by the plugin when it could not connect to the host/port. It is one of: ' +
1738
- '<code>0</code> (connection successful), <code>5</code> (port closed), <code>10</code> (host or port timeout), <code>15</code> (hostname lookup failed). ' +
1739
- 'The error code is most useful for 3rd-party apps and alarms.'
1736
+ valueRange: "[0, 1]",
1737
+ info: 'The <code>error</code> codes are returned by the plugin when it could verify the availability of the service. ' +
1738
+ 'Each error dimension will have a value of <code>1</code> if triggered. Dimension <code>success</code> is always <code>1</code> on successful connects.' +
1739
+ 'The error code is most useful for alarms and third-party apps.'
1740
},
1741
1742
// ------------------------------------------------------------------------