etna 0.1.46 → 0.1.48

Sign up to get free protection for your applications and to get access to all the features.
data/lib/commands.rb CHANGED
@@ -206,7 +206,7 @@ class EtnaApp
206
206
  end
207
207
  end
208
208
 
209
- class Administrate
209
+ class Janus
210
210
  include Etna::CommandExecutor
211
211
 
212
212
  class Token
@@ -238,6 +238,59 @@ class EtnaApp
238
238
  end
239
239
  end
240
240
  end
241
+ end
242
+
243
+ class Magma
244
+ include Etna::CommandExecutor
245
+
246
+ class Materialize < Etna::Command
247
+ include WithEtnaClients
248
+
249
+ string_flags << "--project-name"
250
+ string_flags << "--log-file"
251
+ string_flags << "--log-level"
252
+ string_flags << "--concurrency"
253
+
254
+
255
+ def execute(project_name:, log_file:'/dev/stdout', log_level: ::Logger::INFO, concurrency: 1)
256
+ logger = Etna::Logger.new(log_file, 0, 1048576)
257
+
258
+ logger.level = log_level
259
+
260
+ workflow = Etna::Clients::Magma::MaterializeDataWorkflow.new(
261
+ model_attributes_mask: model_attribute_pairs(project_name),
262
+ record_names: 'all',
263
+ model_filters: {},
264
+ metis_client: metis_client(logger: logger),
265
+ magma_client: magma_client(logger: logger),
266
+ logger: logger,
267
+ project_name: project_name,
268
+ model_name: 'project', filesystem: filesystem,
269
+ concurrency: concurrency.to_i)
270
+
271
+ workflow.materialize_all(project_name)
272
+ logger.info("Done")
273
+ end
274
+
275
+ def model_attribute_pairs(project_name)
276
+ models = magma_client.retrieve(Etna::Clients::Magma::RetrievalRequest.new(
277
+ project_name: project_name,
278
+ model_name: 'all',
279
+ attribute_names: 'all',
280
+ record_names: []
281
+ )).models
282
+
283
+ result = models.model_keys.map do |model_name|
284
+ [ model_name, models.model(model_name).template.attributes.attribute_keys ]
285
+ end.to_h
286
+
287
+ result
288
+ end
289
+
290
+ def filesystem
291
+ @filesystem ||= Etna::Filesystem.new
292
+ end
293
+ end
241
294
 
242
295
  class Project
243
296
  include Etna::CommandExecutor
@@ -68,38 +68,43 @@ module Etna::Application
68
68
  [path.map(&:to_sym), YAML.load(File.read(value))]
69
69
  end
70
70
 
71
+ def controller_tags
72
+ [:controller, :action, :project_name, :user_hash]
73
+ end
74
+
71
75
  def setup_yabeda
72
76
  application = self.id
77
+ ctags = self.controller_tags
73
78
  Yabeda.configure do
74
79
  default_tag :application, application
80
+ default_tag :project_name, 'unknown'
81
+ default_tag :controller, 'none'
82
+ default_tag :action, 'none'
83
+ default_tag :user_hash, 'unknown'
75
84
 
76
85
  group :etna do
77
86
  histogram :response_time do
78
87
  comment "Time spent by a controller returning a response"
79
88
  unit :seconds
80
- tags [:controller, :action, :user_hash, :project_name]
89
+ tags ctags
90
+ buckets [0.01, 0.1, 0.3, 0.5, 1, 5]
91
+ end
92
+
93
+ histogram :perf do
94
+ comment "Time spent inside code path"
95
+ unit :seconds
96
+ tags ctags + [:class_name, :method_name]
81
97
  buckets [0.01, 0.1, 0.3, 0.5, 1, 5]
82
98
  end
83
99
 
84
100
  counter :visits do
85
101
  comment "Counts visits to the controller"
86
- tags [:controller, :action, :user_hash, :project_name]
102
+ tags ctags
87
103
  end
88
104
 
89
105
  counter :rollbar_errors do
90
106
  comment "Counts errors detected by and sent to rollbar"
91
- end
92
-
93
- gauge :last_command_completion do
94
- comment "Unix time of last time command was completed"
95
- tags [:command, :status, :application]
96
- end
97
-
98
- histogram :command_runtime do
99
- comment "Time spent processing a given command"
100
- tags [:command, :status, :application]
101
- unit :seconds
102
- buckets [0.1, 1, 5, 60, 300, 1500]
107
+ tags ctags
103
108
  end
