├── .DS_Store ├── .fasterer.yml ├── .gitignore ├── .irbrc ├── .reek.yml ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── app.rb ├── bin ├── console └── setup ├── examples └── web-ui.png ├── lib ├── generators │ └── stackeye │ │ ├── install_generator.rb │ │ └── templates │ │ └── install.rb ├── stackeye.rb └── stackeye │ ├── application.rb │ ├── configuration.rb │ ├── helpers │ ├── base.rb │ └── init.rb │ ├── metrics │ ├── all.rb │ ├── base.rb │ ├── init.rb │ ├── mysql.rb │ └── server.rb │ ├── public │ ├── fonts │ │ ├── feather.eot │ │ ├── feather.svg │ │ ├── feather.ttf │ │ └── feather.woff │ ├── images │ │ ├── favicon.ico │ │ ├── stackeye.png │ │ └── ubuntu.svg │ ├── javascripts │ │ ├── 3PL.js │ │ └── application.js │ └── stylesheets │ │ ├── 3PL.css │ │ └── application.css │ ├── routes │ ├── base.rb │ ├── init.rb │ └── metrics.rb │ ├── tools │ ├── cli.rb │ ├── database.rb │ ├── init.rb │ └── os.rb │ ├── version.rb │ └── views │ ├── layout.erb │ ├── metrics │ ├── mysql │ │ ├── _queries.erb │ │ └── index.erb │ └── server │ │ ├── _cpu.erb │ │ ├── _disk.erb │ │ ├── _memory.erb │ │ ├── _processes.erb │ │ └── index.erb │ ├── shared │ ├── _header.erb │ └── _navbar.erb │ └── unsupported.erb ├── spec ├── spec_helper.rb ├── stackeye │ └── metrics │ │ └── base_spec.rb └── stackeye_spec.rb ├── stackeye.gemspec └── stackeye └── server.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/.DS_Store -------------------------------------------------------------------------------- /.fasterer.yml: -------------------------------------------------------------------------------- 1 | speedups: 2 | rescue_vs_respond_to: true 3 | module_eval: true 4 | shuffle_first_vs_sample: true 5 | for_loop_vs_each: true 6 | each_with_index_vs_while: false 7 | map_flatten_vs_flat_map: true 8 | reverse_each_vs_reverse_each: true 9 | select_first_vs_detect: true 10 | sort_vs_sort_by: true 11 | fetch_with_argument_vs_block: true 12 | keys_each_vs_each_key: true 13 | hash_merge_bang_vs_hash_brackets: true 14 | block_vs_symbol_to_proc: true 15 | proc_call_vs_yield: true 16 | gsub_vs_tr: true 17 | select_last_vs_reverse_detect: true 18 | getter_vs_attr_reader: true 19 | setter_vs_attr_writer: true 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /log/ 10 | /stackeye/ 11 | /.irbrc_history 12 | 13 | # rspec failure tracking 14 | .rspec_status 15 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Awesome print 4 | begin 5 | require 'awesome_print' 6 | 7 | AwesomePrint.irb! 8 | rescue LoadError => err 9 | warn "Couldn't load awesome_print: #{err}" 10 | end 11 | 12 | # IRB 13 | require 'irb/completion' 14 | 15 | ARGV.concat %w[--readline --prompt-mode simple] 16 | 17 | IRB.conf[:PROMPT_MODE] = :SIMPLE 18 | IRB.conf[:EVAL_HISTORY] = 1000 19 | IRB.conf[:SAVE_HISTORY] = 1000 20 | IRB.conf[:HISTORY_FILE] = File.expand_path('.irbrc_history') 21 | 22 | # Rails 23 | railsrc_path = File.expand_path('.irbrc_rails') 24 | 25 | if (ENV['RAILS_ENV'] || defined?(Rails)) && File.exist?(railsrc_path) 26 | begin 27 | load railsrc_path 28 | rescue Exception => err 29 | warn "Could not load: #{railsrc_path} because of #{err}" 30 | end 31 | end 32 | 33 | # Object 34 | class Object 35 | 36 | def interesting_methods 37 | case self.class 38 | when Class then public_methods.sort - Object.public_methods 39 | when Module then public_methods.sort - Module.public_methods 40 | else public_methods.sort - Object.new.public_methods 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | detectors: 3 | Attribute: 4 | enabled: false 5 | ControlParameter: 6 | enabled: false 7 | DuplicateMethodCall: 8 | enabled: false 9 | InstanceVariableAssumption: 10 | enabled: false 11 | IrresponsibleModule: 12 | enabled: false 13 | NestedIterators: 14 | enabled: false 15 | NilCheck: 16 | enabled: false 17 | RepeatedConditional: 18 | enabled: false 19 | TooManyStatements: 20 | enabled: false 21 | UncommunicativeVariableName: 22 | enabled: false 23 | UtilityFunction: 24 | enabled: false 25 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | DisplayStyleGuide: true 4 | TargetRubyVersion: 2.5 5 | Exclude: 6 | - 'spec/**/**/*' 7 | LineLength: 8 | Max: 100 9 | Layout/EmptyLinesAroundClassBody: 10 | Enabled: false 11 | Layout/EmptyLinesAroundModuleBody: 12 | Enabled: false 13 | Lint/RescueException: 14 | Enabled: false 15 | Style/ClassAndModuleChildren: 16 | Enabled: false 17 | Style/Dir: 18 | Enabled: false 19 | Style/Documentation: 20 | Enabled: false 21 | Style/ExpandPathArguments: 22 | Enabled: false 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - ruby-head 5 | before_install: 6 | - sudo apt-get install libxml2-dev 7 | - gem update --system 8 | - gem install bundler 9 | - bundle install 10 | script: 11 | - bundle exec rake 12 | notifications: 13 | disable: true 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2018-07-09 10 | ### Added 11 | - Initial working project release. 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at j.gomez@drexed.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in stackeye.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | stackeye (0.1.0) 5 | sinatra 6 | sinatra-contrib 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | activesupport (5.2.1) 12 | concurrent-ruby (~> 1.0, >= 1.0.2) 13 | i18n (>= 0.7, < 2) 14 | minitest (~> 5.1) 15 | tzinfo (~> 1.1) 16 | ast (2.4.0) 17 | axiom-types (0.1.1) 18 | descendants_tracker (~> 0.0.4) 19 | ice_nine (~> 0.11.0) 20 | thread_safe (~> 0.3, >= 0.3.1) 21 | backports (3.11.4) 22 | codeclimate-engine-rb (0.4.1) 23 | virtus (~> 1.0) 24 | coercible (1.0.0) 25 | descendants_tracker (~> 0.0.1) 26 | colorize (0.8.1) 27 | concurrent-ruby (1.0.5) 28 | descendants_tracker (0.0.4) 29 | thread_safe (~> 0.3, >= 0.3.1) 30 | diff-lcs (1.3) 31 | equalizer (0.0.11) 32 | fasterer (0.4.1) 33 | colorize (~> 0.7) 34 | ruby_parser (~> 3.11.0) 35 | i18n (1.1.0) 36 | concurrent-ruby (~> 1.0) 37 | ice_nine (0.11.2) 38 | jaro_winkler (1.5.1) 39 | kwalify (0.7.2) 40 | minitest (5.11.3) 41 | multi_json (1.13.1) 42 | mustermann (1.0.3) 43 | parallel (1.12.1) 44 | parser (2.5.1.2) 45 | ast (~> 2.4.0) 46 | powerpack (0.1.2) 47 | rack (2.0.5) 48 | rack-protection (2.0.4) 49 | rack 50 | rainbow (3.0.0) 51 | rake (12.3.1) 52 | reek (5.1.0) 53 | codeclimate-engine-rb (~> 0.4.0) 54 | kwalify (~> 0.7.0) 55 | parser (>= 2.5.0.0, < 2.6, != 2.5.1.1) 56 | rainbow (>= 2.0, < 4.0) 57 | rspec (3.8.0) 58 | rspec-core (~> 3.8.0) 59 | rspec-expectations (~> 3.8.0) 60 | rspec-mocks (~> 3.8.0) 61 | rspec-core (3.8.0) 62 | rspec-support (~> 3.8.0) 63 | rspec-expectations (3.8.1) 64 | diff-lcs (>= 1.2.0, < 2.0) 65 | rspec-support (~> 3.8.0) 66 | rspec-mocks (3.8.0) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.8.0) 69 | rspec-support (3.8.0) 70 | rubocop (0.59.2) 71 | jaro_winkler (~> 1.5.1) 72 | parallel (~> 1.10) 73 | parser (>= 2.5, != 2.5.1.1) 74 | powerpack (~> 0.1) 75 | rainbow (>= 2.2.2, < 4.0) 76 | ruby-progressbar (~> 1.7) 77 | unicode-display_width (~> 1.0, >= 1.0.1) 78 | ruby-progressbar (1.10.0) 79 | ruby_parser (3.11.0) 80 | sexp_processor (~> 4.9) 81 | sexp_processor (4.11.0) 82 | sinatra (2.0.4) 83 | mustermann (~> 1.0) 84 | rack (~> 2.0) 85 | rack-protection (= 2.0.4) 86 | tilt (~> 2.0) 87 | sinatra-contrib (2.0.4) 88 | activesupport (>= 4.0.0) 89 | backports (>= 2.8.2) 90 | multi_json 91 | mustermann (~> 1.0) 92 | rack-protection (= 2.0.4) 93 | sinatra (= 2.0.4) 94 | tilt (>= 1.3, < 3) 95 | thread_safe (0.3.6) 96 | tilt (2.0.8) 97 | tzinfo (1.2.5) 98 | thread_safe (~> 0.1) 99 | unicode-display_width (1.4.0) 100 | virtus (1.0.5) 101 | axiom-types (~> 0.1) 102 | coercible (~> 1.0) 103 | descendants_tracker (~> 0.0, >= 0.0.3) 104 | equalizer (~> 0.0, >= 0.0.9) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | bundler 111 | fasterer 112 | rake 113 | reek 114 | rspec 115 | rubocop 116 | stackeye! 117 | 118 | BUNDLED WITH 119 | 1.16.2 120 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Juan Gomez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stackeye 2 | 3 | #### What is it? 4 | Stackeye is small and lightweight metrics monitoring system. 5 | It's meant for projects where longterm and highly detailed metrics 6 | is not a priority (p.s. thats what Scout and New Relic are for). 7 | 8 | #### What are the design decisions? 9 | One of the main design aspects of this project was to have as little 10 | dependencies as possible (currently just Sinatra). All metric commands 11 | are executed locally and all data is stored locally in JSON files. 12 | 13 | #### What does the future hold? 14 | This project will continue to grow overtime but I would **love some help 15 | from the community** to really make it blossom into a great project. The 16 | following is a list of future must/nice to have: 17 | 18 | * Metrics 19 | * Ruby 20 | * Rails 21 | * SQLite 22 | * PostgreSQL 23 | * Redis 24 | * Alerts 25 | * Email 26 | * SMS 27 | * OS 28 | * Windows 29 | * Mac OS 30 | * Unix 31 | 32 |  33 | 34 | ## Installation 35 | 36 | Add this line to your application's Gemfile: 37 | 38 | ```ruby 39 | gem 'stackeye' 40 | ``` 41 | 42 | And then execute: 43 | 44 | $ bundle 45 | 46 | Or install it yourself as: 47 | 48 | $ gem install stackeye 49 | 50 | ## Usage 51 | 52 | #### Standalone Sinatra app 53 | ```ruby 54 | ruby app.rb 55 | 56 | # crontab 57 | */5 * * * * /bin/bash -l -c 'cd /path/to/project && Stackeye::Metrics::All.set >> /dev/null' 58 | 0 0 * * * /bin/bash -l -c 'cd /path/to/project && Stackeye::Tools::Database.truncate >> /dev/null' 59 | ``` 60 | 61 | #### Mounted Rails app 62 | ```ruby 63 | # Run initializer generator: 64 | rails generate stackeye:install 65 | 66 | # config/routes.rb 67 | mount Stackeye::Application, at: '/stackeye' 68 | 69 | # The following could be used if you are using 70 | # the whenever gem to manage your crons. 71 | 72 | # config/schedule.rb 73 | every 5.minutes do 74 | runner 'Stackeye::Metrics::All.set' 75 | end 76 | 77 | every :day, at: '12:00 am' do 78 | runner 'Stackeye::Tools::Database.truncate' 79 | end 80 | ``` 81 | 82 | ## Contributing 83 | 84 | Bug reports and pull requests are welcome on GitHub at https://github.com/drexed/stackeye. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 85 | 86 | ## License 87 | 88 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 89 | 90 | ## Code of Conduct 91 | 92 | Everyone interacting in the Stackeye project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/drexed/stackeye/blob/master/CODE_OF_CONDUCT.md). 93 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH << File.expand_path(File.dirname(__FILE__)) + '/lib' 4 | 5 | require 'stackeye' 6 | 7 | Stackeye::Application.run! 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #!/usr/bin/env ruby 3 | 4 | require 'bundler/setup' 5 | require 'stackeye' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require 'pry' 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #!/usr/bin/env bash 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | set -vx 6 | 7 | bundle install 8 | 9 | # Do any other automated setup that you need to do here 10 | -------------------------------------------------------------------------------- /examples/web-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/examples/web-ui.png -------------------------------------------------------------------------------- /lib/generators/stackeye/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails/generators' 4 | 5 | class Stackeye::InstallGenerator < Rails::Generators::Base 6 | source_root File.expand_path('../templates', __FILE__) 7 | 8 | def copy_initializer_file 9 | copy_file('install.rb', 'config/initializers/stackeye.rb') 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/stackeye/templates/install.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Stackeye.configure do |config| 4 | config.max_data = 288 5 | config.metrics = %w[server mysql] 6 | config.credentials = {} 7 | end 8 | -------------------------------------------------------------------------------- /lib/stackeye.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[version configuration application].each do |filename| 4 | require "stackeye/#{filename}" 5 | end 6 | 7 | require 'generators/stackeye/install_generator' 8 | -------------------------------------------------------------------------------- /lib/stackeye/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[base cookies].each do |filename| 4 | require "sinatra/#{filename}" 5 | end 6 | 7 | %w[tools metrics helpers routes].each do |dirname| 8 | require_relative "#{dirname}/init" 9 | end 10 | 11 | require 'logger' 12 | 13 | class Stackeye::Application < Sinatra::Base 14 | helpers Sinatra::Cookies 15 | 16 | set :app_file, __FILE__ 17 | set :bind, '0.0.0.0' 18 | 19 | configure :development do 20 | enable :logging, :dump_errors, :raise_errors 21 | end 22 | 23 | configure :production do 24 | dir = File.expand_path('log') 25 | Dir.mkdir(dir) unless File.directory?(dir) 26 | file = File.new("#{dir}/stackeye.log", 'a+') 27 | file.sync = true 28 | 29 | use ::Rack::CommonLogger, file 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/stackeye/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | class Configuration 5 | 6 | MAX_DATA ||= 288 7 | METRICS ||= %w[server mysql].freeze 8 | 9 | attr_accessor :credentials, :max_data, :metrics 10 | 11 | def initialize 12 | @max_data = MAX_DATA 13 | @metrics = METRICS 14 | @credentials = {} 15 | end 16 | 17 | end 18 | 19 | def self.configuration 20 | @configuration ||= Configuration.new 21 | end 22 | 23 | def self.configuration=(config) 24 | @configuration = config 25 | end 26 | 27 | def self.configure 28 | yield(configuration) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/stackeye/helpers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def base_path 4 | return unless ENV['RAILS_ENV'] 5 | 6 | '/stackeye' 7 | end 8 | 9 | def metric_icon_decorator(metric) 10 | case metric 11 | when 'server' then 'server' 12 | else 'database' 13 | end 14 | end 15 | 16 | def metric_name_decorator(metric) 17 | case metric 18 | when 'mysql' then 'MySQL' 19 | else titleize(metric) 20 | end 21 | end 22 | 23 | def modulize(str) 24 | str.tr('_-', ' ').split(' ').map(&:capitalize).join('') 25 | end 26 | 27 | def page?(path) 28 | request.path == "#{base_path}#{path}" 29 | end 30 | 31 | def refreshing? 32 | cookies[:refresh] == '1' 33 | end 34 | 35 | def titleize(str) 36 | str.tr('_', ' ').capitalize 37 | end 38 | 39 | def verified_distro? 40 | Stackeye::Tools::Os.linux? 41 | end 42 | 43 | def verified_os? 44 | cmd = 'lsb_release -ds' 45 | Stackeye::Tools::Cli.execute(cmd).strip.include?('Ubuntu') 46 | end 47 | 48 | def verified_distro_and_os? 49 | verified_distro? && verified_os? 50 | end 51 | 52 | def verify_distro_and_os! 53 | return if verified_distro_and_os? 54 | 55 | redirect("#{base_path}/unsupported") 56 | end 57 | -------------------------------------------------------------------------------- /lib/stackeye/helpers/init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[base].each do |filename| 4 | require_relative filename 5 | end 6 | -------------------------------------------------------------------------------- /lib/stackeye/metrics/all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | module Metrics 5 | class All 6 | 7 | def initialize; end 8 | 9 | def set 10 | Stackeye.configuration.metrics.each do |metric| 11 | klass = "Stackeye::Metrics::#{modulize(metric)}" 12 | Module.const_get(klass).set 13 | end 14 | end 15 | 16 | class << self 17 | def set 18 | klass = new 19 | klass.set 20 | end 21 | end 22 | 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/stackeye/metrics/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | module Metrics 5 | class Base 6 | 7 | MB ||= 1024.0 8 | GB ||= MB**2 9 | 10 | def initialize 11 | @data = { timestamp: Time.now.to_i } 12 | end 13 | 14 | def filepath 15 | return @filepath if defined?(@filepath) 16 | 17 | @filepath ||= begin 18 | path = Stackeye::Tools::Database::DATA_PATH 19 | name = self.class.name.split('::').last.downcase 20 | 21 | "#{path}/#{name}.json" 22 | end 23 | end 24 | 25 | def set 26 | generate_data 27 | return if @data.empty? 28 | 29 | Stackeye::Tools::Database.set(filepath, @data) 30 | end 31 | 32 | def get 33 | return @get if defined?(@get) 34 | 35 | @get ||= Stackeye::Tools::Database.get(filepath) 36 | end 37 | 38 | def pluck(key) 39 | @pluck ||= {} 40 | return @pluck[key] if @pluck.key?(key) 41 | 42 | @pluck[key] = get.collect { |hash| hash[key] } 43 | end 44 | 45 | def mean(key) 46 | @mean ||= {} 47 | return @mean[key] if @mean.key?(key) 48 | 49 | values = pluck(key) 50 | return @mean[key] = 0.0 if values.empty? 51 | 52 | @mean[key] = values.sum / values.length.to_f 53 | end 54 | 55 | # rubocop:disable Metrics/AbcSize 56 | def median(key) 57 | @median ||= {} 58 | return @median[key] if @median.key?(key) 59 | 60 | values = pluck(key) 61 | return @median[key] = 0.0 if values.empty? 62 | 63 | values_sorted = values.sort 64 | values_halved = values.length / 2.0 65 | values_halved_sorted = values_sorted[values_halved] 66 | return @median[key] = values_halved_sorted unless (values.length % 2).zero? 67 | 68 | @median[key] = (values_sorted[values_halved - 1.0] + values_halved_sorted) / 2.0 69 | end 70 | # rubocop:enable Metrics/AbcSize 71 | 72 | def mode(key) 73 | @mode ||= {} 74 | return @mode[key] if @mode.key?(key) 75 | 76 | values = pluck(key) 77 | return @mode[key] = 0.0 if values.empty? 78 | 79 | values_distro = values.each_with_object(Hash.new(0)) { |val, hsh| hsh[val] += 1 } 80 | values_top_two = values_distro.sort_by { |_, val| -val }.take(2) 81 | @mode[key] = values_top_two.first.first 82 | end 83 | 84 | def range(key) 85 | @range ||= {} 86 | return @range[key] if @range.key?(key) 87 | 88 | values = pluck(key) 89 | return @range[key] = 0.0 if values.empty? 90 | 91 | values_sorted = values.sort 92 | @mode[key] = values_sorted.last - values_sorted.first 93 | end 94 | 95 | class << self 96 | %i[filepath set get].each do |name| 97 | define_method(name) do 98 | klass = new 99 | klass.send(name) 100 | end 101 | end 102 | 103 | %i[pluck mean median mode range].each do |name| 104 | define_method(name) do |key| 105 | klass = new 106 | klass.send(name, key) 107 | end 108 | end 109 | end 110 | 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/stackeye/metrics/init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[base server mysql all].each do |filename| 4 | require_relative filename 5 | end 6 | -------------------------------------------------------------------------------- /lib/stackeye/metrics/mysql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | module Metrics 5 | class Mysql < Stackeye::Metrics::Base 6 | 7 | CREDENTIALS ||= { 8 | user: 'root', 9 | host: 'localhost', 10 | password: nil 11 | }.freeze 12 | 13 | def generate_data 14 | generate_stats 15 | end 16 | 17 | private 18 | 19 | # rubocop:disable Metrics/AbcSize 20 | def generate_stats 21 | cmd = "mysqladmin -u#{user} -h#{host} -p#{password} status" 22 | lines = Stackeye::Tools::Cli.execute(cmd).split("\n") 23 | stats = lines.last.strip.split(' ') 24 | 25 | stats.each do |stat| 26 | key, val = stat.split(': ') 27 | key = key.downcase.tr(' ', '_') 28 | key = 'velocity' if key == 'queries_per_second_avg' 29 | 30 | @data[key] = val.to_f.round(2) 31 | end 32 | end 33 | # rubocop:enable Metrics/AbcSize 34 | 35 | %i[host password user].each do |name| 36 | define_method(name) do 37 | credentials = Stackeye.configuration.credentials[:mysql] 38 | credentials[name] || CREDENTIALS[name] 39 | end 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/stackeye/metrics/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | module Metrics 5 | class Server < Stackeye::Metrics::Base 6 | 7 | def generate_data 8 | generate_cpu_loadavg 9 | generate_cpu_utilization 10 | generate_memory_utilization 11 | generate_process_utilization 12 | generate_swap_utilization 13 | generate_volume_utilization 14 | end 15 | 16 | private 17 | 18 | def generate_cpu_loadavg 19 | { cpu_load_1m: 1, cpu_load_5m: 2, cpu_load_15m: 3 }.each do |name, col| 20 | cmd = "cat /proc/loadavg | awk '{ print $#{col} }'" 21 | @data[name] = Stackeye::Tools::Cli.execute(cmd).strip.to_f 22 | end 23 | end 24 | 25 | def generate_cpu_utilization 26 | cmd = "ps -Ao %cpu | awk '{ s += $1 } END { print s }'" 27 | @data[:cpu_utilization] = Stackeye::Tools::Cli.execute(cmd).strip.to_f 28 | end 29 | 30 | def generate_process_utilization 31 | { cpu: 'pcpu', memory: 'pmem' }.each do |label, sort| 32 | key = "#{label}_processes".to_sym 33 | cmd = "ps -Ao user,uid,comm,pid,pcpu,pmem --sort=-#{sort} | head -n 11" 34 | processes = Stackeye::Tools::Cli.execute(cmd) 35 | 36 | @data[key] = [] 37 | processes.split("\n").each_with_index do |process, i| 38 | next if i.zero? 39 | 40 | @data[key] << process.strip.gsub(/\s+/, ' ').split(' ') 41 | end 42 | end 43 | end 44 | 45 | def generate_memory_utilization 46 | { memory_free: 4, memory_total: 2, memory_used: 3 }.each do |name, col| 47 | cmd = "/usr/bin/free | head -n 2 | tail -n 1 | awk '{ print $#{col} }'" 48 | memory = Stackeye::Tools::Cli.execute(cmd).strip.to_f 49 | 50 | @data[name] = (memory / GB).round(2) 51 | end 52 | end 53 | 54 | def generate_swap_utilization 55 | { swap_free: 4, swap_total: 2, swap_used: 3 }.each do |name, col| 56 | cmd = "/usr/bin/free | tail -n 1 | awk '{ print $#{col} }'" 57 | swap = Stackeye::Tools::Cli.execute(cmd).strip.to_f 58 | 59 | @data[name] = (swap / GB).round(2) 60 | end 61 | end 62 | 63 | def generate_volume_utilization 64 | { disk_free: 4, disk_total: 2, disk_used: 3 }.each do |name, col| 65 | cmd = "/bin/df --total | tail -n 1 | awk '{ print $#{col} }'" 66 | volume = Stackeye::Tools::Cli.execute(cmd).strip.to_f 67 | 68 | @data[name] = (volume / GB).round(2) 69 | end 70 | end 71 | 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/stackeye/public/fonts/feather.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/lib/stackeye/public/fonts/feather.eot -------------------------------------------------------------------------------- /lib/stackeye/public/fonts/feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/lib/stackeye/public/fonts/feather.ttf -------------------------------------------------------------------------------- /lib/stackeye/public/fonts/feather.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/lib/stackeye/public/fonts/feather.woff -------------------------------------------------------------------------------- /lib/stackeye/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/lib/stackeye/public/images/favicon.ico -------------------------------------------------------------------------------- /lib/stackeye/public/images/stackeye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drexed/stackeye/e1a879b1918a003db2428d8479cedf7881a6366b/lib/stackeye/public/images/stackeye.png -------------------------------------------------------------------------------- /lib/stackeye/public/images/ubuntu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/stackeye/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | /* 2 | Use this file to add custom scripts. 3 | Please do not override any of the other files 4 | */ 5 | 6 | $(document).ready(function() { 7 | var colors = { 8 | gray: { 9 | 100: "#95AAC9", 10 | 300: "#E3EBF6", 11 | 600: "#95AAC9", 12 | 700: "#6E84A3", 13 | 900: "#283E59" 14 | }, 15 | primary: { 16 | 100: "#D2DDEC", 17 | 300: "#A6C5F7", 18 | 700: "#2C7BE5" 19 | }, 20 | black: "#12263F", 21 | white: "#FFFFFF", 22 | transparent: "transparent" 23 | }; 24 | 25 | Chart.defaults.global.defaultColor = colors.primary[600]; 26 | Chart.defaults.global.defaultFontColor = colors.gray[600]; 27 | Chart.defaults.global.defaultFontFamily = 'Rubik'; 28 | Chart.defaults.global.defaultFontSize = 13; 29 | Chart.defaults.global.elements.line.backgroundColor = colors.primary[100]; 30 | Chart.defaults.global.elements.line.borderCapStyle = "rounded"; 31 | Chart.defaults.global.elements.line.borderColor = colors.primary[700]; 32 | Chart.defaults.global.elements.line.borderWidth = 1; 33 | Chart.defaults.global.elements.line.tension = 0; 34 | Chart.defaults.global.elements.point.backgroundColor = colors.primary[700]; 35 | Chart.defaults.global.elements.point.borderColor = colors.transparent; 36 | Chart.defaults.global.elements.point.hitRadius = 10; 37 | Chart.defaults.global.elements.point.radius = 0; 38 | Chart.defaults.global.elements.rectangle.backgroundColor = colors.primary[700]; 39 | Chart.defaults.global.legend.display = !1; 40 | Chart.defaults.global.legend.position = "bottom"; 41 | Chart.defaults.global.legend.labels.usePointStyle = !0; 42 | Chart.defaults.global.legend.labels.padding = 16; 43 | Chart.defaults.global.maintainAspectRatio = !1; 44 | Chart.defaults.global.maxBarThickness = 10; 45 | Chart.defaults.global.responsive = !0; 46 | Chart.defaults.global.tooltips.backgroundColor = colors.black; 47 | Chart.defaults.global.tooltips.titleFontStyle = 500; 48 | Chart.defaults.global.tooltips.cornerRadius = 4; 49 | Chart.defaults.global.tooltips.xPadding = 10; 50 | Chart.defaults.global.tooltips.yPadding = 10; 51 | 52 | function generateTimeChartLabels(options) { 53 | var labels = []; 54 | 55 | $.each(options.timestamps, function(index, value) { 56 | labels.push(new Date(value * 1000)); 57 | }); 58 | 59 | return labels; 60 | } 61 | 62 | function generateMeanChartLabels(options) { 63 | var labels = []; 64 | 65 | for (var i = 0; i < options.metrics.length; i++) { 66 | labels.push(options.mean); 67 | } 68 | 69 | return labels; 70 | } 71 | 72 | function createTimeChart(options) { 73 | return new Chart(document.getElementById(options.target), { 74 | type: "line", 75 | data: { 76 | labels: options.labels, 77 | datasets: [{ 78 | label: "Average", 79 | data: options.mean, 80 | fill: false, 81 | backgroundColor: colors.black, 82 | borderColor: colors.black, 83 | borderWidth: 1 84 | }, 85 | { 86 | steppedLine: true, 87 | data: options.metrics 88 | }] 89 | }, 90 | options: { 91 | animation: { 92 | duration: 0 93 | }, 94 | hover: { 95 | animationDuration: 0 96 | }, 97 | responsiveAnimationDuration: 0, 98 | tooltips: { 99 | callbacks: { 100 | title: function(tooltipItem, data) { 101 | return moment(tooltipItem[0].xLabel).format('MMMM Do YYYY, h:mm a'); 102 | }, 103 | label: function(tooltipItems, data) { 104 | return (options.prefix || '') + 105 | data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] + 106 | (options.suffix || ''); 107 | } 108 | } 109 | }, 110 | scales: { 111 | xAxes: [{ 112 | type: 'time', 113 | distribution: 'series', 114 | time: { 115 | minUnit: 'minute', 116 | displayFormats: { 117 | minute: 'h:mm a' 118 | } 119 | }, 120 | gridLines: { 121 | display: !1, 122 | drawBorder: !1 123 | } 124 | }], 125 | yAxes: [{ 126 | ticks : { 127 | beginAtZero: true 128 | }, 129 | gridLines: { 130 | borderDash: [2], 131 | borderDashOffset: [2], 132 | color: colors.gray[300], 133 | drawBorder: !1, 134 | drawTicks: !1, 135 | lineWidth: 0, 136 | zeroLineWidth: 0, 137 | zeroLineColor: colors.gray[300], 138 | zeroLineBorderDash: [2], 139 | zeroLineBorderDashOffset: [2] 140 | } 141 | }] 142 | } 143 | } 144 | }); 145 | } 146 | 147 | function createDoughnutChart(options) { 148 | return new Chart(document.getElementById(options.target), { 149 | type: 'doughnut', 150 | data: { 151 | labels: options.labels, 152 | datasets: [{ 153 | data: options.metrics, 154 | borderWidth: 0, 155 | backgroundColor: [ 156 | colors.primary[700], 157 | colors.black 158 | ] 159 | }] 160 | }, 161 | options: { 162 | cutoutPercentage: 75, 163 | tooltips: { 164 | callbacks: { 165 | label: function(tooltipItems, data) { 166 | return (options.prefix || '') + 167 | data.labels[tooltipItems.index] + 168 | ': ' + 169 | data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] + 170 | (options.suffix || ''); 171 | } 172 | } 173 | } 174 | } 175 | }); 176 | } 177 | 178 | function createChartByType(options) { 179 | var chart; 180 | 181 | switch(options.type) { 182 | case 'doughnut': 183 | createDoughnutChart(options); 184 | break; 185 | case 'time': 186 | options.labels = generateTimeChartLabels(options); 187 | options.mean = generateMeanChartLabels(options); 188 | chart = createTimeChart(options); 189 | }; 190 | 191 | return chart; 192 | } 193 | 194 | function expandChartCards(options) { 195 | $(options.contract).addClass('d-none'); 196 | $(options.expand).removeClass('d-none'); 197 | } 198 | 199 | var charts = {}; 200 | var chartHandler = $('[data-toggle="chart"]'); 201 | chartHandler.each(function (index) { 202 | var self = $(this); 203 | var options = { 204 | target: self.data("target") 205 | }; 206 | 207 | if (!charts[options.target]) { 208 | $.each([ 209 | "type", "labels", "metrics", "mean", "timestamps", "prefix", "suffix" 210 | ], function(i, value) { 211 | options[value] = self.data(value); 212 | }); 213 | 214 | charts[options.target] = createChartByType(options); 215 | } 216 | }); 217 | 218 | chartHandler.on("click", function () { 219 | var self = $(this); 220 | var options = {}; 221 | 222 | $.each([ 223 | "target", "labels", "type", "metrics", "mean", "timestamps", "prefix", "suffix" 224 | ], function(i, value) { 225 | options[value] = self.data(value); 226 | }); 227 | 228 | createChartByType(options); 229 | }); 230 | 231 | var expandHandler = $('[data-expand]'); 232 | expandHandler.on("click", function () { 233 | var self = $(this); 234 | var options = { 235 | contract: self.data("contract"), 236 | expand: self.data("expand") 237 | }; 238 | 239 | expandChartCards(options); 240 | }); 241 | 242 | }); 243 | -------------------------------------------------------------------------------- /lib/stackeye/public/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | Use this file to add custom styles. 3 | Please do not override any of the other files 4 | */ 5 | 6 | .unsupported { 7 | margin: 100px 0; 8 | text-align: center; 9 | } 10 | .unsupported img { 11 | max-width: 180px; 12 | margin-bottom: 60px; 13 | } 14 | -------------------------------------------------------------------------------- /lib/stackeye/routes/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Stackeye::Application < Sinatra::Base 4 | 5 | get '/' do 6 | metric = Stackeye.configuration.metrics.first 7 | 8 | redirect("#{base_path}/#{metric}") 9 | end 10 | 11 | get '/refresh' do 12 | cookies[:refresh] = refreshing? ? '0' : '1' 13 | 14 | redirect(back) 15 | end 16 | 17 | get '/unsupported' do 18 | erb(:unsupported) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/stackeye/routes/init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[base metrics].each do |filename| 4 | require_relative filename 5 | end 6 | -------------------------------------------------------------------------------- /lib/stackeye/routes/metrics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Stackeye::Application < Sinatra::Base 4 | 5 | Stackeye.configuration.metrics.each do |metric| 6 | get "/#{metric}" do 7 | verify_distro_and_os! 8 | 9 | klass = "Stackeye::Metrics::#{modulize(metric)}" 10 | @metrics = Module.const_get(klass).new 11 | @title = metric_name_decorator(metric) 12 | 13 | erb(:"metrics/#{metric}/index") 14 | end 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/stackeye/tools/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open3' 4 | 5 | module Stackeye 6 | module Tools 7 | class Cli 8 | 9 | def initialize(command) 10 | @command = command 11 | end 12 | 13 | def execute 14 | output, _status = Open3.capture2(@command) 15 | output 16 | end 17 | 18 | class << self 19 | def execute(command) 20 | klass = new(command) 21 | klass.execute 22 | end 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/stackeye/tools/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module Stackeye 6 | module Tools 7 | class Database 8 | 9 | MAX_DATA ||= Stackeye.configuration.max_data 10 | DATA_PATH ||= File.expand_path('stackeye') 11 | 12 | def initialize(filepath) 13 | @filepath = File.expand_path(filepath) 14 | end 15 | 16 | def get 17 | json = [] 18 | return json unless File.file?(@filepath) 19 | 20 | File.foreach(@filepath).with_index do |line, i| 21 | json << JSON.parse(line) 22 | 23 | break if i == MAX_DATA 24 | end 25 | json 26 | end 27 | 28 | def set(hash) 29 | Dir.mkdir(DATA_PATH) unless File.directory?(DATA_PATH) 30 | 31 | File.open(@filepath, 'a+') do |outfile| 32 | outfile.puts JSON.generate(hash) 33 | end 34 | end 35 | 36 | def truncate 37 | Dir.foreach(DATA_PATH) do |filename| 38 | next if filename.start_with?('.') 39 | 40 | file = "#{DATA_PATH}/#{filename}" 41 | temp = IO.readlines(file)[-MAX_DATA..-1] 42 | next if temp.nil? || (temp.length < MAX_DATA) 43 | 44 | File.open(file, 'w') do |outfile| 45 | temp.each { |line| outfile.puts line } 46 | end 47 | end 48 | end 49 | 50 | class << self 51 | def get(filepath) 52 | klass = new(filepath) 53 | klass.get 54 | end 55 | 56 | def set(filepath, hash) 57 | klass = new(filepath) 58 | klass.set(hash) 59 | end 60 | 61 | def truncate 62 | klass = new('') 63 | klass.truncate 64 | end 65 | end 66 | 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/stackeye/tools/init.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | %w[os cli database].each do |filename| 4 | require_relative filename 5 | end 6 | -------------------------------------------------------------------------------- /lib/stackeye/tools/os.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rbconfig' 4 | 5 | module Stackeye 6 | module Tools 7 | class Os 8 | 9 | HOST_OS ||= { 10 | bsd: /bsd/, 11 | freebsd: /freebsd/, 12 | linux: /linux|cygwin/, 13 | mac: /mac|darwin/, 14 | solaris: /solaris|sunos/, 15 | windows: /mswin|win|mingw/ 16 | }.freeze 17 | 18 | def initialize 19 | @config = RbConfig::CONFIG['host_os'] 20 | end 21 | 22 | def platform 23 | HOST_OS.keys.detect { |name| send("#{name}?") } 24 | end 25 | 26 | HOST_OS.each do |name, regex| 27 | define_method("#{name}?") do 28 | @config =~ regex 29 | end 30 | end 31 | 32 | class << self 33 | def platform 34 | klass = new 35 | klass.platform 36 | end 37 | 38 | HOST_OS.each do |name, _regex| 39 | define_method("#{name}?") do 40 | klass = new 41 | klass.send("#{name}?") 42 | end 43 | end 44 | end 45 | 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/stackeye/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Stackeye 4 | VERSION = '0.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /lib/stackeye/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% if refreshing? %> 12 | 13 | <% end %> 14 |30 | <%= header %> 31 | | 32 | <% end %> 33 |
---|
<%= cell %> | 44 | <% end %> 45 |
9 | The only operating system that is currently supported is Ubuntu. 10 |
11 |