axn 0.1.0.pre.alpha.3 → 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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/CHANGELOG.md +15 -1
  4. data/Rakefile +102 -2
  5. data/docs/.vitepress/config.mjs +12 -8
  6. data/docs/advanced/conventions.md +1 -1
  7. data/docs/advanced/mountable.md +4 -90
  8. data/docs/advanced/profiling.md +26 -30
  9. data/docs/advanced/rough.md +27 -8
  10. data/docs/intro/overview.md +1 -1
  11. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  12. data/docs/recipes/memoization.md +102 -17
  13. data/docs/reference/async.md +269 -0
  14. data/docs/reference/class.md +113 -50
  15. data/docs/reference/configuration.md +226 -75
  16. data/docs/reference/form-object.md +252 -0
  17. data/docs/strategies/client.md +212 -0
  18. data/docs/strategies/form.md +235 -0
  19. data/docs/usage/setup.md +2 -2
  20. data/docs/usage/writing.md +99 -1
  21. data/lib/axn/async/adapters/active_job.rb +19 -10
  22. data/lib/axn/async/adapters/disabled.rb +15 -0
  23. data/lib/axn/async/adapters/sidekiq.rb +25 -32
  24. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  25. data/lib/axn/async/batch_enqueue.rb +99 -0
  26. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  27. data/lib/axn/async.rb +121 -4
  28. data/lib/axn/configuration.rb +53 -13
  29. data/lib/axn/context.rb +1 -0
  30. data/lib/axn/core/automatic_logging.rb +47 -51
  31. data/lib/axn/core/context/facade_inspector.rb +1 -1
  32. data/lib/axn/core/contract.rb +73 -30
  33. data/lib/axn/core/contract_for_subfields.rb +1 -1
  34. data/lib/axn/core/contract_validation.rb +14 -9
  35. data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
  36. data/lib/axn/core/default_call.rb +63 -0
  37. data/lib/axn/core/flow/exception_execution.rb +5 -0
  38. data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
  39. data/lib/axn/core/flow/handlers/invoker.rb +4 -30
  40. data/lib/axn/core/flow/handlers/matcher.rb +4 -14
  41. data/lib/axn/core/flow/messages.rb +1 -1
  42. data/lib/axn/core/hooks.rb +1 -0
  43. data/lib/axn/core/logging.rb +16 -5
  44. data/lib/axn/core/memoization.rb +53 -0
  45. data/lib/axn/core/tracing.rb +77 -4
  46. data/lib/axn/core/validation/validators/type_validator.rb +1 -1
  47. data/lib/axn/core.rb +31 -46
  48. data/lib/axn/extras/strategies/client.rb +150 -0
  49. data/lib/axn/extras/strategies/vernier.rb +121 -0
  50. data/lib/axn/extras.rb +4 -0
  51. data/lib/axn/factory.rb +22 -2
  52. data/lib/axn/form_object.rb +90 -0
  53. data/lib/axn/internal/logging.rb +5 -1
  54. data/lib/axn/mountable/helpers/class_builder.rb +41 -10
  55. data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
  56. data/lib/axn/mountable/inherit_profiles.rb +2 -2
  57. data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
  58. data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
  59. data/lib/axn/mountable.rb +41 -7
  60. data/lib/axn/rails/generators/axn_generator.rb +19 -1
  61. data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
  62. data/lib/axn/result.rb +2 -2
  63. data/lib/axn/strategies/form.rb +98 -0
  64. data/lib/axn/strategies/transaction.rb +7 -0
  65. data/lib/axn/util/callable.rb +120 -0
  66. data/lib/axn/util/contract_error_handling.rb +32 -0
  67. data/lib/axn/util/execution_context.rb +34 -0
  68. data/lib/axn/util/global_id_serialization.rb +52 -0
  69. data/lib/axn/util/logging.rb +87 -0
  70. data/lib/axn/version.rb +1 -1
  71. data/lib/axn.rb +9 -0
  72. metadata +22 -4
  73. data/lib/axn/core/profiling.rb +0 -124
  74. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +0 -55
@@ -149,39 +149,67 @@ def build_error_message(exception:)
149
149
  end
