counters.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. # Copyright (C) 2012-2013 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. """BIND 10 statistics counters module
  16. This module handles the statistics counters for BIND 10 modules. For
  17. using the module `counter.py`, first a counters object should be created
  18. in each module (like b10-xfrin or b10-xfrout) after importing this
  19. module. A spec file can be specified as an argument when creating the
  20. counters object:
  21. from isc.statistics import Counters
  22. self.counters = Counters("/path/to/foo.spec")
  23. The first argument of Counters() can be specified, which is the location
  24. of the specification file (like src/bin/xfrout/xfrout.spec). If Counters
  25. is constructed this way, statistics counters can be accessed from each
  26. module. For example, in case that the item `xfrreqdone` is defined in
  27. statistics_spec in xfrout.spec, the following methods are
  28. callable. Since these methods require the string of the zone name in the
  29. first argument, if we have the following code in b10-xfrout:
  30. self.counters.inc('zones', zone_name, 'xfrreqdone')
  31. then the counter for xfrreqdone corresponding to zone_name is
  32. incremented. For getting the current number of this counter, we can use
  33. the following code:
  34. number = self.counters.get('zones', zone_name, 'xfrreqdone')
  35. then the current count is obtained and set in the variable
  36. `number`. Such a getter method would be mainly used for unit-testing.
  37. As other example, for the item `axfr_running`, the decrementer method is
  38. also callable. This method is used for decrementing a counter. For the
  39. item `axfr_running`, an argument like zone name is not required:
  40. self.counters.dec('axfr_running')
  41. These methods are effective in other modules. For example, in case that
  42. this module `counter.py` is once imported in a main module such as
  43. b10-xfrout, then for the item `notifyoutv4`, the `inc()` method can be
  44. invoked in another module such as notify_out.py, which is firstly
  45. imported in the main module.
  46. self.counters.inc('zones', zone_name, 'notifyoutv4')
  47. In this example this is for incrementing the counter of the item
  48. `notifyoutv4`. Thus, such statement can be also written in another
  49. library like isc.notify.notify_out. If this module `counter.py` isn't
  50. imported in the main module but imported in such a library module as
  51. isc.notify.notify_out, in this example, empty methods would be invoked,
  52. which is directly defined in `counter.py`.
  53. """
  54. import threading
  55. import isc.config
  56. from datetime import datetime
  57. # static internal functions
  58. def _add_counter(element, spec, identifier):
  59. """Returns value of the identifier if the identifier is in the
  60. element. Otherwise, sets a default value from the spec and
  61. returns it. If the top-level type of the identifier is named_set
  62. and the second-level type is map, it sets a set of default values
  63. under the level and then returns the default value of the
  64. identifier. This method raises DataNotFoundError if the element is
  65. invalid for spec."""
  66. try:
  67. return isc.cc.data.find(element, identifier)
  68. except isc.cc.data.DataNotFoundError:
  69. pass
  70. # Note: If there is a named_set type item in the statistics spec
  71. # and if there are map type items under it, all of items under the
  72. # map type item need to be added. For example, we're assuming that
  73. # this method is now adding a counter whose identifier is like
  74. # dir1/dir2/dir3/counter1. If both of dir1 and dir2 are named_set
  75. # types, and if dir3 is a map type, and if counter1, counter2, and
  76. # counter3 are defined as items under dir3 by the statistics spec,
  77. # this method would add other two counters:
  78. #
  79. # dir1/dir2/dir3/counter2
  80. # dir1/dir2/dir3/counter3
  81. #
  82. # Otherwise this method just adds the only counter
  83. # dir1/dir2/dir3/counter1.
  84. # examine spec from the top-level item and know whether
  85. # has_named_set, and check whether spec and identifier are correct
  86. pidr = ''
  87. has_named_set = False
  88. for idr in identifier.split('/'):
  89. if len(pidr) > 0:
  90. idr = pidr + '/' + idr
  91. spec_ = isc.config.find_spec_part(spec, idr)
  92. if isc.config.spec_part_is_named_set(spec_):
  93. has_named_set = True
  94. break
  95. pidr = idr
  96. # add all elements in map type if has_named_set
  97. has_map = False
  98. if has_named_set:
  99. p_idr = identifier.rsplit('/', 1)[0]
  100. p_spec = isc.config.find_spec_part(spec, p_idr)
  101. if isc.config.spec_part_is_map(p_spec):
  102. has_map = True
  103. for name in isc.config.spec_name_list(p_spec['map_item_spec']):
  104. idr_ = p_idr + '/' + name
  105. spc_ = isc.config.find_spec_part(spec, idr_)
  106. isc.cc.data.set(element, idr_, spc_['item_default'])
  107. # otherwise add a specific element
  108. if not has_map:
  109. spec_ = isc.config.find_spec_part(spec, identifier)
  110. isc.cc.data.set(element, identifier, spec_['item_default'])
  111. return isc.cc.data.find(element, identifier)
  112. def _set_counter(element, spec, identifier, value):
  113. """Invokes _add_counter() for checking whether the identifier is
  114. in the element. If not, it creates a new identifier in the element
  115. and set the default value from the spec. After that, it sets the
  116. value specified in the arguments."""
  117. _add_counter(element, spec, identifier)
  118. isc.cc.data.set(element, identifier, value)
  119. def _get_counter(element, identifier):
  120. """Returns the value of the identifier in the element"""
  121. return isc.cc.data.find(element, identifier)
  122. def _inc_counter(element, spec, identifier, step=1):
  123. """Increments the value of the identifier in the element to the
  124. step from the current value. If the identifier isn't in the
  125. element, it creates a new identifier in the element."""
  126. isc.cc.data.set(element, identifier,
  127. _add_counter(element, spec, identifier) + step)
  128. def _start_timer():
  129. """Returns the current datetime as a datetime object."""
  130. return datetime.now()
  131. def _stop_timer(start_time, element, spec, identifier):
  132. """Sets duration time in seconds as a value of the identifier in
  133. the element, which is in seconds between start_time and the
  134. current time and is float-type."""
  135. delta = datetime.now() - start_time
  136. # FIXME: The following statement can be replaced by:
  137. # sec = delta.total_seconds()
  138. # but total_seconds() is not available in Python 3.1. Please update
  139. # this code when we depend on Python 3.2.
  140. sec = round(delta.days * 86400 + delta.seconds + \
  141. delta.microseconds * 1E-6, 6)
  142. _set_counter(element, spec, identifier, sec)
  143. def _concat(*args, sep='/'):
  144. """A helper function that is used to generate an identifier for
  145. statistics item names. It concatenates words in args with a
  146. separator('/')
  147. """
  148. return sep.join(args)
  149. class _Statistics():
  150. """Statistics data set"""
  151. # default statistics data
  152. _data = {}
  153. # default statistics spec used in case the specfile is omitted when
  154. # constructing a Counters() object
  155. _spec = [
  156. {
  157. "item_name": "zones",
  158. "item_type": "named_set",
  159. "item_optional": False,
  160. "item_default": {
  161. "_SERVER_" : {
  162. "notifyoutv4" : 0,
  163. "notifyoutv6" : 0
  164. }
  165. },
  166. "item_title": "Zone names",
  167. "item_description": "Zone names",
  168. "named_set_item_spec": {
  169. "item_name": "classname",
  170. "item_type": "named_set",
  171. "item_optional": False,
  172. "item_default": {},
  173. "item_title": "RR class name",
  174. "item_description": "RR class name",
  175. "named_set_item_spec": {
  176. "item_name": "zonename",
  177. "item_type": "map",
  178. "item_optional": False,
  179. "item_default": {},
  180. "item_title": "Zone name",
  181. "item_description": "Zone name",
  182. "map_item_spec": [
  183. {
  184. "item_name": "notifyoutv4",
  185. "item_type": "integer",
  186. "item_optional": False,
  187. "item_default": 0,
  188. "item_title": "IPv4 notifies",
  189. "item_description": "Number of IPv4 notifies per zone name sent out"
  190. },
  191. {
  192. "item_name": "notifyoutv6",
  193. "item_type": "integer",
  194. "item_optional": False,
  195. "item_default": 0,
  196. "item_title": "IPv6 notifies",
  197. "item_description": "Number of IPv6 notifies per zone name sent out"
  198. }
  199. ]
  200. }
  201. }
  202. }
  203. ]
  204. class Counters():
  205. """A class for holding and manipulating all statistics counters
  206. for a module. A Counters object may be created by specifying a spec
  207. file of the module in argument. According to statistics
  208. specification in the spec file, a counter value can be incremented,
  209. decremented or obtained. Methods such as inc(), dec() and get() are
  210. useful for this. Counters objects also have timer functionality.
  211. The timer can be started and stopped, and the duration between
  212. start and stop can be obtained. Methods such as start_timer(),
  213. stop_timer() and get() are useful for this. Saved counters can be
  214. cleared by the method clear_all(). Manipulating counters and
  215. timers can be temporarily disabled. If disabled, counter values are
  216. not changed even if methods to update them are invoked. Including
  217. per-zone counters, a list of counters which can be handled in the
  218. class are like the following:
  219. zones/IN/example.com./notifyoutv4
  220. zones/IN/example.com./notifyoutv6
  221. zones/IN/example.com./xfrrej
  222. zones/IN/example.com./xfrreqdone
  223. zones/IN/example.com./soaoutv4
  224. zones/IN/example.com./soaoutv6
  225. zones/IN/example.com./axfrreqv4
  226. zones/IN/example.com./axfrreqv6
  227. zones/IN/example.com./ixfrreqv4
  228. zones/IN/example.com./ixfrreqv6
  229. zones/IN/example.com./xfrsuccess
  230. zones/IN/example.com./xfrfail
  231. zones/IN/example.com./last_ixfr_duration
  232. zones/IN/example.com./last_axfr_duration
  233. ixfr_running
  234. axfr_running
  235. socket/unixdomain/open
  236. socket/unixdomain/openfail
  237. socket/unixdomain/close
  238. socket/unixdomain/bindfail
  239. socket/unixdomain/acceptfail
  240. socket/unixdomain/accept
  241. socket/unixdomain/senderr
  242. socket/unixdomain/recverr
  243. socket/ipv4/tcp/open
  244. socket/ipv4/tcp/openfail
  245. socket/ipv4/tcp/close
  246. socket/ipv4/tcp/connfail
  247. socket/ipv4/tcp/conn
  248. socket/ipv4/tcp/senderr
  249. socket/ipv4/tcp/recverr
  250. socket/ipv6/tcp/open
  251. socket/ipv6/tcp/openfail
  252. socket/ipv6/tcp/close
  253. socket/ipv6/tcp/connfail
  254. socket/ipv6/tcp/conn
  255. socket/ipv6/tcp/senderr
  256. socket/ipv6/tcp/recverr
  257. """
  258. # '_SERVER_' is a special zone name representing an entire
  259. # count. It doesn't mean a specific zone, but it means an
  260. # entire count in the server.
  261. _entire_server = '_SERVER_'
  262. # zone names are contained under this dirname in the spec file.
  263. _perzone_prefix = 'zones'
  264. # default statistics data set
  265. _statistics = _Statistics()
  266. def __init__(self, spec_file_name=None):
  267. """A constructor for the Counters class. A path to the spec file
  268. can be specified in spec_file_name. Statistics data based on
  269. statistics spec can be accumulated if spec_file_name is
  270. specified. If omitted, a default statistics spec is used. The
  271. default statistics spec is defined in a hidden class named
  272. _Statistics().
  273. """
  274. self._zones_item_list = []
  275. self._start_time = {}
  276. self._disabled = False
  277. self._rlock = threading.RLock()
  278. if not spec_file_name: return
  279. # change the default statistics spec
  280. self._statistics._spec = \
  281. isc.config.module_spec_from_file(spec_file_name).\
  282. get_statistics_spec()
  283. if self._perzone_prefix in \
  284. isc.config.spec_name_list(self._statistics._spec):
  285. self._zones_item_list = isc.config.spec_name_list(
  286. isc.config.find_spec_part(
  287. self._statistics._spec,
  288. '%s/%s/%s' % (self._perzone_prefix,
  289. '_CLASS_', self._entire_server)))
  290. def clear_all(self):
  291. """clears all statistics data"""
  292. with self._rlock:
  293. self._statistics._data = {}
  294. def disable(self):
  295. """disables incrementing/decrementing counters"""
  296. with self._rlock:
  297. self._disabled = True
  298. def enable(self):
  299. """enables incrementing/decrementing counters"""
  300. with self._rlock:
  301. self._disabled = False
  302. def _incdec(self, *args, step=1):
  303. """A common helper function for incrementing or decrementing a
  304. counter. It acquires a lock to support multi-threaded
  305. use. isc.cc.data.DataNotFoundError is raised when incrementing
  306. the counter of the item undefined in the spec file."""
  307. identifier = _concat(*args)
  308. with self._rlock:
  309. if self._disabled: return
  310. _inc_counter(self._statistics._data,
  311. self._statistics._spec,
  312. identifier, step)
  313. def inc(self, *args):
  314. """An incrementer for a counter. It acquires a lock to support
  315. multi-threaded use. isc.cc.data.DataNotFoundError is raised when
  316. incrementing the counter of the item undefined in the spec file."""
  317. return self._incdec(*args)
  318. def dec(self, *args):
  319. """A decrementer for a counter. It acquires a lock to support
  320. multi-threaded use. isc.cc.data.DataNotFoundError is raised when
  321. decrementing the counter of the item undefined in the spec file."""
  322. return self._incdec(*args, step=-1)
  323. def get(self, *args):
  324. """A getter method for counters. It returns the current number
  325. of the specified counter. isc.cc.data.DataNotFoundError is
  326. raised when the counter doesn't have a number yet."""
  327. identifier = _concat(*args)
  328. return _get_counter(self._statistics._data, identifier)
  329. def start_timer(self, *args):
  330. """Starts a timer which is identified by args and keeps it
  331. running until stop_timer() is called. It acquires a lock to
  332. support multi-threaded use. If the specified timer is already
  333. started but not yet stopped, the last start time is
  334. overwritten."""
  335. identifier = _concat(*args)
  336. with self._rlock:
  337. if self._disabled: return
  338. isc.cc.data.set(self._start_time, identifier, _start_timer())
  339. def stop_timer(self, *args):
  340. """Stops a timer which is identified by args. It acquires a lock
  341. to support multi-threaded use. If the timer isn't started by
  342. start_timer() yet, it raises no exception. However if args
  343. aren't defined in the spec file, it raises DataNotFoundError.
  344. """
  345. identifier = _concat(*args)
  346. with self._rlock:
  347. if self._disabled: return
  348. try:
  349. start_time = isc.cc.data.find(self._start_time,
  350. identifier)
  351. except isc.cc.data.DataNotFoundError:
  352. # do not set the end time if the timer isn't started
  353. return
  354. # set the end time
  355. _stop_timer(
  356. start_time,
  357. self._statistics._data,
  358. self._statistics._spec,
  359. identifier)
  360. # A datetime value of once used timer should be deleted
  361. # for a future use.
  362. # Here, names of branch and leaf are obtained from a
  363. # string of identifier. The branch name is equivalent to
  364. # the position of datetime to be deleted and the leaf name
  365. # is equivalent to the value of datetime to be deleted.
  366. (branch, leaf) = identifier.rsplit('/', 1)
  367. # Then map of branch is obtained from self._start_time by
  368. # using isc.cc.data.find().
  369. branch_map = isc.cc.data.find(self._start_time, branch)
  370. # Finally a value of the leaf name is deleted from the
  371. # map.
  372. del branch_map[leaf]
  373. def get_statistics(self):
  374. """Calculates an entire server's counts, and returns statistics
  375. data in a format to send out to the stats module, including each
  376. counter. If nothing is counted yet, then it returns an empty
  377. dictionary."""
  378. # entire copy
  379. statistics_data = self._statistics._data.copy()
  380. # If there is no 'zones' found in statistics_data,
  381. # i.e. statistics_data contains no per-zone counter, it just
  382. # returns statistics_data because calculating total counts
  383. # across the zone names isn't necessary.
  384. if self._perzone_prefix not in statistics_data:
  385. return statistics_data
  386. zones = statistics_data[self._perzone_prefix]
  387. # Start calculation for '_SERVER_' counts
  388. zones_spec = isc.config.find_spec_part(self._statistics._spec,
  389. self._perzone_prefix)
  390. zones_data = {}
  391. for cls in zones.keys():
  392. for zone in zones[cls].keys():
  393. for (attr, val) in zones[cls][zone].items():
  394. id_str1 = '%s/%s/%s' % (cls, zone, attr)
  395. id_str2 = '%s/%s/%s' % (cls, self._entire_server, attr)
  396. _set_counter(zones_data, zones_spec, id_str1, val)
  397. _inc_counter(zones_data, zones_spec, id_str2, val)
  398. # insert entire-server counts
  399. statistics_data[self._perzone_prefix] = dict(
  400. statistics_data[self._perzone_prefix],
  401. **zones_data)
  402. return statistics_data