service_skeleton 0.0.0.44.g75d07d7 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "service_skeleton/config"
3
+ require_relative "service_skeleton/config_class"
4
4
  require_relative "service_skeleton/config_variables"
5
+ require_relative "service_skeleton/generator"
5
6
  require_relative "service_skeleton/logging_helpers"
6
7
  require_relative "service_skeleton/metrics_methods"
7
- require_relative "service_skeleton/signal_handler"
8
+ require_relative "service_skeleton/service_name"
9
+ require_relative "service_skeleton/signals_methods"
10
+ require_relative "service_skeleton/ultravisor_children"
8
11
 
9
12
  require "frankenstein/ruby_gc_metrics"
10
13
  require "frankenstein/ruby_vm_metrics"
@@ -13,195 +16,26 @@ require "frankenstein/server"
13
16
  require "prometheus/client/registry"
14
17
  require "sigdump"
15
18
 
16
- class ServiceSkeleton
17
- extend ServiceSkeleton::ConfigVariables
18
-
19
+ module ServiceSkeleton
19
20
  include ServiceSkeleton::LoggingHelpers
21
+ extend ServiceSkeleton::Generator
20
22
 
21
- class Terminate < Exception; end
22
-
23
- def self.config_class(klass)
24
- @config_class = klass
25
- end
26
-
27
- def self.service_name
28
- service_name_from_class(self)
23
+ def self.included(mod)
24
+ mod.extend ServiceSkeleton::ServiceName
25
+ mod.extend ServiceSkeleton::ConfigVariables
26
+ mod.extend ServiceSkeleton::ConfigClass
27
+ mod.extend ServiceSkeleton::MetricsMethods
28
+ mod.extend ServiceSkeleton::SignalsMethods
29
+ mod.extend ServiceSkeleton::UltravisorChildren
29
30
  end
30
31
 
31
32
  attr_reader :config, :metrics, :logger
32
33
 
