Diff
checker
文本
文本
圖像
文檔
Excel
文件夾
Legal
Enterprise
桌面版
定價
登入
下載 Diffchecker 桌面版
比較文本
尋找兩個文字檔案之間的差異
工具
歷史
即時編輯器
隱藏空白變更
摺疊未變更行
關閉換行
檢視
拆分
統一
比對精度
智能
單詞
字符
文字樣式
變更外觀
語法突出顯示
選擇語法
忽略
文字轉換
前往第一個差異
編輯輸入
Diffchecker Desktop
執行Diffchecker最安全的方式。取得Diffchecker桌面應用程式:您的差異永遠不會離開您的電腦!
取得桌面版
foremansalt
建立於
2 年前
差異永不過期
清除
匯出
分享
解釋
49 刪除
行
總計
刪除
字符
總計
刪除
要繼續使用此功能,請升級到
Diff
checker
Pro
查看價格
176 行
全部複製
148 新增
行
總計
新增
字符
總計
新增
要繼續使用此功能,請升級到
Diff
checker
Pro
查看價格
262 行
全部複製
module ForemanSalt
module ForemanSalt
class ReportImporter
class ReportImporter
delegate :logger, to: :Rails
delegate :logger, to: :Rails
attr_reader :report
attr_reader :report
複製
已複製
複製
已複製
# Define logger as a class method
def self.logger
Rails.logger
end
def self.import(raw, proxy_id = nil)
def self.import(raw, proxy_id = nil)
複製
已複製
複製
已複製
logger.info "Starting import with raw data: #{raw.inspect} and proxy_id: #{proxy_id}"
raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash)
raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash)
raw.map do |host, report|
raw.map do |host, report|
複製
已複製
複製
已複製
logger.debug "Processing report for host: #{host}"
importer = ForemanSalt::ReportImporter.new(host, report, proxy_id)
importer = ForemanSalt::ReportImporter.new(host, report, proxy_id)
importer.import
importer.import
report = importer.report
report = importer.report
report.origin = 'Salt'
report.origin = 'Salt'
report.save!
report.save!
複製
已複製
複製
已複製
logger.info "Report saved for host: #{host}"
report
report
end
end
複製
已複製
複製
已複製
rescue => e
logger.error "Import failed: #{e.message}"
raise e
end
end
def initialize(host, raw, proxy_id = nil)
def initialize(host, raw, proxy_id = nil)
複製
已複製
複製
已複製
logger.debug "Initializing ReportImporter with host: #{host}, proxy_id: #{proxy_id}"
@host = find_or_create_host(host)
@host = find_or_create_host(host)
@raw = raw
@raw = raw
@proxy_id = proxy_id
@proxy_id = proxy_id
end
end
def import
def import
複製
已複製
複製
已複製
logger.info "
p
rocessing report for #{@host}"
logger.info "
P
rocessing report for #{@host}"
logger.debug { "Report
: #{@raw.inspect}" }
logger.debug { "Report
raw data
: #{@raw.inspect}" }
if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded]
if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded]
複製
已複製
複製
已複製
logger.info("
s
kipping report for #{@host} as it
s an unknown host and create_new_host_when_report_is_uploaded setting is disabled")
logger.info("
S
kipping report for #{@host} as it
'
s an unknown host and create_new_host_when_report_is_uploaded setting is disabled")
return ConfigReport.new
return ConfigReport.new
end
end
@host.salt_proxy_id ||= @proxy_id
@host.salt_proxy_id ||= @proxy_id
@host.last_report = start_time
@host.last_report = start_time
複製
已複製
複製
已複製
if [Array, String].
member?
@raw.class
if [Array, String].
include?(
@raw.class
)
logger.debug "Detected failures in raw data; processing failures"
process_failures # If Salt sends us only an array (or string), it's a list of fatal failures
process_failures # If Salt sends us only an array (or string), it's a list of fatal failures
else
else
複製
已複製
複製
已複製
logger.debug "Processing normal report data"
process_normal
process_normal
end
end
複製
已複製
複製
已複製
@host.save(validate:
false)
if
@host.save(validate:
true)
logger.debug "Host #{@host.name} saved successfully"
else
logger.warn "Host #{@host.name} failed to save: #{@host.errors.full_messages.join(', ')}"
end
@host.reload
@host.reload
@host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')])
@host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')])
複製
已複製
複製
已複製
logger.info("Imported report for #{@host} in #{
(Time.zone.now - start_time).round(2)
} seconds")
duration = (Time.zone.now - start_time).round(2)
logger.info("Imported report for #{@host} in #{
duration
} seconds")
rescue => e
logger.error "Failed to import report for #{@host}: #{e.message}"
raise e
end
end
private
private
def find_or_create_host(host)
def find_or_create_host(host)
複製
已複製
複製
已複製
logger.debug "Finding or creating host: #{host}"
@host ||= Host::Managed.find_by(name: host)
@host ||= Host::Managed.find_by(name: host)
unless @host
unless @host
複製
已複製
複製
已複製
new
= Host::Managed.new(name: host)
logger.info "Host not found; creating new host with name: #{host}"
new.save(validate: false)
new
_host
= Host::Managed.new(name: host)
@host = new
if new_host.save(validate: true)
logger.debug "New host #{host} created successfully"
@host = new_host
else
logger.error "Failed to create host #{host}: #{new_host.errors.full_messages.join(', ')}"
raise ::Foreman::Exception, _('Failed to create host')
end
end
end
@host
@host
複製
已複製
複製
已複製
rescue => e
logger.error "Error in find_or_create_host: #{e.message}"
raise e
end
end
def import_log_messages
def import_log_messages
複製
已複製
複製
已複製
logger.debug "Importing log messages"
@raw.each do |resource, result|
@raw.each do |resource, result|
複製
已複製
複製
已複製
level =
if result['changes'].blank? && result['result']
level =
determine_log_level(result)
:info
source_value = resource.to_s
elsif result['result'] == false
source = Source.find_or_create_by(value:
source_value)
:err
logger.debug "Log source: #{source_value}"
else
# nil mean "unchanged" when running highstate with test=True
:notice
end
source = Source.find_or_create_by(value:
resource)
複製
已複製
複製
已複製
message
= if result['changes']['diff']
message
_value = extract_message(result)
result['changes']['diff']
message = Message.find_or_create_by(value: message_value)
elsif result['pchanges'].present? && result['pchanges'].include?('diff')
logger.debug "Log message: #{message_value}"
result['pchanges']['diff']
elsif result['comment'].presence
result['comment']
else
'No message available'
end
複製
已複製
複製
已複製
message = Message.find_or_create_by(value: message)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: level)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: level)
複製
已複製
複製
已複製
logger.debug "Log entry created with level #{level} for resource #{resource}"
end
end
複製
已複製
複製
已複製
rescue => e
logger.error "Error importing log messages: #{e.message}"
raise e
end
end
def calculate_metrics
def calculate_metrics
複製
已複製
複製
已複製
logger.debug "Calculating metrics from raw data"
success = 0
success = 0
failed = 0
failed = 0
changed = 0
changed = 0
restarted = 0
restarted = 0
restarted_failed = 0
restarted_failed = 0
pending = 0
pending = 0
time = {}
time = {}
@raw.each do |resource, result|
@raw.each do |resource, result|
複製
已複製
複製
已複製
next unless result.is_a?
Hash
next unless result.is_a?
(
Hash
)
logger.debug "Processing resource: #{resource}"
複製
已複製
複製
已複製
if result['result']
if result['result']
== true
success += 1
success += 1
複製
已複製
複製
已複製
logger.debug "Resource #{resource} succeeded"
if resource.match(/^service_/) && result['comment'].include?('restarted')
if resource.match(/^service_/) && result['comment'].include?('restarted')
restarted += 1
restarted += 1
複製
已複製
複製
已複製
logger.debug "Service #{resource} was restarted"
elsif result['changes'].present?
elsif result['changes'].present?
changed += 1
changed += 1
複製
已複製
複製
已複製
logger.debug "Resource #{resource} changed"
elsif result['pchanges'].present?
elsif result['pchanges'].present?
pending += 1
pending += 1
複製
已複製
複製
已複製
logger.debug "Resource #{resource} has pending changes"
end
end
elsif result['result'].nil?
elsif result['result'].nil?
pending += 1
pending += 1
複製
已複製
複製
已複製
elsif
!
result['result']
logger.debug "Resource #{resource} is pending"
elsif
result['result']
== false
if resource.match(/^service_/) && result['comment'].include?('restarted')
if resource.match(/^service_/) && result['comment'].include?('restarted')
restarted_failed += 1
restarted_failed += 1
複製
已複製
複製
已複製
logger.debug "Service #{resource} failed to restart"
else
else
failed += 1
failed += 1
複製
已複製
複製
已複製
logger.debug "Resource #{resource} failed"
end
end
end
end
複製
已複製
複製
已複製
duration =
if result['
duration
'].is_a? String
duration =
parse_
duration
(result['duration'])
begin
Float(result['duration'].delete(' ms'))
rescue StandardError
nil
end
else
result['duration']
end
# Convert duration from milliseconds to seconds
duration /= 1000 if duration.is_a? Float
time[resource] = duration || 0
time[resource] = duration || 0
複製
已複製
複製
已複製
logger.debug "Duration for resource #{resource}: #{time[resource]} seconds"
end
end
time[:total] = time.values.compact.sum || 0
time[:total] = time.values.compact.sum || 0
複製
已複製
複製
已複製
logger.debug "Total execution time: #{time[:total]} seconds"
events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed }
events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed }
複製
已複製
複製
已複製
changes = { total: changed + restarted }
changes = { total: changed + restarted }
複製
已複製
複製
已複製
resources = {
'total' => @raw.size,
'applied' => changed,
'restarted' => restarted,
'failed' => failed,
'failed_restarts' => restarted_failed,
'skipped' => 0,
'scheduled' => 0,
'pending' => pending
}
複製
已複製
複製
已複製
resources = { 'total' => @raw.size, 'applied' => changed, 'restarted' => restarted, 'failed' => failed,
logger.debug "Metrics calculated: events=#{events}, changes=#{changes}, resources=#{resources}"
'failed_restarts' => restarted_failed, 'skipped' => 0, 'scheduled' => 0, 'pending' => pending }
{ events: events, resources: resources, changes: changes, time: time }
{ events: events, resources: resources, changes: changes, time: time }
複製
已複製
複製
已複製
rescue => e
logger.error "Error calculating metrics: #{e.message}"
raise e
end
end
def process_normal
def process_normal
複製
已複製
複製
已複製
logger.debug "Processing normal report"
metrics = calculate_metrics
metrics = calculate_metrics
status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate
status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate
複製
已複製
複製
已複製
logger.debug "Calculated status: #{status.inspect}"
@report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics)
@report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics)
複製
已複製
複製
已複製
return @report unless
@report.save
if
@report.save
logger.debug "Report saved successfully for host #{@host.name}"
import_log_messages
import_log_messages
else
logger.error "Failed to save report: #{@report.errors.full_messages.join(', ')}"
end
rescue => e
logger.error "Error processing normal report: #{e.message}"
raise e
end
end
def process_failures
def process_failures
複製
已複製
複製
已複製
@raw = [@raw] unless @raw.is_a?
Array
logger.debug "Processing failure report"
@raw = [@raw] unless @raw.is_a?
(
Array
)
status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate
status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate
複製
已複製
複製
已複製
logger.debug "Calculated status for failures: #{status.inspect}"
@report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {})
@report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {})
source = Source.find_or_create_by(value: 'Salt')
source = Source.find_or_create_by(value: 'Salt')
@raw.each do |failure|
@raw.each do |failure|
message = Message.find_or_create_by(value: failure)
message = Message.find_or_create_by(value: failure)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err)
Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err)
複製
已複製
複製
已複製
logger.debug "Logged failure message: #{failure}"
end
end
複製
已複製
複製
已複製
rescue => e
logger.error "Error processing failures: #{e.message}"
raise e
end
end
def start_time
def start_time
@start_time ||= Time.zone.now
@start_time ||= Time.zone.now
end
end
複製
已複製
複製
已複製
def determine_log_level(result)
if result['changes'].blank? && result['result'] == true
:info
elsif result['result'] == false
:err
else
:notice
end
end
def extract_message(result)
if result['changes'] && result['changes']['diff']
result['changes']['diff']
elsif result['pchanges'] && result['pchanges']['diff']
result['pchanges']['diff']
elsif result['comment'].present?
result['comment']
else
'No message available'
end
end
def parse_duration(duration)
if duration.is_a?(String)
duration_in_ms = duration.delete(' ms')
Float(duration_in_ms) / 1000
else
duration.to_f / 1000
end
rescue
logger.warn "Unable to parse duration: #{duration}"
nil
end
end
end
end
end
已保存差異
原始文本
開啟檔案
module ForemanSalt class ReportImporter delegate :logger, to: :Rails attr_reader :report def self.import(raw, proxy_id = nil) raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash) raw.map do |host, report| importer = ForemanSalt::ReportImporter.new(host, report, proxy_id) importer.import report = importer.report report.origin = 'Salt' report.save! report end end def initialize(host, raw, proxy_id = nil) @host = find_or_create_host(host) @raw = raw @proxy_id = proxy_id end def import logger.info "processing report for #{@host}" logger.debug { "Report: #{@raw.inspect}" } if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded] logger.info("skipping report for #{@host} as its an unknown host and create_new_host_when_report_is_uploaded setting is disabled") return ConfigReport.new end @host.salt_proxy_id ||= @proxy_id @host.last_report = start_time if [Array, String].member? @raw.class process_failures # If Salt sends us only an array (or string), it's a list of fatal failures else process_normal end @host.save(validate: false) @host.reload @host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')]) logger.info("Imported report for #{@host} in #{(Time.zone.now - start_time).round(2)} seconds") end private def find_or_create_host(host) @host ||= Host::Managed.find_by(name: host) unless @host new = Host::Managed.new(name: host) new.save(validate: false) @host = new end @host end def import_log_messages @raw.each do |resource, result| level = if result['changes'].blank? && result['result'] :info elsif result['result'] == false :err else # nil mean "unchanged" when running highstate with test=True :notice end source = Source.find_or_create_by(value: resource) message = if result['changes']['diff'] result['changes']['diff'] elsif result['pchanges'].present? && result['pchanges'].include?('diff') result['pchanges']['diff'] elsif result['comment'].presence result['comment'] else 'No message available' end message = Message.find_or_create_by(value: message) Log.create(message_id: message.id, source_id: source.id, report: @report, level: level) end end def calculate_metrics success = 0 failed = 0 changed = 0 restarted = 0 restarted_failed = 0 pending = 0 time = {} @raw.each do |resource, result| next unless result.is_a? Hash if result['result'] success += 1 if resource.match(/^service_/) && result['comment'].include?('restarted') restarted += 1 elsif result['changes'].present? changed += 1 elsif result['pchanges'].present? pending += 1 end elsif result['result'].nil? pending += 1 elsif !result['result'] if resource.match(/^service_/) && result['comment'].include?('restarted') restarted_failed += 1 else failed += 1 end end duration = if result['duration'].is_a? String begin Float(result['duration'].delete(' ms')) rescue StandardError nil end else result['duration'] end # Convert duration from milliseconds to seconds duration /= 1000 if duration.is_a? Float time[resource] = duration || 0 end time[:total] = time.values.compact.sum || 0 events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed } changes = { total: changed + restarted } resources = { 'total' => @raw.size, 'applied' => changed, 'restarted' => restarted, 'failed' => failed, 'failed_restarts' => restarted_failed, 'skipped' => 0, 'scheduled' => 0, 'pending' => pending } { events: events, resources: resources, changes: changes, time: time } end def process_normal metrics = calculate_metrics status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate @report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics) return @report unless @report.save import_log_messages end def process_failures @raw = [@raw] unless @raw.is_a? Array status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate @report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {}) source = Source.find_or_create_by(value: 'Salt') @raw.each do |failure| message = Message.find_or_create_by(value: failure) Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err) end end def start_time @start_time ||= Time.zone.now end end end
更改後文本
開啟檔案
module ForemanSalt class ReportImporter delegate :logger, to: :Rails attr_reader :report # Define logger as a class method def self.logger Rails.logger end def self.import(raw, proxy_id = nil) logger.info "Starting import with raw data: #{raw.inspect} and proxy_id: #{proxy_id}" raise ::Foreman::Exception, _('Invalid report') unless raw.is_a?(Hash) raw.map do |host, report| logger.debug "Processing report for host: #{host}" importer = ForemanSalt::ReportImporter.new(host, report, proxy_id) importer.import report = importer.report report.origin = 'Salt' report.save! logger.info "Report saved for host: #{host}" report end rescue => e logger.error "Import failed: #{e.message}" raise e end def initialize(host, raw, proxy_id = nil) logger.debug "Initializing ReportImporter with host: #{host}, proxy_id: #{proxy_id}" @host = find_or_create_host(host) @raw = raw @proxy_id = proxy_id end def import logger.info "Processing report for #{@host}" logger.debug { "Report raw data: #{@raw.inspect}" } if @host.new_record? && !Setting[:create_new_host_when_report_is_uploaded] logger.info("Skipping report for #{@host} as it's an unknown host and create_new_host_when_report_is_uploaded setting is disabled") return ConfigReport.new end @host.salt_proxy_id ||= @proxy_id @host.last_report = start_time if [Array, String].include?(@raw.class) logger.debug "Detected failures in raw data; processing failures" process_failures # If Salt sends us only an array (or string), it's a list of fatal failures else logger.debug "Processing normal report data" process_normal end if @host.save(validate: true) logger.debug "Host #{@host.name} saved successfully" else logger.warn "Host #{@host.name} failed to save: #{@host.errors.full_messages.join(', ')}" end @host.reload @host.refresh_statuses([HostStatus.find_status_by_humanized_name('configuration')]) duration = (Time.zone.now - start_time).round(2) logger.info("Imported report for #{@host} in #{duration} seconds") rescue => e logger.error "Failed to import report for #{@host}: #{e.message}" raise e end private def find_or_create_host(host) logger.debug "Finding or creating host: #{host}" @host ||= Host::Managed.find_by(name: host) unless @host logger.info "Host not found; creating new host with name: #{host}" new_host = Host::Managed.new(name: host) if new_host.save(validate: true) logger.debug "New host #{host} created successfully" @host = new_host else logger.error "Failed to create host #{host}: #{new_host.errors.full_messages.join(', ')}" raise ::Foreman::Exception, _('Failed to create host') end end @host rescue => e logger.error "Error in find_or_create_host: #{e.message}" raise e end def import_log_messages logger.debug "Importing log messages" @raw.each do |resource, result| level = determine_log_level(result) source_value = resource.to_s source = Source.find_or_create_by(value: source_value) logger.debug "Log source: #{source_value}" message_value = extract_message(result) message = Message.find_or_create_by(value: message_value) logger.debug "Log message: #{message_value}" Log.create(message_id: message.id, source_id: source.id, report: @report, level: level) logger.debug "Log entry created with level #{level} for resource #{resource}" end rescue => e logger.error "Error importing log messages: #{e.message}" raise e end def calculate_metrics logger.debug "Calculating metrics from raw data" success = 0 failed = 0 changed = 0 restarted = 0 restarted_failed = 0 pending = 0 time = {} @raw.each do |resource, result| next unless result.is_a?(Hash) logger.debug "Processing resource: #{resource}" if result['result'] == true success += 1 logger.debug "Resource #{resource} succeeded" if resource.match(/^service_/) && result['comment'].include?('restarted') restarted += 1 logger.debug "Service #{resource} was restarted" elsif result['changes'].present? changed += 1 logger.debug "Resource #{resource} changed" elsif result['pchanges'].present? pending += 1 logger.debug "Resource #{resource} has pending changes" end elsif result['result'].nil? pending += 1 logger.debug "Resource #{resource} is pending" elsif result['result'] == false if resource.match(/^service_/) && result['comment'].include?('restarted') restarted_failed += 1 logger.debug "Service #{resource} failed to restart" else failed += 1 logger.debug "Resource #{resource} failed" end end duration = parse_duration(result['duration']) time[resource] = duration || 0 logger.debug "Duration for resource #{resource}: #{time[resource]} seconds" end time[:total] = time.values.compact.sum || 0 logger.debug "Total execution time: #{time[:total]} seconds" events = { total: changed + failed + restarted + restarted_failed, success: success + restarted, failure: failed + restarted_failed } changes = { total: changed + restarted } resources = { 'total' => @raw.size, 'applied' => changed, 'restarted' => restarted, 'failed' => failed, 'failed_restarts' => restarted_failed, 'skipped' => 0, 'scheduled' => 0, 'pending' => pending } logger.debug "Metrics calculated: events=#{events}, changes=#{changes}, resources=#{resources}" { events: events, resources: resources, changes: changes, time: time } rescue => e logger.error "Error calculating metrics: #{e.message}" raise e end def process_normal logger.debug "Processing normal report" metrics = calculate_metrics status = ConfigReportStatusCalculator.new(counters: metrics[:resources].slice(*::ConfigReport::METRIC)).calculate logger.debug "Calculated status: #{status.inspect}" @report = ConfigReport.new(host: @host, reported_at: start_time, status: status, metrics: metrics) if @report.save logger.debug "Report saved successfully for host #{@host.name}" import_log_messages else logger.error "Failed to save report: #{@report.errors.full_messages.join(', ')}" end rescue => e logger.error "Error processing normal report: #{e.message}" raise e end def process_failures logger.debug "Processing failure report" @raw = [@raw] unless @raw.is_a?(Array) status = ConfigReportStatusCalculator.new(counters: { 'failed' => @raw.size }).calculate logger.debug "Calculated status for failures: #{status.inspect}" @report = ConfigReport.create(host: @host, reported_at: Time.zone.now, status: status, metrics: {}) source = Source.find_or_create_by(value: 'Salt') @raw.each do |failure| message = Message.find_or_create_by(value: failure) Log.create(message_id: message.id, source_id: source.id, report: @report, level: :err) logger.debug "Logged failure message: #{failure}" end rescue => e logger.error "Error processing failures: #{e.message}" raise e end def start_time @start_time ||= Time.zone.now end def determine_log_level(result) if result['changes'].blank? && result['result'] == true :info elsif result['result'] == false :err else :notice end end def extract_message(result) if result['changes'] && result['changes']['diff'] result['changes']['diff'] elsif result['pchanges'] && result['pchanges']['diff'] result['pchanges']['diff'] elsif result['comment'].present? result['comment'] else 'No message available' end end def parse_duration(duration) if duration.is_a?(String) duration_in_ms = duration.delete(' ms') Float(duration_in_ms) / 1000 else duration.to_f / 1000 end rescue logger.warn "Unable to parse duration: #{duration}" nil end end end
尋找差異