Skip to content

Commit

Permalink
feat(#9): Sending notifications when status change or when degrade
Browse files Browse the repository at this point in the history
  • Loading branch information
jcagarcia committed Oct 25, 2023
1 parent a031795 commit cb9a2c7
Show file tree
Hide file tree
Showing 16 changed files with 583 additions and 38 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/gem-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ jobs:
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
OWNER: ${{ github.repository_owner }}

- name: Get Gem Version
id: get-version
run: |
VERSION=$(ruby -r './lib/rollout/version.rb' -e "puts Rollout::VERSION")
echo "::set-output name=version::$VERSION"
shell: bash

- name: Check if Gem Version Exists in ruby gems
run: |
gem fetch rollout-redis -v ${{ steps.get-version.outputs.version }}
if [ $? -eq 0 ]; then
echo "Gem version already exists on RubyGems. Skipping the push to RubyGems."
exit 0
fi
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.0.0] - 2023-MM-DD

### Added
- `#with_notifications` method for allowing to send notifications when some different event occurs.

### Changed
- 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.

## [0.3.1] - 2023-10-24
- 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.

## [0.3.0] - 2023-10-24

### Added
Expand Down
21 changes: 21 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@ PATH
remote: .
specs:
rollout-redis (1.0.0)
mail (~> 2.8)
redis (>= 4.0, <= 5)
slack-notifier (~> 2.4)

GEM
remote: https://rubygems.org/
specs:
connection_pool (2.4.1)
date (3.3.3)
diff-lcs (1.5.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
mini_mime (1.1.5)
mock_redis (0.37.0)
net-imap (0.4.2)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
timeout
net-smtp (0.4.0)
net-protocol
rake (13.0.6)
redis (5.0.0)
redis-client (~> 0.7)
Expand All @@ -28,9 +46,12 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.1)
slack-notifier (2.4.0)
timeout (0.4.0)

PLATFORMS
aarch64-linux
arm64-darwin-23

DEPENDENCIES
bundler (>= 2.4)
Expand Down
49 changes: 21 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,41 +165,27 @@ _NOTE_: All the managed or captured errors inside the wrapped code will not be t

### Sending notifications

`rollout-redis` gem can send you different notifications. For enabling it, you just need to use the `with_notifications` instance method providing a list of notifications to be sent.
`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:

```ruby
@rollout ||= Rollout.new(redis)
.with_cache
.with_degrade(min: 100, threshold: 0.1)
.with_notifications(notifications)
```

Check the following sections for knowing more about how to [define the notifications list](#defining-the-notifications-list).

#### Defining the notifications list

`rollout-redis` gem offers different notifications that can be configured to be send to different channels. When instantiating any of the notifications above, you always must provide at least one [channel](#defining-the-channels) as a parameter.

- **StatusChange**: This notification is triggered when a feature flag is activated or deactivated using the `rollout-redis` gem.
- **ExtendedPeriod**: This notification is triggered when a feature flag has been active for an extended period of time. This notification would help teams stay aware of long-running flags and ensure that they are reviewed and, if necessary, either removed or adjusted.
- This notification must be instantiated with the `max_days` parameter. When a feature flag is 100% active for more than the defined days, the notification is sent.
- This notification must be instantiated with the `at` parameter. The reminder notification will be sent to the defined hour.
- **Degraded**: This notification is triggered when a feature flag is automatically degraded because the threshold of errors is reached
- **status_change**: This notification is triggered when a feature flag is activated or deactivated using the `rollout-redis` gem.
- **degraded**: This notification is triggered when a feature flag is automatically degraded because the threshold of errors is reached
- The instance must be configured for automatically degrading using the `with_degrade` instance method.

So this is an example of a notifications list that can be provided to the `with_notifications` method:
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.

```ruby
notifications = [
Rollout::Notifications::StatusChange.new(slack_channel),
Rollout::Notifications::ExtendedPeriod.new(slack_channel, max_days: 30, at: '09:30'),
Rollout::Notifications::Degraded.new([slack_channel, email_channel])
]
@rollout ||= Rollout.new(redis)
.with_cache
.with_degrade(min: 100, threshold: 0.1)
.with_notifications(
status_change: [slack_channel],
degraded: [slack_channel, email_channel]
)
```

#### Defining the channels

When instantiating a notification, you can provide the different channels where the notification should be published. `rollout-redis` gem offers different channels that can be configured.
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.

##### Slack Channel

Expand All @@ -210,9 +196,12 @@ The first thing to do is to setup an incoming webhook service integration. You c
After that, you can provide the obtained webhook url when instantiating the Slack channel.

```ruby
require 'rollout'

slack_channel = Rollout::Notifications::Channels::Slack.new(
webhook_url: ENV.fetch('SLACK_COMPANY_WEBHOOK_URL'),
channel: '#feature-flags-notifications'
channel: '#feature-flags-notifications',
username: 'rollout-redis'
)
```

Expand All @@ -221,6 +210,8 @@ slack_channel = Rollout::Notifications::Channels::Slack.new(
Allows you to send notifications via email.

```ruby
require 'rollout'

