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 +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +69 -0
- data/README.md +33 -0
- data/TESTING.md +38 -0
- data/bugsnag-performance.gemspec +1 -0
- data/lib/bugsnag_performance/configuration.rb +48 -3
- data/lib/bugsnag_performance/internal/configuration_validator.rb +0 -11
- data/lib/bugsnag_performance/internal/delivery.rb +5 -1
- data/lib/bugsnag_performance/internal/nil_errors_configuration.rb +9 -0
- data/lib/bugsnag_performance/internal/parsed_tracestate.rb +24 -0
- data/lib/bugsnag_performance/internal/payload_encoder.rb +0 -5
- data/lib/bugsnag_performance/internal/probability_attribute_span_processor.rb +32 -0
- data/lib/bugsnag_performance/internal/sampler.rb +36 -15
- data/lib/bugsnag_performance/internal/span_exporter.rb +48 -5
- data/lib/bugsnag_performance/internal/tracestate_parser.rb +37 -0
- data/lib/bugsnag_performance/version.rb +2 -1
- data/lib/bugsnag_performance.rb +32 -5
- metadata +25 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9bc8d797c0a3b633e73e88a5d4f399df04bb2fb8063b1a11c42bec974dfec5c
|
4
|
+
data.tar.gz: 4554ce3963bc8b4861d586c539b7186ab4550a871586cbe7ad646af5b3b35cbc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1467c6a19cf3766d3e5794714c2ad36f145c00b4a23b66a55c1375f72aafd9168c70495a2b57e053f28d7a9d9029825e2ea35051e615bf5c5fa5fbd11298e6be
|
7
|
+
data.tar.gz: c6fcb31b7d1ac9cba893c8600a0624662a15e64b6e82ba822da113388238c8772d85e0a585ee50683906f99bbb3b596bdcf8501f1b6628c915f9ba2922585a5c
|
data/.yardopts
ADDED
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
|
+
```
|
data/bugsnag-performance.gemspec
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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:
|
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
|
-
|
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
|
56
|
-
#
|
57
|
-
|
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
|
-
#
|
60
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
data/lib/bugsnag_performance.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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.
|
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.
|
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
|