smart_message 0.0.16 โ 0.0.17
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/CHANGELOG.md +64 -0
- data/Gemfile.lock +4 -4
- data/README.md +10 -6
- data/docs/guides/transport-selection.md +361 -0
- data/docs/reference/transports.md +42 -18
- data/docs/transports/file-transport.md +535 -0
- data/docs/transports/multi-transport.md +1 -1
- data/docs/transports/redis-transport.md +1 -1
- data/docs/transports/stdout-transport.md +580 -0
- data/examples/memory/06_stdout_publish_only.rb +70 -97
- data/lib/smart_message/transport/file_operations.rb +39 -7
- data/lib/smart_message/transport/file_transport.rb +5 -2
- data/lib/smart_message/transport/stdout_transport.rb +5 -93
- data/lib/smart_message/version.rb +1 -1
- data/mkdocs.yml +4 -5
- metadata +4 -2
- data/examples/memory/log/demo_app.log.1 +0 -100
@@ -0,0 +1,535 @@
|
|
1
|
+
# File Transport
|
2
|
+
|
3
|
+
The **File Transport** is a base class for file-based message transports in SmartMessage. It provides the foundation for writing messages to files with automatic directory creation, message serialization, and thread-safe operations.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
File Transport serves as the base class for:
|
8
|
+
- **STDOUT Transport** - Console and file output with formatting
|
9
|
+
- **Custom File Transports** - Application-specific file-based messaging
|
10
|
+
- **Log Transport Extensions** - Specialized logging implementations
|
11
|
+
- **Message Persistence** - File-based message storage and archiving
|
12
|
+
|
13
|
+
## Key Features
|
14
|
+
|
15
|
+
- ๐ **Automatic Directory Creation** - Creates parent directories as needed
|
16
|
+
- ๐งต **Thread-Safe Operations** - Safe for concurrent message publishing
|
17
|
+
- ๐ **Message Serialization** - Handles SmartMessage object encoding
|
18
|
+
- ๐ **File Append Operations** - Messages appended to existing files
|
19
|
+
- โ๏ธ **Extensible Architecture** - Base class for specialized file transports
|
20
|
+
- ๐ก๏ธ **Error Handling** - Graceful handling of file system errors
|
21
|
+
|
22
|
+
## Architecture
|
23
|
+
|
24
|
+
```
|
25
|
+
Message โ FileTransport โ encode_message() โ do_publish() โ File System
|
26
|
+
(base class) (serialization) (file write) (thread-safe)
|
27
|
+
```
|
28
|
+
|
29
|
+
File Transport provides the core infrastructure that derived classes like STDOUT Transport build upon.
|
30
|
+
|
31
|
+
## Class Hierarchy
|
32
|
+
|
33
|
+
```
|
34
|
+
SmartMessage::Transport::BaseTransport
|
35
|
+
โโโ SmartMessage::Transport::FileTransport
|
36
|
+
โโโ SmartMessage::Transport::StdoutTransport
|
37
|
+
```
|
38
|
+
|
39
|
+
## Configuration
|
40
|
+
|
41
|
+
### Basic Setup
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Direct usage (rarely used directly)
|
45
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
46
|
+
file_path: '/var/log/messages.log'
|
47
|
+
)
|
48
|
+
|
49
|
+
# With options
|
50
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
51
|
+
file_path: '/var/log/app/events.log',
|
52
|
+
auto_create_dirs: true
|
53
|
+
)
|
54
|
+
```
|
55
|
+
|
56
|
+
### Inheritance Pattern
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# Custom transport inheriting from FileTransport
|
60
|
+
class CustomFileTransport < SmartMessage::Transport::FileTransport
|
61
|
+
def initialize(file_path:, custom_option: nil, **options)
|
62
|
+
@custom_option = custom_option
|
63
|
+
super(file_path: file_path, **options)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def do_publish(message_class, serialized_message)
|
69
|
+
# Custom formatting before file write
|
70
|
+
formatted_content = format_for_custom_system(serialized_message)
|
71
|
+
|
72
|
+
# Use parent's file writing capability
|
73
|
+
super(message_class, formatted_content)
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_for_custom_system(message)
|
77
|
+
# Custom formatting logic
|
78
|
+
"#{Time.now.iso8601}: #{message}\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## Configuration Options
|
84
|
+
|
85
|
+
| Option | Type | Default | Description |
|
86
|
+
|--------|------|---------|-------------|
|
87
|
+
| `file_path` | String | **Required** | Path to output file |
|
88
|
+
| `auto_create_dirs` | Boolean | `true` | Automatically create parent directories |
|
89
|
+
|
90
|
+
## Core Methods
|
91
|
+
|
92
|
+
### Public Interface
|
93
|
+
|
94
|
+
#### `#publish(message)`
|
95
|
+
Publishes a SmartMessage object to the configured file.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
99
|
+
file_path: '/var/log/messages.log'
|
100
|
+
)
|
101
|
+
|
102
|
+
message = MyMessage.new(data: "example")
|
103
|
+
transport.publish(message)
|
104
|
+
```
|
105
|
+
|
106
|
+
#### `#file_path`
|
107
|
+
Returns the configured file path.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
puts transport.file_path # => '/var/log/messages.log'
|
111
|
+
```
|
112
|
+
|
113
|
+
#### `#connected?`
|
114
|
+
Always returns `true` for file system availability.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
puts transport.connected? # => true
|
118
|
+
```
|
119
|
+
|
120
|
+
### Protected Interface (for Subclasses)
|
121
|
+
|
122
|
+
#### `#encode_message(message)`
|
123
|
+
Serializes a SmartMessage object using the configured serializer.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class MyFileTransport < SmartMessage::Transport::FileTransport
|
127
|
+
private
|
128
|
+
|
129
|
+
def do_publish(message_class, serialized_message)
|
130
|
+
# serialized_message comes from encode_message(message)
|
131
|
+
File.write(file_path, "#{serialized_message}\n", mode: 'a')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
#### `#do_publish(message_class, serialized_message)`
|
137
|
+
Template method for subclasses to implement file writing logic.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Base implementation in FileTransport
|
141
|
+
def do_publish(message_class, serialized_message)
|
142
|
+
File.write(file_path, "#{serialized_message}\n", mode: 'a')
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
## Implementation Details
|
147
|
+
|
148
|
+
### Message Processing Pipeline
|
149
|
+
|
150
|
+
1. **Message Receipt**: `publish(message)` called with SmartMessage object
|
151
|
+
2. **Class Extraction**: Extract message class name from `message._sm_header.message_class`
|
152
|
+
3. **Serialization**: Convert message to string via `encode_message(message)`
|
153
|
+
4. **File Writing**: Call `do_publish(message_class, serialized_message)`
|
154
|
+
5. **Directory Creation**: Create parent directories if needed
|
155
|
+
6. **Thread Safety**: File operations protected for concurrent access
|
156
|
+
|
157
|
+
### Source Code Structure
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class FileTransport < BaseTransport
|
161
|
+
def initialize(file_path:, auto_create_dirs: true, **options)
|
162
|
+
@file_path = file_path
|
163
|
+
@auto_create_dirs = auto_create_dirs
|
164
|
+
super(**options)
|
165
|
+
end
|
166
|
+
|
167
|
+
def publish(message)
|
168
|
+
# Extract message class and serialize the message
|
169
|
+
message_class = message._sm_header.message_class
|
170
|
+
serialized_message = encode_message(message)
|
171
|
+
do_publish(message_class, serialized_message)
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def do_publish(message_class, serialized_message)
|
177
|
+
ensure_directory_exists
|
178
|
+
File.write(file_path, "#{serialized_message}\n", mode: 'a')
|
179
|
+
end
|
180
|
+
|
181
|
+
def ensure_directory_exists
|
182
|
+
return unless auto_create_dirs
|
183
|
+
|
184
|
+
dir = File.dirname(file_path)
|
185
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
## Usage Examples
|
191
|
+
|
192
|
+
### Basic File Logging
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class LogMessage < SmartMessage::Base
|
196
|
+
property :level, required: true
|
197
|
+
property :message, required: true
|
198
|
+
property :timestamp, default: -> { Time.now.iso8601 }
|
199
|
+
|
200
|
+
transport SmartMessage::Transport::FileTransport.new(
|
201
|
+
file_path: '/var/log/application.log'
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
LogMessage.new(
|
206
|
+
level: "INFO",
|
207
|
+
message: "Application started"
|
208
|
+
).publish
|
209
|
+
|
210
|
+
# File contains JSON-serialized message
|
211
|
+
```
|
212
|
+
|
213
|
+
### Custom File Transport
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class AuditFileTransport < SmartMessage::Transport::FileTransport
|
217
|
+
def initialize(file_path:, include_headers: true, **options)
|
218
|
+
@include_headers = include_headers
|
219
|
+
super(file_path: file_path, **options)
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def do_publish(message_class, serialized_message)
|
225
|
+
ensure_directory_exists
|
226
|
+
|
227
|
+
content = if @include_headers
|
228
|
+
"#{Time.now.iso8601} [#{message_class}] #{serialized_message}\n"
|
229
|
+
else
|
230
|
+
"#{serialized_message}\n"
|
231
|
+
end
|
232
|
+
|
233
|
+
File.write(file_path, content, mode: 'a')
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Usage
|
238
|
+
class AuditMessage < SmartMessage::Base
|
239
|
+
property :action, required: true
|
240
|
+
property :user_id, required: true
|
241
|
+
|
242
|
+
transport AuditFileTransport.new(
|
243
|
+
file_path: '/var/log/audit.log',
|
244
|
+
include_headers: true
|
245
|
+
)
|
246
|
+
end
|
247
|
+
|
248
|
+
AuditMessage.new(action: "login", user_id: 123).publish
|
249
|
+
```
|
250
|
+
|
251
|
+
### Rotated Log Files
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
class RotatedFileTransport < SmartMessage::Transport::FileTransport
|
255
|
+
def initialize(base_path:, **options)
|
256
|
+
@base_path = base_path
|
257
|
+
super(file_path: current_log_file, **options)
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def current_log_file
|
263
|
+
date_str = Time.now.strftime("%Y-%m-%d")
|
264
|
+
"#{@base_path}/#{date_str}.log"
|
265
|
+
end
|
266
|
+
|
267
|
+
def do_publish(message_class, serialized_message)
|
268
|
+
# Update file path for current date
|
269
|
+
@file_path = current_log_file
|
270
|
+
super(message_class, serialized_message)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Usage
|
275
|
+
class DailyMessage < SmartMessage::Base
|
276
|
+
property :event, required: true
|
277
|
+
|
278
|
+
transport RotatedFileTransport.new(
|
279
|
+
base_path: '/var/log/daily'
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Messages automatically go to /var/log/daily/2024-01-15.log
|
284
|
+
DailyMessage.new(event: "user_action").publish
|
285
|
+
```
|
286
|
+
|
287
|
+
## Directory Management
|
288
|
+
|
289
|
+
### Automatic Directory Creation
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
# Creates /var/log/app/subsystem/ if it doesn't exist
|
293
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
294
|
+
file_path: '/var/log/app/subsystem/events.log',
|
295
|
+
auto_create_dirs: true # default
|
296
|
+
)
|
297
|
+
```
|
298
|
+
|
299
|
+
### Manual Directory Management
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# Disable automatic creation
|
303
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
304
|
+
file_path: '/existing/path/events.log',
|
305
|
+
auto_create_dirs: false
|
306
|
+
)
|
307
|
+
|
308
|
+
# Create directories manually
|
309
|
+
FileUtils.mkdir_p('/var/log/custom')
|
310
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
311
|
+
file_path: '/var/log/custom/events.log'
|
312
|
+
)
|
313
|
+
```
|
314
|
+
|
315
|
+
## Thread Safety
|
316
|
+
|
317
|
+
File Transport is fully thread-safe:
|
318
|
+
- File append operations are atomic
|
319
|
+
- Directory creation is protected
|
320
|
+
- Multiple threads can publish concurrently
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
transport = SmartMessage::Transport::FileTransport.new(
|
324
|
+
file_path: '/tmp/concurrent.log'
|
325
|
+
)
|
326
|
+
|
327
|
+
class TestMessage < SmartMessage::Base
|
328
|
+
property :thread_id
|
329
|
+
property :sequence
|
330
|
+
transport transport
|
331
|
+
end
|
332
|
+
|
333
|
+
# Thread-safe concurrent publishing
|
334
|
+
threads = []
|
335
|
+
5.times do |thread_id|
|
336
|
+
threads << Thread.new do
|
337
|
+
10.times do |sequence|
|
338
|
+
TestMessage.new(
|
339
|
+
thread_id: thread_id,
|
340
|
+
sequence: sequence
|
341
|
+
).publish
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
threads.each(&:join)
|
346
|
+
|
347
|
+
# All 50 messages safely written to file
|
348
|
+
```
|
349
|
+
|
350
|
+
## Error Handling
|
351
|
+
|
352
|
+
### File System Errors
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
begin
|
356
|
+
message.publish
|
357
|
+
rescue Errno::ENOENT => e
|
358
|
+
puts "Directory doesn't exist: #{e.message}"
|
359
|
+
rescue Errno::EACCES => e
|
360
|
+
puts "Permission denied: #{e.message}"
|
361
|
+
rescue Errno::ENOSPC => e
|
362
|
+
puts "No space left on device: #{e.message}"
|
363
|
+
end
|
364
|
+
```
|
365
|
+
|
366
|
+
### Custom Error Handling
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
class SafeFileTransport < SmartMessage::Transport::FileTransport
|
370
|
+
private
|
371
|
+
|
372
|
+
def do_publish(message_class, serialized_message)
|
373
|
+
super(message_class, serialized_message)
|
374
|
+
rescue => e
|
375
|
+
# Log error and fall back to alternate location
|
376
|
+
fallback_path = "/tmp/fallback_#{File.basename(file_path)}"
|
377
|
+
File.write(fallback_path, "#{serialized_message}\n", mode: 'a')
|
378
|
+
warn "File transport error: #{e.message}, using fallback: #{fallback_path}"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
```
|
382
|
+
|
383
|
+
## Performance Characteristics
|
384
|
+
|
385
|
+
- **Latency**: ~1-5ms (filesystem dependent)
|
386
|
+
- **Throughput**: Limited by I/O operations
|
387
|
+
- **Memory Usage**: Minimal (immediate write)
|
388
|
+
- **Concurrency**: Thread-safe with file locking
|
389
|
+
- **Disk Usage**: Grows with message volume
|
390
|
+
|
391
|
+
## Extension Patterns
|
392
|
+
|
393
|
+
### Formatted Output Transport
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
class FormattedFileTransport < SmartMessage::Transport::FileTransport
|
397
|
+
def initialize(file_path:, format: :json, **options)
|
398
|
+
@format = format
|
399
|
+
super(file_path: file_path, **options)
|
400
|
+
end
|
401
|
+
|
402
|
+
private
|
403
|
+
|
404
|
+
def do_publish(message_class, serialized_message)
|
405
|
+
content = case @format
|
406
|
+
when :csv
|
407
|
+
to_csv(message_class, serialized_message)
|
408
|
+
when :xml
|
409
|
+
to_xml(message_class, serialized_message)
|
410
|
+
else
|
411
|
+
serialized_message
|
412
|
+
end
|
413
|
+
|
414
|
+
File.write(file_path, "#{content}\n", mode: 'a')
|
415
|
+
end
|
416
|
+
|
417
|
+
def to_csv(message_class, data)
|
418
|
+
# Convert JSON to CSV format
|
419
|
+
parsed = JSON.parse(data)
|
420
|
+
parsed.values.join(',')
|
421
|
+
end
|
422
|
+
|
423
|
+
def to_xml(message_class, data)
|
424
|
+
# Convert JSON to XML format
|
425
|
+
"<message class=\"#{message_class}\">#{data}</message>"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
```
|
429
|
+
|
430
|
+
### Buffered File Transport
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
class BufferedFileTransport < SmartMessage::Transport::FileTransport
|
434
|
+
def initialize(file_path:, buffer_size: 100, **options)
|
435
|
+
@buffer_size = buffer_size
|
436
|
+
@buffer = []
|
437
|
+
@buffer_mutex = Mutex.new
|
438
|
+
super(file_path: file_path, **options)
|
439
|
+
end
|
440
|
+
|
441
|
+
private
|
442
|
+
|
443
|
+
def do_publish(message_class, serialized_message)
|
444
|
+
@buffer_mutex.synchronize do
|
445
|
+
@buffer << serialized_message
|
446
|
+
|
447
|
+
if @buffer.size >= @buffer_size
|
448
|
+
flush_buffer
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def flush_buffer
|
454
|
+
return if @buffer.empty?
|
455
|
+
|
456
|
+
content = @buffer.join("\n") + "\n"
|
457
|
+
File.write(file_path, content, mode: 'a')
|
458
|
+
@buffer.clear
|
459
|
+
end
|
460
|
+
|
461
|
+
public
|
462
|
+
|
463
|
+
def close
|
464
|
+
@buffer_mutex.synchronize { flush_buffer }
|
465
|
+
end
|
466
|
+
end
|
467
|
+
```
|
468
|
+
|
469
|
+
## Best Practices
|
470
|
+
|
471
|
+
### Configuration
|
472
|
+
- Use absolute paths for file_path
|
473
|
+
- Enable auto_create_dirs for robustness
|
474
|
+
- Consider log rotation for long-running applications
|
475
|
+
|
476
|
+
### Performance
|
477
|
+
- Use buffered writes for high-volume scenarios
|
478
|
+
- Monitor disk space usage
|
479
|
+
- Consider asynchronous variants for critical paths
|
480
|
+
|
481
|
+
### Error Handling
|
482
|
+
- Implement fallback locations for critical messages
|
483
|
+
- Monitor file system permissions
|
484
|
+
- Handle disk full scenarios gracefully
|
485
|
+
|
486
|
+
### Testing
|
487
|
+
- Use temporary directories in tests
|
488
|
+
- Clean up test files in teardown
|
489
|
+
- Mock file operations for unit tests
|
490
|
+
|
491
|
+
## Testing Support
|
492
|
+
|
493
|
+
### Test Helpers
|
494
|
+
|
495
|
+
```ruby
|
496
|
+
class TestFileTransport < SmartMessage::Transport::FileTransport
|
497
|
+
attr_reader :written_messages
|
498
|
+
|
499
|
+
def initialize(**options)
|
500
|
+
@written_messages = []
|
501
|
+
super(file_path: '/dev/null', **options)
|
502
|
+
end
|
503
|
+
|
504
|
+
private
|
505
|
+
|
506
|
+
def do_publish(message_class, serialized_message)
|
507
|
+
@written_messages << {
|
508
|
+
message_class: message_class,
|
509
|
+
content: serialized_message,
|
510
|
+
timestamp: Time.now
|
511
|
+
}
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# Usage in tests
|
516
|
+
RSpec.describe "Message Publishing" do
|
517
|
+
let(:transport) { TestFileTransport.new }
|
518
|
+
|
519
|
+
it "publishes messages" do
|
520
|
+
MyMessage.transport = transport
|
521
|
+
MyMessage.new(data: "test").publish
|
522
|
+
|
523
|
+
expect(transport.written_messages).to have(1).item
|
524
|
+
expect(transport.written_messages.first[:message_class]).to eq("MyMessage")
|
525
|
+
end
|
526
|
+
end
|
527
|
+
```
|
528
|
+
|
529
|
+
## Related Documentation
|
530
|
+
|
531
|
+
- [STDOUT Transport](stdout-transport.md) - File Transport implementation with formatting
|
532
|
+
- [Transport Overview](../reference/transports.md) - All available transports
|
533
|
+
- [Redis Transport](redis-transport.md) - Distributed messaging transport
|
534
|
+
- [Memory Transport](memory-transport.md) - In-memory development transport
|
535
|
+
- [Troubleshooting Guide](../development/troubleshooting.md) - Testing and debugging strategies
|
@@ -481,4 +481,4 @@ end
|
|
481
481
|
- [Redis Queue Transport](redis-transport.md)
|
482
482
|
- [Memory Transport](memory-transport.md)
|
483
483
|
- [Error Handling and Dead Letter Queues](../reference/dead-letter-queue.md)
|
484
|
-
- [
|
484
|
+
- [Troubleshooting Guide](../development/troubleshooting.md)
|
@@ -480,7 +480,7 @@ Each example includes comprehensive logging and demonstrates production-ready pa
|
|
480
480
|
### Additional Resources
|
481
481
|
|
482
482
|
For more Redis Transport examples and patterns, also see:
|
483
|
-
- **[Memory Transport Examples](
|
483
|
+
- **[Memory Transport Examples](https://github.com/MadBomber/smart_message/tree/main/examples/memory)** - Can be adapted to Redis Transport by changing configuration
|
484
484
|
- **[Complete Documentation](https://github.com/MadBomber/smart_message/blob/main/examples/redis/smart_home_iot_dataflow.md)** - Detailed data flow analysis with SVG diagrams
|
485
485
|
|
486
486
|
## Related Documentation
|