diffend-monitor 0.2.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module Config
5
+ # Class used to figure out the file from which we should load the settings
6
+ module FileFinder
7
+ # Names of the files or paths where we will look for the settings
8
+ #
9
+ # @note We do the double dot trick, to look outside of the current dir because when
10
+ # executed from a docker container, we copy the local uncommitted settings into the
11
+ # dir above the app location not to pollute the reset state of the git repo
12
+ #
13
+ # @note Order is important, as for local env we should load from
14
+ # local file (if present first)
15
+ FILE_NAMES = %w[
16
+ .diffend.yml
17
+ ].map { |name| ["../#{name}", name] }.tap(&:flatten!).freeze
18
+
19
+ private_constant :FILE_NAMES
20
+
21
+ class << self
22
+ # Looks for Diffend settings file for a given env
23
+ #
24
+ # @param build_path [String] path of the current build
25
+ #
26
+ # @return [String] path to the file from which we should load all the settings
27
+ def call(build_path)
28
+ FILE_NAMES
29
+ .map { |name| File.join(build_path, name) }
30
+ .map { |name| Dir[name] }
31
+ .find { |selection| !selection.empty? }
32
+ .tap { |path| path || raise(Errors::MissingConfigurationFile) }
33
+ .first
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Module for all the components related to setting up the config
5
+ module Config
6
+ # Class responsible for validating the config from .diffend.yml
7
+ module Validator
8
+ class << self
9
+ # @param config [OpenStruct] path of the current build
10
+ def call(config)
11
+ raise Errors::ProjectIdMissingInConfigurationFile if missing?(config, 'project_id')
12
+ raise Errors::ShareableIdMissingInConfigurationFile if missing?(config, 'shareable_id')
13
+ raise Errors::ShareableKeyMissingInConfigurationFile if missing?(config, 'shareable_key')
14
+ raise Errors::BuildPathMissingInConfigurationFile if missing?(config, 'build_path')
15
+ end
16
+
17
+ private
18
+
19
+ def missing?(config, key)
20
+ config.public_send(key).nil? || config.public_send(key).empty?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Build runner app errors
5
+ module Errors
6
+ # Base error class from which all the errors should inherit
7
+ BaseError = Class.new(StandardError)
8
+ # Raised when we couldn't find a valid configuration file
9
+ MissingConfigurationFile = Class.new(BaseError)
10
+ # Raised when configuration file is empty
11
+ EmptyConfigurationFile = Class.new(BaseError)
12
+ # Raised when configuration file is malformed
13
+ MalformedConfigurationFile = Class.new(BaseError)
14
+ # Raised when project_id is missing in configuration file
15
+ ProjectIdMissingInConfigurationFile = Class.new(BaseError)
16
+ # Raised when shareable_id is missing in configuration file
17
+ ShareableIdMissingInConfigurationFile = Class.new(BaseError)
18
+ # Raised when shareable_key is missing in configuration file
19
+ ShareableKeyMissingInConfigurationFile = Class.new(BaseError)
20
+ # Raised when build_path is missing in configuration file
21
+ BuildPathMissingInConfigurationFile = Class.new(BaseError)
22
+ # Raised when server-side error occurs
23
+ RequestServerError = Class.new(BaseError)
24
+ # Raised when we had an exception that we know how to handle
25
+ HandledException = Class.new(BaseError)
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Diffend
6
+ module HandleErrors
7
+ # Module responsible for building exception payload
8
+ module BuildExceptionPayload
9
+ class << self
10
+ # Build exception payload
11
+ #
12
+ # @param exception [Exception, NilClass] expection that was raised
13
+ # @param payload [Hash] with versions to check
14
+ #
15
+ # @return [Hash]
16
+ def call(exception, payload)
17
+ {
18
+ request_id: SecureRandom.uuid,
19
+ payload: payload,
20
+ exception: {
21
+ class: exception&.class,
22
+ message: exception&.message,
23
+ backtrace: exception&.backtrace
24
+ }
25
+ }.freeze
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for displaying exception payload to stdout
6
+ module DisplayToStdout
7
+ class << self
8
+ # Display exception payload to stdout
9
+ #
10
+ # @param exception_payload [Hash]
11
+ def call(exception_payload)
12
+ puts exception_payload.to_json
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ module Messages
6
+ PAYLOAD_DUMP = '^^^ Above is the dump of your request ^^^'
7
+ UNHANDLED_EXCEPTION = <<~MSG
8
+ \nSomething went really wrong. We recorded this incident in our system and will review it.\n
9
+ This is a bug, don't hesitate.\n
10
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
11
+ MSG
12
+ UNSUPPORTED_RESPONSE = <<~MSG
13
+ \nAPI returned an unsupported response. We recorded this incident in our system and will review it.\n
14
+ This is a bug, don't hesitate.\n
15
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
16
+ MSG
17
+ UNSUPPORTED_VERDICT = <<~MSG
18
+ \nAPI returned an unsupported verdict. We recorded this incident in our system and will review it.\n
19
+ This is a bug, don't hesitate.\n
20
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
21
+ MSG
22
+ REQUEST_ERROR = <<~MSG
23
+ \nWe were unable to process your request at this time. We recorded this incident in our system and will review it.\n
24
+ If you think that this is a bug, don't hesitate.\n
25
+ Create an issue at https://github.com/diffend-io/diffend-ruby/issues\n
26
+ MSG
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ module HandleErrors
5
+ # Module responsible for reporting errors to diffend
6
+ module Report
7
+ class << self
8
+ # Execute request to Diffend
9
+ #
10
+ # @param exception [Exception] expection that was raised
11
+ # @param payload [Hash] with versions to check
12
+ # @param config [OpenStruct] Diffend config
13
+ # @param message [Symbol] message that we want to display
14
+ # @param report [Boolean] if true we will report the issue to diffend
15
+ # @param raise_exception [Boolean] if true we will raise an exception
16
+ #
17
+ # @return [Net::HTTPResponse] response from Diffend
18
+ def call(config:, message:, exception: nil, payload: {}, report: false, raise_exception: true)
19
+ exception_payload = prepare_exception_payload(exception, payload)
20
+
21
+ Bundler.ui.error(Diffend::HandleErrors::Messages::PAYLOAD_DUMP)
22
+ Bundler.ui.error(Diffend::HandleErrors::Messages.const_get(message.to_s.upcase))
23
+
24
+ if report
25
+ Diffend::Request.call(
26
+ build_request_object(config, exception_payload)
27
+ )
28
+ end
29
+
30
+ raise Diffend::Errors::HandledException if raise_exception
31
+ end
32
+
33
+ # @param config [OpenStruct] diffend config
34
+ # @param payload [Hash]
35
+ #
36
+ # @return [Diffend::RequestObject]
37
+ def build_request_object(config, payload)
38
+ Diffend::RequestObject.new(
39
+ config: config,
40
+ url: errors_url(config.project_id),
41
+ payload: payload,
42
+ request_method: :post
43
+ )
44
+ end
45
+
46
+ # Prepare exception payload and display it to stdout
47
+ #
48
+ # @param exception [Exception] expection that was raised
49
+ # @param payload [Hash] with versions to check
50
+ #
51
+ # @return [Hash]
52
+ def prepare_exception_payload(exception, payload)
53
+ Diffend::HandleErrors::BuildExceptionPayload
54
+ .call(exception, payload)
55
+ .tap(&Diffend::HandleErrors::DisplayToStdout.method(:call))
56
+ end
57
+
58
+ # Provides diffend errors endpoint url
59
+ #
60
+ # @param project_id [String] diffend project_id
61
+ #
62
+ # @return [String] diffend endpoint
63
+ def errors_url(project_id)
64
+ return ENV['DIFFEND_ERROR_URL'] if ENV.key?('DIFFEND_ERROR_URL')
65
+
66
+ "https://my.diffend.io/api/projects/#{project_id}/errors"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ build_bundler_definition
5
+ errors
6
+ config/fetcher
7
+ config/file_finder
8
+ config/validator
9
+ commands
10
+ handle_errors/messages
11
+ handle_errors/build_exception_payload
12
+ handle_errors/display_to_stdout
13
+ handle_errors/report
14
+ request_object
15
+ request
16
+ voting
17
+ track
18
+ ].each { |file| require "diffend/#{file}" }
19
+
20
+ %w[
21
+ versions/local
22
+ versions/remote
23
+ ].each { |file| require "diffend/voting/#{file}" }
24
+
25
+ Thread.new do
26
+ track = Diffend::Track.new
27
+ track.start
28
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ module Diffend
8
+ # Module responsible for doing request to Diffend
9
+ module Request
10
+ # Message displayed when connection issue occured and we will retry
11
+ CONNECTION_MESSAGE = 'We experienced a connection issue, retrying...'
12
+ # List of connection exceptions
13
+ CONNECTION_EXCEPTIONS = [
14
+ Errno::ECONNRESET,
15
+ Errno::ENETUNREACH,
16
+ Errno::EHOSTUNREACH,
17
+ Errno::ECONNREFUSED
18
+ ].freeze
19
+ # Message displayed when timeout occured and we will retry
20
+ TIMEOUT_MESSAGE = 'We experienced a connection issue, retrying...'
21
+ # List of timeout exceptions
22
+ TIMEOUT_EXCEPTIONS = [
23
+ Net::OpenTimeout,
24
+ Net::ReadTimeout
25
+ ].freeze
26
+ # Message displayed when server issue occured and we will retry
27
+ SERVER_ERROR_MESSAGE = 'We experienced a server-side issue, retrying...'
28
+ # List of server issues
29
+ #
30
+ # 500 - Internal Server Error
31
+ # 502 - Bad Gateway
32
+ # 503 - Service Unavailable
33
+ # 504 - Gateway Timeout
34
+ SERVER_ERRORS = [500, 502, 503, 504].freeze
35
+ # Number of retries
36
+ RETRIES = 3
37
+ # Request headers
38
+ HEADERS = { 'Content-Type': 'application/json' }.freeze
39
+
40
+ private_constant :HEADERS
41
+
42
+ class << self
43
+ # Execute request
44
+ #
45
+ # @param request_object [Diffend::RequestObject]
46
+ #
47
+ # @return [Net::HTTPResponse] response from Diffend
48
+ def call(request_object)
49
+ retry_count ||= -1
50
+
51
+ build_http(request_object.url) do |http, uri|
52
+ response = http.request(
53
+ build_request(
54
+ uri,
55
+ request_object.request_method,
56
+ request_object.config,
57
+ request_object.payload
58
+ )
59
+ )
60
+
61
+ if SERVER_ERRORS.include?(response.code.to_i)
62
+ raise Diffend::Errors::RequestServerError, response.code.to_i
63
+ end
64
+
65
+ response
66
+ end
67
+ rescue Diffend::Errors::RequestServerError => e
68
+ retry_count += 1
69
+
70
+ retry if handle_retry(SERVER_ERROR_MESSAGE, retry_count)
71
+
72
+ Diffend::HandleErrors::Report.call(
73
+ exception: e,
74
+ payload: request_object.payload,
75
+ config: request_object.config,
76
+ message: :request_error
77
+ )
78
+ rescue *CONNECTION_EXCEPTIONS => e
79
+ retry_count += 1
80
+
81
+ retry if handle_retry(CONNECTION_MESSAGE, retry_count)
82
+
83
+ Diffend::HandleErrors::Report.call(
84
+ exception: e,
85
+ payload: request_object.payload,
86
+ config: request_object.config,
87
+ message: :request_error
88
+ )
89
+ rescue *TIMEOUT_EXCEPTIONS => e
90
+ retry_count += 1
91
+
92
+ retry if handle_retry(TIMEOUT_MESSAGE, retry_count)
93
+
94
+ Diffend::HandleErrors::Report.call(
95
+ exception: e,
96
+ payload: request_object.payload,
97
+ config: request_object.config,
98
+ message: :request_error
99
+ )
100
+ end
101
+
102
+ # Handle retry
103
+ #
104
+ # @param message [String] message we want to display
105
+ # @param retry_count [Integer]
106
+ def handle_retry(message, retry_count)
107
+ return false if retry_count == RETRIES
108
+
109
+ Bundler.ui.error(message)
110
+ sleep(exponential_backoff(retry_count))
111
+
112
+ retry_count < RETRIES
113
+ end
114
+
115
+ # Builds http connection object
116
+ #
117
+ # @param url [String] command endpoint url
118
+ def build_http(url)
119
+ uri = URI(url)
120
+
121
+ Net::HTTP.start(
122
+ uri.host,
123
+ uri.port,
124
+ use_ssl: uri.scheme == 'https',
125
+ verify_mode: OpenSSL::SSL::VERIFY_NONE,
126
+ open_timeout: 5,
127
+ read_timeout: 5
128
+ ) { |http| yield(http, uri) }
129
+ end
130
+
131
+ # Build http post request and assigns headers and payload
132
+ #
133
+ # @param uri [URI::HTTPS]
134
+ # @param request_method [Symbol]
135
+ # @param config [OpenStruct] Diffend config
136
+ # @param payload [Hash] with versions to check
137
+ #
138
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
139
+ def build_request(uri, request_method, config, payload)
140
+ pick_request_method(request_method)
141
+ .new(uri.request_uri, HEADERS)
142
+ .tap { |request| assign_auth(request, config) }
143
+ .tap { |request| assign_payload(request, payload) }
144
+ end
145
+
146
+ # Pick request method
147
+ #
148
+ # @param request_method [Symbol]
149
+ #
150
+ # @return [Net::HTTP::Post, Net::HTTP::Put]
151
+ def pick_request_method(request_method)
152
+ case request_method
153
+ when :post
154
+ Net::HTTP::Post
155
+ when :put
156
+ Net::HTTP::Put
157
+ end
158
+ end
159
+
160
+ # Assigns basic authorization if provided in the config
161
+ #
162
+ # @param request [Net::HTTP::Post] prepared http post
163
+ # @param config [OpenStruct] Diffend config
164
+ def assign_auth(request, config)
165
+ return unless config
166
+ return unless config.shareable_id
167
+ return unless config.shareable_key
168
+
169
+ request.basic_auth(config.shareable_id, config.shareable_key)
170
+ end
171
+
172
+ # Assigns payload as json
173
+ #
174
+ # @param request [Net::HTTP::Post] prepared http post
175
+ # @param payload [Hash] with versions to check
176
+ def assign_payload(request, payload)
177
+ request.body = JSON.dump(payload: payload)
178
+ end
179
+
180
+ def exponential_backoff(retry_count)
181
+ 2**(retry_count + 1)
182
+ end
183
+ end
184
+ end
185
+ end