150
150
  ```
151
151
 
152
- ::: warning Message Ordering
153
- **Important**: Static success/error messages (those without conditions) should be defined **first** in your action class. If you define conditional messages before static ones, the conditional messages will never be reached because the static message will always match first.
152
+ ## Message Matching Order {#message-matching-order}
153
+
154
+ ::: danger Important: Understanding Handler Evaluation Order
155
+ Message handlers are stored in **last-defined-first** order and evaluated in that order until a match is found. This has critical implications for how you structure your message declarations.
156
+ :::
157
+
158
+ ### How It Works
159
+
160
+ 1. Handlers are registered in reverse definition order (last defined = first evaluated)
161
+ 2. The system evaluates handlers one by one until finding one that matches
162
+ 3. Static handlers (no `if:` or `unless:` condition) **always match**
163
+ 4. Once a match is found, evaluation stops
164
+
165
+ ### Correct Pattern: Static Fallbacks First
166
+
167
+ Because static handlers always match, they must be defined **first** (so they're evaluated **last**):
154
168
 
155
- **Correct order:**
156
169
  ```ruby
157
170
  class MyAction
158
171
  include Axn
159
172
 
160
- # Define static fallback first
161
- success "Default success message"
162
- error "Default error message"
173
+ # 1. Define static fallback FIRST (evaluated last, catches anything unmatched)
174
+ error "Something went wrong"
163
175
 
164
- # Then define conditional messages
165
- success "Special success", if: :special_condition?
166
- error "Special error", if: ArgumentError
176
+ # 2. Define conditional handlers AFTER (evaluated first, catch specific cases)
177
+ error "Invalid input provided", if: ArgumentError
178
+ error "Record not found", if: ActiveRecord::RecordNotFound
167
179
  end
168
180
  ```
169
181
 
170
- **Incorrect order (conditional messages will be shadowed):**
182
+ ### Incorrect Pattern: Conditional Messages Shadowed
183
+
184
+ If you define static handlers last, they match first and conditional handlers are never reached:
185
+
171
186
  ```ruby
172
187
  class MyAction
173
188
  include Axn
174
189
 
175
- # These conditional messages will never be reached!
176
- success "Special success", if: :special_condition?
177
- error "Special error", if: ArgumentError
190
+ # These will NEVER be reached!
191
+ error "Invalid input provided", if: ArgumentError
192
+ error "Record not found", if: ActiveRecord::RecordNotFound
178
193
 
179
- # Static messages defined last will always match first
180
- success "Default success message"
181
- error "Default error message"
194
+ # This static handler is evaluated FIRST and always matches
195
+ error "Something went wrong"
196
+ end
197
+ ```
198
+
199
+ ### With Inheritance
200
+
201
+ Child class handlers are evaluated before parent class handlers:
202
+
203
+ ```ruby
204
+ class ParentAction
205
+ include Axn
206
+ error "Parent error" # Evaluated last
207
+ end
208
+
209
+ class ChildAction < ParentAction
210
+ error "Child error" # Evaluated first
182
211
  end
183
212
  ```
184
- :::
185
213
 
186
214
  ## Conditional messages
187
215
 
@@ -251,6 +279,10 @@ The `from:` parameter allows you to customize error messages when an action call
251
279
 
252
280
  When using `from:`, the error handler receives the exception from the child action, and you can access the child's error message via `e.message` (which contains the `result.error` from the child action).
253
281
 
282
+ ### Basic usage
283
+
284
+ You can use `from:` with a single child action class. The prefix and custom handler are optional:
285
+
254
286
  ```ruby
255
287
  class InnerAction
256
288
  include Axn
@@ -265,7 +297,10 @@ end
265
297
  class OuterAction
266
298
  include Axn
267
299
 
268
- # Customize error messages from InnerAction
300
+ # Simply inherit child's error message (no prefix or custom handler needed)
301
+ error from: InnerAction
302
+
303
+ # Or customize the message
269
304
  error from: InnerAction do |e|
270
305
  "Outer action failed: #{e.message}"
271
306
  end
@@ -279,7 +314,58 @@ end
279
314
  In this example:
280
315
  - When `InnerAction` fails, `OuterAction` will catch the exception
281
316
  - The `e.message` contains the error message from `InnerAction`'s result
