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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09b02bf812a6d2bf771a7d9b3d299113d6600c43d88516e40ff43d724c6681b9'
4
- data.tar.gz: d18a8829c7ded0e56c2c9f43da26a73e2011d28a7aee15a9011d6246bcc5a166
3
+ metadata.gz: 8e4931c09772c7b9e3b1c3042d9912b49cbe8e3d07e6062d1238c6ac9842dd3c
4
+ data.tar.gz: fbc9b0e1f80851d1960038169ddce0e93b813097979a8b12c2c82f8fec916ce2
5
5
  SHA512:
6
- metadata.gz: 8da06a204a6133487f43c52c013485476391b1ff8d2d5680eda8ef9f419aa6b516a63a27fbae3627f0b747b7515fff2347b0594802252ab8b05f78a8b9988154
7
- data.tar.gz: 2850a4238642e603504ea4034bd036d09649d319127d99eae4afbe2903f099c7af6d6cc49e50248bfff4b01354982ce93b4b8e58468fdbf7c37dae6801bd0f10
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 erros is reached, you can enable the degrade feature using the `with_degrade` method.
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 deactivating the feature flag if the threshold of errors is reached. All the managed or captured errors inside the wrapped code will not be taken into consideration.
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. Once you clone the repository, you can use the `Make` commands to build the project.
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,15 @@
1
+ class Rollout
2
+ module Notifications
3
+ module Notifiers
4
+ class Base
5
+ def initialize(channels)
6
+ if channels.respond_to?(:first)
7
+ @channels = channels
8
+ else
9
+ @channels = [channels]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rollout
4
- VERSION = '0.3.1'
4
+ VERSION = '1.0.0'
5
5
  end
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
- @cache[feature_name] = {
40
- feature: feature,
41
- timestamp: Time.now.to_i
42
- } if @cache_enabled
43
- save(feature) == "OK"
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
- deactivate(feature_name) if degraded?(feature)
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
 
@@ -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.3.1
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-24 00:00:00.000000000 Z
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