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

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.
@@ -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