email_channel = Rollout::Notifications::Channels::Email.new(
smtp_host: ENV.fetch('SMTP_HOST'),
smtp_port: ENV.fetch('SMTP_PORT'),
Expand Down Expand Up @@ -295,7 +286,9 @@ We welcome and appreciate contributions from the open-source community. Before y

### Development

This project is dockerized. Once you clone the repository, you can use the `Make` commands to build the project.
This project is dockerized, so be sure you have docker installed in your machine.

Once you clone the repository, you can use the `Make` commands to build the project.

```shell
make build
Expand Down
62 changes: 52 additions & 10 deletions lib/rollout.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# frozen_string_literal: true

require 'rollout/feature'
require 'rollout/version'
require 'redis'
require 'json'

require 'rollout/feature'
require 'rollout/notifications/channels/email'
require 'rollout/notifications/channels/slack'
require 'rollout/notifications/notifiers/degrade'
require 'rollout/notifications/notifiers/status_change'
require 'rollout/version'


class Rollout

class Error < StandardError; end
Expand Down Expand Up @@ -33,22 +39,48 @@ def with_degrade(min: 100, threshold: 0.1)
self
end

def with_notifications(status_change:[], degrade:[])
status_change_channels = status_change
degrade_channels = degrade

if !status_change_channels.empty?
@status_change_notifier = Notifications::Notifiers::StatusChange.new(status_change_channels)
end

if !degrade_channels.empty?
@degrade_notifier = Notifications::Notifiers::Degrade.new(degrade_channels)
end

self
end

def activate(feature_name, percentage=100)
data = { percentage: percentage }
feature = Feature.new(feature_name, data)
@cache[feature_name] = {
feature: feature,
timestamp: Time.now.to_i
} if @cache_enabled
save(feature) == "OK"
result = save(feature) == "OK"

if result
@cache[feature_name] = {
feature: feature,
timestamp: Time.now.to_i
} if @cache_enabled

@status_change_notifier&.notify(feature_name, :activated, percentage)
end

result
end

def activate_percentage(feature_name, percentage)
activate(feature_name, percentage)
end

def deactivate(feature_name)
del(feature_name)
result = del(feature_name)

@status_change_notifier&.notify(feature_name, :deactivated)

result
end

def active?(feature_name, determinator = nil)
Expand Down Expand Up @@ -115,7 +147,11 @@ def migrate_from_rollout_format

@storage.set(new_key, new_data)

puts "Migrated key: #{old_key} to #{new_key} with data #{new_data}"
puts "Migrated key: #{old_key.gsub('feature:', '')} to #{new_key.gsub('feature-rollout-redis:', '')} with data #{new_data}"

if percentage > 0
@status_change_notifier&.notify(new_key.gsub('feature-rollout-redis:', ''), :activated, percentage)
end
end
end
end
Expand Down Expand Up @@ -154,7 +190,13 @@ def degrade(feature_name)
degraded: true,
degraded_at: Time.now
})
@storage.set(key(feature.name), data_with_degrade.to_json)
result = @storage.set(key(feature.name), data_with_degrade.to_json) == "OK"

if result
@degrade_notifier.notify(feature_name, feature.requests, feature.errors)
end

result
end

def from_cache(feature_name)
Expand Down
31 changes: 31 additions & 0 deletions lib/rollout/notifications/channels/email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'mail'

class Rollout
module Notifications
module Channels
class Email
def initialize(smtp_host:, smtp_port:, from:'no-reply@rollout-redis.com', to:)
@smtp_host = smtp_host
@smtp_port = smtp_port
@from = from
@to = to
end

def publish(subject, body)
mail = Mail.new do
subject subject
body body
end
mail.smtp_envelope_from = @from
mail.smtp_envelope_to = @to
mail.delivery_method :smtp, address: @smtp_host, port: @smtp_port
mail.deliver
end

def type
:email
end
end
end
end
end
45 changes: 45 additions & 0 deletions lib/rollout/notifications/channels/slack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'slack-notifier'

class Rollout
module Notifications
module Channels
class Slack
def initialize(webhook_url:, channel:, username:'rollout-redis')
@webhook_url = webhook_url
@channel = channel
@username = username
end

def publish(text)
begin
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
]
slack_notifier.post(blocks: blocks)
rescue => e
puts "[ERROR] Error sending notification to slack webhook. Error => #{e}"
end
end

def type
:slack
end

private

def slack_notifier
@notifier ||= ::Slack::Notifier.new @webhook_url do
defaults channel: @channel,
username: @username
end
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/rollout/notifications/notifiers/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Rollout
module Notifications
module Notifiers
class Base
def initialize(channels)
if channels.respond_to?(:first)
@channels = channels
else
@channels = [channels]
end
end
end
end
end
end
Loading

0 comments on commit cb9a2c7

Please sign in to comment.