daioikachan 0.0.1

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
+ SHA1:
3
+ metadata.gz: 8c10cbfb0cfbfbc731033be5a02a9f8ea843f4f5
4
+ data.tar.gz: fe2a499b9da5d66a57458234d5de4f475034b734
5
+ SHA512:
6
+ metadata.gz: 585730388e9aff3c91ef30e36060957d3363be9cc6b376d9f23c843d1b617112383466a5166d212d4319be2083529989c9bff9d706a29ee4fd72d85f1d3d7f89
7
+ data.tar.gz: 4910660d2dbd34642730b438362b7b9b799efb8946f3cd5abb7bbbd3a6beebce1589e83a61d2029fb50bd21b5763ff5ca69ac3da70f2c3e13fbdebdca2d33f7b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /*.gem
2
+ ~*
3
+ #*
4
+ *~
5
+ .bundle
6
+ Gemfile.lock
7
+ .rbenv-version
8
+ vendor
9
+ doc/*
10
+ tmp/*
11
+ coverage
12
+ .yardoc
13
+ .ruby-version
14
+ pkg/*
15
+ .tags
16
+ .env
17
+ daioikachan.conf
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+
8
+ branches:
9
+ only:
10
+ - master
11
+
12
+ script: bundle exec rake test
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.1 (2015/03/22)
2
+
3
+ Initial version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Naotoshi Seo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Daioikachan
2
+
3
+ [Ikachan](https://github.com/yappo/p5-App-Ikachan) compatible interface with multiple backends (IRC, Slack, etc).
4
+
5
+ ## Requirements
6
+
7
+ * Ruby
8
+
9
+ ## Installation
10
+
11
+ Write Gemfile:
12
+
13
+ ```
14
+ source "https://rubygems.org"
15
+
16
+ gem 'daioikachan'
17
+ ```
18
+
19
+ Run:
20
+
21
+ ```
22
+ $ bundle
23
+ ```
24
+
25
+ IRC, Slack backends are bundled as default.
26
+
27
+ Daioikachan supports plugin architecture. You may add your favirite backends as gems. See Plugin section for details.
28
+
29
+ ## How to Run
30
+
31
+ This is an example to post messages for both IRC and Slack.
32
+
33
+ ### Start
34
+
35
+ Generate a sample [daioikachan.conf](./examples/example.conf):
36
+
37
+ ```
38
+ $ bundle exec daioikachan -g daioikachan.conf
39
+ ```
40
+
41
+ Create `.env` and configure your IRC server and Slack token:
42
+
43
+ ```
44
+ IRC_SERVER=XX.XX.XX.XX
45
+ SLACK_API_TOKEN=XXX-XXXXX-XXXXXX-XXXXX
46
+ ````
47
+
48
+ Start `daioikachan` as
49
+
50
+ ```
51
+ $ bundle exec daioikachan
52
+ ```
53
+
54
+ ### Test
55
+
56
+ Test to post a message to `#daioikachan` channel of both IRC and Slack via `daioikachan` like:
57
+
58
+ ```
59
+ $ curl -d "channel=#daioikachan&message=test daioikachan" http://localhost:4979/notice
60
+ ```
61
+
62
+ Look whether posting to IRC, and Slack succeeds.
63
+
64
+ ## Configuration
65
+
66
+ See [example.conf](./example.conf) or [multi_slack.conf](./examples/multi_slack.conf) as examples.
67
+
68
+ You might've noticed that the config file is similar with Fluentd.
69
+ Yes, `daioikachan` is created based on Fluentd.
70
+ So, you can use `routing` functions which Fluentd has (`tag` and `label`).
71
+
72
+ See [config-file](http://docs.fluentd.org/articles/config-file) documentation of Fluentd for details.
73
+
74
+ See following pages for built-in plugins.
75
+
76
+ * [in_daioikachan](./README/in_daioikachan.md)
77
+ * [fluent-plugin-irc](./README/out_irc.md)
78
+ * [fluent-plugin-slack](https://github.com/sowawa/fluent-plugin-slack)
79
+
80
+ If you need other backends, search other fluentd output plugins and add them. Enjoy!
81
+
82
+ ## Plugin
83
+
84
+ You can create and add your own backends for daioikachan as a Fluentd Plugin.
85
+
86
+ See http://www.fluentd.org/plugins#notifications for available plugins.
87
+
88
+ ## API
89
+
90
+ ### /notice
91
+
92
+ Send `notice` message.
93
+
94
+ ```
95
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
96
+ ```
97
+
98
+ ### /privmsg
99
+
100
+ Send `privmsg` message.
101
+
102
+ ```
103
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/privmsg
104
+ ```
105
+
106
+ ### /join
107
+
108
+ The server always returns 200, and ignore.
109
+
110
+ IRC client (`fluent-plugin-irc`) automatically joins to a channel on sending message.
111
+ Slack client does not require to join to a channel.
112
+
113
+ ### /leave
114
+
115
+ The server always returns 200, and ignores.
116
+
117
+ ## ChangeLog
118
+
119
+ See [CHANGELOG.md](CHANGELOG.md) for details.
120
+
121
+ ## Contributing
122
+
123
+ 1. Fork it
124
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
125
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
126
+ 4. Push to the branch (`git push origin my-new-feature`)
127
+ 5. Create new [Pull Request](../../pull/new/master)
128
+
129
+ ## Copyright
130
+
131
+ ### daioikachan, and fluent-plugin-daioikachan
132
+
133
+ Copyright (c) 2015 Naotoshi Seo. See [LICENSE](LICENSE) for details.
134
+
135
+ ### fluent-plugin-irc
136
+
137
+ See https://github.com/choplin/fluent-plugin-irc
138
+
139
+ ### fluent-plugin-slack
140
+
141
+ See https://github.com/sowawa/fluent-plugin-slack
@@ -0,0 +1,63 @@
1
+ # Daioikachan plugin for Fluentd
2
+
3
+ This plugin provides [ikachan](https://github.com/yappo/p5-App-Ikachan) compatible interface by Fluentd.
4
+
5
+ ## Installation
6
+
7
+ Bundled with `daioikachan` gem.
8
+
9
+ ## Configuration
10
+
11
+ ```apache
12
+ <source>
13
+ type daioikachan
14
+ bind 127.0.0.1
15
+ port 4979
16
+ backlog 2048
17
+
18
+ # optional Puma parameters
19
+ min_threads 0
20
+ max_threads 4
21
+ </source>
22
+ ```
23
+
24
+ Receiving API post like below,
25
+
26
+ ```
27
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
28
+ ```
29
+
30
+ emits an event as
31
+
32
+ ```
33
+ notice.channel {"command":"notice","channel":"channel","message":"test message"}
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### /notice
39
+
40
+ Send `notice` message.
41
+
42
+ ```
43
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
44
+ ```
45
+
46
+ ### /privmsg
47
+
48
+ Send `privmsg` message.
49
+
50
+ ```
51
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/privmsg
52
+ ```
53
+
54
+ ### /join
55
+
56
+ The server always returns 200, and ignored.
57
+
58
+ IRC client should automatically join to a channel on sending message.
59
+ Slack client does not require to join to a channel.
60
+
61
+ ### /leave
62
+
63
+ The server always returns 200, and ignored.
data/README/out_irc.md ADDED
@@ -0,0 +1,58 @@
1
+ # Fluent::Plugin::Irc, a plugin for [Fluentd](http://fluentd.org)
2
+
3
+ [![Build Status](https://travis-ci.org/choplin/fluent-plugin-irc.svg)](https://travis-ci.org/choplin/fluent-plugin-irc)
4
+
5
+ Fluent plugin to send messages to IRC server
6
+
7
+ ## Installation
8
+
9
+ `$ fluent-gem install fluent-plugin-irc`
10
+
11
+ ## Configuration
12
+
13
+ ### Example
14
+
15
+ ```
16
+ <match **>
17
+ type irc
18
+ host localhost
19
+ port 6667
20
+ channel fluentd
21
+ nick fluentd
22
+ user fluentd
23
+ real fluentd
24
+ message notice: %s [%s] %s
25
+ out_keys tag,time,message
26
+ time_key time
27
+ time_format %Y/%m/%d %H:%M:%S
28
+ tag_key tag
29
+ </match>
30
+ ```
31
+
32
+ ### Parameter
33
+
34
+ |parameter|description|default|
35
+ |---|---|---|
36
+ |host|IRC server host|localhost|
37
+ |port|IRC server port number|6667|
38
+ |channel|channel to send messages (without first '#')||
39
+ |channel_keys|keys used to format channel. %s will be replaced with value specified by channel_keys if this option is used|nil|
40
+ |nick|nickname registerd of IRC|fluentd|
41
+ |user|user name registerd of IRC|fluentd|
42
+ |real|real name registerd of IRC|fluentd|
43
+ |message|message format. %s will be replaced with value specified by out_keys||
44
+ |out_keys|keys used to format messages||
45
+ |time_key|key name for time|time|
46
+ |time_format|time format. This will be formatted with Time#strftime.|%Y/%m/%d %H:%M:%S|
47
+ |tag_key|key name for tag|tag|
48
+ |command|irc command. `privmsg` or `notice`|privmsg|
49
+ |command_keys|keys used to format command. %s will be replaced with value specified by command_keys if this option is used|nil|
50
+ |send_interval|interval (sec) to send message. defence Excess Flood|2|
51
+ |max_send_queue|maximum size of send message queue|100|
52
+
53
+ ## Copyright
54
+
55
+ <table>
56
+ <tr><td>Copyright</td><td>Copyright (c) 2015 OKUNO Akihiro</td></tr>
57
+ <tr><td>License</td><td>Apache License, Version 2.0</td></tr>
58
+ </table>
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'lib' << 'test'
10
+ test.test_files = (Dir["test/plugin/test_*.rb"] - ["helper.rb"]).sort
11
+ test.verbose = true
12
+ end
13
+
14
+ task :default => [:test]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/daioikachan ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ require 'rubygems' unless defined?(gem)
4
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
5
+ $LOAD_PATH << File.expand_path(File.join(ROOT, 'lib'))
6
+
7
+ require 'dotenv'
8
+ Dotenv.load
9
+
10
+ # tweak default_options
11
+ require 'fluent/supervisor'
12
+ class Fluent::Supervisor
13
+ class << self
14
+ alias :fluentd_default_options :default_options
15
+ end
16
+ def self.default_options
17
+ fluentd_default_options.merge({
18
+ :config_path => 'daioikachan.conf',
19
+ :plugin_dirs => [File.join(ROOT, 'lib/fluent/plugin')],
20
+ })
21
+ end
22
+ end
23
+
24
+ # handle argv to generate conf
25
+ require 'optparse'
26
+ opts = {}
27
+ opt = OptionParser.new
28
+ opt.on('-g', "--generate PATH", "generate a sample configuration file") {|s|
29
+ opts[:generate_path] = s
30
+ }
31
+ begin
32
+ opt.parse!(ARGV)
33
+ rescue OptionParser::InvalidOption => e
34
+ argv = ARGV # suppress warning to constant
35
+ argv.unshift(*(e.args)) # restore and ignore unknown options to pass to Fluentd
36
+ end
37
+
38
+ if generate_path = opts[:generate_path]
39
+ require 'fileutils'
40
+ FileUtils.mkdir_p File.dirname(generate_path)
41
+ if File.exist?(generate_path)
42
+ puts "#{generate_path} already exists."
43
+ else
44
+ File.open(generate_path, "w") {|f|
45
+ conf = File.read File.join(ROOT, 'examples/example.conf')
46
+ f.write conf
47
+ }
48
+ puts "Installed #{generate_path}."
49
+ end
50
+ exit 0
51
+ end
52
+
53
+ require 'fluent/command/fluentd'
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "daioikachan"
6
+ gem.description = "Ikachan compatible interface with multiple backends (IRC, Slack, etc)"
7
+ gem.homepage = "https://github.com/sonots/daioikachan"
8
+ gem.summary = gem.description
9
+ gem.version = File.read("VERSION").strip
10
+ gem.authors = ["Naotoshi Seo"]
11
+ gem.email = "sonots@gmail.com"
12
+ gem.has_rdoc = false
13
+ gem.license = 'MIT License'
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.require_paths = ['lib']
18
+
19
+ gem.add_dependency "fluentd", "~> 0.12.0"
20
+ gem.add_dependency "dotenv"
21
+ gem.add_dependency "puma"
22
+ # gem.add_dependency "fluent-plugin-irc"
23
+ gem.add_dependency "irc_parser"
24
+ gem.add_dependency "fluent-plugin-slack", ">= 0.5.0"
25
+ gem.add_development_dependency "rake"
26
+ end
@@ -0,0 +1,57 @@
1
+ <source>
2
+ type daioikachan
3
+ bind 0.0.0.0
4
+ port 4979
5
+ min_threads 0
6
+ max_threads 4
7
+ backlog 1024
8
+ @label @raw
9
+ </source>
10
+
11
+ <label @raw>
12
+ <match **>
13
+ type copy
14
+ <store>
15
+ type stdout
16
+ </store>
17
+ <store>
18
+ type relabel
19
+ @label @slack
20
+ </store>
21
+ <store>
22
+ type relabel
23
+ @label @irc
24
+ </store>
25
+ </match>
26
+ </label>
27
+
28
+ <label @irc>
29
+ <match **>
30
+ type irc
31
+ host "#{ENV['IRC_SERVER']}"
32
+ port 6667
33
+ nick daioikachan
34
+ user daioikachan
35
+ real daioikachan
36
+ command %s
37
+ command_keys command
38
+ channel %s
39
+ channel_keys channel
40
+ message %s
41
+ out_keys message
42
+ send_interval 2s # IRC has Excess Flood limit, this is the default value taken from ikachan
43
+ </match>
44
+ </label>
45
+
46
+ <label @slack>
47
+ <match **>
48
+ type slack
49
+ token "#{ENV['SLACK_API_TOKEN']}"
50
+ username daioikachan
51
+ channel %s
52
+ channel_keys channel
53
+ color good
54
+ icon_emoji :ghost:
55
+ flush_interval 1s # slack API has limit as a post / sec
56
+ </match>
57
+ </label>
@@ -0,0 +1,46 @@
1
+ <source>
2
+ type daioikachan
3
+ bind 0.0.0.0
4
+ port 4979
5
+ min_threads 0
6
+ max_threads 4
7
+ backlog 1024
8
+ @label @raw
9
+ </source>
10
+
11
+ <label @raw>
12
+ <match **>
13
+ type copy
14
+ <store>
15
+ type stdout
16
+ </store>
17
+ <store>
18
+ type relabel
19
+ @label @slack
20
+ </store>
21
+ </match>
22
+ </label>
23
+
24
+ <label @slack>
25
+ # #fluentd_warn => team1.slack.com#general
26
+ <match *.fluentd_warn>
27
+ type slack
28
+ token "#{ENV['TEAM1_TOKEN']}"
29
+ username daioikachan
30
+ channel general
31
+ color bad
32
+ icon_emoji :ghost:
33
+ flush_interval 1s # slack API has limit as a post / sec
34
+ </match>
35
+ # other channels => default_team.slack.com#${channel}
36
+ <match **>
37
+ type slack
38
+ token "#{ENV['DEFAULT_TEAM_TOKEN']}"
39
+ username daioikachan
40
+ channel %s
41
+ channel_keys channel
42
+ color bad
43
+ icon_emoji :ghost:
44
+ flush_interval 1s # slack API has limit as a post / sec
45
+ </match>
46
+ </label>
@@ -0,0 +1,160 @@
1
+ module Fluent
2
+ class DaioikachanInput < Input
3
+ Plugin.register_input('daioikachan', self)
4
+
5
+ def initialize
6
+ require 'puma'
7
+ require 'uri'
8
+ super
9
+ end
10
+
11
+ config_param :port, :integer, :default => 4979
12
+ config_param :bind, :string, :default => '0.0.0.0'
13
+ config_param :min_threads, :integer, :default => 0
14
+ config_param :max_threads, :integer, :default => 4
15
+ config_param :backlog, :integer, :default => nil
16
+
17
+ def configure(conf)
18
+ super
19
+ end
20
+
21
+ def start
22
+ super
23
+
24
+ # Refer puma's Runner and Rack handler for puma server setup
25
+ @server = ::Puma::Server.new(method(:on_request))
26
+ @server.min_threads = @min_threads
27
+ @server.max_threads = @max_threads
28
+ @server.leak_stack_on_error = false
29
+ setup_http
30
+
31
+ @app = App.new(self)
32
+
33
+ @thread = Thread.new(&method(:run))
34
+ end
35
+
36
+ def shutdown
37
+ @server.stop(true)
38
+ @thread.join
39
+ end
40
+
41
+ def run
42
+ @server.run(false)
43
+ rescue => e
44
+ log.error "unexpected error", :error => e.to_s
45
+ log.error_backtrace e.backtrace
46
+ end
47
+
48
+ def on_request(env)
49
+ @app.run(env)
50
+ end
51
+
52
+ def setup_http
53
+ log.info "listening http on #{@bind}:#{@port}"
54
+
55
+ opts = [@bind, @port, true]
56
+ opts << @backlog if @backlog
57
+ @server.add_tcp_listener(*opts)
58
+ end
59
+
60
+ class App
61
+ class BadRequest < StandardError; end
62
+ class InternalServerError < StandardError; end
63
+ class NotFound < StandardError; end
64
+
65
+ attr_reader :router, :log
66
+
67
+ def initialize(plugin)
68
+ @router = plugin.router
69
+ @log = plugin.log
70
+ end
71
+
72
+ def run(env)
73
+ # req = Rack::Request.new(env)
74
+ method = env['REQUEST_METHOD'.freeze] # req.method
75
+ path = URI.parse(env['REQUEST_URI'.freeze]).path # req.path
76
+ body = env['rack.input'].read # req.body.read
77
+ params = Rack::Utils.parse_query(body)
78
+
79
+ begin
80
+ if method == 'POST'
81
+ case path
82
+ when '/notice'
83
+ notice(params)
84
+ when '/privmsg'
85
+ privmsg(params)
86
+ when '/join'
87
+ return ok
88
+ when '/leave'
89
+ return ok
90
+ else
91
+ return not_found
92
+ end
93
+ else
94
+ return not_found
95
+ end
96
+ rescue BadRequest => e
97
+ bad_request(e.message)
98
+ rescue NotFound => e
99
+ not_found(e.message)
100
+ rescue InternalServerError => e
101
+ internal_server_error(e.message)
102
+ rescue => e
103
+ internal_server_error("#{e.class} #{e.message} #{e.backtrace.first}")
104
+ else
105
+ ok
106
+ end
107
+ end
108
+
109
+ def notice(params)
110
+ channel, message = build_channel(params), build_message(params)
111
+ tag = "notice.#{channel}"
112
+ record = params.merge('command' => 'notice', 'channel' => channel, 'message' => message)
113
+ router.emit(tag, Fluent::Engine.now, record)
114
+ end
115
+
116
+ def privmsg(params)
117
+ channel, message = build_channel(params), build_message(params)
118
+ tag = "privmsg.#{channel}"
119
+ record = params.merge('command' => 'privmsg', 'channel' => channel, 'message' => message)
120
+ router.emit(tag, Fluent::Engine.now, record)
121
+ end
122
+
123
+ private
124
+
125
+ def build_channel(params)
126
+ unless channel = params.delete('channel')
127
+ raise BadRequest.new('`channel` parameter is mandatory')
128
+ end
129
+ if channel.start_with?('#')
130
+ channel[1..-1] # remove starting #
131
+ else
132
+ channel
133
+ end
134
+ end
135
+
136
+ def build_message(params)
137
+ unless message = params.delete('message')
138
+ raise BadRequest.new('`message` parameter is mandatory')
139
+ end
140
+ message # should I truncate message to max_length?
141
+ end
142
+
143
+ def ok(msg = nil)
144
+ [200, {'Content-type'=>'text/plain'}, ["OK\n#{msg}"]]
145
+ end
146
+
147
+ def bad_request(msg = nil)
148
+ [400, {'Content-type'=>'text/plain'}, ["Bad Request\n#{msg}"]]
149
+ end
150
+
151
+ def not_found(msg = nil)
152
+ [404, {'Content-type'=>'text/plain'}, ["Not Found\n#{msg}"]]
153
+ end
154
+
155
+ def internal_server_error(msg = nil)
156
+ [500, {'Content-type'=>'text/plain'}, ["Internal Server Error\n#{msg}"]]
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,283 @@
1
+ module Fluent
2
+ class IRCOutput < Fluent::Output
3
+ Fluent::Plugin.register_output('irc', self)
4
+
5
+ include SetTimeKeyMixin
6
+ include SetTagKeyMixin
7
+
8
+ config_set_default :include_time_key, true
9
+ config_set_default :include_tag_key, true
10
+
11
+ config_param :host , :string , :default => 'localhost'
12
+ config_param :port , :integer , :default => 6667
13
+ config_param :channel , :string
14
+ config_param :channel_keys, :default => nil do |val|
15
+ val.split(',')
16
+ end
17
+ config_param :nick , :string , :default => 'fluentd'
18
+ config_param :user , :string , :default => 'fluentd'
19
+ config_param :real , :string , :default => 'fluentd'
20
+ config_param :password , :string , :default => nil
21
+ config_param :message , :string
22
+ config_param :out_keys do |val|
23
+ val.split(',')
24
+ end
25
+ config_param :time_key , :string , :default => 'time'
26
+ config_param :time_format , :string , :default => '%Y/%m/%d %H:%M:%S'
27
+ config_param :tag_key , :string , :default => 'tag'
28
+ config_param :command , :string , :default => 'privmsg'
29
+ config_param :command_keys, :default => nil do |val|
30
+ val.split(',')
31
+ end
32
+
33
+ config_param :blocking_timeout, :time, :default => 0.5
34
+ config_param :max_send_queue, :integer, :default => 100
35
+ config_param :send_interval, :time, :default => 2
36
+
37
+ COMMAND_MAP = {
38
+ 'priv_msg' => :priv_msg,
39
+ 'privmsg' => :priv_msg,
40
+ 'notice' => :notice,
41
+ }
42
+
43
+ # To support log_level option implemented by Fluentd v0.10.43
44
+ unless method_defined?(:log)
45
+ define_method("log") { $log }
46
+ end
47
+
48
+ attr_reader :conn # for test
49
+
50
+ def initialize
51
+ super
52
+ require 'irc_parser'
53
+ end
54
+
55
+ def configure(conf)
56
+ super
57
+
58
+ begin
59
+ @message % (['1'] * @out_keys.length)
60
+ rescue ArgumentError
61
+ raise Fluent::ConfigError, "string specifier '%s' and out_keys specification mismatch"
62
+ end
63
+
64
+ if @channel_keys
65
+ begin
66
+ @channel % (['1'] * @channel_keys.length)
67
+ rescue ArgumentError
68
+ raise Fluent::ConfigError, "string specifier '%s' and channel_keys specification mismatch"
69
+ end
70
+ end
71
+ @channel = '#'+@channel
72
+
73
+ if @command_keys
74
+ begin
75
+ @command % (['1'] * @command_keys.length)
76
+ rescue ArgumentError
77
+ raise Fluent::ConfigError, "string specifier '%s' and command_keys specification mismatch"
78
+ end
79
+ else
80
+ unless @command = COMMAND_MAP[@command]
81
+ raise Fluent::ConfigError, "command must be one of #{COMMAND_MAP.keys.join(', ')}"
82
+ end
83
+ end
84
+
85
+ @send_queue = []
86
+ end
87
+
88
+ def start
89
+ super
90
+
91
+ begin
92
+ @loop = Coolio::Loop.new
93
+ @conn = create_connection
94
+ @timer = TimerWatcher.new(@send_interval, true, log, &method(:on_timer))
95
+ @loop.attach(@timer)
96
+ @thread = Thread.new(&method(:run))
97
+ rescue => e
98
+ puts e
99
+ raise Fluent::ConfigError, "failed to connect IRC server #{@host}:#{@port}"
100
+ end
101
+ end
102
+
103
+ def shutdown
104
+ super
105
+ @loop.watchers.each { |w| w.detach }
106
+ @loop.stop
107
+ @conn.close
108
+ @thread.join
109
+ end
110
+
111
+ def run
112
+ @loop.run(@blocking_timeout)
113
+ rescue => e
114
+ log.error "unexpected error", :error => e, :error_class => e.class
115
+ log.error_backtrace
116
+ end
117
+
118
+ def emit(tag, es, chain)
119
+ chain.next
120
+
121
+ if @conn.closed?
122
+ log.warn "out_irc: connection is closed. try to reconnect"
123
+ @conn = create_connection
124
+ end
125
+
126
+ es.each do |time,record|
127
+ if @send_queue.size >= @max_send_queue
128
+ log.warn "out_irc: send queue size exceeded max_send_queue(#{@max_send_queue}), discards"
129
+ break
130
+ end
131
+
132
+ filter_record(tag, time, record)
133
+ command, channel, message = build_command(record), build_channel(record), build_message(record)
134
+ log.debug { "out_irc: push {command:\"#{command}\", channel:\"#{channel}\", message:\"#{message}\"}" }
135
+ @send_queue.push([command, channel, message])
136
+ end
137
+ end
138
+
139
+ def on_timer
140
+ return if @send_queue.empty?
141
+ command, channel, message = @send_queue.shift
142
+ log.info { "out_irc: send {command:\"#{command}\", channel:\"#{channel}\", message:\"#{message}\"}" }
143
+ @conn.send_message(command, channel, message)
144
+ end
145
+
146
+ private
147
+
148
+ def create_connection
149
+ conn = IRCConnection.connect(@host, @port)
150
+ conn.log = log
151
+ conn.nick = @nick
152
+ conn.user = @user
153
+ conn.real = @real
154
+ conn.password = @password
155
+ conn.attach(@loop)
156
+ conn
157
+ end
158
+
159
+ def build_message(record)
160
+ values = fetch_keys(record, @out_keys)
161
+ @message % values
162
+ end
163
+
164
+ def build_channel(record)
165
+ return @channel unless @channel_keys
166
+
167
+ values = fetch_keys(record, @channel_keys)
168
+ @channel % values
169
+ end
170
+
171
+ def build_command(record)
172
+ return @command unless @command_keys
173
+
174
+ values = fetch_keys(record, @command_keys)
175
+ unless command = COMMAND_MAP[@command % values]
176
+ log.warn "out_irc: command is not one of #{COMMAND_MAP.keys.join(', ')}, use privmsg"
177
+ end
178
+ command || :priv_msg
179
+ end
180
+
181
+ def fetch_keys(record, keys)
182
+ Array(keys).map do |key|
183
+ begin
184
+ record.fetch(key).to_s
185
+ rescue KeyError
186
+ log.warn "out_irc: the specified key '#{key}' not found in record. [#{record}]"
187
+ ''
188
+ end
189
+ end
190
+ end
191
+
192
+ class TimerWatcher < Coolio::TimerWatcher
193
+ def initialize(interval, repeat, log, &callback)
194
+ @callback = callback
195
+ @log = log
196
+ super(interval, repeat)
197
+ end
198
+
199
+ def on_timer
200
+ @callback.call
201
+ rescue
202
+ @log.error $!.to_s
203
+ @log.error_backtrace
204
+ end
205
+ end
206
+
207
+ class IRCConnection < Cool.io::TCPSocket
208
+ attr_reader :joined # for test
209
+ attr_accessor :log, :nick, :user, :real, :password
210
+
211
+ def initialize(*args)
212
+ super
213
+ @joined = {}
214
+ end
215
+
216
+ def on_connect
217
+ if @password
218
+ IRCParser.message(:pass) do |m|
219
+ m.password = @password
220
+ write m
221
+ end
222
+ end
223
+ IRCParser.message(:nick) do |m|
224
+ m.nick = @nick
225
+ write m
226
+ end
227
+ IRCParser.message(:user) do |m|
228
+ m.user = @user
229
+ m.postfix = @real
230
+ write m
231
+ end
232
+ end
233
+
234
+ def on_read(data)
235
+ data.each_line do |line|
236
+ begin
237
+ msg = IRCParser.parse(line)
238
+ log.debug { "out_irc: on_read :#{msg.class.to_sym}" }
239
+ case msg.class.to_sym
240
+ when :ping
241
+ IRCParser.message(:pong) do |m|
242
+ m.target = msg.target
243
+ m.body = msg.body
244
+ write m
245
+ end
246
+ when :join
247
+ log.info { "out_irc: joined to #{msg.channels.join(', ')}" }
248
+ msg.channels.each {|channel| @joined[channel] = true }
249
+ when :err_nick_name_in_use
250
+ log.warn "out_irc: nickname \"#{msg.error_nick}\" is already in use."
251
+ when :error
252
+ log.warn "out_irc: an error occured. \"#{msg.error_message}\""
253
+ end
254
+ rescue
255
+ #TODO
256
+ end
257
+ end
258
+ end
259
+
260
+ def joined?(channel)
261
+ @joined[channel]
262
+ end
263
+
264
+ def join(channel)
265
+ IRCParser.message(:join) do |m|
266
+ m.channels = channel
267
+ write m
268
+ end
269
+ log.debug { "out_irc: join to #{channel}" }
270
+ end
271
+
272
+ def send_message(command, channel, message)
273
+ join(channel) unless joined?(channel)
274
+ IRCParser.message(command) do |m|
275
+ m.target = channel
276
+ m.body = message
277
+ write m
278
+ end
279
+ channel # return channel for test
280
+ end
281
+ end
282
+ end
283
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'test/unit'
2
+ require 'fileutils'
3
+ require 'fluent/log'
4
+ require 'fluent/test'
5
+
6
+ unless defined?(Test::Unit::AssertionFailedError)
7
+ class Test::Unit::AssertionFailedError < StandardError
8
+ end
9
+ end
10
+
11
+ def unused_port
12
+ s = TCPServer.open(0)
13
+ port = s.addr[1]
14
+ s.close
15
+ port
16
+ end
17
+
18
+ def ipv6_enabled?
19
+ require 'socket'
20
+
21
+ begin
22
+ TCPServer.open("::1", 0)
23
+ true
24
+ rescue
25
+ false
26
+ end
27
+ end
28
+
29
+ $log = Fluent::Log.new(Fluent::Test::DummyLogDevice.new, Fluent::Log::LEVEL_WARN)
@@ -0,0 +1,105 @@
1
+ require_relative '../helper'
2
+ require 'fluent/plugin/in_daioikachan'
3
+ require 'net/https'
4
+
5
+ class DaioikachanInputTest < Test::Unit::TestCase
6
+ def post(path, params, header = {}, ssl = false)
7
+ http = Net::HTTP.new("127.0.0.1", PORT)
8
+ if ssl
9
+ http.use_ssl = true
10
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ end
12
+ req = Net::HTTP::Post.new(path, header)
13
+ if params.is_a?(String)
14
+ req.body = params
15
+ else
16
+ req.set_form_data(params)
17
+ end
18
+ http.request(req)
19
+ end
20
+
21
+ def setup
22
+ Fluent::Test.setup
23
+ end
24
+
25
+ PORT = unused_port
26
+ CONFIG = %[
27
+ port #{PORT}
28
+ ]
29
+
30
+ def create_driver(conf = CONFIG)
31
+ Fluent::Test::InputTestDriver.new(Fluent::DaioikachanInput).configure(conf, true)
32
+ end
33
+
34
+ def test_default_configure
35
+ d = create_driver(%[])
36
+ assert_equal 4979, d.instance.port
37
+ assert_equal '0.0.0.0', d.instance.bind
38
+ assert_equal 0, d.instance.min_threads
39
+ assert_equal 4, d.instance.max_threads
40
+ assert_equal nil, d.instance.backlog
41
+ end
42
+
43
+ def test_configure
44
+ d = create_driver(%[
45
+ port #{PORT}
46
+ bind 127.0.0.1
47
+ min_threads 0
48
+ max_threads 4
49
+ backlog 1024
50
+ ])
51
+ assert_equal PORT, d.instance.port
52
+ assert_equal '127.0.0.1', d.instance.bind
53
+ assert_equal 0, d.instance.min_threads
54
+ assert_equal 4, d.instance.max_threads
55
+ assert_equal 1024, d.instance.backlog
56
+ end
57
+
58
+ def test_notice
59
+ d = create_driver
60
+
61
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
62
+ Fluent::Engine.now = time
63
+
64
+ d.expect_emit "notice.channel", time, {"command" => "notice", "channel" => "channel", "message" => "message"}
65
+
66
+ d.run do
67
+ d.expected_emits.each {|tag, time, record|
68
+ res = post("/notice", {command: "notice", channel: "channel", message: "message"})
69
+ assert_equal "200", res.code
70
+ }
71
+ end
72
+ end
73
+
74
+ def test_privmsg
75
+ d = create_driver
76
+
77
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
78
+ Fluent::Engine.now = time
79
+
80
+ d.expect_emit "privmsg.channel", time, {"command" => "privmsg", "channel" => "channel", "message" => "message"}
81
+
82
+ d.run do
83
+ d.expected_emits.each {|tag, time, record|
84
+ res = post("/privmsg", {command: "privmsg", channel: "channel", message: "message"})
85
+ assert_equal "200", res.code
86
+ }
87
+ end
88
+ end
89
+
90
+ def test_join
91
+ d = create_driver
92
+ d.run do
93
+ res = post("/join", {})
94
+ assert_equal "200", res.code
95
+ end
96
+ end
97
+
98
+ def test_leave
99
+ d = create_driver
100
+ d.run do
101
+ res = post("/leave", {})
102
+ assert_equal "200", res.code
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: daioikachan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Naotoshi Seo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.12.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: puma
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: irc_parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fluent-plugin-slack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.5.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.5.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Ikachan compatible interface with multiple backends (IRC, Slack, etc)
98
+ email: sonots@gmail.com
99
+ executables:
100
+ - daioikachan
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".travis.yml"
106
+ - CHANGELOG.md
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - README/in_daioikachan.md
111
+ - README/out_irc.md
112
+ - Rakefile
113
+ - VERSION
114
+ - bin/daioikachan
115
+ - daioikachan.gemspec
116
+ - examples/example.conf
117
+ - examples/multi_slack.conf
118
+ - lib/fluent/plugin/in_daioikachan.rb
119
+ - lib/fluent/plugin/out_irc.rb
120
+ - test/helper.rb
121
+ - test/plugin/test_in_daioikachan.rb
122
+ homepage: https://github.com/sonots/daioikachan
123
+ licenses:
124
+ - MIT License
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.2.2
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Ikachan compatible interface with multiple backends (IRC, Slack, etc)
146
+ test_files:
147
+ - test/helper.rb
148
+ - test/plugin/test_in_daioikachan.rb