From 248ad250c483bf59f87b91a8dbdda847e61b305e Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 22 Nov 2022 14:24:37 +0100 Subject: [PATCH 01/11] Add config.instrumenter to switch between sentry and otel instrumentation --- CHANGELOG.md | 7 +++ sentry-rails/lib/sentry/rails/railtie.rb | 2 +- .../spec/sentry/rails/tracing_spec.rb | 17 ++++++ sentry-ruby/lib/sentry/configuration.rb | 11 ++++ sentry-ruby/lib/sentry/hub.rb | 7 ++- sentry-ruby/spec/sentry/configuration_spec.rb | 21 +++++++ sentry-ruby/spec/sentry_spec.rb | 57 +++++++++++++++++++ 7 files changed, 119 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fc0ad32..9462778ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Features + +- Add OpenTelemetry support with new `sentry-opentelemetry` gem + - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) + ## 5.6.0 ### Features diff --git a/sentry-rails/lib/sentry/rails/railtie.rb b/sentry-rails/lib/sentry/rails/railtie.rb index 8e21931ca..63f929b1c 100644 --- a/sentry-rails/lib/sentry/rails/railtie.rb +++ b/sentry-rails/lib/sentry/rails/railtie.rb @@ -115,7 +115,7 @@ def override_streaming_reporter end def activate_tracing - if Sentry.configuration.tracing_enabled? + if Sentry.configuration.tracing_enabled? && Sentry.configuration.instrumenter == :sentry subscribers = Sentry.configuration.rails.tracing_subscribers Sentry::Rails::Tracing.register_subscribers(subscribers) Sentry::Rails::Tracing.subscribe_tracing_events diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb index 8d8b9da54..346471f9c 100644 --- a/sentry-rails/spec/sentry/rails/tracing_spec.rb +++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb @@ -91,6 +91,23 @@ end end + context "with instrumenter :otel" do + before do + make_basic_app do |config| + config.traces_sample_rate = 1.0 + config.instrumenter = :otel + end + end + + it "doesn't do any tracing" do + p = Post.create! + get "/posts/#{p.id}" + + expect(response).to have_http_status(:ok) + expect(transport.events.count).to eq(0) + end + end + context "with sprockets-rails" do let(:string_io) { StringIO.new } let(:logger) do diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 5cbc08355..9c6eebc41 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -211,6 +211,10 @@ class Configuration # @return [Boolean] attr_accessor :auto_session_tracking + # The instrumenter to use, :sentry or :otel + # @return [Symbol] + attr_reader :instrumenter + # these are not config options # @!visibility private attr_reader :errors, :gem_specs @@ -237,6 +241,8 @@ class Configuration MODULE_SEPARATOR = "::".freeze SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder] + INSTRUMENTERS = [:sentry, :otel] + # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions @@post_initialization_callbacks = [] @@ -269,6 +275,7 @@ def initialize self.trusted_proxies = [] self.dsn = ENV['SENTRY_DSN'] self.server_name = server_name_from_env + self.instrumenter = :sentry self.before_send = nil self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT @@ -332,6 +339,10 @@ def environment=(environment) @environment = environment.to_s end + def instrumenter=(instrumenter) + @instrumenter = INSTRUMENTERS.include?(instrumenter) ? instrumenter : :sentry + end + def sending_allowed? @errors = [] diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb index 582855a30..8e58c60fc 100644 --- a/sentry-ruby/lib/sentry/hub.rb +++ b/sentry-ruby/lib/sentry/hub.rb @@ -76,8 +76,9 @@ def pop_scope @stack.pop end - def start_transaction(transaction: nil, custom_sampling_context: {}, **options) + def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options) return unless configuration.tracing_enabled? + return unless instrumenter == configuration.instrumenter transaction ||= Transaction.new(**options.merge(hub: self)) @@ -92,7 +93,9 @@ def start_transaction(transaction: nil, custom_sampling_context: {}, **options) transaction end - def with_child_span(**attributes, &block) + def with_child_span(instrumenter: :sentry, **attributes, &block) + return yield(nil) unless instrumenter == configuration.instrumenter + current_span = current_scope.get_span return yield(nil) unless current_span diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index 6f73bfb77..e164f9313 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -351,4 +351,25 @@ class SentryConfigurationSample < Sentry::Configuration expect(subject.auto_session_tracking).to eq(false) end end + + describe "#instrumenter" do + it "returns :sentry by default" do + expect(subject.instrumenter).to eq(:sentry) + end + + it "can be set to :sentry" do + subject.instrumenter = :sentry + expect(subject.instrumenter).to eq(:sentry) + end + + it "can be set to :otel" do + subject.instrumenter = :otel + expect(subject.instrumenter).to eq(:otel) + end + + it "defaults to :sentry if invalid" do + subject.instrumenter = :foo + expect(subject.instrumenter).to eq(:sentry) + end + end end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 94ea1a948..c8b7cb3b3 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -469,6 +469,24 @@ expect(described_class.start_transaction(op: "foo")).to eq(nil) end end + + context "when instrumenter is not :sentry" do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.instrumenter = :otel + end + end + + it "noops without explicit instrumenter" do + expect(described_class.start_transaction(op: "foo")).to eq(nil) + end + + it "creates transaction with explicit instrumenter" do + transaction = described_class.start_transaction(op: "foo", instrumenter: :otel) + expect(transaction).to be_a(Sentry::Transaction) + end + end end describe ".with_child_span" do @@ -515,6 +533,45 @@ expect(child_span.parent_span_id).to eq(parent_span.span_id) expect(child_span.timestamp).to be_a(Float) end + + context "when instrumenter is not :sentry" do + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + config.instrumenter = :otel + end + + described_class.get_current_scope.set_span(parent_span) + end + + it "yields block with nil without explicit instrumenter" do + span = nil + executed = false + + result = described_class.with_child_span do |child_span| + span = child_span + executed = true + "foobar" + end + + expect(result).to eq("foobar") + expect(span).to eq(nil) + expect(executed).to eq(true) + end + + it "records the child span with explicit instrumenter" do + child_span = nil + + result = described_class.with_child_span(instrumenter: :otel, op: "child") do |span| + child_span = span + "foobar" + end + + expect(result).to eq("foobar") + expect(child_span.parent_span_id).to eq(parent_span.span_id) + expect(child_span.timestamp).to be_a(Float) + end + end end end From 954d6a2d0921489e239f9c2af141df8b4803e8a9 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 22 Nov 2022 16:35:17 +0100 Subject: [PATCH 02/11] Expose span_id in Span constructor --- CHANGELOG.md | 1 + sentry-ruby/lib/sentry/span.rb | 3 ++- sentry-ruby/spec/sentry/span_spec.rb | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9462778ef..a1c80f376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add OpenTelemetry support with new `sentry-opentelemetry` gem - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) + - Expose `span_id` in Span constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) ## 5.6.0 diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index fc41c4d37..9ca450e16 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -68,13 +68,14 @@ def initialize( op: nil, status: nil, trace_id: nil, + span_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil ) @trace_id = trace_id || SecureRandom.uuid.delete("-") - @span_id = SecureRandom.hex(8) + @span_id = span_id || SecureRandom.hex(8) @parent_span_id = parent_span_id @sampled = sampled @start_timestamp = start_timestamp || Sentry.utc_now.to_f diff --git a/sentry-ruby/spec/sentry/span_spec.rb b/sentry-ruby/spec/sentry/span_spec.rb index 6758ed67e..b9518a570 100644 --- a/sentry-ruby/spec/sentry/span_spec.rb +++ b/sentry-ruby/spec/sentry/span_spec.rb @@ -153,6 +153,14 @@ expect(span_2.transaction).to eq(subject.transaction) end + it "initializes a new child Span with explicit span id" do + span_id = SecureRandom.hex(8) + new_span = subject.start_child(op: "foo", span_id: span_id) + + expect(new_span.op).to eq("foo") + expect(new_span.span_id).to eq(span_id) + end + context "when the parent span has a span_recorder" do subject do # inherits the span recorder from the transaction From f2d6d717a124d07be45601bc36d3ad2f577ed98c Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 22 Nov 2022 16:40:58 +0100 Subject: [PATCH 03/11] Expose optional end_timestamp argument in Span#finish and Transaction#finish --- CHANGELOG.md | 3 ++- sentry-ruby/lib/sentry/span.rb | 4 ++-- sentry-ruby/lib/sentry/transaction.rb | 4 ++-- sentry-ruby/spec/sentry/transaction_spec.rb | 10 ++++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c80f376..3005db183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ - Add OpenTelemetry support with new `sentry-opentelemetry` gem - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) - - Expose `span_id` in Span constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) + - Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) + - Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) ## 5.6.0 diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 9ca450e16..f7d16cb83 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -90,11 +90,11 @@ def initialize( # Finishes the span by adding a timestamp. # @return [self] - def finish + def finish(end_timestamp: nil) # already finished return if @timestamp - @timestamp = Sentry.utc_now.to_f + @timestamp = end_timestamp || Sentry.utc_now.to_f self end diff --git a/sentry-ruby/lib/sentry/transaction.rb b/sentry-ruby/lib/sentry/transaction.rb index 2a5ab0f0c..e237b5a9b 100644 --- a/sentry-ruby/lib/sentry/transaction.rb +++ b/sentry-ruby/lib/sentry/transaction.rb @@ -210,7 +210,7 @@ def set_initial_sample_decision(sampling_context:) # Finishes the transaction's recording and send it to Sentry. # @param hub [Hub] the hub that'll send this transaction. (Deprecated) # @return [TransactionEvent] - def finish(hub: nil) + def finish(hub: nil, end_timestamp: nil) if hub log_warn( <<~MSG @@ -222,7 +222,7 @@ def finish(hub: nil) hub ||= @hub - super() # Span#finish doesn't take arguments + super(end_timestamp: end_timestamp) if @name.nil? @name = UNLABELD_NAME diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index 2d4716f66..da7f54a68 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -391,6 +391,16 @@ expect(event[:transaction]).to eq("foo") end + it "finishes the transaction with explicit timestamp" do + timestamp = Sentry.utc_now.to_f + subject.finish(end_timestamp: timestamp) + + expect(events.count).to eq(1) + event = events.last.to_hash + + expect(event[:timestamp]).to eq(timestamp) + end + it "assigns the transaction's tags" do Sentry.set_tags(name: "apple") From 7fedc8e0c229ca85dc6bc8fe722f227f7aff12e0 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 22 Nov 2022 16:48:06 +0100 Subject: [PATCH 04/11] Add Transaction#set_context api --- CHANGELOG.md | 1 + sentry-ruby/lib/sentry/transaction.rb | 49 +++++++++++++++++---- sentry-ruby/lib/sentry/transaction_event.rb | 1 + sentry-ruby/spec/sentry/client_spec.rb | 6 +++ sentry-ruby/spec/sentry/transaction_spec.rb | 15 +++++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3005db183..c11b93a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) - Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) - Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) + - Add `Transaction#set_context` api [#1947](https://github.com/getsentry/sentry-ruby/pull/1947) ## 5.6.0 diff --git a/sentry-ruby/lib/sentry/transaction.rb b/sentry-ruby/lib/sentry/transaction.rb index e237b5a9b..fb8d24970 100644 --- a/sentry-ruby/lib/sentry/transaction.rb +++ b/sentry-ruby/lib/sentry/transaction.rb @@ -50,6 +50,10 @@ class Transaction < Span # @return [Float, nil] attr_reader :effective_sample_rate + # Additional contexts stored directly on the transaction object. + # @return [Hash] + attr_reader :contexts + def initialize( hub:, name: nil, @@ -74,6 +78,7 @@ def initialize( @environment = hub.configuration.environment @dsn = hub.configuration.dsn @effective_sample_rate = nil + @contexts = {} init_span_recorder end @@ -91,16 +96,10 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h return unless hub.configuration.tracing_enabled? return unless sentry_trace - match = SENTRY_TRACE_REGEXP.match(sentry_trace) - return if match.nil? - trace_id, parent_span_id, sampled_flag = match[1..3] + sentry_trace_data = extract_sentry_trace(sentry_trace) + return unless sentry_trace_data - parent_sampled = - if sampled_flag.nil? - nil - else - sampled_flag != "0" - end + trace_id, parent_span_id, parent_sampled = sentry_trace_data baggage = if baggage && !baggage.empty? Baggage.from_incoming_header(baggage) @@ -123,6 +122,20 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h ) end + # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header. + # + # @param sentry_trace [String] the sentry-trace header value from the previous transaction. + # @return [Array, nil] + def self.extract_sentry_trace(sentry_trace) + match = SENTRY_TRACE_REGEXP.match(sentry_trace) + return nil if match.nil? + + trace_id, parent_span_id, sampled_flag = match[1..3] + parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0" + + [trace_id, parent_span_id, parent_sampled] + end + # @return [Hash] def to_hash hash = super @@ -244,6 +257,24 @@ def get_baggage @baggage end + # Set the transaction name directly. + # Considered internal api since it bypasses the usual scope logic. + # @param name [String] + # @param source [Symbol] + # @return [void] + def set_name(name, source: :custom) + @name = name + @source = SOURCES.include?(source) ? source.to_sym : :custom + end + + # Set contexts directly on the transaction. + # @param key [String, Symbol] + # @param value [Object] + # @return [void] + def set_context(key, value) + @contexts[key] = value + end + protected def init_span_recorder(limit = 1000) diff --git a/sentry-ruby/lib/sentry/transaction_event.rb b/sentry-ruby/lib/sentry/transaction_event.rb index 06f89488f..389ca45b0 100644 --- a/sentry-ruby/lib/sentry/transaction_event.rb +++ b/sentry-ruby/lib/sentry/transaction_event.rb @@ -19,6 +19,7 @@ def initialize(transaction:, **options) self.transaction = transaction.name self.transaction_info = { source: transaction.source } + self.contexts.merge!(transaction.contexts) self.contexts.merge!(trace: transaction.get_trace_context) self.timestamp = transaction.timestamp self.start_timestamp = transaction.start_timestamp diff --git a/sentry-ruby/spec/sentry/client_spec.rb b/sentry-ruby/spec/sentry/client_spec.rb index 44a3bda0f..39a04d53e 100644 --- a/sentry-ruby/spec/sentry/client_spec.rb +++ b/sentry-ruby/spec/sentry/client_spec.rb @@ -160,6 +160,12 @@ def sentry_context "trace_id" => transaction.trace_id }) end + + it "adds explicitly added contexts to event" do + transaction.set_context(:foo, { bar: 42 }) + event = subject.event_from_transaction(transaction) + expect(event.contexts).to include({ foo: { bar: 42 } }) + end end describe "#event_from_exception" do diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index da7f54a68..665095aa1 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -559,4 +559,19 @@ end end end + + describe "#set_name" do + it "sets name and source directly" do + subject.set_name("bar", source: :url) + expect(subject.name).to eq("bar") + expect(subject.source).to eq(:url) + end + end + + describe "#set_context" do + it "sets arbitrary context" do + subject.set_context(:foo, { bar: 42 }) + expect(subject.contexts).to eq({ foo: { bar: 42 } }) + end + end end From c812168ef1daca4a8700bc7c33a0bd2b442f226c Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 22 Nov 2022 17:09:13 +0100 Subject: [PATCH 05/11] Add new sentry-opentelemetry gem --- .craft.yml | 1 + .../workflows/sentry_opentelemetry_test.yml | 51 ++++ .scripts/batch_build.rb | 2 +- .scripts/batch_release.rb | 2 +- CHANGELOG.md | 2 +- sentry-opentelemetry/.gitignore | 11 + sentry-opentelemetry/.rspec | 3 + sentry-opentelemetry/CODE_OF_CONDUCT.md | 74 ++++++ sentry-opentelemetry/Gemfile | 24 ++ sentry-opentelemetry/LICENSE.txt | 21 ++ sentry-opentelemetry/Makefile | 3 + sentry-opentelemetry/README.md | 65 +++++ sentry-opentelemetry/Rakefile | 8 + sentry-opentelemetry/bin/console | 15 ++ sentry-opentelemetry/bin/setup | 8 + .../lib/sentry-opentelemetry.rb | 6 + .../lib/sentry/opentelemetry/propagator.rb | 73 +++++ .../sentry/opentelemetry/span_processor.rb | 159 +++++++++++ .../lib/sentry/opentelemetry/version.rb | 7 + .../sentry-opentelemetry.gemspec | 29 ++ .../sentry/opentelemetry/propagator_spec.rb | 144 ++++++++++ .../opentelemetry/span_processor_spec.rb | 251 ++++++++++++++++++ sentry-opentelemetry/spec/spec_helper.rb | 53 ++++ 23 files changed, 1009 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/sentry_opentelemetry_test.yml create mode 100644 sentry-opentelemetry/.gitignore create mode 100644 sentry-opentelemetry/.rspec create mode 100644 sentry-opentelemetry/CODE_OF_CONDUCT.md create mode 100644 sentry-opentelemetry/Gemfile create mode 100644 sentry-opentelemetry/LICENSE.txt create mode 100644 sentry-opentelemetry/Makefile create mode 100644 sentry-opentelemetry/README.md create mode 100644 sentry-opentelemetry/Rakefile create mode 100755 sentry-opentelemetry/bin/console create mode 100755 sentry-opentelemetry/bin/setup create mode 100644 sentry-opentelemetry/lib/sentry-opentelemetry.rb create mode 100644 sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb create mode 100644 sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb create mode 100644 sentry-opentelemetry/lib/sentry/opentelemetry/version.rb create mode 100644 sentry-opentelemetry/sentry-opentelemetry.gemspec create mode 100644 sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb create mode 100644 sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb create mode 100644 sentry-opentelemetry/spec/spec_helper.rb diff --git a/.craft.yml b/.craft.yml index a4972d4c9..0786e6366 100644 --- a/.craft.yml +++ b/.craft.yml @@ -10,4 +10,5 @@ targets: 'gem:sentry-rails': 'gem:sentry-sidekiq': 'gem:sentry-delayed_job': + 'gem:sentry-opentelemetry': - name: github diff --git a/.github/workflows/sentry_opentelemetry_test.yml b/.github/workflows/sentry_opentelemetry_test.yml new file mode 100644 index 000000000..91b800259 --- /dev/null +++ b/.github/workflows/sentry_opentelemetry_test.yml @@ -0,0 +1,51 @@ +name: sentry-opentelemetry Test + +on: + workflow_dispatch: + push: + branches: + - master + - \d+-\d+ + pull_request: +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +jobs: + test: + defaults: + run: + working-directory: sentry-opentelemetry + name: Ruby ${{ matrix.ruby_version }} & OpenTelemetry ${{ matrix.opentelemetry_version }}, options - ${{ toJson(matrix.options) }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + ruby_version: [2.6, 2.7, '3.0', head, jruby] + # opentelemetry_version: [1.2.0] + os: [ubuntu-latest] + include: + - { os: ubuntu-latest, ruby_version: 3.1, options: { rubyopt: "--enable-frozen-string-literal --debug=frozen-string-literal" } } + - { os: ubuntu-latest, ruby_version: 3.1, options: { codecov: 1 } } + steps: + - uses: actions/checkout@v1 + + - name: Set up Ruby ${{ matrix.ruby_version }} + uses: ruby/setup-ruby@8ddb7b3348b3951590db24c346e94ebafdabc926 + with: + ruby-version: ${{ matrix.ruby_version }} + + - name: Run specs + env: + RUBYOPT: ${{ matrix.options.rubyopt }} + OPENTELEMETRY_VERSION: ${{ matrix.opentelemetry_version }} + run: | + bundle install --jobs 4 --retry 3 + bundle exec rake + + - name: Upload Coverage + if: ${{ matrix.options.codecov }} + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} -R `pwd` -f coverage/coverage.xml diff --git a/.scripts/batch_build.rb b/.scripts/batch_build.rb index 8f7c078e0..cfe961077 100755 --- a/.scripts/batch_build.rb +++ b/.scripts/batch_build.rb @@ -1,4 +1,4 @@ -INTEGRATIONS = %w(sentry-rails sentry-sidekiq sentry-delayed_job sentry-resque) +INTEGRATIONS = %w(sentry-rails sentry-sidekiq sentry-delayed_job sentry-resque sentry-opentelemetry) GEMS = %w(sentry-ruby) + INTEGRATIONS GEMS.each do |gem_name| diff --git a/.scripts/batch_release.rb b/.scripts/batch_release.rb index 51273914a..ab368fd9d 100755 --- a/.scripts/batch_release.rb +++ b/.scripts/batch_release.rb @@ -1,4 +1,4 @@ -INTEGRATIONS = %w(sentry-rails sentry-sidekiq sentry-delayed_job sentry-resque) +INTEGRATIONS = %w(sentry-rails sentry-sidekiq sentry-delayed_job sentry-resque sentry-opentelemetry) GEMS = %w(sentry-ruby) + INTEGRATIONS def get_version_file_name(gem_name) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11b93a24..dca91be34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- Add OpenTelemetry support with new `sentry-opentelemetry` gem +- Add OpenTelemetry support with new `sentry-opentelemetry` gem [#1948](https://github.com/getsentry/sentry-ruby/pull/1948) - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) - Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) - Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) diff --git a/sentry-opentelemetry/.gitignore b/sentry-opentelemetry/.gitignore new file mode 100644 index 000000000..b04a8c840 --- /dev/null +++ b/sentry-opentelemetry/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/sentry-opentelemetry/.rspec b/sentry-opentelemetry/.rspec new file mode 100644 index 000000000..34c5164d9 --- /dev/null +++ b/sentry-opentelemetry/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/sentry-opentelemetry/CODE_OF_CONDUCT.md b/sentry-opentelemetry/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..3c12de66b --- /dev/null +++ b/sentry-opentelemetry/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at stan001212@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/sentry-opentelemetry/Gemfile b/sentry-opentelemetry/Gemfile new file mode 100644 index 000000000..3d2667b51 --- /dev/null +++ b/sentry-opentelemetry/Gemfile @@ -0,0 +1,24 @@ +source "https://rubygems.org" +git_source(:github) { |name| "https://github.com/#{name}.git" } + +# Specify your gem's dependencies in sentry-ruby.gemspec +gemspec + +gem "rake", "~> 12.0" +gem "rspec", "~> 3.0" +gem 'simplecov' +gem "simplecov-cobertura", "~> 1.4" +gem "rexml" + +# opentelemetry_version = ENV["OPENTELEMETRY_VERSION"] +# opentelemetry_version = "1.2.0" if opentelemetry_version.nil? +# gem "opentelemetry-sdk", "~> #{opentelemetry_version}" + +gem "opentelemetry-sdk" +gem "opentelemetry-instrumentation-rails" + +gem "sentry-ruby", path: "../sentry-ruby" + +gem "object_tracer" +gem "debug", github: "ruby/debug", platform: :ruby if RUBY_VERSION.to_f >= 2.6 +gem "pry" diff --git a/sentry-opentelemetry/LICENSE.txt b/sentry-opentelemetry/LICENSE.txt new file mode 100644 index 000000000..a53c2869c --- /dev/null +++ b/sentry-opentelemetry/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/sentry-opentelemetry/Makefile b/sentry-opentelemetry/Makefile new file mode 100644 index 000000000..5ce0336fa --- /dev/null +++ b/sentry-opentelemetry/Makefile @@ -0,0 +1,3 @@ +build: + bundle install + gem build sentry-opentelemetry.gemspec diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md new file mode 100644 index 000000000..1c4e645d8 --- /dev/null +++ b/sentry-opentelemetry/README.md @@ -0,0 +1,65 @@ +

