fluent-plugin-heroku-syslog 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: c1ba40b8e6dea0a28ffed82b25787897c0ead301
4
+ data.tar.gz: e2e430813032e6445a4bf0344dca1f746580ca09
5
+ SHA512:
6
+ metadata.gz: cc75128973dfcb608908f1f930e9cf4e6eb436457a12d1cbfddc1308c902e8e56d77df21f34b153c9f32c3c9a2c3b3c47e6490eb31df67cb1c3c699d35dfbf53
7
+ data.tar.gz: 4743b87ccc0fec77eb82613a49308700995aa305a133ed68c55c39631339b4b356e14c6b1dbabe0475491ee69dddbfea839cd21cd2404aa8f4a44cae51f5f98e
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test.conf
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-heroku-syslog.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2014- Kazuyuki Honda
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # fluent-plugin-heroku-syslog
2
+
3
+ fluent plugin to drain heroku syslog.
4
+
5
+ [![Build Status](https://travis-ci.org/hakobera/fluent-plugin-heroku-syslog.png?branch=master)](https://travis-ci.org/hakobera/fluent-plugin-heroku-syslog)
6
+
7
+ ## Component
8
+
9
+ ### HerokuSyslogInput
10
+
11
+ Plugin to accept syslog input from [heroku syslog drains](https://devcenter.heroku.com/articles/logging#syslog-drains).
12
+
13
+ ## Configuration
14
+
15
+ ```
16
+ <source>
17
+ type heroku_syslog
18
+ port 5140
19
+ bind 0.0.0.0
20
+ tag heroku
21
+ </source>
22
+ ```
23
+
24
+ ## TODO
25
+
26
+ - Implement authentication logic or filter like HTTP basic auth.
27
+
28
+ ## Copyright
29
+
30
+ - Copyright
31
+ - Copyright(C) 2014- Kazuyuki Honda (hakobera)
32
+ - License
33
+ - Apache License, Version 2.0
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "fluent-plugin-heroku-syslog"
4
+ gem.version = "0.0.1"
5
+ gem.authors = ["Kazuyuki Honda"]
6
+ gem.email = ["hakobera@gmail.com"]
7
+ gem.description = %q{fluent plugin to drain heroku syslog}
8
+ gem.summary = %q{fluent plugin to drain heroku syslog}
9
+ gem.homepage = "https://github.com/hakobera/fluent-plugin-heroku-syslog"
10
+ gem.license = "APLv2"
11
+
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.require_paths = ["lib"]
16
+
17
+ gem.add_runtime_dependency "fluentd"
18
+ gem.add_development_dependency "rake"
19
+ end
@@ -0,0 +1,221 @@
1
+ module Fluent
2
+ class HerokuSyslogInput < Input
3
+ Plugin.register_input('heroku_syslog', self)
4
+
5
+ OCTET_COUNTING_REGEXP = /^([0-9]+)\s+(.*)/
6
+ SYSLOG_REGEXP = /^\<([0-9]+)\>[0-9]*(.*)/
7
+ SYSLOG_ALL_REGEXP = /^\<(?<pri>[0-9]+)\>[0-9]* (?<time>[^ ]*) (?<drain_id>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*) (?<pid>[a-zA-Z0-9\.]+)? *(?<message>.*)$/
8
+ TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
9
+
10
+ FACILITY_MAP = {
11
+ 0 => 'kern',
12
+ 1 => 'user',
13
+ 2 => 'mail',
14
+ 3 => 'daemon',
15
+ 4 => 'auth',
16
+ 5 => 'syslog',
17
+ 6 => 'lpr',
18
+ 7 => 'news',
19
+ 8 => 'uucp',
20
+ 9 => 'cron',
21
+ 10 => 'authpriv',
22
+ 11 => 'ftp',
23
+ 12 => 'ntp',
24
+ 13 => 'audit',
25
+ 14 => 'alert',
26
+ 15 => 'at',
27
+ 16 => 'local0',
28
+ 17 => 'local1',
29
+ 18 => 'local2',
30
+ 19 => 'local3',
31
+ 20 => 'local4',
32
+ 21 => 'local5',
33
+ 22 => 'local6',
34
+ 23 => 'local7'
35
+ }
36
+
37
+ PRIORITY_MAP = {
38
+ 0 => 'emerg',
39
+ 1 => 'alert',
40
+ 2 => 'crit',
41
+ 3 => 'err',
42
+ 4 => 'warn',
43
+ 5 => 'notice',
44
+ 6 => 'info',
45
+ 7 => 'debug'
46
+ }
47
+
48
+ def initialize
49
+ super
50
+ require 'cool.io'
51
+ require 'fluent/plugin/socket_util'
52
+ end
53
+
54
+ config_param :port, :integer, :default => 5140
55
+ config_param :bind, :string, :default => '0.0.0.0'
56
+ config_param :tag, :string
57
+
58
+ def configure(conf)
59
+ super
60
+
61
+ parser = TextParser.new
62
+ if parser.configure(conf, false)
63
+ @parser = parser
64
+ else
65
+ @parser = nil
66
+ @time_parser = TextParser::TimeParser.new(TIME_FORMAT)
67
+ end
68
+ end
69
+
70
+ def start
71
+ if @parser
72
+ callback = method(:receive_data_parser)
73
+ else
74
+ callback = method(:receive_data)
75
+ end
76
+
77
+ @loop = Coolio::Loop.new
78
+ @handler = listen(callback)
79
+ @loop.attach(@handler)
80
+
81
+ @thread = Thread.new(&method(:run))
82
+ end
83
+
84
+ def shutdown
85
+ @loop.watchers.each {|w| w.detach }
86
+ @loop.stop
87
+ @handler.close
88
+ @thread.join
89
+ end
90
+
91
+ def run
92
+ @loop.run
93
+ rescue
94
+ $log.error "unexpected error", :error=>$!.to_s
95
+ $log.error_backtrace
96
+ end
97
+
98
+ protected
99
+ def receive_data_parser(data)
100
+ m = SYSLOG_REGEXP.match(data)
101
+ unless m
102
+ $log.debug "invalid syslog message: #{data.dump}"
103
+ return
104
+ end
105
+ pri = m[1].to_i
106
+ text = m[2]
107
+
108
+ time, record = @parser.parse(text)
109
+ unless time && record
110
+ return
111
+ end
112
+
113
+ emit(pri, time, record)
114
+
115
+ rescue
116
+ $log.warn data.dump, :error=>$!.to_s
117
+ $log.debug_backtrace
118
+ end
119
+
120
+ def receive_data(data)
121
+ m = SYSLOG_ALL_REGEXP.match(data)
122
+ unless m
123
+ $log.debug "invalid syslog message", :data=>data
124
+ return
125
+ end
126
+
127
+ pri = nil
128
+ time = nil
129
+ record = {}
130
+
131
+ m.names.each {|name|
132
+ if value = m[name]
133
+ case name
134
+ when "pri"
135
+ pri = value.to_i
136
+ when "time"
137
+ time = @time_parser.parse(value.gsub(/ +/, ' ').gsub(/\.[0-9]+/, ''))
138
+ else
139
+ record[name] = value
140
+ end
141
+ end
142
+ }
143
+
144
+ time ||= Engine.now
145
+
146
+ emit(pri, time, record)
147
+
148
+ rescue
149
+ $log.warn data.dump, :error=>$!.to_s
150
+ $log.debug_backtrace
151
+ end
152
+
153
+ private
154
+
155
+ def listen(callback)
156
+ $log.debug "listening heroku syslog socket on #{@bind}:#{@port}"
157
+ Coolio::TCPServer.new(@bind, @port, TcpHandler, callback)
158
+ end
159
+
160
+ def emit(pri, time, record)
161
+ facility = FACILITY_MAP[pri >> 3]
162
+ priority = PRIORITY_MAP[pri & 0b111]
163
+
164
+ tag = "#{@tag}.#{facility}.#{priority}"
165
+
166
+ Engine.emit(tag, time, record)
167
+ rescue => e
168
+ $log.error "syslog failed to emit", :error => e.to_s, :error_class => e.class.to_s, :tag => tag, :record => Yajl.dump(record)
169
+ end
170
+
171
+ class TcpHandler < Coolio::Socket
172
+ def initialize(io, on_message)
173
+ super(io)
174
+ if io.is_a?(TCPSocket)
175
+ opt = [1, @timeout.to_i].pack('I!I!') # { int l_onoff; int l_linger; }
176
+ io.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
177
+ end
178
+ $log.trace { "accepted fluent socket object_id=#{self.object_id}" }
179
+ @on_message = on_message
180
+ @buffer = "".force_encoding('ASCII-8BIT')
181
+ end
182
+
183
+ def on_connect
184
+ end
185
+
186
+ def on_read(data)
187
+ @buffer << data
188
+ pos = 0
189
+
190
+ # syslog family add "\n" to each message and this seems only way to split messages in tcp stream
191
+ while i = @buffer.index("\n", pos)
192
+ msg = @buffer[pos..i]
193
+
194
+ # Support Octet Counting
195
+ # https://tools.ietf.org/html/rfc6587#section-3.4.1
196
+ m = OCTET_COUNTING_REGEXP.match(msg)
197
+ valid = true
198
+ if m
199
+ msg_len = m[1].to_i - 1
200
+ msg = m[2]
201
+
202
+ if msg_len != msg.length
203
+ $log.debug "invalid syslog message length", :expected => msg_len, :actual => msg.length, :data => msg
204
+ valid = false
205
+ end
206
+ end
207
+ @on_message.call(msg) if valid
208
+ pos = i + 1
209
+ end
210
+ @buffer.slice!(0, pos) if pos > 0
211
+ rescue => e
212
+ $log.error "syslog error", :error => e, :error_class => e.class
213
+ close
214
+ end
215
+
216
+ def on_close
217
+ $log.trace { "closed fluent socket object_id=#{self.object_id}" }
218
+ end
219
+ end
220
+ end
221
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+
15
+ require 'fileutils'
16
+ require 'fluent/log'
17
+ require 'fluent/test'
18
+
19
+ $log = Fluent::Log.new(STDOUT, Fluent::Log::LEVEL_DEBUG)
20
+
21
+ unless defined?(Test::Unit::AssertionFailedError)
22
+ class Test::Unit::AssertionFailedError < StandardError
23
+ end
24
+ end
25
+
26
+ def unused_port
27
+ s = TCPServer.open(0)
28
+ port = s.addr[1]
29
+ s.close
30
+ port
31
+ end
32
+
33
+ def ipv6_enabled?
34
+ require 'socket'
35
+
36
+ begin
37
+ TCPServer.open("::1", 0)
38
+ true
39
+ rescue
40
+ false
41
+ end
42
+ end
43
+
44
+ require 'fluent/plugin/in_heroku_syslog'
45
+
46
+ class Test::Unit::TestCase
47
+ end
@@ -0,0 +1,127 @@
1
+ require 'helper'
2
+
3
+ class HerokuSyslogInputTest < Test::Unit::TestCase
4
+ def setup
5
+ Fluent::Test.setup
6
+ end
7
+
8
+ PORT = unused_port
9
+ CONFIG = %[
10
+ port #{PORT}
11
+ bind 127.0.0.1
12
+ tag heroku.syslog
13
+ ]
14
+
15
+ IPv6_CONFIG = %[
16
+ port #{PORT}
17
+ bind ::1
18
+ tag syslog
19
+ ]
20
+
21
+ def create_driver(conf=CONFIG)
22
+ Fluent::Test::InputTestDriver.new(Fluent::HerokuSyslogInput).configure(conf)
23
+ end
24
+
25
+ def test_configure
26
+ configs = {'127.0.0.1' => CONFIG}
27
+ configs.merge!('::1' => IPv6_CONFIG) if ipv6_enabled?
28
+
29
+ configs.each_pair { |k, v|
30
+ d = create_driver(v)
31
+ assert_equal PORT, d.instance.port
32
+ assert_equal k, d.instance.bind
33
+ }
34
+ end
35
+
36
+ def test_time_format
37
+ configs = {'127.0.0.1' => CONFIG}
38
+ configs.merge!('::1' => IPv6_CONFIG) if ipv6_enabled?
39
+
40
+ configs.each_pair { |k, v|
41
+ d = create_driver(v)
42
+
43
+ tests = [
44
+ {
45
+ 'msg' => "92 <13>1 2014-01-29T06:25:52.589365+00:00 d.916a3e50-efa1-4754-aded-ffffffffffff app web.1 foo\n",
46
+ 'expected' => Time.strptime('2014-01-29T06:25:52+00:00', '%Y-%m-%dT%H:%M:%S%z').to_i
47
+ },
48
+ {
49
+ 'msg' => "92 <13>1 2014-01-30T07:35:00.123456+09:00 d.916a3e50-efa1-4754-aded-ffffffffffff app web.1 bar\n",
50
+ 'expected' => Time.strptime('2014-01-30T07:35:00+09:00', '%Y-%m-%dT%H:%M:%S%z').to_i
51
+ }
52
+ ]
53
+
54
+ d.run do
55
+ tests.each {|test|
56
+ TCPSocket.open(k, PORT) do |s|
57
+ s.send(test['msg'], 0)
58
+ end
59
+ }
60
+ sleep 1
61
+ end
62
+
63
+ emits = d.emits
64
+ $log.debug emits
65
+ emits.each_index {|i|
66
+ $log.debug emits[i][1]
67
+ assert_equal(tests[i]['expected'], emits[i][1])
68
+ }
69
+ }
70
+ end
71
+
72
+ def test_msg_size
73
+ d = create_driver
74
+ tests = create_test_case
75
+
76
+ d.run do
77
+ tests.each {|test|
78
+ TCPSocket.open('127.0.0.1', PORT) do |s|
79
+ s.send(test['msg'], 0)
80
+ end
81
+ }
82
+ sleep 1
83
+ end
84
+
85
+ compare_test_result(d.emits, tests)
86
+ end
87
+
88
+ def test_msg_size_with_same_tcp_connection
89
+ d = create_driver
90
+ tests = create_test_case
91
+
92
+ d.run do
93
+ TCPSocket.open('127.0.0.1', PORT) do |s|
94
+ tests.each {|test|
95
+ s.send(test['msg'], 0)
96
+ }
97
+ end
98
+ sleep 1
99
+ end
100
+
101
+ compare_test_result(d.emits, tests)
102
+ end
103
+
104
+ def create_test_case
105
+ # actual syslog message has "\n"
106
+ msgs = [
107
+ {'msg' => '<13>1 2014-01-01T01:23:45.123456+00:00 d.916a3e50-efa1-4754-aded-ffffffffffff app web.1 ' + 'x' * 100 + "\n", 'expected' => 'x' * 100},
108
+ {'msg' => '<13>1 2014-01-01T01:23:45.123456+00:00 d.916a3e50-efa1-4754-aded-ffffffffffff app web.1 ' + 'x' * 1024 + "\n", 'expected' => 'x' * 1024},
109
+ ]
110
+
111
+ msgs.each do |msg|
112
+ msg['msg'] = "#{msg['msg'].length} #{msg['msg']}"
113
+ end
114
+
115
+ msgs
116
+ end
117
+
118
+ def compare_test_result(emits, tests)
119
+ emits.each_index {|i|
120
+ assert_equal(tests[i]['expected'], emits[i][2]['message'])
121
+ assert_equal('d.916a3e50-efa1-4754-aded-ffffffffffff', emits[i][2]['drain_id'])
122
+ assert_equal('app', emits[i][2]['ident'])
123
+ assert_equal('web.1', emits[i][2]['pid'])
124
+ }
125
+ end
126
+
127
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-heroku-syslog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kazuyuki Honda
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-30 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'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: fluent plugin to drain heroku syslog
42
+ email:
43
+ - hakobera@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".travis.yml"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - fluent-plugin-heroku-syslog.gemspec
55
+ - lib/fluent/plugin/in_heroku_syslog.rb
56
+ - test/helper.rb
57
+ - test/plugin/test_in_heroku_syslog.rb
58
+ homepage: https://github.com/hakobera/fluent-plugin-heroku-syslog
59
+ licenses:
60
+ - APLv2
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.2.0
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: fluent plugin to drain heroku syslog
82
+ test_files:
83
+ - test/helper.rb
84
+ - test/plugin/test_in_heroku_syslog.rb