33
- def initialize(env)
34
- @env = env
35
- @config = (self.class.instance_variable_get(:@config_class) || ServiceSkeleton::Config).new(env, self)
36
- @logger = @config.logger
37
- @op_mutex = Mutex.new
38
-
39
- initialize_metrics
40
- initialize_signals
41
- end
42
-
43
- def start
44
- @op_mutex.synchronize { @thread = Thread.current }
45
-
46
- begin
47
- logger.info(logloc) { "Starting service #{service_name}" }
48
- logger.info(logloc) { (["Environment:"] + config.env.map { |k, v| "#{k}=#{v.inspect}" }).join("\n ") }
49
-
50
- start_metrics_server
51
- start_signal_handler
52
- run
53
- rescue ServiceSkeleton::Terminate
54
- # This one is OK
55
- rescue ServiceSkeleton::Error::InheritanceContractError
56
- # We want this one to be fatal
57
- raise
58
- rescue StandardError => ex
59
- log_exception(ex)
60
- end
61
-
62
- @thread = nil
63
- end
64
-
65
- def stop(force = false)
66
- if force
67
- #:nocov:
68
- @op_mutex.synchronize do
69
- if @thread
70
- @thread.raise(ServiceSkeleton::Terminate)
71
- end
72
- end
73
- #:nocov:
74
- else
75
- shutdown
76
- end
77
-
78
- if @metrics_server
79
- @metrics_server.shutdown
80
- @metrics_server = nil
81
- end
82
-
83
- @signal_handler.stop!
84
- end
85
-
86
- def service_name
87
- self.class.service_name
88
- end
89
-
90
- def registered_variables
91
- self.class.registered_variables
92
- end
93
-
94
- def hook_signal(spec, &blk)
95
- @signal_handler.hook_signal(spec, &blk)
96
- end
97
-
98
- private
99
-
100
- def self.service_name_from_class(klass)
101
- klass.to_s
102
- .gsub("::", "_")
103
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
104
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
105
- .downcase
106
- .gsub(/[^a-zA-Z0-9_]/, "_")
107
- end
108
-
109
- def run
110
- raise ServiceSkeleton::Error::InheritanceContractError, "ServiceSkeleton#run method not overridden"
111
- end
112
-
113
- def shutdown
114
- #:nocov:
115
- @op_mutex.synchronize do
116
- if @thread
117
- @thread.raise(ServiceSkeleton::Terminate)
118
- @thread.join
119
- @thread = nil
120
- end
121
- end
122
- #:nocov:
123
- end
124
-
125
- def initialize_metrics
126
- @metrics = Prometheus::Client::Registry.new
127
-
128
- Frankenstein::RubyGCMetrics.register(@metrics)
129
- Frankenstein::RubyVMMetrics.register(@metrics)
130
- Frankenstein::ProcessMetrics.register(@metrics)
131
-
132
- @metrics.singleton_class.prepend(ServiceSkeleton::MetricsMethods)
133
- @metrics.service = self
134
- end
135
-
136
- def start_metrics_server
137
- if config.metrics_port
138
- logger.info(logloc) { "Starting metrics server on port #{config.metrics_port}" }
139
-
140
- @metrics_server = Frankenstein::Server.new(
141
- port: config.metrics_port,
142
- logger: logger,
143
- metrics_prefix: :metrics_server,
144
- registry: @metrics,
145
- )
146
- @metrics_server.run
147
- end
148
- end
149
-
150
- def initialize_signals
151
- metrics.counter(:"#{self.service_name}_signals_handled_total", "How many of each type of signal have been handled")
152
- @signal_handler = ServiceSkeleton::SignalHandler.new(logger: logger, service: self, signal_counter: metrics.signals_handled_total)
153
-
154
- @signal_handler.hook_signal("USR1") do
155
- logger.level -= 1 unless logger.level == Logger::DEBUG
156
- logger.info($0) { "Received SIGUSR1; log level is now #{Logger::SEV_LABEL[logger.level]}." }
157
- end
158
-
159
- @signal_handler.hook_signal("USR2") do
160
- logger.level += 1 unless logger.level == Logger::ERROR
161
- logger.info($0) { "Received SIGUSR2; log level is now #{Logger::SEV_LABEL[logger.level]}." }
162
- end
163
-
164
- @signal_handler.hook_signal("HUP") do
165
- logger.reopen
166
- logger.info($0) { "Received SIGHUP; log file handle reopened" }
167
- end
168
-
169
- @signal_handler.hook_signal("QUIT") do
170
- Sigdump.dump("+")
171
- end
172
-
173
- @signal_handler.hook_signal("INT") do
174
- self.stop(!!@terminating)
175
- @terminating = true
176
- end
177
-
178
- @signal_handler.hook_signal("TERM") do
179
- self.stop(!!@terminating)
180
- @terminating = true
181
- end
182
- end
183
-
184
- def start_signal_handler
185
- @signal_handler.start!
186
- end
187
-
188
- @registered_variables = [
189
- { name: :SERVICE_SKELETON_LOG_LEVEL, class: ConfigVariable::String, opts: { default: "INFO" } },
190
- { name: :SERVICE_SKELETON_LOG_ENABLE_TIMESTAMPS, class: ConfigVariable::Boolean, opts: { default: false } },
191
- { name: :SERVICE_SKELETON_LOG_FILE, class: ConfigVariable::String, opts: { default: nil } },
192
- { name: :SERVICE_SKELETON_LOG_MAX_FILE_SIZE, class: ConfigVariable::Integer, opts: { default: 1048576, range: 0..Float::INFINITY } },
193
- { name: :SERVICE_SKELETON_LOG_MAX_FILES, class: ConfigVariable::Integer, opts: { default: 3, range: 0..Float::INFINITY } },
194
- { name: :SERVICE_SKELETON_LOGSTASH_SERVER, class: ConfigVariable::String, opts: { default: nil } },
195
- { name: :SERVICE_SKELETON_METRICS_PORT, class: ConfigVariable::Integer, opts: { default: nil, range: 1..65535 } },
196
- ]
197
-
198
- def self.inherited(subclass)
199
- subclass.string(:"#{subclass.service_name.upcase}_LOG_LEVEL", default: "INFO")
200
- subclass.boolean(:"#{subclass.service_name.upcase}_LOG_ENABLE_TIMESTAMPS", default: false)
201
- subclass.string(:"#{subclass.service_name.upcase}_LOG_FILE", default: nil)
202
- subclass.integer(:"#{subclass.service_name.upcase}_LOG_MAX_FILE_SIZE", default: 1048576, range: 0..Float::INFINITY)
203
- subclass.integer(:"#{subclass.service_name.upcase}_LOG_MAX_FILES", default: 3, range: 1..Float::INFINITY)
204
- subclass.string(:"#{subclass.service_name.upcase}_LOGSTASH_SERVER", default: "")
205
- subclass.integer(:"#{subclass.service_name.upcase}_METRICS_PORT", default: nil, range: 1..65535)
34
+ def initialize(*_, metrics:, config:)
35
+ @metrics = metrics
36
+ @config = config
37
+ @logger = @config.logger
206
38
  end
