rollout-redis 0.3.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +65 -3
- data/lib/rollout/notifications/channels/email.rb +31 -0
- data/lib/rollout/notifications/channels/slack.rb +45 -0
- data/lib/rollout/notifications/notifiers/base.rb +15 -0
- data/lib/rollout/notifications/notifiers/degrade.rb +33 -0
- data/lib/rollout/notifications/notifiers/status_change.rb +42 -0
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +62 -10
- data/rollout-redis.gemspec +2 -0
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e4931c09772c7b9e3b1c3042d9912b49cbe8e3d07e6062d1238c6ac9842dd3c
|
4
|
+
data.tar.gz: fbc9b0e1f80851d1960038169ddce0e93b813097979a8b12c2c82f8fec916ce2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d543d5e22d3519838ae6b9c15fbd6a982db066ef7ffe8792945d98bf636a4a34c23294fe02618b4484c6f3d3ee5625fb600a8e3942fc843fb393fdcc701e299
|
7
|
+
data.tar.gz: 8a79720ea90eed5001b67e6394e0ae26e19893e70478166aef179da9e5848c266f736fd1a69c962fa162a309fb510df5265cf14a134a15fb43081c6dd5b87af7
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
7
|
|
8
|
+
## [1.0.0] - 2023-10-25
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- `#with_notifications` method for allowing to send notifications when some different event occurs.
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- When the threshold of errors is reached when using the `with_degrade` feature, instead of deleting the feature flag from the redis, we are marking it now as degraded, moving the activation percentage to 0% and adding some useful information to the feature flag stored data.
|
15
|
+
|
16
|
+
## [0.3.1] - 2023-10-24
|
17
|
+
- Same as 0.3.0. When testing GitHub actions for moving to first release `1.0.0` it deployed a new version of the gem by error.
|
18
|
+
|
8
19
|
## [0.3.0] - 2023-10-24
|
9
20
|
|
10
21
|
### Added
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@ Topics covered in this README:
|
|
14
14
|
- [Gradual activation based on percentages](#gradual-activation-based-on-percentages)
|
15
15
|
- [Caching Feature Flags](#caching-feature-flags)
|
16
16
|
- [Auto-deactivating flags](#auto-deactivating-flags)
|
17
|
+
- [Sending Notifications](#sending-notifications)
|
17
18
|
- [Rake tasks](#rake-tasks)
|
18
19
|
- [Migrating from rollout gem](#migrating-from-rollout-gem-)
|
19
20
|
- [Changelog](#changelog)
|
@@ -142,7 +143,7 @@ In the case that you need to clear the cache at any point, you can make use of t
|
|
142
143
|
|
143
144
|
### Auto-deactivating flags
|
144
145
|
|
145
|
-
If you want to allow the gem to deactivate your feature flag automatically when a threshold of
|
146
|
+
If you want to allow the gem to deactivate your feature flag automatically when a threshold of errors is reached, you can enable the degrade feature using the `with_degrade` method.
|
146
147
|
|
147
148
|
```ruby
|
148
149
|
@rollout ||= Rollout.new(redis)
|
@@ -158,7 +159,66 @@ So now, instead of using the `active?` method, you need to wrap your new code un
|
|
158
159
|
end
|
159
160
|
```
|
160
161
|
|
161
|
-
When any unexpected error appears during the wrapped code execution, the Rollout gem will take it into account for automatically
|
162
|
+
When any unexpected error appears during the wrapped code execution, the Rollout gem will take it into account for automatically degrading the feature flag if the threshold of errors is reached. The feature flag will not be removed from the redis, but it will change its percentage to 0 and it will be marked as degraded.
|
163
|
+
|
164
|
+
_NOTE_: All the managed or captured errors inside the wrapped code will not be taken into consideration for degrading the feature flag.
|
165
|
+
|
166
|
+
### Sending notifications
|
167
|
+
|
168
|
+
`rollout-redis` gem can send different notifications to your development team. For enabling this feature, you just need to use the `with_notifications` instance method providing the channels where you want to publish each of the different events that can occur:
|
169
|
+
|
170
|
+
- **status_change**: This notification is triggered when a feature flag is activated or deactivated using the `rollout-redis` gem.
|
171
|
+
- **degrade**: This notification is triggered when a feature flag is automatically degraded because the threshold of errors is reached
|
172
|
+
- The instance must be configured for automatically degrading using the `with_degrade` instance method.
|
173
|
+
|
174
|
+
You must provide at least one [channel](#defining-the-channels) as a parameter if you want to enable the notifications for that specific event. If no channels provided, the notifications will not be sent.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
@rollout ||= Rollout.new(redis)
|
178
|
+
.with_cache
|
179
|
+
.with_degrade(min: 100, threshold: 0.1)
|
180
|
+
.with_notifications(
|
181
|
+
status_change: [slack_channel],
|
182
|
+
degrade: [slack_channel, email_channel]
|
183
|
+
)
|
184
|
+
```
|
185
|
+
|
186
|
+
#### Defining the channels
|
187
|
+
|
188
|
+
When enabling a notification, you can provide the different channels where the notification should be published. `rollout-redis` gem offers different channels that can be configured.
|
189
|
+
|
190
|
+
##### Slack Channel
|
191
|
+
|
192
|
+
Allows you to send notifications using a slack webhook.
|
193
|
+
|
194
|
+
The first thing to do is to setup an incoming webhook service integration. You can do this from your services page.
|
195
|
+
|
196
|
+
After that, you can provide the obtained webhook url when instantiating the Slack channel.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
require 'rollout'
|
200
|
+
|
201
|
+
slack_channel = Rollout::Notifications::Channels::Slack.new(
|
202
|
+
webhook_url: ENV.fetch('SLACK_COMPANY_WEBHOOK_URL'),
|
203
|
+
channel: '#feature-flags-notifications',
|
204
|
+
username: 'rollout-redis'
|
205
|
+
)
|
206
|
+
```
|
207
|
+
|
208
|
+
##### Email Channel
|
209
|
+
|
210
|
+
Allows you to send notifications via email.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
require 'rollout'
|
214
|
+
|
215
|
+
email_channel = Rollout::Notifications::Channels::Email.new(
|
216
|
+
smtp_host: ENV.fetch('SMTP_HOST'),
|
217
|
+
smtp_port: ENV.fetch('SMTP_PORT'),
|
218
|
+
from: 'no-reply@rollout-redis.com',
|
219
|
+
to: 'developers@yourcompany.com'
|
220
|
+
)
|
221
|
+
```
|
162
222
|
|
163
223
|
## Rake tasks
|
164
224
|
|
@@ -226,7 +286,9 @@ We welcome and appreciate contributions from the open-source community. Before y
|
|
226
286
|
|
227
287
|
### Development
|
228
288
|
|
229
|
-
This project is dockerized
|
289
|
+
This project is dockerized, so be sure you have docker installed in your machine.
|
290
|
+
|
291
|
+
Once you clone the repository, you can use the `Make` commands to build the project.
|
230
292
|
|
231
293
|
```shell
|
232
294
|
make build
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Channels
|
6
|
+
class Email
|
7
|
+
def initialize(smtp_host:, smtp_port:, from:'no-reply@rollout-redis.com', to:)
|
8
|
+
@smtp_host = smtp_host
|
9
|
+
@smtp_port = smtp_port
|
10
|
+
@from = from
|
11
|
+
@to = to
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish(subject, body)
|
15
|
+
mail = Mail.new do
|
16
|
+
subject subject
|
17
|
+
body body
|
18
|
+
end
|
19
|
+
mail.smtp_envelope_from = @from
|
20
|
+
mail.smtp_envelope_to = @to
|
21
|
+
mail.delivery_method :smtp, address: @smtp_host, port: @smtp_port
|
22
|
+
mail.deliver
|
23
|
+
end
|
24
|
+
|
25
|
+
def type
|
26
|
+
:email
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'slack-notifier'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Channels
|
6
|
+
class Slack
|
7
|
+
def initialize(webhook_url:, channel:, username:'rollout-redis')
|
8
|
+
@webhook_url = webhook_url
|
9
|
+
@channel = channel
|
10
|
+
@username = username
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish(text)
|
14
|
+
begin
|
15
|
+
blocks = [
|
16
|
+
{
|
17
|
+
"type": "section",
|
18
|
+
"text": {
|
19
|
+
"type": "mrkdwn",
|
20
|
+
"text": text
|
21
|
+
}
|
22
|
+
}
|
23
|
+
]
|
24
|
+
slack_notifier.post(blocks: blocks)
|
25
|
+
rescue => e
|
26
|
+
puts "[ERROR] Error sending notification to slack webhook. Error => #{e}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def type
|
31
|
+
:slack
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def slack_notifier
|
37
|
+
@notifier ||= ::Slack::Notifier.new @webhook_url do
|
38
|
+
defaults channel: @channel,
|
39
|
+
username: @username
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Notifiers
|
6
|
+
class Degrade < Base
|
7
|
+
def initialize(channels)
|
8
|
+
super(channels)
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(feature_name, requests, errors)
|
12
|
+
@channels.each do |c|
|
13
|
+
publish_for_slack_channel(c, feature_name, requests, errors) if c.type == :slack
|
14
|
+
publish_for_email_channel(c, feature_name, requests, errors) if c.type == :email
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def publish_for_slack_channel(c, feature_name, requests, errors)
|
21
|
+
text = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
22
|
+
c.publish(text)
|
23
|
+
end
|
24
|
+
|
25
|
+
def publish_for_email_channel(c, feature_name, requests, errors)
|
26
|
+
subject = 'Feature flag has been automatically deactivated!'
|
27
|
+
content = "Feature flag '#{feature_name}' has been degraded after #{requests} requests and #{errors} errors"
|
28
|
+
c.publish(subject, content)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
class Rollout
|
4
|
+
module Notifications
|
5
|
+
module Notifiers
|
6
|
+
class StatusChange < Base
|
7
|
+
def initialize(channels)
|
8
|
+
super(channels)
|
9
|
+
end
|
10
|
+
|
11
|
+
def notify(feature_name, new_status, new_percentage=nil)
|
12
|
+
@channels.each do |c|
|
13
|
+
publish_for_slack_channel(c, feature_name, new_status, new_percentage) if c.type == :slack
|
14
|
+
publish_for_email_channel(c, feature_name, new_status, new_percentage) if c.type == :email
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def publish_for_slack_channel(c, feature_name, new_status, new_percentage)
|
21
|
+
if new_status == :activated
|
22
|
+
text = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
23
|
+
elsif new_status == :deactivated
|
24
|
+
text = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
25
|
+
end
|
26
|
+
c.publish(text)
|
27
|
+
end
|
28
|
+
|
29
|
+
def publish_for_email_channel(c, feature_name, new_status, new_percentage)
|
30
|
+
if new_status == :activated
|
31
|
+
subject = 'Feature flag has been activated!'
|
32
|
+
content = "Feature flag '#{feature_name}' has been activated with percentage #{new_percentage}!"
|
33
|
+
elsif new_status == :deactivated
|
34
|
+
subject = 'Feature flag has been deactivated!'
|
35
|
+
content = "Feature flag '#{feature_name}' has been deactivated and deleted!"
|
36
|
+
end
|
37
|
+
c.publish(subject, content)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/rollout/version.rb
CHANGED
data/lib/rollout.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'rollout/feature'
|
4
|
-
require 'rollout/version'
|
5
3
|
require 'redis'
|
6
4
|
require 'json'
|
7
5
|
|
6
|
+
require 'rollout/feature'
|
7
|
+
require 'rollout/notifications/channels/email'
|
8
|
+
require 'rollout/notifications/channels/slack'
|
9
|
+
require 'rollout/notifications/notifiers/degrade'
|
10
|
+
require 'rollout/notifications/notifiers/status_change'
|
11
|
+
require 'rollout/version'
|
12
|
+
|
13
|
+
|
8
14
|
class Rollout
|
9
15
|
|
10
16
|
class Error < StandardError; end
|
@@ -33,14 +39,36 @@ class Rollout
|
|
33
39
|
self
|
34
40
|
end
|
35
41
|
|
42
|
+
def with_notifications(status_change:[], degrade:[])
|
43
|
+
status_change_channels = status_change
|
44
|
+
degrade_channels = degrade
|
45
|
+
|
46
|
+
if !status_change_channels.empty?
|
47
|
+
@status_change_notifier = Notifications::Notifiers::StatusChange.new(status_change_channels)
|
48
|
+
end
|
49
|
+
|
50
|
+
if !degrade_channels.empty?
|
51
|
+
@degrade_notifier = Notifications::Notifiers::Degrade.new(degrade_channels)
|
52
|
+
end
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
36
57
|
def activate(feature_name, percentage=100)
|
37
58
|
data = { percentage: percentage }
|
38
59
|
feature = Feature.new(feature_name, data)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
60
|
+
result = save(feature) == "OK"
|
61
|
+
|
62
|
+
if result
|
63
|
+
@cache[feature_name] = {
|
64
|
+
feature: feature,
|
65
|
+
timestamp: Time.now.to_i
|
66
|
+
} if @cache_enabled
|
67
|
+
|
68
|
+
@status_change_notifier&.notify(feature_name, :activated, percentage)
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
44
72
|
end
|
45
73
|
|
46
74
|
def activate_percentage(feature_name, percentage)
|
@@ -48,7 +76,11 @@ class Rollout
|
|
48
76
|
end
|
49
77
|
|
50
78
|
def deactivate(feature_name)
|
51
|
-
del(feature_name)
|
79
|
+
result = del(feature_name)
|
80
|
+
|
81
|
+
@status_change_notifier&.notify(feature_name, :deactivated)
|
82
|
+
|
83
|
+
result
|
52
84
|
end
|
53
85
|
|
54
86
|
def active?(feature_name, determinator = nil)
|
@@ -73,7 +105,7 @@ class Rollout
|
|
73
105
|
feature.add_error
|
74
106
|
save(feature)
|
75
107
|
|
76
|
-
|
108
|
+
degrade(feature_name) if degraded?(feature)
|
77
109
|
end
|
78
110
|
raise e
|
79
111
|
end
|
@@ -115,7 +147,11 @@ class Rollout
|
|
115
147
|
|
116
148
|
@storage.set(new_key, new_data)
|
117
149
|
|
118
|
-
puts "Migrated key: #{old_key} to #{new_key} with data #{new_data}"
|
150
|
+
puts "Migrated key: #{old_key.gsub('feature:', '')} to #{new_key.gsub('feature-rollout-redis:', '')} with data #{new_data}"
|
151
|
+
|
152
|
+
if percentage > 0
|
153
|
+
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
|
154
|
+
end
|
119
155
|
end
|
120
156
|
end
|
121
157
|
end
|
@@ -147,6 +183,22 @@ class Rollout
|
|
147
183
|
@storage.del(key(feature_name)) == 1
|
148
184
|
end
|
149
185
|
|
186
|
+
def degrade(feature_name)
|
187
|
+
feature = get(feature_name)
|
188
|
+
data_with_degrade = feature.data.merge({
|
189
|
+
percentage: 0,
|
190
|
+
degraded: true,
|
191
|
+
degraded_at: Time.now
|
192
|
+
})
|
193
|
+
result = @storage.set(key(feature.name), data_with_degrade.to_json) == "OK"
|
194
|
+
|
195
|
+
if result
|
196
|
+
@degrade_notifier.notify(feature_name, feature.requests, feature.errors)
|
197
|
+
end
|
198
|
+
|
199
|
+
result
|
200
|
+
end
|
201
|
+
|
150
202
|
def from_cache(feature_name)
|
151
203
|
return nil unless @cache_enabled
|
152
204
|
|
data/rollout-redis.gemspec
CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.required_ruby_version = '>= 2.3'
|
24
24
|
|
25
25
|
spec.add_runtime_dependency 'redis', '>= 4.0', '<= 5'
|
26
|
+
spec.add_runtime_dependency 'slack-notifier', '~> 2.4'
|
27
|
+
spec.add_runtime_dependency 'mail', '~> 2.8'
|
26
28
|
|
27
29
|
spec.add_development_dependency 'bundler', '>= 2.4'
|
28
30
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rollout-redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Juan Carlos García
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -30,6 +30,34 @@ dependencies:
|
|
30
30
|
- - "<="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '5'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: slack-notifier
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.4'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.4'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: mail
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.8'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.8'
|
33
61
|
- !ruby/object:Gem::Dependency
|
34
62
|
name: bundler
|
35
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,6 +126,11 @@ files:
|
|
98
126
|
- Rakefile
|
99
127
|
- lib/rollout.rb
|
100
128
|
- lib/rollout/feature.rb
|
129
|
+
- lib/rollout/notifications/channels/email.rb
|
130
|
+
- lib/rollout/notifications/channels/slack.rb
|
131
|
+
- lib/rollout/notifications/notifiers/base.rb
|
132
|
+
- lib/rollout/notifications/notifiers/degrade.rb
|
133
|
+
- lib/rollout/notifications/notifiers/status_change.rb
|
101
134
|
- lib/rollout/tasks/rollout.rake
|
102
135
|
- lib/rollout/version.rb
|
103
136
|
- rollout-redis.gemspec
|