fluent-plugin-cloudwatch-logs 0.9.4 → 0.11.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: 0b2918da30191dd56e19d14043de953c649cb6207226df51a4721b8dda6af464
4
- data.tar.gz: a6a6def2e408711e723f6cec2f5fb706981f23479dd8cb9a3a1a674d859c1cbf
3
+ metadata.gz: 1dc48c250b022126a1de2b125bfa8ad3320daaa5eca5613f51ba7e6571a0b9a9
4
+ data.tar.gz: 23993ce51cac3aacfbe6937c1f928a00a61fbd94f64fb4ccf8c38ac8e4656787
5
5
  SHA512:
6
- metadata.gz: 3aa61613b097ff349ba56fd9a5b7e0ee4f87ed3fa23bfa816c35decc193d1161a1771b6985c745f751cc0d05e8e0be606326e145ec19a509f1d2ebd2698f7d71
7
- data.tar.gz: ffca6e46ee0c87dc22cd60c4cd724bdf22421daaff0338f245436edce33a8f0397f3f08daa4dd0f929cfabb6d73a2718feff15d304b5e79ca6762c0808aebd93
6
+ metadata.gz: 84fd2ea44c0b498364a13da89d422d39b6ea18abdb38add8fbacbc9f0c7b04b6ed18498f26e85920ffe8a7c80e5c14dce8f191c6ecc1a2f1c36809ce67e6961b
7
+ data.tar.gz: e16ab191ba87408d82e1ffa73564aec909a0795cca65c0ab506b2bb538f4c1cd0ad61641035c6b42056ccd4459e4ba677cb35a28d8af74edc3d1d0bd04422db1
@@ -0,0 +1,12 @@
1
+ name: Autocloser
2
+ on: [issues]
3
+ jobs:
4
+ autoclose:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - name: Autoclose issues that did not follow issue template
8
+ uses: roots/issue-closer-action@v1.1
9
+ with:
10
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
11
+ issue-close-message: "@${issue.user.login} this issue was automatically closed because it did not follow the issue template."
12
+ issue-pattern: "(.*Problem.*)|(.*Expected Behavior or What you need to ask.*)|(.*Using Fluentd and CloudWatchLogs plugin versions.*)"
data/README.md CHANGED
@@ -43,6 +43,46 @@ Create IAM user with a policy like the following:
43
43
  }
