sentry-smart-sampler 0.1.0

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