282
- - The final error message will be "Outer action failed: Something went wrong in the inner action"
317
+ - With no handler: the error message will be "Something went wrong in the inner action" (inherited directly)
318
+ - With custom handler: the error message will be "Outer action failed: Something went wrong in the inner action"
319
+
320
+ ### Matching multiple child actions
321
+
322
+ You can pass an array of child action classes to match multiple children:
323
+
324
+ ```ruby
325
+ class OuterAction
326
+ include Axn
327
+
328
+ # Match errors from multiple child actions
329
+ error from: [FirstChildAction, SecondChildAction]
330
+
331
+ # Or with custom handler
332
+ error from: [FirstChildAction, SecondChildAction] do |e|
333
+ "Parent caught: #{e.message}"
334
+ end
335
+
336
+ def call
337
+ # Calls one of the child actions
338
+ end
339
+ end
340
+ ```
341
+
342
+ You can also mix class references and string class names:
343
+
344
+ ```ruby
345
+ error from: [FirstChildAction, "SecondChildAction"]
346
+ ```
347
+
348
+ ### Matching any child action
349
+
350
+ Use `from: true` to match errors from any child action without listing them explicitly:
351
+
352
+ ```ruby
353
+ class OuterAction
354
+ include Axn
355
+
356
+ # Match errors from any child action
357
+ error from: true
358
+
359
+ # Or with custom handler
360
+ error from: true do |e|
361
+ "Any child failed: #{e.message}"
362
+ end
363
+
364
+ def call
365
+ # Can call any child action
366
+ end
367
+ end
368
+ ```
283
369
 
284
370
  This pattern is especially useful for:
285
371
  - Adding context to error messages from sub-actions
@@ -312,46 +398,23 @@ This results in:
312
398
  - With custom message: "API Error: Request failed: Something went wrong in the inner action"
313
399
  - With prefix only: "API Error: Something went wrong in the inner action"
314
400
 
315
- ### Message ordering and inheritance
401
+ ### Message ordering with `from:`
316
402
 