207
39
  end
40
+
41
+ require_relative "service_skeleton/runner"
@@ -6,18 +6,19 @@ require_relative "./filtering_logger"
6
6
 
7
7
  require "loggerstash"
8
8
 
9
- class ServiceSkeleton
9
+ module ServiceSkeleton
10
10
  class Config
11
- attr_reader :logger, :env
11
+ attr_reader :logger, :pre_run_logger, :env, :service_name
12
12
 
13
- def initialize(env, svc)
14
- @svc = svc
13
+ def initialize(env, service_name, variables)
14
+ @service_name = service_name
15
15
 
16
- # Parsing registered variables will redact the environment, so we want
17
- # to take a private unredacted copy before that happens
16
+ # Parsing variables will redact the environment, so we want to take a
17
+ # private unredacted copy before that happens for #[] lookup in the
18
+ # future.
18
19
  @env = env.to_hash.dup.freeze
19
20
 
20
- parse_registered_variables(env)
21
+ parse_variables(internal_variables + variables, env)
21
22
 
22
23
  # Sadly, we can't setup the logger until we know *how* to setup the
23
24
  # logger, which requires parsing config variables
@@ -25,17 +26,17 @@ class ServiceSkeleton
25
26
  end
26
27
 
27
28
  def [](k)
28
- @env[k]
29
+ @env[k].dup
29
30
  end
30
31
 
31
32
  private
32
33
 
33
- def parse_registered_variables(env)
34
- (@svc.registered_variables || []).map do |var|
34
+ def parse_variables(variables, env)
35
+ variables.map do |var|
35
36
  var[:class].new(var[:name], env, **var[:opts])
36
37
  end.each do |var|
37
38
  val = var.value
38
- method_name = var.method_name(@svc.service_name).to_sym
39
+ method_name = var.method_name(@service_name).to_sym
39
40
 
40
41
  define_singleton_method(method_name) do
41
42
  val
@@ -65,30 +66,42 @@ class ServiceSkeleton
65
66
 
66
67
  @logger = Logger.new(log_file || $stderr, shift_age, shift_size)
67
68
 
68
- if self.logstash_server && !self.logstash_server.empty?
69
- loggerstash = Loggerstash.new(logstash_server: logstash_server, logger: @logger)
70
- loggerstash.metrics_registry = @svc.metrics
71
- loggerstash.attach(@logger)
72
- end
69
+ # Can be used prior to a call to ultravisor#run. This prevents a race condition
70
+ # when a logstash server is configured but the logstash writer is not yet
71
+ # initialised. This should never be updated after it is configured.
72
+ @pre_run_logger = Logger.new(log_file || $stderr, shift_age, shift_size)
73
73
 
74
- thread_id_map = {}
75
74
  if Thread.main
76
- thread_id_map[Thread.main.object_id] = 0
75
+ Thread.main[:thread_map_number] = 0
77
76
  else
78
77
  #:nocov:
79
- thread_id_map[Thread.current.object_id] = 0
78
+ Thread.current[:thread_map_number] = 0
80
79
  #:nocov:
81
80
  end
82
81
 
83
- @logger.formatter = ->(s, t, p, m) do
84
- th_n = thread_id_map[Thread.current.object_id] || (thread_id_map[Thread.current.object_id] = thread_id_map.length)
82
+ thread_map_mutex = Mutex.new
83
+
84
+ [@logger, @pre_run_logger].each do |logger|
85
+ logger.formatter = ->(s, t, p, m) do
86
+ th_n = if Thread.current.name
87
+ #:nocov:
88
+ Thread.current.name
89
+ #:nocov:
90
+ else
91
+ thread_map_mutex.synchronize do
92
+ Thread.current[:thread_map_number] ||= begin
93
+ Thread.list.select { |th| th[:thread_map_number] }.length
94
+ end
95
+ end
96
+ end
85
97
 
