slackert 0.1.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: 69f9315335d1406edccde04e2ae402ef7f27b20a153593546415f9632c9b236a
4
+ data.tar.gz: abda8738fba7ea3a95b8b91742853893b9efde5e23394e105bec6ce02f8bff3e
5
+ SHA512:
6
+ metadata.gz: fdd1c3ef7813a16917768a2997bae3171eec7854cac41f14997a76022b511caf0efdf9ba233800f77292a8ca2ef66f0f90a19efc64a04689ee42a0dc2202a4fb
7
+ data.tar.gz: 3ea15b1ba76fac9a88c6e94656e03a266d27e9c651d2b8dfdcb8d95a9dfb481ceaa7f03ce772947369e432a1b9fd5bf56f75811ecfc62082c80c558237107569
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ .DS_Store
11
+ *.gem
12
+ Gemfile.lock
13
+
14
+ .vscode/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Braze
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Slackert
2
+
3
+ A simple way to create and send Slack messages through a webhook.
4
+
5
+ Especially useful for logging and alerting, supports three logging levels `Error, Info, Debug` and templates to simplify sending the same layout message with different content.
6
+
7
+ ## Installation
8
+
9
+ Add Slacker to your Gemfile
10
+
11
+ gem 'slackert', git: git@github.com:braze-inc/braze-growth-shares-slackert.git
12
+
13
+ Then
14
+
15
+ $ bundle install
16
+
17
+ <!-- TODO: Once posted to rubygems -->
18
+ <!-- Or install it yourself as:
19
+
20
+ $ gem install slackert -->
21
+
22
+ ## Usage
23
+
24
+ Slackert sends messages through an Incoming Webhook. To add a webhook to your channel, follow [this guide.](https://api.slack.com/messaging/webhooks)
25
+
26
+ Initialize alert client that will handle sending out your messages:
27
+
28
+ alerts = Slackert::Alerter.new(<WebhookURL>)
29
+
30
+ Create a message with a message builder:
31
+
32
+ message_builder = Slackert::MessageBuilder.new
33
+ message_builder.add_header('Message Title')
34
+ message_builder.add_plain_text('Description of the message.)
35
+ message = message_builder.build
36
+
37
+ And send it out:
38
+
39
+ alerts.info(message)
40
+
41
+ For more complex layouts, use `Blocks`.
42
+
43
+ ### Message Building
44
+
45
+ We can also use a block initialization to simplify the process of creating a message
46
+
47
+ message = Slackert::MessageBuilder.build do |msg|
48
+ msg.add_header("Header)
49
+ msg.add_markdown_text("This is some *bold* text while _this_ is italics. :thumbsup:")
50
+ msg.add_divider
51
+
52
+ msg.notifiy_users(
53
+ ['slackMemberID1', 'slackMemberID2'],
54
+ msg_prefix = 'Look at my message guys, '
55
+ )
56
+
57
+ Include any emojis, styling you want. Go wild!
58
+
59
+ ### Blocks
60
+
61
+ Slack messages use blocks to group and style messages. Blocks allow to include pictures, buttons,
62
+ interactivity and more.
63
+
64
+ The blocks currently supported by slackert: `Divider, Header, Section`
65
+
66
+ You can add a divider or a header directly from the builder:
67
+
68
+ builder.add_header('Header Text')
69
+
70
+ builder.add_divider
71
+
72
+ In order to compose a section block, use `Slackert::Blocks::Section`
73
+
74
+ stats_section = Slackert::Blocks::Section.new
75
+
76
+ # Add section text - description like text, limited to only one per section
77
+ stats_section.add_section_text("The script executed with the following output:")
78
+
79
+ # Add field text - it is like a cell in an invisible 2-column grid on Desktop and 1-column grid one Mobile
80
+ stats_section.add_field_text("*Processed*:\n12345")
81
+ stats_section.add_field_text("*Failed Records*:\n3")
82
+
83
+ Then simply add it to the builder:
84
+
85
+ builder.add_section(stats_section)
86
+
87
+ We can also create a section straight from a hash of values, which simplifies creating sections for key: value type of data.
88
+
89
+ values = {
90
+ 'Rows': 123,
91
+ 'Tables': 2
92
+ }
93
+ section = Slackert::Blocks::Section.new_from_hash(values, bold_keys: true)
94
+
95
+ ### Templates
96
+
97
+ Templates optimize the process of creating and sending alerts by providing layouts and an easy way to fill their content. As an example, to send a Slack message when a job finishes processing:
98
+
99
+ message = Slackert::Templates.job_finish(
100
+ title: "My Job Title",
101
+ desc: "The job is responsible for some real heavy work.",
102
+ result: "OK :thumbsup:",
103
+ stats: {
104
+ "Processed Rows": 10987,
105
+ "Updated Rows": 89,
106
+ "Deleted Rows": 0,
107
+ "Errors": 0
108
+ }
109
+ )
110
+
111
+ alerts.info(message)
112
+
113
+ #### Examples
114
+
115
+ #### Job Executed
116
+ <br>
117
+ <img src="./screenshots/job-executed.png" width="600" height="309">
118
+ <br>
119
+
120
+ #### Job Error
121
+ <br>
122
+ <img src="./screenshots/job-error.png" width="600" height="281">
123
+ <br>
124
+
125
+ ### Logging Level
126
+
127
+ To change the logging/reporting level, simply set `Slackert.level`.
128
+ For example, to only report errors:
129
+
130
+ Slackert.level = Slackert::Level::ERROR
131
+
132
+ By default, slackert starts with level `INFO`
133
+
134
+ <!-- ## Development
135
+
136
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
137
+
138
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, 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). -->
139
+
140
+ <!-- ## Contributing
141
+
142
+ Bug reports and pull requests are welcome on GitHub at # repo link -->
143
+
144
+ # License
145
+
146
+ MIT. See [LICENSE](LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/bundle ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../../Gemfile", __FILE__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_version
64
+ @bundler_version ||=
65
+ env_var_version || cli_arg_version ||
66
+ lockfile_version
67
+ end
68
+
69
+ def bundler_requirement
70
+ return "#{Gem::Requirement.default}.a" unless bundler_version
71
+
72
+ bundler_gem_version = Gem::Version.new(bundler_version)
73
+
74
+ requirement = bundler_gem_version.approximate_recommendation
75
+
76
+ return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
77
+
78
+ requirement += ".a" if bundler_gem_version.prerelease?
79
+
80
+ requirement
81
+ end
82
+
83
+ def load_bundler!
84
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
85
+
86
+ activate_bundler
87
+ end
88
+
89
+ def activate_bundler
90
+ gem_error = activation_error_handling do
91
+ gem "bundler", bundler_requirement
92
+ end
93
+ return if gem_error.nil?
94
+ require_error = activation_error_handling do
95
+ require "bundler/version"
96
+ end
97
+ return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99
+ exit 42
100
+ end
101
+
102
+ def activation_error_handling
103
+ yield
104
+ nil
105
+ rescue StandardError, LoadError => e
106
+ e
107
+ end
108
+ end
109
+
110
+ m.load_bundler!
111
+
112
+ if m.invoked_as_script?
113
+ load Gem.bin_path("bundler", "bundle")
114
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "slackert"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/htmldiff ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'htmldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "htmldiff")
data/bin/ldiff ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "ldiff")
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/slackert.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'slackert/alerter'
2
+ require 'slackert/level'
3
+ require 'slackert/builder'
4
+ require 'slackert/blocks'
5
+ require 'slackert/templates'
6
+
7
+ # Namespace for classes and modules that handle creation and delivery of Slack messages and alerts
8
+ module Slackert
9
+ @level = Level::INFO
10
+
11
+ # Sets logging level for messages. Logging level constants are defined in {Slackert::Level}
12
+ #
13
+ # @param level [Number] logging level
14
+ # @raise [ArgumentError] if the logging level is out of bounds
15
+ #
16
+ def self.level=(value)
17
+ log_values = Level.constants.map { |const| Level.const_get(const) }
18
+ min, max = log_values.minmax
19
+ raise ArgumentError, 'Invalid logging level' if value < min || value > max
20
+
21
+ @level = value
22
+ end
23
+
24
+ # Return current logging level
25
+ # @return [Number] current level
26
+ #
27
+ def self.level
28
+ @level
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/https'
5
+ require 'json'
6
+
7
+ module Slackert
8
+ # Slack client responsible for sending composed Slack messages
9
+ # @param [String] Slack incoming webhook URL to a particular channel
10
+ #
11
+ class Alerter
12
+ def initialize(webhook_url)
13
+ @uri = URI.parse(webhook_url)
14
+ @https = configure_https
15
+ end
16
+
17
+ # Sends a debug Slack message if logging level is set at Level::DEBUG
18
+ # @param [Hash] Slack message
19
+ #
20
+ def debug(content)
21
+ return if Slackert.level < Level::DEBUG
22
+
23
+ post_to_slack(content)
24
+ end
25
+
26
+ # Sends an info Slack message if loggin level is set at Level::INFO or lower
27
+ # @param [Hash] Slack message
28
+ #
29
+ def info(content)
30
+ return if Slackert.level < Level::INFO
31
+
32
+ post_to_slack(content)
33
+ end
34
+
35
+ # Sends an error Slack message
36
+ # @param [Hash] Slack message
37
+ #
38
+ def error(content)
39
+ post_to_slack(content)
40
+ end
41
+
42
+ private
43
+
44
+ def configure_https
45
+ https = Net::HTTP.new(@uri.host, @uri.port)
46
+ https.use_ssl = true
47
+ https
48
+ end
49
+
50
+ def post_to_slack(content)
51
+ raise 'Message content cannot be empty.' if content.empty?
52
+
53
+ req = base_post_req
54
+ req.body = content.to_json
55
+ res = @https.request(req)
56
+ puts "Message sending unsuccesful (Code: #{res.code}, Message: #{res.message})" if res.code != '200'
57
+ end
58
+
59
+ def base_post_req
60
+ req = Net::HTTP::Post.new(@uri)
61
+ req['Content-type'] = 'application/json'
62
+ req
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackert
4
+ # Block elements that a Slack message can be composed of.
5
+ module Blocks
6
+ # Abstract BlockElement.
7
+ # Subclass and override +to_slack+ to add a new block element.
8
+ class BlockElement
9
+ def initialize(type)
10
+ @type = type
11
+ end
12
+
13
+ # Returns a hash of the element, ready to be added to the block formatted message.
14
+ # Abstract method that needs to be implemented in each subclassed element.
15
+ #
16
+ def to_slack
17
+ raise NoMethodError, 'Override this implementation'
18
+ end
19
+ end
20
+
21
+ # Header block element provides a title, rendered as a larger bold text on top of the message.
22
+ class Header < BlockElement
23
+ # @param text [String] header text
24
+ def initialize(text)
25
+ super('header')
26
+ @text = text
27
+ end
28
+
29
+ # See {BlockElement#to_slack}
30
+ def to_slack
31
+ {
32
+ 'type': @type,
33
+ 'text': {
34
+ 'type': 'plain_text',
35
+ 'text': @text
36
+ }
37
+ }
38
+ end
39
+ end
40
+
41
+ # Divider block element provides a horizontal separator, simlarly to HTML's +<hr>+
42
+ class Divider < BlockElement
43
+ def initialize
44
+ super('divider')
45
+ end
46
+
47
+ # See {BlockElement#to_slack}
48
+ def to_slack
49
+ {
50
+ 'type': @type
51
+ }
52
+ end
53
+ end
54
+
55
+ # Section block element is a very flexible layout block that can serve as a simple text block but it also allows
56
+ # for adding block elements such as field text, buttons, images and more. Currently only section text and field
57
+ # texts are supported.
58
+ #
59
+ # To learn more, visit https://api.slack.com/reference/block-kit/blocks#section
60
+ #
61
+ class Section < BlockElement
62
+ def initialize
63
+ @text = {}
64
+ @fields = []
65
+ super('section')
66
+ end
67
+
68
+ # Initialize field text objects from a hash.
69
+ # @param [Hash] key, value pairs that each will become a field text object
70
+ # @param line_break [Boolean] add a line break after each key so values render right under the key
71
+ # instead of next to it
72
+ # @param bold_keys [Boolean] apply bold text formatting to the keys
73
+ # @return [Section]
74
+ def self.new_from_hash(values, line_break: true, bold_keys: true)
75
+ s = new
76
+
77
+ values.each do |key, value|
78
+ title = bold_keys ? "*#{key}*" : key
79
+ title = line_break ? "#{title}\n" : title
80
+ s.add_field_text("#{title}#{value}")
81
+ end
82
+
83
+ s
84
+ end
85
+
86
+ # Adds a text object on top of the section. There can only be one section text added. Adding more will replace
87
+ # the previously added section text. It is limited to 3000 characters.
88
+ # @param message [String] section text message
89
+ # @param type [String] can be either mrkdwn or plain_text
90
+ #
91
+ def add_section_text(message, type = 'mrkdwn')
92
+ @text = {
93
+ 'type': type,
94
+ 'text': message
95
+ }
96
+ end
97
+
98
+ # Adds a field text object to the message. Field texts render in two columns on desktop and are added left
99
+ # to right. They show as one column on mobile.
100
+ # There can only be 10 fields added in total and each text item has a limit of 2000 characters.
101
+ # @param message [String] field text message
102
+ # @param type [String] can be either mrkdwn or plain_text
103
+ # @raise [RuntimeError] if maximum capacity of 10 field objects has been reached
104
+ #
105
+ def add_field_text(message, type = 'mrkdwn')
106
+ raise 'Maximum field text objects has been reached.' if @fields.length == 10
107
+
108
+ @fields.push(
109
+ {
110
+ 'type': type,
111
+ 'text': message
112
+ }
113
+ )
114
+ end
115
+
116
+ # See {BlockElement#to_slack}
117
+ def to_slack
118
+ if @text.empty? && @fields.empty?
119
+ raise 'Either section text or field text needs to be filled in order to compose the section.'
120
+ end
121
+
122
+ section = { 'type': @type }
123
+ section['text'] = @text unless @text.empty?
124
+ section['fields'] = @fields unless @fields.empty?
125
+ section
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'slackert/blocks'
4
+
5
+ module Slackert
6
+ # Builder class that allows to build a Slack message piece by piece.
7
+ # After the message has been constructed, it is built with ++builder.build++ method and can be passed to Alerter
8
+ # class for sending.
9
+ #
10
+ # Supports blocks initialization that returns a ready alert/message
11
+ #
12
+ # msg = Slackert::MessageBuilder.build do |b|
13
+ # b.add_header('header')
14
+ # ...
15
+ # end
16
+ #
17
+ class MessageBuilder
18
+ # Block initialization
19
+ # @return [Hash] built message
20
+ #
21
+ def self.build
22
+ builder = new
23
+ yield(builder)
24
+ builder.build
25
+ end
26
+
27
+ def initialize
28
+ @content = []
29
+ end
30
+
31
+ # Add a header to the message. It formats as a large title on top of the message.
32
+ # @param text [String] header text, plain text only
33
+ #
34
+ def add_header(text)
35
+ return if text.empty?
36
+
37
+ @content.push(Blocks::Header.new(text).to_slack)
38
+ end
39
+
40
+ # Adds a horizontal divider.
41
+ #
42
+ def add_divider
43
+ @content.push(Blocks::Divider.new.to_slack)
44
+ end
45
+
46
+ # Adds markdown text to the message.
47
+ # @param text [String] markdown text
48
+ #
49
+ def add_markdown_text(text)
50
+ return if text.empty?
51
+
52
+ mkd_section = Blocks::Section.new
53
+ mkd_section.add_field_text(text)
54
+ add_section(mkd_section)
55
+ end
56
+
57
+ # Adds plain text to the message.
58
+ # @param text [String] plain text
59
+ #
60
+ def add_plain_text(text)
61
+ return if text.empty?
62
+
63
+ text_section = Blocks::Section.new
64
+ text_section.add_section_text(text)
65
+ add_section(text_section)
66
+ end
67
+
68
+ # Adds Slack user notifications.
69
+ # @param user_ids [Array<String>] Slack member IDs
70
+ # @param msg_prefix [String] message added inline before the notifications
71
+ # @param delim [String] delimiter to separate Slack users
72
+ #
73
+ def notify_users(user_ids, msg_prefix = 'Please look into this: ', delim = ' | ')
74
+ return if user_ids.empty?
75
+
76
+ tag_section = Blocks::Section.new
77
+ user_notifs = user_ids.map { |user_id| "<@#{user_id}>" }
78
+ notifs_msg = user_notifs.join(delim)
79
+ tag_section.add_section_text(msg_prefix + notifs_msg)
80
+ add_section(tag_section)
81
+ end
82
+
83
+ # Adds a {Blocks::Section} block element to the message.
84
+ # @param section [Blocks::Section] section block
85
+ #
86
+ def add_section(section)
87
+ @content.push(section.to_slack)
88
+ end
89
+
90
+ # Inserts a {Blocks::Section} block element on top of the message.
91
+ # @param section [Blocks::Section] section block
92
+ #
93
+ def prepend_section(section)
94
+ @content.unshift(section.to_slack)
95
+ end
96
+
97
+ # Builds a message. Creates a hash object from the added fields in the builder.
98
+ # @return [Hash] Slack message
99
+ #
100
+ def build
101
+ {
102
+ 'blocks': @content
103
+ }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackert
4
+ # Logging level constants for Slackert
5
+ module Level
6
+ ERROR = 0
7
+ INFO = 1
8
+ DEBUG = 2
9
+ end
10
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'slackert/blocks'
4
+
5
+ module Slackert
6
+ # Pre-defined Slack message templates that you can use for a quick message layout
7
+ # instead of building one from scratch. Most of the templates are customizable and you can
8
+ # include or exclude fields like title, description or extra data that will appear in the message.
9
+ #
10
+ # Example Usage
11
+ #
12
+ # message = Slackert::Templates.job_start(
13
+ # title: 'Job Title',
14
+ # desc: 'This job does this and that',
15
+ # overview: {
16
+ # 'Job Type': 'Refresh',
17
+ # 'Action': 'Update',
18
+ # 'Start Time': Time.now.strftime("%Y/%m/%d %H:%M:%S")
19
+ # }
20
+ # )
21
+ #
22
+ # alerts = Slackert::Alerter.new('mywebhook')
23
+ # alerts.info(message)
24
+ #
25
+ module Templates
26
+ # Message layout for a quick notification.
27
+ #
28
+ # @param title [String] optional title
29
+ # @param text [String] notification text, accepts markdown
30
+ #
31
+ # @return [Hash] Slack message
32
+ #
33
+ def self.notification(text:, title: '')
34
+ MessageBuilder.build do |alert|
35
+ alert.add_header(title) unless title.empty?
36
+ alert.add_markdown_text(text)
37
+ end
38
+ end
39
+
40
+ # Message layout to notify of a job start.
41
+ #
42
+ # @param title [String] title of the message, plain text only
43
+ # @param desc [String] description of the message, accepts markdown
44
+ # @param overview [Hash] extra identifiable key: value section fields to be included in the message,
45
+ # both keys and values accept markdown and keys are bold by default
46
+ # @return [Hash] Slack message
47
+ #
48
+ def self.job_start(
49
+ title: '',
50
+ desc: '',
51
+ overview: {}
52
+ )
53
+ MessageBuilder.build do |alert|
54
+ alert.add_header(title) unless title.empty?
55
+ alert.add_plain_text(desc) unless desc.empty?
56
+ add_section_from_hash(alert, overview) unless overview.empty?
57
+ end
58
+ end
59
+
60
+ # Message layout to notify of a job finish.
61
+ #
62
+ # @param title [String] title of the message, plain text only
63
+ # @param desc [String] description of the message, accepts markdown
64
+ # @param result [String] result of the job, accepts markdown
65
+ # @param stats [Hash] extra execution key: value section fields to be included in the message,
66
+ # both keys and values accept markdown and keys are bold by default
67
+ # @return [Hash] Slack message
68
+ #
69
+ def self.job_finish(
70
+ title: '',
71
+ desc: '',
72
+ result: '',
73
+ stats: {}
74
+ )
75
+ MessageBuilder.build do |alert|
76
+ alert.add_header(title)
77
+ alert.add_plain_text(desc)
78
+ alert.add_markdown_text("*Result*: #{result}")
79
+ add_section_from_hash(alert, stats) unless stats.empty?
80
+ end
81
+ end
82
+
83
+ # Combines both job start and job finish into a single message. Best for quick jobs where a separate start
84
+ # and finish alerts are unnecessary.
85
+ #
86
+ # @param title [String] title of the message, plain text only
87
+ # @param desc [String] description of the message, accepts markdown
88
+ # @param result [String] result of the job, accepts markdown
89
+ # @param overview [Hash] a section of key: value fields, both keys and values accept markdown
90
+ # and keys are bold by default
91
+ # @param stats [Hash] a separate section of key: value fields, both keys and values accept markdown
92
+ # and keys are bold by default
93
+ # @return [Hash] Slack message
94
+ #
95
+ def self.job_executed(
96
+ title: '',
97
+ desc: '',
98
+ result: '',
99
+ overview: {},
100
+ stats: {}
101
+ )
102
+ MessageBuilder.build do |alert|
103
+ alert.add_header(title) unless title.empty?
104
+ alert.add_plain_text(desc) unless desc.empty?
105
+ alert.add_markdown_text("*Result*: #{result}") unless result.empty?
106
+ add_section_from_hash(alert, overview) unless overview.empty?
107
+ add_section_from_hash(alert, stats) unless stats.empty?
108
+ end
109
+ end
110
+
111
+ # Message on job error that notifies specified users by tagging them in the alert.
112
+ #
113
+ # @param title [String] title of the job/alert, plain text only
114
+ # @param error [String] error message
115
+ # @param notify_user_ids Array<String> Slack member IDs that will be tagged and notified
116
+ # @param extra [Hash] additional section of key: value fields, both accept markdown and keys are bold by default
117
+ # @param add_alert_emoji [Boolean] adds rotating light emoji in the title
118
+ #
119
+ # @return [Hash] Slack message
120
+ #
121
+ def self.job_error(
122
+ title:,
123
+ error:,
124
+ notify_user_ids: [],
125
+ extra: {},
126
+ add_alert_emoji: true
127
+ )
128
+ MessageBuilder.build do |alert|
129
+ header = "Error while processing #{title}"
130
+ header = add_alert_emoji ? ":rotating_light: #{header}" : header
131
+
132
+ alert.add_header(header)
133
+ alert.notify_users(notify_user_ids) unless notify_user_ids.empty?
134
+ alert.add_divider
135
+ alert.add_markdown_text('*Result*: Fail')
136
+ alert.add_markdown_text("*Error Output*:\n```#{error}```")
137
+ add_section_from_hash(alert, extra) unless extra.empty?
138
+ end
139
+ end
140
+
141
+ def self.add_section_from_hash(alert, values)
142
+ alert.add_divider
143
+ section = Blocks::Section.new_from_hash(values, bold_keys: true)
144
+ alert.add_section(section)
145
+ end
146
+
147
+ private_class_method :add_section_from_hash
148
+ end
149
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slackert
4
+ VERSION = "0.1.0"
5
+ end
Binary file
Binary file
data/slackert.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/slackert/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'slackert'
7
+ spec.version = Slackert::VERSION
8
+ spec.authors = ['Maciej Olko']
9
+ spec.summary = 'Quick and simple way to send message through Slack webhook.'
10
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
11
+ spec.homepage = 'https://github.com/braze-inc/braze-growth-shares-slackert'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
15
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
16
+ end
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'rspec', '~> 3.10.0'
22
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slackert
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maciej Olko
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.10.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.10.0
27
+ description:
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".gitignore"
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - bin/bundle
39
+ - bin/console
40
+ - bin/htmldiff
41
+ - bin/ldiff
42
+ - bin/rake
43
+ - bin/rspec
44
+ - bin/setup
45
+ - lib/slackert.rb
46
+ - lib/slackert/alerter.rb
47
+ - lib/slackert/blocks.rb
48
+ - lib/slackert/builder.rb
49
+ - lib/slackert/level.rb
50
+ - lib/slackert/templates.rb
51
+ - lib/slackert/version.rb
52
+ - screenshots/job-error.png
53
+ - screenshots/job-executed.png
54
+ - slackert.gemspec
55
+ homepage: https://github.com/braze-inc/braze-growth-shares-slackert
56
+ licenses:
57
+ - MIT
58
+ metadata: {}
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.4.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.0.9
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Quick and simple way to send message through Slack webhook.
78
+ test_files: []