uw-fluent-plugin-throttle 0.0.4.pre.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8997cb3815d33b40957a5d90e394112b42d078eb0c87775f9909756a63cd22c8
4
+ data.tar.gz: 2a8e0ddb0ef82a8b94b90537fe3ccaa5f8ac77766d4c96ae264dba2ab64472e8
5
+ SHA512:
6
+ metadata.gz: f3dd854ecf6cadbcc2c3f373d5732f32dfa74832c8ac9c1005df5b2b15011f3463038ef5308fae28dc6df44114dc049e6c18db855bba031b9e55ed79f78605f5
7
+ data.tar.gz: e186e9bfcbb4f8e89a98b45ba05997191063ac3f8d7e42730ba6c9ae09568a7c77b6e7f0b190eaf2fbda44a947d162c491454119ee333c04970a844cdb8bf7ed
@@ -0,0 +1 @@
1
+ *.gem
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ os: linux
3
+ sudo: false
4
+ script: bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'pry'
6
+ gem 'pry-byebug'
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fluent-plugin-throttle (0.0.3)
5
+ fluentd (~> 1.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.5.2)
11
+ public_suffix (>= 2.0.2, < 4.0)
12
+ appraisal (2.2.0)
13
+ bundler
14
+ rake
15
+ thor (>= 0.14.0)
16
+ byebug (10.0.2)
17
+ coderay (1.1.2)
18
+ cool.io (1.5.3)
19
+ crack (0.4.3)
20
+ safe_yaml (~> 1.0.0)
21
+ dig_rb (1.0.1)
22
+ fluentd (1.2.3)
23
+ cool.io (>= 1.4.5, < 2.0.0)
24
+ dig_rb (~> 1.0.0)
25
+ http_parser.rb (>= 0.5.1, < 0.7.0)
26
+ msgpack (>= 0.7.0, < 2.0.0)
27
+ serverengine (>= 2.0.4, < 3.0.0)
28
+ sigdump (~> 0.2.2)
29
+ strptime (>= 0.2.2, < 1.0.0)
30
+ tzinfo (~> 1.0)
31
+ tzinfo-data (~> 1.0)
32
+ yajl-ruby (~> 1.0)
33
+ hashdiff (0.3.7)
34
+ http_parser.rb (0.6.0)
35
+ maxitest (3.1.0)
36
+ minitest (>= 5.0.0, < 5.12.0)
37
+ metaclass (0.0.4)
38
+ method_source (0.9.0)
39
+ minitest (5.11.3)
40
+ mocha (1.6.0)
41
+ metaclass (~> 0.0.1)
42
+ msgpack (1.2.4)
43
+ power_assert (1.1.3)
44
+ pry (0.11.3)
45
+ coderay (~> 1.1.0)
46
+ method_source (~> 0.9.0)
47
+ pry-byebug (3.6.0)
48
+ byebug (~> 10.0)
49
+ pry (~> 0.10)
50
+ public_suffix (3.0.2)
51
+ rake (12.3.1)
52
+ safe_yaml (1.0.4)
53
+ serverengine (2.0.7)
54
+ sigdump (~> 0.2.2)
55
+ sigdump (0.2.4)
56
+ single_cov (1.1.0)
57
+ strptime (0.2.3)
58
+ test-unit (3.2.8)
59
+ power_assert
60
+ thor (0.20.0)
61
+ thread_safe (0.3.6)
62
+ tzinfo (1.2.5)
63
+ thread_safe (~> 0.1)
64
+ tzinfo-data (1.2018.5)
65
+ tzinfo (>= 1.0.0)
66
+ webmock (3.4.2)
67
+ addressable (>= 2.3.6)
68
+ crack (>= 0.3.2)
69
+ hashdiff
70
+ yajl-ruby (1.4.1)
71
+
72
+ PLATFORMS
73
+ ruby
74
+
75
+ DEPENDENCIES
76
+ appraisal (~> 2.2)
77
+ bundler (~> 1.16)
78
+ fluent-plugin-throttle!
79
+ maxitest
80
+ mocha
81
+ pry
82
+ pry-byebug
83
+ rake (~> 12.3)
84
+ single_cov
85
+ test-unit (~> 3.2)
86
+ webmock (~> 3.3)
87
+
88
+ BUNDLED WITH
89
+ 1.16.2
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,138 @@
1
+ # fluent-plugin-throttle
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/rubrikinc/fluent-plugin-throttle/blob/master/LICENSE)
4
+
5
+ A sentry pluging to throttle logs. Logs are grouped by a configurable key. When
6
+ a group exceeds a configuration rate, logs are dropped for this group.
7
+
8
+ ## Installation
9
+
10
+ install with `gem` or td-agent provided command as:
11
+
12
+ ```bash
13
+ # for fluentd
14
+ $ gem install fluent-plugin-throttle
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```xml
20
+ <filter **>
21
+ @type throttle
22
+ group_key kubernetes.container_name
23
+ group_bucket_period_s 60
24
+ group_bucket_limit 6000
25
+ group_reset_rate_s 100
26
+ </filter>
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ #### group\_key
32
+
33
+ Default: `kubernetes.container_name`.
34
+
35
+ Used to group logs. Groups are rate limited independently.
36
+
37
+ A dot indicates a key within a sub-object. As an example, in the following log,
38
+ the group key resolve to "random":
39
+ ```
40
+ {"level": "error", "msg": "plugin test", "kubernetes": { "container_name": "random" } }
41
+ ```
42
+
43
+ Multiple groups can be specified using the fluentd config array syntax,
44
+ e.g. `kubernetes.container_name,kubernetes.pod_name`, in which case each unique pair
45
+ of key values are rate limited independently.
46
+
47
+ If the group cannot be resolved, an anonymous (`nil`) group is used for rate limiting.
48
+
49
+ #### group\_bucket\_period\_s
50
+
51
+ Default: `60` (60 second).
52
+
53
+ This is the period of of time over which `group_bucket_limit` applies.
54
+
55
+ #### group\_bucket\_limit
56
+
57
+ Default: `6000` (logs per `group_bucket_period_s`).
58
+
59
+ Maximum number logs allowed per groups over the period of `group_bucket_period_s`.
60
+
61
+ This translate to a log rate of `group_bucket_limit/group_bucket_period_s`.
62
+ When a group exceeds this rate, logs from this group are dropped.
63
+
64
+ For example, the default is 6000/60s, making for a rate of 100 logs per
65
+ seconds.
66
+
67
+ Note that this is not expressed as a rate directly because there is a
68
+ difference between the overall rate and the distribution of logs over a period
69
+ time. For example, a burst of logs in the middle of a minute bucket might not
70
+ exceed the average rate of the full minute.
71
+
72
+ Consider `60/60s`, 60 logs over a minute, versus `1/1s`, 1 log per second.
73
+ Over a minute, both will emit a maximum of 60 logs. Limiting to a rate of 60
74
+ logs per minute. However `60/60s` will readily emit 60 logs within the first
75
+ second then nothing for the remaining 59 seconds. While the `1/1s` will only
76
+ emit the first log of every second.
77
+
78
+ #### group\_drop\_logs
79
+
80
+ Default: `true`.
81
+
82
+ When a group reaches its limit, logs will be dropped from further processing
83
+ if this value is true (set by default). To prevent the logs from being dropped
84
+ and only receive a warning message when rate limiting would have occurred, set
85
+ this value for false. This can be useful to fine-tune your group bucket limits
86
+ before dropping any logs.
87
+
88
+ #### group\_reset\_rate\_s
89
+
90
+ Default: `group_bucket_limit/group_bucket_period_s` (logs per `group_bucket_period_s`).
91
+ Maximum: `group_bucket_limit/group_bucket_period_s`.
92
+
93
+ After a group has exceeded its bucket limit, logs are dropped until the rate
94
+ per second falls below or equal to `group_reset_rate_s`.
95
+
96
+ The default value is `group_bucket_limits/group_bucket_period_s`. For example
97
+ for 3600 logs per hour, the reset will defaults to `3600/3600s = 1/s`, one log
98
+ per second.
99
+
100
+ Taking the example `3600 log/hour` with the default reset rate of `1 log/s`
101
+ further:
102
+
103
+ - Let's say we have a period of 10 hours.
104
+ - During the first hour, 2 logs/s are produced. After 30 minutes, the hourly
105
+ bucket has reached its limit, and logs are dropped. At this point the rate
106
+ is still 2 logs/s for the remaining 30 minutes.
107
+ - Because the last hour finished on 2 logs/s, which is higher that the
108
+ `1 log/s` reset, all logs are still dropped when starting the second hour. The
109
+ bucket limit is left untouched since nothing is being emitted.
110
+ - Now, at 2 hours and 30 minutes, the log rate halves to `1 log/s`, which is
111
+ equal to the reset rate. Logs are emitted again, counting toward the bucket
112
+ limit as normal. Allowing up to 3600 logs for the last 30 minutes of the second
113
+ hour.
114
+
115
+ Because this could allow for some instability if the log rate hovers around the
116
+ `group_bucket_limit/group_bucket_period_s` rate, it is possible to set a
117
+ different reset rate.
118
+
119
+ Note that a value of `0` effectively means the plugin will drops logs forever
120
+ after a single breach of the limit until the next restart of fluentd.
121
+
122
+ A value of `-1` disables the feature.
123
+
124
+ #### group\_warning\_delay\_s
125
+
126
+ Default: `10` (seconds).
127
+
128
+ When a group reaches its limit and as long as it is not reset, a warning
129
+ message with the current log rate of the group is emitted repeatedly. This is
130
+ the delay between every repetition.
131
+
132
+ ## License
133
+
134
+ Apache License, Version 2.0
135
+
136
+ ## Copyright
137
+
138
+ Copyright © 2018 ([Rubrik Inc.](https://www.rubrik.com))
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ task default: [:test]
5
+ Rake::TestTask.new do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.test_files = FileList['test/**/*_test.rb']
8
+ test.options = '-v'
9
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "uw-fluent-plugin-throttle"
7
+ spec.version = "0.0.4-dev"
8
+ spec.authors = ["François-Xavier Bourlet", "UtilityWarehouse"]
9
+ spec.email = ["fx.bourlet@rubrik.com", "infra@utilitywarehouse.co.uk"]
10
+ spec.summary = %q{Fluentd filter for throttling logs based on a configurable key.}
11
+ spec.homepage = "https://github.com/utilitywarehouse/fluent-plugin-throttle"
12
+ spec.license = "Apache-2.0"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.16"
20
+ spec.add_development_dependency "rake", "~> 12.3"
21
+ spec.add_development_dependency "webmock", "~> 3.3"
22
+ spec.add_development_dependency "test-unit", "~> 3.2"
23
+ spec.add_development_dependency "appraisal", "~> 2.2"
24
+ spec.add_development_dependency "mocha"
25
+ spec.add_development_dependency "maxitest"
26
+ spec.add_development_dependency "single_cov"
27
+
28
+ spec.add_runtime_dependency "fluentd", "~> 1.1"
29
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+ require 'fluent/plugin/filter'
3
+
4
+ module Fluent::Plugin
5
+ class ThrottleFilter < Filter
6
+ Fluent::Plugin.register_filter('throttle', self)
7
+
8
+ desc "Used to group logs. Groups are rate limited independently"
9
+ config_param :group_key, :array, :default => ['kubernetes.container_name']
10
+
11
+ desc <<~DESC
12
+ This is the period of of time over which group_bucket_limit applies
13
+ DESC
14
+ config_param :group_bucket_period_s, :integer, :default => 60
15
+
16
+ desc <<~DESC
17
+ Maximum number logs allowed per groups over the period of
18
+ group_bucket_period_s
19
+ DESC
20
+ config_param :group_bucket_limit, :integer, :default => 6000
21
+
22
+ desc "Whether to drop logs that exceed the bucket limit or not"
23
+ config_param :group_drop_logs, :bool, :default => true
24
+
25
+ desc <<~DESC
26
+ After a group has exceeded its bucket limit, logs are dropped until the
27
+ rate per second falls below or equal to group_reset_rate_s.
28
+ DESC
29
+ config_param :group_reset_rate_s, :integer, :default => nil
30
+
31
+ desc <<~DESC
32
+ When a group reaches its limit and as long as it is not reset, a warning
33
+ message with the current log rate of the group is emitted repeatedly.
34
+ This is the delay between every repetition.
35
+ DESC
36
+ config_param :group_warning_delay_s, :integer, :default => 10
37
+
38
+ Group = Struct.new(
39
+ :rate_count,
40
+ :rate_last_reset,
41
+ :aprox_rate,
42
+ :bucket_count,
43
+ :bucket_last_reset,
44
+ :last_warning)
45
+
46
+ def configure(conf)
47
+ super
48
+
49
+ @group_key_paths = group_key.map { |key| key.split(".") }
50
+
51
+ raise "group_bucket_period_s must be > 0" \
52
+ unless @group_bucket_period_s > 0
53
+
54
+ raise "group_bucket_limit must be > 0" \
55
+ unless @group_bucket_limit > 0
56
+
57
+ @group_rate_limit = (@group_bucket_limit / @group_bucket_period_s)
58
+
59
+ @group_reset_rate_s = @group_rate_limit \
60
+ if @group_reset_rate_s == nil
61
+
62
+ raise "group_reset_rate_s must be >= -1" \
63
+ unless @group_reset_rate_s >= -1
64
+ raise "group_reset_rate_s must be <= group_bucket_limit / group_bucket_period_s" \
65
+ unless @group_reset_rate_s <= @group_rate_limit
66
+
67
+ raise "group_warning_delay_s must be >= 1" \
68
+ unless @group_warning_delay_s >= 1
69
+ end
70
+
71
+ def start
72
+ super
73
+
74
+ @counters = {}
75
+ end
76
+
77
+ def shutdown
78
+ log.debug("counters summary: #{@counters}")
79
+ super
80
+ end
81
+
82
+ def filter(tag, time, record)
83
+ now = Time.now
84
+ rate_limit_exceeded = @group_drop_logs ? nil : record # return nil on rate_limit_exceeded to drop the record
85
+ group = extract_group(record)
86
+ counter = (@counters[group] ||= Group.new(0, now, 0, 0, now, nil))
87
+ counter.rate_count += 1
88
+
89
+ since_last_rate_reset = now - counter.rate_last_reset
90
+ if since_last_rate_reset >= 1
91
+ # compute and store rate/s at most every seconds.
92
+ counter.aprox_rate = (counter.rate_count / since_last_rate_reset).round()
93
+ counter.rate_count = 0
94
+ counter.rate_last_reset = now
95
+ end
96
+
97
+ if (now.to_i / @group_bucket_period_s) \
98
+ > (counter.bucket_last_reset.to_i / @group_bucket_period_s)
99
+ # next time period reached.
100
+
101
+ # wait until rate drops back down (if enabled).
102
+ if counter.bucket_count == -1 and @group_reset_rate_s != -1
103
+ if counter.aprox_rate < @group_reset_rate_s
104
+ log_rate_back_down(now, group, counter)
105
+ else
106
+ log_rate_limit_exceeded(now, group, counter)
107
+ return rate_limit_exceeded
108
+ end
109
+ end
110
+
111
+ # reset counter for the rest of time period.
112
+ counter.bucket_count = 0
113
+ counter.bucket_last_reset = now
114
+ else
115
+ # if current time period credit is exhausted, drop the record.
116
+ if counter.bucket_count == -1
117
+ log_rate_limit_exceeded(now, group, counter)
118
+ return rate_limit_exceeded
119
+ end
120
+ end
121
+
122
+ counter.bucket_count += 1
123
+
124
+ # if we are out of credit, we drop logs for the rest of the time period.
125
+ if counter.bucket_count > @group_bucket_limit
126
+ log_rate_limit_exceeded(now, group, counter)
127
+ counter.bucket_count = -1
128
+ return rate_limit_exceeded
129
+ end
130
+
131
+ record
132
+ end
133
+
134
+ private
135
+
136
+ def extract_group(record)
137
+ @group_key_paths.map do |key_path|
138
+ record.dig(*key_path) || record.dig(*key_path.map(&:to_sym))
139
+ end
140
+ end
141
+
142
+ def log_rate_limit_exceeded(now, group, counter)
143
+ emit = counter.last_warning == nil ? true \
144
+ : (now - counter.last_warning) >= @group_warning_delay_s
145
+ if emit
146
+ log.warn("rate exceeded", log_items(now, group, counter))
147
+ counter.last_warning = now
148
+ end
149
+ end
150
+
151
+ def log_rate_back_down(now, group, counter)
152
+ log.info("rate back down", log_items(now, group, counter))
153
+ end
154
+
155
+ def log_items(now, group, counter)
156
+ since_last_reset = now - counter.bucket_last_reset
157
+ rate = since_last_reset > 0 ? (counter.bucket_count / since_last_reset).round : Float::INFINITY
158
+ aprox_rate = counter.aprox_rate
159
+ rate = aprox_rate if aprox_rate > rate
160
+
161
+ {'group_key': group,
162
+ 'rate_s': rate,
163
+ 'period_s': @group_bucket_period_s,
164
+ 'limit': @group_bucket_limit,
165
+ 'rate_limit_s': @group_rate_limit,
166
+ 'reset_rate_s': @group_reset_rate_s}
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../../helper'
3
+ require 'fluent/plugin/filter_throttle'
4
+
5
+ SingleCov.covered!
6
+
7
+ describe Fluent::Plugin::ThrottleFilter do
8
+ include Fluent::Test::Helpers
9
+
10
+ before do
11
+ Fluent::Test.setup
12
+ end
13
+
14
+ after do
15
+ if instance_variable_defined?(:@driver)
16
+ assert @driver.error_events.empty?, "Errors detected: " + @driver.error_events.map(&:inspect).join("\n")
17
+ end
18
+ end
19
+
20
+ def create_driver(conf='')
21
+ @driver = Fluent::Test::Driver::Filter.new(Fluent::Plugin::ThrottleFilter).configure(conf)
22
+ end
23
+
24
+ describe '#configure' do
25
+ it 'raises on invalid group_bucket_limit' do
26
+ assert_raises { create_driver("group_bucket_limit 0") }
27
+ end
28
+
29
+ it 'raises on invalid group_bucket_period_s' do
30
+ assert_raises { create_driver("group_bucket_period_s 0") }
31
+ end
32
+
33
+ it 'raises on invalid group_reset_rate_s' do
34
+ assert_raises { create_driver("group_bucket_limit 10\ngroup_bucket_period_s 10\ngroup_reset_rate_s 2") }
35
+ end
36
+
37
+ it 'raises on invalid group_reset_rate_s' do
38
+ assert_raises { create_driver("group_bucket_limit 10\ngroup_bucket_period_s 10\ngroup_reset_rate_s -2") }
39
+ end
40
+
41
+ it 'raises on invalid group_warning_delay_s' do
42
+ assert_raises { create_driver("group_warning_delay_s 0") }
43
+ end
44
+ end
45
+
46
+ describe '#filter' do
47
+ it 'throttles per group key' do
48
+ driver = create_driver <<~CONF
49
+ group_key "group"
50
+ group_bucket_period_s 1
51
+ group_bucket_limit 5
52
+ CONF
53
+
54
+ driver.run(default_tag: "test") do
55
+ driver.feed([[event_time, {"msg": "test", "group": "a"}]] * 10)
56
+ driver.feed([[event_time, {"msg": "test", "group": "b"}]] * 10)
57
+ end
58
+
59
+ groups = driver.filtered_records.group_by { |r| r[:group] }
60
+ assert_equal(5, groups["a"].size)
61
+ assert_equal(5, groups["b"].size)
62
+ end
63
+
64
+ it 'allows composite group keys' do
65
+ driver = create_driver <<~CONF
66
+ group_key "group1,group2"
67
+ group_bucket_period_s 1
68
+ group_bucket_limit 5
69
+ CONF
70
+
71
+ driver.run(default_tag: "test") do
72
+ driver.feed([[event_time, {"msg": "test", "group1": "a", "group2": "b"}]] * 10)
73
+ driver.feed([[event_time, {"msg": "test", "group1": "b", "group2": "b"}]] * 10)
74
+ driver.feed([[event_time, {"msg": "test", "group1": "c"}]] * 10)
75
+ driver.feed([[event_time, {"msg": "test", "group2": "c"}]] * 10)
76
+ end
77
+
78
+ groups = driver.filtered_records.group_by { |r| r[:group1] }
79
+ groups.each { |k, g| groups[k] = g.group_by { |r| r[:group2] } }
80
+
81
+ assert_equal(5, groups["a"]["b"].size)
82
+ assert_equal(5, groups["b"]["b"].size)
83
+ assert_equal(5, groups["c"][nil].size)
84
+ assert_equal(5, groups[nil]["c"].size)
85
+ end
86
+
87
+ it 'drops until rate drops below group_reset_rate_s' do
88
+ driver = create_driver <<~CONF
89
+ group_bucket_period_s 60
90
+ group_bucket_limit 180
91
+ group_reset_rate_s 2
92
+ CONF
93
+
94
+ logs_per_sec = [4, 4, 2, 1, 2]
95
+
96
+ driver.run(default_tag: "test") do
97
+ (0...logs_per_sec.size*60).each do |i|
98
+ Time.stubs(now: Time.at(i))
99
+ min = i / 60
100
+
101
+ driver.feed([[event_time, min: min]] * logs_per_sec[min])
102
+ end
103
+ end
104
+
105
+ groups = driver.filtered_records.group_by { |r| r[:min] }
106
+ messages_per_minute = []
107
+ (0...logs_per_sec.size).each do |min|
108
+ messages_per_minute[min] = groups.fetch(min, []).size
109
+ end
110
+
111
+ assert_equal [
112
+ 180, # hits the limit in the first minute
113
+ 0, # still >= group_reset_rate_s
114
+ 0, # still >= group_reset_rate_s
115
+ 59, # now < group_reset_rate_s, stop dropping logs (except the first log is dropped)
116
+ 120 # > group_reset_rate_s is okay now because haven't hit the limit
117
+ ], messages_per_minute
118
+ end
119
+
120
+
121
+ it 'does not throttle when in log only mode' do
122
+ driver = create_driver <<~CONF
123
+ group_bucket_period_s 2
124
+ group_bucket_limit 4
125
+ group_reset_rate_s 2
126
+ group_drop_logs false
127
+ CONF
128
+
129
+ records_expected = 0
130
+ driver.run(default_tag: "test") do
131
+ (0...10).each do |i|
132
+ Time.stubs(now: Time.at(i))
133
+ count = [1,8 - i].max
134
+ records_expected += count
135
+ driver.feed((0...count).map { |j| [event_time, msg: "test#{i}-#{j}"] }) # * count)
136
+ end
137
+ end
138
+
139
+ assert_equal records_expected, driver.filtered_records.size
140
+ assert driver.logs.any? { |log| log.include?('rate exceeded') }
141
+ assert driver.logs.any? { |log| log.include?('rate back down') }
142
+ end
143
+ end
144
+
145
+ describe 'logging' do
146
+ it 'logs when rate exceeded once per group_warning_delay_s' do
147
+ driver = create_driver <<~CONF
148
+ group_bucket_period_s 2
149
+ group_bucket_limit 2
150
+ group_warning_delay_s 3
151
+ CONF
152
+
153
+ logs_per_sec = 4
154
+
155
+ driver.run(default_tag: "test") do
156
+ (0...10).each do |i|
157
+ Time.stubs(now: Time.at(i))
158
+ driver.feed([[event_time, msg: "test"]] * logs_per_sec)
159
+ end
160
+ end
161
+
162
+ assert_equal 4, driver.logs.select { |log| log.include?('rate exceeded') }.size
163
+ end
164
+
165
+ it 'logs when rate drops below group_reset_rate_s' do
166
+ driver = create_driver <<~CONF
167
+ group_bucket_period_s 2
168
+ group_bucket_limit 4
169
+ group_reset_rate_s 2
170
+ CONF
171
+
172
+ driver.run(default_tag: "test") do
173
+ (0...10).each do |i|
174
+ Time.stubs(now: Time.at(i))
175
+ driver.feed([[event_time, msg: "test"]] * [1,8 - i].max)
176
+ end
177
+ end
178
+
179
+ assert driver.logs.any? { |log| log.include?('rate back down') }
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,12 @@
1
+ require 'bundler/setup'
2
+ require 'single_cov'
3
+
4
+ SingleCov.setup :minitest
5
+
6
+ require 'fluent/test'
7
+ require 'fluent/test/driver/input'
8
+ require 'fluent/test/driver/filter'
9
+ require 'fluent/test/driver/output'
10
+ require 'fluent/test/helpers'
11
+ require 'maxitest/autorun'
12
+ require 'mocha/minitest'
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uw-fluent-plugin-throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4.pre.dev
5
+ platform: ruby
6
+ authors:
7
+ - François-Xavier Bourlet
8
+ - UtilityWarehouse
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-11-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.16'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.16'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '12.3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '12.3'
42
+ - !ruby/object:Gem::Dependency
43
+ name: webmock
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.3'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: test-unit
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.2'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.2'
70
+ - !ruby/object:Gem::Dependency
71
+ name: appraisal
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.2'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '2.2'
84
+ - !ruby/object:Gem::Dependency
85
+ name: mocha
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: maxitest
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: single_cov
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: fluentd
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '1.1'
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '1.1'
140
+ description:
141
+ email:
142
+ - fx.bourlet@rubrik.com
143
+ - infra@utilitywarehouse.co.uk
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".gitignore"
149
+ - ".travis.yml"
150
+ - Gemfile
151
+ - Gemfile.lock
152
+ - LICENSE
153
+ - README.md
154
+ - Rakefile
155
+ - fluent-plugin-throttle.gemspec
156
+ - lib/fluent/plugin/filter_throttle.rb
157
+ - test/fluent/plugin/filter_throttle_test.rb
158
+ - test/helper.rb
159
+ homepage: https://github.com/utilitywarehouse/fluent-plugin-throttle
160
+ licenses:
161
+ - Apache-2.0
162
+ metadata: {}
163
+ post_install_message:
164
+ rdoc_options: []
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">"
175
+ - !ruby/object:Gem::Version
176
+ version: 1.3.1
177
+ requirements: []
178
+ rubyforge_project:
179
+ rubygems_version: 2.7.7
180
+ signing_key:
181
+ specification_version: 4
182
+ summary: Fluentd filter for throttling logs based on a configurable key.
183
+ test_files:
184
+ - test/fluent/plugin/filter_throttle_test.rb
185
+ - test/helper.rb