317
- Messages are evaluated in **last-defined-first** order, meaning the most recently defined message that matches its conditions will be used. This applies to both success and error messages:
403
+ When using `from:` with inheritance, the same [message matching order](#message-matching-order) applies. Define your `from:` handlers after your static fallback:
318
404
 
319
405
  ```ruby
320
- class ParentAction
321
- include Axn
322
-
323
- success "Parent success message"
324
- error "Parent error message"
325
- end
326
-
327
- class ChildAction < ParentAction
328
- success "Child success message" # This will be used when action succeeds
329
- error "Child error message" # This will be used when action fails
330
- end
331
- ```
332
-
333
- Within a single class, later definitions override earlier ones:
334
-
335
- ```ruby
336
- class MyAction
406
+ class OuterAction
337
407
  include Axn
338
408
 
339
- success "First success message" # Ignored
340
- success "Second success message" # Ignored
341
- success "Final success message" # This will be used
409
+ # Static fallback first
410
+ error "Something went wrong"
342
411
 
343
- error "First error message" # Ignored
344
- error "Second error message" # Ignored
345
- error "Final error message" # This will be used
412
+ # Then from: handlers for specific child actions
413
+ error from: InnerAction, prefix: "Inner failed: "
414
+ error from: AnotherAction, prefix: "Another failed: "
346
415
  end
347
416
  ```
348
417
 
349
- ::: tip Message Evaluation Order
350
- The system evaluates handlers in the order they were defined until it finds one that matches and doesn't raise an exception. If a handler raises an exception, it falls back to the next matching handler, then to static messages, and finally to the default message.
351
-
352
- **Key point**: Static messages (without conditions) are evaluated **first** in the order they were defined. This means you should define your static fallback messages at the top of your class, before any conditional messages, to ensure proper fallback behavior.
353
- :::
354
-
355
418
  ## `.async`
356
419
 
357
420
  Configures the async execution behavior for the action. This determines how the action will be executed when `call_async` is called.
@@ -53,38 +53,198 @@ 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 Axn 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
- Axn.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
+ :::
204
+
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`
75
208
 
76
- c.emit_metrics = proc do |resource, result|
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`
@@ -109,7 +269,7 @@ This ensures that:
109
269
 
110
270
  ## `additional_includes`
111
271
 
112
- 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`.
113
273
 
114
274
  For example:
115
275
 
@@ -127,7 +287,35 @@ Sets the log level used when you call `log "Some message"` in your Action. Note
127
287
 
128
288
  ## `env`
129
289
 
130
- 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
+ ```
131
319
 
132
320
  ## `set_default_async`
133
321
 
@@ -237,7 +425,7 @@ By default, every `action.call` will emit log lines when it is called and after
237
425
  [YourCustomAction] Execution completed (with outcome: success) in 0.957 milliseconds
238
426
  ```
239
427
 
240
- Automatic logging will log at `Axn.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:
241
429
 
242
430
  ```ruby
243
431
  # Set default for all actions (affects both explicit logging and automatic logging)
@@ -247,79 +435,46 @@ end
247
435
 
248
436
  # Override for specific actions
249
437
  class MyAction
250
- auto_log :warn # Use warn level for this action
438
+ log_calls :warn # Use warn level for this action
251
439
  end
252
440
 
253
441
  class SilentAction
254
- auto_log false # Disable automatic logging for this action
442
+ log_calls false # Disable automatic logging for this action
255
443
  end
256
444
 
257
- # Use default level (no auto_log call needed)
445
+ # Use default level (no log_calls call needed)
258
446
  class DefaultAction
259
447
  # Uses Axn.config.log_level
260
448
  end
261
449
  ```
262
450
 
263
- The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
264
-
265
- ## Profiling
266
-
267
- Axn supports performance profiling using [Vernier](https://github.com/Shopify/vernier), a Ruby sampling profiler. Profiling is enabled per-action by calling the `profile` method.
451
+ The `log_calls` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
268
452
 
269
- ### Usage
453
+ ### Error-Only Logging
270
454
 
271
- Enable profiling on specific actions using the `profile` method:
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)
272
458
 
273
459
  ```ruby
274
460
  class MyAction
275
- include Axn
276
-
277
- # Profile conditionally (only one profile call per action)
278
- profile if: -> { debug_mode }
279
-
280
- expects :name, :debug_mode
281
-
282
- def call
283
- "Hello, #{name}!"
284
- end
461
+ log_calls false # Disable full logging
462
+ log_errors :warn # Only log failures/exceptions at warn level
285
463
  end
286
- ```
287
464
 
288
- ### Configuration Options
289
-
290
- The `profile` method accepts several options:
291
-
292
- ```ruby
293
- class MyAction
294
- include Axn
295
-
296
- # Profile with custom options
297
- profile(
298
- if: -> { debug_mode },
299
- sample_rate: 0.1, # Sampling rate (0.0 to 1.0, default: 0.1)
300
- output_dir: "tmp/profiles" # Output directory (default: Rails.root/tmp/profiles or tmp/profiles)
301
- )
465
+ class SilentOnErrorsAction
466
+ log_calls false
467
+ log_errors false # Disable error logging for this action
468
+ end
302
469
 
303
- def call
304
- # Action logic
305
- end
470
+ # Use default level
471
+ class DefaultErrorLoggingAction
472
+ log_calls false
473
+ log_errors Axn.config.log_level # Uses default log level
306
474
  end
307
475
  ```
308
476
 
309
- **Important**:
310
- - You can only call `profile` **once per action** - subsequent calls will override the previous one
311
- - This prevents accidental profiling of all actions and ensures you only profile what you intend to analyze
312
-
313
- ### Viewing Profiles
314
-
315
- Profiles are saved as JSON files that can be viewed in the [Firefox Profiler](https://profiler.firefox.com/):
316
-
317
- 1. Run your action with profiling enabled
318
- 2. Find the generated profile file in your `profiling_output_dir`
319
- 3. Upload the JSON file to [profiler.firefox.com](https://profiler.firefox.com/)
320
- 4. Analyze the performance data
321
-
322
- For more detailed information, see the [Profiling guide](/advanced/profiling).
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`.
323
478
 
324
479
  ## Complete Configuration Example
325
480
 
@@ -339,13 +494,9 @@ Axn.configure do |c|
339
494
  end
340
495
 
341
496
  # Observability
342
- c.wrap_with_trace = proc do |resource, &action|
343
- Datadog::Tracing.trace("Action", resource:) do
344
- action.call
345
- end
346
- end
497
+ # OpenTelemetry tracing is automatic when OpenTelemetry is available
347
498
 
348
- c.emit_metrics = proc do |resource, result|
499
+ c.emit_metrics = proc do |resource:, result:|
349
500
  Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s })
350
501
  Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
351
502
  end