fluent-plugin-slack 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b835ec9f32cc25b75bd4e0a18d7aa52097ff61e
4
- data.tar.gz: 21a5be237380dc845f1ead2dad7e547500d88c19
3
+ metadata.gz: c08f44114d6d2335cb8cee39f0053e79000270bc
4
+ data.tar.gz: 25186d53c993522e99e92acfb479859d0e6e265d
5
5
  SHA512:
6
- metadata.gz: 7e36906812d93f8887a0994209c7efecf432b71e85e75cbc3b0fa9e7660c2dea8f04fc0eba411a35a11671d28c9d55ed646e153e408e71f5b9ffe79934b42578
7
- data.tar.gz: 7090cf28cbf42af855170144a1c9b446837704f3f84d5c6340a9e9b1d3d86b9dce7f50a76c90d3773da22dbfa9cc68491404b86f94f35c5aebb2afc3738e79c2
6
+ metadata.gz: 9e454cae0012869146257e6c1160b739a4620f27966eeb192991b677d7243de00d24516146de16ca38be2971bd403f7d31197d1fc9f24f54d162ab64c968932c
7
+ data.tar.gz: 1a277e5b7ad062af8e25618b7742c680a3c9878ef36f28f4a7ad47a2a782861c909930251dcf0a699890885a95eb331e31fc27158ff5f06ddf003f514bdc91fa
data/.gitignore CHANGED
@@ -3,3 +3,6 @@
3
3
  /coverage/
4
4
  /vendor/
5
5
  Gemfile.lock
