smart_message 0.0.3 → 0.0.5

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.
data/docs/logging.md ADDED
@@ -0,0 +1,452 @@
1
+ # Logging in SmartMessage
2
+
3
+ SmartMessage provides flexible logging capabilities through its plugin architecture. This document covers the built-in default logger as well as how to create custom loggers.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Default Logger](#default-logger)
8
+ - [Configuration Options](#configuration-options)
9
+ - [Output Destinations](#output-destinations)
10
+ - [Log Levels](#log-levels)
11
+ - [Message Lifecycle Logging](#message-lifecycle-logging)
12
+ - [Rails Integration](#rails-integration)
13
+ - [Custom Loggers](#custom-loggers)
14
+ - [Examples](#examples)
15
+
16
+ ## Default Logger
17
+
18
+ SmartMessage includes a built-in `SmartMessage::Logger::Default` class that automatically detects your environment and chooses the best logging approach:
19
+
20
+ - **Rails Applications**: Uses `Rails.logger` with tagged logging
21
+ - **Standalone Ruby**: Uses Ruby's standard `Logger` class with file output
22
+
23
+ ### Quick Start
24
+
25
+ ```ruby
26
+ class MyMessage < SmartMessage::Base
27
+ property :content
28
+
29
+ config do
30
+ transport SmartMessage::Transport::StdoutTransport.new
31
+ serializer SmartMessage::Serializer::JSON.new
32
+ logger SmartMessage::Logger::Default.new # Zero configuration!
33
+ end
34
+ end
35
+ ```
36
+
37
+ ## Configuration Options
38
+
39
+ The default logger accepts several configuration options:
40
+
41
+ ### Basic Configuration
42
+
43
+ ```ruby
44
+ # Use defaults (Rails.logger or log/smart_message.log)
45
+ logger SmartMessage::Logger::Default.new
46
+
47
+ # Custom log file path
48
+ logger SmartMessage::Logger::Default.new(
49
+ log_file: '/var/log/my_app/messages.log'
50
+ )
51
+
52
+ # Custom log level
53
+ logger SmartMessage::Logger::Default.new(
54
+ level: Logger::DEBUG
55
+ )
56
+
57
+ # Both custom file and level
58
+ logger SmartMessage::Logger::Default.new(
59
+ log_file: 'logs/custom.log',
60
+ level: Logger::WARN
61
+ )
62
+ ```
63
+
64
+ ## Output Destinations
65
+
66
+ The default logger supports multiple output destinations:
67
+
68
+ ### File Logging (Default)
69
+
70
+ ```ruby
71
+ # Default file location (Rails convention)
72
+ logger SmartMessage::Logger::Default.new
73
+ # → log/smart_message.log
74
+
75
+ # Custom file path
76
+ logger SmartMessage::Logger::Default.new(
77
+ log_file: '/var/log/application/messages.log'
78
+ )
79
+ ```
80
+
81
+ **Features:**
82
+ - Automatic log rotation (10 files, 10MB each)
83
+ - Directory creation if needed
84
+ - Timestamped entries with clean formatting
85
+
86
+ ### STDOUT Logging
87
+
88
+ Perfect for containerized applications (Docker, Kubernetes):
89
+
90
+ ```ruby
91
+ logger SmartMessage::Logger::Default.new(
92
+ log_file: STDOUT,
93
+ level: Logger::INFO
94
+ )
95
+ ```
96
+
97
+ ### STDERR Logging
98
+
99
+ For error-focused logging:
100
+
101
+ ```ruby
102
+ logger SmartMessage::Logger::Default.new(
103
+ log_file: STDERR,
104
+ level: Logger::WARN
105
+ )
106
+ ```
107
+
108
+ ### In-Memory Logging (Testing)
109
+
110
+ ```ruby
111
+ require 'stringio'
112
+
113
+ logger SmartMessage::Logger::Default.new(
114
+ log_file: StringIO.new,
115
+ level: Logger::DEBUG
116
+ )
117
+ ```
118
+
119
+ ## Log Levels
120
+
121
+ The default logger supports all standard Ruby log levels:
122
+
123
+ | Level | Numeric Value | Description |
124
+ |-------|--------------|-------------|
125
+ | `Logger::DEBUG` | 0 | Detailed debugging information |
126
+ | `Logger::INFO` | 1 | General information messages |
127
+ | `Logger::WARN` | 2 | Warning messages |
128
+ | `Logger::ERROR` | 3 | Error messages |
129
+ | `Logger::FATAL` | 4 | Fatal error messages |
130
+
131
+ ### Environment-Based Defaults
132
+
133
+ The default logger automatically sets appropriate log levels based on your environment:
134
+
135
+ - **Rails Production**: `Logger::INFO`
136
+ - **Rails Test**: `Logger::ERROR`
137
+ - **Rails Development**: `Logger::DEBUG`
138
+ - **Non-Rails**: `Logger::INFO`
139
+
140
+ ## Message Lifecycle Logging
141
+
142
+ The default logger automatically logs key events in the message lifecycle:
143
+
144
+ ### Message Creation
145
+
146
+ ```ruby
147
+ # Logged at DEBUG level
148
+ message = MyMessage.new(content: "Hello")
149
+ # → [DEBUG] [SmartMessage] Created: MyMessage - {content: "Hello"}
150
+ ```
151
+
152
+ ### Message Publishing
153
+
154
+ ```ruby
155
+ # Logged at INFO level
156
+ message.publish
157
+ # → [INFO] [SmartMessage] Published: MyMessage via StdoutTransport
158
+ ```
159
+
160
+ ### Message Reception
161
+
162
+ ```ruby
163
+ # Logged at INFO level when message is received
164
+ # → [INFO] [SmartMessage] Received: MyMessage (45 bytes)
165
+ ```
166
+
167
+ ### Message Processing
168
+
169
+ ```ruby
170
+ # Logged at INFO level after processing
171
+ # → [INFO] [SmartMessage] Processed: MyMessage - Success
172
+ ```
173
+
174
+ ### Subscription Management
175
+
176
+ ```ruby
177
+ # Logged at INFO level
178
+ MyMessage.subscribe
179
+ # → [INFO] [SmartMessage] Subscribed: MyMessage
180
+
181
+ MyMessage.unsubscribe
182
+ # → [INFO] [SmartMessage] Unsubscribed: MyMessage
183
+ ```
184
+
185
+ ### Error Logging
186
+
187
+ ```ruby
188
+ # Logged at ERROR level with full stack trace (DEBUG level)
189
+ # → [ERROR] [SmartMessage] Error in message processing: RuntimeError - Something went wrong
190
+ # → [DEBUG] [SmartMessage] Backtrace: ...
191
+ ```
192
+
193
+ ## Rails Integration
194
+
195
+ When running in a Rails application, the default logger provides enhanced integration:
196
+
197
+ ### Automatic Detection
198
+
199
+ ```ruby
200
+ # Automatically uses Rails.logger when available
201
+ logger SmartMessage::Logger::Default.new
202
+ ```
203
+
204
+ ### Tagged Logging
205
+
206
+ ```ruby
207
+ # In Rails, all SmartMessage logs are tagged
208
+ Rails.logger.tagged('SmartMessage') do
209
+ # All SmartMessage logging happens here
210
+ end
211
+ ```
212
+
213
+ ### Rails Log File Location
214
+
215
+ ```ruby
216
+ # Uses Rails.root/log/smart_message.log when Rails is detected
217
+ logger SmartMessage::Logger::Default.new
218
+ # → Rails.root.join('log', 'smart_message.log')
219
+ ```
220
+
221
+ ### Rails Environment Handling
222
+
223
+ The logger respects Rails environment settings:
224
+
225
+ - **Production**: INFO level, structured logging
226
+ - **Development**: DEBUG level, verbose output
227
+ - **Test**: ERROR level, minimal output
228
+
229
+ ## Custom Loggers
230
+
231
+ You can create custom loggers by inheriting from `SmartMessage::Logger::Base`:
232
+
233
+ ### Basic Custom Logger
234
+
235
+ ```ruby
236
+ class SmartMessage::Logger::MyCustomLogger < SmartMessage::Logger::Base
237
+ def initialize(external_logger)
238
+ @logger = external_logger
239
+ end
240
+
241
+ def log_message_created(message)
242
+ @logger.debug "Created message: #{message.class.name}"
243
+ end
244
+
245
+ def log_message_published(message, transport)
246
+ @logger.info "Published #{message.class.name} via #{transport.class.name}"
247
+ end
248
+
249
+ # Implement other lifecycle methods as needed...
250
+ end
251
+ ```
252
+
253
+ ### Wrapper for Third-Party Loggers
254
+
255
+ ```ruby
256
+ # Semantic Logger example
257
+ class SmartMessage::Logger::SemanticLogger < SmartMessage::Logger::Base
258
+ def initialize(semantic_logger = nil)
259
+ @logger = semantic_logger || SemanticLogger['SmartMessage']
260
+ end
261
+
262
+ def log_message_created(message)
263
+ @logger.debug "Message created", message_class: message.class.name
264
+ end
265
+
266
+ def log_error(context, error)
267
+ @logger.error "Error in #{context}", exception: error
268
+ end
269
+ end
270
+ ```
271
+
272
+ ### Multi-Logger (Broadcast)
273
+
274
+ ```ruby
275
+ class SmartMessage::Logger::MultiLogger < SmartMessage::Logger::Base
276
+ def initialize(*loggers)
277
+ @loggers = loggers
278
+ end
279
+
280
+ def log_message_created(message)
281
+ @loggers.each { |logger| logger.log_message_created(message) }
282
+ end
283
+
284
+ # Other methods follow same pattern...
285
+ end
286
+
287
+ # Usage
288
+ logger SmartMessage::Logger::MultiLogger.new(
289
+ SmartMessage::Logger::Default.new(log_file: 'app.log'),
290
+ SmartMessage::Logger::Default.new(log_file: STDOUT),
291
+ SmartMessage::Logger::MyCustomLogger.new(external_system)
292
+ )
293
+ ```
294
+
295
+ ## Examples
296
+
297
+ ### Development Setup
298
+
299
+ ```ruby
300
+ class OrderMessage < SmartMessage::Base
301
+ property :order_id, String, required: true
302
+ property :customer_id, String, required: true
303
+ property :amount, Float, required: true
304
+
305
+ config do
306
+ transport SmartMessage::Transport::StdoutTransport.new(loopback: true)
307
+ serializer SmartMessage::Serializer::JSON.new
308
+
309
+ # Verbose logging for development
310
+ logger SmartMessage::Logger::Default.new(
311
+ log_file: STDOUT,
312
+ level: Logger::DEBUG
313
+ )
314
+ end
315
+
316
+ def self.process(message_header, message_payload)
317
+ data = JSON.parse(message_payload)
318
+
319
+ # Logger is available in process method
320
+ logger.info "Processing order #{data['order_id']}"
321
+
322
+ # Business logic here
323
+ result = process_order(data)
324
+
325
+ logger.info "Order #{data['order_id']} completed: #{result}"
326
+ result
327
+ end
328
+ end
329
+ ```
330
+
331
+ ### Production Setup
332
+
333
+ ```ruby
334
+ class NotificationMessage < SmartMessage::Base
335
+ property :recipient, String, required: true
336
+ property :subject, String, required: true
337
+ property :body, String, required: true
338
+
339
+ config do
340
+ transport SmartMessage::Transport::RedisTransport.new
341
+ serializer SmartMessage::Serializer::JSON.new
342
+
343
+ # Production logging with file rotation
344
+ logger SmartMessage::Logger::Default.new(
345
+ log_file: '/var/log/app/notifications.log',
346
+ level: Logger::INFO
347
+ )
348
+ end
349
+ end
350
+ ```
351
+
352
+ ### Docker/Kubernetes Setup
353
+
354
+ ```ruby
355
+ class EventMessage < SmartMessage::Base
356
+ property :event_type, String, required: true
357
+ property :data, Hash, required: true
358
+
359
+ config do
360
+ transport SmartMessage::Transport::RedisTransport.new(
361
+ redis_url: ENV['REDIS_URL']
362
+ )
363
+ serializer SmartMessage::Serializer::JSON.new
364
+
365
+ # Container-friendly STDOUT logging
366
+ logger SmartMessage::Logger::Default.new(
367
+ log_file: STDOUT,
368
+ level: ENV['LOG_LEVEL']&.upcase&.to_sym || Logger::INFO
369
+ )
370
+ end
371
+ end
372
+ ```
373
+
374
+ ### Testing Setup
375
+
376
+ ```ruby
377
+ # In test_helper.rb or similar
378
+ class TestMessage < SmartMessage::Base
379
+ property :test_data, Hash
380
+
381
+ config do
382
+ transport SmartMessage::Transport::MemoryTransport.new
383
+ serializer SmartMessage::Serializer::JSON.new
384
+
385
+ # Minimal logging for tests
386
+ logger SmartMessage::Logger::Default.new(
387
+ log_file: StringIO.new,
388
+ level: Logger::FATAL # Only fatal errors in tests
389
+ )
390
+ end
391
+ end
392
+ ```
393
+
394
+ ### Instance-Level Logger Override
395
+
396
+ ```ruby
397
+ # Different logger for specific instances
398
+ class PriorityMessage < SmartMessage::Base
399
+ property :priority, String
400
+ property :data, Hash
401
+
402
+ config do
403
+ transport SmartMessage::Transport::RedisTransport.new
404
+ serializer SmartMessage::Serializer::JSON.new
405
+ logger SmartMessage::Logger::Default.new # Default logger
406
+ end
407
+ end
408
+
409
+ # Override logger for high-priority messages
410
+ priority_logger = SmartMessage::Logger::Default.new(
411
+ log_file: '/var/log/priority.log',
412
+ level: Logger::DEBUG
413
+ )
414
+
415
+ urgent_message = PriorityMessage.new(priority: 'urgent', data: {...})
416
+ urgent_message.logger(priority_logger) # Override for this instance
417
+ urgent_message.publish
418
+ ```
419
+
420
+ ## Best Practices
421
+
422
+ 1. **Use the default logger** unless you have specific requirements
423
+ 2. **Log to STDOUT** in containerized environments
424
+ 3. **Use appropriate log levels** - avoid DEBUG in production
425
+ 4. **Tag your logs** for better searchability
426
+ 5. **Consider structured logging** for production systems
427
+ 6. **Test your logging** - ensure logs are helpful for debugging
428
+ 7. **Monitor log volume** - excessive logging can impact performance
429
+ 8. **Rotate log files** to prevent disk space issues (default logger handles this)
430
+
431
+ ## Troubleshooting
432
+
433
+ ### No logs appearing
434
+ - Check log level settings
435
+ - Verify file permissions
436
+ - Ensure logger is configured
437
+
438
+ ### Too much logging
439
+ - Increase log level (DEBUG → INFO → WARN → ERROR)
440
+ - Consider filtering in production
441
+
442
+ ### Performance issues
443
+ - Lower log level in production
444
+ - Use asynchronous logging for high-volume systems
445
+ - Consider structured logging formats
446
+
447
+ ### Rails integration not working
448
+ - Ensure Rails is loaded before SmartMessage
449
+ - Check that `Rails.logger` is available
450
+ - Verify Rails environment is set correctly
451
+
452
+ For more troubleshooting tips, see the [Troubleshooting Guide](troubleshooting.md).
data/docs/properties.md CHANGED
@@ -4,6 +4,7 @@ The SmartMessage property system builds on Hashie::Dash to provide a robust, dec
4
4
 
5
5
  ## Table of Contents
6
6
  - [Basic Property Definition](#basic-property-definition)
7
+ - [Schema Versioning](#schema-versioning)
7
8
  - [Class-Level Description](#class-level-description)
8
9
  - [Property Options](#property-options)
9
10
  - [Accessing Property Information](#accessing-property-information)
@@ -20,9 +21,115 @@ class MyMessage < SmartMessage::Base
20
21
  end
21
22
  ```
22
23
 
24
+ ## Schema Versioning
25
+
26
+ SmartMessage supports schema versioning to enable message evolution while maintaining compatibility:
27
+
28
+ ```ruby
29
+ class OrderMessage < SmartMessage::Base
30
+ version 2 # Declare schema version
31
+
32
+ property :order_id, required: true
33
+ property :customer_email # Added in version 2
34
+ end
35
+ ```
36
+
37
+ ### Version Management
38
+
39
+ ```ruby
40
+ # Version 1 message
41
+ class V1OrderMessage < SmartMessage::Base
42
+ version 1 # or omit for default version 1
43
+
44
+ property :order_id, required: true
45
+ property :amount, required: true
46
+ end
47
+
48
+ # Version 2 message with additional field
49
+ class V2OrderMessage < SmartMessage::Base
50
+ version 2
51
+
52
+ property :order_id, required: true
53
+ property :amount, required: true
54
+ property :customer_email # New in version 2
55
+ end
56
+
57
+ # Version 3 message with validation
58
+ class V3OrderMessage < SmartMessage::Base
59
+ version 3
60
+
61
+ property :order_id,
62
+ required: true,
63
+ validate: ->(v) { v.is_a?(String) && v.length > 0 }
64
+
65
+ property :amount,
66
+ required: true,
67
+ validate: ->(v) { v.is_a?(Numeric) && v > 0 }
68
+
69
+ property :customer_email,
70
+ validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
71
+ end
72
+ ```
73
+
74
+ ### Version Validation
75
+
76
+ The framework automatically validates version compatibility:
77
+
78
+ ```ruby
79
+ message = V2OrderMessage.new(order_id: "123", amount: 99.99)
80
+ # Header automatically gets version: 2
81
+
82
+ # Version validation happens automatically
83
+ message.validate! # Validates message + header + version compatibility
84
+
85
+ # Manual version validation
86
+ message.validate_header_version! # Checks header version matches class version
87
+
88
+ # Check version information
89
+ V2OrderMessage.version # => 2
90
+ V2OrderMessage.expected_header_version # => 2
91
+ message._sm_header.version # => 2
92
+ ```
93
+
94
+ ### Version Evolution Patterns
95
+
96
+ ```ruby
97
+ # Pattern 1: Additive changes (safe)
98
+ class UserMessageV1 < SmartMessage::Base
99
+ version 1
100
+ property :user_id, required: true
101
+ property :name, required: true
102
+ end
103
+
104
+ class UserMessageV2 < SmartMessage::Base
105
+ version 2
106
+ property :user_id, required: true
107
+ property :name, required: true
108
+ property :email # Optional addition - backward compatible
109
+ end
110
+
111
+ # Pattern 2: Field validation evolution
112
+ class ProductMessageV1 < SmartMessage::Base
113
+ version 1
114
+ property :product_id, required: true
115
+ property :price, required: true
116
+ end
117
+
118
+ class ProductMessageV2 < SmartMessage::Base
119
+ version 2
120
+ property :product_id,
121
+ required: true,
122
+ validate: ->(v) { v.is_a?(String) && v.match?(/\APROD-\d+\z/) }
123
+
124
+ property :price,
125
+ required: true,
126
+ validate: ->(v) { v.is_a?(Numeric) && v > 0 }
127
+ end
128
+ ```
129
+
23
130
  ## Class-Level Description
24
131
 
25
- In addition to property-level descriptions, you can add a description for the entire message class:
132
+ In addition to property-level descriptions, you can add a description for the entire message class using the `description` DSL method:
26
133
 
27
134
  ```ruby
28
135
  class OrderMessage < SmartMessage::Base
@@ -35,15 +142,29 @@ end
35
142
  # Access the class description
36
143
  OrderMessage.description # => "Handles order processing and fulfillment workflow"
37
144
 
38
- # Class descriptions can also be set after class definition
145
+ # Instance access to class description
146
+ order = OrderMessage.new(order_id: "123", amount: 9999)
147
+ order.description # => "Handles order processing and fulfillment workflow"
148
+ ```
149
+
150
+ ### Setting Descriptions
151
+
152
+ Class descriptions can be set in multiple ways:
153
+
154
+ ```ruby
155
+ # 1. During class definition
39
156
  class PaymentMessage < SmartMessage::Base
157
+ description "Processes payment transactions"
40
158
  property :payment_id
41
159
  end
42
160
 
43
- PaymentMessage.description "Processes payment transactions"
44
- PaymentMessage.description # => "Processes payment transactions"
161
+ # 2. After class definition
162
+ class RefundMessage < SmartMessage::Base
163
+ property :refund_id
164
+ end
165
+ RefundMessage.description "Handles payment refunds and reversals"
45
166
 
46
- # Can be set within config block
167
+ # 3. Within config block
47
168
  class NotificationMessage < SmartMessage::Base
48
169
  config do
49
170
  description "Sends notifications to users"
@@ -53,13 +174,41 @@ class NotificationMessage < SmartMessage::Base
53
174
  end
54
175
  ```
55
176
 
177
+ ### Default Descriptions
178
+
179
+ Classes without explicit descriptions automatically receive a default description:
180
+
181
+ ```ruby
182
+ class MyMessage < SmartMessage::Base
183
+ property :data
184
+ end
185
+
186
+ MyMessage.description # => "MyMessage is a SmartMessage"
187
+
188
+ # This applies to all unnamed message classes
189
+ class SomeModule::ComplexMessage < SmartMessage::Base
190
+ property :info
191
+ end
192
+
193
+ SomeModule::ComplexMessage.description
194
+ # => "SomeModule::ComplexMessage is a SmartMessage"
195
+ ```
196
+
197
+ ### Use Cases
198
+
56
199
  Class descriptions are useful for:
57
200
  - Documenting the overall purpose of a message class
58
201
  - Providing context for code generation tools
59
202
  - Integration with documentation systems
60
203
  - API documentation generation
204
+ - Dynamic message introspection in gateway applications
61
205
 
62
- Note: Class descriptions are not inherited by subclasses. Each class maintains its own description.
206
+ ### Important Notes
207
+
208
+ - Class descriptions are not inherited by subclasses - each class maintains its own description
209
+ - Setting a description to `nil` will revert to the default description
210
+ - Descriptions are stored as strings and can include multiline content
211
+ - Both class and instance methods are available to access descriptions
63
212
 
64
213
  ## Property Options
65
214
 
@@ -181,7 +330,64 @@ msg.tags # => ['important'] (Array)
181
330
  msg.metadata # => {} (Hash)
182
331
  ```
183
332
 
184
- ### 6. Property Descriptions (SmartMessage Enhancement)
333
+ ### 6. Property Validation (SmartMessage Enhancement)
334
+
335
+ Add custom validation logic to ensure data integrity:
336
+
337
+ ```ruby
338
+ class ValidatedMessage < SmartMessage::Base
339
+ # Lambda validation
340
+ property :age,
341
+ validate: ->(v) { v.is_a?(Integer) && v.between?(1, 120) },
342
+ validation_message: "Age must be an integer between 1 and 120"
343
+
344
+ # Regex validation for email
345
+ property :email,
346
+ validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
347
+ validation_message: "Must be a valid email address"
348
+
349
+ # Array inclusion validation
350
+ property :status,
351
+ validate: ['active', 'inactive', 'pending'],
352
+ validation_message: "Status must be active, inactive, or pending"
353
+
354
+ # Range validation
355
+ property :score,
356
+ validate: (0..100),
357
+ validation_message: "Score must be between 0 and 100"
358
+
359
+ # Class type validation
360
+ property :created_at,
361
+ validate: Time,
362
+ validation_message: "Must be a Time object"
363
+
364
+ # Symbol method validation
365
+ property :username,
366
+ validate: :valid_username?,
367
+ validation_message: "Username contains invalid characters"
368
+
369
+ private
370
+
371
+ def valid_username?(value)
372
+ value.to_s.match?(/\A[a-zA-Z0-9_]+\z/)
373
+ end
374
+ end
375
+
376
+ # Validation usage
377
+ message = ValidatedMessage.new(age: 25, email: "test@example.com")
378
+
379
+ # Validate entire message
380
+ message.validate! # Raises SmartMessage::Errors::ValidationError on failure
381
+ message.valid? # Returns true/false
382
+
383
+ # Get validation errors
384
+ errors = message.validation_errors
385
+ errors.each do |error|
386
+ puts "#{error[:property]}: #{error[:message]}"
387
+ end
388
+ ```
389
+
390
+ ### 7. Property Descriptions (SmartMessage Enhancement)
185
391
 
186
392
  Add human-readable descriptions to document your properties for dynamic LLM integration:
187
393
 
@@ -0,0 +1,2 @@
1
+ /log/
2
+ *.log