fluent-plugin-throttle 0.0.1

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
+ 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: []