fluent-plugin-vmware-log-intelligence 2.0.5 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,179 +1,201 @@
1
- # Copyright (c) 2013 ablagoev
2
- # Copyright 2018 VMware, Inc.
3
- # SPDX-License-Identifier: MIT
4
-
5
- require "fluent/plugin/output"
6
- require "fluent/plugin/http_client"
7
-
8
- module Fluent::Plugin
9
- class LogIntelligenceOutput < Output
10
- Fluent::Plugin.register_output('vmware_log_intelligence', self)
11
-
12
- config_param :endpoint_url, :string
13
- config_param :http_retry_statuses, :string, default: ''
14
- config_param :read_timeout, :integer, default: 60
15
- config_param :open_timeout, :integer, default: 60
16
- # config_param :use_ssl, :bool, :default => false
17
- config_param :verify_ssl, :bool, :default => true
18
- config_param :rate_limit_msec, :integer, :default => 0
19
- # Keys from log event whose values should be added as log message/text
20
- # to log-intelligence. Note these key/value pairs won't be added as metadata/fields
21
- config_param :log_text_keys, :array, default: ["log", "message", "msg"], value_type: :string
22
- # Flatten hashes to create one key/val pair w/o losing log data
23
- config_param :flatten_hashes, :bool, :default => true
24
- # Seperator to use for joining flattened keys
25
- config_param :flatten_hashes_separator, :string, :default => "_"
26
-
27
- config_section :buffer do
28
- config_set_default :@type, "memory"
29
- config_set_default :chunk_keys, []
30
- config_set_default :timekey_use_utc, true
31
- end
32
-
33
- def initialize
34
- super
35
- require 'http'
36
- require 'uri'
37
- end
38
-
39
- def validate_uri(uri_string)
40
- unless uri_string =~ /^#{URI.regexp}$/
41
- fail Fluent::ConfigError, 'endpoint_url invalid'
42
- end
43
-
44
- begin
45
- @uri = URI.parse(uri_string)
46
- rescue URI::InvalidURIError
47
- raise Fluent::ConfigError, 'endpoint_url invalid'
48
- end
49
- end
50
-
51
- def retrieve_headers(conf)
52
- headers = {}
53
- conf.elements.each do |element|
54
- if element.name == 'headers'
55
- headers = element.to_hash
56
- end
57
- end
58
- headers
59
- end
60
-
61
- def shorten_key(key)
62
- # LINT doesn't allow some characters in field 'name'
63
- # like '/', '-', '\', '.', etc. so replace them with @flatten_hashes_separator
64
- key = key.gsub(/[\/\.\-\\]/,@flatten_hashes_separator).downcase
65
- key
66
- end
67
-
68
- def create_lint_event(record)
69
- flattened_records = {}
70
- merged_records = {}
71
- if @flatten_hashes
72
- flattened_records = flatten_record(record, [])
73
- else
74
- flattened_records = record
75
- end
76
-
77
- keys = []
78
- log = ''
79
- flattened_records.each do |key, value|
80
- begin
81
- next if value.nil?
82
- # LINT doesn't support duplicate fields, make unique names by appending underscore
83
- key = shorten_key(key)
84
- if keys.include?(key)
85
- value = merged_records[key] + " " + value
86
- end
87
- keys.push(key)
88
- key.force_encoding("utf-8")
89
-
90
- if value.is_a?(String)
91
- value.force_encoding("utf-8")
92
- end
93
- end
94
-
95
- if @log_text_keys.include?(key)
96
- if log != "#{value}"
97
- if log.empty?
98
- log = "#{value}"
99
- else
100
- log += " #{value}"
101
- end
102
- end
103
- else
104
- merged_records[key] = value
105
- end
106
- end
107
- merged_records["text"] = log
108
-
109
- if log == "\\n"
110
- {}
111
- else
112
- merged_records
113
- end
114
- end
115
-
116
- def flatten_record(record, prefix=[])
117
- ret = {}
118
-
119
- case record
120
- when Hash
121
- record.each do |key, value|
122
- if @log_text_keys.include?(key)
123
- ret.merge!({key.to_s => value})
124
- else
125
- ret.merge! flatten_record(value, prefix + [key.to_s])
126
- end
127
- end
128
- when Array
129
- record.each do |value|
130
- ret.merge! flatten_record(value, prefix)
131
- end
132
- else
133
- return {prefix.join(@flatten_hashes_separator) => record}
134
- end
135
- ret
136
- end
137
- def configure(conf)
138
- super
139
- validate_uri(@endpoint_url)
140
-
141
- @statuses = @http_retry_statuses.split(',').map { |status| status.to_i }
142
- @statuses = [] if @statuses.nil?
143
-
144
- @headers = retrieve_headers(conf)
145
-
146
- @http_client = Fluent::Plugin::HttpClient.new(
147
- @endpoint_url, @verify_ssl, @headers, @statuses,
148
- @open_timeout, @read_timeout, @log)
149
- end
150
-
151
- def start
152
- super
153
- end
154
-
155
- def shutdown
156
- super
157
- begin
158
- @http_client.close if @http_client
159
- rescue
160
- end
161
- end
162
-
163
- def write(chunk)
164
- is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
165
- if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
166
- @log.info('Dropped request due to rate limiting')
167
- return
168
- end
169
-
170
- data = []
171
- chunk.each do |time, record|
172
- data << create_lint_event(record)
173
- end
174
-
175
- @last_request_time = Time.now.to_f
176
- @http_client.post(JSON.dump(data))
177
- end
178
- end
179
- end
1
+ # Copyright (c) 2013 ablagoev
2
+ # Copyright 2018 VMware, Inc.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+
6
+ require 'zlib'
7
+ require "fluent/plugin/output"
8
+ require "fluent/plugin/http_client"
9
+
10
+ module Fluent::Plugin
11
+ class LogIntelligenceOutput < Output
12
+ Fluent::Plugin.register_output('vmware_log_intelligence', self)
13
+
14
+ config_param :http_compress, :bool, :default => false
15
+ config_param :endpoint_url, :string
16
+ config_param :bearer_token, :string, default: ''
17
+ config_param :http_retry_statuses, :string, default: ''
18
+ config_param :read_timeout, :integer, default: 60
19
+ config_param :open_timeout, :integer, default: 60
20
+ # config_param :use_ssl, :bool, :default => false
21
+ config_param :verify_ssl, :bool, :default => true
22
+ config_param :rate_limit_msec, :integer, :default => 0
23
+ # Keys from log event whose values should be added as log message/text
24
+ # to log-intelligence. Note these key/value pairs won't be added as metadata/fields
25
+ config_param :log_text_keys, :array, default: ["log", "message", "msg"], value_type: :string
26
+ # Flatten hashes to create one key/val pair w/o losing log data
27
+ config_param :flatten_hashes, :bool, :default => true
28
+ # Seperator to use for joining flattened keys
29
+ config_param :flatten_hashes_separator, :string, :default => "_"
30
+
31
+ config_section :buffer do
32
+ config_set_default :@type, "memory"
33
+ config_set_default :chunk_keys, []
34
+ config_set_default :timekey_use_utc, true
35
+ end
36
+
37
+ def initialize
38
+ super
39
+ require 'http'
40
+ require 'uri'
41
+ end
42
+
43
+ def validate_uri(uri_string)
44
+ unless uri_string =~ /^#{URI.regexp}$/
45
+ fail Fluent::ConfigError, 'endpoint_url invalid'
46
+ end
47
+
48
+ begin
49
+ @uri = URI.parse(uri_string)
50
+ rescue URI::InvalidURIError
51
+ raise Fluent::ConfigError, 'endpoint_url invalid'
52
+ end
53
+ end
54
+
55
+ def retrieve_headers(conf)
56
+ headers = {}
57
+ conf.elements.each do |element|
58
+ if @http_compress
59
+ set_gzip_header(element)
60
+ end
61
+ if element.name == 'headers'
62
+ if @bearer_token != ''
63
+ element['Authorization'] = 'Bearer ' + @bearer_token
64
+ end
65
+ headers = element.to_hash
66
+ end
67
+ end
68
+ headers
69
+ end
70
+
71
+ def set_gzip_header(element)
72
+ element['Content-Encoding'] = 'gzip'
73
+ element
74
+ end
75
+
76
+ def shorten_key(key)
77
+ # LINT doesn't allow some characters in field 'name'
78
+ # like '/', '-', '\', '.', etc. so replace them with @flatten_hashes_separator
79
+ key = key.gsub(/[\/\.\-\\]/,@flatten_hashes_separator).downcase
80
+ key
81
+ end
82
+
83
+ def create_lint_event(record)
84
+ flattened_records = {}
85
+ merged_records = {}
86
+ if @flatten_hashes
87
+ flattened_records = flatten_record(record, [])
88
+ else
89
+ flattened_records = record
90
+ end
91
+
92
+ keys = []
93
+ log = ''
94
+ flattened_records.each do |key, value|
95
+ begin
96
+ next if value.nil?
97
+ # LINT doesn't support duplicate fields, make unique names by appending underscore
98
+ key = shorten_key(key)
99
+ if keys.include?(key)
100
+ value = merged_records[key] + " " + value
101
+ end
102
+ keys.push(key)
103
+ key.force_encoding("utf-8")
104
+
105
+ if value.is_a?(String)
106
+ value.force_encoding("utf-8")
107
+ end
108
+ end
109
+
110
+ if @log_text_keys.include?(key)
111
+ if log != "#{value}"
112
+ if log.empty?
113
+ log = "#{value}"
114
+ else
115
+ log += " #{value}"
116
+ end
117
+ end
118
+ else
119
+ merged_records[key] = value
120
+ end
121
+ end
122
+ merged_records["text"] = log
123
+
124
+ if log == "\\n"
125
+ {}
126
+ else
127
+ merged_records
128
+ end
129
+ end
130
+
131
+ def flatten_record(record, prefix=[])
132
+ ret = {}
133
+
134
+ case record
135
+ when Hash
136
+ record.each do |key, value|
137
+ if @log_text_keys.include?(key)
138
+ ret.merge!({key.to_s => value})
139
+ else
140
+ ret.merge! flatten_record(value, prefix + [key.to_s])
141
+ end
142
+ end
143
+ when Array
144
+ record.each do |value|
145
+ ret.merge! flatten_record(value, prefix)
146
+ end
147
+ else
148
+ return {prefix.join(@flatten_hashes_separator) => record}
149
+ end
150
+ ret
151
+ end
152
+
153
+ def configure(conf)
154
+ super
155
+ validate_uri(@endpoint_url)
156
+
157
+ @statuses = @http_retry_statuses.split(',').map { |status| status.to_i }
158
+ @statuses = [] if @statuses.nil?
159
+
160
+ @headers = retrieve_headers(conf)
161
+
162
+ @http_client = Fluent::Plugin::HttpClient.new(
163
+ @endpoint_url, @verify_ssl, @headers, @statuses,
164
+ @open_timeout, @read_timeout, @log)
165
+ end
166
+
167
+ def start
168
+ super
169
+ end
170
+
171
+ def shutdown
172
+ super
173
+ begin
174
+ @http_client.close if @http_client
175
+ rescue
176
+ end
177
+ end
178
+
179
+ def write(chunk)
180
+ is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
181
+ if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
182
+ @log.info('Dropped request due to rate limiting')
183
+ return
184
+ end
185
+
186
+ data = []
187
+ chunk.each do |time, record|
188
+ data << create_lint_event(record)
189
+ end
190
+
191
+ if @http_compress
192
+ gzip_body = Zlib::GzipWriter.new(StringIO.new)
193
+ gzip_body << data.to_json
194
+ @http_client.post(gzip_body.close.string)
195
+ else
196
+ @last_request_time = Time.now.to_f
197
+ @http_client.post(JSON.dump(data))
198
+ end
199
+ end
200
+ end
201
+ end
@@ -1,31 +1,31 @@
1
- # Copyright (c) 2013 ablagoev
2
- # Copyright 2018 VMware, Inc.
3
- # SPDX-License-Identifier: MIT
4
-
5
- require 'coveralls'
6
- Coveralls.wear!
7
-
8
- require 'rubygems'
9
- require 'bundler'
10
-
11
- begin
12
- Bundler.setup(:default, :development)
13
- rescue Bundler::BundlerError => e
14
- $stderr.puts e.message
15
- $stderr.puts 'Run `bundle install` to install missing gems'
16
- exit e.status_code
17
- end
18
-
19
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
20
- $LOAD_PATH.unshift(File.dirname(__FILE__))
21
-
22
- require 'test-unit'
23
- require 'fluent/test'
24
- require "fluent/test/driver/output"
25
- require "fluent/test/helpers"
26
- Test::Unit::TestCase.include(Fluent::Test::Helpers)
27
- Test::Unit::TestCase.extend(Fluent::Test::Helpers)
28
-
29
- require 'fluent/plugin/out_vmware_log_intelligence'
30
- require 'webmock/test_unit'
31
- WebMock.disable_net_connect!
1
+ # Copyright (c) 2013 ablagoev
2
+ # Copyright 2018 VMware, Inc.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ require 'coveralls'
6
+ Coveralls.wear!
7
+
8
+ require 'rubygems'
9
+ require 'bundler'
10
+
11
+ begin
12
+ Bundler.setup(:default, :development)
13
+ rescue Bundler::BundlerError => e
14
+ $stderr.puts e.message
15
+ $stderr.puts 'Run `bundle install` to install missing gems'
16
+ exit e.status_code
17
+ end
18
+
19
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
20
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
21
+
22
+ require 'test-unit'
23
+ require 'fluent/test'
24
+ require "fluent/test/driver/output"
25
+ require "fluent/test/helpers"
26
+ Test::Unit::TestCase.include(Fluent::Test::Helpers)
27
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)
28
+
29
+ require 'fluent/plugin/out_vmware_log_intelligence'
30
+ require 'webmock/test_unit'
31
+ WebMock.disable_net_connect!
@@ -1,95 +1,95 @@
1
- # Copyright 2018 VMware, Inc.
2
- # SPDX-License-Identifier: MIT
3
-
4
- require 'helper'
5
-
6
- class HttpClientTest < Test::Unit::TestCase
7
- DEFAULT_URL = "https://local.endpoint:3000/dummy/xyz"
8
-
9
- def stub_server_out_of_quota(url=DEFAULT_URL)
10
- stub_request(:post, url).to_return(:status => [429, "User out of ingestion quota."])
11
- end
12
-
13
- def test_check_quota
14
- http_client = create_http_client()
15
- assert_equal http_client.check_quota, true
16
-
17
- stub_server_out_of_quota
18
- http_client.post(JSON.dump(sample_record()))
19
- assert_equal http_client.check_quota, false
20
- end
21
-
22
- def stub_server_unavailable(url=DEFAULT_URL)
23
- stub_request(:post, url).to_return(:status => [503, "Service Unavailable"])
24
- end
25
-
26
- def stub_server_returns_500(url=DEFAULT_URL)
27
- stub_request(:post, url).to_return(:status => [500, "Internal service error"])
28
- end
29
-
30
- def stub_server_raise_error(url=DEFAULT_URL)
31
- stub_request(:post, url).with do |req|
32
- raise IOError
33
- end
34
- end
35
-
36
- def stub_post_logs(url=DEFAULT_URL)
37
- stub_request(:post, url)
38
- .with(
39
- body: "[{\"field1\":26,\"field2\":\"value26\"},{\"field1\":27,\"field2\":\"value27\"},{\"field1\":28,\"field2\":\"value28\"}]",
40
- headers: {
41
- 'Authorization'=>'Bearer EdaNNN68y',
42
- 'Connection'=>'Keep-Alive',
43
- 'Content-Type'=>'application/json',
44
- 'Format'=>'syslog',
45
- 'Structure'=>'simple'
46
- })
47
- .to_return(:status => 200, :body => "ok")
48
- end
49
-
50
- def sample_record()
51
- [
52
- {'field1' => 26, 'field2' => 'value26'},
53
- {'field1' => 27, 'field2' => 'value27'},
54
- {'field1' => 28, 'field2' => 'value28'}
55
- ]
56
- end
57
-
58
- def create_http_client
59
- Fluent::Plugin::HttpClient.new(
60
- DEFAULT_URL, true,
61
- {
62
- 'Authorization'=>'Bearer EdaNNN68y',
63
- 'Connection'=>'Keep-Alive',
64
- 'Content-Type'=>'application/json',
65
- 'Format'=>'syslog',
66
- 'Host'=>'local.endpoint:3000',
67
- 'Structure'=>'simple'
68
- }, [500, 510], 60, 60, Logger.new(STDOUT))
69
- end
70
-
71
- def test_retry_on_response_status_code
72
- http_client = create_http_client()
73
- stub_server_returns_500
74
- assert_raise RuntimeError do
75
- http_client.post(JSON.dump(sample_record()))
76
- end
77
- end
78
-
79
- def test_server_raise_error
80
- http_client = create_http_client()
81
- stub_server_raise_error
82
- assert_raise IOError do
83
- http_client.post(JSON.dump(sample_record()))
84
- end
85
- end
86
-
87
- def test_post_logs
88
- stub_post_logs
89
- http_client = create_http_client()
90
- http_client.post(JSON.dump(sample_record()))
91
-
92
- stub_post_logs
93
- http_client.post(JSON.dump(sample_record()))
94
- end
95
- end
1
+ # Copyright 2018 VMware, Inc.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ require 'helper'
5
+
6
+ class HttpClientTest < Test::Unit::TestCase
7
+ DEFAULT_URL = "https://local.endpoint:3000/dummy/xyz"
8
+
9
+ def stub_server_out_of_quota(url=DEFAULT_URL)
10
+ stub_request(:post, url).to_return(:status => [429, "User out of ingestion quota."])
11
+ end
12
+
13
+ def test_check_quota
14
+ http_client = create_http_client()
15
+ assert_equal http_client.check_quota, true
16
+
17
+ stub_server_out_of_quota
18
+ http_client.post(JSON.dump(sample_record()))
19
+ assert_equal http_client.check_quota, false
20
+ end
21
+
22
+ def stub_server_unavailable(url=DEFAULT_URL)
23
+ stub_request(:post, url).to_return(:status => [503, "Service Unavailable"])
24
+ end
25
+
26
+ def stub_server_returns_500(url=DEFAULT_URL)
27
+ stub_request(:post, url).to_return(:status => [500, "Internal service error"])
28
+ end
29
+
30
+ def stub_server_raise_error(url=DEFAULT_URL)
31
+ stub_request(:post, url).with do |req|
32
+ raise IOError
33
+ end
34
+ end
35
+
36
+ def stub_post_logs(url=DEFAULT_URL)
37
+ stub_request(:post, url)
38
+ .with(
39
+ body: "[{\"field1\":26,\"field2\":\"value26\"},{\"field1\":27,\"field2\":\"value27\"},{\"field1\":28,\"field2\":\"value28\"}]",
40
+ headers: {
41
+ 'Authorization'=>'Bearer EdaNNN68y',
42
+ 'Connection'=>'Keep-Alive',
43
+ 'Content-Type'=>'application/json',
44
+ 'Format'=>'syslog',
45
+ 'Structure'=>'simple'
46
+ })
47
+ .to_return(:status => 200, :body => "ok")
48
+ end
49
+
50
+ def sample_record()
51
+ [
52
+ {'field1' => 26, 'field2' => 'value26'},
53
+ {'field1' => 27, 'field2' => 'value27'},
54
+ {'field1' => 28, 'field2' => 'value28'}
55
+ ]
56
+ end
57
+
58
+ def create_http_client
59
+ Fluent::Plugin::HttpClient.new(
60
+ DEFAULT_URL, true,
61
+ {
62
+ 'Authorization'=>'Bearer EdaNNN68y',
63
+ 'Connection'=>'Keep-Alive',
64
+ 'Content-Type'=>'application/json',
65
+ 'Format'=>'syslog',
66
+ 'Host'=>'local.endpoint:3000',
67
+ 'Structure'=>'simple'
68
+ }, [500, 510], 60, 60, Logger.new(STDOUT))
69
+ end
70
+
71
+ def test_retry_on_response_status_code
72
+ http_client = create_http_client()
73
+ stub_server_returns_500
74
+ assert_raise RuntimeError do
75
+ http_client.post(JSON.dump(sample_record()))
76
+ end
77
+ end
78
+
79
+ def test_server_raise_error
80
+ http_client = create_http_client()
81
+ stub_server_raise_error
82
+ assert_raise IOError do
83
+ http_client.post(JSON.dump(sample_record()))
84
+ end
85
+ end
86
+
87
+ def test_post_logs
88
+ stub_post_logs
89
+ http_client = create_http_client()
90
+ http_client.post(JSON.dump(sample_record()))
91
+
92
+ stub_post_logs
93
+ http_client.post(JSON.dump(sample_record()))
94
+ end
95
+ end