Project

General

Profile

« Previous | Next » 

Revision 33d9f9ee

Added by Lukas Zapletal over 8 years ago

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.

View differences:

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">&times;</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'] %>&nbsp;<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