├── .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 | [](http://travis-ci.org/sonots/fluentd-server)
4 | [](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[
]
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 |
--------------------------------------------------------------------------------
/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('').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown",h),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=" li:not(.divider):visible a",i=f.find("[role=menu]"+h+", [role=listbox]"+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j').appendTo(document.body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());c.is("a")&&b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this,d=this.tip();this.setContent(),this.options.animation&&d.addClass("fade");var e="function"==typeof this.options.placement?this.options.placement.call(this,d[0],this.$element[0]):this.options.placement,f=/\s?auto?\s?/i,g=f.test(e);g&&(e=e.replace(f,"")||"top"),d.detach().css({top:0,left:0,display:"block"}).addClass(e),this.options.container?d.appendTo(this.options.container):d.insertAfter(this.$element);var h=this.getPosition(),i=d[0].offsetWidth,j=d[0].offsetHeight;if(g){var k=this.$element.parent(),l=e,m=document.documentElement.scrollTop||document.body.scrollTop,n="body"==this.options.container?window.innerWidth:k.outerWidth(),o="body"==this.options.container?window.innerHeight:k.outerHeight(),p="body"==this.options.container?0:k.offset().left;e="bottom"==e&&h.top+h.height+j-m>o?"top":"top"==e&&h.top-m-j<0?"bottom":"right"==e&&h.right+i>n?"left":"left"==e&&h.left-i'}),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 |
--------------------------------------------------------------------------------