44
44
  ```
45
45
 
46
+ More restricted IAM policy for `out_cloudwatch_logs` is:
47
+
48
+ ```json
49
+ {
50
+ "Version": "2012-10-17",
51
+ "Statement": [
52
+ {
53
+ "Action": [
54
+ "logs:PutLogEvents",
55
+ "logs:CreateLogGroup",
56
+ "logs:PutRetentionPolicy",
57
+ "logs:CreateLogStream",
58
+ "logs:DescribeLogGroups",
59
+ "logs:DescribeLogStreams"
60
+ ],
61
+ "Effect": "Allow",
62
+ "Resource": "*"
63
+ }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ Also, more restricted IAM policy for `in_cloudwatch_logs` is:
69
+
70
+ ```json
71
+ {
72
+ "Version": "2012-10-17",
73
+ "Statement": [
74
+ {
75
+ "Action": [
76
+ "logs:GetLogEvents",
77
+ "logs:DescribeLogStreams"
78
+ ],
79
+ "Effect": "Allow",
80
+ "Resource": "*"
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
46
86
  ## Authentication
47
87
 
48
88
  There are several methods to provide authentication credentials. Be aware that there are various tradeoffs for these methods,
@@ -120,6 +160,11 @@ Fetch sample log from CloudWatch Logs:
120
160
  #endpoint http://localhost:5000/
121
161
  #json_handler json
122
162
  #log_rejected_request true
163
+ #<web_identity_credentials>
164
+ # role_arn "#{ENV['AWS_ROLE_ARN']}"
165
+ # role_session_name ROLE_SESSION_NAME
166
+ # web_identity_token_file "#{ENV['AWS_WEB_IDENTITY_TOKEN_FILE']}"
167
+ #</web_identity_credentials>
123
168
  </match>
124
169
  ```
125
170
 
@@ -154,6 +199,14 @@ Fetch sample log from CloudWatch Logs:
154
199
  * `retention_in_days_key`: use specified field of records as retention period
155
200
  * `use_tag_as_group`: to use tag as a group name
156
201
  * `use_tag_as_stream`: to use tag as a stream name
202
+ * `<web_identity_credentials>`: For EKS authentication.
203
+ * `role_arn`: The Amazon Resource Name (ARN) of the role to assume. This parameter is required when using `<web_identity_credentials>`.
204
+ * `role_session_name`: An identifier for the assumed role session. This parameter is required when using `<web_identity_credentials>`.
205
+ * `web_identity_token_file`: The absolute path to the file on disk containing the OIDC token. This parameter is required when using `<web_identity_credentials>`.
206
+ * `policy`: An IAM policy in JSON format. (default `nil`)
207
+ * `duration_seconds`: The duration, in seconds, of the role session. The value can range from
208
+ 900 seconds (15 minutes) to 43200 seconds (12 hours). By default, the value
209
+ is set to 3600 seconds (1 hour). (default `nil`)
157
210
 
158
211
  **NOTE:** `retention_in_days` requests additional IAM permission `logs:PutRetentionPolicy` for log_group.
159
212
  Please refer to [the PutRetentionPolicy column in documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/permissions-reference-cwl.html) for details.
@@ -178,6 +231,14 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
178
231
  #<parse>
179
232
  # @type none # or csv, tsv, regexp etc.
180
233
  #</parse>
234
+ #<storage>
235
+ # @type local # or redis, memcached, etc.
236
+ #</storage>
237
+ #<web_identity_credentials>
238
+ # role_arn "#{ENV['AWS_ROLE_ARN']}"
239
+ # role_session_name ROLE_SESSION_NAME
240
+ # web_identity_token_file "#{ENV['AWS_WEB_IDENTITY_TOKEN_FILE']}"
241
+ #</web_identity_credentials>
181
242
  </source>
182
243
  ```
183
244
 
@@ -194,7 +255,8 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
194
255
  * `log_stream_name`: name of log stream to fetch logs
195
256
  * `region`: AWS Region. See [Authentication](#authentication) for more information.
196
257
  * `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)
258
+ * `include_metadata`: include metadata such as `log_group_name` and `log_stream_name`. (default: false)
259
+ * `state_file`: file to store current state (e.g. next\_forward\_token). This parameter is deprecated. Use `<storage>` instead.
198
260
  * `tag`: fluentd tag
199
261
  * `use_log_stream_name_prefix`: to use `log_stream_name` as log stream name prefix (default false)
200
262
  * `use_todays_log_stream`: use todays and yesterdays date as log stream name prefix (formatted YYYY/MM/DD). (default: `false`)
@@ -204,6 +266,15 @@ Please refer to [the PutRetentionPolicy column in documentation](https://docs.aw
204
266
  * `time_range_format`: specify time format for time range. (default: `%Y-%m-%d %H:%M:%S`)
205
267
  * `format`: specify CloudWatchLogs' log format. (default `nil`)
206
268
  * `<parse>`: specify parser plugin configuration. see also: https://docs.fluentd.org/v/1.0/parser#how-to-use
269
+ * `<storage>`: specify storage plugin configuration. see also: https://docs.fluentd.org/v/1.0/storage#how-to-use
270
+ * `<web_identity_credentials>`: For EKS authentication.
271
+ * `role_arn`: The Amazon Resource Name (ARN) of the role to assume. This parameter is required when using `<web_identity_credentials>`.
272
+ * `role_session_name`: An identifier for the assumed role session. This parameter is required when using `<web_identity_credentials>`.
273
+ * `web_identity_token_file`: The absolute path to the file on disk containing the OIDC token. This parameter is required when using `<web_identity_credentials>`.
274
+ * `policy`: An IAM policy in JSON format. (default `nil`)
275
+ * `duration_seconds`: The duration, in seconds, of the role session. The value can range from
276
+ 900 seconds (15 minutes) to 43200 seconds (12 hours). By default, the value
277
+ is set to 3600 seconds (1 hour). (default `nil`)
207
278
 
208
279
  ## Test
209
280
 
@@ -2,7 +2,7 @@ module Fluent
2
2
  module Plugin
3
3
  module Cloudwatch
4
4
  module Logs
5
- VERSION = "0.9.4"
5
+ VERSION = "0.11.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,25 @@ 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
38
+ config_section :web_identity_credentials, multi: false do
39
+ config_param :role_arn, :string
40
+ config_param :role_session_name, :string
41
+ config_param :web_identity_token_file, :string, default: nil #required
42
+ config_param :policy, :string, default: nil
43
+ config_param :duration_seconds, :time, default: nil
44
+ end
34
45
 
35
46
  config_section :parse do
36
47
  config_set_default :@type, 'none'
37
48
  end
38
49
 
50
+ config_section :storage do
51
+ config_set_default :usage, 'store_next_tokens'
52
+ config_set_default :@type, DEFAULT_STORAGE_TYPE
53
+ config_set_default :persistent, false
54
+ end
55
+
39
56
  def initialize
40
57
  super
41
58
 
@@ -53,6 +70,7 @@ module Fluent::Plugin
53
70
  if @start_time && @end_time && (@end_time < @start_time)
54
71
  raise Fluent::ConfigError, "end_time(#{@end_time}) should be greater than start_time(#{@start_time})."
55
72
  end
73
+ @next_token_storage = storage_create(usage: 'store_next_tokens', conf: config, default_type: DEFAULT_STORAGE_TYPE)
56
74
  end
57
75
 
58
76
  def start
@@ -68,6 +86,18 @@ module Fluent::Plugin
68
86
  role_arn: @aws_sts_role_arn,
69
87
  role_session_name: @aws_sts_session_name
70
88
  )
89
+ elsif @web_identity_credentials
90
+ c = @web_identity_credentials
91
+ credentials_options = {}
92
+ credentials_options[:role_arn] = c.role_arn
93
+ credentials_options[:role_session_name] = c.role_session_name
94
+ credentials_options[:web_identity_token_file] = c.web_identity_token_file
95
+ credentials_options[:policy] = c.policy if c.policy
96
+ credentials_options[:duration_seconds] = c.duration_seconds if c.duration_seconds
97
+ if @region
98
+ credentials_options[:client] = Aws::STS::Client.new(:region => @region)
99
+ end
100
+ options[:credentials] = Aws::AssumeRoleWebIdentityCredentials.new(credentials_options)
71
101
  else
72
102
  options[:credentials] = Aws::Credentials.new(@aws_key_id, @aws_sec_key) if @aws_key_id && @aws_sec_key
73
103
  end
@@ -99,20 +129,28 @@ module Fluent::Plugin
99
129
  end
100
130
  end
101
131
 
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
132
+ def state_key_for(log_stream_name)
133
+ if log_stream_name
134
+ "#{@state_file}_#{log_stream_name.gsub(File::SEPARATOR, '-')}"
135
+ else
136
+ @state_file
137
+ end
138
+ end
139
+
140
+ def migrate_state_file_to_storage(log_stream_name)
141
+ @next_token_storage.put(:"#{state_key_for(log_stream_name)}", File.read(state_key_for(log_stream_name)).chomp)
142
+ File.delete(state_key_for(log_stream_name))
105
143
  end
106
144
 
107
145
  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
146
+ if @next_token_storage.persistent && File.exist?(state_key_for(log_stream_name))
147
+ migrate_state_file_to_storage(log_stream_name)
148
+ end
149
+ @next_token_storage.get(:"#{state_key_for(log_stream_name)}")
110
150
  end
111
151
 
112
152
  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
153
+ @next_token_storage.put(:"#{state_key_for(log_stream_name)}", token)
116
154
  end
117
155
 
118
156
  def run
@@ -130,8 +168,16 @@ module Fluent::Plugin
130
168
  log_streams.each do |log_stream|
131
169
  log_stream_name = log_stream.log_stream_name
132
170
  events = get_events(log_stream_name)
171
+ metadata = if @include_metadata
172
+ {
173
+ "log_stream_name" => log_stream_name,
174
+ "log_group_name" => @log_group_name
175
+ }
176
+ else
177
+ {}
178
+ end
133
179
  events.each do |event|
134
- emit(log_stream_name, event)
180
+ emit(log_stream_name, event, metadata)
135
181
  end
136
182
  end
137
183
  rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException
@@ -140,8 +186,16 @@ module Fluent::Plugin
140
186
  end
141
187
  else
142
188
  events = get_events(@log_stream_name)
189
+ metadata = if @include_metadata
190
+ {
191
+ "log_stream_name" => @log_stream_name,
192
+ "log_group_name" => @log_group_name
193
+ }
194
+ else
195
+ {}
196
+ end
143
197
  events.each do |event|
144
- emit(log_stream_name, event)
198
+ emit(log_stream_name, event, metadata)
145
199
  end
146
200
  end
147
201
  end
@@ -149,18 +203,24 @@ module Fluent::Plugin
149
203
  end
150
204
  end
151
205
 
152
- def emit(stream, event)
206
+ def emit(stream, event, metadata)
153
207
  if @parser
154
208
  @parser.parse(event.message) {|time,record|
155
209
  if @use_aws_timestamp
156
210
  time = (event.timestamp / 1000).floor
157
211
  end
212
+ unless metadata.empty?
213
+ record.merge!("metadata" => metadata)
214
+ end
158
215
  router.emit(@tag, time, record)
159
216
  }
160
217
  else
161
218
  time = (event.timestamp / 1000).floor
162
219
  begin
163
220
  record = @json_handler.load(event.message)
221
+ unless metadata.empty?
222
+ record.merge!("metadata" => metadata)
223
+ end
164
224
  router.emit(@tag, time, record)
165
225
  rescue JSON::ParserError, Yajl::ParseError => error # Catch parser errors
166
226
  log.error "Invalid JSON encountered while parsing event.message"
@@ -170,49 +230,57 @@ module Fluent::Plugin
170
230
  end
171
231
 
172
232
  def get_events(log_stream_name)
173
- request = {
174
- log_group_name: @log_group_name,
175
- log_stream_name: log_stream_name
176
- }
177
- request.merge!(start_time: @start_time) if @start_time
178
- request.merge!(end_time: @end_time) if @end_time
179
- log_next_token = next_token(log_stream_name)
180
- request[:next_token] = log_next_token if !log_next_token.nil? && !log_next_token.empty?
181
- response = @logs.get_log_events(request)
182
- if valid_next_token(log_next_token, response.next_forward_token)
183
- store_next_token(response.next_forward_token, log_stream_name)
233
+ throttling_handler('get_log_events') do
234
+ request = {
235
+ log_group_name: @log_group_name,
236
+ log_stream_name: log_stream_name
237
+ }
238
+ request.merge!(start_time: @start_time) if @start_time
239
+ request.merge!(end_time: @end_time) if @end_time
240
+ log_next_token = next_token(log_stream_name)
241
+ request[:next_token] = log_next_token if !log_next_token.nil? && !log_next_token.empty?
242
+ response = @logs.get_log_events(request)
243
+ if valid_next_token(log_next_token, response.next_forward_token)
244
+ store_next_token(response.next_forward_token, log_stream_name)
245
+ end
246
+
247
+ response.events
184
248
  end
249
+ end
185
250
 
186
- response.events
251
+ def describe_log_streams(log_stream_name_prefix, log_streams = nil, next_token = nil)
252
+ throttling_handler('describe_log_streams') do
253
+ request = {
254
+ log_group_name: @log_group_name
255
+ }
256
+ request[:next_token] = next_token if next_token
257
+ request[:log_stream_name_prefix] = log_stream_name_prefix if log_stream_name_prefix
258
+ response = @logs.describe_log_streams(request)
259
+ if log_streams
260
+ log_streams.concat(response.log_streams)
261
+ else
262
+ log_streams = response.log_streams
263
+ end
264
+ if response.next_token
265
+ log_streams = describe_log_streams(log_stream_name_prefix, log_streams, response.next_token)
266
+ end
267
+ log_streams
268
+ end
269
+ end
270
+
271
+ def throttling_handler(method_name)
272
+ yield
187
273
  rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err
188
274
  if throttling_retry_seconds
189
- log.warn "ThrottlingException in get_log_events (#{log_stream_name}). Waiting #{throttling_retry_seconds} seconds to retry."
275
+ log.warn "ThrottlingException #{method_name}. Waiting #{throttling_retry_seconds} seconds to retry."
190
276
  sleep throttling_retry_seconds
191
277
 
192
- get_events(log_stream_name)
278
+ throttling_handler(method_name) { yield }
193
279
  else
194
280
  raise err
195
281
  end
196
282
  end
197
283
 
198
- def describe_log_streams(log_stream_name_prefix, log_streams = nil, next_token = nil)
199
- request = {
200
- log_group_name: @log_group_name
201
- }
202
- request[:next_token] = next_token if next_token
203
- request[:log_stream_name_prefix] = log_stream_name_prefix if log_stream_name_prefix
204
- response = @logs.describe_log_streams(request)
205
- if log_streams
206
- log_streams.concat(response.log_streams)
207
- else
208
- log_streams = response.log_streams
209
- end
210
- if response.next_token
211
- log_streams = describe_log_streams(log_stream_name_prefix, log_streams, response.next_token)
212
- end
213
- log_streams
214
- end
215
-
216
284
  def valid_next_token(prev_token, next_token)
217
285
  next_token && prev_token != next_token.chomp
218
286
  end
@@ -7,6 +7,8 @@ module Fluent::Plugin
7
7
  class CloudwatchLogsOutput < Output
8
8
  Fluent::Plugin.register_output('cloudwatch_logs', self)
9
9
 
10
+ class TooLargeEventError < Fluent::UnrecoverableError; end
11
+
10
12
  helpers :compat_parameters, :inject
11
13
 
12
14
  DEFAULT_BUFFER_TYPE = "memory"
@@ -44,6 +46,13 @@ module Fluent::Plugin
44
46
  config_param :remove_retention_in_days_key, :bool, default: false
45
47
  config_param :json_handler, :enum, list: [:yajl, :json], :default => :yajl
46
48
  config_param :log_rejected_request, :bool, :default => false
49
+ config_section :web_identity_credentials, multi: false do
50
+ config_param :role_arn, :string
51
+ config_param :role_session_name, :string
52
+ config_param :web_identity_token_file, :string, default: nil #required
53
+ config_param :policy, :string, default: nil
54
+ config_param :duration_seconds, :time, default: nil
55
+ end
47
56
 
48
57
  config_section :buffer do
49
58
  config_set_default :@type, DEFAULT_BUFFER_TYPE
@@ -96,6 +105,18 @@ module Fluent::Plugin
96
105
  role_arn: @aws_sts_role_arn,
97
106
  role_session_name: @aws_sts_session_name
98
107
  )
