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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anzen
4
+ VERSION = '0.1.0'
5
+ 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
@@ -0,0 +1,4 @@
1
+ module Anzen
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []