service_skeleton 0.0.0.44.g75d07d7 → 0.0.0.48.g4a40599

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, :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,35 @@ 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
73
-
74
- thread_id_map = {}
75
69
  if Thread.main
76
- thread_id_map[Thread.main.object_id] = 0
70
+ Thread.main[:thread_map_number] = 0
77
71
  else
78
72
  #:nocov:
79
- thread_id_map[Thread.current.object_id] = 0
73
+ Thread.current[:thread_map_number] = 0
80
74
  #:nocov:
81
75
  end
82
76
 
77
+ thread_map_mutex = Mutex.new
78
+
83
79
  @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)
80
+ th_n = if Thread.current.name
81
+ #:nocov:
82
+ Thread.current.name
83
+ #:nocov:
84
+ else
85
+ thread_map_mutex.synchronize do
86
+ Thread.current[:thread_map_number] ||= begin
87
+ Thread.list.select { |th| th[:thread_map_number] }.length
88
+ end
89
+ end
90
+ end
85
91
 
86
92
  ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
87
93
  "#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
88
94
  end
89
95
 
90
96
  @logger.filters = []
91
- @env.fetch("#{@svc.service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
97
+ @env.fetch("#{@service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
92
98
  if spec.index("=")
93
99
  # "Your developers were so preoccupied with whether or not they
94
100
  # could, they didn't stop to think if they should."
@@ -111,5 +117,17 @@ class ServiceSkeleton
111
117
  end
112
118
  end
113
119
  end
120
+
121
+ def internal_variables
122
+ [
123
+ { name: "#{@service_name.upcase}_LOG_LEVEL", class: ConfigVariable::String, opts: { default: "INFO" } },
124
+ { name: "#{@service_name.upcase}_LOG_ENABLE_TIMESTAMPS", class: ConfigVariable::Boolean, opts: { default: false } },
125
+ { name: "#{@service_name.upcase}_LOG_FILE", class: ConfigVariable::String, opts: { default: nil } },
126
+ { name: "#{@service_name.upcase}_LOG_MAX_FILE_SIZE", class: ConfigVariable::Integer, opts: { default: 1048576, range: 0..Float::INFINITY } },
127
+ { name: "#{@service_name.upcase}_LOG_MAX_FILES", class: ConfigVariable::Integer, opts: { default: 3, range: 1..Float::INFINITY } },
128
+ { name: "#{@service_name.upcase}_LOGSTASH_SERVER", class: ConfigVariable::String, opts: { default: "" } },
129
+ { name: "#{@service_name.upcase}_METRICS_PORT", class: ConfigVariable::Integer, opts: { default: nil, range: 1..65535 } },
130
+ ]
131
+ end
114
132
  end
115
133
  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"