service_skeleton 0.0.0.49.g47046b9

Sign up to get free protection for your applications and to get access to all the features.
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