service_skeleton 0.0.0.1.ENOTAG

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.
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