6
+ tmp/
7
+ .ruby-version
8
+ .env
@@ -0,0 +1,87 @@
1
+ # Fluent event to slack plugin.
2
+
3
+ # Installation
4
+
5
+ ```
6
+ $ fluent-gem install fluent-plugin-slack
7
+ ```
8
+
9
+ # Usage (Incoming Webhook)
10
+
11
+ ```apache
12
+ <match slack>
13
+ type slack
14
+ webhook_url https://hooks.slack.com/services/XXX/XXX/XXX
15
+ channel general
16
+ username sowasowa
17
+ color good
18
+ icon_emoji :ghost:
19
+ flush_interval 60s
20
+ </match>
21
+ ```
22
+
23
+ ```ruby
24
+ fluent_logger.post('slack', {
25
+ :message => 'Hello<br>World!'
26
+ })
27
+ ```
28
+
29
+ # Usage (Slack API)
30
+
31
+ ```apache
32
+ <match slack>
33
+ type slack
34
+ token xoxb-XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX
35
+ channel general
36
+ username sowasowa
37
+ color good
38
+ icon_emoji :ghost:
39
+ flush_interval 60s
40
+ </match>
41
+ ```
42
+
43
+ ```ruby
44
+ fluent_logger.post('slack', {
45
+ :message => 'Hello<br>World!'
46
+ })
47
+ ```
48
+
49
+ ### Parameter
50
+
51
+ |parameter|description|default|
52
+ |---|---|---|
53
+ |webhook_uri|Incoming Webhook URI (Required for Incoming Webhook mode)||
54
+ |token|Token for Slack API (Required for Slack API mode)||
55
+ |username|name of bot|fluentd|
56
+ |color|color to use|good|
57
+ |icon_emoji|emoji to use as the icon|`:question:`|
58
+ |channel|channel to send messages (without first '#')||
59
+ |channel_keys|keys used to format channel. %s will be replaced with value specified by channel_keys if this option is used|nil|
60
+ |title|title format. %s will be replaced with value specified by title_keys. title is created from the first appeared record on each tag|nil|
61
+ |title_keys|keys used to format the title|nil|
62
+ |message|message format. %s will be replaced with value specified by message_keys|%s|
63
+ |message_keys|keys used to format messages|message|
64
+
65
+ `fluent-plugin-slack` uses `SetTimeKeyMixin` and `SetTagKeyMixin`, so you can also use:
66
+
67
+ |parameter|description|default|
68
+ |---|---|---|
69
+ |timezone|timezone such as `Asia/Tokyo`||
70
+ |localtime|use localtime as timezone|true|
71
+ |utc|use utc as timezone||
72
+ |time_key|key name for time used in xxx_keys|time|
73
+ |time_format|time format. This will be formatted with Time#strftime.|%H:%M:%S|
74
+ |tag_key|key name for tag used in xxx_keys|tag|
75
+
76
+ `fluent-plugin-slack` is a kind of BufferedOutput plugin, so you can also use [Buffer Parameters](http://docs.fluentd.org/articles/out_exec#buffer-parameters).
77
+
78
+ # Contributors
79
+
80
+ - [@sonots](https://github.com/sonots)
81
+ - [@kenjiskywalker](https://github.com/kenjiskywalker)
82
+
83
+ # Copyright
84
+
85
+ * Copyright:: Copyright (c) 2014- Keisuke SOGAWA
86
+ * License:: Apache License, Version 2.0
87
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.5.0
@@ -16,11 +16,12 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ['lib']
17
17
 
18
18
  gem.add_dependency "fluentd", ">= 0.10.8"
19
- gem.add_dependency "activesupport", "~>3.2.0"
20
- gem.add_dependency "tzinfo", ">=0.3.38"
21
19
 
22
20
  gem.add_development_dependency "rake", ">= 10.1.1"
23
21
  gem.add_development_dependency "rr", ">= 1.0.0"
24
22
  gem.add_development_dependency "pry"
25
- gem.add_development_dependency("minitest", ["~> 4.0"])
23
+ gem.add_development_dependency "pry-nav"
24
+ gem.add_development_dependency "test-unit", "~> 3.0.2"
25
+ gem.add_development_dependency "test-unit-rr", "~> 1.0.3"
26
+ gem.add_development_dependency "dotenv"
26
27
  end
@@ -1,118 +1,199 @@
1
+ require_relative 'slack_client'
2
+
1
3
  module Fluent
2
- class BufferedSlackOutputError < StandardError; end
3
- class BufferedSlackOutput < Fluent::TimeSlicedOutput
4
- Fluent::Plugin.register_output('buffered_slack', self)
5
- config_param :api_key, :string, default: nil
6
- config_param :token, :string, default: nil
7
- config_param :team, :string, default: nil
8
- config_param :channel, :string
9
- config_param :username, :string
10
- config_param :color, :string
11
- config_param :icon_emoji, :string
12
- config_param :timezone, :string, default: nil
13
- config_param :rtm, :bool , default: false
14
- config_param :webhook_url,:string, default: nil
15
-
16
- attr_reader :slack
4
+ class SlackOutput < Fluent::BufferedOutput
5
+ Fluent::Plugin.register_output('buffered_slack', self) # old version compatiblity
6
+ Fluent::Plugin.register_output('slack', self)
17
7
 
18
- def format(tag, time, record)
19
- [tag, time, record].to_msgpack
20
- end
8
+ include SetTimeKeyMixin
9
+ include SetTagKeyMixin
21
10
 
22
- def write(chunk)
23
- messages = {}
24
- chunk.msgpack_each do |tag, time, record|
25
- messages[tag] = '' if messages[tag].nil?
26
- messages[tag] << "[#{Time.at(time).in_time_zone(@timezone).strftime("%H:%M:%S")}] #{record['message']}\n"
27
- end
28
- begin
11
+ config_set_default :include_time_key, true
12
+ config_set_default :include_tag_key, true
13
+
14
+ config_param :webhook_url, :string, default: nil # incoming webhook
15
+ config_param :token, :string, default: nil # api token
16
+ config_param :username, :string, default: 'fluentd'
17
+ config_param :color, :string, default: 'good'
18
+ config_param :icon_emoji, :string, default: ':question:'
29
19
 
30
- # https://api.slack.com/rtm
31
- if @rtm
32
- params = {
33
- :token => @token,
34
- :channel => @channel,
35
- :text => messages.values,
36
- :username => @username,
37
- :icon_emoji => @icon_emoji,
38
- :attachments => [{
39
- :color => @color,
40
- :text => messages.values
41
- }].to_json
42
- }
43
- get_request(params)
44
- else
45
- payload = {
46
- channel: @channel,
47
- username: @username,
48
- icon_emoji: @icon_emoji,
49
- attachments: [{
50
- fallback: messages.keys.join(','),
51
- color: @color,
52
- fields: messages.map{|k,v| {title: k, value: v} }
53
- }]}
54
- post_request(
55
- payload: payload.to_json
56
- )
57
- end
58
- rescue => e
59
- $log.error("Slack Error: #{e.backtrace[0]} / #{e.message}")
60
- end
20
+ config_param :channel, :string
21
+ config_param :channel_keys, default: nil do |val|
22
+ val.split(',')
23
+ end
24
+ config_param :title, :string, default: nil
25
+ config_param :title_keys, default: nil do |val|
26
+ val.split(',')
27
+ end
28
+ config_param :message, :string, default: nil
29
+ config_param :message_keys, default: nil do |val|
30
+ val.split(',')
61
31
  end
62
32
 
33
+ # for test
34
+ attr_reader :slack, :time_format, :localtime, :timef
35
+
63
36
  def initialize
64
37
  super
65
- require 'active_support/time'
66
38
  require 'uri'
67
- require 'net/http'
68
39
  end
69
40
 
70
41
  def configure(conf)
42
+ conf['time_format'] ||= '%H:%M:%S' # old version compatiblity
43
+ conf['localtime'] ||= true unless conf['utc']
44
+
71
45
  super
72
46
 
73
- @channel = URI.unescape(conf['channel'])
74
- @username = conf['username'] || 'fluentd'
75
- @color = conf['color'] || 'good'
76
- @icon_emoji = conf['icon_emoji'] || ':question:'
47
+ @channel = URI.unescape(@channel) # old version compatibility
48
+ @channel = '#' + @channel unless @channel.start_with?('#')
77
49
 
78
- if @rtm
79
- @token = conf['token']
50
+ if @webhook_url
51
+ # following default values are for old version compatibility
52
+ @title ||= '%s'
53
+ @title_keys ||= %w[tag]
54
+ @message ||= '[%s] %s'
55
+ @message_keys ||= %w[time message]
56
+ @slack = Fluent::SlackClient::IncomingWebhook.new(@webhook_url)
80
57
  else
81
- @timezone = conf['timezone'] || 'UTC'
82
- @team = conf['team']
83
- @api_key = conf['api_key']
84
- @webhook_url= conf['webhook_url']
58
+ unless @token
59
+ raise Fluent::ConfigError.new("`token` is required to call slack api")
60
+ end
61
+ @message ||= '%s'
62
+ @message_keys ||= %w[message]
63
+ @slack = Fluent::SlackClient::WebApi.new
64
+ end
65
+ @slack.log = log
66
+ @slack.debug_dev = log.out if log.level <= Fluent::Log::LEVEL_TRACE
67
+
68
+ begin
69
+ @message % (['1'] * @message_keys.length)
70
+ rescue ArgumentError
71
+ raise Fluent::ConfigError, "string specifier '%s' for `message` and `message_keys` specification mismatch"
72
+ end
73
+ if @title and @title_keys
74
+ begin
75
+ @title % (['1'] * @title_keys.length)
76
+ rescue ArgumentError
77
+ raise Fluent::ConfigError, "string specifier '%s' for `title` and `title_keys` specification mismatch"
78
+ end
79
+ end
80
+ if @channel_keys
81
+ begin
82
+ @channel % (['1'] * @channel_keys.length)
83
+ rescue ArgumentError
84
+ raise Fluent::ConfigError, "string specifier '%s' for `channel` and `channel_keys` specification mismatch"
85
+ end
86
+ end
87
+ end
88
+
89
+ def format(tag, time, record)
90
+ [tag, time, record].to_msgpack
91
+ end
92
+
93
+ def write(chunk)
94
+ begin
95
+ payloads = build_payloads(chunk)
96
+ payloads.each {|payload| @slack.post_message(payload) }
97
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
98
+ log.warn "out_slack:", :error => e.to_s, :error_class => e.class.to_s
99
+ raise e # let Fluentd retry
100
+ rescue => e
101
+ log.error "out_slack:", :error => e.to_s, :error_class => e.class.to_s
102
+ log.warn_backtrace e.backtrace
103
+ # discard. @todo: add more retriable errors
85
104
  end
86
105
  end
87
106
 
88
107
  private
89
- def response_check(res)
90
- if res.code != "200"
91
- raise BufferedSlackOutputError, "Slack.com - #{res.code} - #{res.body}"
108
+
109
+ def build_payloads(chunk)
110
+ if @title
111
+ build_title_payloads(chunk)
112
+ else
113
+ build_plain_payloads(chunk)
92
114
  end
93
115
  end
94
116
 
95
- def get_request(params)
96
- query = URI.encode_www_form(params)
97
- uri = URI.parse("https://slack.com/api/chat.postMessage?#{query}")
98
- http = Net::HTTP.new(uri.host, uri.port)
99
- http.use_ssl = true
100
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
101
- res = http.get(uri.request_uri)
102
- response_check(res)
117
+ def common_payload
118
+ return @common_payload if @common_payload
119
+ @common_payload = {
120
+ username: @username,
121
+ icon_emoji: @icon_emoji,
122
+ }
123
+ @common_payload[:token] = @token if @token
124
+ @common_payload
103
125
  end
104
126
 
105
- def endpoint
106
- URI.parse @webhook_url || "https://#{@team}.slack.com/services/hooks/incoming-webhook?token=#{@api_key}"
127
+ Field = Struct.new("Field", :title, :value)
128
+
129
+ def build_title_payloads(chunk)
130
+ ch_fields = {}
131
+ chunk.msgpack_each do |tag, time, record|
132
+ channel = build_channel(record)
133
+ per = tag # title per tag
134
+ ch_fields[channel] ||= {}
135
+ ch_fields[channel][per] ||= Field.new(build_title(record), '')
136
+ ch_fields[channel][per].value << "#{build_message(record)}\n"
137
+ end
138
+ ch_fields.map do |channel, fields|
139
+ {
140
+ channel: channel,
141
+ attachments: [{
142
+ :color => @color,
143
+ :fallback => fields.values.map(&:title).join(' '), # fallback is the message shown on popup
144
+ :fields => fields.values.map(&:to_h)
145
+ }],
146
+ }.merge(common_payload)
147
+ end
107
148
  end
108
149
 
109
- def post_request(data)
110
- req = Net::HTTP::Post.new endpoint.request_uri
111
- req.set_form_data(data)
112
- http = Net::HTTP.new endpoint.host, endpoint.port
113
- http.use_ssl = (endpoint.scheme == "https")
114
- res = http.request(req)
115
- response_check(res)
150
+ def build_plain_payloads(chunk)
151
+ messages = {}
152
+ chunk.msgpack_each do |tag, time, record|
153
+ channel = build_channel(record)
154
+ messages[channel] ||= ''
155
+ messages[channel] << "#{build_message(record)}\n"
156
+ end
157
+ messages.map do |channel, text|
158
+ {
159
+ channel: channel,
160
+ attachments: [{
161
+ :color => @color,
162
+ :fallback => text,
163
+ :text => text,
164
+ }],
165
+ }.merge(common_payload)
166
+ end
167
+ end
168
+
169
+ def build_message(record)
170
+ values = fetch_keys(record, @message_keys)
171
+ @message % values
172
+ end
173
+
174
+ def build_title(record)
175
+ return @title unless @title_keys
176
+
177
+ values = fetch_keys(record, @title_keys)
178
+ @title % values
179
+ end
180
+
181
+ def build_channel(record)
182
+ return @channel unless @channel_keys
183
+
184
+ values = fetch_keys(record, @channel_keys)
185
+ @channel % values
186
+ end
187
+
188
+ def fetch_keys(record, keys)
189
+ Array(keys).map do |key|
190
+ begin
191
+ record.fetch(key).to_s
192
+ rescue KeyError
193
+ log.warn "out_slack: the specified key '#{key}' not found in record. [#{record}]"
194
+ ''
195
+ end
196
+ end
116
197
  end
117
198
  end
118
199
  end
@@ -0,0 +1,199 @@
1
+ require_relative 'slack_client'
2
+
3
+ module Fluent
4
+ class SlackOutput < Fluent::BufferedOutput
5
+ Fluent::Plugin.register_output('buffered_slack', self) # old version compatiblity
6
+ Fluent::Plugin.register_output('slack', self)
7
+
8
+ include SetTimeKeyMixin
9
+ include SetTagKeyMixin
10
+
11
+ config_set_default :include_time_key, true
12
+ config_set_default :include_tag_key, true
13
+
14
+ config_param :webhook_url, :string, default: nil # incoming webhook
15
+ config_param :token, :string, default: nil # api token
16
+ config_param :username, :string, default: 'fluentd'
17
+ config_param :color, :string, default: 'good'
18
+ config_param :icon_emoji, :string, default: ':question:'
19
+
20
+ config_param :channel, :string
21
+ config_param :channel_keys, default: nil do |val|
22
+ val.split(',')
23
+ end
24
+ config_param :title, :string, default: nil
25
+ config_param :title_keys, default: nil do |val|
26
+ val.split(',')
27
+ end
28
+ config_param :message, :string, default: nil
29
+ config_param :message_keys, default: nil do |val|
30
+ val.split(',')
31
+ end
32
+
33
+ # for test
34
+ attr_reader :slack, :time_format, :localtime, :timef
35
+
36
+ def initialize
37
+ super
38
+ require 'uri'
39
+ end
40
+
41
+ def configure(conf)
42
+ conf['time_format'] ||= '%H:%M:%S' # old version compatiblity
43
+ conf['localtime'] ||= true unless conf['utc']
44
+
45
+ super
46
+
47
+ @channel = URI.unescape(@channel) # old version compatibility
48
+ @channel = '#' + @channel unless @channel.start_with?('#')
49
+
50
+ if @webhook_url
51
+ # following default values are for old version compatibility
52
+ @title ||= '%s'
53
+ @title_keys ||= %w[tag]
54
+ @message ||= '[%s] %s'
55
+ @message_keys ||= %w[time message]
56
+ @slack = Fluent::SlackClient::IncomingWebhook.new(@webhook_url)
57
+ else
58
+ unless @token
59
+ raise Fluent::ConfigError.new("`token` is required to call slack api")
60
+ end
61
+ @message ||= '%s'
62
+ @message_keys ||= %w[message]
63
+ @slack = Fluent::SlackClient::WebApi.new
64
+ end
65
+ @slack.log = log
66
+ @slack.debug_dev = log.out if log.level <= Fluent::Log::LEVEL_TRACE
67
+
68
+ begin
69
+ @message % (['1'] * @message_keys.length)
70
+ rescue ArgumentError
71
+ raise Fluent::ConfigError, "string specifier '%s' for `message` and `message_keys` specification mismatch"
72
+ end
73
+ if @title and @title_keys
74
+ begin
75
+ @title % (['1'] * @title_keys.length)
76
+ rescue ArgumentError
77
+ raise Fluent::ConfigError, "string specifier '%s' for `title` and `title_keys` specification mismatch"
78
+ end
79
+ end
80
+ if @channel_keys
81
+ begin
82
+ @channel % (['1'] * @channel_keys.length)
83
+ rescue ArgumentError
84
+ raise Fluent::ConfigError, "string specifier '%s' for `channel` and `channel_keys` specification mismatch"
85
+ end
86
+ end
87
+ end
88
+
89
+ def format(tag, time, record)
90
+ [tag, time, record].to_msgpack
91
+ end
92
+
93
+ def write(chunk)
94
+ begin
95
+ payloads = build_payloads(chunk)
96
+ payloads.each {|payload| @slack.post_message(payload) }
97
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
98
+ log.warn "out_slack:", :error => e.to_s, :error_class => e.class.to_s
99
+ raise e # let Fluentd retry
100
+ rescue => e
101
+ log.error "out_slack:", :error => e.to_s, :error_class => e.class.to_s
102
+ log.warn_backtrace e.backtrace
103
+ # discard. @todo: add more retriable errors
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def build_payloads(chunk)
110
+ if @title
111
+ build_title_payloads(chunk)
112
+ else
113
+ build_plain_payloads(chunk)
114
+ end
115
+ end
116
+
117
+ def common_payload
118
+ return @common_payload if @common_payload
119
+ @common_payload = {
120
+ username: @username,
121
+ icon_emoji: @icon_emoji,
122
+ }
123
+ @common_payload[:token] = @token if @token
124
+ @common_payload
125
+ end
126
+
127
+ Field = Struct.new("Field", :title, :value)
128
+
129
+ def build_title_payloads(chunk)
130
+ ch_fields = {}
131
+ chunk.msgpack_each do |tag, time, record|
132
+ channel = build_channel(record)
133
+ per = tag # title per tag
134
+ ch_fields[channel] ||= {}
135
+ ch_fields[channel][per] ||= Field.new(build_title(record), '')
136
+ ch_fields[channel][per].value << "#{build_message(record)}\n"
137
+ end
138
+ ch_fields.map do |channel, fields|
139
+ {
140
+ channel: channel,
141
+ attachments: [{
142
+ :color => @color,
143
+ :fallback => fields.values.map(&:title).join(' '), # fallback is the message shown on popup
144
+ :fields => fields.values.map(&:to_h)
145
+ }],
146
+ }.merge(common_payload)
147
+ end
148
+ end
149
+
150
+ def build_plain_payloads(chunk)
151
+ messages = {}
152
+ chunk.msgpack_each do |tag, time, record|
153
+ channel = build_channel(record)
154
+ messages[channel] ||= ''
155
+ messages[channel] << "#{build_message(record)}\n"
156
+ end
157
+ messages.map do |channel, text|
158
+ {
159
+ channel: channel,
160
+ attachments: [{
161
+ :color => @color,
162
+ :fallback => text,
163
+ :text => text,
164
+ }],
165
+ }.merge(common_payload)
166
+ end
167
+ end
168
+
169
+ def build_message(record)
170
+ values = fetch_keys(record, @message_keys)
171
+ @message % values
172
+ end
173
+
174
+ def build_title(record)
175
+ return @title unless @title_keys
176
+
177
+ values = fetch_keys(record, @title_keys)
178
+ @title % values
179
+ end
180
+
181
+ def build_channel(record)
182
+ return @channel unless @channel_keys
183
+
184
+ values = fetch_keys(record, @channel_keys)
185
+ @channel % values
186
+ end
187
+
188
+ def fetch_keys(record, keys)
189
+ Array(keys).map do |key|
190
+ begin
191
+ record.fetch(key).to_s
192
+ rescue KeyError
193
+ log.warn "out_slack: the specified key '#{key}' not found in record. [#{record}]"
194
+ ''
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end