spree_opentelemetry 6.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 87b0ae131588d75e378a7faa635e9ad618313efefe73cebb5ad19864518ffcb9
4
+ data.tar.gz: b175bc98dc74a568114db63f62052d7ce3855a775cb25a62b71bac4f48ba75e4
5
+ SHA512:
6
+ metadata.gz: bbefa2928193a25801ea3d99b432a48e52d3ff5a95de592a48648b0fbf227598306ff3b54582679e94a315e57b6ed1c9a9abc4245746a3c7725d5c3e9d0b8ec7
7
+ data.tar.gz: 895c1a90875fb5a049c5653ddd507f51dd28f0d853fd07d172c63e7884fe3677e3340f8fc6edaac05ccd181b2217a873129f92e4905029a70bad940d0e9bdf1e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present, Vendo Sp. z o.o., Vendo Connect Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # spree_opentelemetry
2
+
3
+ Distributed tracing for [Spree Commerce](https://spreecommerce.org) via
4
+ [OpenTelemetry](https://opentelemetry.io).
5
+
6
+ With this gem installed, Spree exports traces as soon as the standard
7
+ OpenTelemetry environment variables point at a collector — no code changes:
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem 'spree_opentelemetry'
12
+ ```
13
+
14
+ ```bash
15
+ OTEL_SERVICE_NAME=spree
16
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
17
+ ```
18
+
19
+ Without an exporter configured, the gem stays dormant. `OTEL_SDK_DISABLED=true`
20
+ is the kill switch.
21
+
22
+ ## What gets traced
23
+
24
+ - **Framework layers** via the official Rails auto-instrumentation: HTTP
25
+ requests (Rack/Action Pack), database queries (Active Record), background
26
+ jobs (Active Job, with trace context carried across the enqueue → perform
27
+ boundary), mail delivery, outbound HTTP (Net::HTTP).
28
+ - **Spree commerce layers** via Spree's own notification surface: workflow
29
+ runs and steps (external steps become `client` spans), extension hook
30
+ dispatch, event subscriber dispatch, webhook deliveries (with W3C
31
+ `traceparent` propagated to the receiving system), and payment gateway
32
+ calls.
33
+
34
+ Span attributes never contain personal data — prefixed IDs, workflow/step
35
+ names, gateway action names, and HTTP metadata only.
36
+
37
+ See the [telemetry guide](https://docs.spreecommerce.org/developer/deployment/telemetry)
38
+ for the full span catalog, collector examples, and sampling guidance.
39
+
40
+ ## Code-level configuration
41
+
42
+ Only needed for what env vars can't express:
43
+
44
+ ```ruby
45
+ # config/initializers/opentelemetry.rb
46
+ SpreeOpenTelemetry.configure do |config|
47
+ config.use 'OpenTelemetry::Instrumentation::Redis' # extra instrumentation
48
+ config.skip 'OpenTelemetry::Instrumentation::ActionMailer' # drop a default
49
+ config.with_sdk { |otel| otel.add_span_processor(my_processor) }
50
+ end
51
+ ```
52
+
53
+ ## License
54
+
55
+ Spree is released under the [New BSD License](https://github.com/spree/spree/blob/main/license.md).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/common_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task default: :spec
10
+
11
+ desc "Generates a dummy app for testing"
12
+ task :test_app do
13
+ ENV['LIB_NAME'] = 'spree_opentelemetry'
14
+ Rake::Task['common:test_app'].invoke
15
+ end
@@ -0,0 +1,61 @@
1
+ module SpreeOpenTelemetry
2
+ # Code-level configuration, for the few things env vars can't express.
3
+ # Set from a host initializer:
4
+ #
5
+ # SpreeOpenTelemetry.configure do |config|
6
+ # config.service_name = 'storefront-api'
7
+ # config.use 'OpenTelemetry::Instrumentation::Redis'
8
+ # config.skip 'OpenTelemetry::Instrumentation::ActionMailer'
9
+ # config.with_sdk { |otel| otel.add_span_processor(my_processor) }
10
+ # end
11
+ class Configuration
12
+ DEFAULT_INSTRUMENTATIONS = {
13
+ 'OpenTelemetry::Instrumentation::Rack' => {},
14
+ 'OpenTelemetry::Instrumentation::ActionPack' => {},
15
+ 'OpenTelemetry::Instrumentation::ActionMailer' => {},
16
+ 'OpenTelemetry::Instrumentation::ActiveRecord' => {},
17
+ 'OpenTelemetry::Instrumentation::ActiveSupport' => {},
18
+ # :link keeps a checkout trace from stretching until the last webhook
19
+ # retry finishes — background work links to the enqueuing trace instead
20
+ # of continuing it.
21
+ 'OpenTelemetry::Instrumentation::ActiveJob' => { propagation_style: :link },
22
+ 'OpenTelemetry::Instrumentation::ConcurrentRuby' => {},
23
+ 'OpenTelemetry::Instrumentation::Net::HTTP' => {},
24
+ }.freeze
25
+
26
+ # Force telemetry on/off regardless of exporter env vars; nil (default)
27
+ # means env-driven activation.
28
+ attr_accessor :enabled
29
+
30
+ # Overrides OTEL_SERVICE_NAME when set.
31
+ attr_accessor :service_name
32
+
33
+ attr_reader :instrumentations, :sdk_hooks
34
+
35
+ def initialize
36
+ @enabled = nil
37
+ @service_name = nil
38
+ @instrumentations = DEFAULT_INSTRUMENTATIONS.transform_values(&:dup)
39
+ @sdk_hooks = []
40
+ end
41
+
42
+ # Adds (or reconfigures) an instrumentation installed at SDK boot.
43
+ #
44
+ # @param name [String] instrumentation class name, e.g. 'OpenTelemetry::Instrumentation::Redis'
45
+ # @param config [Hash] instrumentation-specific options
46
+ def use(name, config = {})
47
+ @instrumentations[name.to_s] = config
48
+ end
49
+
50
+ # Removes an instrumentation from the install list.
51
+ def skip(name)
52
+ @instrumentations.delete(name.to_s)
53
+ end
54
+
55
+ # Registers a block run inside OpenTelemetry::SDK.configure — the escape
56
+ # hatch for samplers, extra span processors, resource attributes.
57
+ def with_sdk(&block)
58
+ @sdk_hooks << block
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/engine'
2
+
3
+ module SpreeOpenTelemetry
4
+ class Engine < Rails::Engine
5
+ engine_name 'spree_opentelemetry'
6
+
7
+ # After the host's initializers so a SpreeOpenTelemetry.configure block
8
+ # is applied before the SDK boots.
9
+ initializer 'spree_opentelemetry.install', after: :load_config_initializers do
10
+ SpreeOpenTelemetry.install!
11
+ end
12
+
13
+ # DeliverWebhook is reloadable — its decorator list resets with the class
14
+ # on every code reload, so registration must happen in to_prepare.
15
+ config.to_prepare do
16
+ if SpreeOpenTelemetry.installed? && defined?(Spree::Webhooks::DeliverWebhook)
17
+ decorators = Spree::Webhooks::DeliverWebhook.header_decorators
18
+ decorators << SpreeOpenTelemetry::WebhookTracePropagation unless decorators.include?(SpreeOpenTelemetry::WebhookTracePropagation)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ module SpreeOpenTelemetry
2
+ # Turns one ActiveSupport::Notifications event into a span. Evented
3
+ # (start/finish) rather than block-subscribed, so the span is current while
4
+ # the instrumented work runs and children nest under it correctly. The span
5
+ # handle rides in the notification payload — the same trick the upstream
6
+ # ActiveSupport instrumentation uses.
7
+ class SpanSubscriber
8
+ PAYLOAD_KEY = :__spree_opentelemetry_span
9
+
10
+ # Workflow control-flow signals inherit Exception by design (a plain
11
+ # rescue must not swallow them). Halted is a successful early exit and
12
+ # must not mark the span errored; FailureSignal is a real failure but is
13
+ # not worth an exception event — the error status carries the verdict.
14
+ HALT_EXCEPTIONS = %w[Spree::Workflow::Halted].freeze
15
+ CONTROL_FLOW_EXCEPTIONS = %w[Spree::Workflow::FailureSignal Spree::Workflow::Halted].freeze
16
+
17
+ # @param tracer [OpenTelemetry::Trace::Tracer]
18
+ # @param name [Proc] payload -> span name
19
+ # @param kind [Proc, nil] payload -> :internal/:client/…; defaults to :internal
20
+ # @param attributes [Proc, nil] payload -> attribute hash (nil values dropped)
21
+ # @param skip [Proc, nil] payload -> true suppresses the span entirely
22
+ # @param error [Proc, nil] payload -> error message when the span failed
23
+ # without an exception (e.g. a failure result), nil otherwise
24
+ def initialize(tracer:, name:, kind: nil, attributes: nil, skip: nil, error: nil)
25
+ @tracer = tracer
26
+ @name = name
27
+ @kind = kind
28
+ @attributes = attributes
29
+ @skip = skip
30
+ @error = error
31
+ end
32
+
33
+ # Instrumentation must never break the instrumented work: a raising
34
+ # name/kind/attributes proc is reported through the OpenTelemetry error
35
+ # handler instead of propagating, and finish/detach always run so a bad
36
+ # proc cannot leak an unfinished span or a stuck context onto the thread.
37
+ # @param _event_name [String] notification name (unused)
38
+ # @param _id [String] notification instrumenter id (unused)
39
+ # @param payload [Hash] notification payload; the span handle is stored on it
40
+ # @return [void]
41
+ def start(_event_name, _id, payload)
42
+ return if @skip&.call(payload)
43
+
44
+ span = @tracer.start_span(
45
+ @name.call(payload),
46
+ kind: @kind ? @kind.call(payload) : :internal
47
+ )
48
+ token = ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(span))
49
+ payload[PAYLOAD_KEY] = [span, token]
50
+ rescue StandardError => error
51
+ ::OpenTelemetry.handle_error(exception: error, message: "SpreeOpenTelemetry failed to start span #{@name.inspect}")
52
+ span&.finish if payload[PAYLOAD_KEY].nil?
53
+ end
54
+
55
+ # @param _event_name [String] notification name (unused)
56
+ # @param _id [String] notification instrumenter id (unused)
57
+ # @param payload [Hash] notification payload; the span handle is removed from it
58
+ # @return [void]
59
+ def finish(_event_name, _id, payload)
60
+ span, token = payload.delete(PAYLOAD_KEY)
61
+ return unless span
62
+
63
+ begin
64
+ @attributes&.call(payload)&.each do |key, value|
65
+ span.set_attribute(key, value) unless value.nil?
66
+ end
67
+ record_outcome(span, payload)
68
+ rescue StandardError => error
69
+ ::OpenTelemetry.handle_error(exception: error, message: 'SpreeOpenTelemetry failed to finalize span')
70
+ ensure
71
+ span.finish
72
+ ::OpenTelemetry::Context.detach(token)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def record_outcome(span, payload)
79
+ exception = payload[:exception_object]
80
+
81
+ if exception && !HALT_EXCEPTIONS.include?(exception.class.name)
82
+ span.record_exception(exception) unless CONTROL_FLOW_EXCEPTIONS.include?(exception.class.name)
83
+ span.status = ::OpenTelemetry::Trace::Status.error(exception.message.to_s)
84
+ elsif (message = @error&.call(payload))
85
+ span.status = ::OpenTelemetry::Trace::Status.error(message.to_s)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,145 @@
1
+ require 'active_support/notifications'
2
+
3
+ module SpreeOpenTelemetry
4
+ # Attaches one SpanSubscriber per Spree notification family — the
5
+ # commerce-domain layer on top of the framework auto-instrumentation.
6
+ # Span names come from bounded vocabularies (workflow keys, step names,
7
+ # gateway actions, event names); attributes are PII-safe by construction
8
+ # because the core notification payloads are (see
9
+ # docs/plans/6.0-opentelemetry.md).
10
+ module Subscribers
11
+ class << self
12
+ # Subscribes one span source per Spree notification family. Idempotent,
13
+ # so a code reload cannot double-subscribe.
14
+ #
15
+ # @return [void]
16
+ def attach!
17
+ return if attached?
18
+
19
+ tracer = SpreeOpenTelemetry.tracer
20
+ @subscriptions = definitions(tracer).map do |notification_name, span_subscriber|
21
+ ActiveSupport::Notifications.subscribe(notification_name, span_subscriber)
22
+ end
23
+ @subscriptions << domain_event_subscription
24
+ end
25
+
26
+ # @return [void]
27
+ def detach!
28
+ Array(@subscriptions).each { |subscription| ActiveSupport::Notifications.unsubscribe(subscription) }
29
+ @subscriptions = nil
30
+ end
31
+
32
+ # @return [Boolean] whether the span sources are currently subscribed
33
+ def attached?
34
+ !@subscriptions.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def definitions(tracer)
40
+ {
41
+ 'perform.spree_workflow' => SpanSubscriber.new(
42
+ tracer: tracer,
43
+ name: ->(payload) { payload[:workflow] },
44
+ attributes: lambda do |payload|
45
+ {
46
+ 'spree.workflow' => payload[:workflow],
47
+ 'spree.workflow.outcome' => payload[:outcome]
48
+ }
49
+ end,
50
+ error: ->(payload) { 'workflow failed' if %w[failure error].include?(payload[:outcome]) }
51
+ ),
52
+
53
+ 'step.spree_workflow' => SpanSubscriber.new(
54
+ tracer: tracer,
55
+ name: ->(payload) { "#{payload[:workflow]} #{payload[:step]}" },
56
+ kind: ->(payload) { payload[:external] ? :client : :internal },
57
+ attributes: lambda do |payload|
58
+ {
59
+ 'spree.workflow' => payload[:workflow],
60
+ 'spree.workflow.step' => payload[:step].to_s,
61
+ 'spree.workflow.step.external' => payload[:external] == true,
62
+ 'spree.workflow.outcome' => payload[:outcome]
63
+ }
64
+ end,
65
+ error: ->(payload) { 'step failed' if payload[:outcome] == 'failure' }
66
+ ),
67
+
68
+ 'hooks.spree_workflow' => SpanSubscriber.new(
69
+ tracer: tracer,
70
+ name: ->(payload) { "#{payload[:workflow]} hooks #{payload[:hook]}" },
71
+ skip: ->(payload) { payload[:handler_count].to_i.zero? },
72
+ attributes: lambda do |payload|
73
+ {
74
+ 'spree.workflow' => payload[:workflow],
75
+ 'spree.hook' => payload[:hook].to_s,
76
+ 'spree.hook.handler_count' => payload[:handler_count]
77
+ }
78
+ end
79
+ ),
80
+
81
+ 'dispatch.spree_events' => SpanSubscriber.new(
82
+ tracer: tracer,
83
+ name: ->(payload) { "#{payload[:event_name]} dispatch" },
84
+ attributes: lambda do |payload|
85
+ {
86
+ 'spree.event.name' => payload[:event_name],
87
+ 'spree.event.subscriber' => payload[:subscriber],
88
+ 'spree.event.async' => payload[:async] == true
89
+ }
90
+ end
91
+ ),
92
+
93
+ 'deliver.spree_webhooks' => SpanSubscriber.new(
94
+ tracer: tracer,
95
+ name: ->(payload) { "spree.webhook.deliver #{payload[:event_name]}" },
96
+ kind: ->(_payload) { :client },
97
+ attributes: lambda do |payload|
98
+ {
99
+ 'spree.webhook.event' => payload[:event_name],
100
+ 'server.address' => payload[:url_host],
101
+ 'http.response.status_code' => payload[:response_code],
102
+ 'spree.webhook.error_type' => payload[:error_type]
103
+ }
104
+ end,
105
+ error: lambda do |payload|
106
+ next payload[:error_type] if payload[:error_type]
107
+ next "HTTP #{payload[:response_code]}" if payload[:response_code].to_i >= 400
108
+
109
+ nil
110
+ end
111
+ ),
112
+
113
+ 'gateway.spree_payments' => SpanSubscriber.new(
114
+ tracer: tracer,
115
+ name: ->(payload) { "spree.gateway.#{payload[:action]}" },
116
+ kind: ->(_payload) { :client },
117
+ attributes: lambda do |payload|
118
+ {
119
+ 'spree.gateway.action' => payload[:action],
120
+ 'spree.gateway.payment_method_type' => payload[:payment_method_type]
121
+ }
122
+ end
123
+ )
124
+ }
125
+ end
126
+
127
+ # Spree domain events ('order.placed' and friends, published as
128
+ # '<name>.spree' with an intentionally empty instrumented block) wrap no
129
+ # work — they surface as span events on the current span, never as
130
+ # spans. Payloads are deliberately excluded: only the name and event id
131
+ # are attached.
132
+ def domain_event_subscription
133
+ ActiveSupport::Notifications.subscribe(/\.spree$/) do |*args|
134
+ event = args.last[:event]
135
+ next unless event
136
+
137
+ ::OpenTelemetry::Trace.current_span.add_event(
138
+ "spree.event #{event.name}",
139
+ attributes: { 'spree.event.id' => event.id.to_s }
140
+ )
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,19 @@
1
+ module SpreeOpenTelemetry
2
+ # DeliverWebhook header decorator — injects W3C trace context
3
+ # (traceparent/tracestate) into outbound webhook requests so the merchant's
4
+ # receiving system can join the trace.
5
+ module WebhookTracePropagation
6
+ # Trace context only, never the process-global propagator: that one is a
7
+ # composite that also injects W3C baggage, and baggage carries arbitrary
8
+ # application context to whatever third-party endpoint the merchant
9
+ # configured.
10
+ #
11
+ # @param headers [Hash] outbound request headers, mutated in place
12
+ # @param _delivery [Spree::WebhookDelivery] unused; part of the decorator contract
13
+ # @return [Hash] the same headers hash
14
+ def self.call(headers, _delivery)
15
+ ::OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator.inject(headers)
16
+ headers
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,120 @@
1
+ require 'spree_core'
2
+
3
+ require 'opentelemetry/sdk'
4
+ require 'opentelemetry-exporter-otlp'
5
+ require 'opentelemetry-instrumentation-rack'
6
+ require 'opentelemetry-instrumentation-action_pack'
7
+ require 'opentelemetry-instrumentation-action_mailer'
8
+ require 'opentelemetry-instrumentation-active_record'
9
+ require 'opentelemetry-instrumentation-active_support'
10
+ require 'opentelemetry-instrumentation-active_job'
11
+ require 'opentelemetry-instrumentation-concurrent_ruby'
12
+ require 'opentelemetry-instrumentation-net_http'
13
+
14
+ require 'spree_opentelemetry/configuration'
15
+ require 'spree_opentelemetry/span_subscriber'
16
+ require 'spree_opentelemetry/subscribers'
17
+ require 'spree_opentelemetry/webhook_trace_propagation'
18
+ require 'spree_opentelemetry/engine'
19
+
20
+ # Optional OpenTelemetry support for Spree (docs/plans/6.0-opentelemetry.md).
21
+ #
22
+ # Activation is environment-driven, the way an SRE configures any other
23
+ # OpenTelemetry service: with the gem installed, traces flow as soon as a
24
+ # standard OTEL_* exporter variable is set, and nothing happens without one.
25
+ # `SpreeOpenTelemetry.configure` exists only for code-level needs (extra
26
+ # instrumentations, custom samplers) — telemetry is deployment configuration,
27
+ # never store data.
28
+ module SpreeOpenTelemetry
29
+ # Env vars whose presence opts this process into exporting traces —
30
+ # signal-specific endpoint first, matching the SDK's own precedence.
31
+ EXPORTER_ENV_KEYS = %w[
32
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
33
+ OTEL_EXPORTER_OTLP_ENDPOINT
34
+ ].freeze
35
+
36
+ class << self
37
+ # @return [SpreeOpenTelemetry::Configuration] the process-wide configuration
38
+ def configuration
39
+ @configuration ||= Configuration.new
40
+ end
41
+
42
+ # @yieldparam configuration [SpreeOpenTelemetry::Configuration]
43
+ # @return [SpreeOpenTelemetry::Configuration]
44
+ def configure
45
+ yield configuration
46
+ configuration
47
+ end
48
+
49
+ # Whether telemetry should activate in this process. Dormant unless an
50
+ # exporter is configured through the standard OpenTelemetry env vars (or
51
+ # forced on via `configure { |c| c.enabled = true }` — the path
52
+ # integrations like Sentry's OTLP mode use, since they register their own
53
+ # exporter); OTEL_SDK_DISABLED is the kill switch and always wins.
54
+ #
55
+ # @return [Boolean]
56
+ def enabled?
57
+ # OpenTelemetry booleans are case-insensitive, so OTEL_SDK_DISABLED=TRUE
58
+ # must silence telemetry exactly like the lowercase spelling.
59
+ return false if ENV['OTEL_SDK_DISABLED'].to_s.casecmp?('true')
60
+ return configuration.enabled unless configuration.enabled.nil?
61
+ return true if EXPORTER_ENV_KEYS.any? { |key| !ENV[key].to_s.empty? }
62
+
63
+ exporter = ENV['OTEL_TRACES_EXPORTER'].to_s
64
+ !exporter.empty? && exporter != 'none'
65
+ end
66
+
67
+ # @return [Boolean] whether {#install!} has already run in this process
68
+ def installed?
69
+ @installed == true
70
+ end
71
+
72
+ # Boots the OpenTelemetry SDK and attaches the Spree span subscribers.
73
+ # No-op when {#enabled?} is false — the opentelemetry-api layer stays a
74
+ # zero-cost no-op in that case. Idempotent.
75
+ #
76
+ # @return [Boolean] whether telemetry is installed after the call
77
+ def install!
78
+ return true if installed?
79
+ return false unless enabled?
80
+
81
+ ::OpenTelemetry::SDK.configure do |otel_config|
82
+ otel_config.service_name = configuration.service_name if configuration.service_name
83
+ configuration.instrumentations.each do |instrumentation_name, instrumentation_config|
84
+ otel_config.use(instrumentation_name, instrumentation_config)
85
+ end
86
+ configuration.sdk_hooks.each { |hook| hook.call(otel_config) }
87
+ end
88
+
89
+ Subscribers.attach!
90
+ @installed = true
91
+ log_activation
92
+ true
93
+ end
94
+
95
+ # @return [OpenTelemetry::Trace::Tracer] the tracer Spree spans are created on
96
+ def tracer
97
+ version = defined?(Spree) && Spree.respond_to?(:version) ? Spree.version : nil
98
+ ::OpenTelemetry.tracer_provider.tracer('spree', version)
99
+ end
100
+
101
+ # Test-only: forget installation state and configuration.
102
+ #
103
+ # @return [void]
104
+ def reset!
105
+ Subscribers.detach!
106
+ @installed = nil
107
+ @configuration = nil
108
+ end
109
+
110
+ private
111
+
112
+ def log_activation
113
+ return unless defined?(Rails) && Rails.logger
114
+
115
+ exporter = ENV['OTEL_TRACES_EXPORTER'].to_s.empty? ? 'otlp' : ENV['OTEL_TRACES_EXPORTER']
116
+ endpoint = EXPORTER_ENV_KEYS.filter_map { |key| ENV[key] unless ENV[key].to_s.empty? }.first || '(default)'
117
+ Rails.logger.info "[Spree OpenTelemetry] traces active — exporter=#{exporter} endpoint=#{endpoint}"
118
+ end
119
+ end
120
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree_opentelemetry
3
+ version: !ruby/object:Gem::Version
4
+ version: 6.0.0.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Vendo Connect Inc.
8
+ - Vendo Sp. z o.o.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-09-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: spree_core
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '='
19
+ - !ruby/object:Gem::Version
20
+ version: 6.0.0.beta1
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '='
26
+ - !ruby/object:Gem::Version
27
+ version: 6.0.0.beta1
28
+ - !ruby/object:Gem::Dependency
29
+ name: opentelemetry-sdk
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: opentelemetry-exporter-otlp
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.26'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.26'
56
+ - !ruby/object:Gem::Dependency
57
+ name: opentelemetry-instrumentation-rack
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: opentelemetry-instrumentation-action_pack
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: opentelemetry-instrumentation-action_mailer
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: opentelemetry-instrumentation-active_record
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: opentelemetry-instrumentation-active_support
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: opentelemetry-instrumentation-active_job
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: opentelemetry-instrumentation-concurrent_ruby
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :runtime
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: opentelemetry-instrumentation-net_http
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :runtime
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ description: Distributed tracing for Spree — boots the OpenTelemetry SDK from standard
169
+ OTEL_* environment variables and turns Spree workflow, event, webhook and payment-gateway
170
+ instrumentation into spans
171
+ email: hello@spreecommerce.org
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - LICENSE
177
+ - README.md
178
+ - Rakefile
179
+ - lib/spree_opentelemetry.rb
180
+ - lib/spree_opentelemetry/configuration.rb
181
+ - lib/spree_opentelemetry/engine.rb
182
+ - lib/spree_opentelemetry/span_subscriber.rb
183
+ - lib/spree_opentelemetry/subscribers.rb
184
+ - lib/spree_opentelemetry/webhook_trace_propagation.rb
185
+ homepage: https://spreecommerce.org
186
+ licenses:
187
+ - MIT
188
+ metadata:
189
+ bug_tracker_uri: https://github.com/spree/spree/issues
190
+ changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.beta1
191
+ documentation_uri: https://docs.spreecommerce.org/
192
+ source_code_uri: https://github.com/spree/spree/tree/v6.0.0.beta1
193
+ post_install_message:
194
+ rdoc_options: []
195
+ require_paths:
196
+ - lib
197
+ required_ruby_version: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '3.2'
202
+ required_rubygems_version: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: '0'
207
+ requirements: []
208
+ rubygems_version: 3.5.22
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: OpenTelemetry instrumentation for Spree eCommerce platform
212
+ test_files: []