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 +7 -0
- data/.github/workflows/ruby.yml +38 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE +201 -0
- data/README.md +150 -0
- data/Rakefile +9 -0
- data/fluent-plugin-quota-throttle.gemspec +30 -0
- data/lib/fluent/plugin/config_parser.rb +74 -0
- data/lib/fluent/plugin/filter_quota_throttle.rb +142 -0
- data/lib/fluent/plugin/matcher.rb +61 -0
- data/lib/fluent/plugin/rate_limiter.rb +113 -0
- data/test/config_files/filter_plugin_test.yml +28 -0
- data/test/config_files/matcher_test.yml +39 -0
- data/test/config_files/parser_test.yml +28 -0
- data/test/fluent/plugin/filter_quota_throttle_test.rb +85 -0
- data/test/helper.rb +10 -0
- data/test/modules/config_parser_test.rb +42 -0
- data/test/modules/matcher_test.rb +67 -0
- data/test/modules/rate_limiter_test.rb +72 -0
- data/test/scripts/example.conf +24 -0
- data/test/scripts/example.yml +24 -0
- data/test/scripts/test.sh +10 -0
- metadata +219 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
##
|
2
|
+
# This module is responsible for matching records to quotas.
|
3
|
+
module Matcher
|
4
|
+
|
5
|
+
##
|
6
|
+
# MatchHelper class is responsible for matching records to quotas.
|
7
|
+
# Methods:
|
8
|
+
# +get_quota+: Takes a list of keys and returns the quota that maximally matches
|
9
|
+
# +matching_score+: Calculates the matching score between two hashes.
|
10
|
+
class MatchHelper
|
11
|
+
|
12
|
+
def initialize(processed_quotas,default_quota)
|
13
|
+
@quotas = processed_quotas
|
14
|
+
@default_quota = default_quota
|
15
|
+
end
|
16
|
+
|
17
|
+
# Takes a list of keys and returns the quota that maximally matches
|
18
|
+
# If no quota matches, returns the default quota
|
19
|
+
# Params:
|
20
|
+
# +record+: (Hash) A hash of keys and values to match against the quotas
|
21
|
+
# Returns:
|
22
|
+
# +quota+: (Quota Class)The quota that maximally matches the record
|
23
|
+
def get_quota(record)
|
24
|
+
|
25
|
+
max_score = 0
|
26
|
+
quota_to_return = @default_quota
|
27
|
+
if @quotas.nil?
|
28
|
+
return @default_quota
|
29
|
+
end
|
30
|
+
@quotas.each do |quota|
|
31
|
+
score = matching_score(quota.match_by, record)
|
32
|
+
if score > max_score
|
33
|
+
max_score = score
|
34
|
+
quota_to_return = quota
|
35
|
+
end
|
36
|
+
end
|
37
|
+
quota_to_return
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Calculates the matching score between two hashes.
|
43
|
+
# Params:
|
44
|
+
# +match+: (Hash) A hash of keys and values to match against the record
|
45
|
+
# +record+: (Hash) A hash of keys and values to match against the match
|
46
|
+
def matching_score(match, record)
|
47
|
+
score = 0
|
48
|
+
if match.nil? || record.nil?
|
49
|
+
return 0
|
50
|
+
end
|
51
|
+
match.each do |key, value|
|
52
|
+
if record.dig(*key) == value
|
53
|
+
score += 1
|
54
|
+
else
|
55
|
+
return 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
score
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
##
|
2
|
+
# Rate Limiter module, contains the rate limiting logic
|
3
|
+
module RateLimiter
|
4
|
+
|
5
|
+
##
|
6
|
+
# Bucket class, contains the rate limiting logic for each group
|
7
|
+
# Attributes:
|
8
|
+
# +bucket_count+: Number of requests in the bucket
|
9
|
+
# +bucket_last_reset+: Time when the bucket was last reset
|
10
|
+
# +approx_rate_per_second+: Approximate rate of requests per second
|
11
|
+
# +rate_last_reset+: Time when the rate was last reset
|
12
|
+
# +curr_count+: Number of requests in the current second
|
13
|
+
# +last_warning+: Time when the last warning was issued
|
14
|
+
# +timeout_s+: Timeout for the bucket
|
15
|
+
# +bucket_limit+: Maximum number of requests allowed in the bucket
|
16
|
+
# +bucket_period+: Time period for the bucket
|
17
|
+
# +rate_limit+: Maximum number of requests allowed per second
|
18
|
+
class Bucket
|
19
|
+
attr_accessor :bucket_count, :bucket_last_reset, :approx_rate_per_second, :rate_last_reset, :curr_count, :last_warning
|
20
|
+
attr_reader :bucket_limit, :bucket_period, :rate_limit, :timeout_s, :group
|
21
|
+
def initialize( group, bucket_limit, bucket_period)
|
22
|
+
now = Time.now
|
23
|
+
@group = group
|
24
|
+
@bucket_count = 0
|
25
|
+
@bucket_last_reset = now
|
26
|
+
@approx_rate_per_second = 0
|
27
|
+
@rate_last_reset = now
|
28
|
+
@curr_count = 0
|
29
|
+
@last_warning = nil
|
30
|
+
@bucket_limit = bucket_limit
|
31
|
+
@bucket_period = bucket_period
|
32
|
+
@rate_limit = bucket_limit/bucket_period
|
33
|
+
@timeout_s = 2*bucket_period
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if the bucket is free or full
|
37
|
+
# Returns:
|
38
|
+
# +true+ if the bucket is free
|
39
|
+
# +false+ if the bucket is full
|
40
|
+
def allow
|
41
|
+
if @bucket_limit == -1
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
now = Time.now
|
45
|
+
@curr_count += 1
|
46
|
+
time_lapsed = now - @rate_last_reset
|
47
|
+
|
48
|
+
if time_lapsed.to_i >= 1
|
49
|
+
@approx_rate_per_second = @curr_count / time_lapsed
|
50
|
+
@rate_last_reset = now
|
51
|
+
@curr_count = 0
|
52
|
+
end
|
53
|
+
|
54
|
+
if now.to_i / @bucket_period > @bucket_last_reset.to_i / @bucket_period
|
55
|
+
reset_bucket
|
56
|
+
end
|
57
|
+
|
58
|
+
if @bucket_count == -1 or @bucket_count > @bucket_limit
|
59
|
+
@bucket_count = -1
|
60
|
+
return false
|
61
|
+
else
|
62
|
+
@bucket_count += 1
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks if bucket is expired
|
68
|
+
# Returns:
|
69
|
+
# +true+ if the bucket is expired
|
70
|
+
# +false+ if the bucket is not expired
|
71
|
+
def expired
|
72
|
+
now = Time.now
|
73
|
+
now.to_i - @rate_last_reset.to_i > @timeout_s
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Resets the bucket when the window moves to the next time period
|
79
|
+
def reset_bucket
|
80
|
+
now = Time.now
|
81
|
+
unless @bucket_count == -1 && @approx_rate_per_second > @rate_limit
|
82
|
+
@bucket_count = 0
|
83
|
+
@bucket_last_reset = now
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# BucketStore class, organizes the all the group buckets
|
90
|
+
# Attributes:
|
91
|
+
# +buckets+: Hash containing all the group buckets
|
92
|
+
class BucketStore
|
93
|
+
def initialize
|
94
|
+
@buckets = {}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Gets the bucket for the group
|
98
|
+
# Arguments:
|
99
|
+
# +group+: Group for which the bucket is required
|
100
|
+
# +quota+: Quota object containing the bucket size and duration
|
101
|
+
def get_bucket(group, quota)
|
102
|
+
@buckets[group] = @buckets.delete(group) || Bucket.new( group, quota.bucket_size, quota.duration)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Cleans the buckets that have expired
|
106
|
+
def clean_buckets
|
107
|
+
lru_group, lru_bucket = @buckets.first
|
108
|
+
if !lru_group.nil? && lru_bucket.expired
|
109
|
+
@buckets.delete(lru_group)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
quotas:
|
2
|
+
- name: quota1
|
3
|
+
description: first quota
|
4
|
+
group_by:
|
5
|
+
- group1.a
|
6
|
+
match_by:
|
7
|
+
group1.a: value1
|
8
|
+
bucket_size: 5
|
9
|
+
duration: 60s
|
10
|
+
action: drop
|
11
|
+
- name: quota2
|
12
|
+
description: second quota
|
13
|
+
group_by:
|
14
|
+
- group1.a
|
15
|
+
- group1.b
|
16
|
+
match_by:
|
17
|
+
group1.a: value2
|
18
|
+
group1.b: value3
|
19
|
+
bucket_size: 6
|
20
|
+
duration: 120s
|
21
|
+
action: reemit
|
22
|
+
default:
|
23
|
+
description: default quota
|
24
|
+
group_by:
|
25
|
+
- group1.a
|
26
|
+
bucket_size: 4
|
27
|
+
duration: 180s
|
28
|
+
action: reemit
|
@@ -0,0 +1,39 @@
|
|
1
|
+
quotas:
|
2
|
+
- name: quota1
|
3
|
+
description: first quota
|
4
|
+
group_by:
|
5
|
+
- group1.a
|
6
|
+
match_by:
|
7
|
+
group1.a: value1
|
8
|
+
bucket_size: 100
|
9
|
+
duration: 60s
|
10
|
+
action: drop
|
11
|
+
- name: quota2
|
12
|
+
description: second quota
|
13
|
+
group_by:
|
14
|
+
- group1.a
|
15
|
+
- group1.b
|
16
|
+
match_by:
|
17
|
+
group1.a: value2
|
18
|
+
group1.b: value3
|
19
|
+
bucket_size: 200
|
20
|
+
duration: 120s
|
21
|
+
action: reemit
|
22
|
+
- name: quota3
|
23
|
+
description: third quota
|
24
|
+
group_by:
|
25
|
+
- group2
|
26
|
+
- group3
|
27
|
+
match_by:
|
28
|
+
group2: value2
|
29
|
+
group3: value3
|
30
|
+
bucket_size: 300
|
31
|
+
duration: 180s
|
32
|
+
action: drop
|
33
|
+
default:
|
34
|
+
description: default quota
|
35
|
+
group_by:
|
36
|
+
- group1.a
|
37
|
+
bucket_size: 300
|
38
|
+
duration: 180s
|
39
|
+
action: reemit
|
@@ -0,0 +1,28 @@
|
|
1
|
+
quotas:
|
2
|
+
- name: quota1
|
3
|
+
description: first quota
|
4
|
+
group_by:
|
5
|
+
- group1.a
|
6
|
+
match_by:
|
7
|
+
group1.a: value1
|
8
|
+
bucket_size: 100
|
9
|
+
duration: 1m
|
10
|
+
action: drop
|
11
|
+
- name: quota2
|
12
|
+
description: second quota
|
13
|
+
group_by:
|
14
|
+
- group1.a
|
15
|
+
- group1.b
|
16
|
+
match_by:
|
17
|
+
group1.a: value2
|
18
|
+
group1.b: value3
|
19
|
+
bucket_size: 200
|
20
|
+
duration: 120s
|
21
|
+
action: reemit
|
22
|
+
default:
|
23
|
+
description: default quota
|
24
|
+
group_by:
|
25
|
+
- group1.a
|
26
|
+
bucket_size: 300
|
27
|
+
duration: 3m
|
28
|
+
action: reemit
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative '../../helper'
|
2
|
+
|
3
|
+
class QuotaThrottleFilterTest < Minitest::Test
|
4
|
+
include Fluent::Test::Helpers
|
5
|
+
|
6
|
+
def setup
|
7
|
+
Fluent::Test.setup
|
8
|
+
end
|
9
|
+
|
10
|
+
CONFIG = %[
|
11
|
+
path test/config_files/filter_plugin_test.yml
|
12
|
+
warning_delay 2m
|
13
|
+
enable_metrics false
|
14
|
+
]
|
15
|
+
|
16
|
+
def create_driver(conf = CONFIG)
|
17
|
+
Fluent::Test::Driver::Filter.new(Fluent::Plugin::QuotaThrottleFilter).configure(conf)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_configure
|
21
|
+
d = create_driver
|
22
|
+
assert_equal "test/config_files/filter_plugin_test.yml", d.instance.path
|
23
|
+
assert_equal 120, d.instance.warning_delay
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_filter
|
27
|
+
d = create_driver
|
28
|
+
d.run(default_tag: 'test') do
|
29
|
+
10.times do
|
30
|
+
d.feed("group1" => { "a" => "value1" , "b" => "value2" })
|
31
|
+
d.feed("group1" => { "a" => "value2" , "b" => "value3" })
|
32
|
+
d.feed("group1" => { "a" => "value2" , "b" => "value2" })
|
33
|
+
d.feed("group1" => { "a" => "value3" , "b" => "value2" })
|
34
|
+
end
|
35
|
+
end
|
36
|
+
events = d.filtered_records
|
37
|
+
assert_equal 23, events.length
|
38
|
+
end
|
39
|
+
|
40
|
+
# Due to the way the Driver is implemented, both metrics tests cannot be same time because the registry of metrics is not cleared between tests
|
41
|
+
# def test_metrics_without_labels
|
42
|
+
# modified_config = CONFIG.sub("enable_metrics false", "enable_metrics true")
|
43
|
+
# d = create_driver(modified_config)
|
44
|
+
# d.run(default_tag: 'test') do
|
45
|
+
# 10.times do
|
46
|
+
# d.feed("group1" => { "a" => "value1" , "b" => "value2" })
|
47
|
+
# d.feed("group1" => { "a" => "value2" , "b" => "value3" })
|
48
|
+
# d.feed("group1" => { "a" => "value2" , "b" => "value2" })
|
49
|
+
# d.feed("group1" => { "a" => "value3" , "b" => "value2" })
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
# assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {quota: 'quota1'})
|
53
|
+
# assert_equal 4, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {quota: 'quota1'})
|
54
|
+
# assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {quota: 'quota2'})
|
55
|
+
# assert_equal 3, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {quota: 'quota2'})
|
56
|
+
# assert_equal 20, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {quota: 'default'})
|
57
|
+
# assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {quota: 'default'})
|
58
|
+
# end
|
59
|
+
def test_metrics_with_labels
|
60
|
+
labels = %[
|
61
|
+
<labels>
|
62
|
+
source $.group1.a
|
63
|
+
dummy d1
|
64
|
+
</labels>
|
65
|
+
]
|
66
|
+
modified_config = CONFIG.sub("enable_metrics false", "enable_metrics true" + labels)
|
67
|
+
d = create_driver(modified_config)
|
68
|
+
d.run(default_tag: 'test') do
|
69
|
+
10.times do
|
70
|
+
d.feed("group1" => { "a" => "value1" , "b" => "value2" })
|
71
|
+
d.feed("group1" => { "a" => "value2" , "b" => "value3" })
|
72
|
+
d.feed("group1" => { "a" => "value2" , "b" => "value2" })
|
73
|
+
d.feed("group1" => { "a" => "value3" , "b" => "value2" })
|
74
|
+
end
|
75
|
+
end
|
76
|
+
assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {source: "value1", quota: 'quota1', dummy: 'd1'})
|
77
|
+
assert_equal 4, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {source: "value1", quota: 'quota1', dummy: 'd1'})
|
78
|
+
assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {source: "value2", quota: 'quota2', dummy: 'd1'})
|
79
|
+
assert_equal 3, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {source: "value2", quota: 'quota2', dummy: 'd1'})
|
80
|
+
assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {source: "value2", quota: 'default', dummy: 'd1'})
|
81
|
+
assert_equal 5, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {source: "value2", quota: 'default', dummy: 'd1'})
|
82
|
+
assert_equal 10, d.instance.registry.get(:fluentd_quota_throttle_input).get(labels: {source: "value3", quota: 'default', dummy: 'd1'})
|
83
|
+
assert_equal 5, d.instance.registry.get(:fluentd_quota_throttle_exceeded).get(labels: {source: "value3", quota: 'default', dummy: 'd1'})
|
84
|
+
end
|
85
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../test", __dir__)
|
2
|
+
require 'fluent/test'
|
3
|
+
require 'fluent/test/driver/filter'
|
4
|
+
require 'fluent/test/helpers'
|
5
|
+
require 'yaml'
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require_relative '../lib/fluent/plugin/config_parser'
|
8
|
+
require_relative '../lib/fluent/plugin/matcher'
|
9
|
+
require_relative '../lib/fluent/plugin/rate_limiter'
|
10
|
+
require_relative '../lib/fluent/plugin/filter_quota_throttle'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../helper'
|
3
|
+
|
4
|
+
class ParserTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
config_file_path = Dir.pwd+"/test/config_files/parser_test.yml"
|
7
|
+
config_parser = ConfigParser::Configuration.new(config_file_path)
|
8
|
+
@quotas = config_parser.quotas
|
9
|
+
@default_quota = config_parser.default_quota
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_parse_quotas
|
13
|
+
# Check if the quotas are parsed correctly
|
14
|
+
# TODO 1: Add wrong configuration files and check if it raises errors
|
15
|
+
refute_nil @quotas
|
16
|
+
assert_equal 2, @quotas.length
|
17
|
+
|
18
|
+
# Check if the quotas are parsed correctly
|
19
|
+
assert_equal "quota1", @quotas[0].name
|
20
|
+
assert_equal "quota2", @quotas[1].name
|
21
|
+
assert_equal "first quota", @quotas[0].desc
|
22
|
+
assert_equal "second quota", @quotas[1].desc
|
23
|
+
assert_equal [["group1", "a"]], @quotas[0].group_by
|
24
|
+
assert_equal [["group1", "a"], ["group1", "b"]], @quotas[1].group_by
|
25
|
+
assert_equal 100, @quotas[0].bucket_size
|
26
|
+
assert_equal 200, @quotas[1].bucket_size
|
27
|
+
assert_equal 60, @quotas[0].duration
|
28
|
+
assert_equal 120, @quotas[1].duration
|
29
|
+
assert_equal "drop", @quotas[0].action
|
30
|
+
assert_equal "reemit", @quotas[1].action
|
31
|
+
assert_equal ({["group1", "a"] => "value1"}), @quotas[0].match_by
|
32
|
+
assert_equal ({["group1", "a"] => "value2", ["group1", "b"] => "value3"}), @quotas[1].match_by
|
33
|
+
assert_equal "default", @default_quota.name
|
34
|
+
assert_equal "default quota", @default_quota.desc
|
35
|
+
assert_equal [["group1", "a"]], @default_quota.group_by
|
36
|
+
assert_equal [], @default_quota.match_by
|
37
|
+
assert_equal 300, @default_quota.bucket_size
|
38
|
+
assert_equal 180, @default_quota.duration
|
39
|
+
assert_equal "reemit", @default_quota.action
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
class MatcherTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
config_file_path = Dir.pwd+"/test/config_files/matcher_test.yml"
|
6
|
+
config_parser = ConfigParser::Configuration.new(config_file_path)
|
7
|
+
quotas = config_parser.quotas
|
8
|
+
@default_quota = config_parser.default_quota
|
9
|
+
@match_helper = Matcher::MatchHelper.new(quotas,@default_quota)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_get_quota
|
13
|
+
# Check if the correct quota is retrieved from above definition
|
14
|
+
# UT 1: Subset of groups match fully
|
15
|
+
keys = { "group1" => { "a" => "value1" , "b" => "value2" } }
|
16
|
+
quota = @match_helper.get_quota(keys)
|
17
|
+
assert_equal "quota1", quota.name
|
18
|
+
|
19
|
+
# UT 2: All groups match fully
|
20
|
+
keys = { "group1" => {"a" => "value2" , "b" => "value3" } }
|
21
|
+
quota = @match_helper.get_quota(keys)
|
22
|
+
assert_equal "quota2", quota.name
|
23
|
+
|
24
|
+
# UT 3: Subset of group match partially
|
25
|
+
keys = { "group1" => { "a" => "value2" , "b" => "value2" } }
|
26
|
+
quota = @match_helper.get_quota(keys)
|
27
|
+
assert_equal @default_quota, quota
|
28
|
+
|
29
|
+
# UT 4: None of the group matches
|
30
|
+
keys = { "group1" => { "a" => "value3" , "b" => "value2" } }
|
31
|
+
quota = @match_helper.get_quota(keys)
|
32
|
+
assert_equal @default_quota, quota
|
33
|
+
|
34
|
+
# UT 5: Non-nested group matches
|
35
|
+
keys = { "group2" => "value2" , "group3" => "value3" }
|
36
|
+
quota = @match_helper.get_quota(keys)
|
37
|
+
assert_equal "quota3", quota.name
|
38
|
+
|
39
|
+
# UT 6: Non-nested group mismatches
|
40
|
+
keys = { "group2" => "value2" , "group3" => "value4" }
|
41
|
+
quota = @match_helper.get_quota(keys)
|
42
|
+
assert_equal @default_quota, quota
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_matching_score
|
46
|
+
# Testing the private helper function
|
47
|
+
score = @match_helper.send(:matching_score, { "key1" => "value1" , "key2" => "value2" }, { "key1" => "value1" , "key2" => "value2" })
|
48
|
+
assert_equal 2, score
|
49
|
+
|
50
|
+
score = @match_helper.send(:matching_score, { "key1" => "value1" }, { "key2" => "value2" })
|
51
|
+
assert_equal 0, score
|
52
|
+
|
53
|
+
score = @match_helper.send(:matching_score, { "key1" => "value1" , "key2" => "value2" }, { "key1" => "value1" , "key2" => "value3" })
|
54
|
+
assert_equal 0, score
|
55
|
+
|
56
|
+
score = @match_helper.send(:matching_score, nil, { "key1" => "value1" })
|
57
|
+
assert_equal 0, score
|
58
|
+
|
59
|
+
score = @match_helper.send(:matching_score, { "key1" => "value1" }, nil)
|
60
|
+
assert_equal 0, score
|
61
|
+
|
62
|
+
score = @match_helper.send(:matching_score, nil, nil)
|
63
|
+
assert_equal 0, score
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
class TestBucket < Minitest::Test
|
4
|
+
def setup
|
5
|
+
# Initialize bucket with dummy parameters
|
6
|
+
@bucket = RateLimiter::Bucket.new( ["group"] ,10, 2)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_bucket_initialization
|
10
|
+
assert_equal 0, @bucket.bucket_count
|
11
|
+
assert @bucket.bucket_last_reset <= Time.now
|
12
|
+
assert_equal 0, @bucket.approx_rate_per_second
|
13
|
+
assert @bucket.rate_last_reset <= Time.now
|
14
|
+
assert_equal 0, @bucket.curr_count
|
15
|
+
assert_nil @bucket.last_warning
|
16
|
+
assert_equal 10, @bucket.instance_variable_get(:@bucket_limit)
|
17
|
+
assert_equal 2, @bucket.instance_variable_get(:@bucket_period)
|
18
|
+
assert_equal 5, @bucket.instance_variable_get(:@rate_limit)
|
19
|
+
assert_equal 4, @bucket.timeout_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_bucket_allow_free
|
23
|
+
@bucket.allow
|
24
|
+
assert_equal 1, @bucket.bucket_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_bucket_allow_full
|
28
|
+
11.times { @bucket.allow }
|
29
|
+
assert_equal false, @bucket.allow
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_reset_bucket
|
33
|
+
@bucket.allow
|
34
|
+
@bucket.send(:reset_bucket)
|
35
|
+
assert_equal 0, @bucket.bucket_count
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_expired
|
39
|
+
@bucket.allow
|
40
|
+
assert_equal false, @bucket.expired
|
41
|
+
sleep(5)
|
42
|
+
assert_equal true, @bucket.expired
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class TestBucketStore < Minitest::Test
|
47
|
+
def setup
|
48
|
+
@bucket_store = RateLimiter::BucketStore.new
|
49
|
+
@quota = ConfigParser::Quota.new("Dummy", "dummy quota for testing",[["group1","a"],["group1","b"]], {["group1","a"] => "value1"}, 10, "2s", "reemit")
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_get_bucket
|
53
|
+
group = "value1"
|
54
|
+
bucket = @bucket_store.get_bucket(group, @quota)
|
55
|
+
assert_instance_of RateLimiter::Bucket, bucket
|
56
|
+
assert_equal 10, bucket.instance_variable_get(:@bucket_limit)
|
57
|
+
assert_equal 2, bucket.instance_variable_get(:@bucket_period)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_clean_buckets
|
61
|
+
group1 = "value1"
|
62
|
+
@bucket_store.get_bucket(group1, @quota)
|
63
|
+
group2 = "value2"
|
64
|
+
@bucket_store.get_bucket(group2, @quota)
|
65
|
+
lru_group, lru_counter = @bucket_store.instance_variable_get(:@buckets).first
|
66
|
+
assert_equal group1, lru_group
|
67
|
+
sleep(5)
|
68
|
+
@bucket_store.clean_buckets
|
69
|
+
lru_group, lru_counter = @bucket_store.instance_variable_get(:@buckets).first
|
70
|
+
assert_equal group2, lru_group
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<source>
|
2
|
+
@type forward
|
3
|
+
port 24224
|
4
|
+
</source>
|
5
|
+
|
6
|
+
<source>
|
7
|
+
@type prometheus
|
8
|
+
port 24231
|
9
|
+
</source>
|
10
|
+
|
11
|
+
<filter test.**>
|
12
|
+
@log_level debug
|
13
|
+
@type quota_throttle
|
14
|
+
path example.yml
|
15
|
+
warning_delay 30
|
16
|
+
enable_metrics true
|
17
|
+
<labels>
|
18
|
+
user_id $.user_id
|
19
|
+
</labels>
|
20
|
+
</filter>
|
21
|
+
|
22
|
+
<match **>
|
23
|
+
@type stdout
|
24
|
+
</match>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
default:
|
2
|
+
action: drop
|
3
|
+
bucket_size: 6
|
4
|
+
duration: 5s
|
5
|
+
group_by:
|
6
|
+
- "user_id"
|
7
|
+
|
8
|
+
quotas:
|
9
|
+
- name: "Quota1"
|
10
|
+
action: reemit
|
11
|
+
bucket_size: 8
|
12
|
+
duration: 5s
|
13
|
+
group_by:
|
14
|
+
- "user_id"
|
15
|
+
match_by:
|
16
|
+
user_id: "user1"
|
17
|
+
- name: "Quota2"
|
18
|
+
action: drop
|
19
|
+
bucket_size: 5
|
20
|
+
duration: 5s
|
21
|
+
group_by:
|
22
|
+
- "user_id"
|
23
|
+
match_by:
|
24
|
+
user_id: "user2"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
for i in {1..20}; do
|
2
|
+
echo "{\"group\": \"group1\", \"user_id\": \"user1\", \"message\": \"Test log $i\"}" | fluent-cat test.default
|
3
|
+
done
|
4
|
+
for i in {1..20}; do
|
5
|
+
echo "{\"group\": \"group1\", \"user_id\": \"user3\", \"message\": \"Test log $i\"}" | fluent-cat test.default
|
6
|
+
done
|
7
|
+
for i in {1..20}; do
|
8
|
+
echo "{\"group\": \"group1\", \"user_id\": \"user2\", \"message\": \"Test log $i\"}" | fluent-cat test.default
|
9
|
+
done
|
10
|
+
|