service_skeleton 0.0.0.1.ENOTAG

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +7 -0
  3. data/.git-blame-ignore-revs +2 -0
  4. data/.github/workflows/ci.yml +50 -0
  5. data/.gitignore +9 -0
  6. data/.rubocop.yml +11 -0
  7. data/.yardopts +1 -0
  8. data/CODE_OF_CONDUCT.md +49 -0
  9. data/CONTRIBUTING.md +13 -0
  10. data/LICENCE +674 -0
  11. data/README.md +767 -0
  12. data/lib/service_skeleton.rb +41 -0
  13. data/lib/service_skeleton/config.rb +140 -0
  14. data/lib/service_skeleton/config_class.rb +16 -0
  15. data/lib/service_skeleton/config_variable.rb +44 -0
  16. data/lib/service_skeleton/config_variable/boolean.rb +21 -0
  17. data/lib/service_skeleton/config_variable/enum.rb +27 -0
  18. data/lib/service_skeleton/config_variable/float.rb +25 -0
  19. data/lib/service_skeleton/config_variable/integer.rb +25 -0
  20. data/lib/service_skeleton/config_variable/kv_list.rb +26 -0
  21. data/lib/service_skeleton/config_variable/path_list.rb +13 -0
  22. data/lib/service_skeleton/config_variable/string.rb +18 -0
  23. data/lib/service_skeleton/config_variable/url.rb +36 -0
  24. data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
  25. data/lib/service_skeleton/config_variables.rb +79 -0
  26. data/lib/service_skeleton/error.rb +10 -0
  27. data/lib/service_skeleton/filtering_logger.rb +38 -0
  28. data/lib/service_skeleton/generator.rb +165 -0
  29. data/lib/service_skeleton/logging_helpers.rb +28 -0
  30. data/lib/service_skeleton/metric_method_name.rb +9 -0
  31. data/lib/service_skeleton/metrics_methods.rb +37 -0
  32. data/lib/service_skeleton/runner.rb +46 -0
  33. data/lib/service_skeleton/service_name.rb +20 -0
  34. data/lib/service_skeleton/signal_manager.rb +202 -0
  35. data/lib/service_skeleton/signals_methods.rb +15 -0
  36. data/lib/service_skeleton/ultravisor_children.rb +20 -0
  37. data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
  38. data/service_skeleton.gemspec +54 -0
  39. data/ultravisor/.yardopts +1 -0
  40. data/ultravisor/Guardfile +9 -0
  41. data/ultravisor/README.md +404 -0
  42. data/ultravisor/lib/ultravisor.rb +216 -0
  43. data/ultravisor/lib/ultravisor/child.rb +481 -0
  44. data/ultravisor/lib/ultravisor/child/call.rb +21 -0
  45. data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
  46. data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
  47. data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
  48. data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
  49. data/ultravisor/lib/ultravisor/error.rb +25 -0
  50. data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
  51. data/ultravisor/spec/example_group_methods.rb +19 -0
  52. data/ultravisor/spec/example_methods.rb +8 -0
  53. data/ultravisor/spec/spec_helper.rb +52 -0
  54. data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
  55. data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
  56. data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
  57. data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
  58. data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
  59. data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
  60. data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
  61. data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
  62. data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
  63. data/ultravisor/spec/ultravisor/child/spawn_spec.rb +107 -0
  64. data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
  65. data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
  66. data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
  67. data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
  68. data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
  69. data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
  70. metadata +375 -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,140 @@
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, :pre_run_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
+ # 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
+
74
+ if Thread.main
75
+ Thread.main[:thread_map_number] = 0
76
+ else
77
+ #:nocov:
78
+ Thread.current[:thread_map_number] = 0
79
+ #:nocov:
80
+ end
81
+
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
97
+
98
+ ts = log_enable_timestamps ? "#{t.utc.strftime("%FT%T.%NZ")} " : ""
99
+ "#{ts}#{$$}##{th_n} #{s[0]} [#{p}] #{m}\n"
100
+ end
101
+ end
102
+
103
+ @logger.filters = []
104
+ @env.fetch("#{@service_name.upcase}_LOG_LEVEL", "INFO").split(/\s*,\s*/).each do |spec|
105
+ if spec.index("=")
106
+ # "Your developers were so preoccupied with whether or not they
107
+ # could, they didn't stop to think if they should."
108
+ re, sev = spec.split(/\s*=\s*(?=[^=]*\z)/)
109
+ match = re.to_regexp || re
110
+ begin
111
+ sev = Logger.const_get(sev.upcase)
112
+ rescue NameError
113
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
114
+ "Unknown logger severity #{sev.inspect} specified in #{spec.inspect}"
115
+ end
116
+ @logger.filters << [match, sev]
117
+ else
118
+ begin
119
+ @logger.level = Logger.const_get(spec.upcase)
120
+ rescue NameError
121
+ raise ServiceSkeleton::Error::InvalidEnvironmentError,
122
+ "Unknown logger severity #{spec.inspect} specified"
123
+ end
124
+ end
125
+ end
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
139
+ end
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
@@ -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