+ + + +
+

+ +# sentry-opentelemetry, the OpenTelemetry integration for Sentry's Ruby client + +--- + + +[![Gem Version](https://img.shields.io/gem/v/sentry-opentelemetry.svg)](https://rubygems.org/gems/sentry-opentelemetry) +![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-opentelemetry%20Test/badge.svg) +[![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) +[![Gem](https://img.shields.io/gem/dt/sentry-opentelemetry.svg)](https://rubygems.org/gems/sentry-opentelemetry/) +[![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-opentelemetry&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-opentelemetry&package-manager=bundler&version-scheme=semver) + + +[Documentation](https://docs.sentry.io/platforms/ruby/guides/opentelemetry/) | [Bug Tracker](https://github.com/getsentry/sentry-ruby/issues) | [Forum](https://forum.sentry.io/) | IRC: irc.freenode.net, #sentry + +The official Ruby-language client and integration layer for the [Sentry](https://github.com/getsentry/sentry) error reporting API. + + +## Getting Started + +### Install + +```ruby +gem "sentry-ruby" +gem "sentry-rails" +gem "sentry-opentelemetry" + +gem "opentelemetry-sdk" +gem "opentelemetry-instrumentation-all" +``` + +### Configuration + +First, make sure to initialize Sentry before OpenTelemetry by prefixing your initializers and set the `instrumenter` to `:otel`. +```ruby +# config/initializers/01_sentry.rb + +Sentry.init do |config| + config.dsn = "MY_DSN" + config.traces_sample_rate = 1.0 + config.instrumenter = :otel +end +``` + +This will disable all Sentry instrumentation and rely on the chosen OpenTelemetry tracers for creating spans. + +Next, configure OpenTelemetry as per your needs and hook in the Sentry span processor and propagator. + +```ruby +# config/initializers/02_otel.rb + +OpenTelemetry::SDK.configure do |c| + c.use_all + c.add_span_processor(Sentry::OpenTelemetry::SpanProcessor.instance) +end + +OpenTelemetry.propagation = Sentry::OpenTelemetry::Propagator.new +``` + diff --git a/sentry-opentelemetry/Rakefile b/sentry-opentelemetry/Rakefile new file mode 100644 index 000000000..1417dbd2d --- /dev/null +++ b/sentry-opentelemetry/Rakefile @@ -0,0 +1,8 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec).tap do |task| + task.rspec_opts = "--order rand" +end + +task :default => :spec diff --git a/sentry-opentelemetry/bin/console b/sentry-opentelemetry/bin/console new file mode 100755 index 000000000..c58bfc8b0 --- /dev/null +++ b/sentry-opentelemetry/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "sentry/opentelemetry" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/sentry-opentelemetry/bin/setup b/sentry-opentelemetry/bin/setup new file mode 100755 index 000000000..dce67d860 --- /dev/null +++ b/sentry-opentelemetry/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/sentry-opentelemetry/lib/sentry-opentelemetry.rb b/sentry-opentelemetry/lib/sentry-opentelemetry.rb new file mode 100644 index 000000000..7a2bb70eb --- /dev/null +++ b/sentry-opentelemetry/lib/sentry-opentelemetry.rb @@ -0,0 +1,6 @@ +require "sentry-ruby" +require "opentelemetry-sdk" + +require "sentry/opentelemetry/version" +require "sentry/opentelemetry/span_processor" +require "sentry/opentelemetry/propagator" diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb new file mode 100644 index 000000000..c2b8a6ab4 --- /dev/null +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb @@ -0,0 +1,73 @@ +module Sentry + module OpenTelemetry + class Propagator + + FIELDS = [SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME].freeze + + SENTRY_TRACE_KEY = ::OpenTelemetry::Context.create_key('sentry-trace') + SENTRY_BAGGAGE_KEY = ::OpenTelemetry::Context.create_key('sentry-baggage') + + def inject( + carrier, + context: ::OpenTelemetry::Context.current, + setter: ::OpenTelemetry::Context::Propagation.text_map_setter + ) + span_context = ::OpenTelemetry::Trace.current_span(context).context + return unless span_context.valid? + + span_map = SpanProcessor.instance.span_map + sentry_span = span_map[span_context.hex_span_id] + return unless sentry_span + + setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_sentry_trace) + + baggage = sentry_span.to_baggage + setter.set(carrier, BAGGAGE_HEADER_NAME, baggage) if baggage && !baggage.empty? + end + + def extract( + carrier, + context: ::OpenTelemetry::Context.current, + getter: ::OpenTelemetry::Context::Propagation.text_map_getter + ) + sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME) + return context unless sentry_trace + + sentry_trace_data = Transaction.extract_sentry_trace(sentry_trace) + return context unless sentry_trace_data + + context = context.set_value(SENTRY_TRACE_KEY, sentry_trace_data) + trace_id, span_id, _parent_sampled = sentry_trace_data + + span_context = ::OpenTelemetry::Trace::SpanContext.new( + trace_id: [trace_id].pack('H*'), + span_id: [span_id].pack('H*'), + # we simulate a sampled trace on the otel side and leave the sampling to sentry + trace_flags: ::OpenTelemetry::Trace::TraceFlags::SAMPLED, + remote: true + ) + + baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME) + + baggage = if baggage_header && !baggage_header.empty? + Baggage.from_incoming_header(baggage_header) + else + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and frozen and won't be populated as head SDK. + Baggage.new({}) + end + + baggage.freeze! + context = context.set_value(SENTRY_BAGGAGE_KEY, baggage) + + span = ::OpenTelemetry::Trace.non_recording_span(span_context) + ::OpenTelemetry::Trace.context_with_span(span, parent_context: context) + end + + def fields + FIELDS + end + end + end +end diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb new file mode 100644 index 000000000..7ec818989 --- /dev/null +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb @@ -0,0 +1,159 @@ +require 'singleton' + +module Sentry + module OpenTelemetry + TraceData = Struct.new(:trace_id, :span_id, :parent_span_id, :parent_sampled, :baggage) + + class SpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor + include Singleton + + SEMANTIC_CONVENTIONS = ::OpenTelemetry::SemanticConventions::Trace + + # The mapping from otel span ids to sentry spans + # @return [Hash] + attr_reader :span_map + + def initialize + @span_map = {} + end + + def on_start(otel_span, parent_context) + return unless Sentry.initialized? && Sentry.configuration.instrumenter == :otel + return unless otel_span.context.valid? + return if from_sentry_sdk?(otel_span) + + trace_data = get_trace_data(otel_span, parent_context) + + sentry_parent_span = @span_map[trace_data.parent_span_id] if trace_data.parent_span_id + + sentry_span = if sentry_parent_span + Sentry.configuration.logger.info("Continuing otel span #{otel_span.name} on parent #{sentry_parent_span.op}") + + sentry_parent_span.start_child( + span_id: trace_data.span_id, + description: otel_span.name, + start_timestamp: otel_span.start_timestamp / 1e9 + ) + else + Sentry.configuration.logger.info("Starting otel transaction #{otel_span.name}") + + options = { + instrumenter: :otel, + name: otel_span.name, + span_id: trace_data.span_id, + trace_id: trace_data.trace_id, + parent_span_id: trace_data.parent_span_id, + parent_sampled: trace_data.parent_sampled, + baggage: trace_data.baggage, + start_timestamp: otel_span.start_timestamp / 1e9 + } + + Sentry.start_transaction(**options) + end + + @span_map[trace_data.span_id] = sentry_span + end + + def on_finish(otel_span) + return unless Sentry.initialized? && Sentry.configuration.instrumenter == :otel + return unless otel_span.context.valid? + + sentry_span = @span_map.delete(otel_span.context.hex_span_id) + return unless sentry_span + + sentry_span.set_op(otel_span.name) + + if sentry_span.is_a?(Sentry::Transaction) + sentry_span.set_name(otel_span.name) + sentry_span.set_context(:otel, otel_context_hash(otel_span)) + else + update_span_with_otel_data(sentry_span, otel_span) + end + + Sentry.configuration.logger.info("Finishing sentry_span #{sentry_span.op}") + sentry_span.finish(end_timestamp: otel_span.end_timestamp / 1e9) + end + + def clear + @span_map = {} + end + + private + + def from_sentry_sdk?(otel_span) + dsn = Sentry.configuration.dsn + return false unless dsn + + if otel_span.name.start_with?("HTTP") + # only check client requests, connects are sometimes internal + return false unless %i(client internal).include?(otel_span.kind) + + address = otel_span.attributes[SEMANTIC_CONVENTIONS::NET_PEER_NAME] + + # if no address drop it, just noise + return true unless address + return true if dsn.host == address + end + + false + end + + def get_trace_data(otel_span, parent_context) + trace_data = TraceData.new + trace_data.span_id = otel_span.context.hex_span_id + trace_data.trace_id = otel_span.context.hex_trace_id + + unless otel_span.parent_span_id == ::OpenTelemetry::Trace::INVALID_SPAN_ID + trace_data.parent_span_id = otel_span.parent_span_id.unpack1("H*") + end + + sentry_trace_data = parent_context[Propagator::SENTRY_TRACE_KEY] + trace_data.parent_sampled = sentry_trace_data[2] if sentry_trace_data + + trace_data.baggage = parent_context[Propagator::SENTRY_BAGGAGE_KEY] + + trace_data + end + + def otel_context_hash(otel_span) + otel_context = {} + otel_context[:attributes] = otel_span.attributes unless otel_span.attributes.empty? + + resource_attributes = otel_span.resource.attribute_enumerator.to_h + otel_context[:resource] = resource_attributes unless resource_attributes.empty? + + otel_context + end + + def update_span_with_otel_data(sentry_span, otel_span) + sentry_span.set_data('otel.kind', otel_span.kind) + otel_span.attributes&.each { |k, v| sentry_span.set_data(k, v) } + + op = otel_span.name + description = otel_span.name + + if (http_method = otel_span.attributes[SEMANTIC_CONVENTIONS::HTTP_METHOD]) + op = "http.#{otel_span.kind}" + description = http_method + + peer_name = otel_span.attributes[SEMANTIC_CONVENTIONS::NET_PEER_NAME] + description += " #{peer_name}" if peer_name + + target = otel_span.attributes[SEMANTIC_CONVENTIONS::HTTP_TARGET] + description += target if target + + status_code = otel_span.attributes[SEMANTIC_CONVENTIONS::HTTP_STATUS_CODE] + sentry_span.set_http_status(status_code) if status_code + elsif otel_span.attributes[SEMANTIC_CONVENTIONS::DB_SYSTEM] + op = "db" + + statement = otel_span.attributes[SEMANTIC_CONVENTIONS::DB_STATEMENT] + description = statement if statement + end + + sentry_span.set_op(op) + sentry_span.set_description(description) + end + end + end +end diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb new file mode 100644 index 000000000..549b0732a --- /dev/null +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Sentry + module OpenTelemetry + VERSION = "5.6.0" + end +end diff --git a/sentry-opentelemetry/sentry-opentelemetry.gemspec b/sentry-opentelemetry/sentry-opentelemetry.gemspec new file mode 100644 index 000000000..e9cd2f156 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry.gemspec @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "lib/sentry/opentelemetry/version" + +Gem::Specification.new do |spec| + spec.name = "sentry-opentelemetry" + spec.version = Sentry::OpenTelemetry::VERSION + spec.authors = ["Sentry Team"] + spec.description = spec.summary = "A gem that provides OpenTelemetry integration for the Sentry error logger" + spec.email = "accounts@sentry.io" + spec.license = 'MIT' + spec.homepage = "https://github.com/getsentry/sentry-ruby" + + spec.platform = Gem::Platform::RUBY + spec.required_ruby_version = '>= 2.4' + spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] + spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n") + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "sentry-ruby", "~> 5.6.0" + spec.add_dependency "opentelemetry-sdk", "~> 1.0" +end diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb new file mode 100644 index 000000000..41503297a --- /dev/null +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +RSpec.describe Sentry::OpenTelemetry::Propagator do + let(:tracer) { ::OpenTelemetry.tracer_provider.tracer('sentry', '1.0') } + let(:span_processor) { Sentry::OpenTelemetry::SpanProcessor.instance } + let(:span_map) { span_processor.span_map } + + before do + perform_basic_setup + perform_otel_setup + span_map.clear + end + + describe '#inject' do + let(:carrier) { {} } + + it 'noops with invalid span_context' do + subject.inject(carrier) + expect(carrier).to eq({}) + end + + it 'noops if span not found in span_map' do + span = tracer.start_root_span('test') + ctx = ::OpenTelemetry::Trace.context_with_span(span) + + expect(span_map).to receive(:[]).and_call_original + subject.inject(carrier, context: ctx) + expect(carrier).to eq({}) + end + + context 'with running trace' do + let(:ctx) do + # setup root span, child span and return current context + empty_context = ::OpenTelemetry::Context.empty + root_span = tracer.start_root_span('test') + span_processor.on_start(root_span, empty_context) + root_context = ::OpenTelemetry::Trace.context_with_span(root_span, parent_context: empty_context) + child_span = tracer.start_span('child test', with_parent: root_context) + span_processor.on_start(child_span, root_context) + expect(span_map.size).to eq(2) + + ::OpenTelemetry::Trace.context_with_span(child_span, parent_context: root_context) + end + + it 'sets sentry-trace and baggage headers on carrier' do + subject.inject(carrier, context: ctx) + + span_id = ::OpenTelemetry::Trace.current_span(ctx).context.hex_span_id + span = span_map[span_id] + + expect(carrier['sentry-trace']).to eq(span.to_sentry_trace) + expect(carrier['baggage']).to eq(span.to_baggage) + end + end + end + + describe '#extract' do + let(:ctx) { ::OpenTelemetry::Context.empty } + + it 'returns unchanged context without sentry-trace' do + carrier = {} + updated_ctx = subject.extract(carrier, context: ctx) + expect(updated_ctx).to eq(ctx) + end + + it 'returns unchanged context with invalid sentry-trace' do + carrier = { 'sentry-trace' => '000-000-0' } + updated_ctx = subject.extract(carrier, context: ctx) + expect(updated_ctx).to eq(ctx) + end + + context 'with only sentry-trace header' do + let(:carrier) do + { 'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1' } + end + + it 'returns context with sentry-trace data' do + updated_ctx = subject.extract(carrier, context: ctx) + + sentry_trace_data = updated_ctx[described_class::SENTRY_TRACE_KEY] + expect(sentry_trace_data).not_to be_nil + + trace_id, parent_span_id, parent_sampled = sentry_trace_data + expect(trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b') + expect(parent_span_id).to eq('6e0c63257de34c92') + expect(parent_sampled).to eq(true) + end + + it 'returns context with empty frozen baggage' do + updated_ctx = subject.extract(carrier, context: ctx) + + baggage = updated_ctx[described_class::SENTRY_BAGGAGE_KEY] + expect(baggage).to be_a(Sentry::Baggage) + expect(baggage.items).to eq({}) + expect(baggage.mutable).to eq(false) + end + + it 'returns context with correct span_context' do + updated_ctx = subject.extract(carrier, context: ctx) + + span_context = ::OpenTelemetry::Trace.current_span(updated_ctx).context + expect(span_context.valid?).to eq(true) + expect(span_context.hex_trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b') + expect(span_context.hex_span_id).to eq('6e0c63257de34c92') + expect(span_context.trace_flags.sampled?).to eq(true) + expect(span_context.remote?).to eq(true) + end + end + + context 'with sentry-trace and baggage headers' do + let(:carrier) do + { + 'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + 'baggage' => 'other-vendor-value-1=foo;bar;baz, '\ + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b, '\ + 'sentry-public_key=49d0f7386ad645858ae85020e393bef3, '\ + 'sentry-sample_rate=0.01337, '\ + 'sentry-user_id=Am%C3%A9lie, '\ + 'other-vendor-value-2=foo;bar;' + } + end + + it 'returns context with baggage' do + updated_ctx = subject.extract(carrier, context: ctx) + + baggage = updated_ctx[described_class::SENTRY_BAGGAGE_KEY] + expect(baggage).to be_a(Sentry::Baggage) + expect(baggage.mutable).to eq(false) + expect(baggage.items).to eq({ + 'sample_rate' => '0.01337', + 'public_key' => '49d0f7386ad645858ae85020e393bef3', + 'trace_id' => 'd4cda95b652f4a1592b449d5929fda1b', + 'user_id' => 'Amélie' + }) + end + end + end + + describe '#fields' do + it 'returns header names' do + expect(subject.fields).to eq(['sentry-trace', 'baggage']) + end + end +end diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb new file mode 100644 index 000000000..86d56bdb7 --- /dev/null +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb @@ -0,0 +1,251 @@ +require 'spec_helper' + +RSpec.describe Sentry::OpenTelemetry::SpanProcessor do + let(:subject) { described_class.instance } + + let(:tracer) { ::OpenTelemetry.tracer_provider.tracer('sentry', '1.0') } + let(:empty_context) { ::OpenTelemetry::Context.empty } + let(:invalid_span) { ::OpenTelemetry::SDK::Trace::Span::INVALID } + + let(:root_span) do + attributes = { + 'http.method' => 'GET', + 'http.host' => 'sentry.io', + 'http.scheme' => 'https' + } + + tracer.start_root_span('HTTP GET', attributes: attributes, kind: :server) + end + + let(:root_parent_context) do + ::OpenTelemetry::Trace.context_with_span(root_span, parent_context: empty_context) + end + + let(:child_db_span) do + attributes = { + 'db.system' => 'postgresql', + 'db.user' => 'foo', + 'db.name' => 'foo', + 'net.peer.name' => 'localhost', + 'net.transport' => 'IP.TCP', + 'net.peer.ip' => '::1,127.0.0.1', + 'net.peer.port' => '5432,5432', + 'db.operation' => 'SELECT', + 'db.statement' => 'SELECT * FROM foo' + } + + tracer.start_span('SELECT table', with_parent: root_parent_context, attributes: attributes, kind: :client) + end + + let(:child_http_span) do + attributes = { + 'http.method' => 'GET', + 'http.scheme' => 'https', + 'http.target' => '/search', + 'net.peer.name' => 'www.google.com', + 'net.peer.port' => 443, + 'http.status_code' => 200 + } + + tracer.start_span('HTTP GET', with_parent: root_parent_context, attributes: attributes, kind: :client) + end + + let(:child_internal_span) do + attributes = { + 'http.method' => 'POST', + 'http.scheme' => 'https', + 'http.target' => '/api/5434472/envelope/', + 'net.peer.name' => 'sentry.localdomain', + 'net.peer.port' => 443 + } + + tracer.start_span('HTTP POST', with_parent: root_parent_context, attributes: attributes, kind: :client) + end + + before do + perform_basic_setup + perform_otel_setup + subject.clear + end + + describe 'singleton instance' do + it 'has empty span_map' do + expect(subject.span_map).to eq({}) + end + + it 'raises error on instantiation' do + expect { described_class.new }.to raise_error(NoMethodError) + end + end + + describe '#on_start' do + it 'noops when not initialized' do + expect(Sentry).to receive(:initialized?).and_return(false) + subject.on_start(root_span, empty_context) + expect(subject.span_map).to eq({}) + end + + it 'noops when instrumenter is not otel' do + perform_basic_setup do |c| + c.instrumenter = :sentry + end + + subject.on_start(root_span, empty_context) + expect(subject.span_map).to eq({}) + end + + it 'noops when invalid span' do + subject.on_start(invalid_span, empty_context) + expect(subject.span_map).to eq({}) + end + + it 'starts a sentry transaction on otel root span' do + expect(Sentry).to receive(:start_transaction).and_call_original + subject.on_start(root_span, empty_context) + + span_id = root_span.context.hex_span_id + trace_id = root_span.context.hex_trace_id + + expect(subject.span_map.size).to eq(1) + expect(subject.span_map.keys.first).to eq(span_id) + + transaction = subject.span_map.values.first + expect(transaction).to be_a(Sentry::Transaction) + expect(transaction.name).to eq(root_span.name) + expect(transaction.span_id).to eq(span_id) + expect(transaction.trace_id).to eq(trace_id) + expect(transaction.start_timestamp).to eq(root_span.start_timestamp / 1e9) + + expect(transaction.parent_span_id).to eq(nil) + expect(transaction.parent_sampled).to eq(nil) + expect(transaction.baggage).to eq(nil) + end + + context 'with started transaction' do + let(:transaction) do + subject.on_start(root_span, empty_context) + subject.span_map.values.first + end + + it 'noops on internal sentry sdk requests' do + expect(transaction).not_to receive(:start_child) + subject.on_start(child_internal_span, root_parent_context) + end + + it 'starts a sentry child span on otel child span' do + expect(transaction).to receive(:start_child).and_call_original + subject.on_start(child_db_span, root_parent_context) + + span_id = child_db_span.context.hex_span_id + trace_id = child_db_span.context.hex_trace_id + + expect(subject.span_map.size).to eq(2) + expect(subject.span_map.keys.last).to eq(span_id) + + sentry_span = subject.span_map[span_id] + expect(sentry_span).to be_a(Sentry::Span) + expect(sentry_span.transaction).to eq(transaction) + expect(sentry_span.span_id).to eq(span_id) + expect(sentry_span.trace_id).to eq(trace_id) + expect(sentry_span.description).to eq(child_db_span.name) + expect(sentry_span.start_timestamp).to eq(child_db_span.start_timestamp / 1e9) + end + end + end + + describe '#on_finish' do + before do + subject.on_start(root_span, empty_context) + subject.on_start(child_db_span, root_parent_context) + subject.on_start(child_http_span, root_parent_context) + end + + let(:finished_db_span) { child_db_span.finish } + let(:finished_http_span) { child_http_span.finish } + let(:finished_root_span) { root_span.finish } + let(:finished_invalid_span) { invalid_span.finish } + + it 'noops when not initialized' do + expect(Sentry).to receive(:initialized?).and_return(false) + expect(subject.span_map).not_to receive(:delete) + subject.on_finish(finished_root_span) + end + + it 'noops when instrumenter is not otel' do + perform_basic_setup do |c| + c.instrumenter = :sentry + end + + expect(subject.span_map).not_to receive(:delete) + subject.on_finish(finished_root_span) + end + + it 'noops when invalid span' do + expect(subject.span_map).not_to receive(:delete) + subject.on_finish(finished_invalid_span) + end + + it 'finishes sentry child span on otel child db span finish' do + expect(subject.span_map).to receive(:delete).and_call_original + + span_id = finished_db_span.context.hex_span_id + sentry_span = subject.span_map[span_id] + expect(sentry_span).to be_a(Sentry::Span) + + expect(sentry_span).to receive(:finish).and_call_original + subject.on_finish(finished_db_span) + + expect(sentry_span.op).to eq('db') + expect(sentry_span.description).to eq(finished_db_span.attributes['db.statement']) + expect(sentry_span.data).to include(finished_db_span.attributes) + expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind }) + expect(sentry_span.timestamp).to eq(finished_db_span.end_timestamp / 1e9) + + expect(subject.span_map.size).to eq(2) + expect(subject.span_map.keys).not_to include(span_id) + end + + it 'finishes sentry child span on otel child http span finish' do + expect(subject.span_map).to receive(:delete).and_call_original + + span_id = finished_http_span.context.hex_span_id + sentry_span = subject.span_map[span_id] + expect(sentry_span).to be_a(Sentry::Span) + + expect(sentry_span).to receive(:finish).and_call_original + subject.on_finish(finished_http_span) + + expect(sentry_span.op).to eq('http.client') + expect(sentry_span.description).to eq('GET www.google.com/search') + expect(sentry_span.data).to include(finished_http_span.attributes) + expect(sentry_span.data).to include({ 'otel.kind' => finished_http_span.kind }) + expect(sentry_span.timestamp).to eq(finished_http_span.end_timestamp / 1e9) + + expect(subject.span_map.size).to eq(2) + expect(subject.span_map.keys).not_to include(span_id) + end + + it 'finishes sentry transaction on otel root span finish' do + subject.on_finish(finished_db_span) + subject.on_finish(finished_http_span) + + expect(subject.span_map).to receive(:delete).and_call_original + + span_id = finished_root_span.context.hex_span_id + transaction = subject.span_map[span_id] + expect(transaction).to be_a(Sentry::Transaction) + + expect(transaction).to receive(:finish).and_call_original + subject.on_finish(finished_root_span) + + expect(transaction.op).to eq(finished_root_span.name) + expect(transaction.name).to eq(finished_root_span.name) + expect(transaction.contexts[:otel]).to eq({ + attributes: finished_root_span.attributes, + resource: finished_root_span.resource.attribute_enumerator.to_h + }) + + expect(subject.span_map).to eq({}) + end + end +end diff --git a/sentry-opentelemetry/spec/spec_helper.rb b/sentry-opentelemetry/spec/spec_helper.rb new file mode 100644 index 000000000..de6215585 --- /dev/null +++ b/sentry-opentelemetry/spec/spec_helper.rb @@ -0,0 +1,53 @@ +require "bundler/setup" +require "pry" +require "debug" if RUBY_VERSION.to_f >= 2.6 + +require 'simplecov' + +SimpleCov.start do + project_name "sentry-opentelemetry" + root File.join(__FILE__, "../../../") + coverage_dir File.join(__FILE__, "../../coverage") +end + +if ENV["CI"] + require 'simplecov-cobertura' + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +end + +require "sentry-ruby" +require "sentry/test_helper" +require "sentry-opentelemetry" +require "opentelemetry/sdk" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end + +def perform_basic_setup + Sentry.init do |config| + config.logger = Logger.new(nil) + config.dsn = Sentry::TestHelper::DUMMY_DSN + config.transport.transport_class = Sentry::DummyTransport + # so the events will be sent synchronously for testing + config.background_worker_threads = 0 + config.instrumenter = :otel + config.traces_sample_rate = 1.0 + yield(config) if block_given? + end +end + +def perform_otel_setup + ::OpenTelemetry::SDK.configure do |c| + # suppress otlp warnings + c.logger = Logger.new(File::NULL) + end +end From 2756a9b5487aad08309dfebcb6a544e9c87e5132 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Wed, 23 Nov 2022 17:17:55 +0100 Subject: [PATCH 06/11] Update changelog/readme --- CHANGELOG.md | 10 ++++++---- sentry-opentelemetry/README.md | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dca91be34..db2ff3f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,12 @@ ### Features - Add OpenTelemetry support with new `sentry-opentelemetry` gem [#1948](https://github.com/getsentry/sentry-ruby/pull/1948) - - Add `config.instrumenter` to switch between sentry and otel instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) - - Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) - - Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) - - Add `Transaction#set_context` api [#1947](https://github.com/getsentry/sentry-ruby/pull/1947) + The new `sentry-opentelemetry` gem adds support to automatically integrate OpenTelemetry performance training with Sentry. [Give it a try](https://github.com/getsentry/sentry-ruby/tree/neel/otel-gem/sentry-opentelemetry#getting-started) and let us know if you have any feedback or problems with using it. + - Add `config.instrumenter` to switch between sentry and OpenTelemetry instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) +- Expose `span_id` in `Span` constructor [#1945](https://github.com/getsentry/sentry-ruby/pull/1945) +- Expose `end_timestamp` in `Span#finish` and `Transaction#finish` [#1946](https://github.com/getsentry/sentry-ruby/pull/1946) +- Add `Transaction#set_context` api [#1947](https://github.com/getsentry/sentry-ruby/pull/1947) + ## 5.6.0 diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md index 1c4e645d8..7103036d9 100644 --- a/sentry-opentelemetry/README.md +++ b/sentry-opentelemetry/README.md @@ -37,9 +37,9 @@ gem "opentelemetry-instrumentation-all" ### Configuration -First, make sure to initialize Sentry before OpenTelemetry by prefixing your initializers and set the `instrumenter` to `:otel`. +Initialize Sentry for tracing and set the `instrumenter` to `:otel`. ```ruby -# config/initializers/01_sentry.rb +# config/initializers/sentry.rb Sentry.init do |config| config.dsn = "MY_DSN" @@ -53,7 +53,7 @@ This will disable all Sentry instrumentation and rely on the chosen OpenTelemetr Next, configure OpenTelemetry as per your needs and hook in the Sentry span processor and propagator. ```ruby -# config/initializers/02_otel.rb +# config/initializers/otel.rb OpenTelemetry::SDK.configure do |c| c.use_all From 976c660a090e4adcbb45dd41d5bca3139b98a4f8 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 29 Nov 2022 15:04:40 +0100 Subject: [PATCH 07/11] Typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7341c882c..2fb3234cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Add OpenTelemetry support with new `sentry-opentelemetry` gem - Add `config.instrumenter` to switch between `:sentry` and `:otel` instrumentation [#1944](https://github.com/getsentry/sentry-ruby/pull/1944) - The new `sentry-opentelemetry` gem adds support to automatically integrate OpenTelemetry performance training with Sentry. [Give it a try](https://github.com/getsentry/sentry-ruby/tree/neel/otel-gem/sentry-opentelemetry#getting-started) and let us know if you have any feedback or problems with using it. + The new `sentry-opentelemetry` gem adds support to automatically integrate OpenTelemetry performance tracing with Sentry. [Give it a try](https://github.com/getsentry/sentry-ruby/tree/neel/otel-gem/sentry-opentelemetry#getting-started) and let us know if you have any feedback or problems with using it. ## 5.6.0 From 8bd63c4689b65c2977fb5fea8d5e94ece9da8336 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 29 Nov 2022 16:54:28 +0100 Subject: [PATCH 08/11] Add frozen_string_literal --- sentry-opentelemetry/lib/sentry-opentelemetry.rb | 2 ++ sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb | 2 ++ sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb | 2 ++ .../spec/sentry/opentelemetry/propagator_spec.rb | 2 ++ .../spec/sentry/opentelemetry/span_processor_spec.rb | 2 ++ sentry-opentelemetry/spec/spec_helper.rb | 2 ++ 6 files changed, 12 insertions(+) diff --git a/sentry-opentelemetry/lib/sentry-opentelemetry.rb b/sentry-opentelemetry/lib/sentry-opentelemetry.rb index 7a2bb70eb..9c9bb7e2f 100644 --- a/sentry-opentelemetry/lib/sentry-opentelemetry.rb +++ b/sentry-opentelemetry/lib/sentry-opentelemetry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "sentry-ruby" require "opentelemetry-sdk" diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb index c2b8a6ab4..b65d2436c 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/propagator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sentry module OpenTelemetry class Propagator diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb index 7ec818989..ed13d9339 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'singleton' module Sentry diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb index 41503297a..275eeed96 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/propagator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::OpenTelemetry::Propagator do diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb index 86d56bdb7..633d75f1e 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe Sentry::OpenTelemetry::SpanProcessor do diff --git a/sentry-opentelemetry/spec/spec_helper.rb b/sentry-opentelemetry/spec/spec_helper.rb index de6215585..c1fb93760 100644 --- a/sentry-opentelemetry/spec/spec_helper.rb +++ b/sentry-opentelemetry/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" require "pry" require "debug" if RUBY_VERSION.to_f >= 2.6 From c46fefe27b08b95beac5199313dc3d596042d360 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 29 Nov 2022 17:03:03 +0100 Subject: [PATCH 09/11] PR fixes --- .../lib/sentry/opentelemetry/span_processor.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb index ed13d9339..89929d4a4 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb @@ -10,6 +10,7 @@ class SpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor include Singleton SEMANTIC_CONVENTIONS = ::OpenTelemetry::SemanticConventions::Trace + INTERNAL_SPAN_KINDS = %i(client internal) # The mapping from otel span ids to sentry spans # @return [Hash] @@ -29,16 +30,12 @@ def on_start(otel_span, parent_context) sentry_parent_span = @span_map[trace_data.parent_span_id] if trace_data.parent_span_id sentry_span = if sentry_parent_span - Sentry.configuration.logger.info("Continuing otel span #{otel_span.name} on parent #{sentry_parent_span.op}") - sentry_parent_span.start_child( span_id: trace_data.span_id, description: otel_span.name, start_timestamp: otel_span.start_timestamp / 1e9 ) else - Sentry.configuration.logger.info("Starting otel transaction #{otel_span.name}") - options = { instrumenter: :otel, name: otel_span.name, @@ -72,7 +69,6 @@ def on_finish(otel_span) update_span_with_otel_data(sentry_span, otel_span) end - Sentry.configuration.logger.info("Finishing sentry_span #{sentry_span.op}") sentry_span.finish(end_timestamp: otel_span.end_timestamp / 1e9) end @@ -88,7 +84,7 @@ def from_sentry_sdk?(otel_span) if otel_span.name.start_with?("HTTP") # only check client requests, connects are sometimes internal - return false unless %i(client internal).include?(otel_span.kind) + return false unless INTERNAL_SPAN_KINDS.include?(otel_span.kind) address = otel_span.attributes[SEMANTIC_CONVENTIONS::NET_PEER_NAME] From b02c3af4b019cfc94efc58164007199c026393a2 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 1 Dec 2022 11:41:06 +0100 Subject: [PATCH 10/11] Remove from registry target for first publish --- .craft.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.craft.yml b/.craft.yml index 0786e6366..a4972d4c9 100644 --- a/.craft.yml +++ b/.craft.yml @@ -10,5 +10,4 @@ targets: 'gem:sentry-rails': 'gem:sentry-sidekiq': 'gem:sentry-delayed_job': - 'gem:sentry-opentelemetry': - name: github From fb5718a3623546ed751e3b4b9ec8f593a2323264 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 1 Dec 2022 11:51:41 +0100 Subject: [PATCH 11/11] Update top-level README --- sentry-ruby/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry-ruby/README.md b/sentry-ruby/README.md index a0d22378b..e988db8fd 100644 --- a/sentry-ruby/README.md +++ b/sentry-ruby/README.md @@ -20,6 +20,7 @@ Sentry SDK for Ruby | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-sidekiq%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-sidekiq.svg)](https://rubygems.org/gems/sentry-sidekiq/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-delayed_job%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-delayed_job.svg)](https://rubygems.org/gems/sentry-delayed_job/) | | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-resque%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-resque.svg)](https://rubygems.org/gems/sentry-resque/) | +| [![Gem Version](https://img.shields.io/gem/v/sentry-opentelemetry?label=sentry-opentelemetry)](https://rubygems.org/gems/sentry-opentelemetry) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-opentelemetry%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_opentelemetry_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-opentelemetry.svg)](https://rubygems.org/gems/sentry-opentelemetry/) | @@ -51,6 +52,7 @@ gem "sentry-rails" gem "sentry-sidekiq" gem "sentry-delayed_job" gem "sentry-resque" +gem "sentry-opentelemetry" ``` ### Configuration @@ -88,6 +90,7 @@ To learn more about sampling transactions, please visit the [official documentat - [Sidekiq](https://docs.sentry.io/platforms/ruby/guides/sidekiq/) - [DelayedJob](https://docs.sentry.io/platforms/ruby/guides/delayed_job/) - [Resque](https://docs.sentry.io/platforms/ruby/guides/resque/) +- [OpenTemeletry](https://docs.sentry.io/platforms/ruby/performance/instrumentation/opentelemetry/) ### Enriching Events