lib/config_skeleton.rb in config_skeleton-0.4.1 vs lib/config_skeleton.rb in config_skeleton-1.0.0

- old
+ new

@@ -1,14 +1,20 @@ require 'diffy' require 'fileutils' require 'frankenstein' require 'logger' -require 'rb-inotify' require 'service_skeleton' require 'tempfile' require 'digest/md5' +begin + require 'rb-inotify' unless ENV["DISABLE_INOTIFY"] +rescue FFI::NotFoundError => e + STDERR.puts "ERROR: Unable to initialize rb-inotify. To disable, set DISABLE_INOTIFY=1" + raise +end + # Framework for creating config generation systems. # # There are many systems which require some sort of configuration file to # operate, and need that configuration to by dynamic over time. The intent # of this class is to provide a common pattern for config generators, with @@ -27,18 +33,17 @@ # (and also potentially #config_ok?, #sleep_duration, #before_regenerate_config, and #after_regenerate_config). # See the documentation for those methods for what they need to do. # # 1. Setup any file watchers you want with .watch and #watch. # -# 1. Instantiate your new class, passing in an environment hash, and then call -# #start. Something like this should do the trick: +# 1. Use the ServiceSkeleton Runner to start the service. Something like this should do the trick: # # class MyConfigGenerator < ConfigSkeleton # # Implement all the necessary methods # end # -# MyConfigGenerator.new(ENV).start if __FILE__ == $0 +# ServiceSkeleton::Runner.new(MyConfigGenerator, ENV).run if __FILE__ == $0 # # 1. Sit back and relax. # # # # Environment Variables @@ -134,11 +139,12 @@ # # To declare a file watch, just call the .watch class method, or #watch instance # method, passing one or more strings containing the full path to files or # directories to watch. # -class ConfigSkeleton < ServiceSkeleton +class ConfigSkeleton + include ServiceSkeleton # All ConfigSkeleton-related errors will be subclasses of this. class Error < StandardError; end # If you get this, someone didn't read the documentation. class NotImplementedError < Error; end @@ -153,10 +159,23 @@ def trigger_regen @io_write << "." end end + def self.inherited(klass) + klass.gauge :"#{klass.service_name}_last_generation_timestamp", docstring: "When the last config generation run was made" + klass.gauge :"#{klass.service_name}_last_change_timestamp", docstring: "When the config file was last written to" + klass.counter :"#{klass.service_name}_reload_total", docstring: "How many times we've asked the server to reload", labels: [:status] + klass.counter :"#{klass.service_name}_signals_total", docstring: "How many signals have been received (and handled)" + klass.gauge :"#{klass.service_name}_config_ok", docstring: "Whether the last config change was accepted by the server" + + klass.hook_signal("HUP") do + logger.info("SIGHUP") { "received SIGHUP, triggering config regeneration" } + @trigger_regen_w << "." + end + end + # Declare a file watch on all instances of the config generator. # # When you're looking to watch a file whose path is well-known and never-changing, you # can declare the watch in the class. # @@ -184,24 +203,12 @@ # def self.watches @watches || [] end - # Create a new config generator. - # - # @param env [Hash<String, String>] the environment in which this config - # generator runs. Typically you'll just pass `ENV` in here, but you can - # pass in any hash you like, for testing purposes. - # - def initialize(env) + def initialize(*_) super - - hook_signal(:HUP) do - logger.info("SIGHUP") { "received SIGHUP, triggering config regeneration" } - @trigger_regen_w << "." - end - initialize_config_skeleton_metrics @trigger_regen_r, @trigger_regen_w = IO.pipe @terminate_r, @terminate_w = IO.pipe end @@ -292,10 +299,11 @@ # @return [void] # # @see .watch for watching files and directories whose path never changes. # def watch(*files) + return if ENV["DISABLE_INOTIFY"] files.each do |f| if File.directory?(f) notifier.watch(f, :recursive, :create, :modify, :delete, :move) { |ev| logger.info("#{logloc} watcher") { "detected #{ev.flags.join(", ")} on #{ev.watcher.path}/#{ev.name}; regenerating config" } } else notifier.watch(f, :close_write) { |ev| logger.info("#{logloc} watcher") { "detected #{ev.flags.join(", ")} on #{ev.watcher.path}; regenerating config" } } @@ -303,26 +311,15 @@ end end private - # Register metrics in the ServiceSkeleton metrics registry - # - # @return [void] - # def initialize_config_skeleton_metrics - @config_generation = Frankenstein::Request.new("#{service_name}_generation", outgoing: false, description: "config generation", registry: metrics) - - metrics.gauge(:"#{service_name}_last_generation_timestamp", "When the last config generation run was made") - metrics.gauge(:"#{service_name}_last_change_timestamp", "When the config file was last written to") - metrics.counter(:"#{service_name}_reload_total", "How many times we've asked the server to reload") - metrics.counter(:"#{service_name}_signals_total", "How many signals have been received (and handled)") - metrics.gauge(:"#{service_name}_config_ok", "Whether the last config change was accepted by the server") - - metrics.last_generation_timestamp.set({}, 0) - metrics.last_change_timestamp.set({}, 0) - metrics.config_ok.set({}, 0) + @config_generation = Frankenstein::Request.new("#{self.class.service_name}_generation", outgoing: false, description: "config generation", registry: metrics) + metrics.last_generation_timestamp.set(0) + metrics.last_change_timestamp.set(0) + metrics.config_ok.set(0) end # Write out a config file if one doesn't exist, or do an initial regen run # to make sure everything's up-to-date. # @@ -333,11 +330,11 @@ logger.info(logloc) { "Triggering a config regen on startup to ensure config is up-to-date" } regenerate_config else logger.info(logloc) { "No existing config file #{config_file} found; writing one" } File.write(config_file, instrumented_config_data) - metrics.last_change_timestamp.set({}, Time.now.to_f) + metrics.last_change_timestamp.set(Time.now.to_f) end end # The file in which the config should be written. # @@ -421,11 +418,11 @@ # # @return [String] # def instrumented_config_data begin - @config_generation.measure { config_data.tap { metrics.last_generation_timestamp.set({}, Time.now.to_f) } } + @config_generation.measure { config_data.tap { metrics.last_generation_timestamp.set(Time.now.to_f) } } rescue => ex log_exception(ex, logloc) { "Call to config_data raised exception" } nil end end @@ -451,10 +448,13 @@ # # @return [INotify::Notifier] # def notifier @notifier ||= INotify::Notifier.new + rescue NameError + raise if !ENV["DISABLE_INOTIFY"] + @notifier ||= Struct.new(:to_io).new(IO.pipe[1]) # Stub for macOS development end # Do the hard yards of actually regenerating the config and performing the reload. # # @param force_reload [Boolean] normally, whether or not to tell the server @@ -472,11 +472,11 @@ existing_config_hash: existing_config_hash, existing_config_data: data ) logger.debug(logloc) { "force? #{force_reload.inspect}" } - tmpfile = Tempfile.new(service_name, File.dirname(config_file)) + tmpfile = Tempfile.new(self.class.service_name, File.dirname(config_file)) logger.debug(logloc) { "Tempfile is #{tmpfile.path}" } unless (new_config = instrumented_config_data).nil? File.write(tmpfile.path, new_config) tmpfile.close @@ -510,11 +510,11 @@ config_was_different: config_was_different, config_was_cycled: config_was_cycled, new_config_hash: new_config_hash ) ensure - metrics.last_change_timestamp.set({}, File.stat(config_file).mtime.to_f) + metrics.last_change_timestamp.set(File.stat(config_file).mtime.to_f) tmpfile.close rescue nil tmpfile.unlink rescue nil end # Ensure the target file's ownership and permission bits match that of the source @@ -560,32 +560,32 @@ log_exception(ex, logloc) { "Server reload failed" } if config_was_ok logger.debug(logloc) { "Restored previous config file" } File.rename(old_copy, config_file) end - metrics.reload_total.increment(status: "failure") + metrics.reload_total.increment(labels: { status: "failure" }) return end logger.debug(logloc) { "Server reloaded successfully" } if config_ok? - metrics.config_ok.set({}, 1) + metrics.config_ok.set(1) logger.debug(logloc) { "Configuration successfully updated." } - metrics.reload_total.increment(status: "success") - metrics.last_change_timestamp.set({}, Time.now.to_f) + metrics.reload_total.increment(labels: { status: "success" }) + metrics.last_change_timestamp.set(Time.now.to_f) else - metrics.config_ok.set({}, 0) + metrics.config_ok.set(0) if config_was_ok logger.warn(logloc) { "New config file failed config_ok? test; rolling back to previous known-good config" } File.rename(old_copy, config_file) reload_server - metrics.reload_total.increment(status: "bad-config") + metrics.reload_total.increment(labels: { status: "bad-config" }) else logger.warn(logloc) { "New config file failed config_ok? test; leaving new config in place because old config is broken too" } - metrics.reload_total.increment(status: "everything-is-awful") - metrics.last_change_timestamp.set({}, Time.now.to_f) + metrics.reload_total.increment(labels: { status: "everything-is-awful" }) + metrics.last_change_timestamp.set(Time.now.to_f) end end ensure File.unlink(old_copy) rescue nil end