sentry-smart-sampler 0.1.0

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
+ SHA256:
3
+ metadata.gz: c153ce539a78bfd853358b91da59c219dd8aa09cc5da4b07aefd7fea0574f3b9
4
+ data.tar.gz: 3fb8b058f618e8738bf7aff076b02b1a90417c9081e6a087c63f0ef25a759730
5
+ SHA512:
6
+ metadata.gz: c085db3f77003cfaf788f7288c2040d730ae7d429c5f53a7c218c2561b024f38a6ec951c99357fa4ac2b09d0ebd2e8891ba017fc17fa7c1263a1288185e7dfc3
7
+ data.tar.gz: 7b3aa4862c7f7c9db9f0631239ff25ae3dfe39109af3ad4b97220f3441bf7876955fac7363b9cdafeb17f2525cd191ea98958f3e24b27a6195ee6f8cdba0598c
@@ -0,0 +1,13 @@
1
+ version: 2.1
2
+ jobs:
3
+ build:
4
+ docker:
5
+ - image: ruby:2.7.2
6
+ steps:
7
+ - checkout
8
+ - run:
9
+ name: Run the default task
10
+ command: |
11
+ gem install bundler -v 2.2.11
12
+ bundle install
13
+ bundle exec rake
@@ -0,0 +1,37 @@
1
+ name: CI
2
+ on: [pull_request]
3
+ jobs:
4
+ rubocop:
5
+ strategy:
6
+ fail-fast: true
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - uses: ruby/setup-ruby@v1
11
+ with:
12
+ ruby-version: 2.7
13
+ bundler-cache: true
14
+ - run: bundle exec rubocop
15
+ rspec:
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby: [ '2.7', '3.0', '3.1' ]
20
+ runs-on: ubuntu-latest
21
+ services:
22
+ redis:
23
+ image: redis
24
+ ports:
25
+ - 6379:6379
26
+ options: >-
27
+ --health-cmd "redis-cli ping"
28
+ --health-interval 10s
29
+ --health-timeout 5s
30
+ --health-retries 5
31
+ steps:
32
+ - uses: actions/checkout@v2
33
+ - uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: ${{ matrix.ruby }}
36
+ bundler-cache: true
37
+ - run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,115 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ require:
4
+ - rubocop-performance
5
+ - rubocop-rspec
6
+ - rubocop-rake
7
+
8
+ inherit_mode:
9
+ merge:
10
+ - Exclude
11
+
12
+ AllCops:
13
+ NewCops: enable
14
+ TargetRubyVersion: 2.7
15
+ Exclude:
16
+ - "db/**/*"
17
+ - "bin/**/*"
18
+ - "tmp/**/*"
19
+ - "log/**/*"
20
+ - "vendor/**/*"
21
+ - "spec/rails_helper.rb"
22
+
23
+ Layout/ArgumentAlignment:
24
+ EnforcedStyle: with_fixed_indentation
25
+
26
+ Layout/ArrayAlignment:
27
+ EnforcedStyle: with_fixed_indentation
28
+
29
+ Layout/EmptyLineBetweenDefs:
30
+ AllowAdjacentOneLineDefs: true
31
+
32
+ Layout/EndAlignment:
33
+ EnforcedStyleAlignWith: variable
34
+
35
+ Layout/FirstArgumentIndentation:
36
+ EnforcedStyle: consistent
37
+
38
+ Layout/FirstArrayElementIndentation:
39
+ EnforcedStyle: consistent
40
+
41
+ Layout/FirstHashElementIndentation:
42
+ EnforcedStyle: consistent
43
+
44
+ Layout/MultilineMethodCallIndentation:
45
+ EnforcedStyle: indented
46
+
47
+ Layout/MultilineOperationIndentation:
48
+ EnforcedStyle: indented
49
+
50
+ Layout/ParameterAlignment:
51
+ EnforcedStyle: with_fixed_indentation
52
+
53
+ Layout/SpaceBeforeBrackets:
54
+ Enabled: false
55
+
56
+ Lint/UnusedMethodArgument:
57
+ AllowUnusedKeywordArguments: true
58
+
59
+ Metrics/ParameterLists:
60
+ MaxOptionalParameters: 4
61
+
62
+ RSpec/ExpectChange:
63
+ EnforcedStyle: block
64
+
65
+ RSpec/LetSetup:
66
+ Enabled: false
67
+
68
+ RSpec/MultipleExpectations:
69
+ Max: 20
70
+
71
+ Style/Alias:
72
+ EnforcedStyle: prefer_alias_method
73
+
74
+ Style/ClassAndModuleChildren:
75
+ EnforcedStyle: nested
76
+
77
+ Style/CommandLiteral:
78
+ EnforcedStyle: percent_x
79
+
80
+ Style/RescueStandardError:
81
+ EnforcedStyle: implicit
82
+
83
+ Style/StringLiterals:
84
+ EnforcedStyle: double_quotes
85
+ ConsistentQuotesInMultiline: true
86
+
87
+ Style/StringLiteralsInInterpolation:
88
+ EnforcedStyle: double_quotes
89
+
90
+ Style/RaiseArgs:
91
+ EnforcedStyle: compact
92
+
93
+ Style/RegexpLiteral:
94
+ EnforcedStyle: percent_r
95
+
96
+ Style/Documentation:
97
+ Enabled: false
98
+
99
+ RSpec/NestedGroups:
100
+ Max: 10
101
+
102
+ RSpec/MultipleMemoizedHelpers:
103
+ Enabled: false
104
+
105
+ Lint/AmbiguousBlockAssociation:
106
+ Exclude:
107
+ - 'spec/**/*'
108
+
109
+ Naming/FileName:
110
+ Exclude:
111
+ - 'lib/sentry-smart-sampler.rb'
112
+
113
+ RSpec/ExampleLength:
114
+ Max: 7
115
+
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,3 @@
1
+ Metrics/MethodLength:
2
+ Exclude:
3
+ - 'lib/sentry_smart_sampler/sampler.rb'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-08-16
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in sentry-smart-sampler.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "redis"
11
+ gem "rspec", "~> 3.0"
12
+ gem "rubocop", require: false
13
+ gem "rubocop-performance"
14
+ gem "rubocop-rake"
15
+ gem "rubocop-rspec"
16
+ gem "timecop"
data/Gemfile.lock ADDED
@@ -0,0 +1,90 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sentry-smart-sampler (0.1.0)
5
+ activesupport (>= 5)
6
+ sentry-ruby (~> 5)
7
+ zeitwerk
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (7.0.3.1)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 1.6, < 2)
15
+ minitest (>= 5.1)
16
+ tzinfo (~> 2.0)
17
+ ast (2.4.2)
18
+ concurrent-ruby (1.1.10)
19
+ diff-lcs (1.5.0)
20
+ i18n (1.12.0)
21
+ concurrent-ruby (~> 1.0)
22
+ json (2.6.2)
23
+ minitest (5.16.2)
24
+ parallel (1.22.1)
25
+ parser (3.1.2.1)
26
+ ast (~> 2.4.1)
27
+ rainbow (3.1.1)
28
+ rake (13.0.6)
29
+ redis (4.7.1)
30
+ regexp_parser (2.5.0)
31
+ rexml (3.2.5)
32
+ rspec (3.11.0)
33
+ rspec-core (~> 3.11.0)
34
+ rspec-expectations (~> 3.11.0)
35
+ rspec-mocks (~> 3.11.0)
36
+ rspec-core (3.11.0)
37
+ rspec-support (~> 3.11.0)
38
+ rspec-expectations (3.11.0)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.11.0)
41
+ rspec-mocks (3.11.1)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.11.0)
44
+ rspec-support (3.11.0)
45
+ rubocop (1.35.0)
46
+ json (~> 2.3)
47
+ parallel (~> 1.10)
48
+ parser (>= 3.1.2.1)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 1.8, < 3.0)
51
+ rexml (>= 3.2.5, < 4.0)
52
+ rubocop-ast (>= 1.20.1, < 2.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 1.4.0, < 3.0)
55
+ rubocop-ast (1.21.0)
56
+ parser (>= 3.1.1.0)
57
+ rubocop-performance (1.14.3)
58
+ rubocop (>= 1.7.0, < 2.0)
59
+ rubocop-ast (>= 0.4.0)
60
+ rubocop-rake (0.6.0)
61
+ rubocop (~> 1.0)
62
+ rubocop-rspec (2.12.1)
63
+ rubocop (~> 1.31)
64
+ ruby-progressbar (1.11.0)
65
+ sentry-ruby (5.4.1)
66
+ concurrent-ruby (~> 1.0, >= 1.0.2)
67
+ timecop (0.9.5)
68
+ tzinfo (2.0.5)
69
+ concurrent-ruby (~> 1.0)
70
+ unicode-display_width (2.2.0)
71
+ zeitwerk (2.6.0)
72
+
73
+ PLATFORMS
74
+ x86_64-darwin-18
75
+ x86_64-darwin-21
76
+ x86_64-linux
77
+
78
+ DEPENDENCIES
79
+ rake (~> 13.0)
80
+ redis
81
+ rspec (~> 3.0)
82
+ rubocop
83
+ rubocop-performance
84
+ rubocop-rake
85
+ rubocop-rspec
86
+ sentry-smart-sampler!
87
+ timecop
88
+
89
+ BUNDLED WITH
90
+ 2.2.11
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Karol Galanciak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # SentrySmartSampler
2
+
3
+ Smart sampler for `sentry-ruby` with rate limiting/throttling and sampling specific errors.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'sentry-smart-sampler'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install sentry-smart-sampler
20
+
21
+ ## Usage
22
+
23
+ Inside Sentry initializer:
24
+
25
+ ``` rb
26
+ Rails.application.config.to_prepare do
27
+ SentrySmartSampler.configure do |config|
28
+ config.cache_storage = Rails.cache # ideally a Rails cache backed by Redis. But could be anything responding to the same interface
29
+ config.logger = Rails.logger
30
+ config.default_sample_rate = 0.5 # defaults to 1
31
+ config.declare_sampling_rate_per_error do
32
+ declare Faraday::ClientError, sample_rate: 0.1
33
+ declare ActiveRecord::RecordInvalid, sample_rate: 0.2
34
+ declare /message pattern as regexp/, sample_rate: 0.3
35
+ declare "message pattern as string", sample_rate: 0.4
36
+ end
37
+
38
+ config.default_throttling_errors_number_threshold = 100 # do not set it if you don't want errors to be throttled
39
+ config.default_throttling_time_unit = :minute # do not set it if you don't want errors to be throttled, other options: [:second, :minute, :hour, :day]
40
+ # this config means that at most 100 errors of the same type can be sent withing a minute
41
+
42
+ config.declare_throttling_per_error do
43
+ declare ActiveRecord::StatementInvalid, time_unit: :hour, threshold: 50
44
+ declare /message pattern as regexp/, time_unit: :hour, threshold: 100
45
+ declare "message pattern as string", time_unit: :hour, threshold: 200
46
+ end
47
+
48
+ config.after_throttling_threshold_reached = lambda do |event, hint|
49
+ # do something when the threshold is reached, e.g. send a Slack notification. This callback will be fired at most once, when the threshold is reached. Not required
50
+ # when not provided, the error will be logged using logger
51
+ end
52
+
53
+ # not-required and not recommended to set unless you really know what you are doing
54
+ # the default definition of of reaching threshold is when the number of errors reaches the provided threshold for a given error within a given time unit
55
+ # if you really want to customize it (because you want e.g. to have another notification delivered from `after_throttling_threshold_reached` when you reach 10x of the threshold
56
+ # you need create an object that responds to `reached?` method taking 3 arguments: rate_limit, throttling_registration, error
57
+ # check `SentrySmartSampler::ThrottlingThresholdReachedDefinition` for more details
58
+ config.throttling_threshold_reached_definition = SomeCustomClassImplementingThrottlingThresholdReachedDefinition.new
59
+ end
60
+ end
61
+
62
+ Sentry.init do |config|
63
+ config.dsn = ENV["SENTRY_DSN"]
64
+
65
+ config.before_send = lambda do |event, hint|
66
+ SentrySmartSampler.call(event, hint) # returns event or nil if the event should be dropped
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Development
72
+
73
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
74
+
75
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
76
+
77
+ ## Contributing
78
+
79
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sentry-smart-sampler.
80
+
81
+ ## License
82
+
83
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "sentry/smart/sampler"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Smart
5
+ module Sampler
6
+ module Version
7
+ end
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sampler/version"
4
+
5
+ # Your code goes here...
6
+ module Sentry
7
+ module Smart
8
+ module Sampler
9
+ class Error < StandardError
10
+ # Your code goes here...
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry_smart_sampler"
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class Configuration
5
+ attr_accessor :cache_storage, :logger, :default_throttling_errors_number_threshold
6
+ attr_reader :default_throttling_time_unit
7
+ attr_writer :default_sample_rate, :after_throttling_threshold_reached, :throttling_threshold_reached_definition
8
+
9
+ TIME_UNITS = %i[second minute hour day].freeze
10
+ private_constant :TIME_UNITS
11
+
12
+ def default_sample_rate
13
+ @default_sample_rate || 1
14
+ end
15
+
16
+ def after_throttling_threshold_reached
17
+ @after_throttling_threshold_reached || default_after_throttling_threshold_reached_callback
18
+ end
19
+
20
+ def default_throttling_time_unit=(time_unit)
21
+ time_unit = time_unit.to_sym
22
+ validate_time_unit(time_unit)
23
+ @default_throttling_time_unit = time_unit
24
+ end
25
+
26
+ def declare_sampling_rate_per_error(&block)
27
+ sampling_rate_per_error_registry.instance_exec(&block)
28
+ end
29
+
30
+ def sampling_rate_per_error_registry
31
+ @sampling_rate_per_error_registry ||= SampleRatePerErrorRegistry.new(default_sample_rate)
32
+ end
33
+
34
+ def declare_throttling_per_error(&block)
35
+ throttling_per_error_registry.instance_exec(&block)
36
+ end
37
+
38
+ def throttling_per_error_registry
39
+ @throttling_per_error_registry ||= ThrottlingPerErrorRegistry.new(default_throttling_errors_number_threshold,
40
+ default_throttling_time_unit)
41
+ end
42
+
43
+ def throttling_threshold_reached_definition
44
+ @throttling_threshold_reached_definition || ThrottlingThresholdReachedDefinition.new
45
+ end
46
+
47
+ private
48
+
49
+ def validate_time_unit(time_unit)
50
+ TIME_UNITS.include?(time_unit) or raise_invalid_time_unit_error(time_unit)
51
+ end
52
+
53
+ def raise_invalid_time_unit_error(time_unit)
54
+ raise(ArgumentError.new("Invalid time unit: :#{time_unit}, allowed_values: #{TIME_UNITS}"))
55
+ end
56
+
57
+ def default_after_throttling_threshold_reached_callback
58
+ lambda do |_event, hint|
59
+ error = hint[:exception]
60
+ logger.info "[SentrySmartSampler] Throttling threshold reached for #{error.class}: #{error.message}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class RateLimit
5
+ KEY_PREFIX = "sentry_smart_sampler"
6
+ private_constant :KEY_PREFIX
7
+
8
+ attr_reader :key, :threshold, :interval, :increment, :cache
9
+ private :key, :threshold, :interval, :increment, :cache
10
+
11
+ def initialize(key:, threshold:, interval:, increment: 1, cache: SentrySmartSampler.configuration.cache_storage)
12
+ @key = key
13
+ @threshold = threshold
14
+ @interval = interval
15
+ @increment = increment
16
+ @cache = cache
17
+ end
18
+
19
+ def throttled?
20
+ count >= threshold
21
+ end
22
+
23
+ def count
24
+ cache.read(storage_key, raw: true).to_i
25
+ end
26
+
27
+ def remaining
28
+ threshold - count
29
+ end
30
+
31
+ def clear!
32
+ cache.delete(storage_key)
33
+ end
34
+
35
+ def increase
36
+ cache.increment(storage_key, increment) || increment
37
+ end
38
+
39
+ def storage_key
40
+ "#{KEY_PREFIX}/#{normalized_key}"
41
+ end
42
+
43
+ # let's say that the interval is 1.hour
44
+ # the current time is 15:05
45
+ # window is going to keep the same value until another interval time starts
46
+ # a new window is going to be start at 16:00
47
+ def window
48
+ Time.current.to_i / interval
49
+ end
50
+
51
+ private
52
+
53
+ def normalized_key
54
+ Digest::MD5.hexdigest([key, window].flatten.join("/"))
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class SampleRatePerErrorRegistry
5
+ attr_reader :default_sample_rate, :registry
6
+ private :default_sample_rate, :registry
7
+
8
+ def initialize(default_sample_rate)
9
+ @default_sample_rate = default_sample_rate
10
+ @registry = []
11
+ end
12
+
13
+ def declare(samplable, sample_rate:)
14
+ registry << Registration.new(samplable: samplable, sample_rate: sample_rate)
15
+ end
16
+
17
+ def sample_rate_registration_for(error)
18
+ registry.find(-> { default_registration }) { |registration| registration.matches?(error) }
19
+ end
20
+
21
+ private
22
+
23
+ def default_registration
24
+ Registration.new(samplable: nil, sample_rate: default_sample_rate)
25
+ end
26
+
27
+ class Registration
28
+ attr_reader :samplable, :sample_rate
29
+
30
+ def initialize(samplable:, sample_rate:)
31
+ @samplable = samplable
32
+ @sample_rate = sample_rate
33
+ end
34
+
35
+ def matches?(matchable_error)
36
+ if samplable.is_a?(Regexp) || samplable.respond_to?(:to_str)
37
+ matchable_error.message.scan(samplable).any?
38
+ else
39
+ matchable_error.is_a?(samplable)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class Sampler
5
+ attr_reader :registry, :random_generator, :cache_storage, :after_throttling_threshold_reached,
6
+ :throttling_threshold_reached_definition
7
+ private :registry, :random_generator, :cache_storage, :after_throttling_threshold_reached,
8
+ :throttling_threshold_reached_definition
9
+
10
+ def initialize(registry, random_generator: Random, configuration: SentrySmartSampler.configuration)
11
+ @registry = registry
12
+ @random_generator = random_generator
13
+ @cache_storage = configuration.cache_storage
14
+ @after_throttling_threshold_reached = configuration.after_throttling_threshold_reached
15
+ @throttling_threshold_reached_definition = configuration.throttling_threshold_reached_definition
16
+ end
17
+
18
+ def call(event, hint)
19
+ error = hint[:exception]
20
+ throttling_registration = registry.throttling_registration_for(error)
21
+
22
+ if apply_throttling?(throttling_registration)
23
+ rate_limit = initialize_rate_limit(throttling_registration, error)
24
+ already_throttled = rate_limit.throttled?
25
+ rate_limit.increase
26
+ after_throttling_threshold_reached.call(event, hint) if throttling_threshold_reached_definition.reached?(
27
+ rate_limit, throttling_registration, error
28
+ )
29
+ return if already_throttled
30
+ end
31
+
32
+ sample(event, error)
33
+ end
34
+
35
+ private
36
+
37
+ def apply_throttling?(throttling_registration)
38
+ throttling_registration.threshold && throttling_registration.time_unit && cache_storage
39
+ end
40
+
41
+ def initialize_rate_limit(throttling_registration, error)
42
+ SentrySmartSampler::RateLimit.new(key: throttling_registration.throttable || error.class,
43
+ threshold: throttling_registration.threshold,
44
+ interval: 1.public_send(throttling_registration.time_unit),
45
+ cache: cache_storage)
46
+ end
47
+
48
+ def threshold_reached?(rate_limit, throttling_registration, error); end
49
+
50
+ def sample(event, error)
51
+ event if random_generator.rand <= registry.sample_rate_registration_for(error).sample_rate
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class ThrottlingPerErrorRegistry
5
+ attr_reader :default_throttling_errors_number_threshold, :default_throttling_time_unit, :registry
6
+ private :default_throttling_errors_number_threshold, :default_throttling_time_unit, :registry
7
+
8
+ def initialize(default_throttling_errors_number_threshold, default_throttling_time_unit)
9
+ @default_throttling_errors_number_threshold = default_throttling_errors_number_threshold
10
+ @default_throttling_time_unit = default_throttling_time_unit
11
+ @registry = []
12
+ end
13
+
14
+ def declare(throttable, time_unit:, threshold:)
15
+ registry << Registration.new(throttable: throttable, time_unit: time_unit, threshold: threshold)
16
+ end
17
+
18
+ def throttling_registration_for(error)
19
+ registry.find(-> { default_registration }) { |registration| registration.matches?(error) }
20
+ end
21
+
22
+ private
23
+
24
+ def default_registration
25
+ Registration.new(throttable: nil, time_unit: default_throttling_time_unit,
26
+ threshold: default_throttling_errors_number_threshold)
27
+ end
28
+
29
+ class Registration
30
+ attr_reader :throttable, :threshold, :time_unit
31
+
32
+ def initialize(throttable:, threshold:, time_unit:)
33
+ @throttable = throttable
34
+ @threshold = threshold
35
+ @time_unit = time_unit
36
+ end
37
+
38
+ def matches?(matchable_error)
39
+ if throttable.is_a?(Regexp) || throttable.respond_to?(:to_str)
40
+ matchable_error.message.scan(throttable).any?
41
+ else
42
+ matchable_error.is_a?(throttable)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SentrySmartSampler
4
+ class ThrottlingThresholdReachedDefinition
5
+ def reached?(rate_limit, throttling_registration, _error)
6
+ rate_limit.throttled? && rate_limit.count == throttling_registration.threshold
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+ require "zeitwerk"
6
+
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.ignore("#{__dir__}/sentry-smart-sampler.rb")
9
+ loader.setup
10
+
11
+ class SentrySmartSampler
12
+ def self.configuration
13
+ @configuration ||= SentrySmartSampler::Configuration.new
14
+ end
15
+
16
+ def self.configure
17
+ yield configuration
18
+ end
19
+
20
+ def self.sample_rate_registration_for(error)
21
+ configuration.sampling_rate_per_error_registry.sample_rate_registration_for(error)
22
+ end
23
+
24
+ def self.throttling_registration_for(error)
25
+ configuration.throttling_per_error_registry.throttling_registration_for(error)
26
+ end
27
+
28
+ def self.call(event, hint)
29
+ SentrySmartSampler::Sampler.new(self).call(event, hint)
30
+ end
31
+
32
+ def self.reset!
33
+ @configuration = nil
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sentry/smart/sampler/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sentry-smart-sampler"
7
+ spec.version = Sentry::Smart::Sampler::VERSION
8
+ spec.authors = ["Karol Galanciak"]
9
+ spec.email = ["karol.galanciak@gmail.com"]
10
+
11
+ spec.summary = "Smart sampler for sentry-ruby with rate limiting/throttling and sampling specific errors"
12
+ spec.description = "Smart sampler for sentry-ruby with rate limiting/throttling and sampling specific errors"
13
+ spec.homepage = "https://github.com/BookingSync/sentry-smart-sampler"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/BookingSync/sentry-smart-sampler"
21
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+
35
+ spec.add_dependency "activesupport", ">= 5"
36
+ spec.add_dependency "sentry-ruby", "~> 5"
37
+ spec.add_dependency "zeitwerk"
38
+
39
+ # For more information and examples about making a new gem, checkout our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
41
+ spec.metadata["rubygems_mfa_required"] = "true"
42
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sentry-smart-sampler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Karol Galanciak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-08-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sentry-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Smart sampler for sentry-ruby with rate limiting/throttling and sampling
56
+ specific errors
57
+ email:
58
+ - karol.galanciak@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".circleci/config.yml"
64
+ - ".github/workflows/ci.yml"
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - ".rubocop.yml"
68
+ - ".rubocop_todo.yml"
69
+ - ".ruby-version"
70
+ - CHANGELOG.md
71
+ - Gemfile
72
+ - Gemfile.lock
73
+ - LICENSE.txt
74
+ - README.md
75
+ - Rakefile
76
+ - bin/console
77
+ - bin/setup
78
+ - lib/sentry-smart-sampler.rb
79
+ - lib/sentry/smart/sampler.rb
80
+ - lib/sentry/smart/sampler/version.rb
81
+ - lib/sentry_smart_sampler.rb
82
+ - lib/sentry_smart_sampler/configuration.rb
83
+ - lib/sentry_smart_sampler/rate_limit.rb
84
+ - lib/sentry_smart_sampler/sample_rate_per_error_registry.rb
85
+ - lib/sentry_smart_sampler/sampler.rb
86
+ - lib/sentry_smart_sampler/throttling_per_error_registry.rb
87
+ - lib/sentry_smart_sampler/throttling_threshold_reached_definition.rb
88
+ - sentry-smart-sampler.gemspec
89
+ homepage: https://github.com/BookingSync/sentry-smart-sampler
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ homepage_uri: https://github.com/BookingSync/sentry-smart-sampler
94
+ source_code_uri: https://github.com/BookingSync/sentry-smart-sampler
95
+ rubygems_mfa_required: 'true'
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 2.7.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.3.7
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Smart sampler for sentry-ruby with rate limiting/throttling and sampling
115
+ specific errors
116
+ test_files: []