Revision 33d9f9ee
Added by Lukas Zapletal over 8 years ago
app/assets/javascripts/application.js | ||
---|---|---|
//= require vendor
|
||
//= require about
|
||
//= require proxy_status
|
||
//= require proxy_status/puppet
|
||
//= require proxy_status/logs
|
||
//= require jquery.extentions
|
||
//= require jquery.multi-select
|
||
//= require settings
|
app/assets/javascripts/proxy_status.js | ||
---|---|---|
$('.nav-tabs a[href='+anchor+']').tab('show');
|
||
}
|
||
}
|
||
|
||
function filterCerts(state) {
|
||
$('#certificates table').dataTable().fnFilter(state, 1, true);
|
||
}
|
||
|
||
function certTable() {
|
||
activateDatatables();
|
||
var filter = $('.puppetca-filters');
|
||
filter.select2();
|
||
filter.on('change', function() {filterCerts(filter.val())});
|
||
filterCerts(__('valid')+'|'+__('pending'));
|
||
}
|
app/assets/javascripts/proxy_status/logs.js | ||
---|---|---|
function filterLogsReset() {
|
||
var table = $('#table-proxy-status-logs').dataTable();
|
||
for (var i = 0; i <= 2; i++) {
|
||
table.fnFilter('', i);
|
||
}
|
||
}
|
||
|
||
function filterLogsByLevel(filter) {
|
||
filterLogsReset();
|
||
var table = $('#table-proxy-status-logs').dataTable();
|
||
table.fnFilter(filter, 1, true, false);
|
||
}
|
||
|
||
function filterLogsByMessage(expression) {
|
||
filterLogsReset();
|
||
changeFilterSelection(1);
|
||
var table = $('#table-proxy-status-logs').dataTable();
|
||
table.fnFilter('ERROR|FATAL', 1, true, false);
|
||
table.fnFilter(expression, 2, true, false);
|
||
}
|
||
|
||
function changeFilterSelection(index) {
|
||
var filter = $('#logs-filter');
|
||
filter[0].options[index].selected = true
|
||
filter.trigger('change');
|
||
filterLogsByLevel(filter.val());
|
||
}
|
||
|
||
function activateLogsDataTable() {
|
||
$('#table-proxy-status-logs').dataTable({
|
||
"sDom": "<'row'<'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
|
||
"sPaginationType": "bootstrap",
|
||
"aoColumnDefs": [{
|
||
"mRender": function ( data, type, row ) {
|
||
return new Date(data * 1000).toLocaleString();
|
||
},
|
||
"sWidth": "15%",
|
||
"aTargets": [0]
|
||
},{
|
||
"sWidth": "10%",
|
||
"aTargets": [1]
|
||
}]});
|
||
var filter = $('#logs-filter');
|
||
filter.select2();
|
||
filter.on('change', function() { filterLogsByLevel(filter.val()) });
|
||
|
||
$('#logEntryModal').on('show.bs.modal', function (event) {
|
||
var link = $(event.relatedTarget);
|
||
var modal = $(this);
|
||
var datetime = new Date(link.data('time') * 1000);
|
||
modal.find('#modal-bt-timegmt').text(datetime.toUTCString());
|
||
modal.find('#modal-bt-time').text(datetime.toLocaleString());
|
||
modal.find('#modal-bt-level').text(link.data('level'));
|
||
if (link.data('message')) modal.find('#modal-bt-message').text(link.data('message'));
|
||
if (link.data('backtrace')) modal.find('#modal-bt-backtrace').text(link.data('backtrace'));
|
||
})
|
||
}
|
||
|
||
function expireLogs(item, from) {
|
||
table_url = item.getAttribute('data-url');
|
||
errors_url = item.getAttribute('data-url-errors');
|
||
modules_url = item.getAttribute('data-url-modules');
|
||
if (table_url && errors_url && modules_url) {
|
||
$.ajax({
|
||
type: 'POST',
|
||
url: table_url,
|
||
data: 'from=' + from,
|
||
success: function(result) {
|
||
$("#logs").html(result);
|
||
activateLogsDataTable();
|
||
},
|
||
complete: function(){
|
||
reloadOnAjaxComplete(item);
|
||
}
|
||
})
|
||
$.ajax({
|
||
type: 'GET',
|
||
url: errors_url,
|
||
success: function(result) {
|
||
$("#ajax-errors-card").html(result);
|
||
},
|
||
complete: function(){
|
||
reloadOnAjaxComplete(item);
|
||
}
|
||
})
|
||
$.ajax({
|
||
type: 'GET',
|
||
url: modules_url,
|
||
success: function(result) {
|
||
$("#ajax-modules-card").html(result);
|
||
},
|
||
complete: function(){
|
||
reloadOnAjaxComplete(item);
|
||
}
|
||
})
|
||
}
|
||
}
|
app/assets/javascripts/proxy_status/puppet.js | ||
---|---|---|
function filterCerts(state) {
|
||
$('#certificates table').dataTable().fnFilter(state, 1, true);
|
||
}
|
||
|
||
function certTable() {
|
||
activateDatatables();
|
||
var filter = $('#puppetca-filter');
|
||
filter.select2();
|
||
filter.on('change', function() {filterCerts(filter.val())});
|
||
filterCerts(__('valid')+'|'+__('pending'));
|
||
}
|
app/assets/stylesheets/application.scss | ||
---|---|---|
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-big {
|
||
width: 80%;
|
||
}
|
||
|
||
@media screen and (min-width: 768px) {
|
||
.modal-big {
|
||
width: 70%;
|
||
}
|
||
}
|
||
|
||
.pre-scrollable-xy-modal {
|
||
overflow: auto;
|
||
-ms-word-wrap: normal;
|
||
word-wrap: normal;
|
||
overflow-wrap: normal;
|
||
white-space: pre;
|
||
max-height: 200px;
|
||
padding: 4px;
|
||
}
|
app/assets/stylesheets/smart_proxy.scss | ||
---|---|---|
min-height: 0px;
|
||
}
|
||
|
||
.puppetca-filters {
|
||
.datatable-filter {
|
||
min-width: 150px;
|
||
}
|
||
|
app/controllers/api/v2/smart_proxies_controller.rb | ||
---|---|---|
include Api::Version2
|
||
include Api::TaxonomyScope
|
||
include Api::ImportPuppetclassesCommonController
|
||
before_filter :find_resource, :only => %w{show update destroy refresh version}
|
||
before_filter :find_resource, :only => %w{show update destroy refresh version logs}
|
||
|
||
api :GET, "/smart_proxies/", N_("List all smart proxies")
|
||
param_group :taxonomy_scope, ::Api::V2::BaseController
|
||
... | ... | |
end
|
||
|
||
def version
|
||
begin
|
||
version = @smart_proxy.version
|
||
rescue Foreman::Exception => exception
|
||
render :version, :locals => {:success => false, :message => exception.message} and return
|
||
end
|
||
render :version, :locals => {:success => true, :message => version[:message]}
|
||
render :version, :locals => {:success => true, :result => @smart_proxy.statuses[:version].version}
|
||
rescue Foreman::Exception => e
|
||
render_error :custom_error, :status => :unprocessable_entity, :locals => {:message => e.message}
|
||
end
|
||
|
||
def logs
|
||
render :logs, :locals => {:success => true, :result => @smart_proxy.statuses[:logs].logs}
|
||
rescue Foreman::Exception => e
|
||
render_error :custom_error, :status => :unprocessable_entity, :locals => {:message => e.message}
|
||
end
|
||
|
||
private
|
||
... | ... | |
case params[:action]
|
||
when 'refresh'
|
||
:edit
|
||
when 'version'
|
||
when 'version', 'logs'
|
||
:view
|
||
else
|
||
super
|
app/controllers/smart_proxies_controller.rb | ||
---|---|---|
class SmartProxiesController < ApplicationController
|
||
include Foreman::Controller::AutoCompleteSearch
|
||
|
||
before_filter :find_resource, :only => [:show, :edit, :update, :refresh, :ping, :tftp_server, :destroy, :puppet_environments, :puppet_dashboard]
|
||
before_filter :find_resource, :only => [:show, :edit, :update, :refresh, :ping, :tftp_server, :destroy, :puppet_environments, :puppet_dashboard, :log_pane, :failed_modules, :errors_card, :modules_card, :expire_logs]
|
||
before_filter :find_status, :only => [:ping, :tftp_server, :puppet_environments]
|
||
|
||
def index
|
||
... | ... | |
end
|
||
end
|
||
|
||
def log_pane
|
||
render :partial => 'smart_proxies/logs/list', :locals => {:log_entries => @smart_proxy.statuses[:logs].logs.log_entries}
|
||
rescue Foreman::Exception => exception
|
||
process_ajax_error exception
|
||
end
|
||
|
||
def expire_logs
|
||
from = (params[:from].to_i rescue 0) || 0
|
||
if from >= 0
|
||
logger.debug "Expired smart-proxy logs, new timestamp is #{from}"
|
||
@smart_proxy.expired_logs = from.to_s
|
||
@smart_proxy.save!
|
||
end
|
||
@smart_proxy.statuses[:logs].revoke_cache!
|
||
log_pane
|
||
rescue Foreman::Exception => exception
|
||
process_ajax_error exception
|
||
end
|
||
|
||
def failed_modules
|
||
modules = @smart_proxy.statuses[:logs].logs.failed_modules || {}
|
||
render :partial => 'smart_proxies/logs/failed_modules', :locals => {:modules => modules}
|
||
rescue Foreman::Exception => exception
|
||
process_ajax_error exception
|
||
end
|
||
|
||
def errors_card
|
||
logs = @smart_proxy.statuses[:logs].logs
|
||
render :partial => 'smart_proxies/logs/errors_card', :locals => {:logs => logs}
|
||
rescue Foreman::Exception => exception
|
||
process_ajax_error exception
|
||
end
|
||
|
||
def modules_card
|
||
logs = @smart_proxy.statuses[:logs].logs
|
||
render :partial => 'smart_proxies/logs/modules_card', :locals => {
|
||
:logs => logs,
|
||
:features => @smart_proxy.features,
|
||
:features_started => @smart_proxy.features.count,
|
||
:names => logs.failed_module_names
|
||
}
|
||
rescue Foreman::Exception => exception
|
||
process_ajax_error exception
|
||
end
|
||
|
||
private
|
||
|
||
def find_status
|
||
... | ... | |
|
||
def action_permission
|
||
case params[:action]
|
||
when 'refresh'
|
||
when 'refresh', 'expire_logs'
|
||
:edit
|
||
when 'ping', 'tftp_server', 'puppet_environments', 'puppet_dashboard'
|
||
when 'ping', 'tftp_server', 'puppet_environments', 'puppet_dashboard', 'log_pane', 'failed_modules', 'errors_card', 'modules_card'
|
||
:view
|
||
else
|
||
super
|
app/helpers/application_helper.rb | ||
---|---|---|
end
|
||
end
|
||
|
||
# Display a link to JS function if user is authorized, otherwise a string
|
||
# +name+ : String to be displayed
|
||
# +options+ : Hash containing options for authorized_for and link_to
|
||
# +html_options+ : Hash containing html options for the link or span
|
||
def link_to_function_if_authorized(name, function, options = {}, html_options = {})
|
||
if authorized_for(options)
|
||
link_to_function name, function, html_options
|
||
else
|
||
link_to_function name, nil, html_options.merge!(:class => "#{html_options[:class]} disabled", :disabled => true)
|
||
end
|
||
end
|
||
|
||
def display_delete_if_authorized(options = {}, html_options = {})
|
||
text = options.delete(:text) || _("Delete")
|
||
options = {:auth_action => :destroy}.merge(options)
|
app/helpers/puppetca_helper.rb | ||
---|---|---|
select_tag "Filter", options_for_select([[_('valid or pending'), _('valid')+'|'+_('pending')]] +
|
||
STATES.map{|s| _(s)} +
|
||
[[_('all'),'']]),
|
||
:class => "puppetca-filters"
|
||
:class => "datatable-filter", :id => "puppetca-filter"
|
||
end
|
||
|
||
def time_column(time, opts = {})
|
app/helpers/smart_proxies_helper.rb | ||
---|---|---|
module SmartProxiesHelper
|
||
TABBED_FEATURES = ["Puppet","Puppet CA"]
|
||
TABBED_FEATURES = ["Puppet", "Puppet CA", "Logs"]
|
||
|
||
def proxy_actions(proxy, authorizer)
|
||
actions = []
|
||
... | ... | |
actions << display_link_if_authorized(_("Import subnets"), hash_for_import_subnets_path(:smart_proxy_id => proxy))
|
||
end
|
||
|
||
if proxy.has_feature?('Logs')
|
||
actions << link_to_function_if_authorized(_('Expire logs'), "expireLogs(this, (new Date).getTime() / 1000);",
|
||
hash_for_expire_logs_smart_proxy_path(:id => proxy), {
|
||
:data => {
|
||
:"url" => expire_logs_smart_proxy_path(:id => proxy),
|
||
:"url-errors" => errors_card_smart_proxy_path(:id => proxy),
|
||
:"url-modules" => modules_card_smart_proxy_path(:id => proxy)
|
||
}
|
||
})
|
||
end
|
||
|
||
actions << render_pagelets_for(:smart_proxy_title_actions, :subject => proxy)
|
||
|
||
actions
|
||
... | ... | |
end
|
||
|
||
def services_tab_features(proxy)
|
||
proxy.features.where('features.name NOT IN (?)', TABBED_FEATURES).uniq.pluck("name")
|
||
proxy.features.where('features.name NOT IN (?)', TABBED_FEATURES).uniq.pluck("name").sort
|
||
end
|
||
|
||
def tabbed_features(proxy)
|
||
proxy.features.where('features.name IN (?)', TABBED_FEATURES).uniq.pluck("name")
|
||
proxy.features.where('features.name IN (?)', TABBED_FEATURES).uniq.pluck("name").sort
|
||
end
|
||
|
||
def show_feature_version(feature)
|
||
render :partial => 'smart_proxies/plugins/plugin_version', :locals => { :feature => feature }
|
||
end
|
||
|
||
def logs_color_map
|
||
{
|
||
'DEBUG' => 'success',
|
||
'INFO' => 'info',
|
||
'WARN' => 'warning',
|
||
'ERROR' => 'danger',
|
||
'FATAL' => 'danger'
|
||
}
|
||
end
|
||
|
||
def logs_filter_tag
|
||
select_tag "Filter", options_for_select(
|
||
[[_('All'), '']] +
|
||
[[_('ERROR or FATAL'), 'ERROR|FATAL']] +
|
||
[[_('WARNING'), 'WARN']] +
|
||
[[_('INFO or DEBUG'), 'INFO|DEBUG']]), :class => "datatable-filter", :id => "logs-filter"
|
||
end
|
||
end
|
app/services/foreman/access_permissions.rb | ||
---|---|---|
permission_set.security_block :smart_proxies do |map|
|
||
map.permission :view_smart_proxies, {:smart_proxies => [:index, :ping, :auto_complete_search, :version,
|
||
:show, :plugin_version, :tftp_server, :puppet_environments,
|
||
:puppet_dashboard],
|
||
:puppet_dashboard, :log_pane, :failed_modules, :errors_card,
|
||
:modules_card],
|
||
:"api/v1/smart_proxies" => [:index, :show],
|
||
:"api/v2/smart_proxies" => [:index, :show, :version]
|
||
:"api/v2/smart_proxies" => [:index, :show, :version, :logs]
|
||
}
|
||
map.permission :create_smart_proxies, {:smart_proxies => [:new, :create],
|
||
:"api/v1/smart_proxies" => [:create],
|
||
:"api/v2/smart_proxies" => [:create]
|
||
}
|
||
map.permission :edit_smart_proxies, {:smart_proxies => [:edit, :update, :refresh],
|
||
map.permission :edit_smart_proxies, {:smart_proxies => [:edit, :update, :refresh, :expire_logs],
|
||
:"api/v1/smart_proxies" => [:update, :refresh],
|
||
:"api/v2/smart_proxies" => [:update, :refresh]
|
||
}
|
app/services/proxy_status.rb | ||
---|---|---|
require_dependency('proxy_status/tftp')
|
||
require_dependency('proxy_status/puppet')
|
||
require_dependency('proxy_status/puppetca')
|
||
require_dependency('proxy_status/logs')
|
app/services/proxy_status/logs.rb | ||
---|---|---|
module ProxyStatus
|
||
class Logs < Base
|
||
def logs
|
||
fetch_proxy_data do
|
||
::SmartProxies::LogBuffer.new(api.all(proxy.expired_logs.to_i || 0))
|
||
end
|
||
end
|
||
|
||
def self.humanized_name
|
||
'Logs'
|
||
end
|
||
|
||
protected
|
||
|
||
def api_class
|
||
ProxyAPI::Logs
|
||
end
|
||
end
|
||
end
|
||
ProxyStatus.status_registry.add(ProxyStatus::Logs)
|
app/services/smart_proxies/log_buffer.rb | ||
---|---|---|
class SmartProxies::LogBuffer
|
||
def initialize(hash)
|
||
@logs = hash
|
||
end
|
||
|
||
def log_entries
|
||
@logs['logs'] || []
|
||
end
|
||
|
||
def aggregated_logs
|
||
@aggregated ||= begin
|
||
aggregated = { 'debug' => 0, 'info' => 0, 'warn' => 0, 'error' => 0, 'fatal' => 0 }
|
||
log_entries.each do |log|
|
||
level = log['level'].try(:downcase)
|
||
if aggregated[level]
|
||
aggregated[level] += 1
|
||
else
|
||
aggregated[level] = 1
|
||
end
|
||
end
|
||
aggregated
|
||
end
|
||
end
|
||
|
||
def failed_modules
|
||
@logs['info'].try(:fetch, 'failed_modules') || {}
|
||
end
|
||
|
||
def failed_module_names
|
||
failed_modules.keys || []
|
||
end
|
||
|
||
def debug_messages
|
||
aggregated_logs['debug'] || 0
|
||
end
|
||
|
||
def info_messages
|
||
aggregated_logs['info'] || 0
|
||
end
|
||
|
||
def info_or_debug_messages
|
||
(info_messages + debug_messages) || 0
|
||
end
|
||
|
||
def warn_messages
|
||
aggregated_logs['warn'] || 0
|
||
end
|
||
|
||
def fatal_messages
|
||
aggregated_logs['fatal'] || 0
|
||
end
|
||
|
||
def error_messages
|
||
aggregated_logs['error'] || 0
|
||
end
|
||
|
||
def error_or_fatal_messages
|
||
(fatal_messages + error_messages) || 0
|
||
end
|
||
end
|
app/views/api/v2/smart_proxies/logs.json.rabl | ||
---|---|---|
object @smart_proxy
|
||
|
||
node :success do
|
||
locals[:success]
|
||
end
|
||
|
||
node :result do
|
||
locals[:result]
|
||
end
|
app/views/api/v2/smart_proxies/version.json.rabl | ||
---|---|---|
locals[:success]
|
||
end
|
||
|
||
node :message do
|
||
locals[:message]
|
||
node :result do
|
||
locals[:result]
|
||
end
|
app/views/common/_modal.html.erb | ||
---|---|---|
<div class="modal fade" id="<%= id %>" tabindex="-1" role="dialog" aria-labelledby="<%= id %>Label">
|
||
<div class="modal-dialog" role="document">
|
||
<div class="modal-dialog <%= defined?(big) ? 'modal-big' : '' %>" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
app/views/puppetca/_counts.html.erb | ||
---|---|---|
<div class="col-md-3">
|
||
<div class="card-pf card-pf-accented card-pf-aggregate-status card-pf-with-action">
|
||
<h2 class="card-pf-title">
|
||
<a href="#certificates" onclick="$('.puppetca-filters').val('').trigger('change')">
|
||
<a href="#certificates" onclick="$('#puppetca-filter').val('').trigger('change')">
|
||
<%= icon_text('lock', '', :kind => 'fa') %><span class="card-pf-aggregate-status-count"><%= @certificates.length.to_s %></span> <%= _("Certificates") %>
|
||
</a>
|
||
</h2>
|
||
... | ... | |
<p class="card-pf-aggregate-status-notifications">
|
||
<% PuppetcaHelper::STATES.each do |state|%>
|
||
<span class="card-pf-aggregate-status-notification">
|
||
<a href="#certificates" title="<%= _(state) %>" onclick="$('.puppetca-filters').val('<%= _(state) %>').trigger('change')"><span class="<%= PuppetcaHelper::CA_ICONS[state] %>"></span><%= @certificates.count{|c| c.state == state}.to_s %></a>
|
||
<a href="#certificates" title="<%= _(state) %>" onclick="$('#puppetca-filter').val('<%= _(state) %>').trigger('change')"><span class="<%= PuppetcaHelper::CA_ICONS[state] %>"></span><%= @certificates.count{|c| c.state == state}.to_s %></a>
|
||
</span>
|
||
<% end %>
|
||
</p>
|
app/views/smart_proxies/logs/_errors_card.html.erb | ||
---|---|---|
<div class="card-pf card-pf-accented card-pf-aggregate-status card-pf-with-action">
|
||
<h2 class="card-pf-title">
|
||
<a href="#logs" onclick="changeFilterSelection(0); setTab();" data-toggle="tooltip" data-placement="top"><%= n_('%s log message', '%s log messages', logs.info_or_debug_messages) % logs.info_or_debug_messages %></a>
|
||
</h2>
|
||
<div class="card-pf-body">
|
||
<p class="card-pf-aggregate-status-notifications">
|
||
<% icon_present = false; if logs.error_or_fatal_messages > 0; icon_present = true %>
|
||
<span class="card-pf-aggregate-status-notification"><a href="#logs" onclick="changeFilterSelection(1); setTab();" data-toggle="tooltip" data-placement="top" title="<%= n_('%s error message', '%s error messages', logs.error_or_fatal_messages) % logs.error_or_fatal_messages %>"><span class="pficon pficon-error-circle-o"></span><%= logs.error_or_fatal_messages %></a></span>
|
||
<% end %>
|
||
<% if logs.warn_messages > 0; icon_present = true %>
|
||
<span class="card-pf-aggregate-status-notification"><a href="#logs" onclick="changeFilterSelection(2); setTab();" data-toggle="tooltip" data-placement="top" title="<%= n_('%s warning message', '%s warning messages', logs.warn_messages) % logs.warn_messages %>"><span class="pficon pficon-warning-triangle-o"></span><%= logs.warn_messages %></a></span>
|
||
<% end %>
|
||
<% unless icon_present %>
|
||
<span class="card-pf-aggregate-status-notification"><span class="pficon pficon-ok"></span></span>
|
||
<% end %>
|
||
</p>
|
||
</div>
|
||
</div>
|
app/views/smart_proxies/logs/_failed_modules.html.erb | ||
---|---|---|
<% modules.each do |module_name, error_message| %>
|
||
<a href="#logs" onclick="filterLogsByMessage('<%= module_name %>'); setTab();" data-toggle="tooltip" data-placement="top" title="<%= error_message %>"><%= Feature.name_map[module_name] || module_name.humanize %></a>
|
||
<% end %>
|
app/views/smart_proxies/logs/_list.html.erb | ||
---|---|---|
<%= render :partial => "smart_proxies/logs/modal" %>
|
||
<div class="fr filter">
|
||
<%= _('Filter by level:') %> <%= logs_filter_tag %>
|
||
<%= link_to_function_if_authorized(_('Refresh'), "expireLogs(this, -1)", hash_for_expire_logs_smart_proxy_path(:id => @smart_proxy), {
|
||
:data => {
|
||
:"url" => expire_logs_smart_proxy_path(:id => @smart_proxy),
|
||
:"url-errors" => errors_card_smart_proxy_path(:id => @smart_proxy),
|
||
:"url-modules" => modules_card_smart_proxy_path(:id => @smart_proxy)
|
||
},
|
||
:class => "btn btn-default"}) %>
|
||
</div>
|
||
<table id="table-proxy-status-logs" class='<%= table_css_classes %> table-fixed'>
|
||
<thead>
|
||
<tr>
|
||
<th class="ca col-md-2 hidden-xs nbsp">Time</th>
|
||
<th class="ca col-md-1 hidden-sm hidden-xs nbsp">Level</th>
|
||
<th class="col-md-9 col-sm-10 col-xs-12 nbsp">Message</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% log_entries.each do |log| %>
|
||
<tr class="<%= logs_color_map[log['level']] || '' %>">
|
||
<td class="ca col-md-2 hidden-xs nbsp"><%= log['timestamp'] %></td>
|
||
<td class="ca col-md-1 hidden-sm hidden-xs nbsp">
|
||
<a href="#"
|
||
data-toggle="modal"
|
||
data-target="#logEntryModal"
|
||
data-time="<%= log['timestamp'] %>"
|
||
data-level="<%= log['level'] %>"
|
||
data-message="<%= log['message'] %>"
|
||
data-backtrace="<%= log['backtrace'] %>">
|
||
<%= log['level'] %></a>
|
||
</td>
|
||
<td class="ellipsis col-md-9 col-sm-10 col-xs-12"><%= log['message'] %><% if log['backtrace'] %> <span class="fa fa-puzzle-piece"/><% end %></td>
|
||
</tr>
|
||
<% end %>
|
||
</tbody>
|
||
</table>
|
app/views/smart_proxies/logs/_modal.html.erb | ||
---|---|---|
<%= render :layout => 'common/modal', :locals => {
|
||
:id => 'logEntryModal',
|
||
:title => _("Log entry details"),
|
||
:big => true,
|
||
:buttons => [ modal_close(_('Ok')) ]} do %>
|
||
<div class="container">
|
||
<div class="row">
|
||
<div class="col-md-2">
|
||
<%= label_tag(:'modal-bt-time', _("Local time"), :class => 'control-label nbsp') %>
|
||
</div>
|
||
<div class="col-md-10" class="" id="modal-bt-time">
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-2">
|
||
<%= label_tag(:'modal-bt-timegmt', _("GMT time"), :class => 'control-label nbsp') %>
|
||
</div>
|
||
<div class="col-md-10" class="" id="modal-bt-timegmt">
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-2">
|
||
<%= label_tag(:'modal-bt-level', _("Level"), :class => 'control-label nbsp') %>
|
||
</div>
|
||
<div class="col-md-10" class="" id="modal-bt-level">
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-12">
|
||
<%= label_tag(:'modal-bt-message', _("Message"), :class => 'control-label nbsp') %><br/>
|
||
<div class="code pre-scrollable-xy-modal bg-warning" id="modal-bt-message">N/A</div>
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-12">
|
||
<%= label_tag(:'modal-bt-backtrace', _("Backtrace"), :class => 'control-label nbsp') %><br/>
|
||
<div class="code pre-scrollable-xy-modal bg-warning" id="modal-bt-backtrace">N/A</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
app/views/smart_proxies/logs/_modules_card.html.erb | ||
---|---|---|
<div class="card-pf card-pf-accented card-pf-aggregate-status card-pf-with-action">
|
||
<h2 class="card-pf-title" data-toggle="tooltip" data-placement="top" title="<%= features.join(', ') %>">
|
||
<%= n_('%s active feature', '%s active features', features_started) % features_started %>
|
||
</h2>
|
||
<div class="card-pf-body">
|
||
<p class="card-pf-aggregate-status-notifications">
|
||
<% if names.count > 0 %>
|
||
<span class="card-pf-aggregate-status-notification"><a href="#logs" onclick="filterLogsByMessage('<%= names.join('|') %>'); setTab();" data-toggle="tooltip" data-placement="top" title="<%= n_('Failed features: %s', 'Failed features: %s', names.count) % names.join(', ') %>"><span class="pficon pficon-warning-triangle-o"></span><%= names.count %></a></span>
|
||
<% else %>
|
||
<span class="card-pf-aggregate-status-notification"><span class="pficon pficon-ok"></span></span>
|
||
<% end %>
|
||
</p>
|
||
</div>
|
||
</div>
|
app/views/smart_proxies/plugins/_logs.html.erb | ||
---|---|---|
<div class="tab-pane" id="logs" data-ajax-url="<%= log_pane_smart_proxy_path(:smart_proxy_id => @smart_proxy) %>"
|
||
data-on-complete="activateLogsDataTable"><%= spinner %>
|
||
</div>
|
app/views/smart_proxies/show.html.erb | ||
---|---|---|
</div>
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<%= _('Features') %>
|
||
<%= _('Active Features') %>
|
||
</div>
|
||
<div class="col-md-8">
|
||
<%= @smart_proxy.features.to_sentence %> <%= refresh_proxy_icon(@smart_proxy, authorizer) %>
|
||
</div>
|
||
</div>
|
||
<% if @smart_proxy.has_feature?('Logs') %>
|
||
<div class="row">
|
||
<div class="col-md-4">
|
||
<%= _('Failed Features') %>
|
||
</div>
|
||
<div class="col-md-8">
|
||
<div class="tab-pane" id="failed-modules"
|
||
data-ajax-url="<%= failed_modules_smart_proxy_path(:smart_proxy_id => @smart_proxy) %>"><%= spinner %></div>
|
||
</div>
|
||
</div>
|
||
<div class="container-fluid container-cards-pf">
|
||
<div class="row row-cards-pf">
|
||
<div id="ajax-errors-card" class="col-md-6" data-ajax-url="<%= errors_card_smart_proxy_path(:smart_proxy_id => @smart_proxy) %>"><%= spinner %></div>
|
||
<div id="ajax-modules-card" class="col-md-6" data-ajax-url="<%= modules_card_smart_proxy_path(:smart_proxy_id => @smart_proxy) %>"><%= spinner %></div>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
<%= render_pagelets_for(:overview_content, :subject => @smart_proxy) %>
|
||
</div>
|
||
<div class="col-md-6">
|
config/routes.rb | ||
---|---|---|
get 'tftp_server'
|
||
get 'puppet_environments'
|
||
get 'puppet_dashboard'
|
||
get 'log_pane'
|
||
get 'failed_modules'
|
||
get 'errors_card'
|
||
get 'modules_card'
|
||
post 'expire_logs'
|
||
end
|
||
constraints(:id => /[^\/]+/) do
|
||
resources :puppetca, :only => [:index, :update, :destroy] do
|
config/routes/api/v2.rb | ||
---|---|---|
(resources :organizations, :only => [:index, :show]) if SETTINGS[:organizations_enabled]
|
||
put :refresh, :on => :member
|
||
get :version, :on => :member
|
||
get :logs, :on => :member
|
||
post :import_puppetclasses, :on => :member
|
||
resources :environments, :only => [] do
|
||
post :import_puppetclasses, :on => :member
|
db/migrate/20160201131211_add_expired_logs_to_smart_proxy.rb | ||
---|---|---|
class AddExpiredLogsToSmartProxy < ActiveRecord::Migration
|
||
def change
|
||
add_column :smart_proxies, :expired_logs, :string, :default => '0'
|
||
end
|
||
end
|
db/seeds.d/11-smart_proxy_features.rb | ||
---|---|---|
# Proxy features
|
||
[ "TFTP", "DNS", "DHCP", "Puppet", "Puppet CA", "BMC", "Realm", "Facts" ].each do |input|
|
||
[ "TFTP", "DNS", "DHCP", "Puppet", "Puppet CA", "BMC", "Realm", "Facts", "Logs" ].each do |input|
|
||
f = Feature.where(:name => input).first_or_create
|
||
raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
|
||
end
|
lib/proxy_api/logs.rb | ||
---|---|---|
module ProxyAPI
|
||
class Logs < ProxyAPI::Resource
|
||
def initialize(args)
|
||
@url = args[:url] + "/logs"
|
||
super args
|
||
end
|
||
|
||
def all(from = 0)
|
||
parse(get("?from_timestamp=#{from}"))
|
||
rescue => e
|
||
raise ProxyException.new(url, e, N_("Unable to fetch logs"))
|
||
end
|
||
end
|
||
end
|
test/fixtures/features.yml | ||
---|---|---|
|
||
realm:
|
||
name: Realm
|
||
|
||
logs:
|
||
name: Logs
|
test/fixtures/smart_proxies.yml | ||
---|---|---|
name: BMC proxy
|
||
url: http://else.where:45673
|
||
features: bmc
|
||
|
||
logs:
|
||
name: A proxy with Logs feature
|
||
url: http://else.where:45674
|
||
features:
|
||
- dhcp
|
||
- dns
|
||
- puppet
|
||
- logs
|
test/functional/api/v2/smart_proxies_controller_test.rb | ||
---|---|---|
end
|
||
|
||
test "smart proxy version succeeded" do
|
||
SmartProxy.any_instance.stubs(:version).returns({:success => true, :message => '1.11'})
|
||
ProxyStatus::Version.any_instance.stubs(:version).returns({"version" => "1.11", "modules" => {}})
|
||
get :version, { :id => smart_proxies(:one).to_param }, set_session_user
|
||
assert_response :success
|
||
show_response = ActiveSupport::JSON.decode(@response.body)
|
||
assert_equal('1.11', show_response['message'])
|
||
assert_equal('1.11', show_response['result']['version'])
|
||
end
|
||
|
||
test "smart proxy version failed" do
|
||
SmartProxy.any_instance.stubs(:version).raises(Foreman::Exception, 'Exception message')
|
||
ProxyStatus::Version.any_instance.stubs(:version).raises(Foreman::Exception, 'Exception message')
|
||
get :version, { :id => smart_proxies(:one).to_param }, set_session_user
|
||
assert_response :unprocessable_entity
|
||
show_response = ActiveSupport::JSON.decode(@response.body)
|
||
assert_match(/Exception message/, show_response['error']['message'])
|
||
end
|
||
|
||
test "smart proxy logs succeeded" do
|
||
ProxyStatus::Logs.any_instance.stubs(:logs).returns({"info" => {"size" => 1000, "tail_size" => 500, }, "logs" => [] })
|
||
get :logs, { :id => smart_proxies(:logs).to_param }, set_session_user
|
||
assert_response :success
|
||
show_response = ActiveSupport::JSON.decode(@response.body)
|
||
assert_match(/Exception message/, show_response['message'])
|
||
assert_equal(1000, show_response['result']['info']['size'])
|
||
end
|
||
|
||
test "smart proxy logs failed" do
|
||
ProxyStatus::Logs.any_instance.stubs(:logs).raises(Foreman::Exception, 'Exception message')
|
||
get :logs, { :id => smart_proxies(:logs).to_param }, set_session_user
|
||
assert_response :unprocessable_entity
|
||
show_response = ActiveSupport::JSON.decode(@response.body)
|
||
assert_match(/Exception message/, show_response['error']['message'])
|
||
end
|
||
end
|
test/functional/smart_proxies_controller_test.rb | ||
---|---|---|
assert @response.body.include?('special_environment')
|
||
assert @response.body.include?('5') #the total is correct
|
||
end
|
||
|
||
test '#log_pane' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
'logs' => [{
|
||
"timestamp" => 1453890750.9860077,
|
||
"level" => "DEBUG",
|
||
"message" => "A debug message"
|
||
}]})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :log_pane, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_list'
|
||
assert @response.body.include?('debug message')
|
||
end
|
||
|
||
test '#expire_logs' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
'logs' => [{
|
||
"timestamp" => 1453890750.9860077,
|
||
"level" => "DEBUG",
|
||
"message" => "A debug message"
|
||
}]})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
SmartProxy.any_instance.expects(:expired_logs=).with('42').returns('42')
|
||
xhr :get, :expire_logs, { :id => proxy.id, :from => 42 }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_list'
|
||
assert @response.body.include?('debug message')
|
||
end
|
||
|
||
test '#failed_modules' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
'info' => {
|
||
"failed_modules" => {
|
||
"BMC" => "Initialization error"
|
||
}}})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :failed_modules, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_failed_modules'
|
||
assert @response.body.include?('BMC')
|
||
end
|
||
|
||
test '#errors_card' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
"info" => {
|
||
"failed_modules" => {}
|
||
},
|
||
"logs" => [
|
||
{ "timestamp" => 1000, "level" => "INFO", "message" => "Message" },
|
||
{ "timestamp" => 1001, "level" => "INFO", "message" => "Message" },
|
||
{ "timestamp" => 1002, "level" => "ERROR", "message" => "Message" },
|
||
{ "timestamp" => 1003, "level" => "FATAL", "message" => "Message" },
|
||
]})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :errors_card, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_errors_card'
|
||
assert @response.body.include?('2 log messages')
|
||
assert @response.body.include?('2 error messages')
|
||
end
|
||
|
||
test '#errors_card_empty' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
"info" => {
|
||
"failed_modules" => {}
|
||
},
|
||
"logs" => []})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :errors_card, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_errors_card'
|
||
assert @response.body.include?('pficon-ok')
|
||
assert @response.body.include?('0 log messages')
|
||
refute @response.body.include?('warning message')
|
||
refute @response.body.include?('error message')
|
||
end
|
||
|
||
test '#modules_card' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
"info" => {
|
||
"failed_modules" => {
|
||
"BMC" => "Message",
|
||
"Puppet" => "Another message",
|
||
}
|
||
},
|
||
"logs" => []})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :modules_card, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_modules_card'
|
||
assert @response.body.include?('4 active features')
|
||
assert @response.body.include?('Failed features: BMC, Puppet')
|
||
end
|
||
|
||
test '#modules_card_empty' do
|
||
proxy = smart_proxies(:logs)
|
||
fake_data = ::SmartProxies::LogBuffer.new(
|
||
{
|
||
"info" => {
|
||
"failed_modules" => {}
|
||
},
|
||
"logs" => []})
|
||
ProxyStatus::Logs.any_instance.expects(:logs).returns(fake_data)
|
||
xhr :get, :modules_card, { :id => proxy.id }, set_session_user
|
||
assert_response :success
|
||
assert_template 'smart_proxies/logs/_modules_card'
|
||
assert @response.body.include?('pficon-ok')
|
||
refute @response.body.include?('BMC')
|
||
end
|
||
end
|
test/unit/proxy_statuses/proxy_status_logs_test.rb | ||
---|---|---|
require 'test_helper'
|
||
|
||
class ProxyStatusLogsTest < ActiveSupport::TestCase
|
||
setup do
|
||
@empty_buffer = {
|
||
"info" => {
|
||
"failed_modules" => {}
|
||
},
|
||
"logs" => []}
|
||
|
||
@four_entries_buffer = {
|
||
"info" => {
|
||
"failed_modules" => {}
|
||
},
|
||
"logs" => [
|
||
{ "timestamp" => 1000, "level" => "INFO", "message" => "Message" },
|
||
{ "timestamp" => 1001, "level" => "INFO", "message" => "Message" },
|
||
{ "timestamp" => 1002, "level" => "ERROR", "message" => "Message" },
|
||
{ "timestamp" => 1003, "level" => "FATAL", "message" => "Message" },
|
||
]}
|
||
|
||
@two_failed_buffer = {
|
||
"info" => {
|
||
"failed_modules" => {
|
||
"BMC" => "Message",
|
||
"Puppet" => "Another message",
|
||
}
|
||
},
|
||
"logs" => []}
|
||
|
||
@proxy = FactoryGirl.build_stubbed(:smart_proxy, :url => 'https://secure.proxy:4568')
|
||
@status = ProxyStatus::Logs.new(@proxy, :cache => false)
|
||
end
|
||
|
||
test 'it returns an empty buffer' do
|
||
ProxyAPI::Logs.any_instance.expects(:all).returns(@empty_buffer)
|
||
assert_equal([], @status.logs.log_entries)
|
||
end
|
||
|
||
test 'it aggregates data' do
|
||
ProxyAPI::Logs.any_instance.expects(:all).returns(@four_entries_buffer)
|
||
aggregates = @status.logs.aggregated_logs
|
||
assert_equal(2, aggregates['info'])
|
||
assert_equal(1, aggregates['error'])
|
||
assert_equal(1, aggregates['fatal'])
|
||
assert_equal(0, aggregates['debug'])
|
||
end
|
||
|
||
test 'it aggregates data' do
|
||
ProxyAPI::Logs.any_instance.expects(:all).returns(@four_entries_buffer)
|
||
logs = @status.logs
|
||
assert_equal(2, logs.info_messages)
|
||
assert_equal(2, logs.error_or_fatal_messages)
|
||
assert_equal(0, logs.warn_messages)
|
||
assert_equal(1, logs.fatal_messages)
|
||
end
|
||
|
||
test 'it returns failed module names' do
|
||
ProxyAPI::Logs.any_instance.expects(:all).returns(@two_failed_buffer)
|
||
assert_equal(["BMC", "Puppet"], @status.logs.failed_module_names)
|
||
end
|
||
|
||
test 'it returns failed modules' do
|
||
ProxyAPI::Logs.any_instance.expects(:all).returns(@two_failed_buffer)
|
||
modules = @status.logs.failed_modules
|
||
assert_equal("Message", modules['BMC'])
|
||
assert_equal("Another message", modules['Puppet'])
|
||
assert_equal(2, modules.count)
|
||
end
|
||
end
|
Also available in: Unified diff
Fixes #12718 - smart proxy log table
This patch adds a table with logs fetched from Smart Proxy /logs new API. It
returns latest N log entries from a simple memory buffer. The table supports
filtering by level, custom filtering and it introduces several overview
status cards with error/warning counts, failed modules list with latest
known error messages.