bugsnag_performance 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31cfcfc45215dec768c1e6040c418f63355ea308cf2d8d660ccad2c3f2de3621
4
- data.tar.gz: e3868f8cd3341f285e0bd868c3db54e640739ed0a755308162c3dac96cf47b76
3
+ metadata.gz: e9bc8d797c0a3b633e73e88a5d4f399df04bb2fb8063b1a11c42bec974dfec5c
4
+ data.tar.gz: 4554ce3963bc8b4861d586c539b7186ab4550a871586cbe7ad646af5b3b35cbc
5
5
  SHA512:
6
- metadata.gz: f394a3a656e3bbd76aa825db67b2e6bd156590d666648ca6484df820fc833f8904e24dce311084bcfc64ff4e064f5bbcd181a92e51a60d27c7ccedf7a029ccd4
7
- data.tar.gz: 9f8ad547b130279f751169c2305ce57faaf34a566782abca3df7fc307efbab8b14873c6ef0946069a0f49f159d40d8b225969eaf5defa5bb236b402e10afd05d
6
+ metadata.gz: 1467c6a19cf3766d3e5794714c2ad36f145c00b4a23b66a55c1375f72aafd9168c70495a2b57e053f28d7a9d9029825e2ea35051e615bf5c5fa5fbd11298e6be
7
+ data.tar.gz: c6fcb31b7d1ac9cba893c8600a0624662a15e64b6e82ba822da113388238c8772d85e0a585ee50683906f99bbb3b596bdcf8501f1b6628c915f9ba2922585a5c
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ --charset UTF-8
2
+ --fail-on-warning
3
+ --hide-api private
4
+ --no-private
5
+ --protected
6
+ --title "BugSnag Ruby Performance API Documentation"
7
+ --embed-mixins
8
+ --exclude /internal/
9
+ -
10
+ README.md
11
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,2 +1,10 @@
1
1
  Changelog
2
2
  =========
