├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── config.ru ├── lib ├── liprug.rb └── liprug │ ├── config.rb │ ├── model.rb │ ├── public │ └── js │ │ └── app.js │ ├── service.rb │ ├── service │ ├── helpers.rb │ └── routes.rb │ ├── version.rb │ └── views │ ├── admin.slim │ └── overview.slim └── liprug.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | InstalledFiles 7 | _yardoc 8 | coverage 9 | doc/ 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in liprug.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | liprug (0.0.2) 5 | rdiscount 6 | redis 7 | sinatra 8 | slim 9 | unicorn 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | kgio (2.11.0) 15 | mustermann (1.0.1) 16 | rack (2.0.3) 17 | rack-protection (2.0.0) 18 | rack 19 | raindrops (0.19.0) 20 | rake (10.1.0) 21 | rdiscount (2.2.0.1) 22 | redis (4.0.1) 23 | sinatra (2.0.0) 24 | mustermann (~> 1.0) 25 | rack (~> 2.0) 26 | rack-protection (= 2.0.0) 27 | tilt (~> 2.0) 28 | slim (3.0.9) 29 | temple (>= 0.7.6, < 0.9) 30 | tilt (>= 1.3.3, < 2.1) 31 | temple (0.8.0) 32 | tilt (2.0.8) 33 | unicorn (5.3.1) 34 | kgio (~> 2.6) 35 | raindrops (~> 0.7) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | bundler (~> 1.3) 42 | liprug! 43 | rake 44 | 45 | BUNDLED WITH 46 | 1.15.4 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Exoscale 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | liprug: simple status board 2 | ---------------------------- 3 | 4 | ![liprug](http://i.imgur.com/nI9l58i.png) 5 | 6 | The idea behind liprug is to give you a simple tool 7 | to communicate your operational status to clients. 8 | 9 | The main objectives behind liprug were: 10 | 11 | * Simple deployment compatible with heroku and AWS 12 | * All-in-one solution 13 | * Simple administration 14 | 15 | ### Deployment 16 | 17 | You can deploy to heroku or any host provider. It is advised to set up liprug to operate 18 | behind SSL only. 19 | 20 | ### Configuration 21 | 22 | Configuration is done through environment variables, here are the relevant ones: 23 | 24 | * `REDIS_URL` or `REDISTOGO_URL`: url of redis host for storage 25 | * `LIPRUG_BRAND_HEADER`: text you want shown on the header bar 26 | * `LIPRUG_BRAND_CONTACT`: if present, a "report issue" button will show up in the bar, with this value as href 27 | * `LIPRUG_BRAND_SCRIPT`: if present, a script to be inserted in the page, to track analytics for instance 28 | * `LIPRUG_CREDENTIALS`: colon separated user and password 29 | 30 | ### Usage 31 | 32 | The main URL shows current service status, the `/admin` URL allows input and modifications 33 | 34 | ### Roadmap 35 | 36 | * Additional authentication methods to admin interface 37 | * Per-service history 38 | * Typed service data to allow graphs 39 | 40 | ### Authors 41 | 42 | liprug is a production of [exoscale](https://exoscale.ch) 43 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $stdout.sync = true 2 | $:.unshift File.dirname(__FILE__) + '/lib' 3 | 4 | require 'liprug/service' 5 | 6 | run Rack::URLMap.new('/' => Liprug::Service) -------------------------------------------------------------------------------- /lib/liprug.rb: -------------------------------------------------------------------------------- 1 | require "liprug/version" 2 | require "liprug/service" 3 | 4 | module Liprug 5 | # Your code goes here... 6 | end 7 | -------------------------------------------------------------------------------- /lib/liprug/config.rb: -------------------------------------------------------------------------------- 1 | module Liprug 2 | class Config 3 | def self.[] index 4 | ENV[index] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/liprug/model.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'json' 3 | require 'uri' 4 | require 'liprug/config' 5 | 6 | module Liprug 7 | class Model 8 | def self.connect! 9 | url = Liprug::Config['REDISTOGO_URL'] 10 | url ||= Liprug::Config['REDIS_URL'] 11 | url ||= "redis://127.0.0.1" 12 | 13 | uri = URI.parse(url) 14 | options = {:host => uri.host} 15 | options[:port] = uri.port if uri.port 16 | options[:password] = uri.password if uri.password 17 | 18 | Redis.new(options) 19 | end 20 | 21 | def self.fetch 22 | redis = connect! 23 | services = redis.smembers 'services' 24 | { 25 | :brand => { 26 | :script => Liprug::Config['LIPRUG_BRAND_SCRIPT'] || '', 27 | :name => Liprug::Config['LIPRUG_BRAND_HEADER'] || 'status board', 28 | :contact => Liprug::Config['LIPRUG_BRAND_CONTACT'] 29 | }, 30 | :services => services.map do |service| 31 | { 32 | service => JSON.parse(redis.get("service:#{service}"), 33 | :symbolize_names => true) 34 | } 35 | end.reduce({}) do |e1,e2| 36 | e1.merge e2 37 | end, 38 | :past => redis.lrange('events_past', 0, -1).map do |event| 39 | JSON.parse event, :symbolize_names => true 40 | end, 41 | :upcoming => redis.lrange('events_upcoming', 0, -1).map do |event| 42 | JSON.parse event, :symbolize_names => true 43 | end 44 | } 45 | end 46 | 47 | def self.add_status service, status 48 | redis = connect! 49 | redis.sadd "services", service 50 | redis.set "service:#{service}", status.to_json 51 | end 52 | 53 | def self.delete_service service 54 | redis = connect! 55 | redis.srem "services", service 56 | redis.del "service:#{service}" 57 | end 58 | 59 | def self.add_upcoming_event event 60 | redis = connect! 61 | event[:date] = Time.new.asctime 62 | event = event.to_json 63 | redis.lpush "events_upcoming", event 64 | end 65 | 66 | def self.add_past_event event 67 | redis = connect! 68 | event[:date] = Time.new.asctime 69 | event = event.to_json 70 | redis.lpush "events_past", event 71 | end 72 | 73 | def self.delete_upcoming_event index 74 | redis = connect! 75 | redis.lset "events_upcoming", index, "XXXXX" 76 | redis.lrem "events_upcoming", 1, "XXXXX" 77 | end 78 | 79 | def self.delete_past_event index 80 | redis = connect! 81 | redis.lset "events_past", index, "XXXXX" 82 | redis.lrem "events_past", 1, "XXXXX" 83 | end 84 | 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/liprug/public/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $("td.delete-service").each(function(index, el) { 3 | var b = $(el).find("button"); 4 | var id = b.attr("id"); 5 | b.click(function (ev) { 6 | ev.preventDefault(); 7 | $.ajax({url: '/api/services/' + id, 8 | type: 'DELETE'}); 9 | }); 10 | }); 11 | 12 | $("td.delete-past-event").each(function(index, el) { 13 | 14 | var b = $(el).find("button"); 15 | var id = b.attr("id"); 16 | b.click(function (ev) { 17 | ev.preventDefault(); 18 | $.ajax({url: '/api/past/' + id, 19 | type: 'DELETE'}); 20 | }); 21 | }); 22 | 23 | $("td.delete-upcoming-event").each(function(index, el) { 24 | 25 | var b = $(el).find("button"); 26 | var id = b.attr("id"); 27 | b.click(function (ev) { 28 | ev.preventDefault(); 29 | $.ajax({url: '/api/upcoming/' + id, 30 | type: 'DELETE'}); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /lib/liprug/service.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | module Liprug 3 | class Service < Sinatra::Base 4 | 5 | require 'liprug/model' 6 | require 'liprug/service/helpers' 7 | require 'liprug/service/routes' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/liprug/service/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'liprug/config' 3 | 4 | module Liprug 5 | class Service < Sinatra::Base 6 | helpers do 7 | 8 | def authorized? 9 | creds = (Liprug::Config['LIPRUG_CREDENTIALS'] || ":").split ":" 10 | @auth ||= Rack::Auth::Basic::Request.new(request.env) 11 | @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == creds 12 | end 13 | 14 | def protected! 15 | return if authorized? 16 | headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' 17 | halt 401, "Not authorized" 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/liprug/service/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'rdiscount' 3 | require 'slim' 4 | require 'liprug/model' 5 | require 'liprug/service/helpers' 6 | 7 | module Liprug 8 | class Service < Sinatra::Base 9 | 10 | get '/' do 11 | slim :overview, :locals => Liprug::Model.fetch 12 | end 13 | 14 | get '/admin' do 15 | protected! 16 | slim :admin, :locals => Liprug::Model.fetch 17 | end 18 | 19 | post '/api/services' do 20 | protected! 21 | Liprug::Model.add_status(params[:name], 22 | { 23 | :class => params[:class], 24 | :message => params[:message] 25 | }) 26 | redirect "/admin" 27 | end 28 | 29 | delete '/api/services/:service' do 30 | protected! 31 | Liprug::Model.delete_service params[:service] 32 | redirect "/admin" 33 | end 34 | 35 | post '/api/past' do 36 | protected! 37 | Liprug::Model.add_past_event({ 38 | :class => params[:class], 39 | :message => params[:message] 40 | }) 41 | redirect "/admin" 42 | end 43 | 44 | post '/api/upcoming' do 45 | protected! 46 | Liprug::Model.add_upcoming_event({ 47 | :class => params[:class], 48 | :message => params[:message] 49 | }) 50 | redirect "/admin" 51 | end 52 | 53 | delete '/api/past/:event' do 54 | protected! 55 | Liprug::Model.delete_past_event params[:event].to_i 56 | 57 | redirect "/admin" 58 | end 59 | 60 | delete '/api/upcoming/:event' do 61 | protected! 62 | Liprug::Model.delete_upcoming_event params[:event].to_i 63 | 64 | redirect "/admin" 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/liprug/version.rb: -------------------------------------------------------------------------------- 1 | module Liprug 2 | VERSION = "0.0.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/liprug/views/admin.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Status Board 5 | meta name="viewport" content="width=device-width, initial-scale=1.0" 6 | link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" 7 | body 8 | 9 | nav.navbar.navbar-default role="navigation" 10 | div.navbar-header 11 | a.navbar-brand href="/" status admin 12 | 13 | div.container 14 | div.row 15 | 16 | div.col-md-8.col-md-offset-2 17 | 18 | h2 Services 19 | 20 | form.form-horizontal role="form" method="POST" action="/api/services" 21 | div.form-group 22 | label.col-lg-2.control-label for="name" Service Name 23 | div.col-lg-6 24 | input.form-control type="text" id="name" name="name" 25 | div.form-group 26 | label.col-lg-2.control-label for="class" Status class 27 | div.col-lg-6 28 | div 29 | input type="radio" id="class" name="class" value="success" success 30 | div 31 | input type="radio" id="class" name="class" value="danger" danger 32 | div 33 | input type="radio" id="class" name="class" value="warning" warning 34 | div.form-group 35 | label.col-lg-2.control-label for="message" Message 36 | div.col-lg-6 37 | input.form-control type="text" id="message" name="message" 38 | div.form-group 39 | div.col-lg-offset-2.col-lg-6 40 | button.btn.btn-default type="submit" update 41 | 42 | table.table.table-bordered.table-striped 43 | tbody 44 | - for name,status in services do 45 | tr 46 | td #{name} 47 | td.delete-service 48 | button.btn.btn-primary.pull-right id="#{name}" 49 | span.glyphicon.glyphicon-trash 50 | | delete 51 | 52 | h2 Upcoming Events 53 | 54 | form.form-horizontal role="form" method="POST" action="/api/upcoming" 55 | div.form-group 56 | label.col-lg-2.control-label for="class" Status class 57 | div.col-lg-6 58 | div 59 | input type="radio" id="event-class" name="class" value="info" info 60 | div 61 | input type="radio" id="event-class" name="class" value="success" success 62 | div 63 | input type="radio" id="event-class" name="class" value="danger" danger 64 | div 65 | input type="radio" id="event-class" name="class" value="warning" warning 66 | div.form-group 67 | label.col-lg-2.control-label for="message" Message 68 | div.col-lg-6 69 | textarea.form-control id="event-message" name="message" 70 | div.form-group 71 | div.col-lg-offset-2.col-lg-6 72 | button.btn.btn-default type="submit" update 73 | 74 | table.table.table-bordered.table-striped 75 | tbody 76 | - for event, index in upcoming.each_with_index do 77 | tr 78 | td #{event[:message]} 79 | td.delete-upcoming-event 80 | button.btn.btn-primary.pull-right id="#{index}" 81 | span.glyphicon.glyphicon-trash 82 | | delete 83 | 84 | h2 Past Events 85 | 86 | form.form-horizontal role="form" method="POST" action="/api/past" 87 | div.form-group 88 | label.col-lg-2.control-label for="class" Status class 89 | div.col-lg-6 90 | div 91 | input type="radio" id="event-class" name="class" value="info" info 92 | div 93 | input type="radio" id="event-class" name="class" value="success" success 94 | div 95 | input type="radio" id="event-class" name="class" value="danger" danger 96 | div 97 | input type="radio" id="event-class" name="class" value="warning" warning 98 | div.form-group 99 | label.col-lg-2.control-label for="message" Message 100 | div.col-lg-6 101 | textarea.form-control id="event-message" name="message" 102 | div.form-group 103 | div.col-lg-offset-2.col-lg-6 104 | button.btn.btn-default type="submit" update 105 | 106 | table.table.table-bordered.table-striped 107 | tbody 108 | - for event, index in past.each_with_index do 109 | tr 110 | td #{event[:message]} 111 | td.delete-past-event 112 | button.btn.btn-primary.pull-right id="#{index}" 113 | span.glyphicon.glyphicon-trash 114 | | delete 115 | 116 | 117 | script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" 118 | script type="text/javascript" src="/js/app.js" 119 | -------------------------------------------------------------------------------- /lib/liprug/views/overview.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Status Board 5 | meta name="viewport" content="width=device-width, initial-scale=1.0" 6 | link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" 7 | body 8 | 9 | style type="text/css" 10 | | .bs-callout { 11 | | margin: 20px 0; 12 | | padding: 15px 30px 15px 15px; 13 | | border-left: 5px solid #eee; 14 | | } 15 | | .bs-callout h4 { margin-top: 0; } 16 | | .bs-callout p:last-child { margin-bottom: 0; } 17 | | .bs-callout code, .bs-callout .highlight {background-color: #fff;} 18 | | .bs-callout-danger {background-color: #fcf2f2; border-color: #dFb5b4;} 19 | | .bs-callout-warning {background-color: #fefbed; border-color: #f1e7bc;} 20 | | .bs-callout-info {background-color: #f0f7fd; border-color: #d0e3f0;} 21 | | .bs-callout-success {background-color: #dff0d8; border-color: #d6e9c6;} 22 | 23 | == brand[:script] 24 | 25 | nav.navbar.navbar-default role="navigation" 26 | div.navbar-header 27 | button.navbar-toggle data-toggle="collapse" data-target=".navbar-main-collapse" 28 | span.sr-only Toggle Navigation 29 | span.icon-bar 30 | span.icon-bar 31 | span.icon-bar 32 | a.navbar-brand href="/" #{brand[:name]} 33 | 34 | - if brand[:contact] 35 | nav.collapse.navbar-collapse.navbar-main-collapse role="navigation" 36 | a.navbar-btn.navbar-right.btn.btn-info.btn-sm href="#{brand[:contact]}" Report issue 37 | 38 | div.container 39 | div.row 40 | div.col-md-8.col-md-offset-2 41 | h2 Services 42 | 43 | table.table.table-bordered.table-striped 44 | thead 45 | tr 46 | th Service 47 | th Status 48 | tbody 49 | - for name in services.keys.sort do 50 | tr 51 | td #{name} 52 | td class="#{services[name][:class]}" 53 | | #{services[name][:message]} 54 | 55 | h2 Upcoming Events 56 | - for event in upcoming do 57 | div class="bs-callout bs-callout-#{event[:class]}" 58 | p 59 | small 60 | i On #{event[:date]} 61 | div 62 | == RDiscount.new(event[:message]).to_html 63 | h2 Latest Events 64 | - for event in past do 65 | div class="bs-callout bs-callout-#{event[:class]}" 66 | p 67 | small 68 | i On #{event[:date]} 69 | div 70 | == RDiscount.new(event[:message]).to_html 71 | script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" 72 | script type="text/javascript" src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js" 73 | 74 | -------------------------------------------------------------------------------- /liprug.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'liprug/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "liprug" 8 | spec.version = Liprug::VERSION 9 | spec.authors = ["Pierre-Yves Ritschard"] 10 | spec.email = ["pyr@spootnik.org"] 11 | spec.description = %q{simple operational dashboard} 12 | spec.summary = %q{operational dashboard} 13 | spec.homepage = "https://github.com/exoscale/liprug" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "sinatra" 22 | spec.add_dependency "slim" 23 | spec.add_dependency "redis" 24 | spec.add_dependency "rdiscount" 25 | spec.add_dependency "unicorn" 26 | 27 | spec.add_development_dependency "bundler", "~> 1.3" 28 | spec.add_development_dependency "rake" 29 | end 30 | --------------------------------------------------------------------------------