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.
- checksums.yaml +4 -4
- data/README.md +265 -150
- data/lib/service_skeleton.rb +20 -186
- data/lib/service_skeleton/config.rb +47 -25
- data/lib/service_skeleton/config_class.rb +16 -0
- data/lib/service_skeleton/config_variable.rb +5 -1
- data/lib/service_skeleton/config_variable/string.rb +6 -1
- data/lib/service_skeleton/config_variable/url.rb +4 -0
- data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
- data/lib/service_skeleton/config_variables.rb +28 -23
- data/lib/service_skeleton/error.rb +3 -3
- data/lib/service_skeleton/generator.rb +165 -0
- data/lib/service_skeleton/logging_helpers.rb +3 -3
- data/lib/service_skeleton/metric_method_name.rb +9 -0
- data/lib/service_skeleton/metrics_methods.rb +26 -13
- data/lib/service_skeleton/runner.rb +46 -0
- data/lib/service_skeleton/service_name.rb +20 -0
- data/lib/service_skeleton/signal_manager.rb +202 -0
- data/lib/service_skeleton/signals_methods.rb +15 -0
- data/lib/service_skeleton/ultravisor_children.rb +19 -0
- data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
- data/service_skeleton.gemspec +8 -9
- metadata +68 -26
- data/lib/service_skeleton/background_worker.rb +0 -96
- data/lib/service_skeleton/signal_handler.rb +0 -197
data/lib/service_skeleton.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "service_skeleton/
|
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/
|
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
|
-
|
17
|
-
extend ServiceSkeleton::ConfigVariables
|
18
|
-
|
19
|
+
module ServiceSkeleton
|
19
20
|
include ServiceSkeleton::LoggingHelpers
|
21
|
+
extend ServiceSkeleton::Generator
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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(
|
34
|
-
@
|
35
|
-
@config
|
36
|
-
@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
|
-
|
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,
|
14
|
-
@
|
13
|
+
def initialize(env, service_name, variables)
|
14
|
+
@service_name = service_name
|
15
15
|
|
16
|
-
# Parsing
|
17
|
-
#
|
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
|
-
|
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
|
34
|
-
|
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(@
|
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
|
49
|
-
|
50
|
-
|
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
|
-
|
70
|
+
Thread.main[:thread_map_number] = 0
|
73
71
|
else
|
74
72
|
#:nocov:
|
75
|
-
|
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 =
|
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("#{@
|
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
|
-
|
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
|