counter.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. # Copyright (C) 2012 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 counter module
  16. This module handles the statistics counters for BIND 10 modules. For
  17. using the module `counter.py`, firstly the init() method should be
  18. invoked in each module like b10-xfrin or b10-xfrout after importing
  19. this module.
  20. from isc.statistics import Counter
  21. Counter.init(SPECFILE_LOCATION)
  22. The first argument of Counter.init() is required, which is the
  23. location of the specification file like src/bin/xfrout/xfrout.spec. If
  24. this initial preparation is done, statistics counters can be accessed
  25. from each module. For example, in case that the item `xfrreqdone` is
  26. defined in statistics_spec in xfrout.spec, the following methods can
  27. be dynamically created: Counter.inc_xfrreqdone(),
  28. Counter.get_xfrreqdone(). Since these methods requires the string of
  29. the zone name in the first argument, in the b10-xfrout,
  30. Counter.inc_xfrreqdone(zone_name)
  31. then the xfrreqdone counter corresponding to zone_name was
  32. incremented. For getting the current number of this counter, we can do
  33. this,
  34. number = Counter.get_xfrreqdone(zone_name)
  35. then the current number was obtained and set in the above variable
  36. `number`. Such a getter method would be mainly used for unittesting.
  37. In other example, regarding the item `axfr_running`,
  38. the decrementer method is also created:
  39. Counter.dec_axfr_running(). This method is used for decrementing the
  40. counter number. Regarding the item `axfr_running`, an argument like
  41. zone name is not required.
  42. Counter.dec_axfr_running()
  43. These accessors are effective in other module. For example, in case
  44. that this module `counter.py` is once imported in such a main module
  45. as b10-xfrout, Regarding the item `notifyoutv4`, the incrementer
  46. inc_notifyoutv4() can be invoked via other module like notify_out.py,
  47. which is firstly imported in the main module.
  48. Counter.inc_notifyoutv4(zone_name)
  49. In this example this is for incrementing the counter of the item
  50. notifyoutv4. Thus, such statement can be also written in the other
  51. library like isc.notify.notify_out. If this module `counter.py` isn't
  52. imported in the main module but imported in such a library module as
  53. isc.notify.notify_out, in this example, empty methods would be
  54. invoked, which is directly defined in `counter.py`.
  55. Other accessors can be also defined in such individual class in
  56. future. For adding or modifying such accessor, we need to implement in
  57. `counter.py`.
  58. """
  59. import threading
  60. import isc.config
  61. from datetime import datetime
  62. # static internal functions
  63. def _add_counter(element, spec, identifier):
  64. """Returns value of the identifier if the identifier is in the
  65. element. Otherwise, sets a default value from the spec then
  66. returns it. If the top-level type of the identifier is named_set
  67. and the second-level type is map, it sets a set of default values
  68. under the level and then returns the default value of the
  69. identifier. Raises DataNotFoundError if the element is invalid for
  70. spec."""
  71. try:
  72. return isc.cc.data.find(element, identifier)
  73. except isc.cc.data.DataNotFoundError:
  74. pass
  75. try:
  76. isc.config.find_spec_part(spec, identifier)
  77. except isc.cc.data.DataNotFoundError:
  78. # spec or identifier is wrong
  79. raise
  80. # examine spec of the top-level item first
  81. spec_ = isc.config.find_spec_part(
  82. spec, '%s' % identifier.split('/')[0])
  83. if spec_['item_type'] == 'named_set' and \
  84. spec_['named_set_item_spec']['item_type'] == 'map':
  85. map_spec = spec_['named_set_item_spec']['map_item_spec']
  86. for name in isc.config.spec_name_list(map_spec):
  87. spec_ = isc.config.find_spec_part(map_spec, name)
  88. id_str = '%s/%s/%s' % \
  89. tuple(identifier.split('/')[0:2] + [name])
  90. isc.cc.data.set(element, id_str, spec_['item_default'])
  91. else:
  92. spec_ = isc.config.find_spec_part(spec, identifier)
  93. isc.cc.data.set(element, identifier, spec_['item_default'])
  94. return isc.cc.data.find(element, identifier)
  95. def _set_counter(element, spec, identifier, value):
  96. """Invokes _add_counter() for checking whether the identifier is
  97. in the element. If not, it creates a new identifier in the element
  98. and set the default value from the spec. After that, it sets the
  99. value specified in the arguments."""
  100. _add_counter(element, spec, identifier)
  101. isc.cc.data.set(element, identifier, value)
  102. def _get_counter(element, identifier):
  103. """Returns the value of the identifier in the element"""
  104. return isc.cc.data.find(element, identifier)
  105. def _inc_counter(element, spec, identifier, step=1):
  106. """Increments the value of the identifier in the element to the
  107. step from the current value. If the identifier isn't in the
  108. element, it creates a new identifier in the element."""
  109. isc.cc.data.set(element, identifier,
  110. _add_counter(element, spec, identifier) + step)
  111. def _start_timer():
  112. """Returns the current datetime as a datetime object."""
  113. return datetime.now()
  114. def _stop_timer(start_time, element, spec, identifier):
  115. """Sets duration time in seconds as a value of the identifier in
  116. the element, which is in seconds between start_time and the
  117. current time and is float-type."""
  118. delta = datetime.now() - start_time
  119. sec = round(delta.days * 86400 + delta.seconds + \
  120. delta.microseconds * 1E-6, 6)
  121. _set_counter(element, spec, identifier, sec)
  122. class Counter():
  123. """A counter class"""
  124. # container of a counter object
  125. _COUNTER = None
  126. @classmethod
  127. def init(cls, spec_file_name):
  128. """A creator method for a counter class. It creates a counter
  129. object by the module name of the given spec file. An argument is a
  130. specification file name."""
  131. if isinstance(cls._COUNTER, _Counter):
  132. # already loaded
  133. return cls._COUNTER
  134. # create an instance once
  135. cls._COUNTER = _Counter(spec_file_name)
  136. # set methods in Counter
  137. for (k, v) in cls._COUNTER._to_global.items():
  138. setattr(Counter, k, v)
  139. return cls._COUNTER
  140. # These method are dummies for isc.notify.notify_out.
  141. @staticmethod
  142. def inc_notifyoutv4(arg):
  143. """An empty method to be disclosed"""
  144. pass
  145. @staticmethod
  146. def inc_notifyoutv6(arg):
  147. """An empty method to be disclosed"""
  148. pass
  149. class _Counter():
  150. """A module for holding all statistics counters of modules. The
  151. counter numbers can be accessed by the accesseers defined
  152. according to a spec file. In this class, the structure of per-zone
  153. counters is assumed to be like this:
  154. zones/example.com./notifyoutv4
  155. zones/example.com./notifyoutv6
  156. zones/example.com./xfrrej
  157. zones/example.com./xfrreqdone
  158. zones/example.com./soaoutv4
  159. zones/example.com./soaoutv6
  160. zones/example.com./axfrreqv4
  161. zones/example.com./axfrreqv6
  162. zones/example.com./ixfrreqv4
  163. zones/example.com./ixfrreqv6
  164. zones/example.com./xfrsuccess
  165. zones/example.com./xfrfail
  166. zones/example.com./time_to_ixfr
  167. zones/example.com./time_to_axfr
  168. ixfr_running
  169. axfr_running
  170. socket/unixdomain/open
  171. socket/unixdomain/openfail
  172. socket/unixdomain/close
  173. socket/unixdomain/bindfail
  174. socket/unixdomain/acceptfail
  175. socket/unixdomain/accept
  176. socket/unixdomain/senderr
  177. socket/unixdomain/recverr
  178. socket/ipv4/tcp/open
  179. socket/ipv4/tcp/openfail
  180. socket/ipv4/tcp/close
  181. socket/ipv4/tcp/connfail
  182. socket/ipv4/tcp/conn
  183. socket/ipv4/tcp/senderr
  184. socket/ipv4/tcp/recverr
  185. socket/ipv6/tcp/open
  186. socket/ipv6/tcp/openfail
  187. socket/ipv6/tcp/close
  188. socket/ipv6/tcp/connfail
  189. socket/ipv6/tcp/conn
  190. socket/ipv6/tcp/senderr
  191. socket/ipv6/tcp/recverr
  192. """
  193. # '_SERVER_' is a special zone name representing an entire
  194. # count. It doesn't mean a specific zone, but it means an
  195. # entire count in the server.
  196. _entire_server = '_SERVER_'
  197. # zone names are contained under this dirname in the spec file.
  198. _perzone_prefix = 'zones'
  199. def __init__(self, spec_file_name):
  200. # for exporting to the global scope
  201. self._to_global = {}
  202. self._statistics_spec = {}
  203. self._statistics_data = {}
  204. self._zones_item_list = []
  205. self._xfrrunning_names = []
  206. self._unixsocket_names = []
  207. self._start_time = {}
  208. self._disabled = False
  209. self._rlock = threading.RLock()
  210. self._module_spec = \
  211. isc.config.module_spec_from_file(spec_file_name)
  212. self._statistics_spec = \
  213. self._module_spec.get_statistics_spec()
  214. self._parse_stats_spec()
  215. self._create_perzone_functors()
  216. self._create_perzone_timer_functors()
  217. self._create_xfrrunning_functors()
  218. self._create_unixsocket_functors()
  219. self._create_ipsocket_functors()
  220. self._to_global['clear_counters'] = self.clear_counters
  221. self._to_global['disable'] = self.disable
  222. self._to_global['enable'] = self.enable
  223. self._to_global['dump_statistics'] = self.dump_statistics
  224. def _parse_stats_spec(self):
  225. """Gets each list of names on statistics spec"""
  226. if self._perzone_prefix in \
  227. isc.config.spec_name_list(self._statistics_spec):
  228. self._zones_item_list = isc.config.spec_name_list(
  229. isc.config.find_spec_part(
  230. self._statistics_spec, self._perzone_prefix)\
  231. ['named_set_item_spec']['map_item_spec'])
  232. self._xfrrunning_names = [
  233. n for n in isc.config.spec_name_list\
  234. (self._statistics_spec) \
  235. if n.find('xfr_running') == 1 \
  236. or n.find('xfr_deferred') == 1 \
  237. or n.find('soa_in_progress') == 0 ]
  238. self._unixsocket_names = [ \
  239. n.split('/')[-1] for n in \
  240. isc.config.spec_name_list(
  241. self._statistics_spec, "", True) \
  242. if n.find('socket/unixdomain/') == 0 ]
  243. self._ipsocket_names = [ \
  244. (n.split('/')[-3], n.split('/')[-1]) for n in \
  245. isc.config.spec_name_list(
  246. self._statistics_spec, "", True) \
  247. if n.find('socket/ipv4/tcp/') == 0 \
  248. or n.find('socket/ipv6/tcp/') == 0 ]
  249. def clear_counters(self):
  250. """clears all statistics data"""
  251. with self._rlock:
  252. self._statistics_data = {}
  253. def disable(self):
  254. """disables incrementing/decrementing counters"""
  255. self._disabled = True
  256. def enable(self):
  257. """enables incrementing/decrementing counters"""
  258. self._disabled = False
  259. def _incrementer(self, identifier, step=1):
  260. """A per-zone incrementer for counter_name. Locks the
  261. thread because it is considered to be invoked by a
  262. multi-threading caller."""
  263. if self._disabled: return
  264. with self._rlock:
  265. _inc_counter(self._statistics_data,
  266. self._statistics_spec,
  267. identifier, step)
  268. def _decrementer(self, identifier, step=-1):
  269. """A decrementer for axfr or ixfr running. Locks the
  270. thread because it is considered to be invoked by a
  271. multi-threading caller."""
  272. self._incrementer(identifier, step)
  273. def _getter(self, identifier):
  274. """A getter method for perzone counters"""
  275. return _get_counter(self._statistics_data, identifier)
  276. def _starttimer(self, identifier):
  277. """Sets the value returned from _start_timer() as a value of
  278. the identifier in the self._start_time which is dict-type"""
  279. isc.cc.data.set(self._start_time, identifier, _start_timer())
  280. def _stoptimer(self, identifier):
  281. """Sets duration time between corresponding time in
  282. self._start_time and current time into the value of the
  283. identifier. It deletes corresponding time in self._start_time
  284. after setting is successfully done. If DataNotFoundError is
  285. raised while invoking _stop_timer(), it stops setting and
  286. ignores the exception."""
  287. try:
  288. _stop_timer(
  289. isc.cc.data.find(self._start_time, identifier),
  290. self._statistics_data,
  291. self._statistics_spec,
  292. identifier)
  293. del isc.cc.data.find(
  294. self._start_time,
  295. '/'.join(identifier.split('/')[0:-1]))\
  296. [identifier.split('/')[-1]]
  297. except isc.cc.data.DataNotFoundError:
  298. # do not set end time if it's not started
  299. pass
  300. def _create_perzone_functors(self):
  301. """Creates increment method of each per-zone counter based on
  302. the spec file. Incrementer can be accessed by name
  303. "inc_${item_name}".Incrementers are passed to the
  304. XfrinConnection class as counter handlers."""
  305. for item in self._zones_item_list:
  306. if item.find('time_to_') == 0: continue
  307. def __incrementer(zone_name, counter_name=item, step=1):
  308. """A per-zone incrementer for counter_name."""
  309. self._incrementer(
  310. '%s/%s/%s' % \
  311. (self._perzone_prefix, zone_name, counter_name),
  312. step)
  313. def __getter(zone_name, counter_name=item):
  314. """A getter method for perzone counters"""
  315. return self._getter(
  316. '%s/%s/%s' % \
  317. (self._perzone_prefix, zone_name, counter_name))
  318. self._to_global['inc_%s' % item] = __incrementer
  319. self._to_global['get_%s' % item] = __getter
  320. def _create_perzone_timer_functors(self):
  321. """Creates timer method of each per-zone counter based on the
  322. spec file. Starter of the timer can be accessed by the name
  323. "start_${item_name}". Stopper of the timer can be accessed by
  324. the name "stop_${item_name}". These starter and stopper are
  325. passed to the XfrinConnection class as timer handlers."""
  326. for item in self._zones_item_list:
  327. if item.find('time_to_') == -1: continue
  328. def __getter(zone_name, counter_name=item):
  329. """A getter method for perzone timer. A zone name in
  330. string is required in argument."""
  331. return self._getter(
  332. '%s/%s/%s' % \
  333. (self._perzone_prefix, zone_name, counter_name))
  334. def __starttimer(zone_name, counter_name=item):
  335. """A starter method for perzone timer. A zone name in
  336. string is required in argument."""
  337. self._starttimer(
  338. '%s/%s/%s' % \
  339. (self._perzone_prefix, zone_name, counter_name))
  340. def __stoptimer(zone_name, counter_name=item):
  341. """A stopper method for perzone timer. A zone name in
  342. string is required in argument."""
  343. self._stoptimer(
  344. '%s/%s/%s' % \
  345. (self._perzone_prefix, zone_name, counter_name))
  346. self._to_global['start_%s' % item] = __starttimer
  347. self._to_global['stop_%s' % item] = __stoptimer
  348. self._to_global['get_%s' % item] = __getter
  349. def _create_xfrrunning_functors(self):
  350. """Creates increment/decrement method of (a|i)xfr_running
  351. based on the spec file. Incrementer can be accessed by name
  352. "inc_${item_name}". Decrementer can be accessed by name
  353. "dec_${item_name}". Both of them are passed to the
  354. XfroutSession as counter handlers."""
  355. for item in self._xfrrunning_names:
  356. def __incrementer(counter_name=item, step=1):
  357. """A incrementer for axfr or ixfr running."""
  358. self._incrementer(counter_name, step)
  359. def __decrementer(counter_name=item, step=-1):
  360. """A decrementer for axfr or ixfr running."""
  361. self._decrementer(counter_name, step)
  362. def __getter(counter_name=item):
  363. """A getter method for xfr_running counters"""
  364. return self._getter(counter_name)
  365. self._to_global['inc_%s' % item] = __incrementer
  366. self._to_global['dec_%s' % item] = __decrementer
  367. self._to_global['get_%s' % item] = __getter
  368. def dump_statistics(self):
  369. """Calculates an entire server counts, and returns statistics
  370. data format to send out the stats module including each
  371. counter. If there is no counts, then it returns an empty
  372. dictionary."""
  373. # entire copy
  374. statistics_data = self._statistics_data.copy()
  375. # If self.statistics_data contains nothing of zone name, it
  376. # returns an empty dict.
  377. if self._perzone_prefix not in statistics_data:
  378. return statistics_data
  379. zones = statistics_data[self._perzone_prefix]
  380. # Start calculation for '_SERVER_' counts
  381. zones_spec = isc.config.find_spec_part(self._statistics_spec,
  382. self._perzone_prefix)
  383. zones_attrs = zones_spec['item_default'][self._entire_server]
  384. zones_data = {}
  385. for attr in zones_attrs:
  386. id_str = '%s/%s' % (self._entire_server, attr)
  387. sum_ = 0
  388. for name in zones:
  389. if attr in zones[name]:
  390. sum_ += zones[name][attr]
  391. if sum_ > 0:
  392. _set_counter(zones_data, zones_spec,
  393. id_str, sum_)
  394. # insert entire-sever counts
  395. statistics_data[self._perzone_prefix] = dict(
  396. statistics_data[self._perzone_prefix],
  397. **zones_data)
  398. return statistics_data
  399. def _create_unixsocket_functors(self):
  400. """Creates increment method of unixsocket socket. Incrementer
  401. can be accessed by name "inc_unixsocket_${item_name}"."""
  402. for item in self._unixsocket_names:
  403. def __incrementer(counter_name=item, step=1):
  404. """A incrementer for unix socket counter"""
  405. self._incrementer(
  406. 'socket/unixdomain/%s' % counter_name,
  407. step)
  408. def __getter(counter_name=item):
  409. """A getter method for unix socket counter"""
  410. return self._getter(
  411. 'socket/unixdomain/%s' % counter_name)
  412. self._to_global['inc_unixsocket_%s' % item] = __incrementer
  413. self._to_global['get_unixsocket_%s' % item] = __getter
  414. def _create_ipsocket_functors(self):
  415. """Creates increment method of ip socket. Incrementer can be
  416. accessed by name "inc_ipv4socket_${item_name}" for ipv4 or
  417. "inc_ipv6socket_${item_name}" for ipv6."""
  418. for item in self._ipsocket_names:
  419. # item should be tuple-type
  420. def __incrementer(counter_name=item, step=1):
  421. """A incrementer for ip socket counter"""
  422. self._incrementer(
  423. 'socket/%s/tcp/%s' % counter_name,
  424. step)
  425. def __getter(counter_name=item):
  426. """A getter method for ip socket counter"""
  427. return self._getter(
  428. 'socket/%s/tcp/%s' % counter_name)
  429. self._to_global['inc_%ssocket_%s' % item] = __incrementer
  430. self._to_global['get_%ssocket_%s' % item] = __getter