fluent-plugin-throttle 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a6e438f87f8be6572c11b82e0dce8c493b3ed92d
4
+ data.tar.gz: e6cb87f9b709ea05d72e8244c72b2daf814ed741
5
+ SHA512:
6
+ metadata.gz: 814306a6ae6eee257ea97734a101f5edd0daacf6754f2dbc7e031f3e8be01dbe05bc0d6592dce6911f43d1c9b082f4597fc43acc687c4ff7180d51eb04190d07
7
+ data.tar.gz: 72ea81c1dd748d0c5ac0650c6add62d4eb2302f1d4e5704b886161cc6fcd00ddc123948e9c2ce58ea69fbcc14d85773361e1a012851e12c4cbc4e273cdb734fb
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # fluent-plugin-throttle
2
+
3
+ A sentry pluging to throttle logs. Logs are grouped by a configurable key. When
4
+ a group exceeds a configuration rate, logs are dropped for this group.
5
+
6
+ ## Installation
7
+
8
+ install with `gem` or td-agent provided command as:
9
+
10
+ ```bash
11
+ # for fluentd
12
+ $ gem install fluent-plugin-throttle
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```xml
18
+ <filter>
19
+ ...
20
+ </filter>
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ ### group\_key
26
+
27
+ Default: `kubernetes.container_name`.
28
+
29
+ Used to group logs. Groups are rate limited independently.
30
+
31
+ A dot indicates a key within a sub-object. As an example, in the following log,
32
+ the group key resolve to "random":
33
+ ```
34
+ {"level": "error", "msg": "plugin test", "kubernetes": { "container_name": "random" } }
35
+ ```
36
+
37
+ If the group cannot be resolved, am anonymous group is used.
38
+
39
+ ## group\_bucket\_period\_s
40
+
41
+ Default: `60` (60 second).
42
+
43
+ This is the period of of time over which `group_bucket_limit` applies.
44
+
45
+ ## group\_bucket\_limit
46
+
47
+ Default: `6000` (logs per `group_bucket_period_s`).
48
+
49
+ Maximum number logs allowed per groups over the period of `group_bucket_period_s`.
50
+
51
+ This translate to a log rate of `group_bucket_limit/group_bucket_period_s`.
52
+ When a group exceeds this rate, logs from this group are dropped.
53
+
54
+ For example, the default is 6000/60s, making for a rate of 100 logs per
55
+ seconds.
56
+
57
+ Note that this is not expressed as a rate directly because there is a
58
+ difference between the overall rate and the distribution of logs over a period
59
+ time. For example, a burst of logs in the middle of a minute bucket might not
60
+ exceed the average rate of the full minute.
61
+
62
+ Consider `60/60s`, 60 logs over a minute, versus `1/1s`, 1 log per second.
63
+ Over a minute, both will emit a maximum of 60 logs. Limiting to a rate of 60
64
+ logs per minute. However `60/60s` will readily emit 60 logs within the first
65
+ second then nothing for the remaining 59 seconds. While the `1/1s` will only
66
+ emit the first log of every second.
67
+
68
+ ## group\_reset\_rate\_s
69
+
70
+ Default: `group_bucket_limit/group_bucket_period_s` (logs per `group_bucket_period_s`).
71
+ Max: `group_bucket_limit`.
72
+
73
+ After a group has exceeded its bucket limit, logs are dropped until the rate
74
+ per second falls below or equal to `group_reset_rate_s`.
75
+
76
+ The default value is `group_bucket_limits/group_bucket_period_s`. For example
77
+ for 3600 logs per hour, the reset will defaults to `3600/3600s = 1/s`, one log
78
+ per second.
79
+
80
+ Taking the example `3600 log/hour` with the default reset rate of `1 log/s`
81
+ further:
82
+
83
+ - Let's say we have a period of 10 hours.
84
+ - During the first hour, 2 logs/s are produced. After 30 minutes, the hourly
85
+ bucket has reached its limit, and logs are dropped. At this point the rate
86
+ is still 2 logs/s for the remaining 30 minutes.
87
+ - Because the last hour finished on 2 logs/s, which is higher that the
88
+ `1 log/s` reset, all logs are still dropped when starting the second hour. The
89
+ bucket limit is left untouched since nothing is being emitted.
90
+ - Now, at 2 hours and 30 minutes, the log rate halves to `1 log/s`, which is
91
+ equal to the reset rate. Logs are emitted again, counting toward the bucket
92
+ limit as normal. Allowing up to 3600 logs for the last 30 minutes of the second
93
+ hour.
94
+
95
+ Because this could allow for some instability if the log rate hovers around the
96
+ `group_bucket_limit/group_bucket_period_s` rate, it is possible to set a
97
+ different reset rate.
98
+
99
+ Note that a value of `0` effectively means the plugin will drops logs forever
100
+ after a single breach of the limit until the next restart of fluentd.
101
+
102
+ A value of `-1` disables the feature.
103
+
104
+ ## group\_warning\_hz
105
+
106
+ Default: `0.1` (10 seconds).
107
+
108
+ When a group reaches its limit and as long as it is not reset, a warning
109
+ message with the current log rate for the group is logged at this frequency.
110
+
111
+ ## Copyright
112
+
113
+ Copyright © 2018 ([Rubrik Inc.](https://www.rubrik.com))
114
+
115
+ ## License
116
+
117
+ Apache License, Version 2.0
@@ -0,0 +1,26 @@
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-throttle"
7
+ spec.version = "0.0.1"
8
+ spec.authors = ["François-Xavier Bourlet"]
9
+ spec.email = ["fx.bourlet@rubrik.com"]
10
+ spec.summary = %q{Fluentd filter for throttling logs based on a configurable key.}
11
+ spec.homepage = "https://github.com/bombela/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
+
25
+ spec.add_runtime_dependency "fluentd", "~> 1.1"
26
+ end
@@ -0,0 +1,139 @@
1
+ require 'fluent/filter'
2
+
3
+ module Fluent
4
+ class ThrottleFilter < Filter
5
+ Fluent::Plugin.register_filter('throttle', self)
6
+
7
+ config_param :group_key, :string, :default => 'kubernetes.container_name'
8
+ config_param :group_bucket_period_s, :integer, :default => 60
9
+ config_param :group_bucket_limit, :integer, :default => 6000
10
+ config_param :group_reset_rate_s, :integer, :default => nil
11
+ config_param :warning_hz, :float, :default => 0.1
12
+
13
+ Bucket = Struct.new(:emitted, :last_reset)
14
+ Group = Struct.new(
15
+ :rate_count,
16
+ :rate_last_reset,
17
+ :rate,
18
+ :bucket_count,
19
+ :bucket_last_reset,
20
+ :last_warning)
21
+
22
+ def configure(conf)
23
+ super
24
+
25
+ @group_key_path = group_key.split(".")
26
+
27
+ raise "group_bucket_period_s must be > 0" \
28
+ unless @group_bucket_period_s > 0
29
+
30
+ raise "group_bucket_limit must be > 0" \
31
+ unless @group_bucket_limit > 0
32
+
33
+ @group_rate_limit = (@group_bucket_limit / @group_bucket_period_s)
34
+
35
+ @group_reset_rate_s = @group_rate_limit \
36
+ if @group_reset_rate_s == nil
37
+
38
+ raise "group_reset_rate_s must be >= -1" \
39
+ unless @group_reset_rate_s >= -1
40
+ raise "group_reset_rate_s must be <= group_bucket_limit / group_bucket_period_s" \
41
+ unless @group_reset_rate_s <= @group_rate_limit
42
+
43
+ @warning_delay = (1.0 / @warning_hz)
44
+ end
45
+
46
+ def start
47
+ super
48
+
49
+ @counters = Hash.new()
50
+ end
51
+
52
+ def shutdown
53
+ $log.info("counters summary: #{@counters}")
54
+ super
55
+ end
56
+
57
+ def filter(tag, time, record)
58
+ now = Time.now
59
+ group = extract_group(record)
60
+ counter = @counters.fetch(group, nil)
61
+ counter = @counters[group] = Group.new(
62
+ 0, now, 0, 0, now, nil) if counter == nil
63
+
64
+ counter.rate_count += 1
65
+
66
+ since_last_rate_reset = now - counter.rate_last_reset
67
+ if since_last_rate_reset >= 1
68
+ # compute and store rate/s at most every seconds.
69
+ counter.rate = (counter.rate_count / since_last_rate_reset).round()
70
+ counter.rate_count = 0
71
+ counter.rate_last_reset = now
72
+ end
73
+
74
+ if (now.to_i / @group_bucket_period_s) \
75
+ > (counter.bucket_last_reset.to_i / @group_bucket_period_s)
76
+ # next time period reached, reset limit.
77
+
78
+ if counter.bucket_count == -1 and @group_reset_rate_s != -1
79
+ # wait until rate drops back down if needed.
80
+ if counter.rate < @group_reset_rate_s
81
+ log_rate_back_down(now, group, counter)
82
+ else
83
+ since_last_warning = now - counter.last_warning
84
+ if since_last_warning > @warning_delay
85
+ log_rate_limit_exceeded(now, group, counter)
86
+ counter.last_warning = now
87
+ end
88
+ return nil
89
+ end
90
+ end
91
+
92
+ counter.bucket_count = 0
93
+ counter.bucket_last_reset = now
94
+ end
95
+
96
+ if counter.bucket_count == -1
97
+ return nil
98
+ end
99
+
100
+ counter.bucket_count += 1
101
+
102
+ if counter.bucket_count > @group_bucket_limit
103
+ log_rate_limit_exceeded(now, group, counter)
104
+ counter.last_warning = now
105
+ counter.bucket_count = -1
106
+ return nil
107
+ end
108
+
109
+ record
110
+ end
111
+
112
+ def extract_group(record)
113
+ record.dig(*@group_key_path)
114
+ end
115
+
116
+ def log_rate_limit_exceeded(now, group, counter)
117
+ $log.warn("rate exceeded", log_items(now, group, counter))
118
+ end
119
+
120
+ def log_rate_back_down(now, group, counter)
121
+ $log.info("rate back down", log_items(now, group, counter))
122
+ end
123
+
124
+ def log_items(now, group, counter)
125
+ rate = counter.rate
126
+ if rate == 0
127
+ since_last_reset = now - counter.bucket_last_reset
128
+ rate = (counter.bucket_count / since_last_reset).round()
129
+ end
130
+
131
+ {'group_key': group,
132
+ 'rate_s': rate,
133
+ 'period_s': @group_bucket_period_s,
134
+ 'limit': @group_bucket_limit,
135
+ 'rate_limit_s': @group_rate_limit,
136
+ 'reset_rate_s': @group_reset_rate_s}
137
+ end
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - François-Xavier Bourlet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: test-unit
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: appraisal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: fluentd
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
97
+ description:
98
+ email:
99
+ - fx.bourlet@rubrik.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - README.md
106
+ - fluent-plugin-throttle.gemspec
107
+ - lib/fluent/plugin/filter_throttle.rb
108
+ homepage: https://github.com/bombela/fluent-plugin-throttle
109
+ licenses:
110
+ - Apache-2.0
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.5.2.1
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Fluentd filter for throttling logs based on a configurable key.
132
+ test_files: []