monkeyshines 0.0.2

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.
Files changed (85) hide show
  1. data/.document +4 -0
  2. data/.gitignore +43 -0
  3. data/LICENSE +20 -0
  4. data/LICENSE.textile +20 -0
  5. data/README.textile +125 -0
  6. data/Rakefile +105 -0
  7. data/VERSION +1 -0
  8. data/examples/.gitignore +4 -0
  9. data/examples/bulk_urls/scrape_bulk_urls.rb +64 -0
  10. data/examples/rename_tree/rename_hdp_tree.rb +151 -0
  11. data/examples/rename_tree/rename_ripd_tree.rb +82 -0
  12. data/examples/rss_feeds/scrape_rss_feeds.rb +52 -0
  13. data/examples/shorturls/README.textile +111 -0
  14. data/examples/shorturls/bulkdump_shorturls.rb +46 -0
  15. data/examples/shorturls/bulkload_shorturls.rb +45 -0
  16. data/examples/shorturls/extract_urls.rb +12 -0
  17. data/examples/shorturls/multiplex_shorturl_cache.rb +32 -0
  18. data/examples/shorturls/old/multidump_and_fix_shorturls.rb +66 -0
  19. data/examples/shorturls/old/shorturl_stats.rb +81 -0
  20. data/examples/shorturls/scrape_shorturls.rb +112 -0
  21. data/examples/shorturls/shorturl_request.rb +29 -0
  22. data/examples/shorturls/shorturl_sequence.rb +121 -0
  23. data/examples/shorturls/shorturl_start_tyrant.sh +16 -0
  24. data/examples/shorturls/start_shorturl_cache.sh +2 -0
  25. data/lib/monkeyshines.rb +31 -0
  26. data/lib/monkeyshines/extensions.rb +16 -0
  27. data/lib/monkeyshines/fetcher.rb +10 -0
  28. data/lib/monkeyshines/fetcher/authed_http_fetcher.rb +35 -0
  29. data/lib/monkeyshines/fetcher/base.rb +44 -0
  30. data/lib/monkeyshines/fetcher/fake_fetcher.rb +19 -0
  31. data/lib/monkeyshines/fetcher/http_fetcher.rb +127 -0
  32. data/lib/monkeyshines/fetcher/http_head_fetcher.rb +23 -0
  33. data/lib/monkeyshines/monitor.rb +7 -0
  34. data/lib/monkeyshines/monitor/chunked_store.rb +23 -0
  35. data/lib/monkeyshines/monitor/periodic_logger.rb +33 -0
  36. data/lib/monkeyshines/monitor/periodic_monitor.rb +65 -0
  37. data/lib/monkeyshines/options.rb +59 -0
  38. data/lib/monkeyshines/recursive_runner.rb +26 -0
  39. data/lib/monkeyshines/repository/base.rb +57 -0
  40. data/lib/monkeyshines/repository/s3.rb +169 -0
  41. data/lib/monkeyshines/request_stream.rb +11 -0
  42. data/lib/monkeyshines/request_stream/base.rb +32 -0
  43. data/lib/monkeyshines/request_stream/edamame_queue.rb +54 -0
  44. data/lib/monkeyshines/request_stream/klass_request_stream.rb +39 -0
  45. data/lib/monkeyshines/request_stream/simple_request_stream.rb +22 -0
  46. data/lib/monkeyshines/runner.rb +161 -0
  47. data/lib/monkeyshines/runner_core/options.rb +5 -0
  48. data/lib/monkeyshines/runner_core/parsing_runner.rb +29 -0
  49. data/lib/monkeyshines/scrape_job/old_paginated.rb +343 -0
  50. data/lib/monkeyshines/scrape_job/recursive.rb +9 -0
  51. data/lib/monkeyshines/scrape_request.rb +136 -0
  52. data/lib/monkeyshines/scrape_request/paginated.rb +290 -0
  53. data/lib/monkeyshines/scrape_request/raw_json_contents.rb +16 -0
  54. data/lib/monkeyshines/scrape_request/signed_url.rb +86 -0
  55. data/lib/monkeyshines/store.rb +14 -0
  56. data/lib/monkeyshines/store/base.rb +29 -0
  57. data/lib/monkeyshines/store/chunked_flat_file_store.rb +37 -0
  58. data/lib/monkeyshines/store/conditional_store.rb +57 -0
  59. data/lib/monkeyshines/store/factory.rb +8 -0
  60. data/lib/monkeyshines/store/flat_file_store.rb +84 -0
  61. data/lib/monkeyshines/store/key_store.rb +51 -0
  62. data/lib/monkeyshines/store/null_store.rb +15 -0
  63. data/lib/monkeyshines/store/read_thru_store.rb +22 -0
  64. data/lib/monkeyshines/store/tokyo_tdb_key_store.rb +33 -0
  65. data/lib/monkeyshines/store/tyrant_rdb_key_store.rb +56 -0
  66. data/lib/monkeyshines/store/tyrant_tdb_key_store.rb +20 -0
  67. data/lib/monkeyshines/utils/factory_module.rb +106 -0
  68. data/lib/monkeyshines/utils/filename_pattern.rb +134 -0
  69. data/lib/monkeyshines/utils/logger.rb +15 -0
  70. data/lib/monkeyshines/utils/trollop-1.14/FAQ.txt +84 -0
  71. data/lib/monkeyshines/utils/trollop-1.14/History.txt +101 -0
  72. data/lib/monkeyshines/utils/trollop-1.14/Manifest.txt +7 -0
  73. data/lib/monkeyshines/utils/trollop-1.14/README.txt +40 -0
  74. data/lib/monkeyshines/utils/trollop-1.14/Rakefile +36 -0
  75. data/lib/monkeyshines/utils/trollop-1.14/lib/trollop.rb +744 -0
  76. data/lib/monkeyshines/utils/trollop-1.14/test/test_trollop.rb +1048 -0
  77. data/lib/monkeyshines/utils/trollop.rb +744 -0
  78. data/lib/monkeyshines/utils/union_interval.rb +52 -0
  79. data/lib/monkeyshines/utils/uri.rb +70 -0
  80. data/lib/monkeyshines/utils/uuid.rb +32 -0
  81. data/monkeyshines.gemspec +147 -0
  82. data/scrape_from_file.rb +44 -0
  83. data/spec/monkeyshines_spec.rb +7 -0
  84. data/spec/spec_helper.rb +9 -0
  85. metadata +183 -0