108
+ elsif @web_identity_credentials
109
+ c = @web_identity_credentials
110
+ credentials_options = {}
111
+ credentials_options[:role_arn] = c.role_arn
112
+ credentials_options[:role_session_name] = c.role_session_name
113
+ credentials_options[:web_identity_token_file] = c.web_identity_token_file
114
+ credentials_options[:policy] = c.policy if c.policy
115
+ credentials_options[:duration_seconds] = c.duration_seconds if c.duration_seconds
116
+ if @region
117
+ credentials_options[:client] = Aws::STS::Client.new(:region => @region)
118
+ end
119
+ options[:credentials] = Aws::AssumeRoleWebIdentityCredentials.new(credentials_options)
99
120
  else
100
121
  options[:credentials] = Aws::Credentials.new(@aws_key_id, @aws_sec_key) if @aws_key_id && @aws_sec_key
101
122
  end
@@ -130,6 +151,9 @@ module Fluent::Plugin
130
151
  def write(chunk)
131
152
  log_group_name = extract_placeholders(@log_group_name, chunk) if @log_group_name
132
153
  log_stream_name = extract_placeholders(@log_stream_name, chunk) if @log_stream_name
154
+ aws_tags = @log_group_aws_tags.each {|k, v|
155
+ @log_group_aws_tags[extract_placeholders(k, chunk)] = extract_placeholders(v, chunk)
156
+ } if @log_group_aws_tags
133
157
 
