/statsite/bin/statsite.py

https://github.com/deactivated/statsite · Python · 153 lines · 82 code · 23 blank · 48 comment · 16 complexity · b9a626f1df8293db8dac6c6ff405cf1f MD5 · raw file

  1. #!/usr/bin/env python
  2. """statsite
  3. .. program:: statsite
  4. """
  5. import logging
  6. import logging.handlers
  7. import signal
  8. import sys
  9. import threading
  10. import ConfigParser
  11. from optparse import OptionParser
  12. from ..statsite import Statsite
  13. class StatsiteCommandError(Exception):
  14. """
  15. This is the exception that will be raised if something goes wrong executing
  16. the Statsite command.
  17. """
  18. pass
  19. class StatsiteCommand(object):
  20. TOPLEVEL_CONFIG_SECTION = "statsite"
  21. """
  22. This is the section to use in the configuration file if you wish to modify
  23. the top-level configuration.
  24. """
  25. def __init__(self, args=None):
  26. # Define and parse the command line options
  27. parser = OptionParser()
  28. parser.add_option("-c", "--config", action="append", dest="config_files",
  29. default=[], help="path to a configuration file")
  30. parser.add_option("-l", "--log-level", action="store", dest="log_level",
  31. default=None, help="log level")
  32. parser.add_option("-s", "--setting", action="append", dest="settings",
  33. default=[],
  34. help="set a setting, e.g. collector.host=0.0.0.0")
  35. (self.options, _) = parser.parse_args(args)
  36. # Defaults
  37. self.statsite = None
  38. # Parse the settings from file, and then from the command line, since
  39. # the command line trumps any file-based settings
  40. self.settings = {}
  41. if len(self.options.config_files) > 0:
  42. self._parse_settings_from_file(self.options.config_files)
  43. self._parse_settings_from_options()
  44. # Setup the logger
  45. handler = logging.StreamHandler(sys.stdout)
  46. handler.setFormatter(logging.Formatter(
  47. "%(asctime)s %(levelname)s:%(name)s:%(lineno)s %(message)s"))
  48. logger = logging.getLogger("statsite")
  49. logger.addHandler(handler)
  50. logger.setLevel(getattr(logging, self.settings["log_level"].upper()))
  51. def start(self):
  52. """
  53. Runs the statiste application.
  54. """
  55. signal.signal(signal.SIGINT, self._on_sigint)
  56. self.statsite = Statsite(self.settings)
  57. # Run Statsite in a separate thread so that signal handlers can
  58. # properly shut it down.
  59. thread = threading.Thread(target=self.statsite.start)
  60. thread.daemon = True
  61. thread.start()
  62. # Apparently `thread.join` blocks the main thread and makes it
  63. # _uninterruptable_, so we need to do this loop so that the main thread
  64. # can respond to signal handlers.
  65. while thread.isAlive():
  66. thread.join(0.2)
  67. def _on_sigint(self, signal, frame):
  68. """
  69. Called when a SIGINT is sent to cleanly shutdown the statsite server.
  70. """
  71. if self.statsite:
  72. self.statsite.shutdown()
  73. def _parse_settings_from_file(self, paths):
  74. """
  75. Parses settings from a configuration file.
  76. """
  77. config = ConfigParser.RawConfigParser()
  78. if config.read(paths) != paths:
  79. raise StatsiteCommandError, "Failed to parse configuration files."
  80. for section in config.sections():
  81. settings_section = section if section != self.TOPLEVEL_CONFIG_SECTION else None
  82. for (key, value) in config.items(section):
  83. self._add_setting(settings_section, key, value)
  84. def _parse_settings_from_options(self):
  85. """
  86. Parses settings from the command line options.
  87. """
  88. # Set the log level up
  89. self.settings.setdefault("log_level", "info")
  90. if self.options.log_level:
  91. self.settings["log_level"] = self.options.log_level
  92. # Set the generic options
  93. for setting in self.options.settings:
  94. key, value = setting.split("=", 2)
  95. section, key = key.split(".", 2)
  96. self._add_setting(section, key, value)
  97. def _add_setting(self, section, key, value):
  98. """
  99. Adds settings to a specific section.
  100. """
  101. if section is None:
  102. # If section is 'None' then we put the key/value
  103. # in the top-level settings
  104. current = self.settings
  105. else:
  106. # Otherwise we put it in the proper section...
  107. self.settings.setdefault(section, {})
  108. # Split the key by "." characters and make sure
  109. # that each character nests the dictionary further
  110. current = self.settings[section]
  111. parts = key.split(".")
  112. for part in parts[:-1]:
  113. current.setdefault(part, {})
  114. current = current[part]
  115. # The key is now the last of the dot-separated parts
  116. key = parts[-1]
  117. # Finally set the value onto the settings
  118. current[key] = value
  119. def main():
  120. "The main entrypoint for the statsite command line program."
  121. try:
  122. command = StatsiteCommand()
  123. command.start()
  124. except StatsiteCommandError, e:
  125. sys.stderr.write("Error: %s\n" % e.message)
  126. sys.exit(1)
  127. if __name__ == "__main__":
  128. main()