idempotency 0.2.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79c1c3a64979f50848f56726163b1210fef91763d50b77fd31f7ed87629a14df
4
- data.tar.gz: 45305008439d1db703c550a4a684c44771329a7d2189faba567b94ec673d21d0
3
+ metadata.gz: b90e384bed1ce0ef7d2e3aeb46f4e2f7d6f822e584d805234762e42d05500ef0
4
+ data.tar.gz: 61d0164bd2b498d160b486082298382d6d3ceef04aca2eca7b61c620ae9e0e6c
5
5
  SHA512:
6
- metadata.gz: 9e9db8c50c5abccf8985e813cd096f4b6ea6198a2ad363877d962f0d9703cce77661415eb949e1216803e053dceff5ae8f943a3f66161ea95da02d5f774e15ff
7
- data.tar.gz: 1d3c09a3f939065bc09a5fcc556fbc724c409335824ef92456148541a31b9b50d49139db6049299a4669f8fc7c8d41d9923602132d4cdd97a9431ce16fdf9b19
6
+ metadata.gz: e3e0d18d788ef5a769450e741fecc05a496cecc829034047d3595b8b667b14bee0cac210c94d815058f95382996075c4e6b95d2f5c8856edcb3748ddf97f715f
7
+ data.tar.gz: ee15500014c64c54c7bb6746c6d4c4ee28bf4a6d69d9a47682514fc4d2bc6e48275a7c759907eb625a6a7f0b36baf99e59b5991a1442fdde2ddec4ad1cd6216f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Change Log]
2
2
 
3
+ ## [0.3.0] - 2025-11-14
4
+
5
+ - Add AppSignal integration for transaction tracking in trace stacks
6
+ - Add Sentry integration for transaction tracking in trace stacks
7
+ - Add observability configuration options (appsignal_enabled, sentry_enabled)
8
+
3
9
  ## [0.2.0] - 2025-07-28
4
10
 
5
11
  - Enforce explicit monkey-patch requirement
data/Gemfile CHANGED
@@ -13,3 +13,6 @@ gem 'dry-monitor'
13
13
  gem 'hanami-controller', '~> 1.3'
14
14
  gem 'pry-byebug'
15
15
  gem 'rubocop', '~> 1.21'
16
+
17
+ # Optional observability integrations for testing
18
+ gem 'appsignal', '>= 1.3.0'
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idempotency (0.2.0)
4
+ idempotency (0.3.0)
5
+ appsignal (>= 1.3.0)
5
6
  base64
6
7
  dry-configurable
7
8
  dry-monitor
@@ -11,6 +12,8 @@ PATH
11
12
  GEM
12
13
  remote: https://rubygems.org/
13
14
  specs:
15
+ appsignal (3.13.1)
16
+ rack
14
17
  ast (2.4.2)
15
18
  base64 (0.2.0)
16
19
  byebug (11.1.3)
@@ -97,6 +100,7 @@ PLATFORMS
97
100
  ruby
98
101
 
99
102
  DEPENDENCIES
103
+ appsignal (>= 1.3.0)
100
104
  connection_pool
101
105
  dry-monitor
102
106
  hanami-controller (~> 1.3)
data/README.md CHANGED
@@ -34,6 +34,11 @@ Idempotency.configure do |config|
34
34
  config.metrics.statsd_client = statsd_client # Your StatsD client instance
35
35
  config.metrics.namespace = 'my_service_name' # Optional namespace for metrics
36
36
 
37
+ # APM/Observability configuration (optional) - adds method to trace stacks
38
+ # You can enable one or both observability tools simultaneously
39
+ config.observability.appsignal_enabled = true # Enable AppSignal transaction tracking
40
+ config.observability.sentry_enabled = true # Enable Sentry transaction tracking
41
+
37
42
  # Custom instrumentation listeners (optional)
38
43
  config.instrumentation_listeners = [my_custom_listener] # Array of custom listeners
39
44
  end
@@ -110,7 +115,11 @@ end
110
115
 
111
116
  ### Instrumentation
112
117
 
113
- The gem supports instrumentation through StatsD out of the box. When you configure a StatsD client in the configuration, the StatsdListener will be automatically set up. It tracks the following metrics:
118
+ The gem supports instrumentation through multiple observability platforms:
119
+
120
+ #### StatsD
121
+
122
+ When you configure a StatsD client in the configuration, the StatsdListener will be automatically set up. It tracks the following metrics:
114
123
 
115
124
  - `idempotency_cache_hit_count` - Incremented when a cached response is found
116
125
  - `idempotency_cache_miss_count` - Incremented when no cached response exists
@@ -122,7 +131,7 @@ Each metric includes tags:
122
131
  - `namespace` - Your configured namespace (if provided)
123
132
  - `metric` - The metric name (for duration histogram only)
124
133
 
125
- To enable StatsD instrumentation, simply configure the metrics settings:
134
+ To enable StatsD instrumentation:
126
135
 
127
136
  ```ruby
128
137
  Idempotency.configure do |config|
@@ -130,3 +139,30 @@ Idempotency.configure do |config|
130
139
  config.metrics.namespace = 'my_service_name'
131
140
  end
132
141
  ```
142
+
143
+ #### AppSignal
144
+
145
+ The gem can add the `use_cache` method to AppSignal transaction traces when enabled. This allows you to see the idempotency check as part of your request traces and helps identify performance bottlenecks.
146
+
147
+ To enable AppSignal transaction tracking:
148
+
149
+ ```ruby
150
+ Idempotency.configure do |config|
151
+ config.observability.appsignal_enabled = true
152
+ end
153
+ ```
154
+
155
+ Note: The AppSignal gem must be installed and configured in your application.
156
+
157
+ #### Using Both AppSignal and Sentry
158
+
159
+ You can enable both observability tools simultaneously. When both are enabled, the `use_cache` method will be instrumented in both APM systems with nested transactions:
160
+
161
+ ```ruby
162
+ Idempotency.configure do |config|
163
+ config.observability.appsignal_enabled = true
164
+ config.observability.sentry_enabled = true
165
+ end
166
+ ```
167
+
168
+ This allows you to see the idempotency check in both your AppSignal and Sentry dashboards, providing comprehensive observability across your monitoring stack.
data/idempotency.gemspec CHANGED
@@ -39,4 +39,6 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency 'dry-monitor'
40
40
  spec.add_dependency 'msgpack'
41
41
  spec.add_dependency 'redis'
42
+
43
+ spec.add_dependency 'appsignal', '>= 1.3.0'
42
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Idempotency
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/idempotency.rb CHANGED
@@ -8,7 +8,7 @@ require_relative 'idempotency/constants'
8
8
  require_relative 'idempotency/instrumentation/statsd_listener'
9
9
  require 'dry-monitor'
10
10
 
11
- class Idempotency
11
+ class Idempotency # rubocop:disable Metrics/ClassLength
12
12
  extend Dry::Configurable
13
13
  @monitor = Monitor.new
14
14
 
@@ -28,6 +28,10 @@ class Idempotency
28
28
  setting :statsd_client
29
29
  end
30
30
 
31
+ setting :observability do
32
+ setting :appsignal_enabled, default: false
33
+ end
34
+
31
35
  setting :default_lock_expiry, default: 300 # 5 minutes
32
36
  setting :idempotent_methods, default: %w[POST PUT PATCH DELETE]
33
37
  setting :idempotent_statuses, default: (200..299).to_a + (400..499).to_a
@@ -60,41 +64,46 @@ class Idempotency
60
64
  new.use_cache(request, request_identifiers, lock_duration:, action:, &blk)
61
65
  end
62
66
 
