fluent-plugin-heroku-syslog 0.0.1

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