3
+
4
+ ## 0.3.0 (2024-11-13)
5
+
6
+ ### Enhancements
7
+
8
+ * Temporarily disable `Bugsnag-Sent-At` header. Add attributes with SDK name.
9
+ [#42](https://github.com/bugsnag/bugsnag-ruby-performance/pull/42)
10
+ [#43](https://github.com/bugsnag/bugsnag-ruby-performance/pull/43)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,69 @@
1
+ ## How to contribute
2
+
3
+ We are glad you're here! First-time and returning contributors are welcome to add bug fixes and new integrations. If you are unsure about the direction of an enhancement or if it would be generally useful, feel free to open an issue or a work-in-progress pull request and ask for input.
4
+
5
+ Thank you!
6
+
7
+ ### Getting started
8
+
9
+ * [Fork](https://help.github.com/articles/fork-a-repo) the [library on github](https://github.com/bugsnag/bugsnag-ruby-performance)
10
+ * Commit and push until you are happy with your contribution
11
+
12
+ ### Polish
13
+
14
+ * Install the test dependencies
15
+
16
+ ```
17
+ bundle install
18
+ ```
19
+
20
+ * Run the tests and make sure they all pass
21
+
22
+ ```
23
+ bundle exec rspec
24
+ ```
25
+
26
+ * Further information on installing and running the tests can be found in [the testing guide](./TESTING.md)
27
+
28
+ ### Document
29
+
30
+ * Write API docs for your contributions using [YARD](https://yardoc.org/)
31
+ * Generate the API documentation locally
32
+ ```
33
+ bin/rake yard
34
+ ```
35
+ * Review your changes by opening `doc/index.html`
36
+
37
+ ### Ship it!
38
+
39
+ * [Make a pull request](https://help.github.com/articles/using-pull-requests)
40
+
41
+ ## How to release
42
+
43
+ If you're a member of the core team, follow these instructions for releasing bugsnag-ruby-performance.
44
+
45
+ ### First time setup
46
+
47
+ * Create a Rubygems account
48
+ * Get someone to add you as contributor on bugsnag-ruby-performance in Rubygems
49
+
50
+ ### Every time
51
+
52
+ * Create a new release branch named in the format `release/v1.x.x`
53
+ * Update the version number in [`lib/bugsnag_performance/version.rb`](./lib/bugsnag_performance/version.rb)
54
+ * Update [`CHANGELOG.md`](./CHANGELOG.md) with any changes
55
+ * Open a pull request into `main` and get it approved
56
+ * Merge the pull request using the message "Release v1.x.x"
57
+ * Make a GitHub release
58
+ * Release to rubygems:
59
+
60
+ ```
61
+ gem build bugsnag-performance.gemspec
62
+ gem push bugsnag_performance-1.x.x.gem
63
+ ```
64
+
65
+ * Update the version running in the bugsnag-website project
66
+
67
+ ### Update docs.bugsnag.com
68
+
69
+ Update the setup guides for Ruby (and its frameworks) with any new content.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ <div align="center">
2
+ <a href="https://www.bugsnag.com/distributed-tracing">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.smartbear.com/m/3dab7e6cf880aa2b/original/BugSnag-Repository-Header-Dark.svg">
5
+ <img alt="SmartBear BugSnag logo" src="https://assets.smartbear.com/m/3945e02cdc983893/original/BugSnag-Repository-Header-Light.svg">
6
+ </picture>
7
+ </a>
8
+ <h1>Performance monitoring for Ruby</h1>
9
+ </div>
10
+
11
+ [![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/performance/ruby/)
12
+ [![Build status](https://github.com/bugsnag/bugsnag-ruby-performance/actions/workflows/maze-runner.yml/badge.svg?branch=main)](https://github.com/bugsnag/bugsnag-ruby-performance/actions/workflows/maze-runner.yml)
13
+
14
+ Convenience SDK for using the [Ruby Otel SDK](https://github.com/open-telemetry/opentelemetry-ruby) with BugSnag.
15
+
16
+ ## Features
17
+
18
+ - Simple configuration using BugSnag API key
19
+ - Control sampling using BugSnag's probability-based sampler
20
+
21
+ ## Getting started
22
+
23
+ For integration instructions, see our online docs: [docs.bugsnag.com/performance/ruby](https://docs.bugsnag.com/performance/ruby)
24
+
25
+ ## Support
26
+
27
+ * [Read the integration guide](https://docs.bugsnag.com/performance/ruby)
28
+ * [Search open and closed issues](https://github.com/bugsnag/bugsnag-ruby-performance/issues?=is%3Aissue) for similar problems
29
+ * [Report a bug or request a feature](https://github.com/bugsnag/bugsnag-ruby-performance/issues/new)
30
+
31
+ ## License
32
+
33
+ The BugSnag Ruby performance SDK is free software released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details.
data/TESTING.md ADDED
@@ -0,0 +1,38 @@
1
+ # Testing the Ruby BugSnag performance SDK
2
+
3
+ ## Unit tests
4
+
5
+ ```
6
+ bundle install
7
+ bundle exec rspec
8
+ ```
9
+
10
+ ## End-to-end tests
11
+
12
+ These tests are implemented with our internal testing tool [Maze Runner](https://github.com/bugsnag/maze-runner).
13
+
14
+ End to end tests are written in cucumber-style `.feature` files, and need Ruby-backed "steps" in order to know what to run. The tests are located in the top level [`features`](./features/) directory.
15
+
16
+ The Maze Runner test fixtures are containerised so you'll need Docker and Docker Compose to run them.
17
+
18
+ ### Running the end to end tests
19
+
20
+ Install Maze Runner:
21
+
22
+ ```sh
23
+ $ BUNDLE_GEMFILE=Gemfile-maze-runner bundle install
24
+ ```
25
+
26
+ Configure the tests to be run in the following way:
27
+
28
+ - Determine the Ruby version to be tested using the environment variable `RUBY_TEST_VERSION`, e.g. `RUBY_TEST_VERSION=3.3`
29
+ - Determine the Open Telemetry SDK version using the environment variable `OPEN_TELEMETRY_SDK_TEST_VERSION`, e.g. `OPEN_TELEMETRY_SDK_TEST_VERSION="~> 1.5"`
30
+
31
+ Use the Maze Runner CLI to run the tests:
32
+
33
+ ```sh
34
+ $ RUBY_TEST_VERSION=3.3 \
35
+ OPEN_TELEMETRY_SDK_TEST_VERSION="~> 1.5" \
36
+ BUNDLE_GEMFILE=Gemfile-maze-runner \
37
+ bundle exec maze-runner
38
+ ```
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "concurrent-ruby", "~> 1.3"
35
35
  spec.add_dependency "opentelemetry-sdk", "~> 1.2"
36
36
 
37
+ spec.add_development_dependency "yard", "~> 0.9"
37
38
  spec.add_development_dependency "rspec", "~> 3.0"
38
39
  spec.add_development_dependency "webmock", "~> 3.23"
39
40
  end
@@ -2,14 +2,52 @@
2
2
 
3
3
  module BugsnagPerformance
4
4
  class Configuration
5
+ # @api private
5
6
  attr_reader :open_telemetry_configure_block
7
+
8
+ # The logger BugSnag Performance will write messages to
9
+ #
10
+ # If not set, this will default to the BugSnag Errors logger or the Open
11
+ # Telemetry SDK logger
12
+ #
13
+ # @return [Logger]
6
14
  attr_reader :logger
7
15
 
16
+ # Your BugSnag API Key
17
+ #
18
+ # If not set, this will be read from the "BUGSNAG_PERFORMANCE_API_KEY" and
19
+ # "BUGSNAG_API_KEY" environment variables or BugSnag Errors configuration.
20
+ # If none of these returns an API key, a {MissingApiKeyError} will be raised
21
+ #
22
+ # @return [String, nil]
8
23
  attr_accessor :api_key
24
+
25
+ # The current version of the application, for example "1.2.3"
26
+ #
27
+ # If not set, this will be read from the "BUGSNAG_PERFORMANCE_APP_VERSION"
28
+ # and "BUGSNAG_APP_VERSION" environment variables or BugSnag Errors
29
+ # configuration
30
+ #
31
+ # @return [String]
9
32
  attr_accessor :app_version
33
+
34
+ # The current stage of the release process, for example "development" or "production"
35
+ #
36
+ # If not set, this will be read from the "BUGSNAG_PERFORMANCE_RELEASE_STAGE"
37
+ # and "BUGSNAG_RELEASE_STAGE" environment variables or BugSnag Errors
38
+ # configuration and defaults to "production"
39
+ #
40
+ # @return [String]
10
41
  attr_accessor :release_stage
42
+
43
+ # Which release stages to send traces for, for example ["staging", production"]
44
+ #
45
+ # If not set, this will be read from the "BUGSNAG_PERFORMANCE_ENABLED_RELEASE_STAGES"
46
+ # and "BUGSNAG_ENABLED_RELEASE_STAGES" environment variables or BugSnag Errors
47
+ # configuration and defaults to allow any release stage
48
+ #
49
+ # @return [Array<String>, nil]
11
50
  attr_accessor :enabled_release_stages
12
- attr_accessor :use_managed_quota
13
51
 
14
52
  attr_writer :endpoint
15
53
 
@@ -18,9 +56,8 @@ module BugsnagPerformance
18
56
  self.logger = errors_configuration.logger || OpenTelemetry.logger
19
57
 
20
58
  @api_key = fetch(errors_configuration, :api_key, env: "BUGSNAG_PERFORMANCE_API_KEY")
21
- @app_version = fetch(errors_configuration, :app_version)
59
+ @app_version = fetch(errors_configuration, :app_version, env: "BUGSNAG_PERFORMANCE_APP_VERSION")
22
60
  @release_stage = fetch(errors_configuration, :release_stage, env: "BUGSNAG_PERFORMANCE_RELEASE_STAGE", default: "production")
23
- @use_managed_quota = true
24
61
 
25
62
  @enabled_release_stages = fetch(errors_configuration, :enabled_release_stages, env: "BUGSNAG_PERFORMANCE_ENABLED_RELEASE_STAGES")
26
63
 
@@ -40,6 +77,11 @@ module BugsnagPerformance
40
77
  end
41
78
  end
42
79
 
80
+ # The URL to send traces to
81
+ #
82
+ # If not set this defaults to "https://<api_key>.otlp.bugsnag.com/v1/traces"
83
+ #
84
+ # @return [String, nil]
43
85
  def endpoint
44
86
  case
45
87
  when defined?(@endpoint)
@@ -53,6 +95,9 @@ module BugsnagPerformance
53
95
  end
54
96
  end
55
97
 
98
+ # Apply configuration for the Open Telemetry SDK
99
+ #
100
+ # This block should *replace* any calls to OpenTelemetry::SDK.configure
56
101
  def configure_open_telemetry(&open_telemetry_configure_block)
57
102
  @open_telemetry_configure_block = open_telemetry_configure_block
58
103
  end
@@ -16,7 +16,6 @@ module BugsnagPerformance
16
16
  validate_string(:app_version, optional: true)
17
17
  validate_string(:release_stage, optional: true)
18
18
  validate_array(:enabled_release_stages, "non-empty strings", optional: true, &method(:valid_string?))
19
- validate_boolean(:use_managed_quota, optional: false)
20
19
  valid_endpoint = validate_endpoint
21
20
 
22
21
  # if the endpoint is invalid then we shouldn't attempt to send traces
@@ -72,16 +71,6 @@ module BugsnagPerformance
72
71
  end
73
72
  end
74
73
 
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
74
  def validate_array(name, description, optional:, &block)
86
75
  value = @configuration.send(name)
87
76
 
@@ -3,9 +3,12 @@
3
3
  module BugsnagPerformance
4
4
  module Internal
5
5
  class Delivery
6
+ attr_reader :uri
7
+
6
8
  def initialize(configuration)
7
9
  @uri = URI(configuration.endpoint)
8
10
  @common_headers = {
11
+ "User-Agent" => "#{BugsnagPerformance::SDK_NAME} v#{BugsnagPerformance::VERSION}",
9
12
  "Bugsnag-Api-Key" => configuration.api_key,
10
13
  "Content-Type" => "application/json",
11
14
  }.freeze
@@ -14,7 +17,8 @@ module BugsnagPerformance
14
17
  def deliver(headers, body)
15
18
  headers = headers.merge(
16
19
  @common_headers,
17
- { "Bugsnag-Sent-At" => Time.now.utc.iso8601(3) },
20
+ # TODO - can be restored after https://smartbear.atlassian.net/browse/PIPE-7498
21
+ # { "Bugsnag-Sent-At" => Time.now.utc.iso8601(3) },
18
22
  )
19
23
 
20
24
  raw_response = OpenTelemetry::Common::Utilities.untraced do
@@ -10,6 +10,15 @@ module BugsnagPerformance
10
10
  attr_accessor :release_stage
11
11
  attr_accessor :enabled_release_stages
12
12
  attr_accessor :logger
13
+
14
+ def initialize
15
+ # if bugsnag errors is not installed we still want to read from the
16
+ # environment variables it supports
17
+ @api_key = ENV["BUGSNAG_API_KEY"]
18
+ @app_version = ENV["BUGSNAG_APP_VERSION"]
19
+ @release_stage = ENV["BUGSNAG_RELEASE_STAGE"]
20
+ @enabled_release_stages = ENV["BUGSNAG_ENABLED_RELEASE_STAGES"]
21
+ end
13
22
  end
14
23
  end
15
24
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BugsnagPerformance
4
+ module Internal
5
+ class ParsedTracestate
6
+ attr_reader :version
7
+ attr_reader :r_value
8
+
9
+ def initialize(version, r_value, r_value_32_bit:)
10
+ @version = version
11
+ @r_value = r_value
12
+ @r_value_32_bit = r_value_32_bit
13
+ end
14
+
15
+ def valid?
16
+ !!(@version && @r_value)
17
+ end
18
+
19
+ def r_value_32_bit?
20
+ @r_value_32_bit
21
+ end
22
+ end
23
+ end
24
+ end
@@ -26,14 +26,9 @@ module BugsnagPerformance
26
26
  :SPAN_STATUS_UNSET,
27
27
  :SPAN_STATUS_ERROR
28
28
 
29
- def initialize(sampler)
30
- @sampler = sampler
31
- end
32
-
33
29
  def encode(span_data)
34
30
  {
35
31
  resourceSpans: span_data
36
- .filter { |span| @sampler.resample_span?(span) }
37
32
  .group_by(&:resource)
38
33
  .map do |resource, scope_spans|
39
34
  {
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BugsnagPerformance
4
+ module Internal
5
+ class ProbabilityAttributeSpanProcessor
6
+ def initialize(probability_manager)
7
+ @probability_manager = probability_manager
8
+ end
9
+
10
+ def on_start(span, parent_context)
11
+ # avoid overwriting the attribute if the sampler has already set it
12
+ if span.attributes.nil? || span.attributes["bugsnag.sampling.p"].nil?
13
+ span.set_attribute("bugsnag.sampling.p", @probability_manager.probability)
14
+ end
15
+
16
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
17
+ end
18
+
19
+ def on_finish(span)
20
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
21
+ end
22
+
23
+ def force_flush(timeout: nil)
24
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
25
+ end
26
+
27
+ def shutdown(timeout: nil)
28
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
29
+ end
30
+ end
31
+ end
32
+ end
@@ -3,12 +3,17 @@
3
3
  module BugsnagPerformance
4
4
  module Internal
5
5
  class Sampler
6
- PROBABILITY_SCALE_FACTOR = 18_446_744_073_709_551_615 # (2 ** 64) - 1
6
+ # the scale factor to use with a 64 bit r value
7
+ PROBABILITY_SCALE_FACTOR_64 = 18_446_744_073_709_551_615 # (2 ** 64) - 1
7
8
 
8
- private_constant :PROBABILITY_SCALE_FACTOR
9
+ # the scale factor to use with a 32 bit r value
10
+ PROBABILITY_SCALE_FACTOR_32 = 4_294_967_295 # (2 ** 32) - 1
9
11
 
10
- def initialize(probability_manager)
12
+ private_constant :PROBABILITY_SCALE_FACTOR_64, :PROBABILITY_SCALE_FACTOR_32
13
+
14
+ def initialize(probability_manager, tracestate_parser)
11
15
  @probability_manager = probability_manager
16
+ @tracestate_parser = tracestate_parser
12
17
  end
13
18
 
14
19
  def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
@@ -17,47 +22,63 @@ module BugsnagPerformance
17
22
  # for the sampling decision & p value attribute which would result
18
23
  # in inconsistent data
19
24
  probability = @probability_manager.probability
25
+ parent_span_context = OpenTelemetry::Trace.current_span(parent_context).context
26
+ tracestate = parent_span_context.tracestate
20
27
 
21
28
  decision =
22
- if sample_using_probability_and_trace_id?(probability, trace_id)
29
+ if sample_using_probability_and_trace?(probability, tracestate, trace_id)
23
30
  OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE
24
31
  else
25
32
  OpenTelemetry::SDK::Trace::Samplers::Decision::DROP
26
33
  end
27
34
 
28
- parent_span_context = OpenTelemetry::Trace.current_span(parent_context).context
29
-
30
35
  OpenTelemetry::SDK::Trace::Samplers::Result.new(
31
36
  decision: decision,
32
- tracestate: parent_span_context.tracestate,
37
+ tracestate: tracestate,
33
38
  attributes: { "bugsnag.sampling.p" => probability },
34
39
  )
35
40
  end
36
41
 
37
42
  # @api private
38
43
  def resample_span?(span)
39
- probability = @probability_manager.probability
40
-
41
44
  # sample all spans that are missing the p value attribute
42
45
  return true if span.attributes.nil? || span.attributes["bugsnag.sampling.p"].nil?
43
46
 
47
+ probability = @probability_manager.probability
48
+
44
49
  # update the p value attribute if it was originally sampled with a larger
45
50
  # probability than the current value
46
51
  if span.attributes["bugsnag.sampling.p"] > probability
47
52
  span.attributes["bugsnag.sampling.p"] = probability
48
53
  end
49
54
 
50
- sample_using_probability_and_trace_id?(span.attributes["bugsnag.sampling.p"], span.trace_id)
55
+ sample_using_probability_and_trace?(
56
+ span.attributes["bugsnag.sampling.p"],
57
+ span.tracestate,
58
+ span.trace_id
59
+ )
51
60
  end
52
61
 
53
62
  private
54
63
 
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
64
+ def sample_using_probability_and_trace?(probability, tracestate, trace_id)
65
+ # parse the r value from tracestate or generate from the trace ID by
66
+ # unpacking it as a u64
67
+ parsed_tracestate = @tracestate_parser.parse(tracestate)
68
+
69
+ if parsed_tracestate.valid?
70
+ # the JS SDK will send a u32 as the r value so we need to scale the
71
+ # probability value to the same range for comparisons to work
72
+ r_value = parsed_tracestate.r_value
73
+ scale_factor = parsed_tracestate.r_value_32_bit? ? PROBABILITY_SCALE_FACTOR_32 : PROBABILITY_SCALE_FACTOR_64
74
+ else
75
+ r_value = trace_id.unpack1("@8Q>")
76
+ scale_factor = PROBABILITY_SCALE_FACTOR_64
77
+ end
58
78
 
59
- # unpack the trace ID as a u64
60
- r_value = trace_id.unpack1("@8Q>")
79
+ # scale the probability (stored as a float from 0-1) to the appropriate
80
+ # size int (u32 or u64)
81
+ p_value = (probability * scale_factor).floor
61
82
 
62
83
  p_value >= r_value
63
84
  end
@@ -7,36 +7,67 @@ module BugsnagPerformance
7
7
  logger,
8
8
  probability_manager,
9
9
  delivery,
10
+ sampler,
10
11
  payload_encoder,
11
12
  sampling_header_encoder
12
13
  )
13
14
  @logger = logger
14
15
  @probability_manager = probability_manager
15
16
  @delivery = delivery
17
+ @sampler = sampler
16
18
  @payload_encoder = payload_encoder
17
19
  @sampling_header_encoder = sampling_header_encoder
18
20
  @disabled = false
21
+ @unmanaged_mode = false
22
+ @logged_first_batch_destination = false
19
23
  end
20
24
 
21
25
  def disable!
22
26
  @disabled = true
23
27
  end
24
28
 
29
+ def unmanaged_mode!
30
+ @unmanaged_mode = true
31
+ end
32
+
33
+ def unmanaged_mode?
34
+ @unmanaged_mode
35
+ end
36
+
25
37
  def export(span_data, timeout: nil)
26
38
  return OpenTelemetry::SDK::Trace::Export::SUCCESS if @disabled
27
39
 
28
40
  with_timeout(timeout) do
41
+ # ensure we're in the correct managed or unmanaged mode
42
+ maybe_enter_unmanaged_mode
43
+ managed_status = unmanaged_mode? ? "unmanaged" : "managed"
44
+
29
45
  headers = {}
30
- sampling_header = @sampling_header_encoder.encode(span_data)
31
46
 
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
47
+ # resample the spans and attach the Bugsnag-Span-Sampling header only
48
+ # if we're in managed mode
49
+ unless unmanaged_mode?
50
+ span_data = span_data.filter { |span| @sampler.resample_span?(span) }
51
+
52
+ sampling_header = @sampling_header_encoder.encode(span_data)
53
+
54
+ if sampling_header.nil?
55
+ @logger.warn("One or more spans are missing the 'bugsnag.sampling.p' attribute. This trace will be sent as unmanaged")
56
+ managed_status = "unmanaged"
57
+ else
58
+ headers["Bugsnag-Span-Sampling"] = sampling_header
59
+ end
36
60
  end
37
61
 
38
62
  body = JSON.generate(@payload_encoder.encode(span_data))
39
63
 
64
+ # log whether we're sending managed or unmanaged spans on the first
65
+ # batch only
66
+ unless @logged_first_batch_destination
67
+ @logger.info("Sending #{managed_status} spans to #{@delivery.uri}")
68
+ @logged_first_batch_destination = true
69
+ end
70
+
40
71
  response = @delivery.deliver(headers, body)
41
72
 
42
73
  if response.sampling_probability
@@ -64,6 +95,18 @@ module BugsnagPerformance
64
95
 
65
96
  private
66
97
 
98
+ def maybe_enter_unmanaged_mode
99
+ # we're in unmanaged mode already so don't need to do anything
100
+ return if unmanaged_mode?
101
+
102
+ # our sampler is in use so we're in managed mode
103
+ return if OpenTelemetry.tracer_provider.sampler.is_a?(Sampler)
104
+
105
+ # the user has changed the sampler from ours to a custom one; enter
106
+ # unmanaged mode
107
+ unmanaged_mode!
108
+ end
109
+
67
110
  def with_timeout(timeout, &block)
68
111
  if timeout.nil?
69
112
  block.call
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BugsnagPerformance
4
+ module Internal
5
+ class TracestateParser
6
+ def parse(tracestate)
7
+ smartbear_values = tracestate.value("sb")
8
+ return ParsedTracestate.new(nil, nil, r_value_32_bit: false) if smartbear_values.nil?
9
+
10
+ version = nil
11
+ r_value_32 = nil
12
+ r_value_64 = nil
13
+
14
+ smartbear_values.split(";").each do |field|
15
+ key, value = field.split(":", 2)
16
+
17
+ case key
18
+ when "v"
19
+ version = value
20
+ when "r32"
21
+ r_value_32 = Integer(value, exception: false)
22
+ when "r64"
23
+ r_value_64 = Integer(value, exception: false)
24
+ end
25
+ end
26
+
27
+ ParsedTracestate.new(
28
+ version,
29
+ # a 64 bit value should take precedence over a 32 bit one if both are
30
+ # present in the tracestate
31
+ r_value_64 || r_value_32,
32
+ r_value_32_bit: r_value_64.nil? && !r_value_32.nil?
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugsnagPerformance
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
+ SDK_NAME = "Ruby Bugsnag Performance SDK"
5
6
  end
@@ -18,13 +18,21 @@ require_relative "bugsnag_performance/internal/span_exporter"
18
18
  require_relative "bugsnag_performance/internal/logger_wrapper"
19
19
  require_relative "bugsnag_performance/internal/task_scheduler"
20
20
  require_relative "bugsnag_performance/internal/payload_encoder"
21
+ require_relative "bugsnag_performance/internal/parsed_tracestate"
22
+ require_relative "bugsnag_performance/internal/tracestate_parser"
21
23
  require_relative "bugsnag_performance/internal/probability_fetcher"
22
24
  require_relative "bugsnag_performance/internal/probability_manager"
23
25
  require_relative "bugsnag_performance/internal/configuration_validator"
24
26
  require_relative "bugsnag_performance/internal/sampling_header_encoder"
25
27
  require_relative "bugsnag_performance/internal/nil_errors_configuration"
28
+ require_relative "bugsnag_performance/internal/probability_attribute_span_processor"
26
29
 
27
30
  module BugsnagPerformance
31
+ # Configure BugSnag Performance
32
+ #
33
+ # Yields a {Configuration} object to use to set application settings.
34
+ #
35
+ # @yieldparam configuration [Configuration]
28
36
  def self.configure(&block)
29
37
  unvalidated_configuration = Configuration.new(load_bugsnag_errors_configuration)
30
38
 
@@ -39,16 +47,23 @@ module BugsnagPerformance
39
47
  task_scheduler = Internal::TaskScheduler.new
40
48
  probability_fetcher = Internal::ProbabilityFetcher.new(configuration.logger, delivery, task_scheduler)
41
49
  probability_manager = Internal::ProbabilityManager.new(probability_fetcher)
42
- sampler = Internal::Sampler.new(probability_manager)
50
+ sampler = Internal::Sampler.new(probability_manager, Internal::TracestateParser.new)
43
51
 
44
52
  exporter = Internal::SpanExporter.new(
45
53
  configuration.logger,
46
54
  probability_manager,
47
55
  delivery,
48
- Internal::PayloadEncoder.new(sampler),
56
+ sampler,
57
+ Internal::PayloadEncoder.new,
49
58
  Internal::SamplingHeaderEncoder.new,
50
59
  )
51
60
 
61
+ # enter unmanaged mode if the OTel sampler environment variable has been set
62
+ # note: we assume any value means a non-default sampler will be used because
63
+ # we don't control what the valid values are
64
+ user_has_custom_sampler = ENV.key?("OTEL_TRACES_SAMPLER")
65
+ exporter.unmanaged_mode! if user_has_custom_sampler
66
+
52
67
  if configuration.enabled_release_stages && !configuration.enabled_release_stages.include?(configuration.release_stage)
53
68
  configuration.logger.info("Not exporting spans as the current release stage is not in the enabled release stages.")
54
69
  exporter.disable!
@@ -69,17 +84,29 @@ module BugsnagPerformance
69
84
  end
70
85
 
71
86
  otel_configurator.resource = OpenTelemetry::SDK::Resources::Resource.create(
72
- OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => configuration.release_stage
87
+ {
88
+ OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT => configuration.release_stage,
89
+ "bugsnag.telemetry.sdk.name" => SDK_NAME,
90
+ "bugsnag.telemetry.sdk.version" => VERSION,
91
+ }
73
92
  )
74
93
 
75
94
  # add batch processor with bugsnag exporter to send payloads
76
95
  otel_configurator.add_span_processor(
77
96
  OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
78
97
  )
98
+
99
+ # ensure the "bugsnag.sampling.p" attribute is set on all spans even when
100
+ # our sampler is not in use
101
+ otel_configurator.add_span_processor(
102
+ Internal::ProbabilityAttributeSpanProcessor.new(probability_manager)
103
+ )
79
104
  end
80
105
 
81
- # use our sampler
82
- OpenTelemetry.tracer_provider.sampler = sampler
106
+ # don't use our sampler if the user has configured a sampler via the OTel
107
+ # environment variable
108
+ # note: the user can still replace our sampler with their own after this
109
+ OpenTelemetry.tracer_provider.sampler = sampler unless user_has_custom_sampler
83
110
 
84
111
  return_value
85
112
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bugsnag_performance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BugSnag
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-21 00:00:00.000000000 Z
11
+ date: 2024-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.9'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -73,8 +87,12 @@ executables: []
73
87
  extensions: []
74
88
  extra_rdoc_files: []
75
89
  files:
90
+ - ".yardopts"
76
91
  - CHANGELOG.md
92
+ - CONTRIBUTING.md
77
93
  - LICENSE.txt
94
+ - README.md
95
+ - TESTING.md
78
96
  - bugsnag-performance.gemspec
79
97
  - lib/bugsnag_performance.rb
80
98
  - lib/bugsnag_performance/configuration.rb
@@ -83,7 +101,9 @@ files:
83
101
  - lib/bugsnag_performance/internal/delivery.rb
84
102
  - lib/bugsnag_performance/internal/logger_wrapper.rb
85
103
  - lib/bugsnag_performance/internal/nil_errors_configuration.rb
104
+ - lib/bugsnag_performance/internal/parsed_tracestate.rb
86
105
  - lib/bugsnag_performance/internal/payload_encoder.rb
106
+ - lib/bugsnag_performance/internal/probability_attribute_span_processor.rb
87
107
  - lib/bugsnag_performance/internal/probability_fetcher.rb
88
108
  - lib/bugsnag_performance/internal/probability_manager.rb
89
109
  - lib/bugsnag_performance/internal/sampler.rb
@@ -91,6 +111,7 @@ files:
91
111
  - lib/bugsnag_performance/internal/span_exporter.rb
92
112
  - lib/bugsnag_performance/internal/task.rb
93
113
  - lib/bugsnag_performance/internal/task_scheduler.rb
114
+ - lib/bugsnag_performance/internal/tracestate_parser.rb
94
115
  - lib/bugsnag_performance/version.rb
95
116
  homepage: https://www.bugsnag.com
96
117
  licenses:
@@ -99,7 +120,7 @@ metadata:
99
120
  homepage_uri: https://www.bugsnag.com
100
121
  source_code_uri: https://github.com/bugsnag/bugsnag-ruby-performance
101
122
  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
123
+ changelog_uri: https://github.com/bugsnag/bugsnag-ruby-performance/blob/v0.3.0/CHANGELOG.md
103
124
  documentation_uri: https://docs.bugsnag.com/performance/integration-guides/ruby/
104
125
  rubygems_mfa_required: 'true'
105
126
  post_install_message:
@@ -117,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
138
  - !ruby/object:Gem::Version
118
139
  version: '0'
119
140
  requirements: []
120
- rubygems_version: 3.4.10
141
+ rubygems_version: 3.4.19
121
142
  signing_key:
122
143
  specification_version: 4
123
144
  summary: BugSnag integration for the Ruby Open Telemetry SDK