rollbar-notification-rules-generator 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7741d04c00243c59a3451ae2626619c1950012aae49833d0354e65a378312eb9
4
+ data.tar.gz: 2295ce6a7e025b9f51b9080bc3e39e87fbcdafdbe1798f5a7621a1078be85a96
5
+ SHA512:
6
+ metadata.gz: 7b43b62215e964fbe5e3790ba57573c1a93ed56745ee1eb6adabc2add13cfde0a23decb78d0289e78fb92f97b30a47623839780cbe8ac330153f047bcb2d999d
7
+ data.tar.gz: d2ee76d26ae45c3be12dcd972cc7d69b6fcd8c72d6dd96a922b364b601be90fa958609d8971cf6056bafc23ceca8cdec51611ec24bf2255a0a262c5d623fd46e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rollbar-notification-rules-generator.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 abicky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # rollbar-notification-rules-generator
2
+
3
+ `rollbar-notification-rules-generator` is a CLI tool that generates Rollbar notification rules
4
+ that are mutually exclusive from a simple YAML file.
5
+ If you outputs the rules in the Terraform language, you can create the rules with Terraform.
6
+
7
+ For example, assume that you want to notify errors whose title contains the substring "foo"
8
+ to the Slack channel "alert-foo" every time they occur and other errors to the default channel.
9
+ You can write the Rollbar notification rules in the Terraform language as follows:
10
+
11
+ ```terraform
12
+ resource "rollbar_notification" "slack_occurrence_0" {
13
+ channel = "slack"
14
+
15
+ rule {
16
+ trigger = "occurrence"
17
+ filters {
18
+ type = "level"
19
+ operation = "gte"
20
+ value = "error"
21
+ }
22
+ filters {
23
+ type = "title"
24
+ operation = "within"
25
+ value = "foo"
26
+ }
27
+ }
28
+ config {
29
+ channel = "alert-foo"
30
+ }
31
+ }
32
+
33
+ resource "rollbar_notification" "slack_occurrence_1" {
34
+ channel = "slack"
35
+
36
+ rule {
37
+ trigger = "occurrence"
38
+ filters {
39
+ type = "level"
40
+ operation = "gte"
41
+ value = "error"
42
+ }
43
+ filters {
44
+ type = "title"
45
+ operation = "nwithin"
46
+ value = "foo"
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ As you can see, you have to add the filter whose operation is "nwithin" to the second rule.
53
+ If there are more rules, you have to write complicated rules to make them mutually exclusive
54
+ and struggle to maintain them.
55
+ With this tool, you can write the rules in YAML format as follows:
56
+
57
+ ```yaml
58
+ channel: "slack"
59
+ triggers:
60
+ occurrence:
61
+ - conditions:
62
+ - type: "level"
63
+ operation: "gte"
64
+ value: "error"
65
+ - type: "title"
66
+ operation: "within"
67
+ value: "foo"
68
+ configs:
69
+ - channel: "alert-foo"
70
+ - conditions:
71
+ - type: "level"
72
+ operation: "gte"
73
+ value: "error"
74
+ ```
75
+
76
+ You don't have to add another condition to the second rule because the tool generates rules
77
+ to make Rollbar behave as if it tries to find the first rule from the top down that matches
78
+ all the conditions.
79
+
80
+ ## Installation
81
+
82
+ $ gem install rollbar-notification-rules-generator
83
+
84
+ ## Usage
85
+
86
+ ```
87
+ Usage: rollbar-notification-rules-generator [options] <input>
88
+ --format FORMAT terraform|text (default: terraform)
89
+ ```
90
+
91
+ The input file is in YAML format like the following:
92
+
93
+ ```yaml
94
+ channel: "slack" # slack or pagerduty
95
+ # This optional value is used as provider meta-argument of Terraform.
96
+ terraform_provider: rollbar.alias_value
97
+ # You can define variables, and they are expanded with the syntax "${{ var.variable_name }}".
98
+ variables:
99
+ slack_channel_for_foo: "alert-foo"
100
+ triggers:
101
+ # You can specify the following triggers:
102
+ # * new_item
103
+ # * deploy
104
+ # * reactivated_item
105
+ # * reopened_item
106
+ # * occurrence_rate
107
+ # * exp_repeat_item
108
+ # * resolved_item
109
+ # * occurrence
110
+ occurrence:
111
+ # Each element corresponds to a Rollbar notification rule.
112
+ # For more details, the following documents help:
113
+ # * https://docs.rollbar.com/reference/post_api-1-notifications-slack-rules
114
+ # * https://docs.rollbar.com/reference/post_api-1-notifications-pagerduty
115
+ # * https://registry.terraform.io/providers/rollbar/rollbar/latest/docs/resources/notification
116
+ # Note that the tool hasn't supported some types yet.
117
+ - conditions:
118
+ - type: "level"
119
+ operation: "gte"
120
+ value: "error"
121
+ - type: "title"
122
+ operation: "within"
123
+ value: "foo"
124
+ # Each element corresponds to "config."
125
+ # If you specify multiple elements, as many rules as the elements are created,
126
+ # and Rollbar notifies items according to each configuration.
127
+ configs:
128
+ - channel: ${{ var.slack_channel_for_foo }}
129
+ - conditions:
130
+ - type: "level"
131
+ operation: "gte"
132
+ value: "error"
133
+ ```
134
+
135
+ The text format makes it easier to read the generated rules as follows:
136
+
137
+ ```
138
+ # Slack
139
+ ## Every Occurrence
140
+ ### Rule 0
141
+ conditions:
142
+ level >= error
143
+ title contains substring "foo"
144
+
145
+ config:
146
+ channel = "alert-foo"
147
+
148
+ ### Rule 1
149
+ conditions:
150
+ level >= error
151
+ title does not contain substring "foo"
152
+ ```
153
+
154
+ ## Examples
155
+
156
+ See the YAML files in [spec/files/yaml](spec/files/yaml). Their output files are in [spec/files/tf](spec/files/tf) and [spec/files/txt](spec/files/txt).
157
+
158
+ ## Development
159
+
160
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
161
+
162
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `rollbar-notification-rules-generator.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
163
+
164
+ ## Contributing
165
+
166
+ Bug reports and pull requests are welcome on GitHub at https://github.com/abicky/rollbar-notification-rules-generator.
167
+
168
+ ## License
169
+
170
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require "optparse"
3
+
4
+ require "rollbar/notification"
5
+
6
+ FORMATS = %w[terraform text]
7
+
8
+ opts = { format: "terraform" }
9
+ opt_parser = OptionParser.new do |opt|
10
+ opt.banner = "Usage: #{File.basename(__FILE__)} [options] <input>"
11
+ opt.on("--format FORMAT", FORMATS, "#{FORMATS.join("|")} (default: #{opts[:format]})")
12
+ end
13
+
14
+ begin
15
+ opt_parser.parse!(into: opts)
16
+ rescue OptionParser::ParseError => e
17
+ $stderr.puts "Error: #{e.message}\n\n"
18
+ $stderr.puts opt_parser.help
19
+ exit 1
20
+ end
21
+
22
+ input = ARGV[0]
23
+ unless input
24
+ $stderr.puts opt_parser.help
25
+ exit 1
26
+ end
27
+
28
+ notification = Rollbar::Notification.new(input)
29
+ case opts[:format]
30
+ when "terraform"
31
+ puts notification.to_tf
32
+ when "text"
33
+ puts notification.to_s
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/trigger"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ class Channel
7
+ SUPPORTED_CHANNELS = %w[slack pagerduty]
8
+
9
+ # @param channel [String]
10
+ # @param triggers [Hash{String => Array<Hash>}]
11
+ # @param variables [Hash{String => String}]
12
+ def initialize(channel, triggers, variables)
13
+ unless SUPPORTED_CHANNELS.include?(channel)
14
+ raise ArgumentError, "Unsupported channel: #{channel}"
15
+ end
16
+
17
+ @triggers = triggers.map do |trigger, rules|
18
+ Rollbar::Notification::Trigger.new(channel, trigger, rules, variables)
19
+ end
20
+ end
21
+
22
+ # @return [String]
23
+ def to_s
24
+ @triggers.map(&:to_s).join.chomp
25
+ end
26
+
27
+ # @param provider [String]
28
+ def to_tf(provider)
29
+ @triggers.map { |t| t.to_tf(provider) }.join("\n")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rollbar
4
+ class Notification
5
+ module Condition
6
+ class Base
7
+ attr_reader :type, :operation, :value
8
+
9
+ # @param operation [String]
10
+ # @param value [String]
11
+ def initialize(operation, value)
12
+ unless self.class::SUPPORTED_OPERATIONS.include?(operation)
13
+ raise ArgumentError, "Unsupported operation: #{operation}"
14
+ end
15
+
16
+ @operation = operation
17
+ @value = value
18
+ end
19
+
20
+ def to_tf
21
+ <<~TF
22
+ filters {
23
+ type = "#{@type}"
24
+ operation = "#{@operation}"
25
+ value = "#{@value}"
26
+ }
27
+ TF
28
+ end
29
+
30
+ # @param other [Base]
31
+ # @return [Boolean]
32
+ def redundant_to?(other)
33
+ self.class == other.class &&
34
+ @operation == "neq" &&
35
+ other.operation == "eq" &&
36
+ @value != other.value
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/base"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ module Condition
7
+ class Environment < Base
8
+ SUPPORTED_OPERATIONS = %w[eq neq]
9
+
10
+ def initialize(operation, value)
11
+ super
12
+ @type = "environment"
13
+ end
14
+
15
+ # @return [String]
16
+ def to_s
17
+ "#{@type} #{@operation == "eq" ? "==" : "!="} #{@value}"
18
+ end
19
+
20
+ # @return [Environment]
21
+ def build_complement_condition
22
+ self.class.new(@operation == "eq" ? "neq" : "eq", @value)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/base"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ module Condition
7
+ class Framework < Base
8
+ SUPPORTED_OPERATIONS = %w[eq]
9
+
10
+ def initialize(operation, value)
11
+ super
12
+ @type = "framework"
13
+ end
14
+
15
+ # @return [String]
16
+ def to_s
17
+ "#{@type} #{@value}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/base"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ module Condition
7
+ class Level < Base
8
+ SUPPORTED_OPERATIONS = %w[eq gte]
9
+ SUPPORTED_VALUES = %w[debug info warning error critical]
10
+
11
+ # @param lowest_target_level [Integer]
12
+ # @return [Array<Level>]
13
+ def self.build_eq_conditions_from(lowest_target_level)
14
+ SUPPORTED_VALUES[lowest_target_level..].map do |value|
15
+ new("eq", value)
16
+ end
17
+ end
18
+
19
+ attr_reader :level
20
+
21
+ # @param operation [String]
22
+ # @param value [String]
23
+ def initialize(operation, value)
24
+ super
25
+ @type = "level"
26
+
27
+ @level = SUPPORTED_VALUES.index(value)
28
+ unless @level
29
+ raise ArgumentError, "Unsupported value: #{value}"
30
+ end
31
+ end
32
+
33
+ # @return [String]
34
+ def to_s
35
+ "#{@type} #{@operation == "eq" ? "==" : ">="} #{@value}"
36
+ end
37
+
38
+ # @return [Array<String>]
39
+ def target_level_values
40
+ @operation == "eq" ? [@value] : SUPPORTED_VALUES[@level..]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/base"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ module Condition
7
+ class Path < Base
8
+ SUPPORTED_OPERATIONS = %w[eq neq within nwithin regex nregex exists nexists]
9
+ OPERATION_TO_TEXT = {
10
+ "eq" => "==",
11
+ "neq" => "!=",
12
+ "within" => "contains substring",
13
+ "nwithin" => "does not contain substring",
14
+ "regex" => "contains substring matching regex",
15
+ "nregex" => "does not contain substring matching regex",
16
+ "exists" => "exists",
17
+ "nexists" => "does not exist",
18
+ }
19
+
20
+ attr_reader :path
21
+
22
+ # @param path [String]
23
+ # @param operation [String]
24
+ # @param value [String]
25
+ def initialize(path, operation, value)
26
+ super(operation, value)
27
+ @type = "path"
28
+ @path = path
29
+ end
30
+
31
+ def to_tf
32
+ <<~TF
33
+ filters {
34
+ type = "#{@type}"
35
+ path = "#{@path}"
36
+ operation = "#{@operation}"
37
+ value = "#{@value}"
38
+ }
39
+ TF
40
+ end
41
+
42
+ # @return [String]
43
+ def to_s
44
+ %Q{#{@type} #{@path} #{OPERATION_TO_TEXT[@operation]} "#{@value}"}
45
+ end
46
+
47
+ # @return [Path]
48
+ def build_complement_condition
49
+ new_operation = @operation.start_with?("n") ? @operation.delete_prefix("n") : "n#{@operation}"
50
+ self.class.new(@path, new_operation, @value)
51
+ end
52
+
53
+ # @return [Boolean]
54
+ def redundant_to?(other)
55
+ super && @path == other.path
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rollbar
4
+ class Notification
5
+ module Condition
6
+ class Rate
7
+ PERIOD_TO_TEXT = {
8
+ 60 => "1 minute",
9
+ 300 => "5 minutes",
10
+ 1800 => "30 minutes",
11
+ 3600 => "1 hour",
12
+ 86400 => "1 day",
13
+ }
14
+
15
+ attr_reader :type
16
+
17
+ # @param count [Integer]
18
+ # @param period [Integer]
19
+ def initialize(count, period)
20
+ @type = "rate"
21
+ @count = count
22
+ @period = period
23
+ end
24
+
25
+ # @return [String]
26
+ def to_s
27
+ "At least #{@count} occurrences within #{PERIOD_TO_TEXT[@period]}"
28
+ end
29
+
30
+ def to_tf
31
+ <<~TF
32
+ filters {
33
+ type = "#{@type}"
34
+ count = #{@count}
35
+ period = #{@period}
36
+ }
37
+ TF
38
+ end
39
+
40
+ # @param other [Base]
41
+ # @return [Boolean]
42
+ def redundant_to?(other)
43
+ false
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/base"
3
+
4
+ module Rollbar
5
+ class Notification
6
+ module Condition
7
+ class Title < Base
8
+ SUPPORTED_OPERATIONS = %w[within nwithin regex nregex]
9
+ OPERATION_TO_TEXT = {
10
+ "within" => "contains substring",
11
+ "nwithin" => "does not contain substring",
12
+ "regex" => "contains substring matching regex",
13
+ "nregex" => "does not contain substring matching regex",
14
+ }
15
+
16
+ def initialize(operation, value)
17
+ super
18
+ @type = "title"
19
+ end
20
+
21
+ # @return [String]
22
+ def to_s
23
+ %Q{#{@type} #{OPERATION_TO_TEXT[@operation]} "#{@value}"}
24
+ end
25
+
26
+ def build_complement_condition
27
+ new_operation = @operation.start_with?("n") ? @operation.delete_prefix("n") : "n#{@operation}"
28
+ self.class.new(new_operation, @value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+ require "rollbar/notification/condition/environment"
3
+ require "rollbar/notification/condition/framework"
4
+ require "rollbar/notification/condition/level"
5
+ require "rollbar/notification/condition/path"
6
+ require "rollbar/notification/condition/rate"
7
+ require "rollbar/notification/condition/title"
8
+
9
+ module Rollbar
10
+ class Notification
11
+ class Rule
12
+ attr_reader :conditions, :configs
13
+
14
+ # @param rule [Hash]
15
+ def initialize(rule)
16
+ @conditions = rule.fetch("conditions").map do |condition|
17
+ case condition.fetch("type")
18
+ when "environment"
19
+ Rollbar::Notification::Condition::Environment.new(condition.fetch("operation"), condition.fetch("value"))
20
+ when "framework"
21
+ Rollbar::Notification::Condition::Framework.new(condition.fetch("operation"), condition.fetch("value"))
22
+ when "level"
23
+ Rollbar::Notification::Condition::Level.new(condition.fetch("operation"), condition.fetch("value"))
24
+ when "path"
25
+ Rollbar::Notification::Condition::Path.new(condition.fetch("path"), condition.fetch("operation"), condition.fetch("value"))
26
+ when "rate"
27
+ Rollbar::Notification::Condition::Rate.new(condition.fetch("count"), condition.fetch("period"))
28
+ when "title"
29
+ Rollbar::Notification::Condition::Title.new(condition.fetch("operation"), condition.fetch("value"))
30
+ else
31
+ raise ArgumentError, "Unsupported condition type: #{condition.fetch("type")}"
32
+ end
33
+ end
34
+ @configs = rule.fetch("configs", [{}])
35
+ end
36
+
37
+ def initialize_dup(original)
38
+ @conditions = original.conditions.dup
39
+ remove_instance_variable(:@level_condition)
40
+ super
41
+ end
42
+
43
+ # @return [Rule]
44
+ def remove_redundant_conditions!
45
+ @conditions.delete_if do |condition|
46
+ @conditions.any? { |other| condition.redundant_to?(other) }
47
+ end
48
+ self
49
+ end
50
+
51
+ # @param old_condition [Condition::Base]
52
+ # @param new_condition [Condition::Base]
53
+ # @return [Rule]
54
+ def replace_condition!(old_condition, new_condition)
55
+ @conditions[@conditions.index(old_condition)] = new_condition
56
+ self
57
+ end
58
+
59
+ # @return [Integer]
60
+ def lowest_target_level
61
+ level_condition&.level || 0
62
+ end
63
+
64
+ # @return [String]
65
+ def lowest_target_level_value
66
+ Rollbar::Notification::Condition::Level::SUPPORTED_VALUES[lowest_target_level]
67
+ end
68
+
69
+ # Splits rules if necessary.
70
+ # Assuming the following two rules:
71
+ # level = critical, title contains substring "bar"
72
+ # level >= error, title contains substring "baz"
73
+ # the second rule will be split into two rules with "eq" operation:
74
+ # level = critical, title contains substring "bar"
75
+ # level = critical, title contains substring "baz"
76
+ # level = error, title contains substring "baz"
77
+ # whereas assuming the following two rules:
78
+ # level = warning, title contains substring "bar"
79
+ # level >= error, title contains substring "baz"
80
+ # this method doesn't split the second rule because each rule
81
+ # is already mutually exclusive.
82
+ #
83
+ # @param highest_lowest_target_level [Integer] the highest lowest_target_level
84
+ # among the preceding rules.
85
+ # @return [Array<Rule>]
86
+ def split_rules(highest_lowest_target_level)
87
+ return [dup] if level_condition&.operation == "eq" || highest_lowest_target_level <= lowest_target_level
88
+
89
+ new_level_conditions = Rollbar::Notification::Condition::Level.build_eq_conditions_from(lowest_target_level)
90
+ if level_condition
91
+ new_level_conditions.map do |condition|
92
+ dup.replace_condition!(level_condition, condition)
93
+ end
94
+ else
95
+ new_level_conditions.map do |condition|
96
+ dup.add_conditions!(condition)
97
+ end
98
+ end
99
+ end
100
+
101
+ # @param new_conditions [Condition::Base, Array<Condition::Base>]
102
+ # @return [Rule]
103
+ def add_conditions!(new_conditions)
104
+ Array(new_conditions).each do |new_condition|
105
+ @conditions << new_condition
106
+ end
107
+ self
108
+ end
109
+
110
+ # @return [Hash{String => Array<Condition::Base>}]
111
+ def build_additional_conditions_set_for_subsequent_rules
112
+ target_levels = level_condition&.target_level_values || Rollbar::Notification::Condition::Level::SUPPORTED_VALUES
113
+
114
+ conditions_with_complement = @conditions.select { |c| c.respond_to?(:build_complement_condition) }
115
+ return {} if conditions_with_complement.empty?
116
+
117
+ if conditions_with_complement.size == 1
118
+ additional_conditions = [[conditions_with_complement.first.build_complement_condition]]
119
+ else
120
+ # [cond1, cond2, cond3]
121
+ # => [
122
+ # [cond1, cond2, not-cond3],
123
+ # [cond1, not-cond2, cond3],
124
+ # [cond1, not-cond2, not-cond3],
125
+ # [not-cond1, cond2, cond3],
126
+ # [not-cond1, cond2, not-cond3],
127
+ # [not-cond1, not-cond2, cond3],
128
+ # [not-cond1, not-cond2, not-cond3],
129
+ # ]
130
+ additional_conditions = conditions_with_complement.map do |condition|
131
+ [condition, condition.build_complement_condition]
132
+ end.reduce(&:product).map(&:flatten) - [conditions_with_complement]
133
+ end
134
+ target_levels.zip([additional_conditions].cycle).to_h
135
+ end
136
+
137
+ private
138
+
139
+ def level_condition
140
+ return @level_condition if defined?(@level_condition)
141
+ @level_condition = @conditions.find { |c| c.type == "level" }
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ require "erb"
3
+
4
+ require "rollbar/notification/rule"
5
+
6
+ module Rollbar
7
+ class Notification
8
+ class Trigger
9
+ TRIGGER_TO_TEXT = {
10
+ "deploy" => "Deploy",
11
+ "exp_repeat_item" => "10^nth Occurrence",
12
+ "occurrence_rate" => "High Occurrence Rate",
13
+ "new_item" => "New Item",
14
+ "occurrence" => "Every Occurrence",
15
+ "reactivated_item" => "Item Reactivated",
16
+ "reopened_item" => "Item Reopened",
17
+ "resolved_item" => "Item Resolved",
18
+ }
19
+
20
+ TEXT_TEMPLATE = ERB.new(<<~TEXT)
21
+ conditions:
22
+ <%= conditions.map { |condition| condition.to_s.gsub(/^/, " ") }.join("\n").chomp %><% unless config.empty? %><% max_key_len = config.keys.map(&:size).max %>
23
+
24
+ config:
25
+ <%= config.compact.map { |key, value| ' %-*s = %s' % [max_key_len, key, value.inspect] }.join("\n") %><% end %>
26
+ TEXT
27
+
28
+ TF_TEMPLATE = ERB.new(<<~TF)
29
+ resource "rollbar_notification" "<%= resource_name %>" {<% if provider %>
30
+ provider = <%= provider %>
31
+ <% end %>
32
+ channel = "<%= channel %>"
33
+
34
+ rule {
35
+ trigger = "<%= trigger %>"
36
+ <%= conditions.map { |condition| condition.to_tf.gsub(/^/, " ") }.join.chomp %>
37
+ }<% unless config.empty? %><% max_key_len = config.keys.map(&:size).max %>
38
+ config {
39
+ <%= config.compact.map { |key, value| ' %-*s = %s' % [max_key_len, key, value.inspect] }.join("\n") %>
40
+ }<% end %>
41
+ }
42
+ TF
43
+
44
+ # @param channel [String]
45
+ # @param name [String]
46
+ # @param rules [Array<Hash>]
47
+ def initialize(channel, name, rules, variables)
48
+ @channel = channel
49
+ @name = name
50
+ @rules = rules.map do |rule|
51
+ Rollbar::Notification::Rule.new(rule)
52
+ end
53
+ @variables = variables
54
+ end
55
+
56
+ def to_s
57
+ str = +"## #{TRIGGER_TO_TEXT.fetch(@name)}\n"
58
+ i = -1
59
+ build_mutually_exclusive_rules.each do |rule|
60
+ rule.configs.map do |config|
61
+ i += 1
62
+ str << "### Rule #{i}\n"
63
+ str << TEXT_TEMPLATE.result_with_hash({
64
+ conditions: rule.conditions,
65
+ config: config,
66
+ }).gsub(/\${{\s*var\.(\w+)\s*}}/) { @variables.fetch($1) }
67
+ end
68
+ str << "\n"
69
+ end
70
+
71
+ str
72
+ end
73
+
74
+ def to_tf(provider)
75
+ i = -1
76
+ build_mutually_exclusive_rules.flat_map do |rule|
77
+ rule.configs.map do |config|
78
+ i += 1
79
+ TF_TEMPLATE.result_with_hash({
80
+ resource_name: "#{@channel}_#{@name}_#{i}",
81
+ provider: provider,
82
+ channel: @channel,
83
+ trigger: @name,
84
+ conditions: rule.conditions,
85
+ config: config,
86
+ }).gsub(/\${{\s*var\.(\w+)\s*}}/) { @variables.fetch($1) }
87
+ end
88
+ end.join("\n")
89
+ end
90
+
91
+ private
92
+
93
+ # @return [Array<Rule>]
94
+ def build_mutually_exclusive_rules
95
+ new_rules = []
96
+ level_value_to_additional_conditions_set = Hash.new([])
97
+ highest_lowest_target_level = 0
98
+ @rules.each do |rule|
99
+ rule.split_rules(highest_lowest_target_level).each do |new_rule|
100
+ additional_conditions_set = level_value_to_additional_conditions_set[new_rule.lowest_target_level_value]
101
+ if additional_conditions_set.empty?
102
+ new_rules << new_rule
103
+ else
104
+ additional_conditions_set.each do |additional_conditions|
105
+ new_rules << new_rule.dup
106
+ .add_conditions!(additional_conditions)
107
+ .remove_redundant_conditions!
108
+ end
109
+ end
110
+ end
111
+
112
+ lowest_target_level = rule.lowest_target_level
113
+ if lowest_target_level > highest_lowest_target_level
114
+ highest_lowest_target_level = lowest_target_level
115
+ end
116
+ level_value_to_additional_conditions_set.merge!(rule.build_additional_conditions_set_for_subsequent_rules) do |_, v1, v2|
117
+ v1.product(v2).map(&:flatten)
118
+ end
119
+ end
120
+
121
+ new_rules
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require "yaml"
3
+
4
+ require "rollbar/notification/channel"
5
+
6
+ module Rollbar
7
+ class Notification
8
+ CHANNEL_TO_TEXT = {
9
+ "slack" => "Slack",
10
+ "pagerduty" => "PagerDuty",
11
+ }
12
+
13
+ # @param config_file [String]
14
+ def initialize(config_file)
15
+ @config = YAML.load_file(config_file)
16
+ @channel = Rollbar::Notification::Channel.new(
17
+ @config.fetch("channel"),
18
+ @config.fetch("triggers"),
19
+ @config.fetch("variables", {})
20
+ )
21
+ end
22
+
23
+ # @return [String]
24
+ def to_s
25
+ "# #{CHANNEL_TO_TEXT.fetch(@config.fetch("channel"))}\n#{@channel.to_s}"
26
+ end
27
+
28
+ # @return [String]
29
+ def to_tf
30
+ @channel.to_tf(@config["terraform_provider"])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rollbar-notification-rules-generator"
5
+ spec.version = "0.2.0"
6
+ spec.authors = ["abicky"]
7
+ spec.email = ["takeshi.arabiki@gmail.com"]
8
+
9
+ spec.summary = "A CLI tool that generates Rollbar notification rules that are mutually exclusive from a simple YAML file"
10
+ spec.description = "This tool generates Rollbar notification rules that are mutually exclusive from a simple YAML file."
11
+
12
+ spec.homepage = "https://github.com/abicky/rollbar-notification-rules-generator"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/abicky/rollbar-notification-rules-generator"
18
+
19
+ spec.files = Dir.chdir(__dir__) do
20
+ `git ls-files -z`.split("\x0").reject do |f|
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
+ end
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rollbar-notification-rules-generator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - abicky
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-12-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This tool generates Rollbar notification rules that are mutually exclusive
14
+ from a simple YAML file.
15
+ email:
16
+ - takeshi.arabiki@gmail.com
17
+ executables:
18
+ - rollbar-notification-rules-generator
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rspec"
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - exe/rollbar-notification-rules-generator
28
+ - lib/rollbar/notification.rb
29
+ - lib/rollbar/notification/channel.rb
30
+ - lib/rollbar/notification/condition/base.rb
31
+ - lib/rollbar/notification/condition/environment.rb
32
+ - lib/rollbar/notification/condition/framework.rb
33
+ - lib/rollbar/notification/condition/level.rb
34
+ - lib/rollbar/notification/condition/path.rb
35
+ - lib/rollbar/notification/condition/rate.rb
36
+ - lib/rollbar/notification/condition/title.rb
37
+ - lib/rollbar/notification/rule.rb
38
+ - lib/rollbar/notification/trigger.rb
39
+ - rollbar-notification-rules-generator.gemspec
40
+ homepage: https://github.com/abicky/rollbar-notification-rules-generator
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/abicky/rollbar-notification-rules-generator
45
+ source_code_uri: https://github.com/abicky/rollbar-notification-rules-generator
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.6.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.3.26
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: A CLI tool that generates Rollbar notification rules that are mutually exclusive
65
+ from a simple YAML file
66
+ test_files: []