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