fluent-plugin-cloudwatch-logs 0.9.5 → 0.10.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13ef3e0d5f4fd4c12c49b16ccbd07db237b7cff26ea272e4ef6337b09b02e975
4
- data.tar.gz: 8cefeed014b1842ccce9f15479ad8cc54aa51b976281abb5d3e1c8828239aaa3
3
+ metadata.gz: 105e96fa516c4f972fc2b01f83c1daa30d39ff4b827b46b811b9f30514231516
4
+ data.tar.gz: 85741a1b0ccad0f9e207c837e7417b786ef68d1fc1552cbe80b73030e5b4191b
5
5
  SHA512:
6
- metadata.gz: 41ca6fa20d0c26955fcbab7f7a5a9fb8c032a5fd09f8676ccaaf5e4dd12694ea74c7c5320590f076405c691d436bc87b820e6bc9b75dc516062b2f3e09d8a377
7
- data.tar.gz: 1f5bbd694c5962de73ee1e0ca622d8892437fb82d00b76ed87086feeba49f6d2c3a4904edad81bec88ba2d3ca47701c15cb56395470f19742c16b3168dd870e5
6
+ metadata.gz: 84a8458e9704102609b382b9cb8fe4c332a1c1b9c9562b89b959961bb3624958cbffc1f965eba2a502c10a0e45f1ceb427a987681d5420c35663bc6df88295ed
7
+ data.tar.gz: d6e22ad22c4eef5cb6c8fab7690ca75585e7ad99756926181f50c43fef70df0e52e12722e14809b793947e56ed8199fc393a318f42308f05fd6d23d6aa9a4b13
data/README.md CHANGED
@@ -178,6 +178,9 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
178
178
  #<parse>
179
179
  # @type none # or csv, tsv, regexp etc.
180
180
  #</parse>
181
+ #<storage>
182
+ # @type local # or redis, memcached, etc.
183
+ #</storage>
181
184
  </source>
