Skip to content

STDOUT Transport

The STDOUT Transport is a development and debugging transport that outputs messages to the console or files in human-readable formats. It inherits from FileTransport and provides specialized output formatting capabilities.

Overview

STDOUT Transport is perfect for: - Development debugging - See messages in real-time during development - Application logging - Structured message logging to files or console - Message tracing - Track message flow through systems - Integration testing - Verify message content and flow - Format testing - Demonstrate different message serialization formats

Key Features

  • ๐Ÿ“„ Multiple Output Formats - Pretty, JSON Lines, and compact JSON
  • ๐Ÿ–ฅ๏ธ Console or File Output - Direct to STDOUT or file paths
  • ๐Ÿ”„ Optional Loopback - Process messages locally after output
  • ๐Ÿงต Thread-Safe - Safe for concurrent message publishing
  • ๐Ÿ› ๏ธ No Dependencies - Built-in Ruby formatting capabilities
  • ๐ŸŽจ Pretty Printing - Human-readable format using amazing_print

Architecture

Publisher โ†’ StdoutTransport โ†’ Console/File Output โ†’ Optional Loopback โ†’ Local Processing
         (format selection)   (thread-safe)      (if enabled)      (message handling)

STDOUT Transport inherits from FileTransport and adds specialized console output capabilities with multiple formatting options.

Configuration

Basic Setup

# Minimal configuration (outputs to console)
transport = SmartMessage::Transport::StdoutTransport.new

# With file output
transport = SmartMessage::Transport::StdoutTransport.new(
  file_path: '/var/log/messages.log'
)

# With format specification
transport = SmartMessage::Transport::StdoutTransport.new(
  format: :pretty,
  loopback: true
)

# Full configuration
transport = SmartMessage::Transport::StdoutTransport.new(
  file_path: '/var/log/app.log',
  format: :jsonl,
  loopback: false,
  auto_create_dirs: true
)

Using with SmartMessage

# Configure as default transport
SmartMessage.configure do |config|
  config.default_transport = SmartMessage::Transport::StdoutTransport.new(
    format: :pretty
  )
end

# Use in message class
class LogMessage < SmartMessage::Base
  property :level, required: true
  property :message, required: true
  property :timestamp, default: -> { Time.now.iso8601 }

  transport :stdout

  def process
    puts "Log entry processed: #{level} - #{message}"
  end
end

Configuration Options

Option Type Default Description
format Symbol :pretty Output format (:pretty, :jsonl, :json)
file_path String nil File path for output (nil = STDOUT)
loopback Boolean false Process messages locally after output
auto_create_dirs Boolean true Automatically create parent directories

Output Formats

Pretty Format (:pretty)

Uses amazing_print for human-readable, colorized output:

transport = SmartMessage::Transport::StdoutTransport.new(format: :pretty)

class UserMessage < SmartMessage::Base
  property :name
  property :email
  transport transport
end

UserMessage.new(name: "Alice Johnson", email: "alice@example.com").publish

# Output:
# {
#     :name => "Alice Johnson",
#     :email => "alice@example.com"
# }

JSON Lines Format (:jsonl)

One JSON object per line, ideal for log processing:

transport = SmartMessage::Transport::StdoutTransport.new(format: :jsonl)

UserMessage.new(name: "Bob Smith", email: "bob@example.com").publish
UserMessage.new(name: "Carol Williams", email: "carol@example.com").publish

# Output:
# {"name":"Bob Smith","email":"bob@example.com"}
# {"name":"Carol Williams","email":"carol@example.com"}

Compact JSON Format (:json)

Compact JSON with no newlines:

transport = SmartMessage::Transport::StdoutTransport.new(format: :json)

UserMessage.new(name: "David Brown", email: "david@example.com").publish

# Output:
# {"name":"David Brown","email":"david@example.com"}

Usage Examples

Development Debugging

class DebugMessage < SmartMessage::Base
  property :component, required: true
  property :action, required: true
  property :data
  property :timestamp, default: -> { Time.now }

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :pretty,
    loopback: true
  )

  def process
    puts "[DEBUG] #{component}.#{action} completed at #{timestamp}"
  end
end

# Publishing shows both formatted output and processes locally
DebugMessage.new(
  component: "UserService",
  action: "create_user",
  data: { user_id: 123, email: "user@example.com" }
).publish

