staugaard-cloudmaster 0.1.3 → 0.1.4

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,36 @@
1
+ require 'policy'
2
+
3
+ module Cloudmaster
4
+
5
+ # Provide manual policy.
6
+ # This policy only changes the instances when requested to do so.
7
+ # This implementation uses a queue to convey manual requests
8
+ # to the policy module.
9
+ class PolicyManual < Policy
10
+ def initialize(reporter, config, instances)
11
+ super(reporter, config, instances)
12
+ @config = config
13
+ @sqs = AwsContext.instance.sqs
14
+ manual_queue_name = @config.append_env(config[:manual_queue_name])
15
+ @manual_queue = NamedQueue.new(manual_queue_name)
16
+ end
17
+
18
+ # Adjust never changes instances.
19
+ def adjust
20
+ n = 0
21
+ # Read all the messages out of the manual queue.
22
+ # Sum up all adjustments.
23
+ while true
24
+ messages = @manual_queue.read_messages(10)
25
+ break(n) if messages.size == 0
26
+ messages.each do |message|
27
+ msg = YAML.load(message[:body])
28
+ n += msg[:adjust]
29
+ @manual_queue.delete_message(message[:receipt_handle])
30
+ end
31
+ end
32
+ # the value of the while is n
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,110 @@
1
+
2
+ require 'policy'
3
+
4
+ module Cloudmaster
5
+
6
+ # Provides resource policy implementation.
7
+ # Instances managed under a resource policy are expected to issue
8
+ # periodic status messages, giving their estimated load (generally
9
+ # between 0 and 1).
10
+ class PolicyResource < Policy
11
+ # Each policy object gets the configuration and the instance collection.
12
+ def initialize(reporter, config, instances)
13
+ super(reporter, config, instances)
14
+ @config = config
15
+ end
16
+
17
+ # Activate the given number of shut_down instances.
18
+ # We prefer those with highest load.
19
+ # Return the number actually activated.
20
+ def activate_shut_down_instances(number_to_activate)
21
+ shutdown_instances = @instances.shut_down_instances.sort do |a,b|
22
+ b.load_estimate - a.load_estimate
23
+ end
24
+ shutdown_instances = shutdown_instances[0..number_to_activate]
25
+ shutdown_instances.each { |i| i.activate }
26
+ shutdown_instances.each { |i| @reporter.info("Activating instance ", i.id) }
27
+ shutdown_instances.size
28
+ end
29
+
30
+ # Shut down the given instances, by changing their state to shut_down.
31
+ def shut_down_instances(instances_to_shut_down)
32
+ instances = @instances.shut_down(instances_to_shut_down)
33
+ instances.each {|i| @reporter.info("Shutting down instance ", i.id) }
34
+ instances.size
35
+ end
36
+
37
+ # Shut down the given number of instances.
38
+ # Shut down the ones with the lowest load.
39
+ def shut_down_n_instances(number_to_shut_down)
40
+ return if number_to_shut_down <= 0
41
+ instances_with_lowest_load = @instances.sorted_by_lowest_load
42
+ instances_to_shut_down = instances_with_lowest_load.find_all do |instance|
43
+ # Don't stop instances before minimum_active_time
44
+ instance.minimum_active_time_elapsed?
45
+ end
46
+ shut_down_instances(instances_to_shut_down[0...number_to_shut_down])
47
+ end
48
+
49
+ # Stop any shut down instances with load below threshold.
50
+ # Also stop instances that have exceeded shut_down_interval.
51
+ def clean_up_shut_down_instances
52
+ idle_instances = @instances.shut_down_idle_instances
53
+ timeout_instances = @instances.shut_down_timeout_instances
54
+ stop_instances(idle_instances | timeout_instances)
55
+ end
56
+
57
+ # Adjust the instance pool up or down.
58
+ # If no instance are running, and there are requests in the work queue, start
59
+ # some.
60
+ # Additional instances are added if the load is too high.
61
+ # Instances are shut down, and then stopped if the load is low.
62
+ def adjust
63
+ depth = @config[:work_queue].empty_queue
64
+ if @instances.active_instances.size == 0
65
+ # capacity consumed by new arrivals
66
+ new_load = depth.to_f / @config[:queue_load_factor].to_f
67
+ initial = (new_load / @config[:target_upper_load].to_f).ceil
68
+ @reporter.info("Resource policy need initial #{initial} depth: #{depth} new_load #{new_load}") if initial > 0
69
+ return initial
70
+ end
71
+ if depth > 0
72
+ @reporter.info("Resource policy residual depth: #{depth}")
73
+ return 0
74
+ end
75
+ # the total capacity remaining below the upper bound
76
+ excess_capacity = @instances.excess_capacity
77
+ if excess_capacity == 0
78
+ # need this many more running at upper bound
79
+ over_capacity = @instances.over_capacity
80
+ additional = (over_capacity / @config[:target_upper_load].to_f).ceil
81
+ @reporter.info("Resource policy need additional #{additional} depth: #{depth} over_capacity #{over_capacity}")
82
+ return additional
83
+ end
84
+ # how many are needed to carry the total load at the lower bound
85
+ needed = (@instances.total_load / @config[:target_lower_load].to_f).ceil
86
+ if needed < @instances.size
87
+ excess = @instances.size - needed
88
+ @reporter.info("Resource policy need fewer #{excess} depth: #{depth} needed #{needed}")
89
+ return -excess
90
+ end
91
+ return 0
92
+ end
93
+
94
+ # We are not using the default apply, because we want to:
95
+ # * activate shut down instances, if posible, otherwise start
96
+ # * shut down instances if fewer are needed
97
+ # * stop inactive or expired shut_down instances
98
+ def apply
99
+ n = @limit_policy.adjust(adjust)
100
+ case
101
+ when n > 0
102
+ n -= activate_shut_down_instances(n)
103
+ start_instances(n)
104
+ when n < 0
105
+ shut_down_n_instances(-n)
106
+ end
107
+ clean_up_shut_down_instances
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,172 @@
1
+ require 'named_queue'
2
+ require 'ec2_image_enumerator'
3
+
4
+ module Cloudmaster
5
+
6
+ # All configuration parameters passed in through the constructor.
7
+ # Items with * must be defined
8
+ #
9
+ # ==aws_config==
10
+ # aws_env -- used to form queue, instance, and s3 key names --
11
+ # typically development|test|production
12
+ # *aws_access_key -- the AWS access key
13
+ # *aws_secret_key -- the AWS secret key
14
+ # *aws_user -- the user name, used to build the image name
15
+ # *aws_bucket -- the bucket to use when storing the active set
16
+ # *aws_keypair -- full path name of the keypair file to use for
17
+ # connecting to instances
18
+ #
19
+ # ==config==
20
+ # ===GENERAL===
21
+ # *name -- the name of this config
22
+ # *policy -- :none, :job, :resource
23
+ # ===QUEUES===
24
+ # poll_interval -- how often to check work queue, etc (seconds)
25
+ # receive_count -- how many status messages to receive at once
26
+ # *work_queue -- name of work queue (aws_env)
27
+ # *status_queue -- name of status queue (aws_env)
28
+ # ===ACTIVE SET===
29
+ # active_set_type -- which active set algorithm to use: :none, :s3, :queue
30
+ # active_set_bucket -- the S3 bucket to use to store the active set
31
+ # active_set_key -- the S3 key used to store the active set (aws_env)
32
+ # active_set_interval -- how often to write active_set
33
+ # ===INSTANCE CREATION PARAMETERS===
34
+ # *ami_name -- the ami name to start and monitor (aws_env)
35
+ # key_pair_name -- the name if the keypair to start the instance with
36
+ # security_groups -- array of security group names to start the instance with
37
+ # instance_type -- the smi instance type to create
38
+ # user_data -- instance data made available to running instance
39
+ # through http://169.254.169.254/latest/user-data
40
+ # This is given as a hash, which is serialized by cloudmaster.
41
+ #
42
+ # ===INSTANCE MANAGEMENT POLICIES===
43
+ # policy_interval -- how often to apply job or resource policy
44
+ # audit_instance_interval -- how often (in minutes) to audit instances (-1 for never)
45
+ # maximum_number_of_instances -- the max number to allow
46
+ # minimum_number_of_instances -- the min number to allow
47
+ # ===INSTANCE START POLICIES===
48
+ # start_limit -- how many instances to start at one time
49
+ # ===INSTANCE STOP POLICIES===
50
+ # stop_limit -- how many to stop at one time
51
+ # minimum_lifetime -- don't stop an instance unless it has run this long (minutes)
52
+ # minimum_active_time -- the minimum amount of time (in minutes) that an instance
53
+ # may remain in the active state
54
+ # watchdog_interval -- if a machine does not report status in this interval, it is
55
+ # considered to be hung, and is stopped
56
+ # ===JOB POLICIES===
57
+ # start_threshold -- if work queue size is greater than start_threshold * number of
58
+ # active instances, start more instances
59
+ # idle_threshold -- if more than idle_threshold active instances with load 0
60
+ # exist, stop some of them
61
+ # ===RESOURCE POLICIES===
62
+ # target_upper_load -- try to keep instances below this load
63
+ # target_lower_load -- try to keep instances above this load
64
+ # queue_load_factor -- the portion of the load that a single queue entry represents.
65
+ # If a server can serve a maximum of 10 clients, then this is 10.
66
+ # shut_down_threshold -- stop instances that have load_estimate below this value
67
+ # shut_down_interval -- stop instances that have been in shut_down state for
68
+ # longer than this interval
69
+ # ===MANUAL POLICIES===
70
+ # manual_queue_name -- the name of the queue used to send manual instance adjustments
71
+ # ===REPORTING===
72
+ # summary_interval -- how often to give summary
73
+ # instance_log -- if set, it is a patname to a directory where individual log files
74
+ # are written for each instance
75
+ # instance_report_interval -- how often to show instance reports
76
+
77
+ # PoolConfiguration holds the configuration parameters for one pool.
78
+ # It also stores aws parameters and defaults, providing a single lookup mechanism
79
+ # for all.
80
+ # If lookup files, then it raise an exception.
81
+
82
+ class PoolConfiguration
83
+ # Create a new PoolConfiguration. The default parameters
84
+ # are used if the desired parameter is not given.
85
+ def initialize(aws_config, default, config)
86
+ # these parameters merge the defaults and the given parbameters
87
+ # merged parameters are also evaluated
88
+ @merge_params = [:user_data]
89
+ @aws_config = aws_config
90
+ @default = default
91
+ @config = config
92
+ end
93
+
94
+ # Get a parameter, either from aws_config, config or default.
95
+ # Don't raise an exception if there is no value.
96
+ def get(param)
97
+ @aws_config[param] || @config[param] || @default[param]
98
+ end
99
+
100
+ # Get a parameter, either from config or from default.
101
+ # Raise an exception if there is none.
102
+ def [](param)
103
+ if @default.nil?
104
+ raise "Missing defaults"
105
+ end
106
+ config_param = @aws_config[param] || @config[param]
107
+ if (res = config_param || @default[param]).nil?
108
+ raise "Missing config: #{param}"
109
+ end
110
+ begin
111
+ if @merge_params.include?(param)
112
+ # fix up default param if needed -- it must be a hash
113
+ @default[param] = {} if @default[param].nil?
114
+ @default[param] = eval(@default[param]) if @default[param].is_a?(String)
115
+ if config_param
116
+ @default[param].merge(eval(config_param))
117
+ else
118
+ @default[param]
119
+ end
120
+ else
121
+ res
122
+ end
123
+ rescue
124
+ raise "Config bad format: #{param} #{config_param} #{$!}"
125
+ end
126
+ end
127
+
128
+ # Store (create or replace) a parameter.
129
+ def []=(param, val)
130
+ @config[param] = val
131
+ end
132
+
133
+ def append_env(name)
134
+ aws_env = @aws_config[:aws_env]
135
+ aws_env.nil? || aws_env == '' ? name : "#{name}-#{aws_env}"
136
+ end
137
+
138
+ # Test to see that the derived parameters are valid.
139
+ def valid?
140
+ @config[:ami_id] &&
141
+ @config[:work_queue] && @config[:work_queue].valid? &&
142
+ @config[:status_queue] && @config[:status_queue].valid?
143
+ end
144
+
145
+ # Looks up a queue given its name.
146
+ # Stores the result in config under the given key (if given).
147
+ # Returns the queue.
148
+ # Raises an exception if none found.
149
+ def setup_queue(key, name)
150
+ return nil unless name
151
+ name = append_env(@config[name])
152
+ queue = NamedQueue.new(name)
153
+ raise "Bad configuration -- no queue #{name}" if !queue
154
+ @config[key] = queue if key
155
+ queue
156
+ end
157
+
158
+ # Looks up the image, given its name.
159
+ # Stores the result in config under the given key (if given).
160
+ # Returns the image.
161
+ # Raises an exception if none found.
162
+ def setup_image(key, name)
163
+ return nil unless name
164
+ name = append_env(@config[name])
165
+ image = EC2ImageEnumerator.new.find_image_id_by_name(name)
166
+ raise "Bad configuration -- no image #{name}" if !image
167
+ @config[key] = image if key
168
+ image
169
+ end
170
+
171
+ end
172
+ end
@@ -0,0 +1,239 @@
1
+ require 'periodic'
2
+ require 'pp'
3
+ require 'logger'
4
+ require 'reporter'
5
+ require 'instance_pool'
6
+ require 'aws_context'
7
+ require 'policy_factory'
8
+ require 'active_set_factory'
9
+ require 'status_parser_factory'
10
+ require 'logger_factory'
11
+ require 'policy'
12
+
13
+ module Cloudmaster
14
+
15
+ # PoolManager
16
+ #
17
+ # Manages one InstancePool, which is collections of EC2 instances
18
+ # running the same image.
19
+ # The InstancePoolMaanger is responsible for starting and terminating
20
+ # instances.
21
+ # It's policies are meant to balance acceptable performance while
22
+ # minimizing cost.
23
+ # To help achieve this goal, the PoolManager receives
24
+ # status reports from instances, through a status queue.
25
+ #
26
+ # Two classes of policies are defined: job and resource.
27
+ # These roughly correspond to stateless and stateful services.
28
+ #
29
+ # ==Job Policy==
30
+ # In the job policy, instances are assigned work through a work queue.
31
+ # * Each request is stateless, and can be serviceed by any instance.
32
+ # * Each instance processes one request at a time.
33
+ # * Each instance is either starting_up or active.
34
+ # * Once it is active, it is either busy (load 1.0) or idle (load 0.0).
35
+ # At startup, the instance reports when it is ready to begin processing, and
36
+ # enters the active state.
37
+ # Each instance reports the load through the status queue when it
38
+ # starts/stops processing a job.
39
+ #
40
+ # The job policy aims to keep the work queue to a reasonable size while not
41
+ # maintaining an excessive number of idle instances.
42
+ #
43
+ # ==Resource Policy==
44
+ # Instance managed by theresource policy have stateful associations with
45
+ # clients, and provide them services on demand.
46
+ # * Each instance processes requests made by clients as requested.
47
+ # * An external entity (the alllocator) assigns clients to instances
48
+ # based on an instance report, which lists the active instances
49
+ # and their associated load.
50
+ # * The instance report (called the active set) is stored in
51
+ # S3, at a configurable bucket and key.
52
+ # * The allocator assigns clients to instances, and also creates a
53
+ # work-queue entry each time it assigns a new client.
54
+ # * The allocator is expected to assign clients only to those instances
55
+ # listed in the active set.
56
+ # * The work queue is emptied by cloudmaster.
57
+ # * Each instance may be starting_up, active, or shutting_down.
58
+ # * At startup, the instance reports when it is ready to begin processing,
59
+ # and enters the active state.
60
+ # * The policy decides when to shut down an instance.
61
+ # It puts it in the shut_down state, but does not stop
62
+ # it immediately (to avoid disturbing existing clients).
63
+ # Instances in shutting_down state with zero load, or who have
64
+ # remained in this state for an excessive time are stopped.
65
+ # * Active instances are available to accept new clients;
66
+ # shutting_down instances are not.
67
+ # During any given time period, each instance can be partially busy (load
68
+ # between 0.0 and 1.0)
69
+ # Each instance periodically reports is load estimate for that period through
70
+ # the status queue.
71
+ # The resource policy seeks to maintain a load between an
72
+ # upper threshold and a lower threshold.
73
+ # It starts instances or stops them to achieve this.
74
+
75
+ class PoolManager
76
+ attr_reader :instances, :logger # for testing only
77
+
78
+ # Set up PoolManager.
79
+ # Creates objects used to access SQS and EC2.
80
+ # Creates instance pool, policy classes, repoter, and queues.
81
+ # Actual processing does not start until "run" is called.
82
+ def initialize(config)
83
+ # set up AWS access objects
84
+ keys = [ config[:aws_access_key], config[:aws_secret_key]]
85
+ aws = AwsContext.instance
86
+ @ec2 = aws.ec2(*keys)
87
+ @sqs = aws.sqs(*keys)
88
+ @s3 = aws.s3(*keys)
89
+ @config = config
90
+
91
+ # set up reporter
92
+ @logger = LoggerFactory.create(@config[:logger], @config[:logfile])
93
+ @reporter = Reporter.setup(@config[:name], @logger)
94
+
95
+ # Create instance pool.
96
+ # Used to keep track of instances in the pool.
97
+ @instances = InstancePool.new(@reporter, @config)
98
+
99
+ # Create a policy class
100
+ @policy = PolicyFactory.create(@config[:policy], @reporter, @config, @instances)
101
+
102
+ # Create ActiveSet
103
+ @active_set = ActiveSetFactory.create(@config[:active_set_type], @config)
104
+
105
+ # Create StatusParser
106
+ @status_parser = StatusParserFactory.create(@config[:status_parser])
107
+
108
+ unless @config[:instance_log].empty?
109
+ @reporter.log_instances(@config[:instance_log])
110
+ end
111
+
112
+ # Look up the work queues and the image from their names.
113
+ # Have policy do most of the work.
114
+ @work_queue = @config.setup_queue(:work_queue, :work_queue_name)
115
+ @status_queue = @config.setup_queue(:status_queue, :status_queue_name)
116
+ @ami_id = @config.setup_image(:ami_id, :ami_name)
117
+
118
+ @keep_running = true
119
+ end
120
+
121
+ # Main loop of cloudmaster
122
+ #
123
+ # * Reads and processes status messages.
124
+ # * Starts and stops instances according to policies
125
+ # * Detects hung instances, and stops them.
126
+ # * Displays periodic reports.
127
+ def run(end_time = nil)
128
+ summary_period = Periodic.new(@config[:summary_interval].to_i)
129
+ instance_report_period = Periodic.new(@config[:instance_report_interval].to_i)
130
+ policy_period = Periodic.new(@config[:policy_interval].to_i)
131
+ active_set_period = Periodic.new(@config[:active_set_interval].to_i * 60)
132
+ audit_instances_period = Periodic.new(@config[:audit_instance_interval].to_i * 60)
133
+
134
+ # loop reading messages from the status queue
135
+ while keep_running(end_time) do
136
+ # upate instance list and get queue depth
137
+ audit_instances_period.check do
138
+ @instances.audit_existing_instances
139
+ end
140
+
141
+ @work_queue.read_queue_depth
142
+ break unless @keep_running
143
+
144
+ # start first instance, if necessary, and ensure the
145
+ # number of running instances stays between maximum and minimum
146
+ @policy.ensure_limits
147
+ break unless @keep_running
148
+
149
+ # handle status and log messages
150
+ process_messages(@config[:receive_count].to_i)
151
+
152
+ # update public dns (for new instances) and show summary reports
153
+ @instances.update_public_dns_all
154
+ summary_period.check do
155
+ @reporter.info("Instances: #{@instances.size} Queue Depth: #{@work_queue.queue_depth}")
156
+ end
157
+ instance_report_period.check do
158
+ @reporter.info("---Instance Summary---")
159
+ @instances.each do |instance|
160
+ @reporter.info(" #{instance.id} #{instance.report}\n")
161
+ end
162
+ @reporter.info("----------------------")
163
+ end
164
+ break unless @keep_running
165
+
166
+ # Based on queue depth and load_estimate, make a decision on
167
+ # whether to start or stop servers.
168
+ policy_period.check { @policy.apply }
169
+
170
+ active_set_period.check { update_active_set }
171
+
172
+ # Stop instances that have not given recent status.
173
+ @policy.stop_hung_instances
174
+ break unless @keep_running
175
+
176
+ Clock.sleep @config[:poll_interval].to_i
177
+ end
178
+ end
179
+
180
+ # Shut down the manager.
181
+ # This may take a little time.
182
+ def shutdown
183
+ @keep_running = false
184
+ end
185
+
186
+ private
187
+
188
+ # Process a batch of status and log messaage.
189
+ # Status messages update the instance usage information, and
190
+ # log messages are just logged.
191
+ # Observed behavior is that only one message is returned per call
192
+ # to SQS, no matter how many are requested.
193
+ def process_message_batch(count)
194
+ # read some messages
195
+ messages = @status_queue.read_messages(count)
196
+ messages.each do |message|
197
+ # parse message
198
+ msg = @status_parser.parse_message(message[:body])
199
+ case msg[:type]
200
+ when "status"
201
+ # save the status and load_estimate
202
+ @instances.update_status(msg)
203
+ when "log"
204
+ # just log the message
205
+ @reporter.info(msg[:message], msg[:instance_id])
206
+ end
207
+ # delete the message once it has been processed
208
+ @status_queue.delete_message(message[:receipt_handle])
209
+ end
210
+ messages.size
211
+ end
212
+
213
+ # Process messages (up to count)
214
+ # Continue until there are no messages remaining.
215
+ def process_messages(count)
216
+ n_remaining = count
217
+ while n_remaining > 0
218
+ n = process_message_batch(n_remaining)
219
+ break if n == 0
220
+ n_remaining -= n
221
+ end
222
+ end
223
+
224
+ # Write active set if it has changed since the last write.
225
+ def update_active_set
226
+ @active_set.update(@instances.active_set)
227
+ end
228
+
229
+ # Returns true if the manager should keep running.
230
+ def keep_running(end_time)
231
+ if end_time && Clock.now > end_time
232
+ false
233
+ else
234
+ @keep_running
235
+ end
236
+ end
237
+
238
+ end
239
+ end
@@ -0,0 +1,54 @@
1
+ require 'pool_configuration'
2
+ require 'pool_manager'
3
+
4
+ module Cloudmaster
5
+
6
+ # oolRunner
7
+ #
8
+ # Manages separate PoolManagers, each in a separate thread.
9
+ #
10
+ # Knows how to start (run) and stop (shutdown) the pools.
11
+ #
12
+ # Creates a thread for each pool in config, and runs a PoolManager in it.
13
+ # This needs to be passed a configuration, normally a InifileConfig object
14
+ # The configuration object contains all the information needed to control
15
+ # the pools, including the number of pools and each one's characteristics.
16
+ class PoolRunner
17
+ attr_reader :pool_managers # for testing only
18
+ # Create empty runner. Until the run method is called, the
19
+ # individual pool managers are not created.
20
+ def initialize(config)
21
+ @config = config
22
+ @pool_managers = []
23
+ Signal.trap("INT") do
24
+ self.shutdown
25
+ end
26
+ end
27
+
28
+ # Create each of the pool managers described in the configuration.
29
+ # We can limit the amount of time it runs, for testing purposes only
30
+ # In testing we can call run again after it returns, so we make sure
31
+ # that we only create pool managers the first time through.
32
+ def run(limit = nil)
33
+ if @pool_managers == []
34
+ @config.pools.each do |pool_config|
35
+ # Wrap pool config parameters up with defaults.
36
+ config = PoolConfiguration.new(@config.aws, @config.default, pool_config)
37
+ @pool_managers << PoolManager.new(config)
38
+ end
39
+ end
40
+ threads = []
41
+ @pool_managers.each do |pool_manager|
42
+ threads << Thread.new(pool_manager) do |mgr|
43
+ mgr.run(limit)
44
+ end
45
+ end
46
+ threads.each { |thread| thread.join }
47
+ end
48
+
49
+ # Shut down each of the pool managers.
50
+ def shutdown
51
+ @pool_managers.each { |mgr| mgr.shutdown }
52
+ end
53
+ end
54
+ end
data/app/reporter.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'instance_logger'
2
+
3
+ module Cloudmaster
4
+
5
+ # Creates and outputs log messages
6
+ # These are formatted with a timestamp and an instance name.
7
+ # This remembers the log device, which is anything with puts.
8
+ # This is treated as a global. It is initialized by calling "Reporter.setup"
9
+ # and then anyone can get a copy by calling "Reporter.instance".
10
+ class Reporter
11
+ attr_accessor :level
12
+
13
+ NONE = 0
14
+ ERROR = 1
15
+ WARNING = 2
16
+ INFO = 3
17
+ TRACE = 4
18
+ DEBUG = 5
19
+ ALL = 10
20
+
21
+ # Reporter displays the given name on every line.
22
+ # reports go to the given log (an IO).
23
+ def initialize(name, log)
24
+ @level = ALL
25
+ @name = name
26
+ @log = log || STDOUT
27
+ @instance_logger = nil
28
+ end
29
+
30
+ def Reporter.setup(name, log)
31
+ new(name, log)
32
+ end
33
+
34
+ def log_instances(dir)
35
+ @instance_logger = InstanceLogger.new(dir)
36
+ end
37
+
38
+ # Log a message
39
+ def log(message, *opts)
40
+ send_to_log("INFO:", message, *opts)
41
+ end
42
+
43
+ def err(msg, *opts)
44
+ send_to_log("ERROR:", msg, *opts) if @level >= ERROR
45
+ end
46
+ alias error err
47
+
48
+ def warning(msg, *opts)
49
+ send_to_log("WARNING:", msg, *opts) if @level >= WARNING
50
+ end
51
+
52
+ def info(msg, *opts)
53
+ send_to_log("INFO:", msg, *opts) if @level >= INFO
54
+ end
55
+
56
+ def trace(msg, *opts)
57
+ send_to_log("TRACE:", msg, *opts) if @level >= TRACE
58
+ end
59
+
60
+ def debug(msg, *opts)
61
+ send_to_log("DEBUG:", msg, *opts) if @level >= DEBUG
62
+ end
63
+
64
+ private
65
+
66
+ def send_to_log(type, message, instance_id = nil)
67
+ msg = [type, format_timestamp(Clock.now), @name]
68
+ msg << instance_id if instance_id
69
+ msg << message
70
+ message = msg.join(' ')
71
+ @log.puts(message)
72
+ if instance_id && @instance_logger
73
+ @instance_logger.puts(instance_id, message)
74
+ end
75
+ end
76
+
77
+ def format_timestamp(ts)
78
+ "#{Clock.now.strftime("%m-%d-%y %H:%M:%S")}"
79
+ end
80
+ end
81
+ end