@@ -0,0 +1,169 @@
1
+ require 'right_aws'
2
+ module Monkeyshines
3
+ module Store
4
+
5
+ #
6
+ # Large portions lifted from Thoughtbot's Paperclip gem.
7
+ #
8
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
9
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
10
+ # There are a few S3-specific options for has_attached_file:
11
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
12
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
13
+ # gives you. You can 'environment-space' this just like you do to your
14
+ # database.yml file, so different environments can use different accounts:
15
+ # development:
16
+ # access_key_id: 123...
17
+ # secret_access_key: 123...
18
+ # test:
19
+ # access_key_id: abc...
20
+ # secret_access_key: abc...
21
+ # production:
22
+ # access_key_id: 456...
23
+ # secret_access_key: 456...
24
+ # This is not required, however, and the file may simply look like this:
25
+ # access_key_id: 456...
26
+ # secret_access_key: 456...
27
+ # In which case, those access keys will be used in all environments. You can also
28
+ # put your bucket name in this file, instead of adding it to the code directly.
29
+ # This is useful when you want the same account but a different bucket for
30
+ # development versus production.
31
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
32
+ # policies that S3 provides (more information can be found here:
33
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
34
+ # The default for Paperclip is "public-read".
35
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
36
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
37
+ # default), and 'https' when your :s3_permissions are anything else.
38
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
39
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
40
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
41
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
42
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
43
+ # Paperclip will call that Proc with attachment as the only argument.
44
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
45
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
46
+ # link in the +url+ entry for more information about S3 domains and buckets.
47
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
48
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
49
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
50
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
51
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
52
+ # Normally, this won't matter in the slightest and you can leave the default (which is
53
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
54
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
55
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
56
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
57
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
58
+ # by S3.
59
+ # * +path+: This is the key under the bucket in which the file will be stored. The
60
+ # URL will be constructed from the bucket and the path. This is what you will want
61
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
62
+ # S3 (strictly speaking) does not support directories, you can still use a / to
63
+ # separate parts of your file name.
64
+ class S3Repository < Monkeyshines::Repository::Base
65
+ attr_reader :bucket_name, :s3_host_alias, :s3_protocol
66
+
67
+ def initialize options={}
68
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
69
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
70
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
71
+ @s3_options = @options[:s3_options] || {}
72
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
73
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
74
+ @s3_headers = @options[:s3_headers] || {}
75
+ @s3_host_alias = @options[:s3_host_alias]
76
+ end
77
+
78
+ #
79
+ # Implementation of Monkeyshines::Repository
80
+ #
81
+
82
+ def exists?(filename)
83
+ s3_bucket.key(filename) ? true : false
84
+ end
85
+
86
+ def md5(filename)
87
+ end
88
+
89
+ #
90
+ # s3 interface
91
+ #
92
+
93
+ # Use with Monkeyshines::Utils::FilenamePattern to generate urls to s3 files.
94
+ # Ex:
95
+ # s3_url = FilenamePattern.new(":s3_path_url_base/path/to/file.ext", s3_repo.filename_pattern_tokens)
96
+ #
97
+ def filename_pattern_tokens
98
+ { :s3_domain_url_base => "#{s3_protocol}://#{bucket_name}.s3.amazonaws.com",
99
+ :s3_alias_url_base => "#{s3_protocol}://#{s3_host_alias}",
100
+ :s3_path_url_base => "#{s3_protocol}://s3.amazonaws.com/#{bucket_name}", }
101
+ end
102
+
103
+ def s3
104
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
105
+ @s3_credentials[:secret_access_key],
106
+ @s3_options)
107
+ end
108
+
109
+ def s3_bucket
110
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
111
+ end
112
+
113
+ def parse_credentials creds
114
+ find_credentials(creds)
115
+ end
116
+
117
+ def find_credentials creds
118
+ case creds
119
+ when File
120
+ YAML.load_file(creds.path)
121
+ when String
122
+ YAML.load_file(creds)
123
+ when Hash
124
+ creds
125
+ else
126
+ raise ArgumentError, "Credentials are not a path, file, or hash."
127
+ end
128
+ end
129
+ private :find_credentials
130
+
131
+ # # Returns representation of the data of the file assigned to the given
132
+ # # style, in the format most representative of the current storage.
133
+ # def to_file path
134
+ # @queued_for_write[path] || s3_bucket.key(path(style))
135
+ # end
136
+ # alias_method :to_io, :to_file
137
+ #
138
+ # def flush_writes #:nodoc:
139
+ # @queued_for_write.each do |style, file|
140
+ # begin
141
+ # log("saving #{path(style)}")
142
+ # key = s3_bucket.key(path(style))
143
+ # key.data = file
144
+ # key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
145
+ # rescue RightAws::AwsError => e
146
+ # raise
147
+ # end
148
+ # end
149
+ # @queued_for_write = {}
150
+ # end
151
+ #
152
+ # def flush_deletes #:nodoc:
153
+ # @queued_for_delete.each do |path|
154
+ # begin
155
+ # log("deleting #{path}")
156
+ # if file = s3_bucket.key(path)
157
+ # file.delete
158
+ # end
159
+ # rescue RightAws::AwsError
160
+ # # Ignore this.
161
+ # end
162
+ # end
163
+ # @queued_for_delete = []
164
+ # end
165
+
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,11 @@
1
+ module Monkeyshines
2
+ module RequestStream
3
+ extend FactoryModule
4
+ autoload :Base, 'monkeyshines/request_stream/base'
5
+ autoload :KlassRequestStream, 'monkeyshines/request_stream/klass_request_stream'
6
+ autoload :KlassHashRequestStream, 'monkeyshines/request_stream/klass_request_stream'
7
+ autoload :SimpleRequestStream, 'monkeyshines/request_stream/simple_request_stream'
8
+ autoload :BeanstalkQueue, 'monkeyshines/request_stream/beanstalk_queue'
9
+ autoload :EdamameQueue, 'monkeyshines/request_stream/edamame_queue'
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Monkeyshines
2
+ module RequestStream
3
+
4
+ #
5
+ # RequestStream::Base
6
+ #
7
+ #
8
+ class Base
9
+ attr_accessor :options
10
+ Base::DEFAULT_OPTIONS = {}
11
+ def initialize _options={}
12
+ self.options = Base::DEFAULT_OPTIONS.deep_merge(_options)
13
+ Log.debug "New #{self.class} as #{options.inspect}"
14
+ end
15
+
16
+ def each *args, &block
17
+ self.request_store.each(*args) do |*raw_req_args|
18
+ req = request_from_raw(*raw_req_args)
19
+ yield req
20
+ end
21
+ end
22
+
23
+ def put *args
24
+ request_store.put *args
25
+ end
26
+
27
+ def skip! *args
28
+ request_store.skip! *args
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ module Monkeyshines
2
+ module RequestStream
3
+ #
4
+ # Watch for jobs in an Edamame priority queue
5
+ # (http://mrflip.github.com/edamame)
6
+ #
7
+ class EdamameQueue < Edamame::Broker
8
+ # How long to wait for tasks
9
+ cattr_accessor :queue_request_timeout
10
+ self.queue_request_timeout = 5 * 60 # seconds
11
+ # priority for search jobs if not otherwise given
12
+ QUEUE_PRIORITY = 65536
13
+
14
+ def initialize _options
15
+ tube = Monkeyshines::CONFIG[:handle].to_s.gsub(/_/, '-')
16
+ super _options.deep_merge( :tube => tube )
17
+ if _options[:queue_request_timeout]
18
+ Log.info "Setting timeout to #{_options[:queue_request_timeout]}"
19
+ self.queue_request_timeout = _options[:queue_request_timeout]
20
+ end
21
+ end
22
+
23
+ # def each klass, &block
24
+ # work(queue_request_timeout, klass) do |job|
25
+ # job.each_request(&block)
26
+ # end
27
+ # Log.info [queue, queue.beanstalk_stats]
28
+ # end
29
+
30
+ def each &block
31
+ work(queue_request_timeout) do |job|
32
+ yield job.obj['type'], job.obj
33
+ end
34
+ Log.info [queue, queue.beanstalk_stats]
35
+ end
36
+
37
+ def req_to_job req, job_options={}
38
+ obj_hash = req.to_hash.merge(
39
+ 'type' => req.class.to_s,
40
+ 'key' => [req.class.to_s, req.key].join('-') )
41
+ Edamame::Job.from_hash(job_options.merge("obj" => obj_hash,
42
+ 'priority' => (66000 + 1000*req.req_generation),
43
+ 'tube' => tube ))
44
+ end
45
+
46
+ def put job, *args
47
+ job_options = args.extract_options!
48
+ job = req_to_job(job, job_options) unless job.is_a?(Beanstalk::Job) || job.is_a?(Edamame::Job)
49
+ # p [self.class, job.key, job.obj,job.scheduling, job_options, args]
50
+ super job, *args
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ module Monkeyshines
2
+ module RequestStream
3
+
4
+ #
5
+ # KlassRequestStream is an abstract factory for requests -- the first arg
6
+ # gives the request type
7
+ #
8
+ class KlassRequestStream < Base
9
+ attr_accessor :request_store
10
+ attr_accessor :klass_scope
11
+ KlassRequestStream::DEFAULT_OPTIONS = {
12
+ :store => { :type => :flat_file_store },
13
+ :klass_scope => Kernel,
14
+ }
15
+ def initialize _options={}
16
+ super KlassRequestStream::DEFAULT_OPTIONS.deep_merge(_options)
17
+ self.request_store = Monkeyshines::Store.create(options.merge(options[:store]))
18
+ self.klass_scope = options[:klass_scope]
19
+ end
20
+ #
21
+ # use the first arg as a klass name
22
+ # to create a scrape request using rest of args
23
+ #
24
+ def request_from_raw klass_name, *raw_req_args
25
+ klass = FactoryModule.get_class(klass_scope, klass_name)
26
+ klass.new(*raw_req_args)
27
+ end
28
+ end
29
+
30
+ class KlassHashRequestStream < KlassRequestStream
31
+ def request_from_raw klass_name, hsh
32
+ klass = FactoryModule.get_class(klass_scope, klass_name)
33
+ klass.from_hash(hsh)
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+
@@ -0,0 +1,22 @@
1
+ module Monkeyshines
2
+ module RequestStream
3
+
4
+ #
5
+ # SimpleRequestStream generates an instance of options[:klass] from each element of its store
6
+ #
7
+ class SimpleRequestStream < KlassRequestStream
8
+ attr_accessor :klass
9
+ SimpleRequestStream::DEFAULT_OPTIONS = {
10
+ :klass => Monkeyshines::ScrapeRequest,
11
+ }
12
+ def initialize _options={}
13
+ super SimpleRequestStream::DEFAULT_OPTIONS.merge(_options)
14
+ self.klass = options[:klass]
15
+ end
16
+ def request_from_raw *raw_req_args
17
+ klass.new(*raw_req_args)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,161 @@
1
+ require 'yaml'
2
+ require 'monkeyshines/runner_core/options'
3
+
4
+ module Monkeyshines
5
+ class Runner
6
+ attr_accessor :options
7
+ attr_accessor :fetcher
8
+ attr_accessor :source
9
+ attr_accessor :dest
10
+ attr_accessor :periodic_log
11
+ attr_accessor :sleep_time, :force_fetch
12
+
13
+ DEFAULT_OPTIONS = {
14
+ :source => { :type => :simple_request_stream, },
15
+ :dest => { :type => :flat_file_store, :filemode => 'w'},
16
+ :fetcher => { :type => :http_fetcher, },
17
+ :log => { :dest => nil, :iters => 100, :time => 30 },
18
+ :skip => nil,
19
+ :sleep_time => 0.5,
20
+ :force_fetch => false,
21
+ }
22
+
23
+ #
24
+ # Assembles a MonkeyshinesRunner from the given plan.
25
+ #
26
+ # options_hashes is a hash tree of options to build each particular
27
+ # component. The options are deep merged with the class and global defaults.
28
+ #
29
+ # The options for each of :fetcher, :request_stream and :dest are passed to
30
+ # the Fetcher, RequestStream and Store factories respectively
31
+ #
32
+ def initialize *options_hashes
33
+ prepare_options(*options_hashes)
34
+ setup_main_log
35
+ self.source = create_source
36
+ self.fetcher = create_fetcher
37
+ self.dest = create_dest
38
+ self.sleep_time = options[:sleep_time]
39
+ self.force_fetch = options[:force_fetch]
40
+ end
41
+
42
+ def create_source
43
+ Monkeyshines::RequestStream.create(options[:source])
44
+ end
45
+
46
+ def create_dest
47
+ Monkeyshines::Store.create(options[:dest])
48
+ end
49
+
50
+ def create_fetcher
51
+ Monkeyshines::Fetcher.create(options[:fetcher])
52
+ end
53
+
54
+ #
55
+ # Deep merges:
56
+ # * the DEFAULT_OPTIONS in runner.rb,
57
+ # * the global Monkeyshines::CONFIG loaded from disk
58
+ # * the options passed in as arguments
59
+ #
60
+ # Options appearing later win out.
61
+ #
62
+ def prepare_options *options_hashes
63
+ self.options = Hash.deep_sum(
64
+ Monkeyshines::Runner::DEFAULT_OPTIONS,
65
+ Monkeyshines::CONFIG,
66
+ *options_hashes
67
+ )
68
+ end
69
+
70
+ #
71
+ # * For each entry in #source,
72
+ # ** create scrape_request(s)
73
+ # ** fetch request (...if appropriate)
74
+ # ** store result (...if fetch was successful)
75
+ # ** do logging
76
+ #
77
+ def run
78
+ Log.info "Beginning scrape itself"
79
+ before_scrape()
80
+ each_request do |req|
81
+ next unless req
82
+ before_fetch(req)
83
+ fetch_and_store(req)
84
+ after_fetch(req)
85
+ sleep sleep_time
86
+ req
87
+ end
88
+ after_scrape()
89
+ end
90
+
91
+ #
92
+ # before_scrape filter chain.
93
+ #
94
+ def before_scrape
95
+ source.skip!(options[:skip].to_i) if options[:skip]
96
+ end
97
+
98
+ #
99
+ # enumerates requests
100
+ #
101
+ def each_request &block
102
+ source.each(&block)
103
+ end
104
+
105
+ #
106
+ # before_scrape filter chain.
107
+ #
108
+ def before_fetch req
109
+ end
110
+
111
+ #
112
+ # Fetch and store result
113
+ #
114
+ #
115
+ def fetch_and_store req
116
+ # some stores (eg.conditional) only call fetcher if url key is missing.
117
+ dest.set(req.url, force_fetch) do
118
+ response = fetcher.get(req) # do the url fetch
119
+ return unless response.healthy? # don't store bad fetches
120
+ [response.scraped_at, response] # timestamp for bookkeeper, result for dest
121
+ end
122
+ end
123
+
124
+ #
125
+ # after_fetch
126
+ #
127
+ def after_fetch req
128
+ periodic_log.periodically{ self.log_line(req) }
129
+ end
130
+
131
+ #
132
+ # after_scrape
133
+ #
134
+ def after_scrape
135
+ dest.close
136
+ fetcher.close
137
+ end
138
+
139
+ #
140
+ # Logging
141
+ #
142
+ def setup_main_log
143
+ unless options[:log][:dest].blank?
144
+ log_file = "%s/log/%s" % [WORK_DIR, options[:log][:dest]]
145
+ FileUtils.mkdir_p(File.dirname(log_file))
146
+ $stdout = $stderr = File.open( log_file+"-console.log", "a" )
147
+ end
148
+ end
149
+
150
+ def periodic_log
151
+ @periodic_log ||= Monkeyshines::Monitor::PeriodicLogger.new(options[:log])
152
+ end
153
+
154
+ def log_line result
155
+ result_log_line = result.blank? ? ['-','-','-'] : [result.response_code, result.url, result.contents.to_s[0..80]]
156
+ [ dest.log_line, result_log_line ].flatten
157
+ end
158
+
159
+
160
+ end
161
+ end