104
109
  end
105
110
  end
@@ -176,9 +181,6 @@ module Etna::Application
176
181
  end
177
182
 
178
183
  def run_command(config, *args, &block)
179
- application = self.id
180
- status = 'success'
181
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
182
184
  cmd, cmd_args, cmd_kwds = find_command(*args)
183
185
 
184
186
  begin
@@ -190,18 +192,7 @@ module Etna::Application
190
192
  cmd.execute(*cmd.fill_in_missing_params(cmd_args), **cmd_kwds)
191
193
  rescue => e
192
194
  Rollbar.error(e)
193
- status = 'failed'
194
195
  raise
195
- ensure
196
- if defined?(Yabeda) && Yabeda.configured?
197
- tags = { command: cmd.class.name, status: status, application: application }
198
- dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
199
-
200
- Yabeda.etna.last_command_completion.set(tags, Time.now.to_i)
201
- Yabeda.etna.command_runtime.measure(tags, dur)
202
-
203
- write_job_metrics("run_command.#{cmd.class.name}")
204
- end
205
196
  end
206
197
  end
207
198
  end
data/lib/etna/auth.rb CHANGED
@@ -18,7 +18,7 @@ module Etna
18
18
  if [ approve_noauth(request), approve_hmac(request), approve_user(request) ].all?{|approved| !approved}
19
19
  return fail_or_redirect(request)
20
20
  end
21
-
21
+
22
22
  @app.call(request.env)
23
23
  end
24
24
 
@@ -97,7 +97,7 @@ module Etna
97
97
 
98
98
  return payload unless route
99
99
 
100
- begin
100
+ begin
101
101
  permissions = permissions(payload)
102
102
 
103
103
  janus.resource_projects(token).each do |resource_project|
data/lib/etna/client.rb CHANGED
@@ -4,10 +4,17 @@ require 'rack/utils'
4
4
 
5
5
  module Etna
6
6
  class Client
7
- def initialize(host, token, routes_available: true, ignore_ssl: false)
7
+
8
+ def initialize(host, token, routes_available: true, ignore_ssl: false, max_retries: 10, backoff_time: 15, logger: nil)
8
9
  @host = host.sub(%r!/$!, '')
9
10
  @token = token
10
11
  @ignore_ssl = ignore_ssl
12
+ @max_retries = max_retries
13
+ @backoff_time = backoff_time
14
+
15
+ default_logger = ::Etna::Logger.new('/dev/stdout', 0, 1048576)
16
+ default_logger.level = ::Logger::WARN
17
+ @logger = logger || default_logger
11
18
 
12
19
  if routes_available
13
20
  set_routes
@@ -55,7 +62,7 @@ module Etna
55
62
  uri = request_uri(endpoint)
56
63
  multipart = Net::HTTP::Post::Multipart.new uri.request_uri, content
57
64
  multipart.add_field('Authorization', "Etna #{@token}")
58
- request(uri, multipart, &block)
65
+ retrier.retry_request(uri, multipart, &block)
59
66
  end
60
67
 
61
68
  def post(endpoint, params = {}, &block)
@@ -80,9 +87,12 @@ module Etna
80
87
 
81
88
  private
82
89
 
90
+ def retrier
91
+ @retrier ||= Retrier.new(ignore_ssl: @ignore_ssl, max_retries: @max_retries, backoff_time: @backoff_time, logger: @logger)
92
+ end
93
+
83
94
  def set_routes
84
95
  response = options('/')
85
- status_check!(response)
86
96
  @routes = JSON.parse(response.body, symbolize_names: true)
87
97
  end
88
98
 
@@ -120,7 +130,7 @@ module Etna
120
130
  uri = request_uri(endpoint)
121
131
  req = type.new(uri.request_uri, request_headers)
122
132
  req.body = params.to_json
123
- request(uri, req, &block)
133
+ retrier.retry_request(uri, req, &block)
124
134
  end
125
135
 
126
136
  def query_request(type, endpoint, params = {}, &block)
@@ -132,7 +142,7 @@ module Etna
132
142
  uri.query = URI.encode_www_form(params)
133
143
  end
134
144
  req = type.new(uri.request_uri, request_headers)
