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 +7 -0
- data/.gitignore +1 -0
- data/README.md +117 -0
- data/fluent-plugin-throttle.gemspec +26 -0
- data/lib/fluent/plugin/filter_throttle.rb +139 -0
- metadata +132 -0
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: []
|