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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +7 -0
  3. data/.gitignore +9 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +11 -0
  6. data/.yardopts +1 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/CONTRIBUTING.md +13 -0
  9. data/LICENCE +674 -0
  10. data/README.md +767 -0
  11. data/lib/service_skeleton.rb +41 -0
  12. data/lib/service_skeleton/config.rb +133 -0
  13. data/lib/service_skeleton/config_class.rb +16 -0
  14. data/lib/service_skeleton/config_variable.rb +44 -0
  15. data/lib/service_skeleton/config_variable/boolean.rb +21 -0
  16. data/lib/service_skeleton/config_variable/enum.rb +27 -0
  17. data/lib/service_skeleton/config_variable/float.rb +25 -0
  18. data/lib/service_skeleton/config_variable/integer.rb +25 -0
  19. data/lib/service_skeleton/config_variable/kv_list.rb +26 -0
  20. data/lib/service_skeleton/config_variable/path_list.rb +13 -0
  21. data/lib/service_skeleton/config_variable/string.rb +18 -0
  22. data/lib/service_skeleton/config_variable/url.rb +36 -0
  23. data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
  24. data/lib/service_skeleton/config_variables.rb +79 -0
  25. data/lib/service_skeleton/error.rb +10 -0
  26. data/lib/service_skeleton/filtering_logger.rb +38 -0
  27. data/lib/service_skeleton/generator.rb +165 -0
  28. data/lib/service_skeleton/logging_helpers.rb +28 -0
  29. data/lib/service_skeleton/metric_method_name.rb +9 -0
  30. data/lib/service_skeleton/metrics_methods.rb +37 -0
  31. data/lib/service_skeleton/runner.rb +46 -0
  32. data/lib/service_skeleton/service_name.rb +20 -0
  33. data/lib/service_skeleton/signal_manager.rb +202 -0
  34. data/lib/service_skeleton/signals_methods.rb +15 -0
  35. data/lib/service_skeleton/ultravisor_children.rb +17 -0
  36. data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
  37. data/service_skeleton.gemspec +55 -0
  38. 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