service_skeleton 0.0.0.49.g47046b9
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 +7 -0
- data/.editorconfig +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +11 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +13 -0
- data/LICENCE +674 -0
- data/README.md +767 -0
- data/lib/service_skeleton.rb +41 -0
- data/lib/service_skeleton/config.rb +133 -0
- data/lib/service_skeleton/config_class.rb +16 -0
- data/lib/service_skeleton/config_variable.rb +44 -0
- data/lib/service_skeleton/config_variable/boolean.rb +21 -0
- data/lib/service_skeleton/config_variable/enum.rb +27 -0
- data/lib/service_skeleton/config_variable/float.rb +25 -0
- data/lib/service_skeleton/config_variable/integer.rb +25 -0
- data/lib/service_skeleton/config_variable/kv_list.rb +26 -0
- data/lib/service_skeleton/config_variable/path_list.rb +13 -0
- data/lib/service_skeleton/config_variable/string.rb +18 -0
- data/lib/service_skeleton/config_variable/url.rb +36 -0
- data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
- data/lib/service_skeleton/config_variables.rb +79 -0
- data/lib/service_skeleton/error.rb +10 -0
- data/lib/service_skeleton/filtering_logger.rb +38 -0
- data/lib/service_skeleton/generator.rb +165 -0
- data/lib/service_skeleton/logging_helpers.rb +28 -0
- data/lib/service_skeleton/metric_method_name.rb +9 -0
- data/lib/service_skeleton/metrics_methods.rb +37 -0
- 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 +17 -0
- data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
- data/service_skeleton.gemspec +55 -0
- metadata +356 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "service_skeleton/config_class"
|
4
|
+
require_relative "service_skeleton/config_variables"
|
5
|
+
require_relative "service_skeleton/generator"
|
6
|
+
require_relative "service_skeleton/logging_helpers"
|
7
|
+
require_relative "service_skeleton/metrics_methods"
|
8
|
+
require_relative "service_skeleton/service_name"
|
9
|
+
require_relative "service_skeleton/signals_methods"
|
10
|
+
require_relative "service_skeleton/ultravisor_children"
|
11
|
+
|
12
|
+
require "frankenstein/ruby_gc_metrics"
|
13
|
+
require "frankenstein/ruby_vm_metrics"
|
14
|
+
require "frankenstein/process_metrics"
|
15
|
+
require "frankenstein/server"
|
16
|
+
require "prometheus/client/registry"
|
17
|
+
require "sigdump"
|
18
|
+
|
19
|
+
module ServiceSkeleton
|
20
|
+
include ServiceSkeleton::LoggingHelpers
|
21
|
+
extend ServiceSkeleton::Generator
|
22
|
+
|
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
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :config, :metrics, :logger
|
33
|
+
|
34
|
+
def initialize(*_, metrics:, config:)
|
35
|
+
@metrics = metrics
|
36
|
+
@config = config
|
37
|
+
@logger = @config.logger
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require_relative "service_skeleton/runner"
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "to_regexp"
|
4
|
+
|
5
|
+
require_relative "./filtering_logger"
|
6
|
+
|
7
|
+
require "loggerstash"
|
8
|
+
|
9
|
+
module ServiceSkeleton
|
10
|
+
class Config
|
11
|
+
attr_reader :logger, :env, :service_name
|
12
|
+
|
13
|
+
def initialize(env, service_name, variables)
|
14
|
+
@service_name = service_name
|
15
|
+
|
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.
|
19
|
+
@env = env.to_hash.dup.freeze
|
20
|
+
|
21
|
+
parse_variables(internal_variables + variables, env)
|
22
|
+
|
23
|
+
# Sadly, we can't setup the logger until we know *how* to setup the
|
24
|
+
# logger, which requires parsing config variables
|
25
|
+
setup_logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](k)
|
29
|
+
@env[k].dup
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_variables(variables, env)
|
35
|
+
variables.map do |var|
|
36
|
+
var[:class].new(var[:name], env, **var[:opts])
|
37
|
+
end.each do |var|
|
38
|
+
val = var.value
|
39
|
+
method_name = var.method_name(@service_name).to_sym
|
40
|
+
|
41
|
+
define_singleton_method(method_name) do
|
42
|
+
val
|
43
|
+
end
|
44
|
+
|
45
|
+
define_singleton_method(:"#{method_name}=") do |new_value|
|
46
|
+
val = new_value
|
47
|
+
end
|
48
|
+
end.each do |var|
|
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
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_logger
|
61
|
+
shift_age, shift_size = if log_max_file_size == 0
|
62
|
+
[0, 0]
|
63
|
+
else
|
64
|
+
[log_max_files, log_max_file_size]
|
65
|
+
end
|
66
|
+
|
67
|
+
@logger = Logger.new(log_file || $stderr, shift_age, shift_size)
|
68
|
+
|
69
|
+
if Thread.main
|
70
|
+
Thread.main[:thread_map_number] = 0
|
71
|
+
else
|
72
|
+
#:nocov:
|
73
|
+
Thread.current[:thread_map_number] = 0
|
74
|
+
#:nocov:
|
75
|
+
end
|
76
|
+
|
77
|
+
thread_map_mutex = Mutex.new
|
78
|
+
|
79
|
+
@logger.formatter = ->(s, t, p, m) do
|
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
|
91
|
+
|
92
|
+
ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
|
93
|
+
"#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
|
94
|
+
end
|
95
|
+
|
96
|
+
@logger.filters = []
|
97
|
+
@env.fetch("#{@service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
|
98
|
+
if spec.index("=")
|
99
|
+
# "Your developers were so preoccupied with whether or not they
|
100
|
+
# could, they didn't stop to think if they should."
|
101
|
+
re, sev = spec.split(/\s*=\s*(?=[^=]*\z)/)
|
102
|
+
match = re.to_regexp || re
|
103
|
+
begin
|
104
|
+
sev = Logger.const_get(sev.upcase)
|
105
|
+
rescue NameError
|
106
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
107
|
+
"Unknown logger severity #{sev.inspect} specified in #{spec.inspect}"
|
108
|
+
end
|
109
|
+
@logger.filters << [match, sev]
|
110
|
+
else
|
111
|
+
begin
|
112
|
+
@logger.level = Logger.const_get(spec.upcase)
|
113
|
+
rescue NameError
|
114
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
115
|
+
"Unknown logger severity #{spec.inspect} specified"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
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
|
132
|
+
end
|
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
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ServiceSkeleton
|
4
|
+
class ConfigVariable
|
5
|
+
attr_reader :name, :value
|
6
|
+
|
7
|
+
def initialize(name, env, **opts, &blk)
|
8
|
+
@name = name
|
9
|
+
@opts = opts
|
10
|
+
@blk = blk
|
11
|
+
|
12
|
+
@value = pluck_value(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_name(svc_name)
|
16
|
+
@name.to_s.gsub(/\A#{Regexp.quote(svc_name)}_/i, '').downcase
|
17
|
+
end
|
18
|
+
|
19
|
+
def redact?(env)
|
20
|
+
@opts[:sensitive]
|
21
|
+
end
|
22
|
+
|
23
|
+
def redact!(env)
|
24
|
+
if @opts[:sensitive]
|
25
|
+
env[@name.to_s] = "*SENSITIVE*" if env.has_key?(@name.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def maybe_default(env)
|
32
|
+
if env.has_key?(@name.to_s)
|
33
|
+
yield
|
34
|
+
else
|
35
|
+
if @opts.has_key?(:default)
|
36
|
+
@opts[:default]
|
37
|
+
else
|
38
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
39
|
+
"Value for required environment variable #{@name} not specified"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::Boolean < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
10
|
+
case env[@name.to_s]
|
11
|
+
when /\A(no|n|off|0|false)\z/i
|
12
|
+
false
|
13
|
+
when /\A(yes|y|on|1|true)\z/i
|
14
|
+
true
|
15
|
+
else
|
16
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
17
|
+
"Value #{env[@name.to_s].inspect} for environment variable #{@name} is not a valid boolean value"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::Enum < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
10
|
+
v = env[@name.to_s]
|
11
|
+
|
12
|
+
if @opts[:values].is_a?(Array)
|
13
|
+
unless @opts[:values].include?(v)
|
14
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
15
|
+
"Invalid value for #{@name}; must be one of #{@opts[:values].join(", ")}"
|
16
|
+
end
|
17
|
+
v
|
18
|
+
elsif @opts[:values].is_a?(Hash)
|
19
|
+
unless @opts[:values].keys.include?(v)
|
20
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
21
|
+
"Invalid value for #{@name}; must be one of #{@opts[:values].keys.join(", ")}"
|
22
|
+
end
|
23
|
+
@opts[:values][v]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::Float < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
10
|
+
value = env[@name.to_s]
|
11
|
+
|
12
|
+
if value =~ /\A-?\d+.?\d*\z/
|
13
|
+
value.to_f.tap do |f|
|
14
|
+
unless @opts[:range].include?(f)
|
15
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
16
|
+
"Value #{f} for environment variable #{@name} is out of the valid range (must be between #{@opts[:range].first} and #{@opts[:range].last} inclusive)"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
21
|
+
"Value #{value.inspect} for environment variable #{@name} is not a valid numeric value"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::Integer < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
10
|
+
value = env[@name.to_s]
|
11
|
+
|
12
|
+
if value =~ /\A-?\d+\z/
|
13
|
+
value.to_i.tap do |i|
|
14
|
+
unless @opts[:range].include?(i)
|
15
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
16
|
+
"Value #{i} for environment variable #{@name} is out of the valid range (must be between #{@opts[:range].first} and #{@opts[:range].last} inclusive)"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
21
|
+
"Value #{value.inspect} for environment variable #{@name} is not a valid integer value"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::KVList < ServiceSkeleton::ConfigVariable
|
6
|
+
def redact!(env)
|
7
|
+
env.keys.each { |k| env[k] = "*SENSITIVE*" if k =~ @opts[:key_pattern] }
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def pluck_value(env)
|
13
|
+
matches = env.select { |k, _| k.to_s =~ @opts[:key_pattern] }
|
14
|
+
|
15
|
+
if matches.empty?
|
16
|
+
if @opts.has_key?(:default)
|
17
|
+
@opts[:default]
|
18
|
+
else
|
19
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
20
|
+
"no keys for key-value list #{@name} specified"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
matches.transform_keys { |k| @opts[:key_pattern].match(k.to_s)[1].to_sym }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::PathList < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
10
|
+
env[@name.to_s].split(":")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
5
|
+
class ServiceSkeleton::ConfigVariable::String < ServiceSkeleton::ConfigVariable
|
6
|
+
private
|
7
|
+
|
8
|
+
def pluck_value(env)
|
9
|
+
maybe_default(env) do
|
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
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "service_skeleton/config_variable"
|
4
|
+
|
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
|
+
|
10
|
+
def redact!(env)
|
11
|
+
if env.has_key?(@name.to_s)
|
12
|
+
super
|
13
|
+
uri = URI(env[@name.to_s])
|
14
|
+
if uri.password
|
15
|
+
uri.password = "*REDACTED*"
|
16
|
+
env[@name.to_s] = uri.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def pluck_value(env)
|
24
|
+
maybe_default(env) do
|
25
|
+
begin
|
26
|
+
v = env[@name.to_s]
|
27
|
+
URI(v)
|
28
|
+
rescue URI::InvalidURIError
|
29
|
+
raise ServiceSkeleton::Error::InvalidEnvironmentError,
|
30
|
+
"Value for #{@name} (#{v}) does not appear to be a valid URL"
|
31
|
+
end
|
32
|
+
|
33
|
+
v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -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
|