134
158
  queue = Thread::Queue.new
135
159
 
@@ -182,7 +206,7 @@ module Fluent::Plugin
182
206
  #as we create log group only once, values from first record will persist
183
207
  record = rs[0][2]
184
208
 
185
- awstags = @log_group_aws_tags
209
+ awstags = aws_tags
186
210
  unless @log_group_aws_tags_key.nil?
187
211
  if @remove_log_group_aws_tags_key
188
212
  awstags = record.delete(@log_group_aws_tags_key)
@@ -319,8 +343,7 @@ module Fluent::Plugin
319
343
  while event = events.shift
320
344
  event_bytesize = event[:message].bytesize + EVENT_HEADER_SIZE
321
345
  if MAX_EVENT_SIZE < event_bytesize
322
- log.warn "Log event in #{group_name} is discarded because it is too large: #{event_bytesize} bytes exceeds limit of #{MAX_EVENT_SIZE}"
323
- break
346
+ raise TooLargeEventError, "Log event in #{group_name} is discarded because it is too large: #{event_bytesize} bytes exceeds limit of #{MAX_EVENT_SIZE}"
324
347
  end
325
348
 
326
349
  new_chunk = chunk + [event]
@@ -377,8 +400,7 @@ module Fluent::Plugin
377
400
  end