135
- request(uri, req, &block)
145
+ retrier.retry_request(uri, req, &block)
136
146
  end
137
147
 
138
148
  def request_uri(endpoint)
@@ -140,6 +150,8 @@ module Etna
140
150
  end
141
151
 
142
152
  def request_headers
153
+ refresh_token
154
+
143
155
  {
144
156
  'Content-Type' => 'application/json',
145
157
  'Accept' => 'application/json, text/*',
@@ -149,46 +161,234 @@ module Etna
149
161
  )
150
162
  end
151
163
 
152
- def status_check!(response)
153
- status = response.code.to_i
154
- if status >= 400
155
- msg = response.content_type == 'application/json' ?
156
- json_error(response.body) :
157
- response.body
158
- raise Etna::Error.new(msg, status)
159
- end
164
+ def refresh_token
165
+ @token = TokenRefresher.new(@host, @token, @logger).active_token
160
166
  end
161
167
 
162
- def json_error(body)
163
- msg = JSON.parse(body, symbolize_names: true)
164
- if (msg.has_key?(:errors) && msg[:errors].is_a?(Array))
165
- return JSON.generate(msg[:errors])
166
- elsif msg.has_key?(:error)
167
- return JSON.generate(msg[:error])
168
+ class TokenRefresher
169
+ def initialize(host, token, logger)
170
+ @token = token
171
+ @host = host
172
+ @logger = logger
173
+ end
174
+
175
+ def active_token
176
+ token_will_expire? ?
177
+ refresh_token :
178
+ @token
179
+ end
180
+
181
+ private
182
+
183
+ def token_expired?
184
+ # Has the token already expired?
185
+ token_will_expire?(0)
186
+ end
187
+
188
+ def token_will_expire?(offset=3000)
189
+ return false if @token.nil?
190
+
191
+ # Will the user's token expire in the given amount of time?
192
+ payload = @token.split('.')[1]
193
+ return false if payload.nil?
194
+
195
+ epoch_seconds = JSON.parse(Base64.urlsafe_decode64(payload))["exp"]
196
+
197
+ return false if epoch_seconds.nil?
198
+
199
+ expiration = DateTime.strptime(epoch_seconds.to_s, "%s").to_time
200
+ expiration <= DateTime.now.new_offset.to_time + offset
201
+ end
202
+
203
+ def refresh_token
204
+ @logger.debug("Requesting a refreshed token.")
205
+ uri = refresh_uri
206
+ req = Net::HTTP::Post.new(uri.request_uri, request_headers)
207
+ retrier.retry_request(uri, req).body
208
+ end
209
+
210
+ def refresh_uri
211
+ URI("#{janus_host}#{refresh_endpoint}")
212
+ end
213
+
214
+ def janus_host
215
+ @host.gsub(/(metis|magma|timur|polyphemus|janus|gnomon)/, "janus")
216
+ end
217
+
218
+ def refresh_endpoint
219
+ "/api/tokens/generate"
220
+ end
221
+
222
+ def request_headers
223
+ {
224
+ 'Content-Type' => 'application/json',
225
+ 'Accept' => 'application/json, text/*',
226
+ 'Authorization' => "Etna #{@token}"
227
+ }
228
+ end
229
+
230
+ def retrier
231
+ @retrier ||= Retrier.new(max_retries: 5, backoff_time: 10, logger: @logger)
168
232
  end
169
233
  end
170
234
 
