fluent-plugin-syslog-tls 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +15 -0
- data/.gitignore +40 -0
- data/.travis.yml +29 -0
- data/Gemfile +19 -0
- data/LICENSE +235 -0
- data/README.md +83 -0
- data/Rakefile +24 -0
- data/docs/configuration.md +121 -0
- data/fluent-plugin-syslog-tls.gemspec +44 -0
- data/lib/fluent/plugin/out_syslog_tls.rb +133 -0
- data/lib/syslog_tls/facility.rb +46 -0
- data/lib/syslog_tls/logger.rb +82 -0
- data/lib/syslog_tls/lookup_from_const.rb +36 -0
- data/lib/syslog_tls/protocol.rb +140 -0
- data/lib/syslog_tls/severity.rb +30 -0
- data/lib/syslog_tls/ssl_transport.rb +74 -0
- data/lib/syslog_tls/version.rb +18 -0
- data/test/fluent/test_out_syslog_tls.rb +200 -0
- data/test/helper.rb +34 -0
- data/test/ssl.rb +51 -0
- data/test/syslog_tls/test_logger.rb +48 -0
- data/test/syslog_tls/test_protocol.rb +150 -0
- data/test/syslog_tls/test_ssl_transport.rb +54 -0
- metadata +185 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
2
|
+
# Copyright 2016 t.e.morgan.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require 'date'
|
17
|
+
|
18
|
+
require_relative 'facility'
|
19
|
+
require_relative 'severity'
|
20
|
+
|
21
|
+
# Syslog protocol https://tools.ietf.org/html/rfc5424
|
22
|
+
module SyslogTls
|
23
|
+
# RFC defined nil value
|
24
|
+
NIL_VALUE = '-'
|
25
|
+
|
26
|
+
# All headers by specification wrapped in single object
|
27
|
+
class Header
|
28
|
+
attr_accessor :facility, :severity, :version, :timestamp, :hostname, :app_name, :procid, :msgid
|
29
|
+
|
30
|
+
FACILITIES = {}
|
31
|
+
SEVERITIES = {}
|
32
|
+
|
33
|
+
Facility.setup_constants FACILITIES
|
34
|
+
Severity.setup_constants SEVERITIES
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@timestamp = Time.now
|
38
|
+
@severity = 'INFO'
|
39
|
+
@facility = 'LOCAL0'
|
40
|
+
@version = 1
|
41
|
+
@hostname = NIL_VALUE
|
42
|
+
@app_name = NIL_VALUE
|
43
|
+
@procid = NIL_VALUE
|
44
|
+
@msgid = NIL_VALUE
|
45
|
+
end
|
46
|
+
|
47
|
+
def timestamp=(val)
|
48
|
+
raise ArgumentError.new("Must provide Time object value instead: #{val.inspect}") unless val.is_a?(Time)
|
49
|
+
@timestamp = val
|
50
|
+
end
|
51
|
+
|
52
|
+
def facility=(val)
|
53
|
+
raise ArgumentError.new("Invalid facility value: #{val.inspect}") unless FACILITIES.key?(val)
|
54
|
+
@facility = val
|
55
|
+
end
|
56
|
+
|
57
|
+
def severity=(val)
|
58
|
+
raise ArgumentError.new("Invalid severity value: #{val.inspect}") unless SEVERITIES.key?(val)
|
59
|
+
@severity = val
|
60
|
+
end
|
61
|
+
|
62
|
+
# Priority value is calculated by first multiplying the Facility
|
63
|
+
# number by 8 and then adding the numerical value of the Severity.
|
64
|
+
def pri
|
65
|
+
FACILITIES[facility] * 8 + SEVERITIES[severity]
|
66
|
+
end
|
67
|
+
|
68
|
+
def assemble
|
69
|
+
[
|
70
|
+
"<#{pri}>#{version}",
|
71
|
+
timestamp.to_datetime.rfc3339,
|
72
|
+
hostname,
|
73
|
+
app_name,
|
74
|
+
procid,
|
75
|
+
msgid
|
76
|
+
].join(' ')
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
assemble
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Structured data field
|
85
|
+
class StructuredData
|
86
|
+
attr_accessor :id, :data
|
87
|
+
|
88
|
+
def initialize(id)
|
89
|
+
@id = id
|
90
|
+
@data = {}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Format data structured data to
|
94
|
+
# [id k="v" ...]
|
95
|
+
def assemble
|
96
|
+
return NIL_VALUE unless id
|
97
|
+
parts = [id]
|
98
|
+
data.each do |k, v|
|
99
|
+
# Characters ", ] and \ must be escaped to prevent any parsing errors
|
100
|
+
v = v.gsub(/(\"|\]|\\)/) { |match| '\\' + match }
|
101
|
+
parts << "#{k}=\"#{v}\""
|
102
|
+
end
|
103
|
+
"[#{parts.join(' ')}]"
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_s
|
107
|
+
assemble
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Message represents full message that can be sent to syslog
|
112
|
+
class Message
|
113
|
+
attr_accessor :header, :structured_data, :msg
|
114
|
+
|
115
|
+
def initialize
|
116
|
+
@msg = ''
|
117
|
+
@structured_data = []
|
118
|
+
@header = Header.new
|
119
|
+
end
|
120
|
+
|
121
|
+
def assemble
|
122
|
+
# Start with header
|
123
|
+
out = [header.to_s]
|
124
|
+
# Add all structured data
|
125
|
+
if structured_data.length > 0
|
126
|
+
out << structured_data.map(&:to_s).join('')
|
127
|
+
else
|
128
|
+
out << NIL_VALUE
|
129
|
+
end
|
130
|
+
# Add message
|
131
|
+
out << msg if msg.length > 0
|
132
|
+
# Message must end with new line delimiter
|
133
|
+
out.join(' ') + "\n"
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_s
|
137
|
+
assemble
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
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.
|
14
|
+
|
15
|
+
require_relative 'lookup_from_const'
|
16
|
+
|
17
|
+
module SyslogTls
|
18
|
+
module Severity
|
19
|
+
extend LookupFromConst
|
20
|
+
EMERG = PANIC = 0
|
21
|
+
ALERT = 1
|
22
|
+
CRIT = 2
|
23
|
+
ERR = ERROR = 3
|
24
|
+
WARN = WARNING = 4
|
25
|
+
NOTICE = 5
|
26
|
+
INFO = 6
|
27
|
+
DEBUG = 7
|
28
|
+
NONE = 10
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
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.
|
14
|
+
|
15
|
+
require 'socket'
|
16
|
+
require 'openssl'
|
17
|
+
|
18
|
+
module SyslogTls
|
19
|
+
# Supports SSL connection to remote host
|
20
|
+
class SSLTransport
|
21
|
+
attr_accessor :socket
|
22
|
+
|
23
|
+
attr_reader :host, :port, :cert, :key, :ssl_version
|
24
|
+
|
25
|
+
attr_writer :retries
|
26
|
+
|
27
|
+
def initialize(host, port, cert: nil, key: nil, ssl_version: :TLSv1_2, max_retries: 1)
|
28
|
+
@host = host
|
29
|
+
@port = port
|
30
|
+
@cert = cert
|
31
|
+
@key = key
|
32
|
+
@ssl_version = ssl_version
|
33
|
+
@retries = max_retries
|
34
|
+
connect
|
35
|
+
end
|
36
|
+
|
37
|
+
def connect
|
38
|
+
@socket = get_ssl_connection
|
39
|
+
@socket.connect
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_ssl_connection
|
43
|
+
tcp = TCPSocket.new(host, port)
|
44
|
+
|
45
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
46
|
+
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
47
|
+
ctx.ssl_version = ssl_version
|
48
|
+
|
49
|
+
ctx.cert = OpenSSL::X509::Certificate.new(File.open(cert)) if cert
|
50
|
+
ctx.key = OpenSSL::PKey::RSA.new(File.open(key)) if key
|
51
|
+
OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Allow to retry on failed writes
|
55
|
+
def write(s)
|
56
|
+
begin
|
57
|
+
retry_id ||= 0
|
58
|
+
@socket.send(:write, s)
|
59
|
+
rescue => e
|
60
|
+
if (retry_id += 1) < @retries
|
61
|
+
connect
|
62
|
+
retry
|
63
|
+
else
|
64
|
+
raise e
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Forward any methods directly to SSLSocket
|
70
|
+
def method_missing(method_sym, *arguments, &block)
|
71
|
+
@socket.send(method_sym, *arguments, &block)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
2
|
+
# Copyright 2016 t.e.morgan.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
module SyslogTls
|
17
|
+
VERSION = '0.5.0'
|
18
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
2
|
+
# Copyright 2016 t.e.morgan.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require 'helper'
|
17
|
+
require 'ssl'
|
18
|
+
require 'date'
|
19
|
+
require 'minitest/mock'
|
20
|
+
require 'fluent/plugin/out_syslog_tls'
|
21
|
+
|
22
|
+
class SyslogTlsOutputTest < Test::Unit::TestCase
|
23
|
+
include SSLTestHelper
|
24
|
+
|
25
|
+
def setup
|
26
|
+
Fluent::Test.setup
|
27
|
+
@driver = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def driver(tag='test', conf='')
|
31
|
+
@driver ||= Fluent::Test::OutputTestDriver.new(Fluent::SyslogTlsOutput, tag).configure(conf)
|
32
|
+
end
|
33
|
+
|
34
|
+
def sample_record
|
35
|
+
{
|
36
|
+
"app_name" => "app",
|
37
|
+
"hostname" => "host",
|
38
|
+
"procid" => $$,
|
39
|
+
"msgid" => 1000,
|
40
|
+
"message" => "MESSAGE",
|
41
|
+
"severity" => "PANIC",
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def mock_logger(token='TOKEN')
|
46
|
+
io = StringIO.new
|
47
|
+
io.set_encoding('utf-8')
|
48
|
+
logger = ::SyslogTls::Logger.new(io, token)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_configure
|
52
|
+
config = %{
|
53
|
+
host syslog.collection.us1.sumologic.com
|
54
|
+
port 6514
|
55
|
+
cert
|
56
|
+
key
|
57
|
+
token 1234567890
|
58
|
+
}
|
59
|
+
instance = driver('test', config).instance
|
60
|
+
|
61
|
+
assert_equal 'syslog.collection.us1.sumologic.com', instance.host
|
62
|
+
assert_equal '6514', instance.port
|
63
|
+
assert_equal '', instance.cert
|
64
|
+
assert_equal '', instance.key
|
65
|
+
assert_equal '1234567890', instance.token
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_default_emit
|
69
|
+
config = %{
|
70
|
+
host syslog.collection.us1.sumologic.com
|
71
|
+
port 6514
|
72
|
+
cert
|
73
|
+
key
|
74
|
+
}
|
75
|
+
instance = driver('test', config).instance
|
76
|
+
|
77
|
+
time = Time.now
|
78
|
+
record = sample_record
|
79
|
+
logger = mock_logger(instance.token)
|
80
|
+
|
81
|
+
instance.stub(:new_logger, logger) do
|
82
|
+
chain = Minitest::Mock.new
|
83
|
+
chain.expect(:next, nil)
|
84
|
+
instance.emit('test', {time.to_i => record}, chain)
|
85
|
+
end
|
86
|
+
|
87
|
+
assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - - #{record.to_json.to_s}\n\n", logger.transport.string
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_message_headers_mapping
|
91
|
+
config = %{
|
92
|
+
host syslog.collection.us1.sumologic.com
|
93
|
+
port 6514
|
94
|
+
cert
|
95
|
+
key
|
96
|
+
token 1234567890
|
97
|
+
hostname_key hostname
|
98
|
+
procid_key procid
|
99
|
+
app_name_key app_name
|
100
|
+
msgid_key msgid
|
101
|
+
}
|
102
|
+
instance = driver('test', config).instance
|
103
|
+
|
104
|
+
time = Time.now
|
105
|
+
record = sample_record
|
106
|
+
logger = mock_logger
|
107
|
+
|
108
|
+
instance.stub(:new_logger, logger) do
|
109
|
+
chain = Minitest::Mock.new
|
110
|
+
chain.expect(:next, nil)
|
111
|
+
instance.emit('test', {time.to_i => record}, chain)
|
112
|
+
end
|
113
|
+
|
114
|
+
assert_true logger.transport.string.start_with?("<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [TOKEN]")
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_message_severity_mapping
|
118
|
+
config = %{
|
119
|
+
host syslog.collection.us1.sumologic.com
|
120
|
+
port 6514
|
121
|
+
cert
|
122
|
+
key
|
123
|
+
token 1234567890
|
124
|
+
severity_key severity
|
125
|
+
}
|
126
|
+
instance = driver('test', config).instance
|
127
|
+
|
128
|
+
time = Time.now
|
129
|
+
record = sample_record
|
130
|
+
logger = mock_logger
|
131
|
+
|
132
|
+
instance.stub(:new_logger, logger) do
|
133
|
+
chain = Minitest::Mock.new
|
134
|
+
chain.expect(:next, nil)
|
135
|
+
instance.emit('test', {time.to_i => record}, chain)
|
136
|
+
end
|
137
|
+
|
138
|
+
assert_true logger.transport.string.start_with?("<128>1")
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_formatter
|
142
|
+
config = %{
|
143
|
+
host syslog.collection.us1.sumologic.com
|
144
|
+
port 6514
|
145
|
+
cert
|
146
|
+
key
|
147
|
+
token 1234567890
|
148
|
+
format out_file
|
149
|
+
utc true
|
150
|
+
}
|
151
|
+
instance = driver('test', config).instance
|
152
|
+
|
153
|
+
time = Time.now
|
154
|
+
record = sample_record
|
155
|
+
logger = mock_logger
|
156
|
+
|
157
|
+
instance.stub(:new_logger, logger) do
|
158
|
+
chain = Minitest::Mock.new
|
159
|
+
chain.expect(:next, nil)
|
160
|
+
instance.emit('test', {time.to_i => record}, chain)
|
161
|
+
end
|
162
|
+
|
163
|
+
formatted_time = time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
164
|
+
assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - [TOKEN] #{formatted_time}\ttest\t#{record.to_json.to_s}\n\n", logger.transport.string
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_ssl
|
168
|
+
time = Time.now
|
169
|
+
record = sample_record
|
170
|
+
|
171
|
+
server = ssl_server
|
172
|
+
st = Thread.new {
|
173
|
+
client = server.accept
|
174
|
+
assert_equal "<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [1234567890] #{record.to_json.to_s}\n", client.gets
|
175
|
+
client.close
|
176
|
+
}
|
177
|
+
|
178
|
+
config = %{
|
179
|
+
host localhost
|
180
|
+
port #{server.addr[1]}
|
181
|
+
cert
|
182
|
+
key
|
183
|
+
token 1234567890
|
184
|
+
hostname_key hostname
|
185
|
+
procid_key procid
|
186
|
+
app_name_key app_name
|
187
|
+
msgid_key msgid
|
188
|
+
}
|
189
|
+
instance = driver('test', config).instance
|
190
|
+
|
191
|
+
chain = Minitest::Mock.new
|
192
|
+
chain.expect(:next, nil)
|
193
|
+
|
194
|
+
SyslogTls::SSLTransport.stub_any_instance(:get_ssl_connection, ssl_client) do
|
195
|
+
instance.emit('test', {time.to_i => record}, chain)
|
196
|
+
end
|
197
|
+
|
198
|
+
st.join
|
199
|
+
end
|
200
|
+
end
|