fluent-plugin-sumologic-cloud-syslog 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,138 @@
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 'date'
16
+
17
+ require_relative 'facility'
18
+ require_relative 'severity'
19
+
20
+ # Syslog protocol https://tools.ietf.org/html/rfc5424
21
+ module SumologicCloudSyslog
22
+ # RFC defined nil value
23
+ NIL_VALUE = '-'
24
+
25
+ # All headers by specification wrapped in single object
26
+ class Header
27
+ attr_accessor :facility, :severity, :version, :timestamp, :hostname, :app_name, :procid, :msgid
28
+
29
+ FACILITIES = {}
30
+ SEVERITIES = {}
31
+
32
+ Facility.setup_constants FACILITIES
33
+ Severity.setup_constants SEVERITIES
34
+
35
+ def initialize
36
+ @timestamp = Time.now
37
+ @severity = 'INFO'
38
+ @facility = 'LOCAL0'
39
+ @version = 1
40
+ @hostname = SumologicCloudSyslog::NIL_VALUE
41
+ @app_name = SumologicCloudSyslog::NIL_VALUE
42
+ @procid = SumologicCloudSyslog::NIL_VALUE
43
+ @msgid = SumologicCloudSyslog::NIL_VALUE
44
+ end
45
+
46
+ def timestamp=(val)
47
+ raise ArgumentError.new("Must provide Time object value instead: #{val.inspect}") unless val.is_a?(Time)
48
+ @timestamp = val
49
+ end
50
+
51
+ def facility=(val)
52
+ raise ArgumentError.new("Invalid facility value: #{val.inspect}") unless FACILITIES.key?(val)
53
+ @facility = val
54
+ end
55
+
56
+ def severity=(val)
57
+ raise ArgumentError.new("Invalid severity value: #{val.inspect}") unless SEVERITIES.key?(val)
58
+ @severity = val
59
+ end
60
+
61
+ # Priority value is calculated by first multiplying the Facility
62
+ # number by 8 and then adding the numerical value of the Severity.
63
+ def pri
64
+ FACILITIES[facility] * 8 + SEVERITIES[severity]
65
+ end
66
+
67
+ def assemble
68
+ [
69
+ "<#{pri}>#{version}",
70
+ timestamp.to_datetime.rfc3339,
71
+ hostname,
72
+ app_name,
73
+ procid,
74
+ msgid
75
+ ].join(' ')
76
+ end
77
+
78
+ def to_s
79
+ assemble
80
+ end
81
+ end
82
+
83
+ # Structured data field
84
+ class StructuredData
85
+ attr_accessor :id, :data
86
+
87
+ def initialize(id)
88
+ @id = id
89
+ @data = {}
90
+ end
91
+
92
+ # Format data structured data to
93
+ # [id k="v" ...]
94
+ def assemble
95
+ parts = [id]
96
+ data.each do |k, v|
97
+ # Characters ", ] and \ must be escaped to prevent any parsing errors
98
+ v = v.gsub(/(\"|\]|\\)/) { |match| '\\' + match }
99
+ parts << "#{k}=\"#{v}\""
100
+ end
101
+ "[#{parts.join(' ')}]"
102
+ end
103
+
104
+ def to_s
105
+ assemble
106
+ end
107
+ end
108
+
109
+ # Message represents full message that can be sent to syslog
110
+ class Message
111
+ attr_accessor :header, :structured_data, :msg
112
+
113
+ def initialize
114
+ @msg = ''
115
+ @structured_data = []
116
+ @header = Header.new
117
+ end
118
+
119
+ def assemble
120
+ # Start with header
121
+ out = [header.to_s]
122
+ # Add all structured data
123
+ if structured_data.length > 0
124
+ out << structured_data.map(&:to_s).join('')
125
+ else
126
+ out << SumologicCloudSyslog::NIL_VALUE
127
+ end
128
+ # Add message
129
+ out << msg if msg.length > 0
130
+ # Message must end with new line delimiter
131
+ out.join(' ') + "\n"
132
+ end
133
+
134
+ def to_s
135
+ assemble
136
+ end
137
+ end
138
+ 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 SumologicCloudSyslog
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,53 @@
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 SumologicCloudSyslog
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
+ def initialize(host, port, cert: nil, key: nil, ssl_version: :TLSv1_2)
26
+ @host = host
27
+ @port = port
28
+ @cert = cert
29
+ @key = key
30
+ @ssl_version = ssl_version
31
+ connect
32
+ end
33
+
34
+ def connect
35
+ tcp = TCPSocket.new(host, port)
36
+
37
+ ctx = OpenSSL::SSL::SSLContext.new
38
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
39
+ ctx.ssl_version = ssl_version
40
+
41
+ ctx.cert = OpenSSL::X509::Certificate.new(File.open(cert)) if cert
42
+ ctx.key = OpenSSL::PKey::RSA.new(File.open(key)) if key
43
+
44
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
45
+ @socket.connect
46
+ end
47
+
48
+ # Forward any methods directly to SSLSocket
49
+ def method_missing(method_sym, *arguments, &block)
50
+ @socket.send(method_sym, *arguments, &block)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
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
+ module SumologicCloudSyslog
16
+ VERSION = '0.1.2'
17
+ end
@@ -0,0 +1,138 @@
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 'helper'
16
+ require 'date'
17
+ require 'minitest/mock'
18
+ require 'fluent/plugin/out_sumologic_cloud_syslog'
19
+
20
+ class SumologicCloudSyslogOutput < Test::Unit::TestCase
21
+ def setup
22
+ Fluent::Test.setup
23
+ @driver = nil
24
+ end
25
+
26
+ def driver(tag='test', conf='')
27
+ @driver ||= Fluent::Test::OutputTestDriver.new(Fluent::SumologicCloudSyslogOutput, tag).configure(conf)
28
+ end
29
+
30
+ def sample_record
31
+ {
32
+ "app_name" => "app",
33
+ "hostname" => "host",
34
+ "procid" => $$,
35
+ "msgid" => 1000,
36
+ "message" => "MESSAGE",
37
+ "severity" => "PANIC",
38
+ }
39
+ end
40
+
41
+ def mock_logger
42
+ io = StringIO.new
43
+ io.set_encoding('utf-8')
44
+ logger = ::SumologicCloudSyslog::Logger.new(io, "TOKEN")
45
+ end
46
+
47
+ def test_configure
48
+ config = %{
49
+ host syslog.collection.us1.sumologic.com
50
+ port 6514
51
+ cert
52
+ key
53
+ token 1234567890
54
+ }
55
+ instance = driver('test', config).instance
56
+
57
+ assert_equal 'syslog.collection.us1.sumologic.com', instance.host
58
+ assert_equal '6514', instance.port
59
+ assert_equal '', instance.cert
60
+ assert_equal '', instance.key
61
+ assert_equal '1234567890', instance.token
62
+ end
63
+
64
+ def test_default_emit
65
+ config = %{
66
+ host syslog.collection.us1.sumologic.com
67
+ port 6514
68
+ cert
69
+ key
70
+ token 1234567890
71
+ }
72
+ instance = driver('test', config).instance
73
+
74
+ time = Time.now
75
+ record = sample_record
76
+ logger = mock_logger
77
+
78
+ instance.stub(:new_logger, logger) do
79
+ chain = Minitest::Mock.new
80
+ chain.expect(:next, nil)
81
+ instance.emit('test', {time.to_i => record}, chain)
82
+ end
83
+
84
+ formatted_time = time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
85
+ assert_equal logger.transport.string, "<134>1 #{time.to_datetime.rfc3339} - - - - [TOKEN] #{formatted_time}\ttest\t#{record.to_json.to_s}\n\n"
86
+ end
87
+
88
+ def test_message_headers_mapping
89
+ config = %{
90
+ host syslog.collection.us1.sumologic.com
91
+ port 6514
92
+ cert
93
+ key
94
+ token 1234567890
95
+ hostname_key hostname
96
+ procid_key procid
97
+ app_name_key app_name
98
+ msgid_key msgid
99
+ }
100
+ instance = driver('test', config).instance
101
+
102
+ time = Time.now
103
+ record = sample_record
104
+ logger = mock_logger
105
+
106
+ instance.stub(:new_logger, logger) do
107
+ chain = Minitest::Mock.new
108
+ chain.expect(:next, nil)
109
+ instance.emit('test', {time.to_i => record}, chain)
110
+ end
111
+
112
+ assert_true logger.transport.string.start_with?("<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [TOKEN]")
113
+ end
114
+
115
+ def test_message_severity_mapping
116
+ config = %{
117
+ host syslog.collection.us1.sumologic.com
118
+ port 6514
119
+ cert
120
+ key
121
+ token 1234567890
122
+ severity_key severity
123
+ }
124
+ instance = driver('test', config).instance
125
+
126
+ time = Time.now
127
+ record = sample_record
128
+ logger = mock_logger
129
+
130
+ instance.stub(:new_logger, logger) do
131
+ chain = Minitest::Mock.new
132
+ chain.expect(:next, nil)
133
+ instance.emit('test', {time.to_i => record}, chain)
134
+ end
135
+
136
+ assert_true logger.transport.string.start_with?("<128>1")
137
+ end
138
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,33 @@
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 'coveralls'
16
+ require 'simplecov'
17
+
18
+ SimpleCov.start
19
+
20
+ Coveralls.wear! if ENV['TRAVIS']
21
+
22
+ # Fluentd sets default encoding to ASCII-8BIT, but coverall can load git data which can contain UTF-8 characters
23
+ at_exit do
24
+ Encoding.default_internal = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_internal)
25
+ Encoding.default_external = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_external)
26
+ end
27
+
28
+ require 'test/unit'
29
+ require 'fluent/test'
30
+ require 'minitest/pride'
31
+
32
+ require 'webmock/test_unit'
33
+ WebMock.disable_net_connect!
@@ -0,0 +1,47 @@
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 'helper'
16
+ require 'date'
17
+ require 'sumologic_cloud_syslog/logger'
18
+
19
+ class Logger < Test::Unit::TestCase
20
+ def test_logger_defaults
21
+ io = StringIO.new
22
+ l = SumologicCloudSyslog::Logger.new(io, "TOKEN")
23
+ time = Time.now
24
+ l.log(:WARN, "MESSAGE", time: time)
25
+ assert_equal io.string, "<132>1 #{time.to_datetime.rfc3339} - - - - [TOKEN] MESSAGE\n"
26
+ end
27
+
28
+ def test_logger_default_headers
29
+ io = StringIO.new
30
+ l = SumologicCloudSyslog::Logger.new(io, "TOKEN")
31
+ l.hostname("hostname")
32
+ l.app_name("appname")
33
+ l.procid($$)
34
+ l.facility("SYSLOG")
35
+ time = Time.now
36
+ l.log(:WARN, "MESSAGE", time: time)
37
+ assert_equal io.string, "<44>1 #{time.to_datetime.rfc3339} hostname appname #{$$} - [TOKEN] MESSAGE\n"
38
+ end
39
+
40
+ def test_logger_closed
41
+ io = StringIO.new
42
+ l = SumologicCloudSyslog::Logger.new(io, "TOKEN")
43
+ assert_false l.closed?
44
+ l.close
45
+ assert_true l.closed?
46
+ end
47
+ end