mirror of
https://github.com/elastic/logstash.git
synced 2025-04-24 14:47:19 -04:00
325 lines
9.3 KiB
Ruby
Executable file
325 lines
9.3 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
# I don't want folks to have to learn to use yet another tool (rackup)
|
|
# just to launch logstash-web. So let's work like a standard ruby
|
|
# executable.
|
|
##rackup -Ilib:../lib -s thin
|
|
|
|
$:.unshift("%s/../lib" % File.dirname(__FILE__))
|
|
$:.unshift(File.dirname(__FILE__))
|
|
|
|
require "eventmachine"
|
|
require "json"
|
|
require "logstash/search/elasticsearch"
|
|
require "logstash/search/query"
|
|
require "logstash/namespace"
|
|
require "rack"
|
|
require "rubygems"
|
|
require "sinatra/async"
|
|
require "logstash/web/helpers/require_param"
|
|
|
|
class EventMachine::ConnectionError < RuntimeError; end
|
|
module LogStash::Web; end
|
|
|
|
class LogStash::Web::Server < Sinatra::Base
|
|
register Sinatra::Async
|
|
helpers Sinatra::RequireParam # logstash/web/helpers/require_param
|
|
|
|
set :haml, :format => :html5
|
|
set :logging, true
|
|
set :public, "#{File.dirname(__FILE__)}/public"
|
|
set :views, "#{File.dirname(__FILE__)}/views"
|
|
|
|
use Rack::CommonLogger
|
|
#use Rack::ShowExceptions
|
|
|
|
def initialize(settings={})
|
|
super
|
|
# TODO(sissel): Support alternate backends
|
|
backend_url = URI.parse(settings.backend_url)
|
|
|
|
case backend_url.scheme
|
|
when "elasticsearch"
|
|
@backend = LogStash::Search::ElasticSearch.new(
|
|
:host => backend_url.host,
|
|
:port => backend_url.port
|
|
)
|
|
when "twitter"
|
|
require "logstash/search/twitter"
|
|
@backend = LogStash::Search::Twitter.new(
|
|
:host => backend_url.host,
|
|
:port => backend_url.port
|
|
)
|
|
end # backend_url.scheme
|
|
end # def initialize
|
|
|
|
aget '/style.css' do
|
|
headers "Content-Type" => "text/css; charset=utf8"
|
|
body sass :style
|
|
end # /style.css
|
|
|
|
aget '/' do
|
|
redirect "/search"
|
|
end # '/'
|
|
|
|
aget '/search' do
|
|
result_callback = proc do |results|
|
|
status 500 if @error
|
|
@results = results
|
|
|
|
p :got => results
|
|
|
|
params[:format] ||= "html"
|
|
case params[:format]
|
|
when "html"
|
|
headers({"Content-Type" => "text/html" })
|
|
body haml :"search/results", :layout => !request.xhr?
|
|
when "text"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/results.txt", :layout => false
|
|
when "txt"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/results.txt", :layout => false
|
|
when "json"
|
|
headers({"Content-Type" => "text/plain" })
|
|
# TODO(sissel): issue/30 - needs refactoring here.
|
|
hits = @hits.collect { |h| h["_source"] }
|
|
response = {
|
|
"hits" => hits,
|
|
}
|
|
|
|
response["error"] = @error if @error
|
|
body response.to_json
|
|
end # case params[:format]
|
|
end # proc result_callback
|
|
|
|
# We'll still do a search query here even though most users
|
|
# have javascript enabled, we need to show the results in
|
|
# case a user doesn't have javascript.
|
|
if params[:q] and params[:q] != ""
|
|
query = LogStash::Search::Query.new(
|
|
:query_string => params[:q],
|
|
:offset => params[:offset],
|
|
:count => params[:count]
|
|
)
|
|
|
|
@backend.search(query) do |results|
|
|
p :got => results
|
|
begin
|
|
result_callback.call results
|
|
rescue => e
|
|
p :exception => e
|
|
end
|
|
end # @backend.search
|
|
else
|
|
results = LogStash::Search::Result.new(
|
|
:events => [],
|
|
:error_message => "No query given"
|
|
)
|
|
result_callback.call results
|
|
end
|
|
end # aget '/search'
|
|
|
|
apost '/api/search' do
|
|
api_search
|
|
end # apost /api/search
|
|
|
|
aget '/api/search' do
|
|
api_search
|
|
end # aget /api/search
|
|
|
|
def api_search
|
|
|
|
headers({"Content-Type" => "text/html" })
|
|
count = params["count"] = (params["count"] or 50).to_i
|
|
offset = params["offset"] = (params["offset"] or 0).to_i
|
|
format = (params[:format] or "json")
|
|
|
|
query = LogStash::Search::Query.new(
|
|
:query_string => params[:q],
|
|
:offset => offset,
|
|
:count => count
|
|
)
|
|
|
|
@backend.search(query) do |results|
|
|
@results = results
|
|
if @results.error?
|
|
status 500
|
|
case format
|
|
when "html"
|
|
headers({"Content-Type" => "text/html" })
|
|
body haml :"search/error", :layout => !request.xhr?
|
|
when "text"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/error.txt", :layout => false
|
|
when "txt"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/error.txt", :layout => false
|
|
when "json"
|
|
headers({"Content-Type" => "text/plain" })
|
|
# TODO(sissel): issue/30 - needs refactoring here.
|
|
if @results.error?
|
|
body({ "error" => @results.error_message }.to_json)
|
|
else
|
|
body @results.to_json
|
|
end
|
|
end # case params[:format]
|
|
next
|
|
end
|
|
|
|
@events = @results.events
|
|
@total = (@results.total rescue 0)
|
|
count = @results.events.size
|
|
|
|
if count and offset
|
|
if @total > (count + offset)
|
|
@result_end = (count + offset)
|
|
else
|
|
@result_end = @total
|
|
end
|
|
@result_start = offset
|
|
end
|
|
|
|
if count + offset < @total
|
|
next_params = params.clone
|
|
next_params["offset"] = [offset + count, @total - count].min
|
|
@next_href = "?" + next_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
|
last_params = next_params.clone
|
|
last_params["offset"] = @total - offset
|
|
@last_href = "?" + last_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
|
end
|
|
|
|
if offset > 0
|
|
prev_params = params.clone
|
|
prev_params["offset"] = [offset - count, 0].max
|
|
@prev_href = "?" + prev_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
|
|
|
if prev_params["offset"] > 0
|
|
first_params = prev_params.clone
|
|
first_params["offset"] = 0
|
|
@first_href = "?" + first_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
|
end
|
|
end
|
|
|
|
# TODO(sissel): make a helper function taht goes hash -> cgi querystring
|
|
@refresh_href = "?" + params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
|
|
|
case format
|
|
when "html"
|
|
headers({"Content-Type" => "text/html" })
|
|
body haml :"search/ajax", :layout => !request.xhr?
|
|
when "text"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/results.txt", :layout => false
|
|
when "txt"
|
|
headers({"Content-Type" => "text/plain" })
|
|
body erb :"search/results.txt", :layout => false
|
|
when "json"
|
|
headers({"Content-Type" => "text/plain" })
|
|
# TODO(sissel): issue/30 - needs refactoring here.
|
|
response = @results
|
|
body response.to_json
|
|
end # case params[:format]
|
|
end # @backend.search
|
|
end # def api_search
|
|
|
|
aget '/api/histogram' do
|
|
headers({"Content-Type" => "text/plain" })
|
|
missing = require_param(:q)
|
|
if !missing.empty?
|
|
status 500
|
|
body({ "error" => "Missing requiremed parameters",
|
|
"missing" => missing }.to_json)
|
|
next
|
|
end # if !missing.empty?
|
|
format = (params[:format] or "json")
|
|
field = (params[:field] or "@timestamp")
|
|
interval = (params[:interval] or 3600 * 1000)
|
|
@backend.histogram(params[:q], field, interval) do |results|
|
|
@results = results
|
|
if @results.error?
|
|
status 500
|
|
body({ "error" => @results.error_message }.to_json)
|
|
next
|
|
end
|
|
|
|
begin
|
|
a = results.results.to_json
|
|
rescue => e
|
|
status 500
|
|
body e.inspect
|
|
p :exception => e
|
|
p e
|
|
raise e
|
|
end
|
|
status 200
|
|
body a
|
|
end # @backend.search
|
|
end # aget '/api/histogram'
|
|
|
|
aget '/*' do
|
|
status 404 if @error
|
|
body "Invalid path."
|
|
end # aget /*
|
|
end # class LogStash::Web::Server
|
|
|
|
require "optparse"
|
|
Settings = Struct.new(:daemonize, :logfile, :address, :port, :backend_url)
|
|
settings = Settings.new
|
|
|
|
settings.address = "0.0.0.0"
|
|
settings.port = 9292
|
|
settings.backend_url = "elasticsearch://localhost:9200/"
|
|
|
|
progname = File.basename($0)
|
|
|
|
opts = OptionParser.new do |opts|
|
|
opts.banner = "Usage: #{progname} [options]"
|
|
|
|
opts.on("-d", "--daemonize", "Daemonize (default is run in foreground).") do
|
|
settings.daemonize = true
|
|
end
|
|
|
|
opts.on("-l", "--log FILE", "Log to a given path. Default is stdout.") do |path|
|
|
settings.logfile = path
|
|
end
|
|
|
|
opts.on("-a", "--address ADDRESS", "Address on which to start webserver. Default is 0.0.0.0.") do |address|
|
|
settings.address = address
|
|
end
|
|
|
|
opts.on("-p", "--port PORT", "Port on which to start webserver. Default is 9292.") do |port|
|
|
settings.port = port.to_i
|
|
end
|
|
|
|
opts.on("-b", "--backend URL",
|
|
"The backend URL to use. Default is elasticserach://localhost:9200/") do |url|
|
|
settings.backend_url = url
|
|
end
|
|
end
|
|
|
|
opts.parse!
|
|
|
|
if settings.daemonize
|
|
if Process.fork == nil
|
|
Process.setsid
|
|
else
|
|
exit(0)
|
|
end
|
|
end
|
|
|
|
if settings.logfile
|
|
logfile = File.open(settings.logfile, "w")
|
|
STDOUT.reopen(logfile)
|
|
STDERR.reopen(logfile)
|
|
elsif settings.daemonize
|
|
# Write to /dev/null if
|
|
devnull = File.open("/dev/null", "w")
|
|
STDOUT.reopen(devnull)
|
|
STDERR.reopen(devnull)
|
|
end
|
|
|
|
Rack::Handler::Thin.run(
|
|
Rack::CommonLogger.new( \
|
|
Rack::ShowExceptions.new( \
|
|
LogStash::Web::Server.new(settings))),
|
|
:Port => settings.port, :Host => settings.address)
|