378
401
  rescue Aws::CloudWatchLogs::Errors::InvalidSequenceTokenException, Aws::CloudWatchLogs::Errors::DataAlreadyAcceptedException => err
379
402
  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)
403
+ store_next_sequence_token(group_name, stream_name, err.expected_sequence_token)
382
404
  log.warn "updating upload sequence token forcefully because unrecoverable error occured", {
383
405
  "error" => err,
384
406
  "log_group" => group_name,
@@ -400,7 +422,13 @@ module Fluent::Plugin
400
422
  raise err
401
423
  end
402
424
  rescue Aws::CloudWatchLogs::Errors::ThrottlingException => err
403
- if !@put_log_events_disable_retry_limit && @put_log_events_retry_limit < retry_count
425
+ if @put_log_events_retry_limit < 1
426
+ log.warn "failed to PutLogEvents and discard logs because put_log_events_retry_limit is less than 1", {
427
+ "error_class" => err.class.to_s,
428
+ "error" => err.message,
429
+ }
430
+ return
431
+ elsif !@put_log_events_disable_retry_limit && @put_log_events_retry_limit < retry_count
404
432
  log.error "failed to PutLogEvents and discard logs because retry count exceeded put_log_events_retry_limit", {
405
433
  "error_class" => err.class.to_s,
406
434
  "error" => err.message,
@@ -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
 
@@ -552,6 +630,8 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
552
630
  end
553
631
 
554
632
  test "emit with today's log stream" do
633
+ omit "This testcase is unstable in CI." if ENV["CI"] == "true"
634
+
555
635
  config = <<-CONFIG
556
636
  tag test
557
637
  @type cloudwatch_logs
@@ -611,7 +691,7 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
611
691
  assert_equal(["test", ((time_ms + 8000) / 1000), { "cloudwatch" => "logs8" }], events[7])
612
692
  end
613
693
 
614
- test "retry on Aws::CloudWatchLogs::Errors::ThrottlingException" do
694
+ test "retry on Aws::CloudWatchLogs::Errors::ThrottlingException in get_log_events" do
615
695
  config = <<-CONFIG
616
696
  tag test
617
697
  @type cloudwatch_logs
@@ -634,11 +714,34 @@ class CloudwatchLogsInputTest < Test::Unit::TestCase
634
714
  # so, it is expected to valid_next_token once
635
715
  mock(d.instance).valid_next_token(nil, nil).once
636
716
 
717
+ d.run
718
+ assert_equal(2, d.logs.select {|l| l =~ /ThrottlingException get_log_events. Waiting 0.2 seconds to retry/ }.size)
719
+ end
720
+
721
+ test "retry on Aws::CloudWatchLogs::Errors::ThrottlingException in describe_log_streams" do
722
+ config = <<-CONFIG
723
+ tag test
724
+ @type cloudwatch_logs
725
+ log_group_name #{log_group_name}
726
+ use_log_stream_name_prefix true
727
+ state_file /tmp/state
728
+ fetch_interval 0.1
729
+ throttling_retry_seconds 0.2
730
+ CONFIG
731
+
732
+ # it will raises the error 2 times
637
733
  log_stream = Aws::CloudWatchLogs::Types::LogStream.new(log_stream_name: "stream_name")
638
- @client.stub_responses(:describe_log_streams, { log_streams: [log_stream], next_token: nil })
734
+ counter = 0
735
+ times = 2
736
+ stub(@client).describe_log_streams(anything) {
737
+ counter += 1
738
+ counter <= times ? raise(Aws::CloudWatchLogs::Errors::ThrottlingException.new(nil, "error")) : OpenStruct.new(log_streams: [log_stream], next_token: nil)
739
+ }
740
+
741
+ d = create_driver(config)
639
742
 
640
743
  d.run
641
- assert_equal(2, d.logs.select {|l| l =~ /Waiting 0.2 seconds to retry/ }.size)
744
+ assert_equal(2, d.logs.select {|l| l =~ /ThrottlingException describe_log_streams. Waiting 0.2 seconds to retry/ }.size)
642
745
  end
643
746
  end
644
747
 
@@ -492,6 +492,47 @@ class CloudwatchLogsOutputTest < Test::Unit::TestCase
492
492
  assert_equal("value2", awstags.fetch("tag2"))
493
493
  end
494
494
 
495
+ def test_log_group_aws_tags_with_placeholders
496
+ clear_log_group
497
+
498
+ config = {
499
+ "@type" => "cloudwatch_logs",
500
+ "auto_create_stream" => true,
501
+ "use_tag_as_stream" => true,
502
+ "log_group_name_key" => "group_name_key",
503
+ "log_group_aws_tags" => '{"tag1": "${tag}", "tag2": "${namespace_name}"}',
504
+ }
505
+ config.merge!(config_elementify(aws_key_id)) if aws_key_id
506
+ config.merge!(config_elementify(aws_sec_key)) if aws_sec_key
507
+ config.merge!(config_elementify(region)) if region
508
+ config.merge!(config_elementify(endpoint)) if endpoint
509
+
510
+ d = create_driver(
511
+ Fluent::Config::Element.new('ROOT', '', config, [
512
+ Fluent::Config::Element.new('buffer', 'tag, namespace_name', {
513
+ '@type' => 'memory',
514
+ }, [])
515
+ ])
516
+ )
517
+
518
+ records = [
519
+ {'cloudwatch' => 'logs1', 'message' => 'message1', 'group_name_key' => log_group_name, "namespace_name" => "fluentd"},
520
+ {'cloudwatch' => 'logs2', 'message' => 'message1', 'group_name_key' => log_group_name, "namespace_name" => "fluentd"},
521
+ {'cloudwatch' => 'logs3', 'message' => 'message1', 'group_name_key' => log_group_name, "namespace_name" => "fluentd"},
522
+ ]
523
+
524
+ time = Time.now
525
+ d.run(default_tag: fluentd_tag) do
526
+ records.each_with_index do |record, i|
527
+ d.feed(time.to_i + i, record)
528
+ end
529
+ end
530
+
531
+ awstags = get_log_group_tags
532
+ assert_equal(fluentd_tag, awstags.fetch("tag1"))
533
+ assert_equal("fluentd", awstags.fetch("tag2"))
534
+ end
535
+
495
536
  def test_retention_in_days
496
537
  clear_log_group
497
538
 
@@ -713,6 +754,32 @@ class CloudwatchLogsOutputTest < Test::Unit::TestCase
713
754
  assert_equal({'cloudwatch' => 'logs2', 'message' => 'message2'}, JSON.parse(events[1].message))
714
755
  end
715
756
 
757
+ def test_retrying_on_throttling_exception_with_put_log_events_retry_limit_as_zero
758
+ client = Aws::CloudWatchLogs::Client.new
759
+ @called = false
760
+ stub(client).put_log_events(anything) {
761
+ raise(Aws::CloudWatchLogs::Errors::ThrottlingException.new(nil, "error"))
762
+ }.once.ordered
763
+
764
+ d = create_driver(<<-EOC)
765
+ #{default_config}
766
+ log_group_name #{log_group_name}
767
+ log_stream_name #{log_stream_name}
768
+ @log_level debug
769
+ put_log_events_retry_limit 0
770
+ EOC
771
+ time = event_time
772
+ d.instance.instance_variable_set(:@logs, client)
773
+ d.run(default_tag: fluentd_tag) do
774
+ d.feed(time, {'message' => 'message1'})
775
+ end
776
+
777
+ logs = d.logs
778
+ assert_equal(0, logs.select {|l| l =~ /Called PutLogEvents API/ }.size)
779
+ assert_equal(1, logs.select {|l| l =~ /failed to PutLogEvents/ }.size)
780
+ assert_equal(0, logs.select {|l| l =~ /retry succeeded/ }.size)
781
+ end
782
+
716
783
  def test_retrying_on_throttling_exception
717
784
  resp = Object.new
718
785
  mock(resp).rejected_log_events_info {}
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.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryota Arai
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-22 00:00:00.000000000 Z
11
+ date: 2020-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd
@@ -108,13 +108,14 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
- description:
111
+ description:
112
112
  email:
113
113
  - ryota.arai@gmail.com
114
114
  executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - ".github/workflows/issue-auto-closer.yml"
118
119
  - ".gitignore"
119
120
  - ".travis.yml"
120
121
  - Gemfile
@@ -135,7 +136,7 @@ homepage: https://github.com/fluent-plugins-nursery/fluent-plugin-cloudwatch-log
135
136
  licenses:
136
137
  - MIT
137
138
  metadata: {}
138
- post_install_message:
139
+ post_install_message:
139
140
  rdoc_options: []
140
141
  require_paths:
141
142
  - lib
@@ -150,8 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
151
  - !ruby/object:Gem::Version
151
152
  version: '0'
152
153
  requirements: []
153
- rubygems_version: 3.0.3
154
- signing_key:
154
+ rubygems_version: 3.1.2
155
+ signing_key:
155
156
  specification_version: 4
156
157
  summary: CloudWatch Logs Plugin for Fluentd
157
158
  test_files: