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 +7 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/Rakefile +15 -0
- data/lib/spree_opentelemetry/configuration.rb +61 -0
- data/lib/spree_opentelemetry/engine.rb +22 -0
- data/lib/spree_opentelemetry/span_subscriber.rb +89 -0
- data/lib/spree_opentelemetry/subscribers.rb +145 -0
- data/lib/spree_opentelemetry/webhook_trace_propagation.rb +19 -0
- data/lib/spree_opentelemetry.rb +120 -0
- metadata +212 -0
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: []
|