rollbar-notification-rules-generator 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []