axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4

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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
@@ -1,9 +1,9 @@
1
1
  # Configuration
2
2
 
3
- Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call `Action.configure` to adjust a few global settings.
3
+ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call `Axn.configure` to adjust a few global settings.
4
4
 
5
5
  ```ruby
6
- Action.configure do |c|
6
+ Axn.configure do |c|
7
7
  c.log_level = :info
8
8
  c.logger = ...
9
9
  c.on_exception = proc do |e, action:, context:|
@@ -22,7 +22,7 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
22
22
  For example, if you're using Honeybadger this could look something like:
23
23
 
24
24
  ```ruby
25
- Action.configure do |c|
25
+ Axn.configure do |c|
26
26
  c.on_exception = proc do |e, action:, context:|
27
27
  message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
28
28
 
@@ -53,54 +53,228 @@ A couple notes:
53
53
  * `context` will contain the arguments passed to the `action`, _but_ any marked as sensitive (e.g. `expects :foo, sensitive: true`) will be filtered out in the logs.
54
54
  * If your handler raises, the failure will _also_ be swallowed and logged
55
55
  * This handler is global across _all_ Axns. You can also specify per-Action handlers via [the class-level declaration](/reference/class#on-exception).
56
+ * The `context` hash may contain complex objects (like ActiveRecord models, `ActionController::Parameters`, or `Axn::FormObject` instances) that aren't easily serialized by error tracking systems. See [Formatting Context for Error Tracking Systems](/recipes/formatting-context-for-error-tracking) for a recipe to convert these to readable formats.
56
57
 
57
- ## `wrap_with_trace` and `emit_metrics`
58
+ ### Adding Additional Context to Exception Logging
58
59
 
59
- If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Action calls and/or emitting count metrics after each call completes.
60
+ When processing records in a loop or performing batch operations, you may want to include additional context (like which record is being processed) in exception logs. You can do this in two ways:
60
61
 
61
- The framework provides two distinct hooks for observability:
62
+ **Option 1: Explicit setter** - Call `set_logging_context` during execution:
62
63
 
63
- - **`wrap_with_trace`**: An around hook that wraps the entire action execution. You MUST call the provided block to execute the action.
64
- - **`emit_metrics`**: A post-execution hook that receives the action result. Do NOT call any blocks.
64
+ ```ruby
65
+ class ProcessPendingRecords
66
+ include Axn
67
+
68
+ def call
69
+ pending_records.each do |record|
70
+ set_logging_context(current_record_id: record.id, batch_index: @index)
71
+ # ... process record ...
72
+ end
73
+ end
74
+ end
75
+ ```
65
76
 
66
- For example, to wire up Datadog:
77
+ **Option 2: Hook method** - Define a private `additional_logging_context` method that returns a hash:
67
78
 
68
79
  ```ruby
69
- Action.configure do |c|
70
- c.wrap_with_trace = proc do |resource, &action|
71
- Datadog::Tracing.trace("Action", resource:) do
72
- action.call
73
- end
80
+ class ProcessPendingRecords
81
+ include Axn
82
+
83
+ def call
84
+ pending_records.each do |record|
85
+ @current_record = record
86
+ # ... process record ...
74
87
  end
88
+ end
89
+
90
+ private
91
+
92
+ def additional_logging_context
93
+ return {} unless @current_record
94
+
95
+ {
96
+ current_record_id: @current_record.id,
97
+ record_type: @current_record.class.name
98
+ }
99
+ end
100
+ end
101
+ ```
102
+
103
+ Both approaches can be used together - they will be merged. The additional context is **only** included in exception logging (not in normal pre/post execution logs), and is evaluated lazily (the hook method is only called when an exception occurs).
104
+
105
+ Action-specific `on_exception` handlers can also access this context by calling `context_for_logging` directly:
106
+
107
+ ```ruby
108
+ class ProcessPendingRecords
109
+ include Axn
110
+
111
+ on_exception do |exception:|
112
+ log "Failed with this extra context: #{context_for_logging}"
113
+ # ... handle exception with context ...
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## `raise_piping_errors_in_dev`
119
+
120
+ By default, errors that occur in framework code (e.g., in logging hooks, exception handlers, validators, or other user-provided callbacks) are swallowed and logged to prevent them from interfering with the main action execution. In development, you can opt-in to have these errors raised instead of logged:
121
+
122
+ ```ruby
123
+ Axn.configure do |c|
124
+ c.raise_piping_errors_in_dev = true
125
+ end
126
+ ```
127
+
128
+ **Important notes:**
129
+ - This setting only applies in the development environment—errors are always swallowed in test and production
130
+ - Test and production environments behave identically (errors swallowed), ensuring tests verify actual production behavior
131
+ - When enabled in development, errors in framework code (like logging hooks, exception handlers, validators) will be raised instead of logged, putting issues front and center during manual testing
132
+
133
+ ## OpenTelemetry Tracing
134
+
135
+ Axn automatically creates OpenTelemetry spans for all action executions when OpenTelemetry is available. The framework creates a span named `"axn.call"` with the following attributes:
136
+
137
+ - `axn.resource`: The action class name (e.g., `"UserManagement::CreateUser"`)
138
+ - `axn.outcome`: The execution outcome (`"success"`, `"failure"`, or `"exception"`)
139
+
140
+ When an action fails or raises an exception, the span is marked as an error with the exception details recorded.
141
+
142
+ ### Basic Setup
143
+
144
+ If you just want OpenTelemetry spans (without sending to an APM provider), install the API gem:
145
+
146
+ ```ruby
147
+ # Gemfile
148
+ gem "opentelemetry-api"
149
+ ```
150
+
151
+ Then configure a tracer provider:
152
+
153
+ ```ruby
154
+ # config/initializers/opentelemetry.rb
155
+ require "opentelemetry-sdk"
156
+
157
+ OpenTelemetry::SDK.configure do |c|
158
+ c.service_name = "my-app"
159
+ end
160
+ ```
161
+
162
+ ### Datadog Integration
163
+
164
+ To send OpenTelemetry spans to Datadog APM, you need both the OpenTelemetry SDK and the Datadog bridge. The bridge intercepts `OpenTelemetry::SDK.configure` and routes spans to Datadog's tracer.
165
+
166
+ **1. Add the required gems:**
167
+
168
+ ```ruby
169
+ # Gemfile
170
+ gem "datadog" # Datadog APM
171
+ gem "opentelemetry-api" # OpenTelemetry API
172
+ gem "opentelemetry-sdk" # OpenTelemetry SDK (required for Datadog bridge)
173
+ ```
174
+
175
+ **2. Configure Datadog first, then OpenTelemetry:**
176
+
177
+ The order matters — Datadog must be configured before loading the OpenTelemetry bridge, and `OpenTelemetry::SDK.configure` must be called after the bridge is loaded.
178
+
179
+ ```ruby
180
+ # config/initializers/datadog.rb (use a filename that loads early, e.g., 00_datadog.rb)
181
+
182
+ # 1. Configure Datadog first
183
+ Datadog.configure do |c|
184
+ c.env = Rails.env
185
+ c.service = "my-app"
186
+ c.tracing.enabled = Rails.env.production? || Rails.env.staging?
187
+ c.tracing.instrument :rails
188
+ end
189
+
190
+ # 2. Load the OpenTelemetry SDK and Datadog bridge
191
+ require "opentelemetry-api"
192
+ require "opentelemetry-sdk"
193
+ require "datadog/opentelemetry"
194
+
195
+ # 3. Configure OpenTelemetry SDK (Datadog intercepts this)
196
+ OpenTelemetry::SDK.configure do |c|
197
+ c.service_name = "my-app"
198
+ end
199
+ ```
200
+
201
+ ::: warning Important
202
+ The `opentelemetry-sdk` gem is required — not just `opentelemetry-api`. The Datadog bridge only activates when `OpenTelemetry::SDK` is defined and `OpenTelemetry::SDK.configure` is called.
203
+ :::
75
204
 
76
- c.emit_metrics = proc do |resource, result|
205
+ With this setup, all Axn actions will automatically create spans that appear in Datadog APM as children of your Rails request traces.
206
+
207
+ ## `emit_metrics`
208
+
209
+ If you're using a metrics provider, you can emit custom metrics after each action completes using the `emit_metrics` hook. This is a post-execution hook that receives the action result—do NOT call any blocks.
210
+
211
+ The hook only receives the keyword arguments it explicitly expects (e.g., if you only define `resource:`, you won't receive `result:`).
212
+
213
+ For example, to wire up Datadog metrics:
214
+
215
+ ```ruby
216
+ Axn.configure do |c|
217
+ c.emit_metrics = proc do |resource:, result:|
77
218
  TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s, resource: })
78
219
  TS::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
79
220
  end
80
221
  end
81
222
  ```
82
223
 
224
+ You can also define `emit_metrics` to only receive the arguments you need:
225
+
226
+ ```ruby
227
+ # Only receive resource (if you don't need the result)
228
+ c.emit_metrics = proc do |resource:|
229
+ TS::Metrics.increment("action.#{resource.underscore}")
230
+ end
231
+
232
+ # Only receive result (if you don't need the resource)
233
+ c.emit_metrics = proc do |result:|
234
+ TS::Metrics.increment("action.call", tags: { outcome: result.outcome.to_s })
235
+ end
236
+
237
+ # Accept any keyword arguments (receives both)
238
+ c.emit_metrics = proc do |**kwargs|
239
+ # kwargs will contain both :resource and :result
240
+ end
241
+ ```
242
+
243
+ **Important:** When using `result:` in your `emit_metrics` hook, be careful about cardinality. Avoid creating metrics with unbounded tag values from the result (e.g., user IDs, email addresses, or other high-cardinality data). Instead, use bounded values like `result.outcome.to_s` or aggregate data. High-cardinality metrics can cause performance issues and increased costs with metrics providers.
244
+
83
245
  A couple notes:
84
246
 
85
- * `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
86
247
  * `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`result.outcome.success?`, `result.outcome.failure?`, `result.outcome.exception?`) and elapsed time of the action.
87
- * The `wrap_with_trace` hook is an around hook - you must call the provided block to execute the action
88
248
  * The `emit_metrics` hook is called after execution with the result - do not call any blocks
89
249
 
90
250
  ## `logger`
91
251
 
92
252
  Defaults to `Rails.logger`, if present, otherwise falls back to `Logger.new($stdout)`. But can be set to a custom logger as necessary.
93
253
 
254
+ ### Background Job Logging
255
+
256
+ When using background jobs, you may want different loggers for web requests vs. background job execution. Here's a recommended pattern:
257
+
258
+ ```ruby
259
+ Axn.configure do |c|
260
+ # Use Sidekiq's logger when running in Sidekiq workers, otherwise use Rails logger
261
+ c.logger = (defined?(Sidekiq) && Sidekiq.server?) ? Sidekiq.logger : Rails.logger
262
+ end
263
+ ```
264
+
265
+ This ensures that:
266
+ - Web requests log to `Rails.logger` (typically `log/production.log`)
267
+ - Background jobs log to `Sidekiq.logger` (typically STDOUT or a separate log file)
94
268
 
95
269
 
96
270
  ## `additional_includes`
97
271
 
98
- This is much less critical than the preceding options, but on the off chance you want to add additional customization to _all_ your actions you can set additional modules to be included alongside `include Action`.
272
+ This is much less critical than the preceding options, but on the off chance you want to add additional customization to _all_ your actions you can set additional modules to be included alongside `include Axn`.
99
273
 
100
274
  For example:
101
275
 
102
276
  ```ruby
103
- Action.configure do |c|
277
+ Axn.configure do |c|
104
278
  c.additional_includes = [SomeFancyCustomModule]
105
279
  end
106
280
  ```
@@ -113,7 +287,134 @@ Sets the log level used when you call `log "Some message"` in your Action. Note
113
287
 
114
288
  ## `env`
115
289
 
116
- Automatically detects the environment from `RACK_ENV` or `RAILS_ENV`, defaulting to `"development"`. This is used internally for conditional behavior (e.g., more verbose logging in non-production environments).
290
+ Automatically detects the environment from `RACK_ENV` or `RAILS_ENV`, defaulting to `"development"`. Returns an `ActiveSupport::StringInquirer`, allowing you to use predicate methods like `env.production?` or `env.development?`.
291
+
292
+ ```ruby
293
+ Axn.config.env.production? # => true/false
294
+ Axn.config.env.development? # => true/false
295
+ Axn.config.env.test? # => true/false
296
+ ```
297
+
298
+ ### Environment-Dependent Behavior
299
+
300
+ Several Axn behaviors change based on the detected environment:
301
+
302
+ | Behavior | Production | Test | Development |
303
+ | -------- | ---------- | ---- | ----------- |
304
+ | Log separators in async calls | Hidden | Visible (`------`) | Visible (`------`) |
305
+ | `raise_piping_errors_in_dev` | Always swallowed | Always swallowed | Configurable |
306
+ | Error message verbosity | Minimal | More detailed | More detailed |
307
+
308
+ ### Overriding the Environment
309
+
310
+ You can explicitly set the environment if auto-detection doesn't work for your setup:
311
+
312
+ ```ruby
313
+ Axn.configure do |c|
314
+ c.env = "staging"
315
+ end
316
+
317
+ Axn.config.env.staging? # => true
318
+ ```
319
+
320
+ ## `set_default_async`
321
+
322
+ Configures the default async adapter and settings for all actions that don't explicitly specify their own async configuration.
323
+
324
+ ```ruby
325
+ Axn.configure do |c|
326
+ # Set default async adapter with configuration
327
+ c.set_default_async(:sidekiq, queue: "default", retry: 3) do
328
+ sidekiq_options priority: 5
329
+ end
330
+
331
+ # Set default async adapter with just configuration
332
+ c.set_default_async(:active_job) do
333
+ queue_as "default"
334
+ self.priority = 5
335
+ end
336
+
337
+ # Disable async by default
338
+ c.set_default_async(false)
339
+ end
340
+ ```
341
+
342
+ ### Async Configuration
343
+
344
+ Axn supports asynchronous execution through background job processing libraries. You can configure async behavior globally or per-action.
345
+
346
+ **Available adapters:**
347
+ - `:sidekiq` - Sidekiq background job processing
348
+ - `:active_job` - Rails ActiveJob framework
349
+ - `false` - Disable async execution
350
+
351
+ **Basic usage:**
352
+ ```ruby
353
+ # Configure per-action
354
+ async :sidekiq, queue: "high_priority"
355
+
356
+ # Configure globally
357
+ Axn.configure do |c|
358
+ c.set_default_async(:sidekiq, queue: "default")
359
+ end
360
+ ```
361
+
362
+ For detailed information about async execution, including delayed execution, adapter configuration options, and best practices, see the [Async Execution documentation](/reference/async).
363
+
364
+ #### Disabled
365
+
366
+ Disables async execution entirely. The action will raise a `NotImplementedError` when `call_async` is called.
367
+
368
+ ```ruby
369
+ # In your action class
370
+ async false
371
+ ```
372
+
373
+ ### Default Configuration
374
+
375
+ By default, async execution is disabled (`false`). You can set a default configuration that will be applied to all actions that don't explicitly configure their own async behavior:
376
+
377
+ ```ruby
378
+ Axn.configure do |c|
379
+ # Set a default async configuration
380
+ c.set_default_async(:sidekiq, queue: "default") do
381
+ sidekiq_options retry: 3
382
+ end
383
+ end
384
+
385
+ # Now all actions will use Sidekiq by default
386
+ class MyAction
387
+ include Axn
388
+ # No async configuration needed - uses default
389
+ end
390
+ ```
391
+
392
+ ## Rails-specific Configuration
393
+
394
+ When using Axn in a Rails application, additional configuration options are available under `Axn.config.rails`:
395
+
396
+ ### `app_actions_autoload_namespace`
397
+
398
+ Controls the namespace for actions in `app/actions`. Defaults to `nil` (no namespace).
399
+
400
+ ```ruby
401
+ Axn.configure do |c|
402
+ # No namespace (default behavior)
403
+ c.rails.app_actions_autoload_namespace = nil
404
+
405
+ # Use Actions namespace
406
+ c.rails.app_actions_autoload_namespace = :Actions
407
+
408
+ # Use any other namespace
409
+ c.rails.app_actions_autoload_namespace = :MyApp
410
+ end
411
+ ```
412
+
413
+ When `nil` (default), actions in `app/actions/user_management/create_user.rb` will be available as `UserManagement::CreateUser`.
414
+
415
+ When set to `:Actions`, the same action will be available as `Actions::UserManagement::CreateUser`.
416
+
417
+ When set to any other symbol (e.g., `:MyApp`), the action will be available as `MyApp::UserManagement::CreateUser`.
117
418
 
118
419
  ## Automatic Logging
119
420
 
@@ -124,37 +425,63 @@ By default, every `action.call` will emit log lines when it is called and after
124
425
  [YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
125
426
  ```
126
427
 
127
- Automatic logging will log at `Action.config.log_level` by default, but can be overridden or disabled using the declarative `auto_log` method:
428
+ Automatic logging will log at `Axn.config.log_level` by default, but can be overridden or disabled using the declarative `log_calls` method:
128
429
 
129
430
  ```ruby
130
431
  # Set default for all actions (affects both explicit logging and automatic logging)
131
- Action.configure do |c|
432
+ Axn.configure do |c|
132
433
  c.log_level = :debug
133
434
  end
134
435
 
135
436
  # Override for specific actions
136
437
  class MyAction
137
- auto_log :warn # Use warn level for this action
438
+ log_calls :warn # Use warn level for this action
138
439
  end
139
440
 
140
441
  class SilentAction
141
- auto_log false # Disable automatic logging for this action
442
+ log_calls false # Disable automatic logging for this action
142
443
  end
143
444
 
144
- # Use default level (no auto_log call needed)
445
+ # Use default level (no log_calls call needed)
145
446
  class DefaultAction
146
- # Uses Action.config.log_level
447
+ # Uses Axn.config.log_level
448
+ end
449
+ ```
450
+
451
+ The `log_calls` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
452
+
453
+ ### Error-Only Logging
454
+
455
+ For actions where you only want to log when something goes wrong, use `log_errors` instead of `log_calls`. This will:
456
+ - **Not** log before execution
457
+ - **Only** log after execution if `result.ok?` is false (i.e., on failures or exceptions)
458
+
459
+ ```ruby
460
+ class MyAction
461
+ log_calls false # Disable full logging
462
+ log_errors :warn # Only log failures/exceptions at warn level
463
+ end
464
+
465
+ class SilentOnErrorsAction
466
+ log_calls false
467
+ log_errors false # Disable error logging for this action
468
+ end
469
+
470
+ # Use default level
471
+ class DefaultErrorLoggingAction
472
+ log_calls false
473
+ log_errors Axn.config.log_level # Uses default log level
147
474
  end
148
475
  ```
149
476
 
150
- The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
477
+ The `log_errors` method supports inheritance, just like `log_calls`. If both `log_calls` and `log_errors` are set, `log_calls` takes precedence (it will log before and after for all outcomes). To use `log_errors` exclusively, you must first disable `log_calls` with `log_calls false`.
151
478
 
152
479
  ## Complete Configuration Example
153
480
 
154
481
  Here's a complete example showing all available configuration options:
155
482
 
156
483
  ```ruby
157
- Action.configure do |c|
484
+ Axn.configure do |c|
158
485
  # Logging
159
486
  c.log_level = :info
160
487
  c.logger = Rails.logger
@@ -167,18 +494,23 @@ Action.configure do |c|
167
494
  end
168
495
 
169
496
  # Observability
170
- c.wrap_with_trace = proc do |resource, &action|
171
- Datadog::Tracing.trace("Action", resource:) do
172
- action.call
173
- end
174
- end
497
+ # OpenTelemetry tracing is automatic when OpenTelemetry is available
175
498
 
176
- c.emit_metrics = proc do |resource, result|
499
+ c.emit_metrics = proc do |resource:, result:|
177
500
  Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s })
178
501
  Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
179
502
  end
180
503
 
504
+
505
+ # Async configuration
506
+ c.set_default_async(:sidekiq, queue: "default") do
507
+ sidekiq_options retry: 3, priority: 5
508
+ end
509
+
181
510
  # Global includes
182
511
  c.additional_includes = [MyCustomModule]
512
+
513
+ # Rails-specific configuration
514
+ c.rails.app_actions_autoload_namespace = :Actions
183
515
  end
184
516
  ```