├── .env ├── .gitignore ├── .travis.yml ├── API.md ├── CHANGELOG.md ├── Gemfile ├── Procfile ├── README.md ├── Rakefile ├── bin └── fluentd-server ├── config.ru ├── config └── unicorn.conf ├── data ├── .gitignore └── .gitkeep ├── db ├── migrate │ ├── 0001_create_posts.rb │ ├── 0002_create_delayed_jobs.rb │ └── 0003_create_tasks.rb └── schema.rb ├── fluentd-server.gemspec ├── jobs └── .gitkeep ├── lib ├── fluentd_server.rb └── fluentd_server │ ├── cli.rb │ ├── config.rb │ ├── decorator.rb │ ├── environment.rb │ ├── logger.rb │ ├── model.rb │ ├── sync_runner.rb │ ├── sync_worker.rb │ ├── task_runner.rb │ ├── version.rb │ ├── web.rb │ └── web_helper.rb ├── log └── .gitkeep ├── public ├── css │ └── bootstrap.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── js │ ├── bootstrap.min.js │ ├── jquery-1.10.2.min.js │ ├── jquery-1.10.2.min.map │ └── jquery.storageapi.min.js ├── spec ├── bench │ ├── bench.rb │ └── result.md ├── model_spec.rb ├── spec_helper.rb ├── sync_runner_spec.rb ├── task_runner_spec.rb ├── tmp │ └── .gitkeep ├── web_helper_spec.rb └── web_spec.rb └── views ├── _js.slim ├── _navbar.slim ├── _style.slim ├── layout.slim ├── posts ├── create.slim ├── edit.slim ├── layout.slim └── menu.slim └── tasks ├── ajax.erb └── show.slim /.env: -------------------------------------------------------------------------------- 1 | PORT=5126 2 | HOST=0.0.0.0 3 | # DATABASE_URL=sqlite3:data/fluentd_server.db 4 | # JOB_DIR=jobs 5 | # LOG_PATH=STDOUT 6 | # LOG_LEVEL=debug 7 | # LOG_SHIFT_AGE=0 8 | # LOG_SHIFT_SIZE=1048576 9 | # FILE_STORAGE=false 10 | # DATA_DIR=data 11 | # SYNC_INTERVAL=60 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.gem 2 | ~* 3 | #* 4 | *~ 5 | .bundle 6 | Gemfile.lock 7 | .rbenv-version 8 | vendor 9 | doc/* 10 | tmp/* 11 | coverage 12 | .yardoc 13 | .ruby-version 14 | pkg/* 15 | data/* 16 | log/* 17 | fluentd-server/* 18 | spec/tmp 19 | jobs/* 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.0.0 3 | - 2.1 4 | gemfile: 5 | - Gemfile 6 | env: 7 | - FILE_STORAGE=false 8 | - FILE_STORAGE=true DATA_DIR=spec/tmp 9 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | ### GET /api/:name 4 | 5 | Get the contents of Fluentd config whose name is :name. 6 | Query parameters are replaced with variables in erb. 7 | 8 | Supported query parameter formats are: 9 | 10 | * var=value 11 | 12 | * The variable `var` is replaced with its value in erb. 13 | 14 | * var[]=value1&var[]=value2 15 | 16 | * Array. The variable `var[idx]` such as `var[0]` and `var[1]` is replaced with its value in erb. 17 | 18 | * var[key1]=value1&var[key2]=value2 19 | 20 | * Hash. The variable `var[key]` such as `var['key1']` and `var['key2']` is replaced with its value in erb. 21 | 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.2 (2014/06/08) 2 | 3 | Changes: 4 | 5 | * Rename `LOCAL_STORAGE` to `FILE_STORAGE` 6 | 7 | # 0.3.1 8 | 9 | Enhancements: 10 | 11 | * Add `fluentd-server sync` command 12 | * Add `fluentd-server td-agent-start` command 13 | * Add `fluentd-server td-agent-stop` command 14 | * Add `fluentd-server td-agent-reload` command 15 | * Add `fluentd-server td-agent-restart` command 16 | * Add `fluentd-server td-agent-condrestart` command 17 | * Add `fluentd-server td-agent-status` command 18 | * Add `fluentd-server td-agent-configtest` command 19 | 20 | Changes: 21 | 22 | * Rename `fluentd-server sync` command to `fluentd-server sync-worker` 23 | * Rename `fluentd-server job` command to `fluentd-server job-worker` 24 | * Rename `fluentd-server job_clear` command to `fluentd-server job-clean` 25 | 26 | # 0.3.0 27 | 28 | Enhancements: 29 | 30 | * Add sync_worker 31 | 32 | Changes: 33 | 34 | * Use env LOCAL_STORAGE rather than DATA_DIR to enable local file storage feature 35 | 36 | # 0.2.0 37 | 38 | Enhancements: 39 | 40 | * Support restarting td-agent via serf 41 | * Created serf-td-agent gem 42 | * Introduce Resque 43 | * Ajax reload of command result 44 | * Save/load the content of configuration into/from a file (experimental) 45 | * Created acts_as_file gem 46 | 47 | # 0.1.0 48 | 49 | First version 50 | 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | gem 'sqlite3' 5 | 6 | group :development, :test do 7 | gem 'coveralls', :require => false 8 | gem 'parallel' 9 | end 10 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -E production -p $PORT -o $HOST -c config/unicorn.conf 2 | job: bundle exec bin/fluentd-server job-worker 3 | sync: bundle exec bin/fluentd-server sync-worker 4 | serf: $(bundle exec gem path serf-td-agent)/bin/serf agent 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluentd Server 2 | 3 | [![Build Status](https://secure.travis-ci.org/sonots/fluentd-server.png?branch=master)](http://travis-ci.org/sonots/fluentd-server) 4 | [![Coverage Status](https://coveralls.io/repos/sonots/fluentd-server/badge.png?branch=master)](https://coveralls.io/r/sonots/fluentd-server?branch=master) 5 | 6 | A Fluentd config distribution server 7 | 8 | Demo: [http://fluentd-server.herokuapp.com](http://fluentd-server.herokuapp.com) 9 | 10 | ## What You Can Do 11 | 12 | With Fluentd Server, you can manage fluentd configuration files centrally with `erb`. 13 | 14 | For example, you may create a config post whose name is `worker` as: 15 | 16 | ``` 17 | 18 | type forward 19 | port <%= port %> 20 | 21 | 22 | 23 | type stdout 24 | 25 | ``` 26 | 27 | Then you can download the config via an API whose uri is like `/api/worker?port=24224` where its query parameters are replaced with variables in the erb. 28 | The downloaded contents should become as follows: 29 | 30 | ``` 31 | 32 | type forward 33 | port 24224 34 | 35 | 36 | 37 | type stdout 38 | 39 | ``` 40 | 41 | ## How to Use 42 | 43 | The `include` directive of fluentd config supports `http`, so write just one line on your fluentd.conf as: 44 | 45 | ``` 46 | # /etc/fluentd.conf 47 | include http://fqdn.to.fluentd-server/api/:name?port=24224 48 | ``` 49 | 50 | so that it will download the real configuration from the Fluentd Server where :name is the name of your post. 51 | 52 | ## Installation 53 | 54 | Prerequisites 55 | 56 | * SQLite 57 | * Ruby 2.0 or later 58 | 59 | ### From Gem package 60 | 61 | Easy steps on installation with gem and SQLite. 62 | 63 | ```bash 64 | $ gem install fluentd-server 65 | $ gem install sqlite3 66 | $ fluentd-server new 67 | $ cd fluentd-server 68 | $ fluentd-server init # creates database scheme on SQLite 69 | $ fluentd-server start 70 | ``` 71 | 72 | Then see `http://localhost:5126/`. 73 | 74 | ### From Git repository 75 | 76 | Install from git repository. 77 | 78 | ```bash 79 | $ git clone https://github.com/sonots/fluentd-server.git 80 | $ cd fluentd-server 81 | $ bundle 82 | $ bundle exec fluentd-server init # creates database scheme on SQLite 83 | $ bundle exec fluentd-server start 84 | ``` 85 | 86 | Then see `http://localhost:5126/`. 87 | 88 | ## Configuration 89 | 90 | To configure fluentd-server, edit the `.env` file in the project root directory. 91 | 92 | The default configuration is as follows: 93 | 94 | ``` 95 | PORT=5126 96 | HOST=0.0.0.0 97 | # DATABASE_URL=sqlite3:data/fluentd_server.db 98 | # JOB_DIR=jobs 99 | # LOG_PATH=STDOUT 100 | # LOG_LEVEL=debug 101 | # LOG_SHIFT_AGE=0 102 | # LOG_SHIFT_SIZE=1048576 103 | ``` 104 | 105 | ## HTTP API 106 | 107 | See [API.md](API.md). 108 | 109 | ### Use Fluentd Server from Command Line 110 | 111 | For the case you want to edit Fluentd configuration files from your favorite editors rather than from the Web UI, `FILE STORAGE` feature is available. 112 | With this feature, you should also be able to manage your configuration files with git (or any VCS). 113 | 114 | To use this feature, enable `FILE_STORAGE` in `.env` file as: 115 | 116 | ``` 117 | FILE_STORAGE=true 118 | DATA_DIR=data 119 | SYNC_INTERVAL=60 120 | ``` 121 | 122 | where the `DATA_DIR` is the location to place your configuration files locally, and the `SYNC_INTERVAL` is the interval where a synchronization worker works. 123 | 124 | Place your `erb` files in the `DATA_DIR` directory, and please execute `sync` command to synchronize the file existence information with DB 125 | when you newly add or remove the configuration files. 126 | 127 | ``` 128 | $ fluentd-server sync 129 | ``` 130 | 131 | Or, you may just wait `SYNC_INTERVAL` senconds until a synchronization worker automatically synchronizes the information. 132 | Please note that modifying the file content does not require to synchronize because the content is read from the local file directly. 133 | 134 | NOTE: Enabling this feature disables to edit the Fluentd configuration from the Web UI. 135 | 136 | ### CLI (Command Line Interface) 137 | 138 | Here is a full list of fluentd-server commands. 139 | 140 | ```bash 141 | $ fluentd-server help 142 | Commands: 143 | fluentd-server help [COMMAND] # Describe available commands or one specific command 144 | fluentd-server init # Creates database schema 145 | fluentd-server job-clean # Clean fluentd_server delayed_job queue 146 | fluentd-server job-worker # Sartup fluentd_server job worker 147 | fluentd-server migrate # Migrate database schema 148 | fluentd-server new # Creates fluentd-server resource directory 149 | fluentd-server start # Sartup fluentd_server 150 | fluentd-server sync # Synchronize local file storage with db immediately 151 | fluentd-server sync-worker # Sartup fluentd_server sync worker 152 | fluentd-server td-agent-condrestart # Run `/etc/init.d/td-agent condrestart` via serf event 153 | fluentd-server td-agent-configtest # Run `/etc/init.d/td-agent configtest` via serf query 154 | fluentd-server td-agent-reload # Run `/etc/init.d/td-agent reload` via serf event 155 | fluentd-server td-agent-restart # Run `/etc/init.d/td-agent restart` via serf event 156 | fluentd-server td-agent-start # Run `/etc/init.d/td-agent start` via serf event 157 | fluentd-server td-agent-status # Run `/etc/init.d/td-agent status` via serf query 158 | fluentd-server td-agent-stop # Run `/etc/init.d/td-agent stop` via serf event 159 | ``` 160 | 161 | ## ToDo 162 | 163 | * Automatic deployment (restart) support like the one of chef-server 164 | 165 | * Need a notification function for when configtest or restart failed 166 | * Pipe the resulted json to a command (I may prepare email.rb as an example). 167 | 168 | ## ChangeLog 169 | 170 | See [CHANGELOG.md](CHANGELOG.md) for details. 171 | 172 | ## Contributing 173 | 174 | 1. Fork it 175 | 2. Create your feature branch (`git checkout -b my-new-feature`) 176 | 3. Commit your changes (`git commit -am 'Add some feature'`) 177 | 4. Push to the branch (`git push origin my-new-feature`) 178 | 5. Create new [Pull Request](../../pull/new/master) 179 | 180 | ## Copyright 181 | 182 | Copyright (c) 2014 Naotoshi Seo. See [LICENSE](LICENSE) for details. 183 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dotenv/tasks' 5 | require 'fluentd_server/environment' 6 | 7 | require 'rspec/core/rake_task' 8 | RSpec::Core::RakeTask.new(:spec) do |t| 9 | t.rspec_opts = ["-c", "-f progress"] # '--format specdoc' 10 | t.pattern = 'spec/**/*_spec.rb' 11 | end 12 | task :test => :spec 13 | task :default => :spec 14 | 15 | task :console => :dotenv do 16 | require "fluentd_server" 17 | require 'irb' 18 | # require 'irb/completion' 19 | ARGV.clear 20 | IRB.start 21 | end 22 | task :c => :console 23 | 24 | require 'sinatra/activerecord/rake' 25 | -------------------------------------------------------------------------------- /bin/fluentd-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path("../../lib", __FILE__) 4 | 5 | require "fluentd_server/cli" 6 | 7 | FluentdServer::CLI.start 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "sinatra" 3 | require "fluentd_server" 4 | require "fluentd_server/web" 5 | 6 | ## Unicorn self-process killer 7 | #require 'unicorn/worker_killer' 8 | # 9 | ## Max requests per worker 10 | #use Unicorn::WorkerKiller::MaxRequests, 3072, 4096 11 | # 12 | ## Max memory size (RSS) per worker 13 | #use Unicorn::WorkerKiller::Oom, (192*(1024**2)), (256*(1024**2)) 14 | 15 | run FluentdServer::Web 16 | -------------------------------------------------------------------------------- /config/unicorn.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1 2 | # timeout 6000 3 | # preload_app true 4 | 5 | GC.respond_to?(:copy_on_write_friendly=) and GC.copy_on_write_friendly = true 6 | 7 | before_fork do |server, worker| 8 | defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! 9 | 10 | old_pid = "#{server.config[:pid]}.oldbin" 11 | if old_pid != server.pid 12 | begin 13 | sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU 14 | Process.kill(sig, File.read(old_pid).to_i) 15 | rescue Errno::ENOENT, Errno::ESRCH 16 | end 17 | end 18 | 19 | sleep 1 20 | end 21 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/data/.gitkeep -------------------------------------------------------------------------------- /db/migrate/0001_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration 2 | def self.up 3 | create_table :posts do |t| 4 | t.string :name 5 | t.text :body 6 | t.timestamps 7 | end 8 | add_index :posts, :name, length: 255, unique: true # explicit length is required for MySQL 9 | end 10 | 11 | def self.down 12 | drop_table :posts 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/0002_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs do |table| 4 | table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. 6 | table.text :handler, :null => false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/0003_create_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateTasks < ActiveRecord::Migration 2 | def self.up 3 | create_table :tasks do |t| 4 | t.string :name 5 | t.text :body 6 | t.integer :exit_code 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :tasks 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 3) do 15 | 16 | create_table "delayed_jobs", force: true do |t| 17 | t.integer "priority", default: 0, null: false 18 | t.integer "attempts", default: 0, null: false 19 | t.text "handler", null: false 20 | t.text "last_error" 21 | t.datetime "run_at" 22 | t.datetime "locked_at" 23 | t.datetime "failed_at" 24 | t.string "locked_by" 25 | t.string "queue" 26 | t.datetime "created_at" 27 | t.datetime "updated_at" 28 | end 29 | 30 | add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority" 31 | 32 | create_table "posts", force: true do |t| 33 | t.string "name" 34 | t.text "body" 35 | t.datetime "created_at" 36 | t.datetime "updated_at" 37 | end 38 | 39 | add_index "posts", ["name"], name: "index_posts_on_name", unique: true 40 | 41 | create_table "tasks", force: true do |t| 42 | t.string "name" 43 | t.text "body" 44 | t.integer "exit_code" 45 | t.datetime "created_at" 46 | t.datetime "updated_at" 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /fluentd-server.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'fluentd_server/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "fluentd-server" 8 | spec.version = FluentdServer::VERSION 9 | spec.authors = ["Naotoshi Seo"] 10 | spec.email = ["sonots@gmail.com"] 11 | spec.description = %q{Fluentd config distribution server} 12 | spec.summary = spec.description 13 | spec.homepage = "https://github.com/sonots/fluentd-server" 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_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rspec", "~> 3" 23 | spec.add_development_dependency "capybara" 24 | spec.add_development_dependency "pry" 25 | spec.add_development_dependency "pry-nav" 26 | spec.add_development_dependency "rake" 27 | 28 | spec.add_runtime_dependency "dotenv" 29 | spec.add_runtime_dependency "foreman" 30 | spec.add_runtime_dependency "thor" 31 | 32 | spec.add_runtime_dependency "sinatra" 33 | spec.add_runtime_dependency "activerecord" 34 | spec.add_runtime_dependency "sinatra-contrib" 35 | spec.add_runtime_dependency "sinatra-activerecord" 36 | spec.add_runtime_dependency 'sinatra-flash' 37 | spec.add_runtime_dependency 'sinatra-redirect-with-flash' 38 | spec.add_runtime_dependency 'sinatra-decorator' 39 | spec.add_runtime_dependency 'slim' 40 | spec.add_runtime_dependency "unicorn" 41 | spec.add_runtime_dependency "unicorn-worker-killer" 42 | spec.add_runtime_dependency "delayed_job_active_record" 43 | spec.add_runtime_dependency "daemons" 44 | spec.add_runtime_dependency "serf-td-agent" 45 | spec.add_runtime_dependency "acts_as_file" 46 | # spec.add_runtime_dependency 'sqlite3' 47 | end 48 | -------------------------------------------------------------------------------- /jobs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/jobs/.gitkeep -------------------------------------------------------------------------------- /lib/fluentd_server.rb: -------------------------------------------------------------------------------- 1 | require "fluentd_server/version" 2 | -------------------------------------------------------------------------------- /lib/fluentd_server/cli.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "dotenv" 3 | require "thor" 4 | require 'fluentd_server' 5 | 6 | class FluentdServer::CLI < Thor 7 | BASE_DIR = File.join(Dir.pwd, "fluentd-server") 8 | DATA_DIR = File.join(BASE_DIR, "data") 9 | LOG_DIR = File.join(BASE_DIR, "log") 10 | JOB_DIR = File.join(BASE_DIR, "jobs") 11 | LOG_FILE = File.join(LOG_DIR, "application.log") 12 | ENV_FILE = File.join(BASE_DIR, ".env") 13 | PROCFILE = File.join(BASE_DIR, "Procfile") 14 | CONFIGRU_FILE = File.join(BASE_DIR, "config.ru") 15 | CONFIG_DIR= File.join(BASE_DIR, "config") 16 | 17 | DEFAULT_DOTENV =<<-EOS 18 | PORT=5126 19 | HOST=0.0.0.0 20 | DATABASE_URL=sqlite3:#{DATA_DIR}/fluentd_server.db 21 | JOB_DIR=#{JOB_DIR} 22 | LOG_PATH=#{LOG_FILE} 23 | LOG_LEVEL=warn 24 | LOG_SHIFT_AGE=0 25 | LOG_SHIFT_SIZE=1048576 26 | FILE_STORAGE=false 27 | DATA_DIR=#{DATA_DIR} 28 | SYNC_INTERVAL=60 29 | EOS 30 | 31 | DEFAULT_PROCFILE =<<-EOS 32 | web: unicorn -E production -p $PORT -o $HOST -c config/unicorn.conf 33 | job: fluentd-server job-worker 34 | sync: fluentd-server sync-worker 35 | serf: $(gem path serf-td-agent)/bin/serf agent 36 | EOS 37 | 38 | default_command :start 39 | 40 | def initialize(args = [], opts = [], config = {}) 41 | super(args, opts, config) 42 | end 43 | 44 | desc "new", "Creates fluentd-server resource directory" 45 | def new 46 | FileUtils.mkdir_p(LOG_DIR) 47 | FileUtils.mkdir_p(JOB_DIR) 48 | File.write ENV_FILE, DEFAULT_DOTENV 49 | File.write PROCFILE, DEFAULT_PROCFILE 50 | FileUtils.cp(File.expand_path("../../../config.ru", __FILE__), CONFIGRU_FILE) 51 | FileUtils.cp_r(File.expand_path("../../../config", __FILE__), CONFIG_DIR) 52 | puts 'fluentd-server new finished.' 53 | end 54 | 55 | desc "init", "Creates database schema" 56 | def init 57 | Dotenv.load 58 | require 'fluentd_server/environment' 59 | require 'rake' 60 | require 'sinatra/activerecord/rake' 61 | ActiveRecord::Tasks::DatabaseTasks.db_dir = File.expand_path("../../../db", __FILE__) 62 | ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [File.expand_path("../../../db/migrate", __FILE__)] 63 | # ToDo: Fix that db:migrate raises an error in the case of sqlite3 like 64 | # SQLite3::SQLException: database schema has changed: INSERT INTO "schema_migrations" ("version") VALUES (?) 65 | # Rake::Task['db:migrate'].invoke 66 | # Use db:schema:load after generating db/schema.rb by executing db:migrate several times for now 67 | Rake::Task['db:schema:load'].invoke 68 | puts 'fluentd-server init finished.' 69 | end 70 | 71 | desc "migrate", "Migrate database schema" 72 | def migrate 73 | Dotenv.load 74 | require 'fluentd_server/environment' 75 | require 'rake' 76 | require 'sinatra/activerecord/rake' 77 | ActiveRecord::Tasks::DatabaseTasks.db_dir = File.expand_path("../../../db", __FILE__) 78 | ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [File.expand_path("../../../db/migrate", __FILE__)] 79 | Rake::Task['db:migrate'].invoke 80 | end 81 | 82 | desc "start", "Sartup fluentd_server" 83 | def start 84 | self.migrate # do migration before start not to forget on updation 85 | require "foreman/cli" 86 | Foreman::CLI.new.invoke(:start, [], {}) 87 | end 88 | 89 | # reference: https://gist.github.com/robhurring/732327 90 | desc "job-worker", "Sartup fluentd_server job worker" 91 | def job_worker 92 | Dotenv.load 93 | require 'delayed_job' 94 | require 'fluentd_server/model' 95 | worker_options = { 96 | :min_priority => ENV['MIN_PRIORITY'], 97 | :max_priority => ENV['MAX_PRIORITY'], 98 | :queues => (ENV['QUEUES'] || ENV['QUEUE'] || '').split(','), 99 | :quiet => false 100 | } 101 | Delayed::Worker.new(worker_options).start 102 | end 103 | 104 | desc "job-clean", "Clean fluentd_server delayed_job queue" 105 | def job_clean 106 | Dotenv.load 107 | require 'delayed_job' 108 | require 'fluentd_server/model' 109 | Delayed::Job.delete_all 110 | end 111 | 112 | desc "sync-worker", "Sartup fluentd_server sync worker" 113 | def sync_worker 114 | Dotenv.load 115 | require 'fluentd_server/sync_worker' 116 | FluentdServer::SyncWorker.start 117 | end 118 | 119 | desc "sync", "Synchronize local file storage with db immediately" 120 | def sync 121 | Dotenv.load 122 | require 'fluentd_server/sync_runner' 123 | FluentdServer::SyncRunner.run 124 | end 125 | 126 | desc "td-agent-start", "Run `/etc/init.d/td-agent start` via serf event" 127 | def td_agent_start 128 | Dotenv.load 129 | require 'fluentd_server/model' 130 | system("#{::Task.serf_path} event td-agent-start") 131 | end 132 | 133 | desc "td-agent-stop", "Run `/etc/init.d/td-agent stop` via serf event" 134 | def td_agent_stop 135 | Dotenv.load 136 | require 'fluentd_server/model' 137 | system("#{::Task.serf_path} event td-agent-stop") 138 | end 139 | 140 | desc "td-agent-reload", "Run `/etc/init.d/td-agent reload` via serf event" 141 | def td_agent_reload 142 | Dotenv.load 143 | require 'fluentd_server/model' 144 | system("#{::Task.serf_path} event td-agent-reload") 145 | end 146 | 147 | desc "td-agent-restart", "Run `/etc/init.d/td-agent restart` via serf event" 148 | def td_agent_restart 149 | Dotenv.load 150 | require 'fluentd_server/model' 151 | # ::Task.create_and_delete(name: 'Restart').restart # using delayed_job 152 | system("#{::Task.serf_path} event td-agent-restart") 153 | end 154 | 155 | desc "td-agent-condrestart", "Run `/etc/init.d/td-agent condrestart` via serf event" 156 | def td_agent_condrestart 157 | Dotenv.load 158 | require 'fluentd_server/model' 159 | system("#{::Task.serf_path} event td-agent-condrestart") 160 | end 161 | 162 | desc "td-agent-status", "Run `/etc/init.d/td-agent status` via serf query" 163 | def td_agent_status 164 | Dotenv.load 165 | require 'fluentd_server/model' 166 | # ::Task.create_and_delete(name: 'Status').status # using delayed_job 167 | system("#{::Task.serf_path} query td-agent-status") 168 | end 169 | 170 | desc "td-agent-configtest", "Run `/etc/init.d/td-agent configtest` via serf query" 171 | def td_agent_configtest 172 | Dotenv.load 173 | require 'fluentd_server/model' 174 | # ::Task.create_and_delete(name: 'Configtest').configtest # using delayed_job 175 | system("#{::Task.serf_path} query td-agent-configtest") 176 | end 177 | 178 | no_tasks do 179 | def abort(msg) 180 | $stderr.puts msg 181 | exit 1 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/fluentd_server/config.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/activerecord' 3 | require 'fluentd_server' 4 | require 'dotenv' 5 | Dotenv.load 6 | 7 | module FluentdServer::Config 8 | def self.database_url 9 | ENV.fetch('DATABASE_URL', 'sqlite3:data/fluentd_server.db') 10 | end 11 | 12 | def self.job_dir 13 | ENV.fetch('JOB_DIR', 'jobs') 14 | end 15 | 16 | def self.log_path 17 | ENV.fetch('LOG_PATH', 'STDOUT') 18 | end 19 | 20 | def self.log_level 21 | ENV.fetch('LOG_LEVEL', 'debug') 22 | end 23 | 24 | def self.log_shift_age 25 | ENV.fetch('LOG_SHIFT_AGE', '0') 26 | end 27 | 28 | def self.log_shift_size 29 | ENV.fetch('LOG_SHIFT_SIZE', '1048576') 30 | end 31 | 32 | def self.task_max_num 33 | ENV.fetch('TASK_MAX_NUM', '20').to_i 34 | end 35 | 36 | def self.file_storage 37 | ENV.fetch('FILE_STORAGE', 'false') == 'true' ? true : false 38 | end 39 | 40 | def self.data_dir 41 | ENV.fetch('DATA_DIR', 'data') 42 | end 43 | 44 | def self.sync_interval 45 | ENV.fetch('SYNC_INTERVAL', '60').to_i 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/fluentd_server/decorator.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/decorator' 2 | require 'fluentd_server/web_helper' 3 | 4 | class PostDecorator < Sinatra::Decorator::Base 5 | include FluentdServer::WebHelper 6 | 7 | def success_message 8 | 'Success!' 9 | end 10 | 11 | def error_message 12 | message = 'Failure! ' 13 | message += self.errors.map {|key, msg| escape_html("`#{key}` #{msg}") }.join('. ') 14 | message 15 | end 16 | 17 | def render_body(locals) 18 | namespace = OpenStruct.new(locals) 19 | ERB.new(self.body, nil, "-").result(namespace.instance_eval { binding }) 20 | end 21 | 22 | def link_to 23 | %Q[ 24 |   ##{h(self.id)} #{h(self.name)} 25 | ] 26 | end 27 | 28 | def create_button 29 | %Q[
30 | Create Config
] 32 | end 33 | end 34 | 35 | class TaskDecorator < Sinatra::Decorator::Base 36 | include FluentdServer::WebHelper 37 | 38 | def link_to 39 | %Q[ 40 |   ##{h(self.id)} #{h(self.name)} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/fluentd_server/environment.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/activerecord' 3 | require 'fluentd_server/config' 4 | require 'fluentd_server/logger' 5 | 6 | ROOT = File.expand_path('../../..', __FILE__) 7 | 8 | configure do 9 | set :show_exceptions, true 10 | ActiveRecord::Base.logger = FluentdServer.logger 11 | I18n.enforce_available_locales = false 12 | end 13 | 14 | configure :production, :development do 15 | if FluentdServer::Config.database_url.start_with?('sqlite') 16 | set :database, FluentdServer::Config.database_url 17 | else 18 | # DATABASE_URL => "postgres://randuser:randpass@randhost:randport/randdb" on heroku 19 | db = URI.parse(FluentdServer::Config.database_url) 20 | ActiveRecord::Base.establish_connection( 21 | :adapter => db.scheme == 'postgres' ? 'postgresql' : db.scheme, 22 | :host => db.host, 23 | :username => db.user, 24 | :password => db.password, 25 | :database => db.path[1..-1], 26 | :encoding => 'utf8' 27 | ) 28 | end 29 | end 30 | 31 | configure :test do 32 | ActiveRecord::Base.establish_connection( 33 | :adapter => 'sqlite3', 34 | :database => ':memory:' 35 | ) 36 | end 37 | 38 | # Configure DelayedJob 39 | require 'delayed_job' 40 | configure do 41 | Delayed::Worker.backend = :active_record # This defines Delayed::Job model 42 | Delayed::Worker.logger = FluentdServer.logger 43 | end 44 | 45 | configure :development, :test do 46 | Delayed::Worker.destroy_failed_jobs = true 47 | Delayed::Worker.sleep_delay = 5 48 | Delayed::Worker.max_attempts = 5 49 | Delayed::Worker.max_run_time = 5.minutes 50 | end 51 | -------------------------------------------------------------------------------- /lib/fluentd_server/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'fluentd_server' 3 | require "fluentd_server/config" 4 | 5 | module FluentdServer 6 | module Logger 7 | def self.included(klass) 8 | # To define logger *class* method 9 | klass.extend(self) 10 | end 11 | 12 | # for test 13 | def logger=(logger) 14 | FluentdServer.logger = logger 15 | end 16 | 17 | def logger 18 | FluentdServer.logger 19 | end 20 | end 21 | 22 | # for test 23 | def self.logger=(logger) 24 | @logger = logger 25 | end 26 | 27 | def self.logger 28 | return @logger if @logger 29 | 30 | log_path = FluentdServer::Logger::Config.log_path 31 | log_level = FluentdServer::Logger::Config.log_level 32 | # NOTE: Please note that ruby 2.0.0's Logger has a problem on log rotation. 33 | # Update to ruby 2.1.0. See https://github.com/ruby/ruby/pull/428 for details. 34 | log_shift_age = FluentdServer::Logger::Config.log_shift_age 35 | log_shift_size = FluentdServer::Logger::Config.log_shift_size 36 | @logger = ::Logger.new(log_path, log_shift_age, log_shift_size) 37 | @logger.level = log_level 38 | @logger 39 | end 40 | 41 | class Logger::Config 42 | def self.log_path(log_path = FluentdServer::Config.log_path) 43 | case log_path 44 | when 'STDOUT' 45 | $stdout 46 | when 'STDERR' 47 | $stderr 48 | else 49 | log_path 50 | end 51 | end 52 | 53 | def self.log_level(log_level = FluentdServer::Config.log_level) 54 | case log_level 55 | when 'debug' 56 | ::Logger::DEBUG 57 | when 'info' 58 | ::Logger::INFO 59 | when 'warn' 60 | ::Logger::WARN 61 | when 'error' 62 | ::Logger::ERROR 63 | when 'fatal' 64 | ::Logger::FATAL 65 | else 66 | raise ArgumentError, "invalid log_level #{log_level}" 67 | end 68 | end 69 | 70 | def self.log_shift_age(log_shift_age = FluentdServer::Config.log_shift_age) 71 | case log_shift_age 72 | when /\d+/ 73 | log_shift_age.to_i 74 | when 'daily' 75 | log_shift_age 76 | when 'weekly' 77 | log_shift_age 78 | when 'monthly' 79 | log_shift_age 80 | else 81 | raise ArgumentError, "invalid log_shift_age #{log_shift_age}" 82 | end 83 | end 84 | 85 | def self.log_shift_size(log_shift_size = FluentdServer::Config.log_shift_size) 86 | log_shift_size.to_i 87 | end 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /lib/fluentd_server/model.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/activerecord' 2 | require 'sinatra/decorator' 3 | require 'fluentd_server/environment' 4 | require 'fluentd_server/task_runner' 5 | require 'acts_as_file' 6 | 7 | class Delayed::Job < ActiveRecord::Base; end 8 | 9 | class Post < ActiveRecord::Base 10 | include Sinatra::Decorator::Decoratable 11 | include FluentdServer::Logger 12 | 13 | validates :name, presence: true 14 | 15 | if FluentdServer::Config.file_storage 16 | include ActsAsFile 17 | 18 | def filename 19 | File.join(FluentdServer::Config.data_dir, "#{self.name}.erb") if self.name 20 | end 21 | 22 | acts_as_file :body => self.instance_method(:filename) 23 | end 24 | 25 | def new? 26 | self.id.nil? 27 | end 28 | end 29 | 30 | class Task < ActiveRecord::Base 31 | include Sinatra::Decorator::Decoratable 32 | include FluentdServer::Logger 33 | include ActsAsFile 34 | include TaskRunner # task runnable codes are here 35 | 36 | def filename 37 | prefix = "#{self.id.to_s.rjust(4, '0')}" if self.id 38 | File.join(FluentdServer::Config.job_dir, "#{prefix}_result.txt") if prefix 39 | end 40 | 41 | acts_as_file :body => self.instance_method(:filename) 42 | 43 | def finished? 44 | !self.exit_code.nil? 45 | end 46 | 47 | def successful? 48 | self.finished? and self.exit_code == 0 49 | end 50 | 51 | def error? 52 | self.finished? and self.exit_code != 0 53 | end 54 | 55 | def new? 56 | self.id.nil? 57 | end 58 | 59 | def self.create_and_delete(*args) 60 | created = self.create(*args) 61 | if self.count > FluentdServer::Config.task_max_num 62 | oldest = self.first 63 | oldest.destroy_with_file 64 | end 65 | created 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/fluentd_server/sync_runner.rb: -------------------------------------------------------------------------------- 1 | require 'fluentd_server/model' 2 | require 'fluentd_server/logger' 3 | 4 | class FluentdServer::SyncRunner 5 | include FluentdServer::Logger 6 | 7 | def self.run(opts = {}) 8 | self.new(opts).run 9 | end 10 | 11 | def initialize(opts = {}) 12 | end 13 | 14 | def run 15 | logger.debug "[sync] sync runner started" 16 | return nil unless FluentdServer::Config.file_storage 17 | plus, minus = find_diff 18 | create(plus) 19 | delete(minus) 20 | end 21 | 22 | def find_locals 23 | return [] unless FluentdServer::Config.file_storage 24 | names = [] 25 | Dir.chdir(FluentdServer::Config.data_dir) do 26 | Dir.glob("*.erb") do |filename| 27 | names << filename.chomp('.erb') 28 | end 29 | end 30 | names 31 | end 32 | 33 | def create(names) 34 | # ToDo: bulk insert with sqlite, postgresql? use activerecord-import for mysql2 35 | logger.debug "[sync] create #{names}" 36 | names.each do |name| 37 | begin 38 | Post.create(name: name) 39 | rescue ActiveRecord::RecordNotUnique => e 40 | logger.debug "#{e.class} #{e.message} #{name}" 41 | rescue => e 42 | logger.warn "#{e.class} #{e.message} #{name}" 43 | end 44 | end 45 | end 46 | 47 | def delete(names) 48 | logger.debug "[sync] remove #{names}" 49 | begin 50 | Post.where(:name => names).delete_all 51 | rescue => e 52 | logger.warn "#{e.class} #{e.message} #{names}" 53 | end 54 | end 55 | 56 | # Find difference between given array of paths and paths stored in DB 57 | # 58 | # @param [Integer] batch_size The batch size of a select query 59 | # @return [Array] Plus (array) and minus (array) differences 60 | def find_diff(batch_size: 1000) 61 | names = find_locals 62 | plus = names 63 | minus = [] 64 | Post.select('id, name').find_in_batches(batch_size: batch_size) do |batches| 65 | batches = batches.map(&:name) 66 | plus -= batches 67 | minus += (batches - names) 68 | end 69 | [plus, minus] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/fluentd_server/sync_worker.rb: -------------------------------------------------------------------------------- 1 | require "fluentd_server/config" 2 | require "fluentd_server/logger" 3 | require "fluentd_server/sync_runner" 4 | 5 | # reference: https://github.com/focuslight/focuslight/blob/master/lib/focuslight/worker.rb 6 | # thanks! 7 | class FluentdServer::SyncWorker 8 | include FluentdServer::Logger 9 | 10 | DEFAULT_INTERVAL = 60 11 | attr_reader :interval 12 | 13 | def self.start(opts = {}) 14 | self.new(opts).start 15 | end 16 | 17 | def initialize(opts = {}) 18 | @opts = opts 19 | @interval = opts[:interval] || FluentdServer::Config.sync_interval || DEFAULT_INTERVAL 20 | @signals = [] 21 | end 22 | 23 | def update_next! 24 | now = Time.now 25 | @next_time = now - ( now.to_i % @interval ) + @interval 26 | end 27 | 28 | def start 29 | Signal.trap(:INT){ @signals << :INT } 30 | Signal.trap(:HUP){ @signals << :HUP } 31 | Signal.trap(:TERM){ @signals << :TERM } 32 | Signal.trap(:PIPE, "IGNORE") 33 | 34 | update_next! 35 | logger.info("[sync] first updater start in #{@next_time}") 36 | 37 | childpid = nil 38 | while sleep(0.5) do 39 | if childpid 40 | begin 41 | if Process.waitpid(childpid, Process::WNOHANG) 42 | #TODO: $? (Process::Status object) 43 | logger.debug("[sync] update finished pid: #{childpid}, code: #{$? >> 8}") 44 | logger.debug("[sync] next updater start in #{@next_time}") 45 | childpid = nil 46 | end 47 | rescue Errno::ECHILD 48 | logger.warn("[sync] no child process"); 49 | childpid = nil 50 | end 51 | end 52 | 53 | unless @signals.empty? 54 | logger.warn("[sync] signals_received: #{@signals.join(',')}") 55 | break 56 | end 57 | 58 | next if Time.now < @next_time 59 | update_next! 60 | logger.debug("[sync] (#{@next_time}) updater start") 61 | 62 | if childpid 63 | logger.warn("[sync] previous updater exists, skipping this time") 64 | next 65 | end 66 | 67 | childpid = fork do 68 | FluentdServer::SyncRunner.run(@opts) 69 | end 70 | end 71 | 72 | if childpid 73 | logger.warn("[sync] waiting for updater process finishing") 74 | begin 75 | waitpid childpid 76 | rescue Errno::ECHILD 77 | # ignore 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/fluentd_server/task_runner.rb: -------------------------------------------------------------------------------- 1 | # included by class Task 2 | module TaskRunner 3 | def self.included(klass) 4 | require 'fileutils' 5 | klass.extend(ClassMethods) 6 | end 7 | 8 | def restart 9 | system(write_event_header('restart')) 10 | cmd = serf_event('restart') 11 | logger.debug "run #{cmd}" 12 | self.exit_code = system(cmd) 13 | self.save! 14 | end 15 | 16 | def status 17 | system(write_query_header('status')) 18 | self.delay.delayed_status 19 | end 20 | 21 | def delayed_status 22 | cmd = serf_query('status') 23 | logger.debug "run #{cmd}" 24 | self.exit_code = system(cmd) 25 | self.save! 26 | end 27 | 28 | def configtest 29 | system(write_query_header('configtest')) 30 | self.delay.delayed_configtest 31 | end 32 | 33 | def delayed_configtest 34 | cmd = serf_query('configtest') 35 | logger.debug "run #{cmd}" 36 | self.exit_code = system(cmd) 37 | self.save! 38 | end 39 | 40 | ## delayed_job hooks 41 | 42 | def before(job) 43 | @job = job 44 | end 45 | 46 | def failure 47 | logger.warn "job #{@job.attributes} failed" 48 | end 49 | 50 | ## helpers 51 | 52 | def write_event_header(cmd) 53 | "echo '$ serf event td-agent-#{cmd}' > #{self.filename}" 54 | end 55 | 56 | def write_query_header(cmd) 57 | "echo '$ serf query td-agent-#{cmd}' > #{self.filename}" 58 | end 59 | 60 | # serf event works asynchronously, so it does not take time 61 | def serf_event(cmd) 62 | "#{self.class.serf_path} event td-agent-#{cmd} >> #{self.filename} 2>&1" 63 | end 64 | 65 | # serf query works synchronously, so it takes time 66 | def serf_query(cmd) 67 | "#{self.class.serf_path} query td-agent-#{cmd} >> #{self.filename} 2>&1" 68 | end 69 | 70 | module ClassMethods 71 | def serf_path 72 | @serf_path ||= "#{find_path_gem('serf-td-agent')}/bin/serf" 73 | end 74 | 75 | # from gem-path gem 76 | def find_path_gem name 77 | path_gem = Gem.path.find do |base| 78 | path_gem = $LOAD_PATH.find do |path| 79 | path_gem = path[%r{#{base}/gems/#{name}\-[^/-]+/}] 80 | break path_gem if path_gem 81 | end 82 | break path_gem if path_gem 83 | end 84 | path_gem[0...-1] if path_gem 85 | end 86 | end 87 | end 88 | 89 | -------------------------------------------------------------------------------- /lib/fluentd_server/version.rb: -------------------------------------------------------------------------------- 1 | module FluentdServer 2 | VERSION = "0.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /lib/fluentd_server/web.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sinatra/flash' 3 | require 'sinatra/redirect_with_flash' 4 | require 'slim' 5 | 6 | require 'fluentd_server' 7 | require 'fluentd_server/config' 8 | require 'fluentd_server/environment' 9 | require 'fluentd_server/model' 10 | require 'fluentd_server/decorator' 11 | require 'fluentd_server/logger' 12 | require 'fluentd_server/web_helper' 13 | 14 | class FluentdServer::Web < Sinatra::Base 15 | include FluentdServer::Logger 16 | helpers FluentdServer::WebHelper 17 | 18 | set :dump_errors, true 19 | set :public_folder, File.join(__dir__, '..', '..', 'public') 20 | set :views, File.join(__dir__, '..', '..', 'views') 21 | 22 | enable :sessions 23 | register Sinatra::Flash 24 | helpers Sinatra::RedirectWithFlash 25 | register Sinatra::ActiveRecordExtension 26 | 27 | # create new post 28 | get %r{^/$|^/posts/create$} do 29 | @tab = 'posts' 30 | @posts = Post.order("name ASC") 31 | @post = Post.new 32 | slim :"posts/layout" 33 | end 34 | 35 | post "/posts" do 36 | @post = Post.new(params[:post]) 37 | if @post.save 38 | redirect "posts/#{@post.id}/edit", :notice => @post.decorate.success_message 39 | else 40 | redirect "posts/create", :error => @post.decorate.error_message 41 | end 42 | end 43 | 44 | # edit post 45 | get "/posts/:id/edit" do 46 | @tab = 'posts' 47 | @post = Post.find_by(id: params[:id]) 48 | redirect "/" unless @post 49 | @posts = Post.order("name ASC") 50 | slim :"posts/layout" 51 | end 52 | 53 | post "/posts/:id" do 54 | @post = Post.find_by(id: params[:id]) 55 | redirect "/" unless @post 56 | if @post.update(params[:post]) 57 | redirect "/posts/#{@post.id}/edit", :notice => @post.decorate.success_message 58 | else 59 | redirect "/posts/#{@post.id}/edit", :error => @post.decorate.error_message 60 | end 61 | end 62 | 63 | # delete post 64 | post "/posts/:id/delete" do 65 | @post = Post.find_by(id: params[:id]) 66 | if @post.destroy 67 | redirect "/posts/create", :notice => @post.decorate.success_message 68 | else 69 | redirect "/posts/create", :error => @post.decorate.error_message 70 | end 71 | end 72 | 73 | # get ALL posts in json 74 | get "/json/list" do 75 | @posts = Post.order("id ASC") 76 | content_type :json 77 | @posts.to_json 78 | end 79 | 80 | # get post in json 81 | get "/json/:name" do 82 | @post = Post.find_by(name: params[:name]) 83 | return 404 unless @post 84 | content_type :json 85 | @post.to_json 86 | end 87 | 88 | # render api 89 | get "/api/:name" do 90 | @post = Post.find_by(name: params[:name]) 91 | return 404 unless @post 92 | query_params = parse_query(request.query_string) 93 | content_type :text 94 | @post.decorate.render_body(query_params) 95 | end 96 | 97 | # list tasks 98 | get "/tasks" do 99 | @tab = 'tasks' 100 | @tasks = Task.limit(20).order("id DESC") 101 | slim :"tasks/show" 102 | end 103 | 104 | # show task 105 | get "/tasks/:id" do 106 | @tab = 'tasks' 107 | @tasks = Task.limit(20).order("id DESC") 108 | @task = Task.find_by(id: params[:id]) 109 | slim :"tasks/show" 110 | end 111 | 112 | # get task body in json for ajax 113 | get "/json/tasks/:id/body" do 114 | content_type :json 115 | offset = params[:offset].to_i 116 | task = Task.find_by(id: params[:id]) 117 | body = task.body(offset).to_s 118 | bytes = body.bytesize 119 | more_data = !task.finished? || bytes > offset 120 | { 121 | body: body, 122 | bytes: bytes, 123 | moreData: more_data, 124 | }.to_json.tap {|json| logger.debug json } 125 | end 126 | 127 | # restart task 128 | post "/task/restart" do 129 | @task = ::Task.create_and_delete(name: 'Restart') 130 | @task.restart 131 | redirect "/tasks/#{@task.id}" 132 | end 133 | 134 | # status task 135 | post "/task/status" do 136 | @task = ::Task.create_and_delete(name: 'Status') 137 | @task.status 138 | redirect "/tasks/#{@task.id}" 139 | end 140 | 141 | # configtest task 142 | post "/task/configtest" do 143 | @task = ::Task.create_and_delete(name: 'Configtest') 144 | @task.configtest 145 | redirect "/tasks/#{@task.id}" 146 | end 147 | 148 | end 149 | -------------------------------------------------------------------------------- /lib/fluentd_server/web_helper.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'fluentd_server' 3 | 4 | module FluentdServer::WebHelper 5 | include Rack::Utils 6 | alias_method :h, :escape_html 7 | 8 | # override RackUtil.parse_query 9 | # @param qs query string 10 | # @param d delimiter 11 | # @return 12 | def parse_query(qs, d=nil) 13 | params = {} 14 | (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| 15 | k, v = p.split('=', 2).map { |x| unescape(x) } 16 | if k.ends_with?('[]') 17 | k1 = k[0..-3] 18 | if params[k1] and params[k1].class == Array 19 | params[k1] << v 20 | else 21 | params[k1] = [v] 22 | end 23 | elsif k.ends_with?(']') and md = k.match(/^([^\[]+)\[([^\]]+)\]$/) 24 | k1, k2 = md[1], md[2] 25 | if params[k1] and params[k1].class == Hash 26 | params[k1][k2] = v 27 | else 28 | params[k1] = { k2 => v } 29 | end 30 | else 31 | params[k] = v 32 | end 33 | end 34 | params 35 | end 36 | 37 | def url_for(url_fragment, mode=nil, options = nil) 38 | if mode.is_a? Hash 39 | options = mode 40 | mode = nil 41 | end 42 | 43 | if mode.nil? 44 | mode = :path_only 45 | end 46 | 47 | mode = mode.to_sym unless mode.is_a? Symbol 48 | optstring = nil 49 | 50 | if options.is_a? Hash 51 | optstring = '?' + options.map { |k,v| "#{k}=#{URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/)}" }.join('&') 52 | end 53 | 54 | case mode 55 | when :path_only 56 | base = request.script_name 57 | when :full 58 | scheme = request.scheme 59 | if (scheme == 'http' && request.port == 80 || 60 | scheme == 'https' && request.port == 443) 61 | port = "" 62 | else 63 | port = ":#{request.port}" 64 | end 65 | base = "#{scheme}://#{request.host}#{port}#{request.script_name}" 66 | else 67 | raise TypeError, "Unknown url_for mode #{mode.inspect}" 68 | end 69 | "#{base}#{url_fragment}#{optstring}" 70 | end 71 | 72 | def escape_url(str) 73 | CGI.escape(str) 74 | end 75 | 76 | def active_if(cond) 77 | 'active' if cond 78 | end 79 | 80 | def disabled_if(cond) 81 | 'disabled="disabled"' if cond 82 | end 83 | 84 | def bootstrap_flash 85 | slim <<-EOF 86 | - if flash[:notice] 87 | p.alert.alert-success 88 | == flash[:notice] 89 | - if flash[:error] 90 | p.alert.alert-danger 91 | == flash[:error] 92 | EOF 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/log/.gitkeep -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /public/js/jquery-1.10.2.min.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/public/js/jquery-1.10.2.min.map -------------------------------------------------------------------------------- /public/js/jquery.storageapi.min.js: -------------------------------------------------------------------------------- 1 | /* jQuery Storage API Plugin 1.6.0 https://github.com/julien-maurel/jQuery-Storage-API */ 2 | !function(e){function t(t){var r,n,i,o=arguments.length,s=window[t],a=arguments,u=a[1];if(2>o)throw Error("Minimum 2 arguments must be given");if(e.isArray(u)){n={};for(var f in u){r=u[f];try{n[r]=JSON.parse(s.getItem(r))}catch(m){n[r]=s.getItem(r)}}return n}if(2!=o){try{n=JSON.parse(s.getItem(u))}catch(m){throw new ReferenceError(u+" is not defined in this storage")}for(var f=2;o-1>f;f++)if(n=n[a[f]],void 0===n)throw new ReferenceError([].slice.call(a,1,f+1).join(".")+" is not defined in this storage");if(e.isArray(a[f])){i=n,n={};for(var c in a[f])n[a[f][c]]=i[a[f][c]];return n}return n[a[f]]}try{return JSON.parse(s.getItem(u))}catch(m){return s.getItem(u)}}function n(t){var r,n,i=arguments.length,o=window[t],s=arguments,a=s[1],u=s[2],f={};if(2>i||!e.isPlainObject(a)&&3>i)throw Error("Minimum 3 arguments must be given or second parameter must be an object");if(e.isPlainObject(a)){for(var m in a)r=a[m],e.isPlainObject(r)?o.setItem(m,JSON.stringify(r)):o.setItem(m,r);return a}if(3==i)return"object"==typeof u?o.setItem(a,JSON.stringify(u)):o.setItem(a,u),u;try{n=o.getItem(a),null!=n&&(f=JSON.parse(n))}catch(c){}n=f;for(var m=2;i-2>m;m++)r=s[m],n[r]&&e.isPlainObject(n[r])||(n[r]={}),n=n[r];return n[s[m]]=s[m+1],o.setItem(a,JSON.stringify(f)),f}function i(t){var r,n,i=arguments.length,o=window[t],s=arguments,a=s[1];if(2>i)throw Error("Minimum 2 arguments must be given");if(e.isArray(a)){for(var u in a)o.removeItem(a[u]);return!0}if(2==i)return o.removeItem(a),!0;try{r=n=JSON.parse(o.getItem(a))}catch(f){throw new ReferenceError(a+" is not defined in this storage")}for(var u=2;i-1>u;u++)if(n=n[s[u]],void 0===n)throw new ReferenceError([].slice.call(s,1,u).join(".")+" is not defined in this storage");if(e.isArray(s[u]))for(var m in s[u])delete n[s[u][m]];else delete n[s[u]];return o.setItem(a,JSON.stringify(r)),!0}function o(t,r){var n=u(t);for(var o in n)i(t,n[o]);if(r)for(var o in e.namespaceStorages)f(o)}function s(r){var n=arguments.length,i=arguments,o=(window[r],i[1]);if(1==n)return 0==u(r).length;if(e.isArray(o)){for(var a=0;an)throw Error("Minimum 2 arguments must be given");if(e.isArray(o)){for(var s=0;s1?t.apply(this,o):i,a._cookie)for(var u in e.cookie())""!=u&&s.push(u.replace(a._prefix,""));else for(var f in a)s.push(f);return s}function f(t){if(!t||"string"!=typeof t)throw Error("First parameter must be a string");window.localStorage.getItem(t)||window.localStorage.setItem(t,"{}"),window.sessionStorage.getItem(t)||window.sessionStorage.setItem(t,"{}");var r={localStorage:e.extend({},e.localStorage,{_ns:t}),sessionStorage:e.extend({},e.sessionStorage,{_ns:t})};return e.cookie&&(window.cookieStorage.getItem(t)||window.cookieStorage.setItem(t,"{}"),r.cookieStorage=e.extend({},e.cookieStorage,{_ns:t})),e.namespaceStorages[t]=r,r}var m="ls_",c="ss_",g={_type:"",_ns:"",_callMethod:function(e,t){var r=[this._type];return this._ns&&r.push(this._ns),[].push.apply(r,t),e.apply(this,r)},get:function(){return this._callMethod(t,arguments)},set:function(){var t=arguments.length,i=arguments,o=i[0];if(1>t||!e.isPlainObject(o)&&2>t)throw Error("Minimum 2 arguments must be given or first parameter must be an object");if(e.isPlainObject(o)&&this._ns){for(var s in o)n(this._type,this._ns,s,o[s]);return o}return r=this._callMethod(n,i),this._ns?r[o]:r},remove:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(i,arguments)},removeAll:function(e){return this._ns?(n(this._type,this._ns,{}),!0):o(this._type,e)},isEmpty:function(){return this._callMethod(s,arguments)},isSet:function(){if(arguments.length<1)throw Error("Minimum 1 argument must be given");return this._callMethod(a,arguments)},keys:function(){return this._callMethod(u,arguments)}};if(e.cookie){window.name||(window.name=Math.floor(1e8*Math.random()));var h={_cookie:!0,_prefix:"",_expires:null,_path:null,_domain:null,setItem:function(t,r){e.cookie(this._prefix+t,r,{expires:this._expires,path:this._path,domain:this._domain})},getItem:function(t){return e.cookie(this._prefix+t)},removeItem:function(t){return e.removeCookie(this._prefix+t)},clear:function(){for(var t in e.cookie())""!=t&&(!this._prefix&&-1===t.indexOf(m)&&-1===t.indexOf(c)||this._prefix&&0===t.indexOf(this._prefix))&&e.removeCookie(t)},setExpires:function(e){return this._expires=e,this},setPath:function(e){return this._path=e,this},setDomain:function(e){return this._domain=e,this},setConf:function(e){return e.path&&(this._path=e.path),e.domain&&(this._domain=e.domain),e.expires&&(this._expires=e.expires),this},setDefaultConf:function(){this._path=this._domain=this._expires=null}};window.localStorage||(window.localStorage=e.extend({},h,{_prefix:m,_expires:3650}),window.sessionStorage=e.extend({},h,{_prefix:c+window.name+"_"})),window.cookieStorage=e.extend({},h),e.cookieStorage=e.extend({},g,{_type:"cookieStorage",setExpires:function(e){return window.cookieStorage.setExpires(e),this},setPath:function(e){return window.cookieStorage.setPath(e),this},setDomain:function(e){return window.cookieStorage.setDomain(e),this},setConf:function(e){return window.cookieStorage.setConf(e),this},setDefaultConf:function(){return window.cookieStorage.setDefaultConf(),this}})}e.initNamespaceStorage=function(e){return f(e)},e.localStorage=e.extend({},g,{_type:"localStorage"}),e.sessionStorage=e.extend({},g,{_type:"sessionStorage"}),e.namespaceStorages={},e.removeAllStorages=function(t){e.localStorage.removeAll(t),e.sessionStorage.removeAll(t),e.cookieStorage&&e.cookieStorage.removeAll(t),t||(e.namespaceStorages={})}}(jQuery); 3 | -------------------------------------------------------------------------------- /spec/bench/bench.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'parallel' 4 | require 'net/http' 5 | 6 | def main 7 | requests = 20000 # number of requests to perform 8 | concurrency = 126 # number of multiple requests to make 9 | name = 'worker' 10 | puts "requests = #{requests}" 11 | puts "concurrency = #{concurrency}" 12 | 13 | client = Client.new("http://localhost:5126") 14 | 15 | duration = elapsed_time do 16 | Parallel.each_with_index([name]*requests, :in_processes => concurrency) do |name, i| 17 | puts "processing #{i}" if i % 1000 == 0 18 | res = client.get(name) 19 | puts 'error' unless res.code == '200' 20 | end 21 | end 22 | 23 | req_per_sec = ( duration > 0 ) ? requests/duration : 0 24 | puts "req/sec = #{req_per_sec}" 25 | end 26 | 27 | def elapsed_time(&block) 28 | s = Time.now 29 | yield 30 | Time.now - s 31 | end 32 | 33 | class Client 34 | attr_reader :base_uri 35 | attr_reader :host 36 | attr_reader :port 37 | attr_accessor :debug_dev 38 | attr_accessor :open_timeout 39 | attr_accessor :read_timeout 40 | attr_accessor :verify_ssl 41 | attr_accessor :keepalive 42 | 43 | def initialize(base_uri = 'http://127.0.0.1:5126', opts = {}) 44 | @base_uri = base_uri 45 | 46 | URI.parse(base_uri).tap {|uri| 47 | @host = uri.host 48 | @port = uri.port 49 | @use_ssl = uri.scheme == 'https' 50 | } 51 | @debug_dev = opts['debug_dev'] # IO object such as STDOUT 52 | @open_timeout = opts['open_timeout'] # 60 53 | @read_timeout = opts['read_timeout'] # 60 54 | @verify_ssl = opts['verify_ssl'] 55 | @keepalive = opts['keepalive'] 56 | end 57 | 58 | def http_connection 59 | Net::HTTP.new(@host, @port).tap {|http| 60 | http.use_ssl = @use_ssl 61 | http.open_timeout = @open_timeout if @open_timeout 62 | http.read_timeout = @read_timeout if @read_timeout 63 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @verify_ssl 64 | http.set_debug_output(@debug_dev) if @debug_dev 65 | } 66 | end 67 | 68 | def get_request(path, extheader = {}) 69 | Net::HTTP::Get.new(path).tap {|req| 70 | req['Host'] = @host 71 | req['Connection'] = 'Keep-Alive' if @keepalive 72 | extheader.each {|key, value| req[key] = value } 73 | } 74 | end 75 | 76 | def get(name) 77 | path = "/api/#{name}" 78 | req = get_request(path) 79 | @res = http_connection.start {|http| http.request(req) } 80 | end 81 | end 82 | 83 | main 84 | -------------------------------------------------------------------------------- /spec/bench/result.md: -------------------------------------------------------------------------------- 1 | Take benchmark of `/api/:name`. The conf body is as following: 2 | 3 | ``` 4 | 5 | type forward 6 | port <%= port %> 7 | 8 | ``` 9 | 10 | | # of requests | concurrency | server | # of workers | req/sec | 11 | |---------------|-------------|---------|--------------|---------| 12 | |20000 |126 | unicorn | 1 |865.07 | 13 | |20000 |126 | unicorn | 12 |4116.78 | 14 | |20000 |126 | puma | 1 |634.20| 15 | |20000 |126 | puma | 12 |3765.66 | 16 | 17 | Let's use unicorn 18 | -------------------------------------------------------------------------------- /spec/model_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'fluentd_server/model' 3 | 4 | describe Delayed::Job do 5 | end 6 | 7 | describe Post do 8 | end 9 | 10 | describe Task do 11 | after { Task.delete_all } 12 | 13 | context '#new?' do 14 | it { expect(Task.new.new?).to be_truthy } 15 | it { expect(Task.create.new?).to be_falsey } 16 | end 17 | 18 | context '#filename' do 19 | it { expect(Task.new.filename).to be_nil } 20 | it { expect(Task.create.filename).to be_kind_of(String) } 21 | end 22 | 23 | context '#create_and_delete' do 24 | before { allow(FluentdServer::Config).to receive(:task_max_num).and_return(1) } 25 | before { @oldest = Task.create(name: 'Restart') } 26 | it { 27 | Task.create_and_delete(name: 'Restart') 28 | expect(Task.find_by(id: @oldest.id)).to be_nil 29 | expect(Task.count).to eql(1) 30 | } 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'pry' 4 | $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib') 5 | 6 | ENV['RACK_ENV'] = 'test' 7 | ENV['JOB_DIR'] = 'spec/tmp' 8 | 9 | # NOTE: DATABASE_URL in .env must be commented out 10 | require 'fluentd_server' 11 | require 'fluentd_server/environment' 12 | require 'rake' 13 | require 'sinatra/activerecord/rake' 14 | Rake::Task['db:schema:load'].invoke 15 | 16 | if ENV['TRAVIS'] 17 | require 'simplecov' 18 | require 'coveralls' 19 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 20 | SimpleCov.start do 21 | add_filter 'spec' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/sync_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'fluentd_server/sync_runner' 3 | 4 | if FluentdServer::Config.file_storage 5 | describe 'SyncRunner' do 6 | def clean 7 | filenames = File.join(FluentdServer::Config.data_dir, '*.erb') 8 | Dir.glob(filenames).each { |f| File.delete(f) rescue nil } 9 | Post.delete_all 10 | end 11 | around {|example| clean; example.run; clean } 12 | let(:runner) { FluentdServer::SyncRunner.new } 13 | 14 | context '#find_locals' do 15 | before { Post.create(name: 'post1', body: 'a') } 16 | before { Post.create(name: 'post2', body: 'a') } 17 | let(:subject) { runner.find_locals } 18 | it { should =~ ['post1', 'post2' ] } 19 | end 20 | 21 | context '#find_diff' do 22 | before { Post.new(name: 'post1').save_without_file } 23 | before { Post.create(name: 'post2', body: 'a') } 24 | before { File.open(Post.new(name: 'post3').filename, "w") {} } 25 | it { 26 | plus, minus = runner.find_diff 27 | expect(minus).to eql(['post1']) 28 | expect(plus).to eql(['post3']) 29 | } 30 | end 31 | 32 | context '#create' do 33 | before { Post.create(name: 'post1', body: 'a') } 34 | before { runner.create(%w[post1 post2]) } 35 | it { 36 | expect(Post.find_by(name: 'post1').body).not_to be_nil 37 | expect(Post.find_by(name: 'post2').body).to be_nil 38 | } 39 | end 40 | 41 | context '#delete' do 42 | before { 43 | post1 = Post.create(name: 'post1', body: 'a') 44 | post2 = Post.create(name: 'post2', body: 'a') 45 | runner.delete(%w[post1]) 46 | } 47 | it { 48 | expect(Post.find_by(name: 'post1')).to be_nil 49 | expect(Post.find_by(name: 'post2')).not_to be_nil 50 | } 51 | end 52 | 53 | context '#run' do 54 | before { Post.new(name: 'post1').save_without_file } 55 | before { Post.create(name: 'post2', body: 'a') } 56 | before { File.open(Post.new(name: 'post3').filename, "w") {} } 57 | it { 58 | runner.run 59 | expect(Post.find_by(name: 'post1')).to be_nil 60 | expect(Post.find_by(name: 'post2')).not_to be_nil 61 | expect(Post.find_by(name: 'post3')).not_to be_nil 62 | } 63 | end 64 | end 65 | else 66 | describe 'SyncRunner' do 67 | context '#run' do 68 | let(:subject) { FluentdServer::SyncRunner.new.run } 69 | it { should be_nil } 70 | end 71 | context '#find_locals' do 72 | before { Post.create(name: 'post1', body: 'a') } 73 | before { Post.create(name: 'post2', body: 'a') } 74 | let(:subject) { FluentdServer::SyncRunner.new.find_locals } 75 | it { should == [] } 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/task_runner_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'fluentd_server/task_runner' 3 | require 'fluentd_server/model' 4 | 5 | describe 'TaskRunner' do 6 | let(:task) { Task.create } 7 | before { allow(task).to receive(:delay).and_return(task) } 8 | after { Task.delete_all } 9 | 10 | context '.serf_path' do 11 | it { expect(File.executable?(Task.serf_path)).to be_truthy } 12 | end 13 | 14 | # ToDo: Test whether the serf command is executed correctly 15 | context '#restart' do 16 | it { expect { task.restart }.not_to raise_error } 17 | end 18 | 19 | context '#status' do 20 | it { expect { task.status }.not_to raise_error } 21 | end 22 | 23 | context '#configtest' do 24 | it { expect { task.configtest }.not_to raise_error } 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonots/fluentd-server/771daec3872a953d21e2e48480d2eb6802da022b/spec/tmp/.gitkeep -------------------------------------------------------------------------------- /spec/web_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'fluentd_server/web_helper' 3 | 4 | describe 'WebHelper' do 5 | include FluentdServer::WebHelper 6 | 7 | context '#parse_query' do 8 | it 'key=val' do 9 | expect(parse_query('key=val')).to eql({'key'=>'val'}) 10 | end 11 | 12 | it 'array' do 13 | # Array support of Rack::Utils.parse_query is as 14 | # `key=1&key=2` #=> key => ['1', '2'] 15 | # But, I change it as `key[]=1&key[]=2` referring PHP 16 | expect(parse_query('key[]=1&key[]=2')).to eql({'key'=>['1','2']}) 17 | end 18 | 19 | it 'hash' do 20 | expect(parse_query('name[key1]=1&name[key2]=2')).to eql( 21 | {'name' => {'key1'=>'1', 'key2'=>'2'}} 22 | ) 23 | end 24 | end 25 | 26 | context '#escape_url' do 27 | it { expect(escape_url('a b')).to eql('a+b') } 28 | end 29 | 30 | context '#active_if' do 31 | it { expect(active_if(true)).to eql('active') } 32 | it { expect(active_if(false)).to be_nil } 33 | end 34 | 35 | context '#disabled_if' do 36 | it { expect(disabled_if(true)).to eql('disabled="disabled"') } 37 | it { expect(disabled_if(false)).to be_nil } 38 | end 39 | 40 | end 41 | 42 | 43 | -------------------------------------------------------------------------------- /spec/web_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | require 'fluentd_server/web' 3 | require 'capybara' 4 | require 'capybara/dsl' 5 | 6 | Capybara.app = FluentdServer::Web 7 | 8 | describe 'Post' do 9 | include Capybara::DSL 10 | after { Post.delete_all } 11 | 12 | context 'get ALL posts' do 13 | it 'visit' do 14 | visit '/' 15 | expect(page.status_code).to be == 200 16 | end 17 | end 18 | 19 | context 'create new post' do 20 | before { visit '/posts/create' } 21 | 22 | it 'visit' do 23 | expect(page.status_code).to be == 200 24 | end 25 | 26 | it 'create' do 27 | expect { 28 | fill_in "post[name]", with: 'aaaa' 29 | fill_in "post[body]", with: 'aaaa' 30 | click_button('Submit') 31 | }.to change(Post, :count).by(1) 32 | end 33 | 34 | it 'fails to create' do 35 | expect { 36 | fill_in "post[name]", with: '' 37 | fill_in "post[body]", with: '' 38 | click_button('Submit') 39 | }.to change(Post, :count).by(0) 40 | end 41 | end 42 | 43 | context 'edit post' do 44 | before { Post.create(name: 'aaaa', body: 'aaaa') } 45 | let(:post) { Post.first } 46 | before { visit "/posts/#{post.id}/edit" } 47 | 48 | it 'visit' do 49 | expect(page.status_code).to be == 200 50 | end 51 | 52 | it 'edit' do 53 | fill_in "post[name]", with: 'bbbb' 54 | fill_in "post[body]", with: 'bbbb' 55 | click_button('Submit') 56 | edit = Post.find(post.id) 57 | expect(edit.name).to eql('bbbb') 58 | expect(edit.body).to eql('bbbb') 59 | end 60 | 61 | it 'fails to edit' do 62 | fill_in "post[name]", with: '' 63 | fill_in "post[body]", with: '' 64 | click_button('Submit') 65 | edit = Post.find(post.id) 66 | expect(edit.name).not_to eql('') 67 | expect(edit.body).not_to eql('') 68 | end 69 | 70 | # javascript click for `Really?` is required 71 | #it 'delete' do 72 | # click_link('Delete') 73 | # expect{Post.find(post.id)}.to raise_error 74 | #end 75 | end 76 | 77 | context 'delete post' do 78 | include Rack::Test::Methods 79 | 80 | def app 81 | FluentdServer::Web 82 | end 83 | 84 | before { Post.create(name: 'aaaa', body: '<%= key %>') } 85 | let(:subject) { Post.first } 86 | 87 | it 'delete' do 88 | post "/posts/#{subject.id}/delete" 89 | expect(Post.find_by(id: subject.id)).to be_nil 90 | end 91 | end 92 | end 93 | 94 | describe 'Task' do 95 | include Capybara::DSL 96 | after { Post.delete_all } 97 | 98 | context 'list tasks' do 99 | it 'visit' do 100 | visit '/tasks' 101 | expect(page.status_code).to be == 200 102 | end 103 | end 104 | 105 | context 'show task' do 106 | before { @task = Task.create } 107 | it 'visit' do 108 | visit "/tasks/#{@task.id}" 109 | expect(page.status_code).to be == 200 110 | end 111 | end 112 | 113 | context '/json/tasks/:id/body' do 114 | include Rack::Test::Methods 115 | def app; FluentdServer::Web; end 116 | 117 | before { @task = Task.create } 118 | it 'visit' do 119 | get "/json/tasks/#{@task.id}/body" 120 | expect(last_response.status).to eql(200) 121 | body = JSON.parse(last_response.body) 122 | expect(body).to be_has_key('body') 123 | expect(body).to be_has_key('bytes') 124 | expect(body).to be_has_key('moreData') 125 | end 126 | end 127 | 128 | context 'task button' do 129 | include Rack::Test::Methods 130 | def app; FluentdServer::Web; end 131 | 132 | it 'restart' do 133 | expect { 134 | post "/task/restart" 135 | }.to change(Task, :count).by(1) 136 | end 137 | 138 | it 'status' do 139 | expect { 140 | post "/task/status" 141 | }.to change(Task, :count).by(1) 142 | end 143 | 144 | it 'configtest' do 145 | expect { 146 | post "/task/configtest" 147 | }.to change(Task, :count).by(1) 148 | end 149 | end 150 | end 151 | 152 | describe 'API' do 153 | include Rack::Test::Methods 154 | 155 | def app 156 | FluentdServer::Web 157 | end 158 | 159 | after { Post.delete_all } 160 | before { Post.create(name: 'aaaa', body: '<%= key %>') } 161 | let(:post) { Post.first } 162 | 163 | context 'render api' do 164 | it 'render' do 165 | get "/api/#{post.name}?key=value" 166 | expect(last_response.status).to eql(200) 167 | expect(last_response.body).to eql('value') 168 | end 169 | end 170 | 171 | context 'get ALL posts in json' do 172 | it 'get' do 173 | get "/json/list" 174 | body = JSON.parse(last_response.body) 175 | expect(body[0]["name"]).to eql('aaaa') 176 | end 177 | end 178 | 179 | context 'get post in json' do 180 | it 'get' do 181 | get "/json/#{post.name}" 182 | body = JSON.parse(last_response.body) 183 | expect(body["name"]).to eql('aaaa') 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /views/_js.slim: -------------------------------------------------------------------------------- 1 | javascript: 2 | // cancel enterkey event 3 | $(document).on('keypress', '[data-provide="typeahead"]', function(ev){ 4 | if ((ev.which && ev.which === 13) || (ev.keyCode && ev.keyCode === 13)) { 5 | return false; 6 | } else { 7 | return true; 8 | } 9 | }); 10 | // 11 | $(document).on('click', 'a[data-method="post"], a[data-method="put"], a[data-method="delete"]', function() { 12 | var $link = $(this), 13 | href = $link.attr('href'), 14 | method = $link.data('method'), 15 | form = $('
'), 16 | metadata_input = ''; 17 | 18 | if($link.attr("disabled")) { 19 | return false; 20 | } 21 | 22 | if($link.data("confirm")){ 23 | if(!window.confirm('Really?')) 24 | return false; 25 | } 26 | 27 | form.hide().append(metadata_input).appendTo('body'); 28 | form.submit(); 29 | return false; 30 | }); 31 | - if FluentdServer::Config.file_storage 32 | javascript: 33 | $('#post_name').attr("disabled", "disabled"); 34 | $('#post_body').attr("disabled", "disabled"); 35 | $('#post_submit').addClass('disabled'); 36 | $('#post_delete').addClass('disabled'); 37 | -------------------------------------------------------------------------------- /views/_navbar.slim: -------------------------------------------------------------------------------- 1 | .navbar.navbar-inverse.navbar-fixed-top 2 | .container-fluid 3 | .navbar-header 4 | button.navbar-toggle[type="button" data-toggle="collapse" data-target=".navbar-collapse"] 5 | span.icon-bar 6 | span.icon-bar 7 | span.icon-bar 8 | a.navbar-brand[href="/"] 9 | | Fluentd Server 10 | .collapse.navbar-collapse 11 | ul.nav.navbar-nav 12 | li[class=active_if(@tab == 'posts')] 13 | a[href="/posts/create"] 14 | | Conf 15 | li[class=active_if(@tab == 'tasks')] 16 | a[href="/tasks"] 17 | | Task 18 | -------------------------------------------------------------------------------- /views/_style.slim: -------------------------------------------------------------------------------- 1 | style 2 | | body { padding-top: 75px; } 3 | | .starter-template { padding: 40px 15px; text-align: center; } 4 | | .container { max-width:1000px; } 5 | -------------------------------------------------------------------------------- /views/layout.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html[lang="en"] 3 | head 4 | meta[charset="utf-8"] 5 | meta[name="viewport" content="width=device-width, initial-scale=1.0"] 6 | title 7 | | Fluentd Server 8 | link[href=url_for("/css/bootstrap.min.css") rel="stylesheet"] 9 | == slim :'_style' 10 | body 11 | == slim :'_navbar' 12 | 13 | .container-fluid 14 | .row 15 | == yield 16 | script[src=url_for("/js/jquery-1.10.2.min.js")] 17 | script[src=url_for("/js/bootstrap.min.js")] 18 | == slim :'_js' 19 | == erb :'tasks/ajax' if @tab == 'tasks' 20 | -------------------------------------------------------------------------------- /views/posts/create.slim: -------------------------------------------------------------------------------- 1 | h2[style="margin-top:0px;"] 2 | | Create Config 3 | br 4 | == bootstrap_flash 5 | form[id="post_form" action=url_for("/posts") method="post" role="form"] 6 | .form-group 7 | label[for="post_name"] 8 | | Name: 9 | br 10 | input id="post_name" class="form-control" name="post[name]" type="text" value=@post.name style="width=90%" 11 | .form-group 12 | label[for="post_body"] 13 | | Body: 14 | br 15 | textarea#post_body.form-control[name="post[body]" rows="10"] 16 | = @post.body 17 | br 18 | button.btn.btn-success[id="post_submit" type="submit"] 19 | | Submit 20 | br 21 | -------------------------------------------------------------------------------- /views/posts/edit.slim: -------------------------------------------------------------------------------- 1 | h2[style="margin-top:0px;"] 2 | | Edit Config 3 | br 4 | == bootstrap_flash 5 | form id="post_form" action=url_for("/posts/#{@post.id}") method="post" 6 | .form-group 7 | label[for="post_name"] 8 | | Name: 9 | br 10 | input id="post_name" class="form-control" name="post[name]" type="text" value=@post.name 11 | .form-group 12 | label[for="post_body"] 13 | | Body: 14 | br 15 | textarea#post_body.form-control[name="post[body]" rows="10"] 16 | = @post.body 17 | br 18 | a href=url_for("/api/#{@post.name}") 19 | | Render 20 | button.btn.btn-success[id="post_submit" type="submit"] 21 | | Submit 22 | |   23 | a.btn.btn-danger id="post_delete" data-method="post" data-confirm="true" href=url_for("/posts/#{@post.id}/delete") 24 | | Delete 25 | -------------------------------------------------------------------------------- /views/posts/layout.slim: -------------------------------------------------------------------------------- 1 | .col-xs-3 2 | == slim :'posts/menu' 3 | .col-xs-9 4 | - if @post.new? 5 | == slim :'posts/create' 6 | - else 7 | == slim :'posts/edit' 8 | 9 | -------------------------------------------------------------------------------- /views/posts/menu.slim: -------------------------------------------------------------------------------- 1 | == @post.decorate.create_button 2 | ul.nav.nav-pills.nav-stacked 3 | - for post in @posts do 4 | - if @post and @post.id == post.id 5 | li.active== post.decorate.link_to 6 | - else 7 | li== post.decorate.link_to 8 | -------------------------------------------------------------------------------- /views/tasks/ajax.erb: -------------------------------------------------------------------------------- 1 | <% if @task and !@task.finished? %> 2 | 21 | <% end %> 22 | -------------------------------------------------------------------------------- /views/tasks/show.slim: -------------------------------------------------------------------------------- 1 | .col-xs-3 2 | div[style="padding: 0 0 10px 10px;font-variant:small-caps;"] 3 | a.btn.btn-danger data-method="post" data-confirm="true" href=url_for("/task/restart") 4 | | Restart 5 | |   6 | a.btn.btn-info data-method="post" data-confirm="true" href=url_for("/task/status") 7 | | Status 8 | |   9 | a.btn.btn-success data-method="post" data-confirm="true" href=url_for("/task/configtest") 10 | | Configtest 11 | ul.nav.nav-pills.nav-stacked 12 | - for task in @tasks do 13 | - if @task and @task.id == task.id 14 | li.active== task.decorate.link_to 15 | - else 16 | li== task.decorate.link_to 17 | .col-xs-9 18 | == bootstrap_flash 19 | - if @task 20 | - if !@task.finished? 21 | div.alert.alert-info id="progress" 22 | | NOW PROCESSING ... 23 | pre id="out" 24 | == @task.body 25 | --------------------------------------------------------------------------------