fluent-plugin-quota-throttle 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2426b566cfd2e8daca3b7f8906ec9158f0155dc70ce3cf0d6411d28393213ae
4
+ data.tar.gz: 4e81bc0f686d1ea4c55205abbbc7166d523812f644a17c381ed3265c5a9e1349
5
+ SHA512:
6
+ metadata.gz: 8e075b4fea32c31c4c34a1f67b96c60ded654fa6c835d5a3cce55c95d4ffda7923ec2badfc9819f754fa042614f8e783b981ee6d327f10235b409db205148d5d
7
+ data.tar.gz: 504c88b37e54ab1184f20bbf1096e3ef57a5f2613028e1dffbbb5eec336f37b55b22640bb969e954493e45b7fe5e14144665f44f91c0f0914374ec8f6662e8ff
@@ -0,0 +1,38 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "master" ]
13
+ pull_request:
14
+ branches: [ "master" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ ruby-version: ['2.6', '2.7', '3.0']
26
+
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Set up Ruby
30
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
+ # uses: ruby/setup-ruby@v1
33
+ uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
34
+ with:
35
+ ruby-version: ${{ matrix.ruby-version }}
36
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
37
+ - name: Run tests
38
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .idea
2
+ Gemfile.lock
3
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # fluent-plugin-quota-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) ![Rake Test](https://github.com/rubrikinc/fluent-plugin-quota-throttle/actions/workflows/ruby.yml/badge.svg)
4
+
5
+ A sentry plugin to throttle logs based on well defined quotas. Logs are grouped by configurable keys. When
6
+ a group exceeds a configuration rate, logs are dropped for this group.
7
+
8
+ ## Installation
9
+
10
+ TBD once the plugin is published
11
+
12
+ [//]: # (install with `gem` or td-agent provided command as:)
13
+
14
+ [//]: # ()
15
+ [//]: # (```bash)
16
+
17
+ [//]: # (# for fluentd)
18
+
19
+ [//]: # ($ gem install fluent-plugin-quota-throttle)
20
+
21
+ [//]: # (```)
22
+
23
+ ## Configuration
24
+
25
+ #### name
26
+
27
+ The name of the quota. This is used for logging and debugging purposes.
28
+
29
+ #### description
30
+
31
+ A description of the quota. This is used for documentation of the defined quota
32
+
33
+ #### group\_by
34
+
35
+ Used to group logs into buckets. Quotas are applied to buckets independently.
36
+
37
+ A dot indicates a key within a sub-object. As an example, in the following log,
38
+ the group by key `kubernetes.container_name` resolve to `random`:
39
+ ```
40
+ {"level": "error", "msg": "plugin test", "kubernetes": { "container_name": "random" } }
41
+ ```
42
+
43
+ Multiple groups can be specified, in which case each unique pair
44
+ of key values are rate limited independently.
45
+
46
+ If the group cannot be resolved, an anonymous (`nil`) group is used for rate limiting.
47
+
48
+ #### match\_by
49
+
50
+ Used to match logs to their respective quotas. If a log does not match any quota, it will be matched to the default quota.
51
+
52
+ #### bucket\_size
53
+
54
+ Maximum number logs allowed per groups over the period of `duration`.
55
+
56
+ This translate to a log rate of `bucket_size/duration`.
57
+ When a group exceeds bucket limit, logs from this group are dropped/reemitted with new tag.
58
+
59
+ For example, the rate is 6000/60s, making for a rate of 100 logs per
60
+ seconds.
61
+
62
+ Note that this is not expressed as a rate directly because there is a
63
+ difference between the overall rate and the distribution of logs over a period
64
+ time. For example, a burst of logs in the middle of a minute bucket might not
65
+ exceed the average rate of the full minute.
66
+
67
+ Consider `60/60s`, 60 logs over a minute, versus `1/1s`, 1 log per second.
68
+ Over a minute, both will emit a maximum of 60 logs. Limiting to a rate of 60
69
+ logs per minute. However `60/60s` will readily emit 60 logs within the first
70
+ second then nothing for the remaining 59 seconds. While the `1/1s` will only
71
+ emit the first log of every second.
72
+
73
+ #### duration
74
+
75
+ This is the period of of time over which `bucket_size` applies. This should be given in seconds.
76
+
77
+ #### action
78
+
79
+ Either `drop` or `reemit`.
80
+
81
+ When a group exceeds its rate limit, logs are either dropped or re-emitted with a new tag `<tag>.secondary`
82
+
83
+
84
+
85
+ #### warning\_delay
86
+
87
+ Default: `60` (seconds).
88
+
89
+ When a group reaches its limit and as long as it is not reset, a warning
90
+ message with the current log rate of the group is emitted repeatedly. This is
91
+ the delay between every repetition.
92
+
93
+ ## Usage
94
+
95
+ ```xml
96
+ <filter **>
97
+ @type quota_throttle
98
+ @path /etc/fluentd/quota_throttle.yaml
99
+ @warning_delay 30
100
+ </filter>
101
+ ```
102
+ ```yaml
103
+ quotas:
104
+ - name: quota1
105
+ description: first quota
106
+ group_by:
107
+ - group1.a
108
+ match_by:
109
+ group1.a: value1
110
+ bucket_size: 100
111
+ duration: 60
112
+ action: drop
113
+ - name: quota2
114
+ description: second quota
115
+ group_by:
116
+ - group1.a
117
+ - group1.b
118
+ match_by:
119
+ group1.a: value2
120
+ group1.b: value3
121
+ bucket_size: 200
122
+ duration: 120
123
+ action: reemit
124
+ - name: quota3
125
+ description: third quota
126
+ group_by:
127
+ - group2
128
+ - group3
129
+ match_by:
130
+ group2: value2
131
+ group3: value3
132
+ bucket_size: 300
133
+ duration: 180
134
+ action: drop
135
+ default:
136
+ description: default quota
137
+ group_by:
138
+ - group1.a
139
+ bucket_size: 300
140
+ duration: 180
141
+ action: reemit
142
+ ```
143
+
144
+ ## License
145
+
146
+ Apache License, Version 2.0
147
+
148
+ ## Copyright
149
+
150
+ Copyright © 2018 ([Rubrik Inc.](https://www.rubrik.com))
data/Rakefile ADDED
@@ -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,30 @@
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 = "fluent-plugin-quota-throttle"
7
+ spec.version = "0.0.2"
8
+ spec.authors = ["Athish Pranav D", "Dipendra Singh", "Rubrik Inc."]
9
+ spec.email = ["athish.pranav@rubrik.com", "Dipendra.Singh@rubrik.com"]
10
+ spec.summary = %q{Fluentd filter for throttling logs based on a configurable quotas.}
11
+ spec.homepage = "https://github.com/rubrikinc/fluent-plugin-quota-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"
20
+ spec.add_development_dependency "minitest" , "~> 5.14"
21
+ spec.add_development_dependency "test-unit" , "~> 3.6"
22
+ spec.add_development_dependency "rake" , "~> 13.0"
23
+ spec.add_development_dependency "mutex_m" , "~> 0.1"
24
+ spec.add_development_dependency "webrick" , "~> 1.8"
25
+ spec.add_development_dependency "csv" , "~> 3.3"
26
+ spec.add_development_dependency "base64" , "~> 0.2"
27
+
28
+ spec.add_runtime_dependency "fluentd" , "~> 1.9"
29
+ spec.add_runtime_dependency "fluent-plugin-prometheus", " = 2.1.0"
30
+ end
@@ -0,0 +1,74 @@
1
+ require 'yaml'
2
+ require 'fluent/config'
3
+ ##
4
+ # ConfigParser module contains classes to parse the configuration file and store the quotas
5
+ module ConfigParser
6
+
7
+ ##
8
+ # Quota class represents a single quota configuration
9
+ # Attributes:
10
+ # +name+: (String) The name of the quota.
11
+ # +desc+: (String) A description of the quota.
12
+ # +group_by+: (Array of Arrays of Strings) Specifies how to group transactions or events for quota tracking.
13
+ # +match_by+: (Hash) Defines the conditions that must be met for the quota to apply. Keys are the conditions to match, and values are the expected values.
14
+ # +bucket_size+: (Integer) The size of the quota bucket, indicating how many transactions or events can occur before the quota is exceeded. A value of -1 indicates no quota limit.
15
+ # +duration+: (Integer) The duration (in seconds) for which the quota bucket size is valid.
16
+ # +action+: (String) The action to take when the quota is reached. Must be one of the predefined actions in @@allowed_actions.
17
+ class Quota
18
+
19
+ attr_accessor :name, :desc, :group_by, :match_by, :bucket_size, :duration, :action
20
+
21
+ @@allowed_actions = Set["drop", "reemit"]
22
+
23
+ def initialize(name, desc, group_by, match_by, bucket_size, duration, action)
24
+ raise "Name cannot be empty" if name.nil?
25
+ raise "Group by cannot be empty" if group_by.nil?
26
+ raise "Bucket size cannot be empty" unless bucket_size.is_a?(Integer)
27
+ raise "Duration must be time delta (eg. 2s, 4m)" if duration.nil? || !duration.is_a?(String) || duration.strip.empty?
28
+ raise "Action must be one of #{@@allowed_actions}" unless @@allowed_actions.include?action
29
+ @name = name
30
+ @desc = desc
31
+ @group_by = group_by
32
+ @match_by = match_by
33
+ @bucket_size = bucket_size
34
+ @duration = Fluent::Config.time_value(duration)
35
+ @action = action
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Configuration class parses the configuration file and stores the quotas
41
+ # Attributes:
42
+ # +quotas+: (Array of Quota) The list of quotas parsed from the configuration file.
43
+ # +default_quota+: (Quota) The default quota to use when no other quota matches.
44
+ class Configuration
45
+
46
+ attr_reader :quotas,:default_quota
47
+
48
+ def initialize(config_file_path)
49
+ @config_file= YAML.load_file(config_file_path)
50
+ @quotas = nil
51
+ @default_quota = nil
52
+ parse_quotas
53
+ end
54
+
55
+ private
56
+
57
+ # Parses the quotas from the configuration file into Quota objects and stores them in @quotas
58
+ def parse_quotas
59
+ if @config_file.has_key?("quotas")
60
+ @quotas = @config_file["quotas"].map do |quota|
61
+ group_key = quota["group_by"].map { |key| key.split(".") }
62
+ match_by = quota["match_by"].map { |key,value| [key.split(".") , value] }.to_h
63
+ Quota.new(quota["name"], quota["description"], group_key, match_by, quota["bucket_size"], quota["duration"], quota["action"])
64
+ end
65
+ end
66
+ if @config_file.has_key?("default")
67
+ default_quota_config = @config_file["default"]
68
+ @default_quota = Quota.new("default", default_quota_config["description"], default_quota_config["group_by"].map { |key| key.split(".") }, [], default_quota_config["bucket_size"], default_quota_config["duration"], default_quota_config["action"])
69
+ else
70
+ @default_quota = Quota.new("default", "Default quota", [], [], -1, 0, "drop")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,142 @@
1
+ require 'fluent/plugin/filter'
2
+ require 'fluent/plugin/prometheus'
3
+ require_relative 'config_parser'
4
+ require_relative 'matcher'
5
+ require_relative 'rate_limiter'
6
+
7
+ ##
8
+ # Fluentd plugin that filters records based on quotas
9
+ module Fluent::Plugin
10
+
11
+ ##
12
+ # QuotaThrottleFilter class is derived from the Filter class and is responsible for filtering records based on quotas
13
+ class QuotaThrottleFilter < Filter
14
+ Fluent::Plugin.register_filter('quota_throttle', self)
15
+ include Fluent::Plugin::PrometheusLabelParser
16
+ include Fluent::Plugin::Prometheus
17
+ attr_reader :registry
18
+
19
+ desc "Path for the quota config file"
20
+ config_param :path, :string, :default => nil
21
+
22
+ desc "Delay in seconds between warnings for the same group when the quota is breached"
23
+ config_param :warning_delay, :time, :default => 60
24
+
25
+ desc "Enable prometheus metrics"
26
+ config_param :enable_metrics, :bool, :default => false
27
+
28
+ def initialize
29
+ super
30
+ @reemit_tag_prefix = "secondary"
31
+ @registry = ::Prometheus::Client.registry
32
+ @placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
33
+ end
34
+
35
+ # Configures the plugin
36
+ def configure(conf)
37
+ super
38
+ raise "quota config file should not be empty" \
39
+ if @path.nil? or !File.exist?(@path)
40
+ raise "Warning delay should be non negative" \
41
+ if @warning_delay < 0
42
+ parsed_config = ConfigParser::Configuration.new(@path)
43
+ @match_helper = Matcher::MatchHelper.new(parsed_config.quotas, parsed_config.default_quota)
44
+ if @enable_metrics
45
+ @base_labels = parse_labels_elements(conf)
46
+ end
47
+ end
48
+
49
+ def start
50
+ super
51
+ @bucket_store = RateLimiter::BucketStore.new
52
+ if @enable_metrics
53
+ @metrics = {
54
+ quota_input: get_counter(:fluentd_quota_throttle_input, "Number of records entering quota throttle plugin"),
55
+ quota_exceeded: get_counter(:fluentd_quota_throttle_exceeded, "Number of records exceeded the quota"),
56
+ }
57
+ end
58
+ end
59
+
60
+ def shutdown
61
+ super
62
+ if @enable_metrics
63
+ log.info "Clearing Counters"
64
+ @metrics.each do |name, metric|
65
+ @registry.unregister(name)
66
+ end
67
+ end
68
+ log.info "Shutting down"
69
+ end
70
+
71
+ ##
72
+ # Filters records based on quotas
73
+ # Params:
74
+ # +tag+: (String) The tag of the record
75
+ # +time+: (Time) The timestamp of the record
76
+ # +record+: (Hash) The record to filter
77
+ def filter(tag, time, record)
78
+ @bucket_store.clean_buckets
79
+ quota = @match_helper.get_quota(record)
80
+ group = quota.group_by.map { |key| record.dig(*key) }
81
+ bucket = @bucket_store.get_bucket(group, quota)
82
+ labels = {}
83
+ if @enable_metrics
84
+ labels = get_labels(record)
85
+ @metrics[:quota_input].increment(by: 1, labels: labels.merge({quota: quota.name}))
86
+ end
87
+ if bucket.allow
88
+ record
89
+ else
90
+ if @enable_metrics
91
+ @metrics[:quota_exceeded].increment(by: 1, labels: labels.merge({quota: quota.name}))
92
+ end
93
+ quota_breached(tag, time, record, bucket, quota)
94
+ nil
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # Logs a warning and takes the action specified in the quota
101
+ # Params:
102
+ # +bucket+: (Bucket) The bucket that has breached the quota
103
+ # +quota+: (Quota) The quota that has been breached
104
+ # +timestamp+: (Time) The timestamp of the record
105
+ def quota_breached(tag, timestamp, record, bucket, quota)
106
+ if bucket.last_warning.nil? || Time.now - bucket.last_warning > @warning_delay
107
+ log.warn "Quota breached for group #{bucket.group} in quota #{quota.name}"
108
+ bucket.last_warning = Time.now
109
+ end
110
+ case quota.action
111
+ when "drop"
112
+ log.debug "Dropping record"
113
+ when "reemit"
114
+ log.debug "Reemitting record"
115
+ new_tag = "#{@reemit_tag_prefix}.#{tag}"
116
+ router.emit(new_tag, timestamp, record)
117
+ end
118
+ end
119
+
120
+ def get_labels(record)
121
+ placeholders = stringify_keys(record)
122
+ expander = @placeholder_expander_builder.build(placeholders)
123
+ labels = {}
124
+ @base_labels.each do |key, value|
125
+ if value.is_a?(String)
126
+ labels[key] = expander.expand(value)
127
+ elsif value.respond_to?(:call)
128
+ labels[key] = value.call(record)
129
+ end
130
+ end
131
+ labels
132
+ end
133
+
134
+ def get_counter(name, docstring)
135
+ if @registry.exist?(name)
136
+ @registry.get(name)
137
+ else
138
+ @registry.counter(name, docstring: docstring, labels: @base_labels.keys + ["quota"].map(&:to_sym))
139
+ end
140
+ end
141
+ end
142
+ end