From 8906ed0b5e64f735ea7717f9b0710e0adc106a73 Mon Sep 17 00:00:00 2001 From: Gary Paige Date: Tue, 14 Oct 2014 12:53:03 -0500 Subject: [PATCH] added cadvisor meter for host and services --- CHANGELOG.md | 8 ++ app/assets/javascripts/init.js | 4 +- .../jquery.docker_run/jquery.docker_run.js | 2 +- .../jquery.health/jquery.health_graph.js | 66 ++++++++++ .../jquery.health/jquery.host_health.js | 43 +++++++ .../jquery.health/jquery.service_health.js | 68 ++++++++++ app/assets/javascripts/jquery.host_health.js | 109 ---------------- app/assets/stylesheets/panamax.css.scss | 72 +++++------ .../stylesheets/panamax/service_details.scss | 23 +++- app/controllers/host_health_controller.rb | 4 +- app/controllers/service_health_controller.rb | 8 ++ app/helpers/application_helper.rb | 8 ++ app/models/concerns/cadvisor_measurable.rb | 60 +++++++++ app/models/host_health.rb | 35 +----- app/models/machine_info.rb | 11 ++ app/models/service_health.rb | 12 ++ app/views/layouts/application.html.haml | 10 +- app/views/services/show.html.haml | 40 +++--- app/views/shared/_host_health.html.haml | 14 --- app/views/shared/_metrics.html.haml | 9 ++ config/routes.rb | 2 + spec/concerns/cadvisor_measurable_spec.rb | 74 +++++++++++ .../service_health_controller_spec.rb | 16 +++ spec/helpers/application_helper_spec.rb | 20 +++ .../fixtures/host_health.html.haml | 30 ++--- .../jquery.health/jquery.health_graph_spec.js | 64 ++++++++++ .../jquery.health/jquery.host_health_spec.js | 49 ++++++++ .../jquery.service_health_spec.js | 65 ++++++++++ spec/javascripts/jquery.host_health_spec.js | 119 ------------------ spec/models/host_health_spec.rb | 67 +--------- spec/models/machine_info_spec.rb | 12 ++ spec/models/service_health_spec.rb | 5 + 32 files changed, 697 insertions(+), 432 deletions(-) create mode 100644 app/assets/javascripts/jquery.health/jquery.health_graph.js create mode 100644 app/assets/javascripts/jquery.health/jquery.host_health.js create mode 100644 app/assets/javascripts/jquery.health/jquery.service_health.js delete mode 100644 app/assets/javascripts/jquery.host_health.js create mode 100644 app/controllers/service_health_controller.rb create mode 100644 app/models/concerns/cadvisor_measurable.rb create mode 100644 app/models/machine_info.rb create mode 100644 app/models/service_health.rb delete mode 100644 app/views/shared/_host_health.html.haml create mode 100644 app/views/shared/_metrics.html.haml create mode 100644 spec/concerns/cadvisor_measurable_spec.rb create mode 100644 spec/controllers/service_health_controller_spec.rb create mode 100644 spec/helpers/application_helper_spec.rb create mode 100644 spec/javascripts/jquery.health/jquery.health_graph_spec.js create mode 100644 spec/javascripts/jquery.health/jquery.host_health_spec.js create mode 100644 spec/javascripts/jquery.health/jquery.service_health_spec.js delete mode 100644 spec/javascripts/jquery.host_health_spec.js create mode 100644 spec/models/machine_info_spec.rb create mode 100644 spec/models/service_health_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d7071b40..581000d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog All notable changes to this project will be documented in this file. +LATEST +------------------ + +### Added +- Updated cAdvisor meter for host and services + + + 0.2.4 - 2014-10-10 ------------------ diff --git a/app/assets/javascripts/init.js b/app/assets/javascripts/init.js index f90b5926..91992b24 100644 --- a/app/assets/javascripts/init.js +++ b/app/assets/javascripts/init.js @@ -37,7 +37,9 @@ $('.service-edit-form').progressiveForm(); - $('aside.host').hostHealth(); + $('aside.metrics').hostHealth(); + + $('.service-details .state span.metrics').serviceHealth(); $('.service-help-icon').contextHelp(); diff --git a/app/assets/javascripts/jquery.docker_run/jquery.docker_run.js b/app/assets/javascripts/jquery.docker_run/jquery.docker_run.js index 79a9fac4..4649ef44 100644 --- a/app/assets/javascripts/jquery.docker_run/jquery.docker_run.js +++ b/app/assets/javascripts/jquery.docker_run/jquery.docker_run.js @@ -141,7 +141,7 @@ var $containerPath = $(element).find(base.options.volumeContainerPathSelector); if ($containerPath.length == 1) { volumes.push({ - hostPath: base.extractText($hostPath), + hostPath: ($hostPath.length == 1) ? base.extractText($hostPath) : '', containerPath: base.extractText($containerPath) }); } diff --git a/app/assets/javascripts/jquery.health/jquery.health_graph.js b/app/assets/javascripts/jquery.health/jquery.health_graph.js new file mode 100644 index 00000000..409209b5 --- /dev/null +++ b/app/assets/javascripts/jquery.health/jquery.health_graph.js @@ -0,0 +1,66 @@ +(function($) { + + + $.PMX.HealthGraph = function($el, options) { + var base = this; + + base.$el = $el; + + base.defaultOptions = { + cpuSelector: '.health .cpu .bar', + memSelector: '.health .mem .bar', + maxWidth: 60 + }; + + base.init = function() { + base.options = $.extend({}, base.defaultOptions, options); + }; + + base.calculateHealth = function(data) { + var datum = [ + { + 'element': base.$el.find(base.options.cpuSelector), + 'percent': data.overall_cpu.percent + }, + { + 'element': base.$el.find(base.options.memSelector), + 'percent': data.overall_mem.percent + } + ]; + + datum.forEach(function(item) { + base.healthColor(item.element, item.percent); + base.datumSize(item.element, item.percent); + }); + }; + + base.datumSize = function($el, percent) { + $el.animate({ + 'width': Math.ceil((percent / 100) * base.options.maxWidth) + 'px' + }, 600); + }; + + base.healthColor = function($el, percent) { + $el.removeClass('good warning danger').addClass(base.colorLevel(percent)); + }; + + base.colorLevel = function(percentage) { + if (percentage >= 90) { + return 'danger'; + } + + if (percentage >= 75) { + return 'warning'; + } + + return 'good'; + }; + }; + + $.fn.healthGraph = function(options){ + return this.each(function(){ + (new $.PMX.HealthGraph($(this), options)).init(); + }); + }; + +})(jQuery); diff --git a/app/assets/javascripts/jquery.health/jquery.host_health.js b/app/assets/javascripts/jquery.health/jquery.host_health.js new file mode 100644 index 00000000..6ec82fbe --- /dev/null +++ b/app/assets/javascripts/jquery.health/jquery.host_health.js @@ -0,0 +1,43 @@ +(function($) { + $.PMX.HostHealth = function($el, options) { + var base = this; + + base.$el = $el; + + base.defaultOptions = { + baseUrl: '/host_health', + interval: 6 * 1000 + }; + + base.init = function() { + base.options = $.extend({}, base.defaultOptions, options); + base.buildGraph(); + base.initiateRequest(); + }; + + base.buildGraph = function() { + base.graph = new $.PMX.HealthGraph(base.$el, base.options); + base.graph.init(); + }; + + base.initiateRequest = function() { + $.ajax({ + url: base.options.baseUrl, + headers: { + 'Accept': 'application/json' + } + }).success(function(response) { + base.graph.calculateHealth(response); + }).always(function() { + if (base.timer !== null) { clearTimeout(base.timer); } + base.timer = setTimeout(base.initiateRequest, base.options.interval); + }); + }; + }; + + $.fn.hostHealth = function(options){ + return this.each(function(){ + (new $.PMX.HostHealth($(this), options)).init(); + }); + }; +})(jQuery); diff --git a/app/assets/javascripts/jquery.health/jquery.service_health.js b/app/assets/javascripts/jquery.health/jquery.service_health.js new file mode 100644 index 00000000..29f2435d --- /dev/null +++ b/app/assets/javascripts/jquery.health/jquery.service_health.js @@ -0,0 +1,68 @@ +(function($) { + $.PMX.ServiceHealth = function($el, options) { + var base = this; + + base.$el = $el; + + base.defaultOptions = { + baseUrl: '/service_health', + serviceData: 'data-service-name', + $dockerStatus: $('.service-details .service-status'), + $metricLink: $('.service-details .metric-details') + }; + + base.init = function() { + base.options = $.extend({}, base.defaultOptions, options); + base.hideHealth(); + base.buildGraph(); + base.bindEvents(); + }; + + base.buildGraph = function() { + base.graph = new $.PMX.HealthGraph(base.$el, base.options); + base.graph.init(); + }; + + base.bindEvents = function () { + base.options.$dockerStatus.on('update-service-status', base.checkStatusHandler); + }; + + base.showHealth = function() { + base.options.$metricLink.show(); + base.$el.show(); + base.initiateRequest(); + }; + + base.hideHealth = function() { + base.options.$metricLink.hide(); + base.$el.hide(); + }; + + base.checkStatusHandler = function(_, response) { + (response.sub_state === 'running') ? base.showHealth() : base.hideHealth(); + }; + + base.healthUrl = function() { + var serviceName = base.$el.attr(base.options.serviceData); + + return base.options.baseUrl + '/' + serviceName; + }; + + base.initiateRequest = function() { + $.ajax({ + url: base.healthUrl(), + headers: { + 'Accept': 'application/json' + } + }).success(function(response) { + base.graph.calculateHealth(response); + }); + }; + }; + + $.fn.serviceHealth = function(options){ + return this.each(function(){ + (new $.PMX.ServiceHealth($(this), options)).init(); + }); + }; +})(jQuery); diff --git a/app/assets/javascripts/jquery.host_health.js b/app/assets/javascripts/jquery.host_health.js deleted file mode 100644 index c1e8b712..00000000 --- a/app/assets/javascripts/jquery.host_health.js +++ /dev/null @@ -1,109 +0,0 @@ -(function($){ - $.PMX.HostHealth = function(el, options) { - var base = this; - - base.timer = null; - - base.$el = $(el); - base.data = { - cpu: 0, - memory: 0, - time_stamp: 'NA', - cpu_percent: 0, - mem_percent: 0 - }; - - base.defaultOptions = { - timeFormat: 'YYYY/MM/DD, hh:mm:ss', - interval: 10 * 1000, - healthSelector: 'aside.host > .health', - cpuSelector: '.cpu .health', - memSelector: '.memory .health', - detailTemplate: Handlebars.compile($('#host_health_template').html()) - }; - - base.init = function() { - base.options = $.extend({}, base.defaultOptions, options); - base.bindEvents(); - base.initiateRequest(); - }; - - base.bindEvents = function() { - base.$el.on('mouseenter', base.showHealthDetails); - base.$el.on('mouseleave', base.hideHealthDetails); - }; - - base.showHealthDetails = function() { - var details = $(base.options.detailTemplate(base.data)); - - base.$el.addClass('showing'); - $(base.options.healthSelector).append(details); - base.healthColors(); - }; - - base.hideHealthDetails = function() { - base.$el.removeClass('showing'); - $(base.options.healthSelector).empty(); - }; - - base.initiateRequest = function() { - var oneMegabyte = 1024 * 1024; - - $.ajax({ - url: '/host_health', - headers: { - 'Accept': 'application/json' - } - }).success(function(response){ - base.data.cpu = response.overall_cpu.usage; - base.data.memory = (response.overall_mem.usage / oneMegabyte).toFixed(2) + ' MB'; - base.data.time_stamp = moment(response.timestamp).format(base.options.timeFormat); - base.calculateHealth(response); - }).always(function() { - if (base.timer !== null) { clearTimeout(base.timer); } - base.timer = setTimeout(base.initiateRequest, base.options.interval); - }); - }; - - base.calculateHealth = function(data) { - // Convert to millicores and take the percentage - var cpuPercentage = data.overall_cpu.percent, - memoryPercentage = data.overall_mem.percent; - - base.data.cpu_percent = cpuPercentage; - base.data.mem_percent = memoryPercentage; - if (base.$el.hasClass('showing')) { - $(base.options.healthSelector).empty().append($(base.options.detailTemplate(base.data))); - } - base.healthColors(); - }; - - base.healthColors = function() { - var cpuPercentage = base.data.cpu_percent, - memoryPercentage = base.data.mem_percent; - - // overall health is max of cpu and memory - $(base.options.healthSelector).removeClass('good warning danger').addClass(base.colorLevel(Math.max(cpuPercentage, memoryPercentage))); - base.$el.find(base.options.cpuSelector).removeClass('good warning danger').addClass(base.colorLevel(cpuPercentage)); - base.$el.find(base.options.memSelector).removeClass('good warning danger').addClass(base.colorLevel(memoryPercentage)); - }; - - base.colorLevel = function(percentage) { - if (percentage >= 90) { - return 'danger'; - } - - if (percentage >= 80) { - return 'warning'; - } - - return 'good'; - }; - }; - - $.fn.hostHealth = function(options){ - return this.each(function(){ - (new $.PMX.HostHealth(this, options)).init(); - }); - }; -})(jQuery); diff --git a/app/assets/stylesheets/panamax.css.scss b/app/assets/stylesheets/panamax.css.scss index 98530192..ad1a18db 100644 --- a/app/assets/stylesheets/panamax.css.scss +++ b/app/assets/stylesheets/panamax.css.scss @@ -102,14 +102,10 @@ cite.built-by { } } -aside.host { +.metrics { float: left; margin-left: 30px; - &:hover { - cursor: pointer; - } - span { float: left; height: 20px; @@ -117,59 +113,49 @@ aside.host { margin-right: 8px; color: $dark_grey; font-weight: 400; + margin-top: 8px; } .health { margin-top: 5px; position: relative; float: left; - width: 12px; + width: auto; height: 12px; - border-radius: 4px; - background: darken($ctl_light_green, 10%); - section.details { - padding: 10px; - width: 220px; - top: 24px; - left: -30px; + section { + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 11px; + margin-bottom: 2px; + clear: both; - &:before { - left: 30px; + .label { + float: left; + padding: 0 5px 3px 0; } - } - - &.danger { - background-color: $dark_red; - } - &.warning { - background: #F5D04C; - } + .bar { + float: left; + height: 8px; + width: 0; + transition: background-color 0.6s; + -o-transition: background-color 0.6s; + -moz-transition: background-color 0.6s; + -webkit-transition: background-color 0.6s; + + &.danger { + background-color: $dark_red; + } - &.good { - background: darken($ctl_light_green, 10%); - } + &.warning { + background: #F5D04C; + } - li { - clear:both; - height: 25px; - line-height: 25px; - - span { - line-height: inherit; - margin: -1px 0 0 4px; - padding-right: 2px; - font-weight: bold; + &.good { + background: darken($ctl_light_green, 10%); + } } } - - footer { - margin-top: 15px; - font-style: italic; - font-size: 0.9em; - background-color: $yellow; - } } } diff --git a/app/assets/stylesheets/panamax/service_details.scss b/app/assets/stylesheets/panamax/service_details.scss index 17d7f742..55d5679e 100644 --- a/app/assets/stylesheets/panamax/service_details.scss +++ b/app/assets/stylesheets/panamax/service_details.scss @@ -6,6 +6,25 @@ .service-details { padding-bottom: 20px; + .metrics { + float: right; + padding-left: 0; + padding-right: 15px; + } + + .state { + width: 370px; + float: right; + + .metric-details { + float: right; + font-size: 13px; + margin-right: 15px; + padding: 4px 15px 0 0; + border-right: 1px solid $grey; + } + } + a.button-add { margin: 10px 0; padding-left: 40px; @@ -69,7 +88,6 @@ background-repeat: no-repeat; background-position: left center; position: relative; - z-index: 10; .panamax-state { cursor: pointer; @@ -160,7 +178,7 @@ .base-image-detail { position: relative; margin-bottom: 20px; - width: 80%; + width: 70%; * { display: inline-block; @@ -731,4 +749,5 @@ input.empty { #docker-inspect { color: $blue_grey; font-size: 0.9em; + display: none; } diff --git a/app/controllers/host_health_controller.rb b/app/controllers/host_health_controller.rb index 2a3e9027..81761bfd 100644 --- a/app/controllers/host_health_controller.rb +++ b/app/controllers/host_health_controller.rb @@ -2,7 +2,7 @@ class HostHealthController < ApplicationController respond_to :json def index - @health = HostHealth.find - respond_with @health.overall + health = HostHealth.find + respond_with health.overall end end diff --git a/app/controllers/service_health_controller.rb b/app/controllers/service_health_controller.rb new file mode 100644 index 00000000..c76dbb7c --- /dev/null +++ b/app/controllers/service_health_controller.rb @@ -0,0 +1,8 @@ +class ServiceHealthController < ApplicationController + respond_to :json + + def show + health = ServiceHealth.find(params[:id]) + respond_with health.overall + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 28002bb0..91aab643 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,4 +2,12 @@ module ApplicationHelper def icon_source_for(type) ActionController::Base.helpers.asset_path("type_icons/#{type.downcase.gsub(/\s/, '_')}.svg") end + + def metrics_url + HostHealth.site + end + + def metrics_url_for(name) + "#{HostHealth.site}containers/docker/#{name}" + end end diff --git a/app/models/concerns/cadvisor_measurable.rb b/app/models/concerns/cadvisor_measurable.rb new file mode 100644 index 00000000..849eca89 --- /dev/null +++ b/app/models/concerns/cadvisor_measurable.rb @@ -0,0 +1,60 @@ +require 'date' + +module CadvisorMeasurable + extend ActiveSupport::Concern + + def overall + self.attributes[:stats].last.tap do |results| + results.attributes[:overall_mem] = overall_memory(self.attributes[:stats], self.attributes[:spec]) + results.attributes[:overall_cpu] = overall_cpu(self.attributes[:stats], self.attributes[:spec]) + end + end + + private + + def overall_cpu(stats, spec) + machine_info = MachineInfo.find + cur = stats[stats.count -1] + percent = 0 + + if (spec.attributes[:has_cpu] && stats.count >=2) + prev = stats[stats.count - 2] + raw = (cur.attributes[:cpu].attributes[:usage].attributes[:total] - prev.attributes[:cpu].attributes[:usage].attributes[:total]).abs + interval = nanosecond_interval(cur.attributes[:timestamp], prev.attributes[:timestamp]) + cores = machine_info.attributes[:num_cores] || 1 + + percent = (((raw / interval) / cores) * 100).round + percent = 100 if percent > 100 + end + { + 'usage' => raw || 0, + 'percent' => percent || 0 + } + + end + + def overall_memory(stats, spec) + machine_info = MachineInfo.find + cur = stats[stats.count -1] + + if (spec.attributes[:has_memory]) + limit = spec.attributes[:memory].attributes[:limit] + if (limit > machine_info.attributes[:memory_capacity]) + limit = machine_info.attributes[:memory_capacity] + end + percent = ((cur.attributes[:memory].attributes[:usage] / limit.to_f) * 100).round + end + { + 'usage' => cur.attributes[:memory].attributes[:usage] || 0, + 'percent' => percent || 0, + } + end + + def nanosecond_interval(current, previous) + cur = DateTime.iso8601(current) + prev = DateTime.iso8601(previous) + + [1, ((cur.to_f - prev.to_f) * 1000000000.0).abs].max + end + +end diff --git a/app/models/host_health.rb b/app/models/host_health.rb index 4d55c99b..54a620e4 100644 --- a/app/models/host_health.rb +++ b/app/models/host_health.rb @@ -2,45 +2,12 @@ class HostHealth < BaseResource include ActiveResource::Singleton + include CadvisorMeasurable self.site = "#{ENV['CADVISOR_PORT']}" - def self.singleton_path(_prefix_options={}, _query_options=nil) '/api/v1.0/containers/' end - def overall - self.attributes[:stats].last.tap do |results| - results.attributes[:overall_mem] = overall_memory(self.attributes[:stats], self.attributes[:spec]) - results.attributes[:overall_cpu] = overall_cpu(self.attributes[:stats]) - end - end - - private - def overall_cpu(stats) - if stats.count > 1 - sample = stats.pop(2) - raw = sample[1].attributes[:cpu].attributes[:usage].attributes[:total] - - sample[0].attributes[:cpu].attributes[:usage].attributes[:total] - spec = sample[1].attributes[:cpu].attributes[:usage].attributes[:per_cpu_usage].length - usage = (raw / 1000000000.0).round(3) - percent = ((usage / spec) * 100).round(2) - end - { - 'usage' => usage || 0, - 'percent' => percent || 0 - } - end - - def overall_memory(stats, spec) - if stats.count > 0 - usage = (stats.last.attributes[:memory].attributes[:usage] / 1024 * 1024).round(3) - percent = (usage / spec.attributes[:memory].attributes[:limit] * 100).round(2) - end - { - 'usage' => usage || 0, - 'percent' => percent || 0 - } - end end diff --git a/app/models/machine_info.rb b/app/models/machine_info.rb new file mode 100644 index 00000000..aaf76ed2 --- /dev/null +++ b/app/models/machine_info.rb @@ -0,0 +1,11 @@ +require 'active_resource' + +class MachineInfo < BaseResource + include ActiveResource::Singleton + + self.site = "#{ENV['CADVISOR_PORT']}" + + def self.singleton_path(_prefix_options={}, _query_options=nil) + '/api/v1.0/machine' + end +end diff --git a/app/models/service_health.rb b/app/models/service_health.rb new file mode 100644 index 00000000..e06e6bc4 --- /dev/null +++ b/app/models/service_health.rb @@ -0,0 +1,12 @@ +require 'active_resource' + +class ServiceHealth < BaseResource + include CadvisorMeasurable + + self.site = "#{ENV['CADVISOR_PORT']}" + + self.collection_name = 'docker' + self.element_name = 'docker' + self.prefix = '/api/v1.0/containers/' + self.include_format_in_path = false +end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0bd271d0..44680078 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -7,8 +7,6 @@ = javascript_include_tag 'application' = csrf_meta_tags = favicon_link_tag asset_path('favicons/favicon-160x160.png') - %script#host_health_template{type: 'text/x-handlebars-template'} - = render '/shared/host_health' %script#ajax_error_template{type: 'text/x-handlebars-template'} = render '/shared/notice' @@ -32,9 +30,11 @@ = link_to 'Manage', dashboard_index_path, title: 'Manage' %li.documentation-link = link_to 'Documentation', 'http://panamax.io/documentation', target: '_blank', title: 'Documentation' - %aside.host - %span CoreOS Host: - .health + %aside.metrics + %span + %a{ href: metrics_url, target: '_blank' } + CoreOS Host Performance: + = render 'shared/metrics' %cite.built-by %h2 Century Link %main diff --git a/app/views/services/show.html.haml b/app/views/services/show.html.haml index ef729cc0..1bea1d40 100644 --- a/app/views/services/show.html.haml +++ b/app/views/services/show.html.haml @@ -7,24 +7,28 @@ @service.name] %section.service-details - - .inspect - = link_to 'Docker Inspect', '#' - - %span.service-status{ class: @service.status, - data: { source: app_service_path(@app.id, @service.id, format: :json) } } - %span.panamax-state= @service.status.to_s.humanize - .tooltip - %ul - %li - Sub State: - %span.sub-state= @service.sub_state - %li - Active State: - %span.active-state= @service.active_state - %li - Load State: - %span.load-state= @service.load_state + .state + .inspect + = link_to 'Docker Inspect', '#' + %span.service-status{ class: @service.status, + data: { source: app_service_path(@app.id, @service.id, format: :json) } } + %span.panamax-state= @service.status.to_s.humanize + .tooltip + %ul + %li + Sub State: + %span.sub-state= @service.sub_state + %li + Active State: + %span.active-state= @service.active_state + %li + Load State: + %span.load-state= @service.load_state + %span.metric-details + %a{ href: metrics_url_for(@service.name), target: '_blank' } + Details + %span.metrics{ data: { 'service-name' => @service.name } } + = render 'shared/metrics' .base-image-detail .image-details diff --git a/app/views/shared/_host_health.html.haml b/app/views/shared/_host_health.html.haml deleted file mode 100644 index b0352688..00000000 --- a/app/views/shared/_host_health.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%section.details.tooltip - %ul - %li.cpu - .health - %span - Load Average: - {{cpu}} - %li.memory - .health - %span - Memory Usage: - {{memory}} - %footer - Last Updated: {{time_stamp}} diff --git a/app/views/shared/_metrics.html.haml b/app/views/shared/_metrics.html.haml new file mode 100644 index 00000000..7b05a431 --- /dev/null +++ b/app/views/shared/_metrics.html.haml @@ -0,0 +1,9 @@ +.health + %section.cpu + .label + CPU + .bar + %section.mem + .label + MEM + .bar diff --git a/config/routes.rb b/config/routes.rb index 30482841..c4e62641 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,6 +48,8 @@ resources :host_health, only: [:index] + resources :service_health, only: [:show] + get '/404', to: 'errors#not_found' get '/422', to: 'errors#unacceptable' get '/500', to: 'errors#internal_error' diff --git a/spec/concerns/cadvisor_measurable_spec.rb b/spec/concerns/cadvisor_measurable_spec.rb new file mode 100644 index 00000000..faf81754 --- /dev/null +++ b/spec/concerns/cadvisor_measurable_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +shared_examples_for "cadvisor_measurable" do + before do + MachineInfo.stub(:find).and_return(MachineInfo.new({ num_cores: 2, memory_capacity: 1000 })) + end + + let(:attributes) do + { + stats: [ + { + cpu: { + usage: { + total: 0, + per_cpu_usage: [100] + } + }, + memory: { + usage: 1024 + }, + timestamp: '2014-10-14T17:43:45.818251017Z' + }, + { + cpu: { + usage: { + total: 10000000, + per_cpu_usage: [100] + } + }, + memory: { + usage: 1024 + }, + timestamp: '2014-10-14T18:43:45.818251017Z' + } + ], + spec: { + has_cpu: true, + has_memory: true, + memory: { + limit: 2000 + }, + cpu: { + limit: 100 + } + } + } + end + + let(:overall) do + { + 'usage' => 0, + 'percent' => 0 + } + end + + let(:overall_mem) do + { + 'usage' => 1024, + 'percent' => 102 + } + end + + subject { described_class.new(attributes) } + + describe '#overall' do + it 'returns overall_mem result' do + expect(subject.overall.overall_mem).to eq overall_mem + end + + it 'returns overall_cpu result' do + expect(subject.overall.overall_cpu).to eq overall + end + end +end diff --git a/spec/controllers/service_health_controller_spec.rb b/spec/controllers/service_health_controller_spec.rb new file mode 100644 index 00000000..d6710cad --- /dev/null +++ b/spec/controllers/service_health_controller_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ServiceHealthController do + let(:fake_response) { double(:fake_response, overall: {}) } + + before do + ServiceHealth.stub(:find).and_return(fake_response) + end + + describe 'GET #show' do + it 'uses ServiceHealth model' do + expect(ServiceHealth).to receive(:find) + get :show, id: 'TEST', format: :json + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb new file mode 100644 index 00000000..d448af47 --- /dev/null +++ b/spec/helpers/application_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe ApplicationHelper do + before do + HostHealth.stub(:site).and_return('/health/') + end + + describe '#metrics_url' do + + it 'returns HostHealth.site value' do + expect(metrics_url).to eq '/health/' + end + end + + describe '#metrics_url_for' do + it 'returns HostHealth.site with service path appended' do + expect(metrics_url_for('ME')).to eq '/health/containers/docker/ME' + end + end +end diff --git a/spec/javascripts/fixtures/host_health.html.haml b/spec/javascripts/fixtures/host_health.html.haml index fabb1686..377e3984 100644 --- a/spec/javascripts/fixtures/host_health.html.haml +++ b/spec/javascripts/fixtures/host_health.html.haml @@ -1,18 +1,12 @@ -%script#host_health_template{type: 'text/x-handlebars-template'} - %section.details - %ul - %li.cpu - .health - %span - Load Average: - {{cpu}} - %li.memory - .health - %span - Memory Usage: - {{memory}} - %footer - Last Updated: {{time_stamp}} - -%aside.host - .health +.health{ data: { 'service-name' => 'Service' } } + %section.cpu + .label + CPU + .bar + %section.mem + .label + MEM + .bar +.service-link + Service +.service-status diff --git a/spec/javascripts/jquery.health/jquery.health_graph_spec.js b/spec/javascripts/jquery.health/jquery.health_graph_spec.js new file mode 100644 index 00000000..cbd3c242 --- /dev/null +++ b/spec/javascripts/jquery.health/jquery.health_graph_spec.js @@ -0,0 +1,64 @@ +describe('$.fn.healthGraph', function() { + + beforeEach(function() { + fixture.load('host_health.html'); + subject = new $.PMX.HealthGraph($('.health')); + }); + + describe('#calculateHealth', function() { + beforeEach(function() { + spyOn(subject, 'healthColor'); + spyOn(subject, 'datumSize'); + subject.init(); + }); + it('calls healthColor', function() { + subject.calculateHealth({ overall_cpu: 100, overall_mem: 100 }); + expect(subject.healthColor).toHaveBeenCalled(); + }); + + it('calls datumSize', function() { + subject.calculateHealth({ overall_cpu: 100, overall_mem: 100 }); + expect(subject.datumSize).toHaveBeenCalled(); + }); + }); + + describe('#healthColor', function() { + beforeEach(function() { + spyOn(subject, 'colorLevel').andReturn('white'); + subject.init(); + }); + + it('sets color level of the cpu details', function() { + subject.healthColor($('.health')); + expect(subject.colorLevel).toHaveBeenCalled(); + expect($('.health').hasClass('white')).toBeTruthy(); + }); + + it('sets color level of the memory details', function() { + subject.healthColor($('.health')); + expect(subject.colorLevel).toHaveBeenCalled(); + expect($('.health').hasClass('white')).toBeTruthy(); + }); + }); + + describe('#colorLevel', function() { + it('returns danger when value is 90 or higher', function() { + subject.init(); + result = subject.colorLevel(90); + expect(result).toEqual('danger'); + }); + + it('returns warning when value is 75 or higher', function() { + subject.init(); + result = subject.colorLevel(80); + expect(result).toEqual('warning'); + }); + + it('returns good when value is below 75', function() { + subject.init(); + result = subject.colorLevel(0); + expect(result).toEqual('good'); + }); + }); + +}); diff --git a/spec/javascripts/jquery.health/jquery.host_health_spec.js b/spec/javascripts/jquery.health/jquery.host_health_spec.js new file mode 100644 index 00000000..49cb7964 --- /dev/null +++ b/spec/javascripts/jquery.health/jquery.host_health_spec.js @@ -0,0 +1,49 @@ +describe('$.fn.hostHealth', function() { + var subject; + + beforeEach(function() { + jasmine.Ajax.useMock(); + fixture.load('host_health.html'); + subject = new $.PMX.HostHealth($('.health')); + spyOn(subject, 'buildGraph'); + spyOn(window, 'setTimeout'); + spyOn(window, 'clearTimeout'); + }); + + describe('#init', function() { + it('queries for the overall metrics', function() { + subject.init(); + var request = mostRecentAjaxRequest(); + expect(request.url).toBe('/host_health'); + }); + + it('creates HealthGraph', function() { + subject.init(); + expect(subject.buildGraph).toHaveBeenCalled(); + }); + + it('initiates setTimeout', function() { + subject.init(); + var request = mostRecentAjaxRequest(); + + request.response({ + status: 200, + responseText: '' + }); + expect(window.setTimeout).toHaveBeenCalled(); + }); + + it('clears the previously set timeout', function() { + var fakeTimer = {fakeTimer: true}; + subject.timer = fakeTimer; + subject.init(); + var request = mostRecentAjaxRequest(); + + request.response({ + status: 200, + responseText: '' + }); + expect(window.clearTimeout).toHaveBeenCalledWith(fakeTimer); + }); + }); +}); diff --git a/spec/javascripts/jquery.health/jquery.service_health_spec.js b/spec/javascripts/jquery.health/jquery.service_health_spec.js new file mode 100644 index 00000000..fe86767c --- /dev/null +++ b/spec/javascripts/jquery.health/jquery.service_health_spec.js @@ -0,0 +1,65 @@ +describe('$.fn.serviceHealth', function() { + var subject; + + beforeEach(function() { + jasmine.Ajax.useMock(); + fixture.load('host_health.html'); + subject = new $.PMX.ServiceHealth($('.health'), + { $metricLink: $('.service-link'), + $dockerStatus: $('.service-status') + }); + }); + + describe('#init', function() { + it('creates HealthGraph', function() { + spyOn(subject, 'buildGraph'); + subject.init(); + expect(subject.buildGraph).toHaveBeenCalled(); + }); + + it('hides the element', function() { + subject.init(); + expect($('.health').css('display')).toEqual('none'); + }); + }); + + describe('when update-service-status is triggered', function() { + it('calls checkStatusHandler', function() { + spyOn(subject, 'checkStatusHandler'); + subject.init(); + $('.service-status').trigger('update-service-status', { sub_state: 'running' }); + expect(subject.checkStatusHandler).toHaveBeenCalled(); + }); + + describe('service sub_state is running', function() { + it('shows the element when sub_state is running', function() { + spyOn(subject, 'showHealth'); + subject.init(); + $('.service-status').trigger('update-service-status', { sub_state: 'running' }); + expect(subject.showHealth).toHaveBeenCalled(); + }); + + it('calls service with proper url', function() { + jasmine.Ajax.useMock(); + subject.init(); + $('.service-status').trigger('update-service-status', { sub_state: 'running' }); + var request = mostRecentAjaxRequest(); + request.response({ + status: 200, + responseText: JSON.stringify({ overall_cpu: 10, overall_mem: 10 }) + }); + expect(request.method).toBe('GET'); + expect(request.url).toBe('/service_health/Service'); + }); + }); + + describe('service sub_state is not running', function() { + it('hides the element when sub_state is not running', function() { + spyOn(subject, 'hideHealth'); + subject.init(); + $('.service-status').trigger('update-service-status', {sub_state: 'loading'}); + expect(subject.hideHealth).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/jquery.host_health_spec.js b/spec/javascripts/jquery.host_health_spec.js deleted file mode 100644 index 5371094d..00000000 --- a/spec/javascripts/jquery.host_health_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -describe('$.fn.hostHealth', function() { - var subject, - overallResponse = [ - { - 'time_stamp': '1397167308398403' - } - ]; - - beforeEach(function() { - jasmine.Ajax.useMock(); - fixture.load('host_health.html'); - subject = new $.PMX.HostHealth($('.health')); - spyOn(window, 'setTimeout'); - spyOn(window, 'clearTimeout'); - }); - - describe('#init', function() { - it('queries for the overall metrics', function() { - subject.init(); - var request = mostRecentAjaxRequest(); - expect(request.url).toBe('/host_health'); - }); - - it('initiates setTimeout', function() { - subject.init(); - var request = mostRecentAjaxRequest(); - - request.response({ - status: 200, - responseText: '' - }); - expect(window.setTimeout).toHaveBeenCalled(); - }); - - it('clears the previously set timeout', function() { - var fakeTimer = {fakeTimer: true}; - subject.timer = fakeTimer; - subject.init(); - var request = mostRecentAjaxRequest(); - - request.response({ - status: 200, - responseText: '' - }); - expect(window.clearTimeout).toHaveBeenCalledWith(fakeTimer); - }); - }); - - describe('hovering on root element', function() { - it('adds the "showing" class', function() { - var event = $.Event('mouseenter'); - subject.init(); - $('.health').trigger(event); - expect($('.health').hasClass('showing')).toBeTruthy - }); - - it('removes "showing" when hover off', function() { - var $elem = $('.health'), - event = $.Event('mouseleave'); - subject.init(); - $elem.addClass('showing'); - $elem.trigger(event); - expect($elem.hasClass('showing')).toBeFalsy - }); - - it('displays details when showing', function() { - subject.init(); - $('.health').click(); - expect($('.health').find('.details')).toBeDefined - }); - }); - - describe('#healthColors', function() { - beforeEach(function() { - spyOn(subject, 'colorLevel').andReturn('white'); - subject.init(); - subject.healthColors(); - }); - - it('sets color level of the root element', function() { - expect(subject.colorLevel).toHaveBeenCalled(); - expect($('.health').hasClass('white')).toBeTruthy(); - }); - - it('sets color level of the cpu details', function() { - var event = $.Event('mouseenter'); - $('.health').trigger(event); - expect(subject.colorLevel).toHaveBeenCalled(); - expect($('.cpu .health').hasClass('white')).toBeTruthy(); - }); - - it('sets color level of the memory details', function() { - var event = $.Event('mouseenter'); - $('.health').trigger(event); - expect(subject.colorLevel).toHaveBeenCalled(); - expect($('.memory .health').hasClass('white')).toBeTruthy(); - }); - }); - - describe('#colorLevel', function() { - it('returns danger when value is 90 or higher', function() { - subject.init(); - result = subject.colorLevel(90); - expect(result).toEqual('danger'); - }); - - it('returns warning when value is 90 or higher', function() { - subject.init(); - result = subject.colorLevel(80); - expect(result).toEqual('warning'); - }); - - it('returns good when value is below 80', function() { - subject.init(); - result = subject.colorLevel(0); - expect(result).toEqual('good'); - }); - }); -}); diff --git a/spec/models/host_health_spec.rb b/spec/models/host_health_spec.rb index ec6c7a52..07bc207e 100644 --- a/spec/models/host_health_spec.rb +++ b/spec/models/host_health_spec.rb @@ -1,76 +1,11 @@ require 'spec_helper' describe HostHealth do - - let(:attributes) do - { - stats: [ - { - cpu: { - usage: { - total: 0, - per_cpu_usage: [100] - } - }, - memory: { - usage: 1024 - } - }, - { - cpu: { - usage: { - total: 10000000, - per_cpu_usage: [100] - } - }, - memory: { - usage: 1024 - } - } - ], - spec: { - memory: { - limit: 100 - }, - cpu: { - limit: 100 - } - } - } - end - - let(:overall) do - { - 'usage' => 0.01, - 'percent' => 1.0 - } - end - - let(:overall_mem) do - { - 'usage' => 1024, - 'percent' => 1024 - } - end - - subject { described_class.new(attributes) } - - it { should respond_to :overall } - - describe '#overall' do - it 'returns overall_mem result' do - expect(subject.overall.overall_mem).to eq overall_mem - end - - it 'returns overall_cpu result' do - expect(subject.overall.overall_cpu).to eq overall - end - end + it_behaves_like 'cadvisor_measurable' describe '.singleton_path' do it 'returns /api/v1.0/containers/' do expect(HostHealth.singleton_path).to eq '/api/v1.0/containers/' end - end end diff --git a/spec/models/machine_info_spec.rb b/spec/models/machine_info_spec.rb new file mode 100644 index 00000000..eb881d31 --- /dev/null +++ b/spec/models/machine_info_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe MachineInfo do + + it_behaves_like 'an active resource model' + + describe '.singleton_path' do + it 'returns /api/v1.0/machine' do + expect(MachineInfo.singleton_path).to eq '/api/v1.0/machine' + end + end +end diff --git a/spec/models/service_health_spec.rb b/spec/models/service_health_spec.rb new file mode 100644 index 00000000..afbb3f01 --- /dev/null +++ b/spec/models/service_health_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe ServiceHealth do + it_behaves_like 'cadvisor_measurable' +end