anzen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +51 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/README.md +321 -0
- data/Rakefile +23 -0
- data/bin/anzen +168 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/anzen/README_API.md +608 -0
- data/lib/anzen/cli.rb +301 -0
- data/lib/anzen/configuration.rb +254 -0
- data/lib/anzen/exceptions.rb +130 -0
- data/lib/anzen/monitor.rb +85 -0
- data/lib/anzen/monitors/call_stack_depth.rb +147 -0
- data/lib/anzen/monitors/memory.rb +228 -0
- data/lib/anzen/monitors/recursion.rb +193 -0
- data/lib/anzen/registry.rb +169 -0
- data/lib/anzen/version.rb +5 -0
- data/lib/anzen.rb +210 -0
- data/sig/anzen.rbs +4 -0
- metadata +74 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anzen
|
|
4
|
+
# Registry for managing safety monitors
|
|
5
|
+
#
|
|
6
|
+
# Handles registration, lifecycle management, and coordination of monitors.
|
|
7
|
+
# Thread-safe for monitor state queries.
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
class Registry
|
|
11
|
+
# Initialize Registry
|
|
12
|
+
def initialize
|
|
13
|
+
@monitors = {}
|
|
14
|
+
@enabled_monitors = Set.new
|
|
15
|
+
@violations_count = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Register a monitor
|
|
20
|
+
#
|
|
21
|
+
# @param monitor [Anzen::Monitor] monitor instance implementing Monitor interface
|
|
22
|
+
# @raise [InvalidMonitorError] if monitor doesn't implement required interface
|
|
23
|
+
# @raise [MonitorNameConflictError] if monitor name already registered
|
|
24
|
+
def register(monitor)
|
|
25
|
+
validate_monitor_interface(monitor)
|
|
26
|
+
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
raise Anzen::MonitorNameConflictError.new(monitor.name) if @monitors.key?(monitor.name)
|
|
29
|
+
|
|
30
|
+
@monitors[monitor.name] = monitor
|
|
31
|
+
@violations_count[monitor.name] = 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Unregister a monitor
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] monitor name
|
|
38
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
39
|
+
# @raise [InvalidMonitorError] if monitor is currently enabled
|
|
40
|
+
def unregister(name)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
raise Anzen::MonitorNotFoundError.new(name) unless @monitors.key?(name)
|
|
43
|
+
|
|
44
|
+
if @enabled_monitors.include?(name)
|
|
45
|
+
raise Anzen::InvalidMonitorError.new("Cannot unregister enabled monitor '#{name}'")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@monitors.delete(name)
|
|
49
|
+
@violations_count.delete(name)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get monitor by name
|
|
54
|
+
#
|
|
55
|
+
# @param name [String] monitor name
|
|
56
|
+
# @return [Anzen::Monitor] monitor instance
|
|
57
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
58
|
+
def get(name)
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
raise Anzen::MonitorNotFoundError.new(name) unless @monitors.key?(name)
|
|
61
|
+
|
|
62
|
+
@monitors[name]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# List all registered monitors
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Anzen::Monitor>] all monitors
|
|
69
|
+
def list
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
@monitors.values.dup
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# List enabled monitors
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<Anzen::Monitor>] enabled monitors
|
|
78
|
+
def list_enabled
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
@enabled_monitors.map { |name| @monitors[name] }.dup
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Enable a monitor by name
|
|
85
|
+
#
|
|
86
|
+
# @param name [String] monitor name
|
|
87
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
88
|
+
def enable(name)
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
raise Anzen::MonitorNotFoundError.new(name) unless @monitors.key?(name)
|
|
91
|
+
|
|
92
|
+
monitor = @monitors[name]
|
|
93
|
+
monitor.enable
|
|
94
|
+
@enabled_monitors.add(name)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Disable a monitor by name
|
|
99
|
+
#
|
|
100
|
+
# @param name [String] monitor name
|
|
101
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
102
|
+
def disable(name)
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
raise Anzen::MonitorNotFoundError.new(name) unless @monitors.key?(name)
|
|
105
|
+
|
|
106
|
+
monitor = @monitors[name]
|
|
107
|
+
monitor.disable
|
|
108
|
+
@enabled_monitors.delete(name)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Run all enabled monitors' checks
|
|
113
|
+
#
|
|
114
|
+
# Calls check! on each enabled monitor in order.
|
|
115
|
+
# Raises first violation immediately (fail-fast).
|
|
116
|
+
#
|
|
117
|
+
# @return [nil]
|
|
118
|
+
# @raise [ViolationError] subclass on first violation detected
|
|
119
|
+
# @raise [CheckFailedError] on infrastructure failure
|
|
120
|
+
def check_all!
|
|
121
|
+
enabled = @mutex.synchronize { @enabled_monitors.dup }
|
|
122
|
+
|
|
123
|
+
enabled.each do |name|
|
|
124
|
+
monitor = @mutex.synchronize { @monitors[name] }
|
|
125
|
+
monitor.check!
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return status of all monitors
|
|
132
|
+
#
|
|
133
|
+
# @return [Hash] aggregated status with keys:
|
|
134
|
+
# - monitors (Array): status of each monitor
|
|
135
|
+
# - enabled_count (Integer): number of enabled monitors
|
|
136
|
+
# - violations_total (Integer): total violations across all monitors
|
|
137
|
+
def status
|
|
138
|
+
@mutex.synchronize do
|
|
139
|
+
monitor_statuses = @monitors.values.map(&:status)
|
|
140
|
+
enabled_count = @enabled_monitors.size
|
|
141
|
+
violations_total = @monitors.values.sum { |m| m.status[:violations] }
|
|
142
|
+
|
|
143
|
+
{
|
|
144
|
+
monitors: monitor_statuses,
|
|
145
|
+
enabled_count: enabled_count,
|
|
146
|
+
violations_total: violations_total
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# Validate monitor implements required interface
|
|
154
|
+
#
|
|
155
|
+
# @param monitor [Object] monitor to validate
|
|
156
|
+
# @raise [InvalidMonitorError] if monitor doesn't implement interface
|
|
157
|
+
def validate_monitor_interface(monitor)
|
|
158
|
+
required_methods = %i[name enable disable enabled? check! status to_cli]
|
|
159
|
+
|
|
160
|
+
required_methods.each do |method|
|
|
161
|
+
next if monitor.respond_to?(method)
|
|
162
|
+
|
|
163
|
+
raise Anzen::InvalidMonitorError.new(
|
|
164
|
+
"Monitor must implement ##{method} method"
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/anzen.rb
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'anzen/version'
|
|
4
|
+
require_relative 'anzen/exceptions'
|
|
5
|
+
require_relative 'anzen/monitor'
|
|
6
|
+
require_relative 'anzen/monitors/call_stack_depth'
|
|
7
|
+
require_relative 'anzen/monitors/recursion'
|
|
8
|
+
require_relative 'anzen/monitors/memory'
|
|
9
|
+
require_relative 'anzen/registry'
|
|
10
|
+
require_relative 'anzen/configuration'
|
|
11
|
+
|
|
12
|
+
# Anzen - Runtime safety protection gem
|
|
13
|
+
#
|
|
14
|
+
# Provides safety monitoring for recursion, memory, and other runtime concerns.
|
|
15
|
+
# Use Anzen.setup to initialize with monitors and configuration.
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# Anzen.setup(config: { enabled_monitors: ['recursion'], monitors: { recursion: { depth_limit: 1000 } } })
|
|
19
|
+
# # ... your code ...
|
|
20
|
+
# Anzen.check! # Raises if any monitor detects violation
|
|
21
|
+
#
|
|
22
|
+
# @api public
|
|
23
|
+
module Anzen
|
|
24
|
+
# @!visibility private
|
|
25
|
+
@@registry = nil
|
|
26
|
+
|
|
27
|
+
# @!visibility private
|
|
28
|
+
@@initialized = false
|
|
29
|
+
|
|
30
|
+
# @!visibility private
|
|
31
|
+
@@setup_at = nil
|
|
32
|
+
|
|
33
|
+
# Reset Anzen state (for testing only)
|
|
34
|
+
#
|
|
35
|
+
# @private
|
|
36
|
+
# @api private
|
|
37
|
+
def self._reset_for_testing
|
|
38
|
+
@@registry = nil
|
|
39
|
+
@@initialized = false
|
|
40
|
+
@@setup_at = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Setup Anzen with configuration and monitors
|
|
44
|
+
#
|
|
45
|
+
# Initializes the registry, creates and registers default monitors (call_stack_depth, recursion, memory),
|
|
46
|
+
# and enables specified ones based on configuration sources (programmatic, env var, or file).
|
|
47
|
+
# Can only be called once per process.
|
|
48
|
+
#
|
|
49
|
+
# @param config [Hash] configuration hash with keys:
|
|
50
|
+
# - config_file (String): path to YAML/JSON config file (optional)
|
|
51
|
+
# - enabled_monitors (Array): list of monitor names to enable
|
|
52
|
+
# - monitors (Hash): per-monitor configurations
|
|
53
|
+
# @raise [InitializationError] if Anzen is already initialized
|
|
54
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
# @example Programmatic setup
|
|
58
|
+
# Anzen.setup(config: {
|
|
59
|
+
# enabled_monitors: ['recursion'],
|
|
60
|
+
# monitors: { recursion: { depth_limit: 500 } }
|
|
61
|
+
# })
|
|
62
|
+
#
|
|
63
|
+
# @example Environment variable setup
|
|
64
|
+
# ENV['ANZEN_CONFIG'] = '{"enabled_monitors": ["memory"], "monitors": {"memory": {"limit_mb": 1024}}}'
|
|
65
|
+
# Anzen.setup # Uses env var config
|
|
66
|
+
#
|
|
67
|
+
# @example File-based setup
|
|
68
|
+
# Anzen.setup(config: { config_file: 'config/anzen.yml' })
|
|
69
|
+
def self.setup(config: {})
|
|
70
|
+
raise Anzen::InitializationError if @@initialized
|
|
71
|
+
|
|
72
|
+
@@registry = Registry.new
|
|
73
|
+
|
|
74
|
+
# Determine configuration source
|
|
75
|
+
configuration = if config.key?(:config_file)
|
|
76
|
+
Configuration.from_file(config[:config_file])
|
|
77
|
+
elsif ENV['ANZEN_CONFIG']
|
|
78
|
+
Configuration.from_env
|
|
79
|
+
else
|
|
80
|
+
Configuration.programmatic(config)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Register CallStackDepthMonitor
|
|
84
|
+
depth_limit = 1000
|
|
85
|
+
begin
|
|
86
|
+
depth_limit = configuration.monitor_config('call_stack_depth')['depth_limit']
|
|
87
|
+
rescue Anzen::ConfigurationError
|
|
88
|
+
# Use default if not configured
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
call_stack_depth_monitor = Monitors::CallStackDepthMonitor.new(depth_limit: depth_limit)
|
|
92
|
+
@@registry.register(call_stack_depth_monitor)
|
|
93
|
+
|
|
94
|
+
# Register RecursionMonitor
|
|
95
|
+
recursion_config = {}
|
|
96
|
+
begin
|
|
97
|
+
recursion_config = configuration.monitor_config('recursion')
|
|
98
|
+
rescue Anzen::ConfigurationError
|
|
99
|
+
# Use defaults if not configured
|
|
100
|
+
end
|
|
101
|
+
depth_limit = recursion_config['depth_limit'] || 1000
|
|
102
|
+
|
|
103
|
+
recursion_monitor = Monitors::RecursionMonitor.new(depth_limit: depth_limit)
|
|
104
|
+
@@registry.register(recursion_monitor)
|
|
105
|
+
|
|
106
|
+
# Register MemoryMonitor
|
|
107
|
+
memory_config = {}
|
|
108
|
+
begin
|
|
109
|
+
memory_config = configuration.monitor_config('memory')
|
|
110
|
+
rescue Anzen::ConfigurationError
|
|
111
|
+
# Use defaults if not configured
|
|
112
|
+
end
|
|
113
|
+
limit_mb = memory_config['limit_mb'] || 512
|
|
114
|
+
sampling_interval_ms = memory_config['sampling_interval_ms'] || 100
|
|
115
|
+
|
|
116
|
+
memory_monitor = Monitors::MemoryMonitor.new(
|
|
117
|
+
limit_mb: limit_mb,
|
|
118
|
+
sampling_interval_ms: sampling_interval_ms
|
|
119
|
+
)
|
|
120
|
+
@@registry.register(memory_monitor)
|
|
121
|
+
|
|
122
|
+
# Enable specified monitors
|
|
123
|
+
@@registry.enable('call_stack_depth') if configuration.monitor_enabled?('call_stack_depth')
|
|
124
|
+
@@registry.enable('recursion') if configuration.monitor_enabled?('recursion')
|
|
125
|
+
@@registry.enable('memory') if configuration.monitor_enabled?('memory')
|
|
126
|
+
|
|
127
|
+
@@setup_at = Time.now
|
|
128
|
+
@@initialized = true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Enable a monitor by name
|
|
132
|
+
#
|
|
133
|
+
# @param name [String] monitor name
|
|
134
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
135
|
+
# @return [void]
|
|
136
|
+
def self.enable(name)
|
|
137
|
+
ensure_initialized
|
|
138
|
+
@@registry.enable(name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Disable a monitor by name
|
|
142
|
+
#
|
|
143
|
+
# @param name [String] monitor name
|
|
144
|
+
# @raise [MonitorNotFoundError] if monitor not found
|
|
145
|
+
# @return [void]
|
|
146
|
+
def self.disable(name)
|
|
147
|
+
ensure_initialized
|
|
148
|
+
@@registry.disable(name)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Execute all enabled monitors' checks
|
|
152
|
+
#
|
|
153
|
+
# Runs check! on each enabled monitor. Raises immediately if any violation detected.
|
|
154
|
+
#
|
|
155
|
+
# @raise [ViolationError] subclass on first violation detected
|
|
156
|
+
# @raise [CheckFailedError] on infrastructure failure
|
|
157
|
+
# @return [nil]
|
|
158
|
+
def self.check!
|
|
159
|
+
ensure_initialized
|
|
160
|
+
@@registry.check_all!
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Return status of all monitors
|
|
164
|
+
#
|
|
165
|
+
# @return [Hash] status hash with keys:
|
|
166
|
+
# - enabled (Array): array of enabled monitor names
|
|
167
|
+
# - enabled_count (Integer): number of enabled monitors
|
|
168
|
+
# - setup_at (Time): when Anzen was initialized
|
|
169
|
+
# - monitors (Array): array of monitor status hashes with keys: name, enabled, thresholds, last_check, violations
|
|
170
|
+
# - violations_total (Integer): total violations across all monitors
|
|
171
|
+
def self.status
|
|
172
|
+
ensure_initialized
|
|
173
|
+
registry_status = @@registry.status
|
|
174
|
+
|
|
175
|
+
# Transform registry status to API contract format
|
|
176
|
+
enabled_monitor_names = @@registry.instance_variable_get(:@enabled_monitors).to_a
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
monitors: registry_status[:monitors],
|
|
180
|
+
enabled: enabled_monitor_names,
|
|
181
|
+
enabled_count: registry_status[:enabled_count],
|
|
182
|
+
violations_total: registry_status[:violations_total],
|
|
183
|
+
setup_at: @@setup_at
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Register a custom monitor
|
|
188
|
+
#
|
|
189
|
+
# Monitor must implement the Monitor interface.
|
|
190
|
+
#
|
|
191
|
+
# @param monitor [Anzen::Monitor] monitor instance
|
|
192
|
+
# @raise [InvalidMonitorError] if monitor doesn't implement required interface
|
|
193
|
+
# @raise [MonitorNameConflictError] if monitor name already registered
|
|
194
|
+
# @return [void]
|
|
195
|
+
def self.register_monitor(monitor)
|
|
196
|
+
ensure_initialized
|
|
197
|
+
@@registry.register(monitor)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class << self
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
# Ensure Anzen is initialized
|
|
204
|
+
#
|
|
205
|
+
# @raise [InitializationError] if not initialized
|
|
206
|
+
def ensure_initialized
|
|
207
|
+
raise Anzen::InitializationError, 'Anzen not initialized. Call Anzen.setup first.' unless @@initialized
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
data/sig/anzen.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: anzen
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Korakot Leemakdej
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Anzen detects and prevents bad code patterns (recursive call stacks,
|
|
14
|
+
memory overflow) before they crash your production system. Provides pluggable monitors
|
|
15
|
+
for safety protection via Ruby API and CLI interface.
|
|
16
|
+
email:
|
|
17
|
+
- kleemakdej@gmail.com
|
|
18
|
+
executables:
|
|
19
|
+
- anzen
|
|
20
|
+
- console
|
|
21
|
+
- setup
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- ".rspec"
|
|
26
|
+
- ".rubocop.yml"
|
|
27
|
+
- CHANGELOG.md
|
|
28
|
+
- CODE_OF_CONDUCT.md
|
|
29
|
+
- README.md
|
|
30
|
+
- Rakefile
|
|
31
|
+
- bin/anzen
|
|
32
|
+
- bin/console
|
|
33
|
+
- bin/setup
|
|
34
|
+
- lib/anzen.rb
|
|
35
|
+
- lib/anzen/README_API.md
|
|
36
|
+
- lib/anzen/cli.rb
|
|
37
|
+
- lib/anzen/configuration.rb
|
|
38
|
+
- lib/anzen/exceptions.rb
|
|
39
|
+
- lib/anzen/monitor.rb
|
|
40
|
+
- lib/anzen/monitors/call_stack_depth.rb
|
|
41
|
+
- lib/anzen/monitors/memory.rb
|
|
42
|
+
- lib/anzen/monitors/recursion.rb
|
|
43
|
+
- lib/anzen/registry.rb
|
|
44
|
+
- lib/anzen/version.rb
|
|
45
|
+
- sig/anzen.rbs
|
|
46
|
+
homepage: https://github.com/korakot-leemakdej/anzen
|
|
47
|
+
licenses:
|
|
48
|
+
- MIT
|
|
49
|
+
metadata:
|
|
50
|
+
allowed_push_host: https://rubygems.org
|
|
51
|
+
homepage_uri: https://github.com/korakot-leemakdej/anzen
|
|
52
|
+
source_code_uri: https://github.com/korakotlee/anzen
|
|
53
|
+
changelog_uri: https://github.com/korakotlee/anzen/blob/main/CHANGELOG.md
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
post_install_message:
|
|
56
|
+
rdoc_options: []
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 3.0.0
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
requirements: []
|
|
70
|
+
rubygems_version: 3.5.9
|
|
71
|
+
signing_key:
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Runtime safety protection gem for Ruby applications
|
|
74
|
+
test_files: []
|