182
185
  ```
183
186
 
@@ -194,7 +197,8 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
194
197
  * `log_stream_name`: name of log stream to fetch logs
195
198
  * `region`: AWS Region. See [Authentication](#authentication) for more information.
196
199
  * `throttling_retry_seconds`: time period in seconds to retry a request when aws CloudWatch rate limit exceeds (default: nil)
197
- * `state_file`: file to store current state (e.g. next\_forward\_token)
200
+ * `include_metadata`: include metadata such as `log_group_name` and `log_stream_name`. (default: false)
201
+ * `state_file`: file to store current state (e.g. next\_forward\_token). This parameter is deprecated. Use `<storage>` instead.
198
202
  * `tag`: fluentd tag
199
203
  * `use_log_stream_name_prefix`: to use `log_stream_name` as log stream name prefix (default false)
200
204
  * `use_todays_log_stream`: use todays and yesterdays date as log stream name prefix (formatted YYYY/MM/DD). (default: `false`)
@@ -204,6 +208,7 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
204
208
  * `time_range_format`: specify time format for time range. (default: `%Y-%m-%d %H:%M:%S`)
205
209
  * `format`: specify CloudWatchLogs' log format. (default `nil`)
206
210
  * `<parse>`: specify parser plugin configuration. see also: https://docs.fluentd.org/v/1.0/parser#how-to-use
211
+ * `<storage>`: specify storage plugin configuration. see also: https://docs.fluentd.org/v/1.0/storage#how-to-use
207
212
 
208
213
  ## Test
209
214
 
@@ -2,7 +2,7 @@ module Fluent
2
2
  module Plugin
3
3
  module Cloudwatch
4
4
  module Logs
5
- VERSION = "0.9.5"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
8
8
  end
@@ -8,7 +8,9 @@ module Fluent::Plugin
8
8
  class CloudwatchLogsInput < Input
9
9
  Fluent::Plugin.register_input('cloudwatch_logs', self)
10
10
 
11
- helpers :parser, :thread, :compat_parameters
11
+ helpers :parser, :thread, :compat_parameters, :storage
12
+
13
+ DEFAULT_STORAGE_TYPE = 'local'
12
14
 
13
15
  config_param :aws_key_id, :string, default: nil, secret: true
14
16
  config_param :aws_sec_key, :string, default: nil, secret: true
@@ -21,7 +23,8 @@ module Fluent::Plugin
21
23
  config_param :log_group_name, :string
22
24
  config_param :log_stream_name, :string, default: nil
23
25
  config_param :use_log_stream_name_prefix, :bool, default: false
24
- config_param :state_file, :string
26
+ config_param :state_file, :string, default: nil,
27
+ deprecated: "Use <stroage> instead."
25
28
  config_param :fetch_interval, :time, default: 60
26
29
  config_param :http_proxy, :string, default: nil
27
30
  config_param :json_handler, :enum, list: [:yajl, :json], default: :yajl
@@ -31,11 +34,18 @@ module Fluent::Plugin
31
34
  config_param :end_time, :string, default: nil
32
35
  config_param :time_range_format, :string, default: "%Y-%m-%d %H:%M:%S"
33
36
  config_param :throttling_retry_seconds, :time, default: nil
37
+ config_param :include_metadata, :bool, default: false
34
38
 
35
39
  config_section :parse do
36
40
  config_set_default :@type, 'none'
37
41
  end
38
42
 
43
+ config_section :storage do
44
+ config_set_default :usage, 'store_next_tokens'
45
+ config_set_default :@type, DEFAULT_STORAGE_TYPE
46
+ config_set_default :persistent, false
47
+ end
48
+
39
49
  def initialize
40
50
  super
41
51
 
@@ -53,6 +63,7 @@ module Fluent::Plugin
53
63
  if @start_time && @end_time && (@end_time < @start_time)
54
64
  raise Fluent::ConfigError, "end_time(#{@end_time}) should be greater than start_time(#{@start_time})."
55
65
  end
66
+ @next_token_storage = storage_create(usage: 'store_next_tokens', conf: config, default_type: DEFAULT_STORAGE_TYPE)
56
67
  end
57
68
 
58
69
  def start
@@ -99,20 +110,28 @@ module Fluent::Plugin
99
110
  end
100
111
  end
101
112
 
102
- def state_file_for(log_stream_name)
103
- return "#{@state_file}_#{log_stream_name.gsub(File::SEPARATOR, '-')}" if log_stream_name
104
- return @state_file
113
+ def state_key_for(log_stream_name)
114
+ if log_stream_name
115
+ "#{@state_file}_#{log_stream_name.gsub(File::SEPARATOR, '-')}"
116
+ else
117
+ @state_file
118
+ end
119
+ end
120
+
121
+ def migrate_state_file_to_storage(log_stream_name)
122
+ @next_token_storage.put(:"#{state_key_for(log_stream_name)}", File.read(state_key_for(log_stream_name)).chomp)
123
+ File.delete(state_key_for(log_stream_name))
105
124
  end
106
125
 
107
126
  def next_token(log_stream_name)
108
- return nil unless File.exist?(state_file_for(log_stream_name))
109
- File.read(state_file_for(log_stream_name)).chomp
127
+ if @next_token_storage.persistent && File.exist?(state_key_for(log_stream_name))
128
+ migrate_state_file_to_storage(log_stream_name)
129
+ end
130
+ @next_token_storage.get(:"#{state_key_for(log_stream_name)}")
110
131
  end
111
132
 
112
133
  def store_next_token(token, log_stream_name = nil)
113
- File.open(state_file_for(log_stream_name), 'w') do |f|
114
- f.write token
115
- end
134
+ @next_token_storage.put(:"#{state_key_for(log_stream_name)}", token)
116
135
  end
117
136
 
118
137
  def run
@@ -130,8 +149,16 @@ module Fluent::Plugin
130
149
  log_streams.each do |log_stream|
131
150
  log_stream_name = log_stream.log_stream_name
132
151
  events = get_events(log_stream_name)
152
+ metadata = if @include_metadata
153
+ {
154
+ "log_stream_name" => log_stream_name,
155
+ "log_group_name" => @log_group_name
156
+ }
157
+ else
158
+ {}
159
+ end
133
160
  events.each do |event|
134
- emit(log_stream_name, event)
161
+ emit(log_stream_name, event, metadata)
135
162
  end
136
163
  end
137
164
  rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException
@@ -140,8 +167,16 @@ module Fluent::Plugin
140
167
  end
141
168
  else
142
169
  events = get_events(@log_stream_name)
170
+ metadata = if @include_metadata
171
+ {
172
+ "log_stream_name" => @log_stream_name,
173
+ "log_group_name" => @log_group_name
174
+ }
175
+ else
176
+ {}
177
+ end
143
178
  events.each do |event|
144
- emit(log_stream_name, event)
179
+ emit(log_stream_name, event, metadata)
145
180
  end
146
181
  end
147
182
  end
@@ -149,18 +184,24 @@ module Fluent::Plugin
149
184
  end
150
185
  end
151
186
 
152
- def emit(stream, event)
187
+ def emit(stream, event, metadata)
153
188
  if @parser
154
189
  @parser.parse(event.message) {|time,record|
155
190
  if @use_aws_timestamp
156
191
  time = (event.timestamp / 1000).floor
157
192
  end
193
+ unless metadata.empty?
194
+ record.merge!("metadata" => metadata)
195
+ end
158
196
  router.emit(@tag, time, record)
159
197
  }
160
198
  else
161
199
  time = (event.timestamp / 1000).floor
162
200
  begin
163
201
  record = @json_handler.load(event.message)
202
+ unless metadata.empty?
203
+ record.merge!("metadata" => metadata)
204
+ end
164
205
  router.emit(@tag, time, record)
165
206
  rescue JSON::ParserError, Yajl::ParseError => error # Catch parser errors
166
207
  log.error "Invalid JSON encountered while parsing event.message"
@@ -377,8 +377,7 @@ module Fluent::Plugin
377
377
  end
378
378
  rescue Aws::CloudWatchLogs::Errors::InvalidSequenceTokenException, Aws::CloudWatchLogs::Errors::DataAlreadyAcceptedException => err
379
379
  sleep 1 # to avoid too many API calls
380
- log_stream = find_log_stream(group_name, stream_name)
381
- store_next_sequence_token(group_name, stream_name, log_stream.upload_sequence_token)
380
+ store_next_sequence_token(group_name, stream_name, err.expected_sequence_token)
382
381
  log.warn "updating upload sequence token forcefully because unrecoverable error occured", {
383
382
  "error" => err,
384
383
  "log_group" => group_name,
@@ -99,6 +99,34 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
99
99
  assert_equal(['test', (time_ms / 1000).floor, {'cloudwatch' => 'logs2'}], emits[1])
100
100
  end
101
101
 
102
+ def test_emit_with_metadata
103
+ create_log_stream
104
+
105
+ time_ms = (Time.now.to_f * 1000).floor
106
+ put_log_events([
107
+ {timestamp: time_ms, message: '{"cloudwatch":"logs1"}'},
108
+ {timestamp: time_ms, message: '{"cloudwatch":"logs2"}'},
109
+ ])
110
+
111
+ sleep 5
112
+
113
+ d = create_driver(default_config + %[include_metadata true])
114
+ d.run(expect_emits: 2, timeout: 5)
115
+
116
+ emits = d.events
117
+ assert_true(emits[0][2].has_key?("metadata"))
118
+ assert_true(emits[1][2].has_key?("metadata"))
119
+ emits[0][2].delete_if {|k, v|
120
+ k == "metadata"
121
+ }
122
+ emits[1][2].delete_if {|k, v|
123
+ k == "metadata"
124
+ }
125
+ assert_equal(2, emits.size)
126
+ assert_equal(['test', (time_ms / 1000).floor, {'cloudwatch' => 'logs1'}], emits[0])
127
+ assert_equal(['test', (time_ms / 1000).floor, {'cloudwatch' => 'logs2'}], emits[1])
128
+ end
129
+
102
130
  def test_emit_with_aws_timestamp
103
131
  create_log_stream
104
132
 
@@ -173,7 +201,6 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
173
201
  '@type' => 'cloudwatch_logs',
174
202
  'log_group_name' => "#{log_group_name}",
175
203
  'log_stream_name' => "#{log_stream_name}",
176
- 'state_file' => '/tmp/state',
177
204
  }
178
205
  cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_key_id)) if ENV['aws_key_id']
179
206
  cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_sec_key)) if ENV['aws_sec_key']
@@ -183,7 +210,9 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
183
210
  csv_format_config = config_element('ROOT', '', cloudwatch_config, [
184
211
  config_element('parse', '', {'@type' => 'csv',
185
212
  'keys' => 'time,message',
186
- 'time_key' => 'time'})
213
+ 'time_key' => 'time'}),
214
+ config_element('storage', '', {'@type' => 'local',
215
+ 'path' => '/tmp/state'})
187
216
  ])
188
217
  create_log_stream
189
218
 
@@ -205,6 +234,54 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
205
234
  assert_equal(['test', (log_time_ms / 1000).floor, {"message"=>"Cloudwatch non json logs2"}], emits[1])
206
235
  end
207
236
 
237
+ test "emit with <parse> csv with metadata" do
238
+ cloudwatch_config = {'tag' => "test",
239
+ '@type' => 'cloudwatch_logs',
240
+ 'log_group_name' => "#{log_group_name}",
241
+ 'log_stream_name' => "#{log_stream_name}",
242
+ 'include_metadata' => true,
243
+ }
244
+ cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_key_id)) if ENV['aws_key_id']
245
+ cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_sec_key)) if ENV['aws_sec_key']
246
+ cloudwatch_config = cloudwatch_config.merge!(config_elementify(region)) if ENV['region']
247
+ cloudwatch_config = cloudwatch_config.merge!(config_elementify(endpoint)) if ENV['endpoint']
248
+
249
+ csv_format_config = config_element('ROOT', '', cloudwatch_config, [
250
+ config_element('parse', '', {'@type' => 'csv',
251
+ 'keys' => 'time,message',
252
+ 'time_key' => 'time'}),
253
+ config_element('storage', '', {'@type' => 'local',
254
+ 'path' => '/tmp/state'})
255
+
256
+ ])
257
+ create_log_stream
258
+
259
+ time_ms = (Time.now.to_f * 1000).floor
260
+ log_time_ms = time_ms - 10000
261
+ put_log_events([
262
+ {timestamp: time_ms, message: Time.at(log_time_ms/1000.floor).to_s + ",Cloudwatch non json logs1"},
263
+ {timestamp: time_ms, message: Time.at(log_time_ms/1000.floor).to_s + ",Cloudwatch non json logs2"},
264
+ ])
265
+
266
+ sleep 5
267
+
268
+ d = create_driver(csv_format_config)
269
+ d.run(expect_emits: 2, timeout: 5)
270
+
271
+ emits = d.events
272
+ assert_true(emits[0][2].has_key?("metadata"))
273
+ assert_true(emits[1][2].has_key?("metadata"))
274
+ emits[0][2].delete_if {|k, v|
275
+ k == "metadata"
276
+ }
277
+ emits[1][2].delete_if {|k, v|
278
+ k == "metadata"
279
+ }
280
+ assert_equal(2, emits.size)
281
+ assert_equal(['test', (log_time_ms / 1000).floor, {"message"=>"Cloudwatch non json logs1"}], emits[0])
282
+ assert_equal(['test', (log_time_ms / 1000).floor, {"message"=>"Cloudwatch non json logs2"}], emits[1])
283
+ end
284
+
208
285
  def test_emit_width_format
209
286
  create_log_stream
210
287
 
@@ -246,7 +323,6 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
246
323
  '@type' => 'cloudwatch_logs',
247
324
  'log_group_name' => "#{log_group_name}",
248
325
  'log_stream_name' => "#{log_stream_name}",
249
- 'state_file' => '/tmp/state',
250
326
  }
251
327
  cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_key_id)) if ENV['aws_key_id']
252
328
  cloudwatch_config = cloudwatch_config.merge!(config_elementify(aws_sec_key)) if ENV['aws_sec_key']
@@ -256,7 +332,9 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
256
332
  regex_format_config = config_element('ROOT', '', cloudwatch_config, [
257
333
  config_element('parse', '', {'@type' => 'regexp',
258
334
  'expression' => "/^(?<cloudwatch>[^ ]*)?/",
259
- })
335
+ }),
336
+ config_element('storage', '', {'@type' => 'local',
337
+ 'path' => '/tmp/state'})
260
338
  ])
261
339
  create_log_stream
262
340
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-cloudwatch-logs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryota Arai
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-19 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd