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