171
- def request(uri, data)
172
- if block_given?
173
- verify_mode = @ignore_ssl ?
174
- OpenSSL::SSL::VERIFY_NONE :
175
- OpenSSL::SSL::VERIFY_PEER
176
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
177
- http.request(data) do |response|
235
+ class Retrier
236
+ # Ideally the retry code would be centralized with metis_client ...
237
+ # unsure what would be the best approach to do that, at this moment.
238
+ def initialize(ignore_ssl: false, max_retries: 10, backoff_time: 15, logger:)
239
+ @max_retries = max_retries
240
+ @ignore_ssl = ignore_ssl
241
+ @backoff_time = backoff_time
242
+ @logger = logger
243
+ end
244
+
245
+ def retry_request(uri, req, retries: 0, &block)
246
+ retries += 1
247
+
248
+ begin
249
+ @logger.debug("\rWaiting for #{uri.host} restart"+"."*retries+"\x1b[0K")
250
+
251
+ sleep @backoff_time * retries
252
+ end if retries > 1
253
+
254
+ if retries < @max_retries
255
+ begin
256
+ if block_given?
257
+ request(uri, req) do |block_response|
258
+ if net_exceptions.include?(block_response.class)
259
+ @logger.debug("Received #{block_response.class.name}, retrying")
260
+ retry_request(uri, req, retries: retries, &block)
261
+ elsif block_response.is_a?(OpenSSL::SSL::SSLError)
262
+ @logger.debug("SSL error, retrying")
263
+ retry_request(uri, req, retries: retries, &block)
264
+ else
265
+ status_check!(block_response)
266
+ yield block_response
267
+ end
268
+ end
269
+ else
270
+ response = request(uri, req)
271
+ end
272
+ rescue OpenSSL::SSL::SSLError => e
273
+ if e.message =~ /write client hello/
274
+ @logger.debug("SSL error, retrying")
275
+ return retry_request(uri, req, retries: retries)
276
+ end
277
+ raise e
278
+ rescue *net_exceptions => e
279
+ @logger.debug("Received #{e.class.name}, retrying")
280
+ return retry_request(uri, req, retries: retries)
281
+ end
282
+
283
+ begin
284
+ retry_codes = ['503', '502', '504', '408']
285
+ if retry_codes.include?(response.code)
286
+ @logger.debug("Received response with code #{response.code}, retrying")
287
+ return retry_request(uri, req, retries: retries)
288
+ elsif response.code == '500' && response.body.start_with?("Puma caught")
289
+ @logger.debug("Received 500 Puma error #{response.body.split("\n").first}, retrying")
290
+ return retry_request(uri, req, retries: retries)
291
+ end
292
+
178
293
  status_check!(response)
179
- yield response
294
+ return response
295
+ end unless block_given?
296
+ end
297
+
298
+ raise ::Etna::Error, "Could not contact server, giving up" unless block_given?
299
+ end
300
+
301
+ private
302
+
303
+ def request(uri, data)
304
+ if block_given?
305
+ verify_mode = @ignore_ssl ?
306
+ OpenSSL::SSL::VERIFY_NONE :
307
+ OpenSSL::SSL::VERIFY_PEER
308
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
309
+ http.request(data) do |response|
310
+ api_error_check!(response)
311
+ yield response
312
+ end
313
+ end
314
+ else
315
+ verify_mode = @ignore_ssl ?
316
+ OpenSSL::SSL::VERIFY_NONE :
317
+ OpenSSL::SSL::VERIFY_PEER
318
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
319
+ response = http.request(data)
320
+ api_error_check!(response)
321
+ return response
180
322
  end
181
323
  end
182
- else
183
- verify_mode = @ignore_ssl ?
184
- OpenSSL::SSL::VERIFY_NONE :
185
- OpenSSL::SSL::VERIFY_PEER
186
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
187
- response = http.request(data)
188
- status_check!(response)
189
- return response
324
+ end
325
+
326
+ def api_error_check!(response)
327
+ status = response.code.to_i
328
+ raise_status_error(response) if 400 <= status && status < 500
329
+ end
330
+
331
+ def status_check!(response)
332
+ status = response.code.to_i
333
+ raise_status_error(response) if status >= 400
334
+ end
335
+
336
+ def raise_status_error(response)
337
+ msg = response.content_type == 'application/json' ?
338
+ json_error(response.body) :
339
+ response.body
340
+ raise Etna::Error.new(msg, response.code.to_i)
341
+ end
342
+
343
+ def json_error(body)
344
+ msg = JSON.parse(body, symbolize_names: true)
345
+ if (msg.has_key?(:errors) && msg[:errors].is_a?(Array))
346
+ return JSON.generate(msg[:errors])
347
+ elsif msg.has_key?(:error)
348
+ return JSON.generate(msg[:error])
190
349
  end
191
350
  end
