sc4ry 0.1.7 → 0.2.1
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 +4 -4
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +47 -0
- data/Gemfile +9 -5
- data/README.md +283 -39
- data/Rakefile +41 -3
- data/VERSION +1 -0
- data/assets/images/sc4ry_workflow.png +0 -0
- data/bin/console +4 -3
- data/lib/sc4ry/backends/init.rb +3 -1
- data/lib/sc4ry/backends/memory.rb +41 -16
- data/lib/sc4ry/backends/redis.rb +59 -61
- data/lib/sc4ry/circuits.rb +240 -59
- data/lib/sc4ry/config.rb +99 -0
- data/lib/sc4ry/constants.rb +55 -0
- data/lib/sc4ry/dependencies.rb +14 -5
- data/lib/sc4ry/exceptions.rb +41 -8
- data/lib/sc4ry/exporters/init.rb +3 -1
- data/lib/sc4ry/helpers.rb +39 -30
- data/lib/sc4ry/logger.rb +55 -18
- data/lib/sc4ry/notifiers/init.rb +47 -19
- data/lib/sc4ry/notifiers/mattermost.rb +38 -31
- data/lib/sc4ry/notifiers/prometheus.rb +21 -19
- data/lib/sc4ry/run_controller.rb +41 -32
- data/lib/sc4ry/store.rb +87 -18
- data/lib/sc4ry/version.rb +8 -1
- data/lib/sc4ry.rb +3 -9
- data/samples/test.rb +159 -0
- data/sc4ry.gemspec +20 -16
- metadata +98 -8
- data/assets/logo_sc4ry.png +0 -0
data/lib/sc4ry/exceptions.rb
CHANGED
@@ -1,11 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
1
5
|
module Sc4ry
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
# Sc4ry::Exceptions module
|
7
|
+
# @note namespace
|
8
|
+
module Exceptions
|
9
|
+
# Exception use in {Sc4ry::Circuits} when running circuit {Sc4ry::Circuits::run}
|
10
|
+
class CircuitBreaked < StandardError
|
11
|
+
def initialize(msg = 'Circuit just opened')
|
12
|
+
super(msg)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generic Exception use in {Sc4ry::Circuits}
|
17
|
+
class Sc4ryGenericError < StandardError
|
18
|
+
def initialize(msg = '')
|
19
|
+
super(msg)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Exception use in {Sc4ry::Store} or/and {Sc4ry::Backend} on data string issues
|
24
|
+
class Sc4ryBackendError < StandardError
|
25
|
+
def initialize(msg = '')
|
26
|
+
super(msg)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Exception use in {Sc4ry::Notifiers} on notification issues
|
31
|
+
class Sc4ryNotifierError < StandardError
|
32
|
+
def initialize(msg = '')
|
33
|
+
super(msg)
|
34
|
+
end
|
35
|
+
end
|
9
36
|
|
37
|
+
# Exception use in {Sc4ry::Circuits} on config management issues
|
38
|
+
class ConfigError < StandardError
|
39
|
+
def initialize(msg = '')
|
40
|
+
super(msg)
|
41
|
+
end
|
10
42
|
end
|
11
|
-
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/sc4ry/exporters/init.rb
CHANGED
data/lib/sc4ry/helpers.rb
CHANGED
@@ -1,10 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
2
5
|
module Sc4ry
|
6
|
+
# Sc4ry::Helpers module
|
7
|
+
# @note namespace
|
3
8
|
module Helpers
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
9
|
+
# class method (module) to help logging messages
|
10
|
+
# @param [Symbol] target a specific logger, restored old after
|
11
|
+
# @param [Symbol] level (default :info) a logging level (see Logger Stdlib)
|
12
|
+
# @param [String] message your message
|
13
|
+
# @return [Boolean]
|
14
|
+
def self.log(message:, target: nil, level: :info)
|
15
|
+
save = Sc4ry::Loggers.current
|
16
|
+
Sc4ry::Loggers.current = target if target
|
17
|
+
Sc4ry::Loggers.get.send level, "Sc4ry : #{message}"
|
18
|
+
Sc4ry::Loggers.current = save
|
19
|
+
true
|
8
20
|
end
|
9
21
|
|
10
22
|
# TCP/IP service checker
|
@@ -13,37 +25,34 @@ module Sc4ry
|
|
13
25
|
# @option options [String] :host hostname
|
14
26
|
# @option options [String] :port TCP port
|
15
27
|
# @option options [String] :url full URL, priority on :host and :port
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
return true
|
31
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
32
|
-
return false
|
33
|
-
end
|
34
|
-
end
|
35
|
-
rescue Timeout::Error
|
28
|
+
def self.verify_service(options = {})
|
29
|
+
if options[:url]
|
30
|
+
uri = URI.parse(options[:url])
|
31
|
+
host = uri.host
|
32
|
+
port = uri.port
|
33
|
+
else
|
34
|
+
host = options[:host]
|
35
|
+
port = options[:port]
|
36
|
+
end
|
37
|
+
Timeout.timeout(1) do
|
38
|
+
s = TCPSocket.new(host, port)
|
39
|
+
s.close
|
40
|
+
return true
|
41
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
36
42
|
return false
|
37
43
|
end
|
44
|
+
rescue Timeout::Error
|
45
|
+
false
|
38
46
|
end
|
39
47
|
|
40
|
-
|
41
|
-
|
48
|
+
# class method (module) to help send notifiesby Sc4ry::Notifiers
|
49
|
+
# @param [Hash] options a Notifying structure
|
50
|
+
# @return [Boolean]
|
51
|
+
def self.notify(options = {})
|
42
52
|
Sc4ry::Notifiers.list.each do |record|
|
43
53
|
notifier = Sc4ry::Notifiers.get name: record
|
44
|
-
notifier[:class].notify(options) if options[:config][:notifiers].include? record
|
54
|
+
notifier[:class].notify(options) if options[:config][:notifiers].include? record
|
45
55
|
end
|
46
56
|
end
|
47
|
-
|
48
|
-
|
49
|
-
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/sc4ry/logger.rb
CHANGED
@@ -1,30 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
2
5
|
module Sc4ry
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
# Sc4ry loggers Factory/provider
|
7
|
+
# @note must be accessed by [Sc4ry::Circuits.loggers]
|
8
|
+
class Loggers
|
9
|
+
@@loggers = { stdout: ::Logger.new($stdout) }
|
6
10
|
@@current = :stdout
|
7
|
-
|
8
|
-
|
9
|
-
|
11
|
+
|
12
|
+
# give the list of available loggers (initially internal Sc4ry logger )
|
13
|
+
# @return [Array] of [symbol] the list of defined loggers
|
14
|
+
# @note default :stdout => ::Logger($stdout) from Ruby Stdlib
|
15
|
+
# @example usage
|
16
|
+
# include Sc4ry
|
17
|
+
# Circuits.loggers.list_available.each {|logger| puts logger }
|
18
|
+
def self.list_available
|
19
|
+
@@loggers.keys
|
10
20
|
end
|
11
|
-
|
12
|
-
|
13
|
-
|
21
|
+
|
22
|
+
# return the current logger name (initially :stdtout )
|
23
|
+
# @return [symbol] the name of the logger
|
24
|
+
# @example usage
|
25
|
+
# include Sc4ry
|
26
|
+
# puts Circuits.loggers.current
|
27
|
+
def self.current
|
28
|
+
@@current
|
14
29
|
end
|
15
|
-
|
16
|
-
|
17
|
-
|
30
|
+
|
31
|
+
# return the current logger Object (initially internal Sc4ry Stdlib Logger on STDOUT )
|
32
|
+
# @return [symbol] the name of the logger
|
33
|
+
# @example usage
|
34
|
+
# include Sc4ry
|
35
|
+
# Circuits.loggers.get :stdout
|
36
|
+
def self.get
|
37
|
+
@@loggers[@@current]
|
18
38
|
end
|
19
|
-
|
20
|
-
|
39
|
+
|
40
|
+
# Set the current logger
|
41
|
+
# @param [Symbol] sym the name of the logger
|
42
|
+
# @return [symbol] the name of the logger updated
|
43
|
+
# @example usage
|
44
|
+
# include Sc4ry
|
45
|
+
# Circuits.loggers.current = :newlogger
|
46
|
+
def self.current=(sym)
|
21
47
|
raise "Logger not define : #{sym}" unless @@loggers.keys.include? sym
|
48
|
+
|
22
49
|
@@current = sym
|
50
|
+
@@current
|
23
51
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
52
|
+
|
53
|
+
# register un new logger
|
54
|
+
# @param [Symbol] name the name of the new logger
|
55
|
+
# @param [Object] instance the new logger object
|
56
|
+
# raise Sc4ry::Exceptions::Sc4ryGenericError if name is not a Symbol
|
57
|
+
# @example usage
|
58
|
+
# include Sc4ry
|
59
|
+
# Circuits.loggers.register name: :newlogger, instance: Logger::new('/path/to/my.log')
|
60
|
+
def self.register(name:, instance:)
|
61
|
+
raise Sc4ry::Exceptions::Sc4ryGenericError, 'name: keyword must be a Symbol' unless name.instance_of?(Symbol)
|
62
|
+
|
63
|
+
@@loggers[name] = instance
|
64
|
+
name
|
27
65
|
end
|
28
|
-
|
29
66
|
end
|
30
67
|
end
|
data/lib/sc4ry/notifiers/init.rb
CHANGED
@@ -1,30 +1,58 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
Dir["#{File.dirname(__FILE__)}/*.rb"].sort.each { |file| require file unless File.basename(file) == 'init.rb' }
|
4
|
+
|
5
|
+
# Sc4ry module
|
6
|
+
# @note namespace
|
7
|
+
module Sc4ry
|
8
|
+
# Sc4ry::Notifiers module
|
9
|
+
# @note namespace
|
4
10
|
module Notifiers
|
5
|
-
|
6
|
-
|
7
|
-
|
11
|
+
# default notifiers specifications
|
12
|
+
DEFAULT_NOTIFIERS = { prometheus: { class: Sc4ry::Notifiers::Prometheus, config: { url: 'http://localhost:9091' } },
|
13
|
+
mattermost: { class: Sc4ry::Notifiers::Mattermost, config: { url: 'http://localhost:9999', token: '<CHANGE_ME>' } } }
|
14
|
+
@@notifiers_list = DEFAULT_NOTIFIERS.dup
|
15
|
+
|
16
|
+
# class method how display a specific notifier config
|
17
|
+
# @param notifier [Symbol] a notifier name
|
18
|
+
# @return [Hash] the config
|
19
|
+
def self.display_config(notifier:)
|
20
|
+
unless @@notifiers_list.include? notifier
|
21
|
+
raise Sc4ry::Exceptions::Sc4ryNotifierError,
|
22
|
+
"Notifier #{notifier} not found"
|
23
|
+
end
|
8
24
|
|
9
|
-
|
10
|
-
return @@notifiers_list.keys
|
25
|
+
@@notifiers_list[notifier][:config]
|
11
26
|
end
|
12
27
|
|
13
|
-
|
14
|
-
|
28
|
+
# class method how return the list of known notifiers
|
29
|
+
# @return [Array] a list of [Symbol] notifiers name
|
30
|
+
def self.list
|
31
|
+
@@notifiers_list.keys
|
15
32
|
end
|
16
33
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
34
|
+
# class method how return a specific notifier by name
|
35
|
+
# @param name [Symbol] a notifier name
|
36
|
+
# @return [Hash] the notifier structure
|
37
|
+
def self.get(name:)
|
38
|
+
@@notifiers_list[name]
|
21
39
|
end
|
22
40
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
41
|
+
# class method how register a specific notifier
|
42
|
+
# @param name [Symbol] a notifier name
|
43
|
+
# @param definition [Hash] a notifier definition
|
44
|
+
# @return [Hash] the notifier structure
|
45
|
+
def self.register(name:, definition:)
|
46
|
+
@@notifiers_list[name] = definition
|
27
47
|
end
|
28
|
-
end
|
29
|
-
end
|
30
48
|
|
49
|
+
# class method how configure a specific notifier
|
50
|
+
# @param name [Symbol] a notifier name
|
51
|
+
# @param config [Hash] a notifier config
|
52
|
+
# @return [Hash] the notifier structure
|
53
|
+
def self.config(name:, config:)
|
54
|
+
@@notifiers_list[name][:config] = config
|
55
|
+
config
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,35 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
1
5
|
module Sc4ry
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
else
|
30
|
-
Sc4ry::Helpers.log level: :warn, message: "Mattermost Notifier : can't notify Mattermost not reachable."
|
31
|
-
end
|
6
|
+
# Sc4ry::Notifiers module
|
7
|
+
# @note namespace
|
8
|
+
module Notifiers
|
9
|
+
# Mattermost Notifier class
|
10
|
+
class Mattermost
|
11
|
+
# send metrics to Prometheus PushGateway
|
12
|
+
# @return [Bool]
|
13
|
+
def self.notify(options = {})
|
14
|
+
config = Sc4ry::Notifiers.get(name: :mattermost)[:config]
|
15
|
+
status = options[:config][:status][:general]
|
16
|
+
circuit = options[:circuit]
|
17
|
+
begin
|
18
|
+
uri = URI.parse("#{config[:url]}/hooks/#{config[:token]}")
|
19
|
+
message = "notifying for circuit #{circuit}, state : #{status}."
|
20
|
+
if Sc4ry::Helpers.verify_service url: config[:url]
|
21
|
+
request = ::Net::HTTP::Post.new(uri)
|
22
|
+
request.content_type = 'application/json'
|
23
|
+
req_options = {
|
24
|
+
use_ssl: uri.scheme == 'https'
|
25
|
+
}
|
26
|
+
payload = { 'text' => "message : #{message} from #{Socket.gethostname}", 'username' => 'Sc4ry' }
|
27
|
+
Sc4ry::Helpers.log level: :debug, message: "Mattermost Notifying : #{message}"
|
28
|
+
request.body = ::JSON.dump(payload)
|
29
|
+
::Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
30
|
+
http.request(request)
|
32
31
|
end
|
32
|
+
|
33
|
+
else
|
34
|
+
Sc4ry::Helpers.log level: :warn, message: "Mattermost Notifier : can't notify Mattermost not reachable."
|
35
|
+
end
|
36
|
+
rescue URI::InvalidURIError
|
37
|
+
Sc4ry::Helpers.log level: :warn, message: 'Mattermost Notifier : URL malformed'
|
33
38
|
end
|
39
|
+
end
|
34
40
|
end
|
35
|
-
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,37 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
1
5
|
module Sc4ry
|
6
|
+
# Sc4ry::Notifiers module
|
7
|
+
# @note namespace
|
2
8
|
module Notifiers
|
3
|
-
|
4
|
-
|
9
|
+
# Prometheus notifier class
|
5
10
|
class Prometheus
|
6
|
-
|
7
|
-
@@
|
8
|
-
|
11
|
+
@@registry = ::Prometheus::Client::Registry.new
|
12
|
+
@@metric_circuit_state = ::Prometheus::Client::Gauge.new(:cuircuit_state,
|
13
|
+
docstring: 'Sc4ry metric : state of a circuit',
|
14
|
+
labels: [:circuit])
|
9
15
|
|
10
16
|
@@registry.register(@@metric_circuit_state)
|
11
17
|
|
12
|
-
|
13
|
-
|
14
18
|
# send metrics to Prometheus PushGateway
|
15
19
|
# @return [Bool]
|
16
|
-
def
|
17
|
-
@config = Sc4ry::Notifiers.get(
|
20
|
+
def self.notify(options = {})
|
21
|
+
@config = Sc4ry::Notifiers.get(name: :prometheus)[:config]
|
18
22
|
status = options[:config][:status][:general]
|
19
23
|
circuit = options[:circuit]
|
20
|
-
status_map = {:
|
21
|
-
if Sc4ry::Helpers
|
22
|
-
|
23
|
-
@@metric_circuit_state.set(status_map[status], labels: {circuit: circuit.to_s })
|
24
|
-
Sc4ry::Helpers.log level: :debug, message: "Prometheus Notifier : notifying for circuit #{circuit.to_s}, state : #{status.to_s}."
|
24
|
+
status_map = { open: 0, half_open: 1, closed: 2 }
|
25
|
+
if Sc4ry::Helpers.verify_service url: @config[:url]
|
25
26
|
|
26
|
-
|
27
|
+
@@metric_circuit_state.set(status_map[status], labels: { circuit: circuit.to_s })
|
28
|
+
Sc4ry::Helpers.log level: :debug,
|
29
|
+
message: "Prometheus Notifier : notifying for circuit #{circuit}, state : #{status}."
|
30
|
+
|
31
|
+
::Prometheus::Client::Push.new(job: 'Sc4ry', instance: Socket.gethostname,
|
32
|
+
gateway: @config[:url]).add(@@registry)
|
27
33
|
else
|
28
34
|
Sc4ry::Helpers.log level: :warn, message: "Prometheus Notifier : can't notify Push Gateway not reachable."
|
29
35
|
end
|
30
36
|
end
|
31
37
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
38
|
end
|
36
|
-
|
37
39
|
end
|
data/lib/sc4ry/run_controller.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
3
|
+
# Sc4ry module
|
4
|
+
# @note namespace
|
2
5
|
module Sc4ry
|
6
|
+
# class Facility to run and update values/status for a circuit Proc
|
3
7
|
class RunController
|
4
|
-
|
8
|
+
# return the execution time of the proc
|
5
9
|
attr_reader :execution_time
|
6
10
|
|
11
|
+
# constructor
|
12
|
+
# @param [Hash] circuit the data of the circuit
|
7
13
|
def initialize(circuit = {})
|
8
14
|
@circuit = circuit
|
9
15
|
@execution_time = 0
|
@@ -12,53 +18,56 @@ module Sc4ry
|
|
12
18
|
@overtime = false
|
13
19
|
end
|
14
20
|
|
21
|
+
# return if the Proc failed on a covered exception by this circuit
|
22
|
+
# @return [Boolean]
|
15
23
|
def failed?
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
|
20
|
-
|
24
|
+
@failure
|
25
|
+
end
|
26
|
+
|
27
|
+
# return if the Proc overtime the specified time of the circuit
|
28
|
+
# @return [Boolean]
|
29
|
+
def overtimed?
|
30
|
+
@overtime
|
21
31
|
end
|
22
32
|
|
23
|
-
|
24
|
-
|
33
|
+
# return if the Proc timeout the timeout defined value of the circuit, if timeout is active
|
34
|
+
# @return [Boolean]
|
35
|
+
def timeout?
|
36
|
+
@timeout
|
25
37
|
end
|
26
38
|
|
27
|
-
|
28
|
-
|
39
|
+
# run and update values for the bloc given by keyword
|
40
|
+
# @param [Proc] block a block to run and calculate
|
41
|
+
# @return [Hash] a result Hash
|
42
|
+
def run(block:)
|
29
43
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
30
|
-
begin
|
44
|
+
begin
|
31
45
|
if @circuit[:timeout] == true
|
32
|
-
Timeout
|
33
|
-
|
46
|
+
Timeout.timeout(@circuit[:timeout_value]) do
|
47
|
+
block.call
|
34
48
|
end
|
35
49
|
@timeout = false
|
36
50
|
else
|
37
|
-
|
51
|
+
block.call
|
38
52
|
end
|
39
|
-
rescue
|
40
|
-
@last_exception = e.class
|
41
|
-
if e.
|
53
|
+
rescue StandardError => e
|
54
|
+
@last_exception = e.class.to_s
|
55
|
+
if e.instance_of?(Timeout::Error)
|
42
56
|
@timeout = true
|
43
57
|
elsif @circuit[:exceptions].include? e.class
|
44
58
|
@failure = true
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
59
|
+
elsif @circuit[:forward_unknown_exceptions]
|
60
|
+
raise e.class, "Sc4ry forward: #{e.message}"
|
61
|
+
else
|
62
|
+
Sc4ry::Helpers.log level: :debug, message: "skipped : #{@last_exception}"
|
63
|
+
|
64
|
+
end
|
53
65
|
end
|
54
66
|
@end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
55
67
|
@execution_time = @end_time - start_time
|
56
|
-
@overtime = @execution_time > @circuit[:max_time]
|
57
|
-
|
58
|
-
|
68
|
+
@overtime = @execution_time > @circuit[:max_time]
|
69
|
+
{ failure: @failure, overtime: @overtime, timeout: @timeout, execution_time: @execution_time,
|
70
|
+
end_time: @end_time, last_exception: @last_exception }
|
59
71
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
72
|
end
|
64
|
-
end
|
73
|
+
end
|