fluent-plugin-slack 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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