portcheck.chart.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. # -*- coding: utf-8 -*-
  2. # Description: simple port check netdata python.d module
  3. # Original Author: ccremer (github.com/ccremer)
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. import socket
  6. try:
  7. from time import monotonic as time
  8. except ImportError:
  9. from time import time
  10. from bases.FrameworkServices.SimpleService import SimpleService
  11. # default module values (can be overridden per job in `config`)
  12. priority = 60000
  13. PORT_LATENCY = 'connect'
  14. PORT_SUCCESS = 'success'
  15. PORT_TIMEOUT = 'timeout'
  16. PORT_FAILED = 'no_connection'
  17. ORDER = ['latency', 'status']
  18. CHARTS = {
  19. 'latency': {
  20. 'options': [None, 'TCP connect latency', 'ms', 'latency', 'portcheck.latency', 'line'],
  21. 'lines': [
  22. [PORT_LATENCY, 'connect', 'absolute', 100, 1000]
  23. ]
  24. },
  25. 'status': {
  26. 'options': [None, 'Portcheck status', 'boolean', 'status', 'portcheck.status', 'line'],
  27. 'lines': [
  28. [PORT_SUCCESS, 'success', 'absolute'],
  29. [PORT_TIMEOUT, 'timeout', 'absolute'],
  30. [PORT_FAILED, 'no connection', 'absolute']
  31. ]
  32. }
  33. }
  34. # Not deriving from SocketService, too much is different
  35. class Service(SimpleService):
  36. def __init__(self, configuration=None, name=None):
  37. SimpleService.__init__(self, configuration=configuration, name=name)
  38. self.order = ORDER
  39. self.definitions = CHARTS
  40. self.host = self.configuration.get('host')
  41. self.port = self.configuration.get('port')
  42. self.timeout = self.configuration.get('timeout', 1)
  43. def check(self):
  44. """
  45. Parse configuration, check if configuration is available, and dynamically create chart lines data
  46. :return: boolean
  47. """
  48. if self.host is None or self.port is None:
  49. self.error('Host or port missing')
  50. return False
  51. if not isinstance(self.port, int):
  52. self.error('"port" is not an integer. Specify a numerical value, not service name.')
  53. return False
  54. self.debug('Enabled portcheck: {host}:{port}, update every {update}s, timeout: {timeout}s'.format(
  55. host=self.host, port=self.port, update=self.update_every, timeout=self.timeout
  56. ))
  57. # We will accept any (valid-ish) configuration, even if initial connection fails (a service might be down from
  58. # the beginning)
  59. return True
  60. def _get_data(self):
  61. """
  62. Get data from socket
  63. :return: dict
  64. """
  65. data = dict()
  66. data[PORT_SUCCESS] = 0
  67. data[PORT_TIMEOUT] = 0
  68. data[PORT_FAILED] = 0
  69. success = False
  70. try:
  71. for socket_config in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
  72. # use first working socket
  73. sock = self._create_socket(socket_config)
  74. if sock is not None:
  75. self._connect2socket(data, socket_config, sock)
  76. self._disconnect(sock)
  77. success = True
  78. break
  79. except socket.gaierror as error:
  80. self.debug('Failed to connect to "{host}:{port}", error: {error}'.format(
  81. host=self.host, port=self.port, error=error
  82. ))
  83. # We could not connect
  84. if not success:
  85. data[PORT_FAILED] = 1
  86. return data
  87. def _create_socket(self, socket_config):
  88. af, sock_type, proto, _, sa = socket_config
  89. try:
  90. self.debug('Creating socket to "{address}", port {port}'.format(address=sa[0], port=sa[1]))
  91. sock = socket.socket(af, sock_type, proto)
  92. sock.settimeout(self.timeout)
  93. return sock
  94. except socket.error as error:
  95. self.debug('Failed to create socket "{address}", port {port}, error: {error}'.format(
  96. address=sa[0], port=sa[1], error=error
  97. ))
  98. return None
  99. def _connect2socket(self, data, socket_config, sock):
  100. """
  101. Connect to a socket, passing the result of getaddrinfo()
  102. :return: dict
  103. """
  104. af, _, proto, _, sa = socket_config
  105. port = str(sa[1])
  106. try:
  107. self.debug('Connecting socket to "{address}", port {port}'.format(address=sa[0], port=port))
  108. start = time()
  109. sock.connect(sa)
  110. diff = time() - start
  111. self.debug('Connected to "{address}", port {port}, latency {latency}'.format(
  112. address=sa[0], port=port, latency=diff
  113. ))
  114. # we will set it at least 0.1 ms. 0.0 would mean failed connection (handy for 3rd-party-APIs)
  115. data[PORT_LATENCY] = max(round(diff * 10000), 0)
  116. data[PORT_SUCCESS] = 1
  117. except socket.timeout as error:
  118. self.debug('Socket timed out on "{address}", port {port}, error: {error}'.format(
  119. address=sa[0], port=port, error=error
  120. ))
  121. data[PORT_TIMEOUT] = 1
  122. except socket.error as error:
  123. self.debug('Failed to connect to "{address}", port {port}, error: {error}'.format(
  124. address=sa[0], port=port, error=error
  125. ))
  126. data[PORT_FAILED] = 1
  127. def _disconnect(self, sock):
  128. """
  129. Close socket connection
  130. :return:
  131. """
  132. if sock is not None:
  133. try:
  134. self.debug('Closing socket')
  135. sock.shutdown(2) # 0 - read, 1 - write, 2 - all
  136. sock.close()
  137. except socket.error:
  138. pass