# Output (formatted):
# {
#     :component => "UserService",
#        :action => "create_user",
#          :data => {
#         :user_id => 123,
#            :email => "user@example.com"
#     },
#     :timestamp => 2024-01-15 10:30:45 -0800
# }
# [DEBUG] UserService.create_user completed at 2024-01-15 10:30:45 -0800

Application Logging

class ApplicationLog < SmartMessage::Base
  property :level, required: true, validation: %w[DEBUG INFO WARN ERROR FATAL]
  property :message, required: true
  property :module_name
  property :timestamp, default: -> { Time.now.iso8601 }

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: '/var/log/application.log'
  )
end

# Log entries
ApplicationLog.new(
  level: "INFO",
  message: "Application started successfully",
  module_name: "Main"
).publish

ApplicationLog.new(
  level: "ERROR",
  message: "Database connection failed",
  module_name: "DatabaseConnector"
).publish

# File contents (/var/log/application.log):
# {"level":"INFO","message":"Application started successfully","module_name":"Main","timestamp":"2024-01-15T18:30:45Z"}
# {"level":"ERROR","message":"Database connection failed","module_name":"DatabaseConnector","timestamp":"2024-01-15T18:30:46Z"}

Format Comparison Demo

class DemoMessage < SmartMessage::Base
  property :first_name, description: "Person's first name"
  property :last_name, description: "Person's last name"
end

# Pretty format example
puts "=== Pretty Format ==="
DemoMessage.new(first_name: "Alice", last_name: "Johnson").tap do |msg|
  msg.transport(SmartMessage::Transport::StdoutTransport.new(format: :pretty))
end.publish

# JSON Lines format example  
puts "\n=== JSON Lines Format ==="
transport_jsonl = SmartMessage::Transport::StdoutTransport.new(format: :jsonl)
DemoMessage.new(first_name: "Bob", last_name: "Smith").tap do |msg|
  msg.transport(transport_jsonl)
end.publish
DemoMessage.new(first_name: "Carol", last_name: "Williams").tap do |msg|
  msg.transport(transport_jsonl)
end.publish

# JSON format example
puts "\n=== JSON Format ==="
transport_json = SmartMessage::Transport::StdoutTransport.new(format: :json)
DemoMessage.new(first_name: "David", last_name: "Brown").tap do |msg|
  msg.transport(transport_json)
end.publish
DemoMessage.new(first_name: "Emma", last_name: "Davis").tap do |msg|
  msg.transport(transport_json)  
end.publish
DemoMessage.new(first_name: "Frank", last_name: "Miller").tap do |msg|
  msg.transport(transport_json)
end.publish

Integration Testing

class TestMessage < SmartMessage::Base
  property :test_id, required: true
  property :expected_result
  property :actual_result
  property :status, default: 'pending'

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: '/tmp/test_results.log',
    loopback: true
  )

  def process
    self.status = (expected_result == actual_result) ? 'passed' : 'failed'
    puts "Test #{test_id}: #{status}"
  end
end

# Test execution
TestMessage.new(
  test_id: "AUTH_001",
  expected_result: "authenticated",
  actual_result: "authenticated"
).publish

# Outputs to both file and console via loopback processing

File Output Management

Automatic Directory Creation

# Creates parent directories automatically
transport = SmartMessage::Transport::StdoutTransport.new(
  file_path: '/var/log/app/events/user_actions.log',
  auto_create_dirs: true  # Creates /var/log/app/events/ if needed
)

File Rotation and Management

# Daily log rotation example
class DailyLogger < SmartMessage::Base
  property :event, required: true
  property :data

  def self.current_log_path
    date_str = Time.now.strftime("%Y-%m-%d")
    "/var/log/app/daily/#{date_str}.log"
  end

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: current_log_path
  )
end

# Each day's events go to separate files
DailyLogger.new(event: "user_login", data: { user_id: 123 }).publish

API Reference

Instance Methods

#format

Returns the current output format.

transport = SmartMessage::Transport::StdoutTransport.new(format: :jsonl)
puts transport.format  # => :jsonl

#file_path

Returns the configured file path (nil for STDOUT).

transport = SmartMessage::Transport::StdoutTransport.new(file_path: '/tmp/output.log')
puts transport.file_path  # => '/tmp/output.log'

#loopback?

Checks if loopback processing is enabled.

transport = SmartMessage::Transport::StdoutTransport.new(loopback: true)
puts transport.loopback?  # => true

#publish(message)

Publishes a message object with the configured format.

transport.publish(message_instance)

