service_skeleton 0.0.0.41.g9507cda → 1.0.1

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
@@ -45,9 +46,13 @@ class ServiceSkeleton
45
46
  val = new_value
46
47
  end
47
48
  end.each do |var|
48
- if var.redact!(env) && env.object_id != ENV.object_id
49
- raise ServiceSkeleton::Error::CannotSanitizeEnvironmentError,
50
- "Attempted to sanitize sensitive variable #{var.name}, but we're not operating on the process' environment"
49
+ if var.redact?(env)
50
+ if env.object_id != ENV.object_id
51
+ raise ServiceSkeleton::Error::CannotSanitizeEnvironmentError,
52
+ "Attempted to sanitize sensitive variable #{var.name}, but we're not operating on the process' environment"
53
+ else
54
+ var.redact!(env)
55
+ end
51
56
  end
52
57
  end
53
58
  end
@@ -61,30 +66,35 @@ class ServiceSkeleton
61
66
 
62
67
  @logger = Logger.new(log_file || $stderr, shift_age, shift_size)
63
68
 
64
- if self.logstash_server && !self.logstash_server.empty?
65
- loggerstash = Loggerstash.new(logstash_server: logstash_server, logger: @logger)
66
- loggerstash.metrics_registry = @svc.metrics
67
- loggerstash.attach(@logger)
68
- end
69
-
70
- thread_id_map = {}
71
69
  if Thread.main
72
- thread_id_map[Thread.main.object_id] = 0
70
+ Thread.main[:thread_map_number] = 0
73
71
  else
74
72
  #:nocov:
75
- thread_id_map[Thread.current.object_id] = 0
73
+ Thread.current[:thread_map_number] = 0
76
74
  #:nocov:
77
75
  end
78
76
 
77
+ thread_map_mutex = Mutex.new
78
+
79
79
  @logger.formatter = ->(s, t, p, m) do
80
- 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
81
91
 
82
92
  ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
83
93
  "#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
84
94
  end
85
95
 
86
96
  @logger.filters = []
87
- @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|
88
98
  if spec.index("=")
89
99
  # "Your developers were so preoccupied with whether or not they
90
100
  # could, they didn't stop to think if they should."
@@ -107,5 +117,17 @@ class ServiceSkeleton
107
117
  end
108
118
  end
109
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
110
132
  end
111
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
 
@@ -16,6 +16,10 @@ class ServiceSkeleton
16
16
  @name.to_s.gsub(/\A#{Regexp.quote(svc_name)}_/i, '').downcase
17
17
  end
18
18
 
19
+ def redact?(env)
20
+ @opts[:sensitive]
21
+ end
22
+
19
23
  def redact!(env)
20
24
  if @opts[:sensitive]
21
25
  env[@name.to_s] = "*SENSITIVE*" if env.has_key?(@name.to_s)
@@ -7,7 +7,12 @@ class ServiceSkeleton::ConfigVariable::String < ServiceSkeleton::ConfigVariable
7
7
 
8
8
  def pluck_value(env)
9
9
  maybe_default(env) do
10
- env[@name.to_s]
10
+ env[@name.to_s].tap do |s|
11
+ if @opts[:match] && s !~ @opts[:match]
12
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
13
+ "Value for #{@name} must match #{@opts[:match]}"
14
+ end
15
+ end
11
16
  end
12
17
  end
13
18
  end
@@ -3,6 +3,10 @@
3
3
  require "service_skeleton/config_variable"
4
4
 
5
5
  class ServiceSkeleton::ConfigVariable::URL < ServiceSkeleton::ConfigVariable
6
+ def redact?(env)
7
+ !!(env.has_key?(@name.to_s) && (@opts[:sensitive] || URI(env[@name.to_s] || "").password))
8
+ end
9
+
6
10
  def redact!(env)
7
11
  if env.has_key?(@name.to_s)
8
12
  super
@@ -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