63
- def use_cache(request, request_identifiers, lock_duration: nil, action: nil) # rubocop:disable Metrics/AbcSize
67
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
68
+ def use_cache(request, request_identifiers, lock_duration: nil, action: nil)
64
69
  duration_start = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
70
+ action_name = action || "#{request.request_method}:#{request.path}"
65
71
 
66
- return yield unless cache_request?(request)
72
+ with_apm_instrumentation('idempotency.use_cache', action_name) do
73
+ return yield unless cache_request?(request)
67
74
 
68
- request_headers = request.env
69
- idempotency_key = unquote(request_headers[Constants::RACK_HEADER_KEY] || SecureRandom.hex)
75
+ request_headers = request.env
76
+ idempotency_key = unquote(request_headers[Constants::RACK_HEADER_KEY] || SecureRandom.hex)
70
77
 
71
- fingerprint = calculate_fingerprint(request, idempotency_key, request_identifiers)
78
+ fingerprint = calculate_fingerprint(request, idempotency_key, request_identifiers)
72
79
 
73
- cached_response = cache.get(fingerprint)
80
+ cached_response = cache.get(fingerprint)
74
81
 
75
- if (cached_status, cached_headers, cached_body = cached_response)
76
- cached_headers.merge!(Constants::HEADER_KEY => idempotency_key)
77
- instrument(Events::CACHE_HIT, request:, action:, duration: calculate_duration(duration_start))
82
+ if (cached_status, cached_headers, cached_body = cached_response)
83
+ cached_headers.merge!(Constants::HEADER_KEY => idempotency_key)
84
+ instrument(Events::CACHE_HIT, request:, action:, duration: calculate_duration(duration_start))
78
85
 
79
- return [cached_status, cached_headers, cached_body]
80
- end
86
+ return [cached_status, cached_headers, cached_body]
87
+ end
81
88
 
82
- lock_duration ||= config.default_lock_expiry
83
- response_status, response_headers, response_body = cache.with_lock(fingerprint, lock_duration) do
84
- yield
85
- end
89
+ lock_duration ||= config.default_lock_expiry
90
+ response_status, response_headers, response_body = cache.with_lock(fingerprint, lock_duration) do
91
+ yield
92
+ end
86
93
 
87
- if cache_response?(response_status)
88
- cache.set(fingerprint, response_status, response_headers, response_body)
89
- response_headers.merge!({ Constants::HEADER_KEY => idempotency_key })
90
- end
94
+ if cache_response?(response_status)
95
+ cache.set(fingerprint, response_status, response_headers, response_body)
96
+ response_headers.merge!({ Constants::HEADER_KEY => idempotency_key })
97
+ end
91
98
 
92
- instrument(Events::CACHE_MISS, request:, action:, duration: calculate_duration(duration_start))
93
- [response_status, response_headers, response_body]
99
+ instrument(Events::CACHE_MISS, request:, action:, duration: calculate_duration(duration_start))
100
+ [response_status, response_headers, response_body]
101
+ end
94
102
  rescue Idempotency::Cache::LockConflict
95
103
  instrument(Events::LOCK_CONFLICT, request:, action:, duration: calculate_duration(duration_start))
96
104
  [409, {}, config.response_body.concurrent_error]
97
105
  end
106
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
98
107
 
99
108
  private
100
109
 
@@ -137,4 +146,14 @@ class Idempotency
137
146
  str
138
147
  end
139
148
  end
149
+
150
+ def with_apm_instrumentation(name, action, &)
151
+ if config.observability.appsignal_enabled
152
+ Appsignal.instrument(name, action) do
153
+ yield
154
+ end
155
+ else
156
+ yield
157
+ end
158
+ end
140
159
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idempotency
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vu Hoang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-28 00:00:00.000000000 Z
11
+ date: 2025-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: appsignal
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 1.3.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.3.0
83
97
  description: Caching requests for idempotency purpose
84
98
  email: vu.hoang@ascenda.com
85
99
  executables: []