Performance Characteristics

  • Latency: ~1ms (I/O dependent)
  • Throughput: Limited by I/O operations (console/file writes)
  • Memory Usage: Minimal (immediate output)
  • Threading: Thread-safe file operations
  • Format Overhead: Pretty > JSON Lines > JSON

Use Cases

Development Environment

# config/environments/development.rb
SmartMessage.configure do |config|
  config.default_transport = SmartMessage::Transport::StdoutTransport.new(
    format: :pretty,
    loopback: true
  )
  config.logger.level = Logger::DEBUG
end

Testing Environment

# config/environments/test.rb
SmartMessage.configure do |config|
  config.default_transport = SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: Rails.root.join('tmp', 'test_messages.log')
  )
end

CI/CD Pipeline Integration

class CIMessage < SmartMessage::Base
  property :stage, required: true
  property :status, required: true
  property :duration
  property :details

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: ENV['CI_LOG_PATH'] || '/tmp/ci_pipeline.log'
  )
end

# CI stages can publish structured logs
CIMessage.new(
  stage: "build",
  status: "success", 
  duration: 45.2,
  details: { artifacts: 3, warnings: 0 }
).publish

Microservices Debugging

class ServiceMessage < SmartMessage::Base
  property :service_name, required: true
  property :operation, required: true
  property :request_id
  property :response_time
  property :status_code

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :pretty,
    loopback: false
  )
end

# Service calls generate readable debug output
ServiceMessage.new(
  service_name: "UserService",
  operation: "authenticate",
  request_id: "req_123",
  response_time: 23.4,
  status_code: 200
).publish

Best Practices

Development

  • Use :pretty format for interactive debugging
  • Enable loopback to process messages locally
  • Use descriptive property names for clarity

Production Logging

  • Use :jsonl format for structured logs
  • Specify file paths with rotation patterns
  • Disable loopback unless local processing needed

Testing

  • Use file output to capture test message flows
  • Use :json format for minimal output
  • Clear log files between test runs

Performance

  • :json format has lowest overhead
  • Console output is slower than file output
  • Consider asynchronous logging for high-volume scenarios

Thread Safety

STDOUT Transport is fully thread-safe: - File operations use proper locking - Format operations are stateless - Multiple threads can publish concurrently

# Thread-safe concurrent publishing
threads = []
10.times do |i|
  threads << Thread.new do
    100.times do |j|
      LogMessage.new(
        level: "INFO",
        message: "Thread #{i}, Message #{j}"
      ).publish
    end
  end
end
threads.each(&:join)

Error Handling

class ErrorMessage < SmartMessage::Base
  property :error_type, required: true
  property :error_message, required: true
  property :stack_trace

  transport SmartMessage::Transport::StdoutTransport.new(
    format: :jsonl,
    file_path: '/var/log/errors.log'
  )

  def process
    # Handle error processing
    case error_type
    when 'critical'
      send_alert(error_message)
    when 'warning'
      log_warning(error_message)
    end
  end
end

begin
  risky_operation()
rescue => e
  ErrorMessage.new(
    error_type: 'critical',
    error_message: e.message,
    stack_trace: e.backtrace.join("\n")
  ).publish
end

Migration Patterns

From Console to File Logging

# Development: Console output
transport = SmartMessage::Transport::StdoutTransport.new(format: :pretty)

# Production: File logging  
transport = SmartMessage::Transport::StdoutTransport.new(
  format: :jsonl,
  file_path: '/var/log/production.log'
)

Format Evolution

# Start with pretty format for development
# Move to JSONL for production structured logging
# Upgrade to specialized transports (Redis) for distribution

case Rails.env
when 'development'
  SmartMessage::Transport::StdoutTransport.new(format: :pretty, loopback: true)
when 'test'  
  SmartMessage::Transport::StdoutTransport.new(format: :jsonl, file_path: 'tmp/test.log')
when 'production'
  SmartMessage::Transport::RedisTransport.new(url: ENV['REDIS_URL'])
end

Examples

The STDOUT Transport is demonstrated in: - examples/memory/06_stdout_publish_only.rb - Comprehensive format demonstration with all three output formats

Running Examples

# Navigate to SmartMessage directory
cd smart_message

# Run the STDOUT Transport format demo
ruby examples/memory/06_stdout_publish_only.rb

# Shows all three formats:
# - Pretty format output
# - JSON Lines format output  
# - Compact JSON format output