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