351
+
352
+ def net_exceptions
353
+ retry_exceptions = [
354
+ Errno::ECONNREFUSED,
355
+ Errno::ECONNRESET,
356
+ Errno::ENETRESET,
357
+ Errno::EPIPE,
358
+ Errno::ECONNABORTED,
359
+ Errno::EHOSTDOWN,
360
+ Errno::EHOSTUNREACH,
361
+ Errno::EINVAL,
362
+ Errno::ETIMEDOUT,
363
+ Net::ReadTimeout,
364
+ Net::HTTPFatalError,
365
+ Net::HTTPBadResponse,
366
+ Net::HTTPHeaderSyntaxError,
367
+ Net::ProtocolError,
368
+ Net::HTTPRequestTimeOut,
369
+ Net::HTTPGatewayTimeOut,
370
+ Net::HTTPBadRequest,
371
+ Net::HTTPBadGateway,
372
+ Net::HTTPError,
373
+ Net::HTTPInternalServerError,
374
+ Net::HTTPRetriableError,
375
+ Net::HTTPServerError,
376
+ Net::HTTPServiceUnavailable,
377
+ Net::HTTPUnprocessableEntity,
378
+ Net::OpenTimeout,
379
+ IOError,
380
+ EOFError,
381
+ Timeout::Error
382
+ ]
383
+
384
+ begin
385
+ retry_exceptions << Net::HTTPRequestTimeout
386
+ retry_exceptions << Net::HTTPGatewayTimeout
387
+ retry_exceptions << Net::WriteTimeout
388
+ end if RUBY_VERSION > "2.5.8"
389
+
390
+ retry_exceptions
391
+ end
192
392
  end
193
393
  end
194
394
  end
@@ -6,7 +6,7 @@ module Etna
6
6
  module Clients
7
7
  class BaseClient
8
8
  attr_reader :host, :token, :ignore_ssl
9
- def initialize(host:, token:, ignore_ssl: false)
9
+ def initialize(host:, token:, ignore_ssl: false, logger: nil)
10
10
  raise "#{self.class.name} client configuration is missing host." unless host
11
11
 
12
12
  @token = token
@@ -16,7 +16,8 @@ module Etna
16
16
  host,
17
17
  token,
18
18
  routes_available: false,
19
- ignore_ssl: ignore_ssl)
19
+ ignore_ssl: ignore_ssl,
20
+ logger: logger)
20
21
  @host = host
21
22
  @ignore_ssl = ignore_ssl
22
23
  end
@@ -41,6 +41,7 @@ module Etna
41
41
 
42
42
  begin
43
43
  if (error = errors.pop(true))
44
+ logger&.error(error)
44
45
  raise error
45
46
  end
46
47
  rescue ThreadError
@@ -51,6 +52,7 @@ module Etna
51
52
  begin
52
53
  materialize_record(dest, template, document)
53
54
  rescue => e
55
+ logger&.error(e)
54
56
  errors << e
55
57
  ensure
56
58
  semaphore.release
@@ -62,6 +64,7 @@ module Etna
62
64
 
63
65
  begin
64
66
  if (error = errors.pop(true))
67
+ logger&.error(error)
65
68
  raise error
66
69
  end
67
70
  rescue ThreadError
@@ -105,6 +108,7 @@ module Etna
105
108
  end
106
109
 
107
110
  dest_file = File.join(dest_dir, metadata_file_name(record_name: record[template.identifier], record_model_name: template.name, ext: "_#{attr_name}_#{idx}#{File.extname(filename)}"))
111
+ filesystem.mkdir_p(File.dirname(dest_file))
108
112
  sync_metis_data_workflow.copy_file(dest: dest_file, url: url, stub: stub_files)
109
113
  record_to_serialize[attr_name] << {file: dest_file, original_filename: filename}
110
114
  end
@@ -94,7 +94,6 @@ module Etna
94
94
  collections = []
95
95
  links = []
96
96
  attributes = []
97
-
98
97
  model = response.models.model(model_name)
99
98
 
100
99
  template.attributes.attribute_keys.each do |attr_name|
@@ -10,10 +10,10 @@ module Etna
10
10
  module Clients
11
11
  class Metis < Etna::Clients::BaseClient
12
12
 
13
- def initialize(host:, token:, ignore_ssl: false)
13
+ def initialize(host:, token:, ignore_ssl: false, logger: nil)
14
14
  raise 'Metis client configuration is missing host.' unless host
15
15
  raise 'Metis client configuration is missing token.' unless token
16
- @etna_client = ::Etna::Client.new(host, token, ignore_ssl: ignore_ssl)
16
+ @etna_client = ::Etna::Client.new(host, token, ignore_ssl: ignore_ssl, logger: logger)
17
17
 
18
18
  @token = token
19
19
  end
