fluent-plugin-quota-throttle 0.0.2
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/.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
|
+
|