Skip to content

Commit 956c5e3

Browse files
johnnytclaude
andauthored
Enhances logging with better ergonomics and performance (#91)
Enhances logging system with improved ergonomics and debugging - Adds atom-based log adapter configuration (:elixir, :internal, :test, :silent) - Adds metadata_fields/0 function to LogManager for centralized field definitions - Adds environment-aware logging defaults (trace in dev, debug in test, info in prod) - Adds enhanced debugging logs throughout transition resolution and action execution - Adds comprehensive trace logging for transition evaluation, condition checking, and state changes - Adds better error context logging for failed conditions and expression evaluations - Fixes ElixirLoggerAdapter to support :trace level by mapping to :debug - Fixes TransitionResolver to return state chart tuples for proper logging threading - Fixes credo issues by extracting helper functions to reduce complexity and nesting - Adds config/config.exs for environment-specific Logger configuration - Updates CLAUDE.md with comprehensive debugging configuration examples Converts logging functions to performance-optimized macros - Converts all LogManager level functions (trace, debug, info, warn, error) to macros - Adds lazy evaluation for optimal performance - arguments only evaluated when logging is enabled - Adds zero overhead when logging is disabled through early bailout - Maintains familiar LogManager.level/3 API while providing significant performance benefits - Adds bind_quoted macro implementation for proper variable scoping - Adds require LogManager statements to all consuming modules for macro usage - Removes manual LogManager.enabled? checks since macros handle this automatically - Removes @SPEC declarations from macro functions to fix dialyzer warnings - Updates CLAUDE.md with performance optimization documentation and examples - Maintains all existing functionality while providing hot path performance improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72d0105 commit 956c5e3

File tree

17 files changed

+592
-68
lines changed

17 files changed

+592
-68
lines changed

CLAUDE.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,103 @@ The implementation plan transforms Statifier from a basic state machine library
533533

534534
This logging system will integrate seamlessly with both the current functional API and future GenServer-based persistent interpreters.
535535

536+
## Debugging State Charts
537+
538+
When debugging state chart execution, configure enhanced logging for detailed visibility:
539+
540+
```elixir
541+
# Enable detailed tracing for debugging
542+
{:ok, state_chart} = Interpreter.initialize(document, [
543+
log_adapter: :elixir,
544+
log_level: :trace
545+
])
546+
547+
# Alternative: use internal adapter for testing/development
548+
{:ok, state_chart} = Interpreter.initialize(document, [
549+
log_adapter: :internal,
550+
log_level: :trace
551+
])
552+
```
553+
554+
### Log Adapter Options
555+
556+
- `:elixir` - Uses ElixirLoggerAdapter (integrates with Elixir's Logger system)
557+
- `:internal` - Uses TestAdapter for internal debugging
558+
- `:test` - Uses TestAdapter (alias for test environments)
559+
- `:silent` - Uses TestAdapter with no log storage (disables logging)
560+
561+
### Log Levels
562+
563+
- `:trace` - Very detailed execution tracing (transitions, conditions, actions)
564+
- `:debug` - General debugging information (state changes, events)
565+
- `:info` - High-level execution flow
566+
- `:warn` - Unusual conditions
567+
- `:error` - Execution errors
568+
569+
### Performance-Optimized Logging
570+
571+
All LogManager logging functions (`trace/3`, `debug/3`, `info/3`, `warn/3`, `error/3`) are implemented as macros that provide lazy evaluation for optimal performance:
572+
573+
```elixir
574+
# Expensive computations are only performed if logging level is enabled
575+
state_chart = LogManager.debug(state_chart, "Complex operation", %{
576+
expensive_data: build_debug_info(), # Only called if debug enabled
577+
complex_calculation: heavy_computation() # Only called if debug enabled
578+
})
579+
580+
# Zero overhead when logging is disabled - arguments are never evaluated
581+
state_chart = LogManager.trace(state_chart, "Detailed info", %{
582+
massive_object: serialize_entire_state() # Never called if trace disabled
583+
})
584+
```
585+
586+
This provides significant performance benefits in hot code paths while maintaining the familiar `LogManager.level/3` API.
587+
588+
### Automatic Environment Configuration
589+
590+
Statifier automatically detects your environment and configures appropriate logging defaults:
591+
592+
- **Development (`MIX_ENV=dev`)**: `:trace` level with `:elixir` adapter for detailed debugging
593+
- **Test (`MIX_ENV=test`)**: `:debug` level with `:test` adapter for clean test output
594+
- **Production (other)**: `:info` level with `:elixir` adapter for essential information
595+
596+
Users can override these defaults via application configuration:
597+
598+
```elixir
599+
# config/config.exs
600+
config :statifier,
601+
default_log_adapter: :elixir,
602+
default_log_level: :trace
603+
```
604+
605+
### Debugging Examples
606+
607+
```elixir
608+
# In dev environment, no additional configuration needed
609+
{:ok, document, _warnings} = Statifier.parse(xml)
610+
{:ok, state_chart} = Interpreter.initialize(document) # Auto-configured for dev
611+
612+
# Manual configuration for other environments
613+
{:ok, state_chart} = Interpreter.initialize(document, [
614+
log_adapter: :elixir,
615+
log_level: :trace
616+
])
617+
618+
# Debug specific state chart behavior
619+
xml = """
620+
<scxml initial="s1">
621+
<state id="s1">
622+
<transition event="go" target="s2"/>
623+
</state>
624+
<state id="s2"/>
625+
</scxml>
626+
"""
627+
628+
# Send event - will show detailed trace logs with full metadata
629+
event = %Event{name: "go"}
630+
{:ok, new_state_chart} = Interpreter.send_event(state_chart, event)
631+
```
632+
536633
- Always refer to state machines as state charts
537634
- Always run 'mix format' after writing an Elixir file.
538635
- When creating git commit messages:

config/config.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Config
2+
3+
# Configure Elixir Logger for enhanced debugging in dev environment
4+
if config_env() == :dev do
5+
config :logger,
6+
level: :debug
7+
8+
config :logger, :console,
9+
format: "[$level] $message $metadata\n",
10+
metadata: [
11+
:current_state,
12+
:event,
13+
:action_type,
14+
:state_id,
15+
:phase,
16+
:transition_type,
17+
:source_state,
18+
:condition,
19+
:targets
20+
]
21+
22+
# Default Statifier logging configuration for dev
23+
config :statifier,
24+
default_log_adapter: :elixir,
25+
default_log_level: :trace
26+
end
27+
28+
# Test environment uses TestAdapter for clean test output
29+
if config_env() == :test do
30+
config :logger, level: :warning
31+
32+
config :statifier,
33+
default_log_adapter: :test,
34+
default_log_level: :debug
35+
end

lib/statifier/actions/action_executor.ex

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ defmodule Statifier.Actions.ActionExecutor do
1717
}
1818

1919
alias Statifier.Logging.LogManager
20+
require LogManager
2021

2122
@doc """
2223
Execute onentry actions for a list of states being entered.
@@ -82,6 +83,16 @@ defmodule Statifier.Actions.ActionExecutor do
8283
# Private functions
8384

8485
defp execute_actions(actions, state_id, phase, state_chart) do
86+
# Log the start of action execution
87+
state_chart =
88+
LogManager.trace(state_chart, "Executing actions", %{
89+
action_type: "action_execution",
90+
phase: phase,
91+
state_id: state_id,
92+
action_count: length(actions),
93+
actions: summarize_actions(actions)
94+
})
95+
8596
actions
8697
|> Enum.reduce(state_chart, fn action, acc_state_chart ->
8798
execute_single_action(action, state_id, phase, acc_state_chart)
@@ -94,7 +105,9 @@ defmodule Statifier.Actions.ActionExecutor do
94105
LogManager.debug(state_chart, "Executing log action", %{
95106
action_type: "log_action",
96107
state_id: state_id,
97-
phase: phase
108+
phase: phase,
109+
log_expr: log_action.expr,
110+
log_label: log_action.label
98111
})
99112

100113
# Delegate to LogAction's own execute method
@@ -107,7 +120,8 @@ defmodule Statifier.Actions.ActionExecutor do
107120
LogManager.debug(state_chart, "Executing raise action", %{
108121
action_type: "raise_action",
109122
state_id: state_id,
110-
phase: phase
123+
phase: phase,
124+
event_name: raise_action.event
111125
})
112126

113127
# Delegate to RaiseAction's own execute method
@@ -117,8 +131,8 @@ defmodule Statifier.Actions.ActionExecutor do
117131
defp execute_single_action(%AssignAction{} = assign_action, state_id, phase, state_chart) do
118132
# Log context information for debugging
119133
state_chart =
120-
LogManager.debug(state_chart, "Executing assign action", %{
121-
action_type: "assign_action",
134+
LogManager.trace(state_chart, "Executing assign action", %{
135+
action_type: "assign_action_execution",
122136
state_id: state_id,
123137
phase: phase,
124138
location: assign_action.location,
@@ -132,8 +146,8 @@ defmodule Statifier.Actions.ActionExecutor do
132146
defp execute_single_action(%IfAction{} = if_action, state_id, phase, state_chart) do
133147
# Log context information for debugging
134148
state_chart =
135-
LogManager.debug(state_chart, "Executing if action", %{
136-
action_type: "if_action",
149+
LogManager.trace(state_chart, "Executing if action", %{
150+
action_type: "if_action_execution",
137151
state_id: state_id,
138152
phase: phase,
139153
conditional_blocks_count: length(if_action.conditional_blocks)
@@ -203,4 +217,32 @@ defmodule Statifier.Actions.ActionExecutor do
203217
end)
204218
end
205219
end
220+
221+
# Create a summary of actions for logging
222+
defp summarize_actions(actions) do
223+
Enum.map(actions, fn action ->
224+
case action do
225+
%LogAction{expr: expr} ->
226+
%{type: "log", expr: expr}
227+
228+
%RaiseAction{event: event} ->
229+
%{type: "raise", event: event}
230+
231+
%AssignAction{location: location, expr: expr} ->
232+
%{type: "assign", location: location, expr: expr}
233+
234+
%IfAction{} ->
235+
%{type: "if", condition: "..."}
236+
237+
%ForeachAction{} ->
238+
%{type: "foreach", array: "..."}
239+
240+
%SendAction{} ->
241+
%{type: "send", event: "..."}
242+
243+
_unknown_action ->
244+
%{type: "unknown"}
245+
end
246+
end)
247+
end
206248
end

lib/statifier/actions/assign_action.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ defmodule Statifier.Actions.AssignAction do
2929

3030
alias Statifier.{Evaluator, StateChart}
3131
alias Statifier.Logging.LogManager
32+
require LogManager
3233

3334
@enforce_keys [:location, :expr]
3435
defstruct [:location, :expr, :compiled_expr, :source_location]

lib/statifier/actions/foreach_action.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ defmodule Statifier.Actions.ForeachAction do
4646

4747
alias Statifier.{Actions.ActionExecutor, Evaluator, Event, StateChart}
4848
alias Statifier.Logging.LogManager
49+
require LogManager
4950

5051
@enforce_keys [:array, :item, :actions]
5152
defstruct [:array, :item, :index, :actions, :compiled_array, :source_location]

lib/statifier/actions/log_action.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Statifier.Actions.LogAction do
1212

1313
alias Statifier.Evaluator
1414
alias Statifier.Logging.LogManager
15+
require LogManager
1516

1617
defstruct [:label, :expr, :source_location]
1718

@@ -71,7 +72,15 @@ defmodule Statifier.Actions.LogAction do
7172
result = safe_to_string(value)
7273
if result == "", do: expr, else: result
7374

74-
{:error, _reason} ->
75+
{:error, reason} ->
76+
# Log the evaluation error for debugging
77+
LogManager.warn(state_chart, "Log expression evaluation failed", %{
78+
action_type: "log_evaluation_error",
79+
expression: expr,
80+
error: inspect(reason),
81+
fallback: "using string parsing"
82+
})
83+
7584
# Fall back to simple string parsing for basic quoted strings
7685
parse_quoted_string_fallback(expr)
7786
end

lib/statifier/actions/raise_action.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Statifier.Actions.RaiseAction do
99

1010
alias Statifier.{Event, StateChart}
1111
alias Statifier.Logging.LogManager
12+
require LogManager
1213

1314
@type t :: %__MODULE__{
1415
event: String.t() | nil,

lib/statifier/actions/send_action.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ defmodule Statifier.Actions.SendAction do
1212

1313
alias Statifier.{Evaluator, Event, StateChart}
1414
alias Statifier.Logging.LogManager
15+
require LogManager
1516

1617
@type t :: %__MODULE__{
1718
event: String.t() | nil,

lib/statifier/datamodel.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ defmodule Statifier.Datamodel do
2929

3030
alias Statifier.{Configuration, Evaluator}
3131
alias Statifier.Logging.LogManager
32+
require LogManager
3233

3334
@type t :: map()
3435

lib/statifier/interpreter.ex

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ defmodule Statifier.Interpreter do
2121
Validator
2222
}
2323

24+
require LogManager
25+
2426
@doc """
2527
Initialize a state chart from a parsed document.
2628
@@ -131,7 +133,8 @@ defmodule Statifier.Interpreter do
131133
@spec send_event(StateChart.t(), Event.t()) :: {:ok, StateChart.t()}
132134
def send_event(%StateChart{} = state_chart, %Event{} = event) do
133135
# Find optimal transition set enabled by this event
134-
enabled_transitions = TransitionResolver.find_enabled_transitions(state_chart, event)
136+
{state_chart, enabled_transitions} =
137+
TransitionResolver.find_enabled_transitions(state_chart, event)
135138

136139
case enabled_transitions do
137140
[] ->
@@ -170,13 +173,31 @@ defmodule Statifier.Interpreter do
170173
# Recursive helper with cycle detection (max 1000 iterations)
171174
defp execute_microsteps(%StateChart{} = state_chart, iterations)
172175
when iterations >= 1000 do
176+
# Log performance warning for high iteration count
177+
state_chart =
178+
LogManager.warn(state_chart, "High microstep iteration count", %{
179+
action_type: "performance_warning",
180+
iteration: iterations,
181+
threshold: 1000,
182+
suggestion: "Check for infinite eventless transition loops"
183+
})
184+
173185
# Prevent infinite loops - return current state
174186
state_chart
175187
end
176188

177189
defp execute_microsteps(%StateChart{} = state_chart, iterations) do
190+
# Log microstep processing
191+
state_chart =
192+
LogManager.trace(state_chart, "Processing microstep", %{
193+
action_type: "microstep",
194+
iteration: iterations,
195+
current_states: state_chart.configuration.active_states |> MapSet.to_list()
196+
})
197+
178198
# Per SCXML specification: eventless transitions have higher priority than internal events
179-
eventless_transitions = TransitionResolver.find_eventless_transitions(state_chart)
199+
{state_chart, eventless_transitions} =
200+
TransitionResolver.find_eventless_transitions(state_chart)
180201

181202
case eventless_transitions do
182203
[] ->
@@ -463,6 +484,17 @@ defmodule Statifier.Interpreter do
463484
|> MapSet.difference(states_to_exit)
464485
|> MapSet.union(new_target_set)
465486

487+
# Log configuration change
488+
state_chart =
489+
LogManager.debug(state_chart, "Configuration change (internal transition)", %{
490+
action_type: "state_change",
491+
transition_type: "internal",
492+
from_states: MapSet.to_list(current_active),
493+
to_states: MapSet.to_list(final_active_states),
494+
exited_states: MapSet.to_list(states_to_exit),
495+
entered_states: MapSet.to_list(states_to_enter)
496+
})
497+
466498
new_config = Configuration.new(MapSet.to_list(final_active_states))
467499
StateChart.update_configuration(state_chart, new_config)
468500
end
@@ -502,6 +534,19 @@ defmodule Statifier.Interpreter do
502534

503535
# Combine preserved states with new target states
504536
final_active_states = MapSet.union(preserved_states, new_target_set)
537+
538+
# Log configuration change
539+
state_chart =
540+
LogManager.debug(state_chart, "Configuration change (external transition)", %{
541+
action_type: "state_change",
542+
transition_type: "external",
543+
from_states: MapSet.to_list(current_active),
544+
to_states: MapSet.to_list(final_active_states),
545+
exited_states: MapSet.to_list(exit_set),
546+
entered_states: MapSet.to_list(entering_states),
547+
preserved_states: MapSet.to_list(preserved_states)
548+
})
549+
505550
new_config = Configuration.new(MapSet.to_list(final_active_states))
506551

507552
# Update the state chart with the new configuration

0 commit comments

Comments
 (0)