@@ -22,36 +22,56 @@ module Etna
22
22
  end
23
23
 
24
24
  def copy_file(dest:, url:, stub: false)
25
- # This does not work due to the magma bucket's restrictions, but if it did work, it'd be super sweet.
26
- # url_match = DOWNLOAD_REGEX.match(url)
27
- #
28
- # if filesystem.instance_of?(Etna::Filesystem::Metis) && !url_match.nil?
29
- # bucket_name = url_match[:bucket_name]
30
- # project_name = url_match[:project_name]
31
- # file_path = url_match[:file_path]
32
- #
33
- # metis_client.copy_files(
34
- # Etna::Clients::Metis::CopyFilesRequest.new(
35
- # project_name: project_name,
36
- # revisions: [
37
- # Etna::Clients::Metis::CopyRevision.new(
38
- # source: "metis://#{project_name}/#{bucket_name}/#{file_path}",
39
- # dest: "metis://#{filesystem.project_name}/#{filesystem.bucket_name}#{dest}",
40
- # )
41
- # ]
42
- # )
43
- # )
44
- # end
25
+ url_match = DOWNLOAD_REGEX.match(url)
26
+
27
+ if filesystem.instance_of?(Etna::Filesystem::Metis) && !url_match.nil?
28
+ bucket_name = url_match[:bucket_name]
29
+ project_name = url_match[:project_name]
30
+ file_path = url_match[:file_path]
31
+
32
+ # ensure target parent directory exists
33
+ metis_client.ensure_parent_folder_exists(
34
+ project_name: filesystem.project_name,
35
+ bucket_name: filesystem.bucket_name,
36
+ path: dest
37
+ )
38
+
39
+ metis_client.copy_files(
40
+ Etna::Clients::Metis::CopyFilesRequest.new(
41
+ project_name: project_name,
42
+ revisions: [
43
+ Etna::Clients::Metis::CopyRevision.new(
44
+ source: "metis://#{project_name}/#{bucket_name}/#{file_path}",
45
+ dest: "metis://#{filesystem.project_name}/#{filesystem.bucket_name}/#{dest}",
46
+ )
47
+ ]
48
+ )
49
+ )
50
+
51
+ return
52
+ end
45
53
 
46
54
  metadata = metis_client.file_metadata(url)
47
55
  size = metadata[:size]
48
56
 
57
+ begin
58
+ if filesystem.exist?(dest) && filesystem.stat(dest).size == size
59
+ logger&.info "Already downloaded #{dest}"
60
+ return
61
+ end
62
+ rescue Etna::Filesystem::Error => e
63
+ unless e.message =~ /stat not supported/
64
+ raise e
65
+ end
66
+ end
67
+
49
68
  tmp_file = dest
50
69
  upload_timings = []
51
70
  upload_amount = 0
52
71
  last_rate = 0.00001
53
72
  remaining = size
54
73
 
74
+ logger&.info "Downloading #{dest} - #{Etna::Formatting.as_size(size)}"
55
75
  filesystem.with_writeable(tmp_file, "w", size_hint: size) do |io|
56
76
  if stub
57
77
  io.write("(stub) #{size} bytes")
@@ -78,7 +98,7 @@ module Etna
78
98
  rate = upload_amount / (end_time - start_time)
79
99
 
80
100
  if rate / last_rate > 1.3 || rate / last_rate < 0.7
81
- logger&.info("Uploading #{Etna::Formatting.as_size(rate)} per second, #{Etna::Formatting.as_size(remaining)} remaining")
101
+ logger&.debug("Uploading #{Etna::Formatting.as_size(rate)} per second, #{Etna::Formatting.as_size(remaining)} remaining")
82
102
 
83
103
  if rate == 0
84
104
  last_rate = 0.0001
@@ -87,7 +107,6 @@ module Etna
87
107
  end
88
108
  end
89
109
  end
90
-
91
110
  end
92
111
  end
93
112
  end
data/lib/etna/command.rb CHANGED
@@ -136,7 +136,7 @@ module Etna
136
136
  "<#{name}>..."
137
137
  when :keyrest
138
138
  "[flags...]"
139
- when :key
139
+ when :key, :keyreq
140
140
  flag = "--#{name.to_s.gsub('_', '-')}"
141
141
  if self.class.boolean_flags.include?(flag)
142
142
  "[#{flag}]"