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.
- checksums.yaml +4 -4
- data/.cursor/commands/pr.md +36 -0
- data/CHANGELOG.md +15 -1
- data/Rakefile +102 -2
- data/docs/.vitepress/config.mjs +12 -8
- data/docs/advanced/conventions.md +1 -1
- data/docs/advanced/mountable.md +4 -90
- data/docs/advanced/profiling.md +26 -30
- data/docs/advanced/rough.md +27 -8
- data/docs/intro/overview.md +1 -1
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +102 -17
- data/docs/reference/async.md +269 -0
- data/docs/reference/class.md +113 -50
- data/docs/reference/configuration.md +226 -75
- data/docs/reference/form-object.md +252 -0
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/usage/setup.md +2 -2
- data/docs/usage/writing.md +99 -1
- data/lib/axn/async/adapters/active_job.rb +19 -10
- data/lib/axn/async/adapters/disabled.rb +15 -0
- data/lib/axn/async/adapters/sidekiq.rb +25 -32
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +121 -4
- data/lib/axn/configuration.rb +53 -13
- data/lib/axn/context.rb +1 -0
- data/lib/axn/core/automatic_logging.rb +47 -51
- data/lib/axn/core/context/facade_inspector.rb +1 -1
- data/lib/axn/core/contract.rb +73 -30
- data/lib/axn/core/contract_for_subfields.rb +1 -1
- data/lib/axn/core/contract_validation.rb +14 -9
- data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/flow/exception_execution.rb +5 -0
- data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
- data/lib/axn/core/flow/handlers/invoker.rb +4 -30
- data/lib/axn/core/flow/handlers/matcher.rb +4 -14
- data/lib/axn/core/flow/messages.rb +1 -1
- data/lib/axn/core/hooks.rb +1 -0
- data/lib/axn/core/logging.rb +16 -5
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/axn/core/tracing.rb +77 -4
- data/lib/axn/core/validation/validators/type_validator.rb +1 -1
- data/lib/axn/core.rb +31 -46
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +22 -2
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +5 -1
- data/lib/axn/mountable/helpers/class_builder.rb +41 -10
- data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
- data/lib/axn/mountable/inherit_profiles.rb +2 -2
- data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
- data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
- data/lib/axn/mountable.rb +41 -7
- data/lib/axn/rails/generators/axn_generator.rb +19 -1
- data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
- data/lib/axn/result.rb +2 -2
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +7 -0
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +9 -0
- metadata +22 -4
- data/lib/axn/core/profiling.rb +0 -124
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +0 -55
data/docs/reference/class.md
CHANGED
|
@@ -149,39 +149,67 @@ def build_error_message(exception:)
|
|
|
149
149
|
end
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
error "Default error message"
|
|
173
|
+
# 1. Define static fallback FIRST (evaluated last, catches anything unmatched)
|
|
174
|
+
error "Something went wrong"
|
|
163
175
|
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
error "
|
|
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
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
error "
|
|
190
|
+
# These will NEVER be reached!
|
|
191
|
+
error "Invalid input provided", if: ArgumentError
|
|
192
|
+
error "Record not found", if: ActiveRecord::RecordNotFound
|
|
178
193
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
#
|
|
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
|
-
-
|
|
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
|
|
401
|
+
### Message ordering with `from:`
|
|
316
402
|
|
|
317
|
-
|
|
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
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
success "Final success message" # This will be used
|
|
409
|
+
# Static fallback first
|
|
410
|
+
error "Something went wrong"
|
|
342
411
|
|
|
343
|
-
|
|
344
|
-
error "
|
|
345
|
-
error
|
|
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
|
-
|
|
58
|
+
### Adding Additional Context to Exception Logging
|
|
58
59
|
|
|
59
|
-
|
|
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
|
-
|
|
62
|
+
**Option 1: Explicit setter** - Call `set_logging_context` during execution:
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
77
|
+
**Option 2: Hook method** - Define a private `additional_logging_context` method that returns a hash:
|
|
67
78
|
|
|
68
79
|
```ruby
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
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"`.
|
|
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 `
|
|
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
|
-
|
|
438
|
+
log_calls :warn # Use warn level for this action
|
|
251
439
|
end
|
|
252
440
|
|
|
253
441
|
class SilentAction
|
|
254
|
-
|
|
442
|
+
log_calls false # Disable automatic logging for this action
|
|
255
443
|
end
|
|
256
444
|
|
|
257
|
-
# Use default level (no
|
|
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 `
|
|
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
|
-
###
|
|
453
|
+
### Error-Only Logging
|
|
270
454
|
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|