86
- ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
87
- "#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
98
+ ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
99
+ "#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
100
+ end
88
101
  end
89
102
 
90
103
  @logger.filters = []
91
- @env.fetch("#{@svc.service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
104
+ @env.fetch("#{@service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
92
105
  if spec.index("=")
93
106
  # "Your developers were so preoccupied with whether or not they
94
107
  # could, they didn't stop to think if they should."
@@ -111,5 +124,17 @@ class ServiceSkeleton
111
124
  end
112
125
  end
113
126
  end
127
+
128
+ def internal_variables
129
+ [
130
+ { name: "#{@service_name.upcase}_LOG_LEVEL", class: ConfigVariable::String, opts: { default: "INFO" } },
131
+ { name: "#{@service_name.upcase}_LOG_ENABLE_TIMESTAMPS", class: ConfigVariable::Boolean, opts: { default: false } },
132
+ { name: "#{@service_name.upcase}_LOG_FILE", class: ConfigVariable::String, opts: { default: nil } },
133
+ { name: "#{@service_name.upcase}_LOG_MAX_FILE_SIZE", class: ConfigVariable::Integer, opts: { default: 1048576, range: 0..Float::INFINITY } },
134
+ { name: "#{@service_name.upcase}_LOG_MAX_FILES", class: ConfigVariable::Integer, opts: { default: 3, range: 1..Float::INFINITY } },
135
+ { name: "#{@service_name.upcase}_LOGSTASH_SERVER", class: ConfigVariable::String, opts: { default: "" } },
136
+ { name: "#{@service_name.upcase}_METRICS_PORT", class: ConfigVariable::Integer, opts: { default: nil, range: 1..65535 } },
137
+ ]
138
+ end
114
139
  end
115
140
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceSkeleton
4
+ module ConfigClass
5
+ Undefined = Module.new
6
+ private_constant :Undefined
7
+
8
+ def config_class(klass = Undefined)
9
+ unless klass == Undefined
10
+ @config_class = klass
11
+ end
12
+
13
+ @config_class
14
+ end
15
+ end
16
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ServiceSkeleton
3
+ module ServiceSkeleton
4
4
  class ConfigVariable
5
5
  attr_reader :name, :value
6
6
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ require "service_skeleton/config_variable"
6
+
7
+ class ServiceSkeleton::ConfigVariable::YamlFile < ServiceSkeleton::ConfigVariable
8
+ def redact!(env)
9
+ if env.has_key?(@name.to_s)
10
+ if File.world_readable?(env[@name.to_s])
11
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
12
+ "Sensitive file #{env[@name.to_s]} is world-readable!"
13
+ end
14
+
15
+ super
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def pluck_value(env)
22
+ maybe_default(env) do
23
+ begin
24
+ val = YAML.safe_load(File.read(env[@name.to_s]))
25
+ if @opts[:klass]
26
+ val = @opts[:klass].new(val)
27
+ end
28
+
29
+ val
30
+ rescue Errno::ENOENT
31
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
32
+ "YAML file #{env[@name.to_s]} does not exist"
33
+ rescue Errno::EPERM
34
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
35
+ "Do not have permission to read YAML file #{env[@name.to_s]}"
36
+ rescue Psych::SyntaxError => ex
37
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
38
+ "Invalid YAML syntax: #{ex.message}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,71 +4,76 @@ require_relative "./error"
4
4
 
5
5
  require "service_skeleton/config_variable"
6
6
 
7
- class ServiceSkeleton
7
+ module ServiceSkeleton
8
8
  module ConfigVariables
9
- attr_reader :registered_variables
10
-
11
9
  UNDEFINED = Module.new
12
10
  private_constant :UNDEFINED
13
11
 
14
- def register_variable(name, klass, **opts)
12
+ def registered_variables
15
13
  @registered_variables ||= []
14
+ end
16
15
 
16
+ def register_variable(name, klass, **opts)
17
17
  if opts[:default] == UNDEFINED
18
18
  opts.delete(:default)
19
19
  end
20
20
 
21
- @registered_variables << {
21
+ registered_variables << {
22
22
  name: name,
23
23
  class: klass,
24
24
  opts: opts,
25
25
  }
26
26
  end
27
27
 
28
- def string(var_name, default: UNDEFINED, sensitive: false, match: nil)
29
- register_variable(var_name, ConfigVariable::String, default: default, sensitive: sensitive, match: match)
30
- end
31
-
32
28
  def boolean(var_name, default: UNDEFINED, sensitive: false)
33
29
  register_variable(var_name, ConfigVariable::Boolean, default: default, sensitive: sensitive)
34
30
  end
35
31
 
36
- def integer(var_name, default: UNDEFINED, sensitive: false, range: -Float::INFINITY..Float::INFINITY)
37
- register_variable(var_name, ConfigVariable::Integer, default: default, sensitive: sensitive, range: range)
32
+ def enum(var_name, values:, default: UNDEFINED, sensitive: false)
33
+ unless values.is_a?(Hash) || values.is_a?(Array)
34
+ raise ArgumentError,
35
+ "values option to enum must be a hash or array"
36
+ end
37
+
38
+ register_variable(var_name, ConfigVariable::Enum, default: default, sensitive: sensitive, values: values)
38
39
  end
39
40
 
40
41
  def float(var_name, default: UNDEFINED, sensitive: false, range: -Float::INFINITY..Float::INFINITY)
41
42
  register_variable(var_name, ConfigVariable::Float, default: default, sensitive: sensitive, range: range)
42
43
  end
43
44
 
44
- def path_list(var_name, default: UNDEFINED, sensitive: false)
45
- register_variable(var_name, ConfigVariable::PathList, default: default, sensitive: sensitive)
45
+ def integer(var_name, default: UNDEFINED, sensitive: false, range: -Float::INFINITY..Float::INFINITY)
46
+ register_variable(var_name, ConfigVariable::Integer, default: default, sensitive: sensitive, range: range)
46
47
  end
47
48
 
48
49
  def kv_list(var_name, default: UNDEFINED, sensitive: false, key_pattern: /\A#{var_name}_(.*)\z/)
49
50
  register_variable(var_name, ConfigVariable::KVList, default: default, sensitive: sensitive, key_pattern: key_pattern)
50
51
  end
51
52
 
52
- def enum(var_name, values:, default: UNDEFINED, sensitive: false)
53
- unless values.is_a?(Hash) || values.is_a?(Array)
54
- raise ArgumentError,
55
- "values option to enum must be a hash or array"
56
- end
53
+ def path_list(var_name, default: UNDEFINED, sensitive: false)
54
+ register_variable(var_name, ConfigVariable::PathList, default: default, sensitive: sensitive)
55
+ end
57
56
 
58
- register_variable(var_name, ConfigVariable::Enum, default: default, sensitive: sensitive, values: values)
57
+ def string(var_name, default: UNDEFINED, sensitive: false, match: nil)
58
+ register_variable(var_name, ConfigVariable::String, default: default, sensitive: sensitive, match: match)
59
59
  end
60
60
 
61
61
  def url(var_name, default: UNDEFINED, sensitive: false)
62
62
  register_variable(var_name, ConfigVariable::URL, default: default, sensitive: sensitive)
63
63
  end
64
+
65
+ def yaml_file(var_name, default: UNDEFINED, sensitive: false, klass: nil)
66
+ register_variable(var_name, ConfigVariable::YamlFile, default: default, sensitive: sensitive, klass: klass)
67
+ end
64
68
  end
65
69
  end
66
70
 
67
- require_relative "config_variable/string"
68
71
  require_relative "config_variable/boolean"
69
- require_relative "config_variable/integer"
72
+ require_relative "config_variable/enum"
70
73
  require_relative "config_variable/float"
71
- require_relative "config_variable/path_list"
74
+ require_relative "config_variable/integer"
72
75
  require_relative "config_variable/kv_list"
73
- require_relative "config_variable/enum"
76
+ require_relative "config_variable/path_list"
77
+ require_relative "config_variable/string"
74
78
  require_relative "config_variable/url"
79
+ require_relative "config_variable/yaml_file"