bugsnag_performance 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE.txt +21 -0
- data/bugsnag-performance.gemspec +39 -0
- data/lib/bugsnag_performance/configuration.rb +80 -0
- data/lib/bugsnag_performance/error.rb +11 -0
- data/lib/bugsnag_performance/internal/configuration_validator.rb +135 -0
- data/lib/bugsnag_performance/internal/delivery.rb +81 -0
- data/lib/bugsnag_performance/internal/logger_wrapper.rb +29 -0
- data/lib/bugsnag_performance/internal/nil_errors_configuration.rb +15 -0
- data/lib/bugsnag_performance/internal/payload_encoder.rb +141 -0
- data/lib/bugsnag_performance/internal/probability_fetcher.rb +66 -0
- data/lib/bugsnag_performance/internal/probability_manager.rb +35 -0
- data/lib/bugsnag_performance/internal/sampler.rb +66 -0
- data/lib/bugsnag_performance/internal/sampling_header_encoder.rb +25 -0
- data/lib/bugsnag_performance/internal/span_exporter.rb +76 -0
- data/lib/bugsnag_performance/internal/task.rb +72 -0
- data/lib/bugsnag_performance/internal/task_scheduler.rb +13 -0
- data/lib/bugsnag_performance/version.rb +5 -0
- data/lib/bugsnag_performance.rb +111 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 31cfcfc45215dec768c1e6040c418f63355ea308cf2d8d660ccad2c3f2de3621
|
4
|
+
data.tar.gz: e3868f8cd3341f285e0bd868c3db54e640739ed0a755308162c3dac96cf47b76
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f394a3a656e3bbd76aa825db67b2e6bd156590d666648ca6484df820fc833f8904e24dce311084bcfc64ff4e064f5bbcd181a92e51a60d27c7ccedf7a029ccd4
|
7
|
+
data.tar.gz: 9f8ad547b130279f751169c2305ce57faaf34a566782abca3df7fc307efbab8b14873c6ef0946069a0f49f159d40d8b225969eaf5defa5bb236b402e10afd05d
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 BugSnag
|
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.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/bugsnag_performance/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "bugsnag_performance"
|
7
|
+
spec.version = BugsnagPerformance::VERSION
|
8
|
+
spec.authors = ["BugSnag"]
|
9
|
+
spec.email = ["notifiers@bugsnag.com"]
|
10
|
+
|
11
|
+
spec.summary = "BugSnag integration for the Ruby Open Telemetry SDK"
|
12
|
+
spec.homepage = "https://www.bugsnag.com"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 3.0"
|
15
|
+
spec.require_paths = ["lib"]
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
|
19
|
+
github_url = "https://github.com/bugsnag/bugsnag-ruby-performance"
|
20
|
+
|
21
|
+
spec.metadata["source_code_uri"] = "#{github_url}"
|
22
|
+
spec.metadata["bug_tracker_uri"] = "#{github_url}/issues"
|
23
|
+
spec.metadata["changelog_uri"] = "#{github_url}/blob/v#{BugsnagPerformance::VERSION}/CHANGELOG.md"
|
24
|
+
spec.metadata["documentation_uri"] = "https://docs.bugsnag.com/performance/integration-guides/ruby/"
|
25
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
26
|
+
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
28
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
29
|
+
(File.expand_path(f) == __FILE__) ||
|
30
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github .rspec Gemfile])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
spec.add_dependency "concurrent-ruby", "~> 1.3"
|
35
|
+
spec.add_dependency "opentelemetry-sdk", "~> 1.2"
|
36
|
+
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "webmock", "~> 3.23"
|
39
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
class Configuration
|
5
|
+
attr_reader :open_telemetry_configure_block
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :app_version
|
10
|
+
attr_accessor :release_stage
|
11
|
+
attr_accessor :enabled_release_stages
|
12
|
+
attr_accessor :use_managed_quota
|
13
|
+
|
14
|
+
attr_writer :endpoint
|
15
|
+
|
16
|
+
def initialize(errors_configuration)
|
17
|
+
@open_telemetry_configure_block = proc { |c| }
|
18
|
+
self.logger = errors_configuration.logger || OpenTelemetry.logger
|
19
|
+
|
20
|
+
@api_key = fetch(errors_configuration, :api_key, env: "BUGSNAG_PERFORMANCE_API_KEY")
|
21
|
+
@app_version = fetch(errors_configuration, :app_version)
|
22
|
+
@release_stage = fetch(errors_configuration, :release_stage, env: "BUGSNAG_PERFORMANCE_RELEASE_STAGE", default: "production")
|
23
|
+
@use_managed_quota = true
|
24
|
+
|
25
|
+
@enabled_release_stages = fetch(errors_configuration, :enabled_release_stages, env: "BUGSNAG_PERFORMANCE_ENABLED_RELEASE_STAGES")
|
26
|
+
|
27
|
+
# transform enabled release stages into an array if we read its value from
|
28
|
+
# the environment
|
29
|
+
if @enabled_release_stages.is_a?(String)
|
30
|
+
@enabled_release_stages = @enabled_release_stages.split(",").map(&:strip)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger=(logger)
|
35
|
+
@logger =
|
36
|
+
if logger.is_a?(Internal::LoggerWrapper)
|
37
|
+
logger
|
38
|
+
else
|
39
|
+
Internal::LoggerWrapper.new(logger)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def endpoint
|
44
|
+
case
|
45
|
+
when defined?(@endpoint)
|
46
|
+
# if a custom endpoint has been set then use it directly
|
47
|
+
@endpoint
|
48
|
+
when @api_key.nil?
|
49
|
+
# if there's no API key then we can't construct the default URL
|
50
|
+
nil
|
51
|
+
else
|
52
|
+
"https://#{@api_key}.otlp.bugsnag.com/v1/traces"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure_open_telemetry(&open_telemetry_configure_block)
|
57
|
+
@open_telemetry_configure_block = open_telemetry_configure_block
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def fetch(
|
63
|
+
errors_configuration,
|
64
|
+
name,
|
65
|
+
env: nil,
|
66
|
+
default: nil
|
67
|
+
)
|
68
|
+
if env
|
69
|
+
value = ENV[env]
|
70
|
+
|
71
|
+
return value unless value.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
value = errors_configuration.send(name)
|
75
|
+
return value unless value.nil?
|
76
|
+
|
77
|
+
default
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class ConfigurationValidator
|
6
|
+
def self.validate(configuration)
|
7
|
+
new(configuration).validate
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate
|
11
|
+
raise MissingApiKeyError.new if @configuration.api_key.nil?
|
12
|
+
|
13
|
+
validate_open_telemetry_configure_block
|
14
|
+
validate_logger
|
15
|
+
validate_api_key
|
16
|
+
validate_string(:app_version, optional: true)
|
17
|
+
validate_string(:release_stage, optional: true)
|
18
|
+
validate_array(:enabled_release_stages, "non-empty strings", optional: true, &method(:valid_string?))
|
19
|
+
validate_boolean(:use_managed_quota, optional: false)
|
20
|
+
valid_endpoint = validate_endpoint
|
21
|
+
|
22
|
+
# if the endpoint is invalid then we shouldn't attempt to send traces
|
23
|
+
Result.new(@messages, @valid_configuration, send_traces: valid_endpoint)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def initialize(configuration)
|
29
|
+
@configuration = configuration
|
30
|
+
@valid_configuration = BugsnagPerformance::Configuration.new(BugsnagPerformance::Internal::NilErrorsConfiguration.new)
|
31
|
+
@messages = []
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_open_telemetry_configure_block
|
35
|
+
value = @configuration.open_telemetry_configure_block
|
36
|
+
|
37
|
+
if value.respond_to?(:call) && value.arity == 1
|
38
|
+
@valid_configuration.configure_open_telemetry(&value)
|
39
|
+
else
|
40
|
+
@messages << "configure_open_telemetry requires a callable with an arity of 1"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_logger
|
45
|
+
value = @configuration.logger
|
46
|
+
|
47
|
+
if value.is_a?(LoggerWrapper) && value.logger.is_a?(::Logger)
|
48
|
+
@valid_configuration.logger = value
|
49
|
+
else
|
50
|
+
@messages << "logger should be a ::Logger, got #{value.logger.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_api_key
|
55
|
+
value = @configuration.api_key
|
56
|
+
|
57
|
+
# we always use the provided API key even if it's invalid
|
58
|
+
@valid_configuration.api_key = value
|
59
|
+
|
60
|
+
return if value.is_a?(String) && value =~ /\A[0-9a-f]{32}\z/i
|
61
|
+
|
62
|
+
@messages << "api_key should be a 32 character hexadecimal string, got #{value.inspect}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_string(name, optional:)
|
66
|
+
value = @configuration.send(name)
|
67
|
+
|
68
|
+
if (value.nil? && optional) || valid_string?(value)
|
69
|
+
@valid_configuration.send("#{name}=", value)
|
70
|
+
else
|
71
|
+
@messages << "#{name} should be a non-empty string, got #{value.inspect}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_boolean(name, optional:)
|
76
|
+
value = @configuration.send(name)
|
77
|
+
|
78
|
+
if (value.nil? && optional) || value == true || value == false
|
79
|
+
@valid_configuration.send("#{name}=", value)
|
80
|
+
else
|
81
|
+
@messages << "#{name} should be a boolean, got #{value.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_array(name, description, optional:, &block)
|
86
|
+
value = @configuration.send(name)
|
87
|
+
|
88
|
+
if (value.nil? && optional) || value.is_a?(Array) && value.all?(&block)
|
89
|
+
@valid_configuration.send("#{name}=", value)
|
90
|
+
else
|
91
|
+
@messages << "#{name} should be an array of #{description}, got #{value.inspect}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_endpoint
|
96
|
+
value = @configuration.endpoint
|
97
|
+
|
98
|
+
# we always use the provided endpoint even if it's invalid to prevent
|
99
|
+
# leaking data to the saas bugsnag instance
|
100
|
+
@valid_configuration.endpoint = value
|
101
|
+
|
102
|
+
if valid_string?(value)
|
103
|
+
true
|
104
|
+
else
|
105
|
+
@messages << "endpoint should be a valid URL, got #{value.inspect}"
|
106
|
+
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid_string?(value)
|
112
|
+
value.is_a?(String) && !value.empty?
|
113
|
+
end
|
114
|
+
|
115
|
+
class Result
|
116
|
+
attr_reader :messages
|
117
|
+
attr_reader :configuration
|
118
|
+
|
119
|
+
def initialize(messages, configuration, send_traces:)
|
120
|
+
@messages = messages
|
121
|
+
@configuration = configuration.freeze
|
122
|
+
@send_traces = send_traces
|
123
|
+
end
|
124
|
+
|
125
|
+
def valid?
|
126
|
+
@messages.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
def send_traces?
|
130
|
+
@send_traces
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class Delivery
|
6
|
+
def initialize(configuration)
|
7
|
+
@uri = URI(configuration.endpoint)
|
8
|
+
@common_headers = {
|
9
|
+
"Bugsnag-Api-Key" => configuration.api_key,
|
10
|
+
"Content-Type" => "application/json",
|
11
|
+
}.freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def deliver(headers, body)
|
15
|
+
headers = headers.merge(
|
16
|
+
@common_headers,
|
17
|
+
{ "Bugsnag-Sent-At" => Time.now.utc.iso8601(3) },
|
18
|
+
)
|
19
|
+
|
20
|
+
raw_response = OpenTelemetry::Common::Utilities.untraced do
|
21
|
+
Net::HTTP.post(@uri, body, headers)
|
22
|
+
end
|
23
|
+
|
24
|
+
Response.new(raw_response)
|
25
|
+
end
|
26
|
+
|
27
|
+
class Response
|
28
|
+
SAMPLING_PROBABILITY_HEADER = "Bugsnag-Sampling-Probability"
|
29
|
+
RETRYABLE_STATUS_CODES = Set[402, 407, 408, 429]
|
30
|
+
|
31
|
+
private_constant :SAMPLING_PROBABILITY_HEADER, :RETRYABLE_STATUS_CODES
|
32
|
+
|
33
|
+
attr_reader :state
|
34
|
+
attr_reader :sampling_probability
|
35
|
+
|
36
|
+
def initialize(raw_response)
|
37
|
+
if raw_response.nil?
|
38
|
+
@state = :failure_retryable
|
39
|
+
@sampling_probability = nil
|
40
|
+
else
|
41
|
+
@state = response_state_from_status_code(raw_response.code)
|
42
|
+
@sampling_probability = parse_sampling_probability(raw_response[SAMPLING_PROBABILITY_HEADER])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def successful?
|
47
|
+
@state == :success
|
48
|
+
end
|
49
|
+
|
50
|
+
def retryable?
|
51
|
+
@state == :failure_retryable
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def response_state_from_status_code(raw_status_code)
|
57
|
+
case Integer(raw_status_code, exception: false)
|
58
|
+
when 200...300
|
59
|
+
:success
|
60
|
+
when RETRYABLE_STATUS_CODES
|
61
|
+
:failure_retryable
|
62
|
+
when 400...500
|
63
|
+
:failure_discard
|
64
|
+
else
|
65
|
+
:failure_retryable
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_sampling_probability(raw_probability)
|
70
|
+
parsed = Float(raw_probability, exception: false)
|
71
|
+
|
72
|
+
if parsed && parsed >= 0.0 && parsed <= 1.0
|
73
|
+
parsed
|
74
|
+
else
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class LoggerWrapper
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
def initialize(logger)
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def debug(message)
|
13
|
+
@logger.debug("[BugsnagPerformance] #{message}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def info(message)
|
17
|
+
@logger.info("[BugsnagPerformance] #{message}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(message)
|
21
|
+
@logger.warn("[BugsnagPerformance] #{message}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def error(message)
|
25
|
+
@logger.error("[BugsnagPerformance] #{message}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
# this class is used when the bugsnag-ruby (aka "bugsnag errors") gem isn't
|
6
|
+
# installed to provide the API we need in BugsnagPerformance::Configuration
|
7
|
+
class NilErrorsConfiguration
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :app_version
|
10
|
+
attr_accessor :release_stage
|
11
|
+
attr_accessor :enabled_release_stages
|
12
|
+
attr_accessor :logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class PayloadEncoder
|
6
|
+
# https://github.com/open-telemetry/opentelemetry-proto/blob/484241a016d7b81f20b6e19d00ffbc4a3b864a22/opentelemetry/proto/trace/v1/trace.proto#L150-L176
|
7
|
+
SPAN_KIND_UNSPECIFIED = 0
|
8
|
+
SPAN_KIND_INTERNAL = 1
|
9
|
+
SPAN_KIND_SERVER = 2
|
10
|
+
SPAN_KIND_CLIENT = 3
|
11
|
+
SPAN_KIND_PRODUCER = 4
|
12
|
+
SPAN_KIND_CONSUMER = 5
|
13
|
+
|
14
|
+
# https://github.com/open-telemetry/opentelemetry-proto/blob/484241a016d7b81f20b6e19d00ffbc4a3b864a22/opentelemetry/proto/trace/v1/trace.proto#L312-L320
|
15
|
+
SPAN_STATUS_OK = 0
|
16
|
+
SPAN_STATUS_UNSET = 1
|
17
|
+
SPAN_STATUS_ERROR = 2
|
18
|
+
|
19
|
+
private_constant :SPAN_KIND_UNSPECIFIED,
|
20
|
+
:SPAN_KIND_INTERNAL,
|
21
|
+
:SPAN_KIND_SERVER,
|
22
|
+
:SPAN_KIND_CLIENT,
|
23
|
+
:SPAN_KIND_PRODUCER,
|
24
|
+
:SPAN_KIND_CONSUMER,
|
25
|
+
:SPAN_STATUS_OK,
|
26
|
+
:SPAN_STATUS_UNSET,
|
27
|
+
:SPAN_STATUS_ERROR
|
28
|
+
|
29
|
+
def initialize(sampler)
|
30
|
+
@sampler = sampler
|
31
|
+
end
|
32
|
+
|
33
|
+
def encode(span_data)
|
34
|
+
{
|
35
|
+
resourceSpans: span_data
|
36
|
+
.filter { |span| @sampler.resample_span?(span) }
|
37
|
+
.group_by(&:resource)
|
38
|
+
.map do |resource, scope_spans|
|
39
|
+
{
|
40
|
+
resource: {
|
41
|
+
attributes: resource.attribute_enumerator.map(&method(:attribute_to_json)),
|
42
|
+
},
|
43
|
+
scopeSpans: scope_spans
|
44
|
+
.group_by(&:instrumentation_scope)
|
45
|
+
.map do |scope, spans|
|
46
|
+
{
|
47
|
+
scope: { name: scope.name, version: scope.version },
|
48
|
+
spans: spans.map(&method(:span_to_json)),
|
49
|
+
}
|
50
|
+
end,
|
51
|
+
}
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def span_to_json(span)
|
59
|
+
{
|
60
|
+
name: span.name,
|
61
|
+
kind: kind_to_json(span.kind),
|
62
|
+
spanId: span.hex_span_id,
|
63
|
+
traceId: span.hex_trace_id,
|
64
|
+
parentSpanId: span.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID ? nil : span.hex_parent_span_id,
|
65
|
+
startTimeUnixNano: span.start_timestamp.to_s,
|
66
|
+
endTimeUnixNano: span.end_timestamp.to_s,
|
67
|
+
traceState: span.tracestate.to_s,
|
68
|
+
droppedAttributesCount: calculate_dropped_count(span.total_recorded_attributes, span.attributes),
|
69
|
+
droppedEventsCount: calculate_dropped_count(span.total_recorded_events, span.events),
|
70
|
+
droppedLinksCount: calculate_dropped_count(span.total_recorded_links, span.links),
|
71
|
+
status: { code: span.status.code, message: span.status.description },
|
72
|
+
attributes: span.attributes&.map(&method(:attribute_to_json)),
|
73
|
+
events: span.events&.map do |event|
|
74
|
+
{
|
75
|
+
name: event.name,
|
76
|
+
timeUnixNano: event.timestamp.to_s,
|
77
|
+
attributes: event.attributes&.map(&method(:attribute_to_json))
|
78
|
+
# the OTel SDK doesn't provide dropped_attributes_count for events
|
79
|
+
}.tap(&:compact!)
|
80
|
+
end,
|
81
|
+
links: span.links&.map do |link|
|
82
|
+
context = link.span_context
|
83
|
+
|
84
|
+
{
|
85
|
+
traceId: context.hex_trace_id,
|
86
|
+
spanId: context.hex_span_id,
|
87
|
+
traceState: context.tracestate.to_s,
|
88
|
+
attributes: link.attributes&.map(&method(:attribute_to_json))
|
89
|
+
# the OTel SDK doesn't provide dropped_attributes_count for links
|
90
|
+
}.tap(&:compact!)
|
91
|
+
end,
|
92
|
+
}.tap(&:compact!)
|
93
|
+
end
|
94
|
+
|
95
|
+
def attribute_to_json(key, value)
|
96
|
+
{ key: key, value: attribute_value_to_json(value) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def attribute_value_to_json(value)
|
100
|
+
case value
|
101
|
+
when Integer
|
102
|
+
{ intValue: value.to_s }
|
103
|
+
when Float
|
104
|
+
{ doubleValue: value }
|
105
|
+
when true, false
|
106
|
+
{ boolValue: value }
|
107
|
+
when String
|
108
|
+
{ stringValue: value }
|
109
|
+
when Array
|
110
|
+
{ arrayValue: value.map(&method(:attribute_value_to_json)) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def kind_to_json(kind)
|
115
|
+
case kind
|
116
|
+
when :internal
|
117
|
+
SPAN_KIND_INTERNAL
|
118
|
+
when :server
|
119
|
+
SPAN_KIND_SERVER
|
120
|
+
when :client
|
121
|
+
SPAN_KIND_CLIENT
|
122
|
+
when :producer
|
123
|
+
SPAN_KIND_PRODUCER
|
124
|
+
when :consumer
|
125
|
+
SPAN_KIND_CONSUMER
|
126
|
+
else
|
127
|
+
SPAN_KIND_UNSPECIFIED
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def calculate_dropped_count(total_items, remaining_items)
|
132
|
+
return 0 if remaining_items.nil?
|
133
|
+
|
134
|
+
dropped_count = total_items - remaining_items.length
|
135
|
+
return 0 if dropped_count.negative?
|
136
|
+
|
137
|
+
dropped_count
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class ProbabilityFetcher
|
6
|
+
# the time to wait before retrying a failed request
|
7
|
+
RETRY_SECONDS = 30
|
8
|
+
STALE_PROBABILITY_SECONDS = 60 * 60 * 24 # 24 hours
|
9
|
+
HEADERS = { "Bugsnag-Span-Sampling" => "1.0:0" }
|
10
|
+
BODY = '{"resourceSpans": []}'
|
11
|
+
|
12
|
+
private_constant :RETRY_SECONDS, :STALE_PROBABILITY_SECONDS, :HEADERS, :BODY
|
13
|
+
|
14
|
+
def initialize(logger, delivery, task_scheduler)
|
15
|
+
@logger = logger
|
16
|
+
@delivery = delivery
|
17
|
+
@task_scheduler = task_scheduler
|
18
|
+
end
|
19
|
+
|
20
|
+
def stale_in(seconds)
|
21
|
+
@task.schedule(seconds)
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_new_probability(&on_new_probability_callback)
|
25
|
+
@task = @task_scheduler.now do |done|
|
26
|
+
get_new_probability do |new_probability|
|
27
|
+
on_new_probability_callback.call(new_probability)
|
28
|
+
|
29
|
+
done.call
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def get_new_probability(&block)
|
37
|
+
# keep making requests until we get a new probability from the server
|
38
|
+
loop do
|
39
|
+
begin
|
40
|
+
response = @delivery.deliver(HEADERS, BODY)
|
41
|
+
rescue => exception
|
42
|
+
# do nothing, we'll warn about this shortly...
|
43
|
+
end
|
44
|
+
|
45
|
+
# in theory this should always be present, but it's possible the request
|
46
|
+
# fails or there's a bug on the server side causing it not to be returned
|
47
|
+
if response && new_probability = response.sampling_probability
|
48
|
+
new_probability = Float(new_probability, exception: false)
|
49
|
+
|
50
|
+
if new_probability && new_probability >= 0.0 && new_probability <= 1.0
|
51
|
+
block.call(new_probability)
|
52
|
+
|
53
|
+
break
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@logger.warn("Failed to retrieve a probability value from BugSnag. Retrying in 30 seconds.")
|
58
|
+
@logger.warn(exception) if exception
|
59
|
+
|
60
|
+
# wait a bit before trying again
|
61
|
+
sleep(RETRY_SECONDS)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class ProbabilityManager
|
6
|
+
# the duration (in seconds) that a probability value is considered stale
|
7
|
+
# and therefore we need to fetch a new value
|
8
|
+
STALE_PROBABILITY_SECONDS = 60 * 60 * 24 # 24 hours
|
9
|
+
private_constant :STALE_PROBABILITY_SECONDS
|
10
|
+
|
11
|
+
def initialize(probability_fetcher)
|
12
|
+
@probability_fetcher = probability_fetcher
|
13
|
+
@probability = 1.0
|
14
|
+
@lock = Mutex.new
|
15
|
+
|
16
|
+
@probability_fetcher.on_new_probability do |new_probability|
|
17
|
+
self.probability = new_probability
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def probability
|
22
|
+
@lock.synchronize do
|
23
|
+
@probability
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def probability=(new_probability)
|
28
|
+
@lock.synchronize do
|
29
|
+
@probability = new_probability
|
30
|
+
@probability_fetcher.stale_in(STALE_PROBABILITY_SECONDS)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class Sampler
|
6
|
+
PROBABILITY_SCALE_FACTOR = 18_446_744_073_709_551_615 # (2 ** 64) - 1
|
7
|
+
|
8
|
+
private_constant :PROBABILITY_SCALE_FACTOR
|
9
|
+
|
10
|
+
def initialize(probability_manager)
|
11
|
+
@probability_manager = probability_manager
|
12
|
+
end
|
13
|
+
|
14
|
+
def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
|
15
|
+
# NOTE: the probability could change at any time so we _must_ only read
|
16
|
+
# it once in this method, otherwise we could use different values
|
17
|
+
# for the sampling decision & p value attribute which would result
|
18
|
+
# in inconsistent data
|
19
|
+
probability = @probability_manager.probability
|
20
|
+
|
21
|
+
decision =
|
22
|
+
if sample_using_probability_and_trace_id?(probability, trace_id)
|
23
|
+
OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE
|
24
|
+
else
|
25
|
+
OpenTelemetry::SDK::Trace::Samplers::Decision::DROP
|
26
|
+
end
|
27
|
+
|
28
|
+
parent_span_context = OpenTelemetry::Trace.current_span(parent_context).context
|
29
|
+
|
30
|
+
OpenTelemetry::SDK::Trace::Samplers::Result.new(
|
31
|
+
decision: decision,
|
32
|
+
tracestate: parent_span_context.tracestate,
|
33
|
+
attributes: { "bugsnag.sampling.p" => probability },
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api private
|
38
|
+
def resample_span?(span)
|
39
|
+
probability = @probability_manager.probability
|
40
|
+
|
41
|
+
# sample all spans that are missing the p value attribute
|
42
|
+
return true if span.attributes.nil? || span.attributes["bugsnag.sampling.p"].nil?
|
43
|
+
|
44
|
+
# update the p value attribute if it was originally sampled with a larger
|
45
|
+
# probability than the current value
|
46
|
+
if span.attributes["bugsnag.sampling.p"] > probability
|
47
|
+
span.attributes["bugsnag.sampling.p"] = probability
|
48
|
+
end
|
49
|
+
|
50
|
+
sample_using_probability_and_trace_id?(span.attributes["bugsnag.sampling.p"], span.trace_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def sample_using_probability_and_trace_id?(probability, trace_id)
|
56
|
+
# scale the probability (stored as a float from 0-1) to a u64
|
57
|
+
p_value = (probability * PROBABILITY_SCALE_FACTOR).floor
|
58
|
+
|
59
|
+
# unpack the trace ID as a u64
|
60
|
+
r_value = trace_id.unpack1("@8Q>")
|
61
|
+
|
62
|
+
p_value >= r_value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class SamplingHeaderEncoder
|
6
|
+
def encode(spans)
|
7
|
+
return "1.0:0" if spans.empty?
|
8
|
+
|
9
|
+
spans
|
10
|
+
.group_by do |span|
|
11
|
+
# bail if the atrribute is missing; we'll warn about this later as it
|
12
|
+
# means something has gone wrong
|
13
|
+
return nil if span.attributes.nil?
|
14
|
+
|
15
|
+
probability = span.attributes["bugsnag.sampling.p"]
|
16
|
+
return nil if probability.nil?
|
17
|
+
|
18
|
+
probability
|
19
|
+
end
|
20
|
+
.map { |probability, spans| "#{probability}:#{spans.length}" }
|
21
|
+
.join(";")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
class SpanExporter
|
6
|
+
def initialize(
|
7
|
+
logger,
|
8
|
+
probability_manager,
|
9
|
+
delivery,
|
10
|
+
payload_encoder,
|
11
|
+
sampling_header_encoder
|
12
|
+
)
|
13
|
+
@logger = logger
|
14
|
+
@probability_manager = probability_manager
|
15
|
+
@delivery = delivery
|
16
|
+
@payload_encoder = payload_encoder
|
17
|
+
@sampling_header_encoder = sampling_header_encoder
|
18
|
+
@disabled = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def disable!
|
22
|
+
@disabled = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def export(span_data, timeout: nil)
|
26
|
+
return OpenTelemetry::SDK::Trace::Export::SUCCESS if @disabled
|
27
|
+
|
28
|
+
with_timeout(timeout) do
|
29
|
+
headers = {}
|
30
|
+
sampling_header = @sampling_header_encoder.encode(span_data)
|
31
|
+
|
32
|
+
if sampling_header.nil?
|
33
|
+
@logger.warn("One or more spans are missing the 'bugsnag.sampling.p' attribute. This trace will be sent as 'unmanaged'.")
|
34
|
+
else
|
35
|
+
headers["Bugsnag-Span-Sampling"] = sampling_header
|
36
|
+
end
|
37
|
+
|
38
|
+
body = JSON.generate(@payload_encoder.encode(span_data))
|
39
|
+
|
40
|
+
response = @delivery.deliver(headers, body)
|
41
|
+
|
42
|
+
if response.sampling_probability
|
43
|
+
@probability_manager.probability = response.sampling_probability
|
44
|
+
end
|
45
|
+
|
46
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
47
|
+
end
|
48
|
+
rescue => exception
|
49
|
+
@logger.error("Failed to deliver trace to BugSnag.")
|
50
|
+
@logger.error(exception)
|
51
|
+
|
52
|
+
return OpenTelemetry::SDK::Trace::Export::TIMEOUT if exception.is_a?(Timeout::Error)
|
53
|
+
|
54
|
+
OpenTelemetry::SDK::Trace::Export::FAILURE
|
55
|
+
end
|
56
|
+
|
57
|
+
def force_flush(timeout: nil)
|
58
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
59
|
+
end
|
60
|
+
|
61
|
+
def shutdown(timeout: nil)
|
62
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def with_timeout(timeout, &block)
|
68
|
+
if timeout.nil?
|
69
|
+
block.call
|
70
|
+
else
|
71
|
+
Timeout::timeout(timeout) { block.call }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BugsnagPerformance
|
4
|
+
module Internal
|
5
|
+
# A wrapper around Concurrent::ScheduledTask that is easier to work with when
|
6
|
+
# a task needs to be run repeatedly (a ScheduledTask can only run once)
|
7
|
+
class Task
|
8
|
+
class UnscheduledTaskError < ::BugsnagPerformance::Error
|
9
|
+
def initialize
|
10
|
+
super("Task has not been scheduled")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(on_finish)
|
15
|
+
@on_finish = on_finish
|
16
|
+
@lock = Mutex.new
|
17
|
+
@scheduled_task = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def schedule(delay)
|
21
|
+
@lock.synchronize do
|
22
|
+
if @scheduled_task && @scheduled_task.pending?
|
23
|
+
# if we have a pending task we can reschedule it
|
24
|
+
@scheduled_task.reschedule(delay)
|
25
|
+
elsif @scheduled_task && @scheduled_task.processing?
|
26
|
+
# task is currently running so re-schedule when it finishes and remove
|
27
|
+
# any existing reschedules
|
28
|
+
@scheduled_task.delete_observers
|
29
|
+
|
30
|
+
@scheduled_task.add_observer(OnTaskFinish.new(proc do
|
31
|
+
self.schedule(delay)
|
32
|
+
end))
|
33
|
+
else
|
34
|
+
# otherwise make a new task; if there's an existing task and it isn't
|
35
|
+
# pending or processing then it must have already finished so we don't
|
36
|
+
# need to worry about cancelling it
|
37
|
+
@scheduled_task = Concurrent::ScheduledTask.execute(delay) { @on_finish.call }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def wait
|
45
|
+
raise UnscheduledTaskError.new if @scheduled_task.nil?
|
46
|
+
|
47
|
+
@scheduled_task.wait
|
48
|
+
end
|
49
|
+
|
50
|
+
def state
|
51
|
+
case @scheduled_task&.state
|
52
|
+
when nil then :unscheduled
|
53
|
+
when :pending then :pending
|
54
|
+
when :processing then :processing
|
55
|
+
else :finished
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
class OnTaskFinish
|
62
|
+
def initialize(on_finish)
|
63
|
+
@on_finish = on_finish
|
64
|
+
end
|
65
|
+
|
66
|
+
def update(...)
|
67
|
+
@on_finish.call
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "time"
|
5
|
+
require "timeout"
|
6
|
+
require "net/http"
|
7
|
+
require "concurrent-ruby"
|
8
|
+
require "opentelemetry-sdk"
|
9
|
+
|
10
|
+
require_relative "bugsnag_performance/error"
|
11
|
+
require_relative "bugsnag_performance/version"
|
12
|
+
require_relative "bugsnag_performance/configuration"
|
13
|
+
|
14
|
+
require_relative "bugsnag_performance/internal/task"
|
15
|
+
require_relative "bugsnag_performance/internal/sampler"
|
16
|
+
require_relative "bugsnag_performance/internal/delivery"
|
17
|
+
require_relative "bugsnag_performance/internal/span_exporter"
|
18
|
+
require_relative "bugsnag_performance/internal/logger_wrapper"
|
19
|
+
require_relative "bugsnag_performance/internal/task_scheduler"
|
20
|
+
require_relative "bugsnag_performance/internal/payload_encoder"
|
21
|
+
require_relative "bugsnag_performance/internal/probability_fetcher"
|
22
|
+
require_relative "bugsnag_performance/internal/probability_manager"
|
23
|
+
require_relative "bugsnag_performance/internal/configuration_validator"
|
24
|
+
require_relative "bugsnag_performance/internal/sampling_header_encoder"
|
25
|
+
require_relative "bugsnag_performance/internal/nil_errors_configuration"
|
26
|
+
|
27
|
+
module BugsnagPerformance
|
28
|
+
def self.configure(&block)
|
29
|
+
unvalidated_configuration = Configuration.new(load_bugsnag_errors_configuration)
|
30
|
+
|
31
|
+
block.call(unvalidated_configuration) unless block.nil?
|
32
|
+
|
33
|
+
result = Internal::ConfigurationValidator.validate(unvalidated_configuration)
|
34
|
+
configuration = result.configuration
|
35
|
+
|
36
|
+
log_validation_messages(configuration.logger, result.messages) unless result.valid?
|
37
|
+
|
38
|
+
delivery = Internal::Delivery.new(configuration)
|
39
|
+
task_scheduler = Internal::TaskScheduler.new
|
40
|
+
probability_fetcher = Internal::ProbabilityFetcher.new(configuration.logger, delivery, task_scheduler)
|
41
|
+
probability_manager = Internal::ProbabilityManager.new(probability_fetcher)
|
42
|
+
sampler = Internal::Sampler.new(probability_manager)
|
43
|
+
|
44
|
+
exporter = Internal::SpanExporter.new(
|
45
|
+
configuration.logger,
|
46
|
+
probability_manager,
|
47
|
+
delivery,
|
48
|
+
Internal::PayloadEncoder.new(sampler),
|
49
|
+
Internal::SamplingHeaderEncoder.new,
|
50
|
+
)
|
51
|
+
|
52
|
+
if configuration.enabled_release_stages && !configuration.enabled_release_stages.include?(configuration.release_stage)
|
53
|
+
configuration.logger.info("Not exporting spans as the current release stage is not in the enabled release stages.")
|
54
|
+
exporter.disable!
|
55
|
+
end
|
56
|
+
|
57
|
+
# return the result of the user's configuration block so we don't change
|
58
|
+
# any existing behaviour
|
59
|
+
return_value = nil
|
60
|
+
|
61
|
+
OpenTelemetry::SDK.configure do |otel_configurator|
|
62
|
+
# call the user's OTel configuration block
|
63
|
+
return_value = configuration.open_telemetry_configure_block.call(otel_configurator)
|
64
|
+
|
65
|
+
# add app version and release stage as the 'service.version' and
|
66
|
+
# 'deployment.environment' resource attributes
|
67
|
+
if app_version = configuration.app_version
|
68
|
+
otel_configurator.service_version = app_version
|
69
|
+
end
|
70
|
+
|
71
|
+
otel_configurator.resource = OpenTelemetry::SDK::Resources::Resource.create(
|
72
|
+
OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => configuration.release_stage
|
73
|
+
)
|
74
|
+
|
75
|
+
# add batch processor with bugsnag exporter to send payloads
|
76
|
+
otel_configurator.add_span_processor(
|
77
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
# use our sampler
|
82
|
+
OpenTelemetry.tracer_provider.sampler = sampler
|
83
|
+
|
84
|
+
return_value
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def self.load_bugsnag_errors_configuration
|
90
|
+
# try to require bugsnag errors and use its configuration
|
91
|
+
require "bugsnag"
|
92
|
+
|
93
|
+
Bugsnag.configuration
|
94
|
+
rescue LoadError
|
95
|
+
# bugsnag errors is not installed
|
96
|
+
Internal::NilErrorsConfiguration.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.log_validation_messages(logger, messages)
|
100
|
+
if messages.length == 1
|
101
|
+
logger.warn("Invalid configuration. #{messages.first}")
|
102
|
+
else
|
103
|
+
logger.warn(
|
104
|
+
<<~MESSAGE
|
105
|
+
Invalid configuration:
|
106
|
+
- #{messages.join("\n - ")}
|
107
|
+
MESSAGE
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bugsnag_performance
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- BugSnag
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-08-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: opentelemetry-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: webmock
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.23'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.23'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- notifiers@bugsnag.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- CHANGELOG.md
|
77
|
+
- LICENSE.txt
|
78
|
+
- bugsnag-performance.gemspec
|
79
|
+
- lib/bugsnag_performance.rb
|
80
|
+
- lib/bugsnag_performance/configuration.rb
|
81
|
+
- lib/bugsnag_performance/error.rb
|
82
|
+
- lib/bugsnag_performance/internal/configuration_validator.rb
|
83
|
+
- lib/bugsnag_performance/internal/delivery.rb
|
84
|
+
- lib/bugsnag_performance/internal/logger_wrapper.rb
|
85
|
+
- lib/bugsnag_performance/internal/nil_errors_configuration.rb
|
86
|
+
- lib/bugsnag_performance/internal/payload_encoder.rb
|
87
|
+
- lib/bugsnag_performance/internal/probability_fetcher.rb
|
88
|
+
- lib/bugsnag_performance/internal/probability_manager.rb
|
89
|
+
- lib/bugsnag_performance/internal/sampler.rb
|
90
|
+
- lib/bugsnag_performance/internal/sampling_header_encoder.rb
|
91
|
+
- lib/bugsnag_performance/internal/span_exporter.rb
|
92
|
+
- lib/bugsnag_performance/internal/task.rb
|
93
|
+
- lib/bugsnag_performance/internal/task_scheduler.rb
|
94
|
+
- lib/bugsnag_performance/version.rb
|
95
|
+
homepage: https://www.bugsnag.com
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata:
|
99
|
+
homepage_uri: https://www.bugsnag.com
|
100
|
+
source_code_uri: https://github.com/bugsnag/bugsnag-ruby-performance
|
101
|
+
bug_tracker_uri: https://github.com/bugsnag/bugsnag-ruby-performance/issues
|
102
|
+
changelog_uri: https://github.com/bugsnag/bugsnag-ruby-performance/blob/v0.1.0/CHANGELOG.md
|
103
|
+
documentation_uri: https://docs.bugsnag.com/performance/integration-guides/ruby/
|
104
|
+
rubygems_mfa_required: 'true'
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '3.0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubygems_version: 3.4.10
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: BugSnag integration for the Ruby Open Telemetry SDK
|
124
|
+
test_files: []
|