diff --git a/lib/logstash/web/controllers/api_v1.rb b/lib/logstash/web/controllers/api_v1.rb new file mode 100644 index 000000000..bc53858c8 --- /dev/null +++ b/lib/logstash/web/controllers/api_v1.rb @@ -0,0 +1,146 @@ +require "logstash/search/elasticsearch" +require "logstash/search/query" +require "logstash/web/helpers/require_param" +require "sinatra/base" # gem sinatra + +# TODO(sissel): facter out the emitter (json pretty print handling, etc) +helpers Sinatra::RequireParam + +# TODO(sissel): move this to a lib? +# Or a LogStash::Search::WebHelper or something? +def api_search + 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, async=false) do |results| + @results = results + if @results.error? + status 500 + case format + when "html" + content_type :html + body haml :"search/error", :layout => !request.xhr? + when "text" + content_type :txt + body erb :"search/error.txt", :layout => false + when "txt" + content_type :txt + body erb :"search/error.txt", :layout => false + when "json" + content_type :json + # TODO(sissel): issue/30 - needs refactoring here. + body({ "error" => @results.error_message }.to_json) + 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 - count + @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" + content_type :html + body haml :"search/ajax", :layout => !request.xhr? + when "text" + content_type :txt + body erb :"search/results.txt", :layout => false + when "txt" + content_type :txt + body erb :"search/results.txt", :layout => false + when "json" + content_type :json + pretty = params.has_key?("pretty") + if pretty + body JSON.pretty_generate(@results.to_hash) + else + body @results.to_json + end + end # case params[:format] + end # @backend.search +end # def api_search + +# TODO(sissel): Update these to all be /api/v1 +post '/api/search' do + api_search +end # post /api/search + +get '/api/search' do + api_search +end # get /api/search + +get '/api/histogram' do + 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") # default json + field = (params[:field] or "@timestamp") # default @timestamp + interval = (params[:interval] or 3600000).to_i # default 1 hour + + results = @backend.histogram(params[:q], field, interval, async=false) + + p :results => results + if results.error? + status 500 + body({ "error" => results.error_message }.to_json) + next + end + + begin + json = results.results.to_json + p :json => json + content_type :json + status 200 + body json + rescue => e + p :exception => e + p e + raise e + end +end # get '/api/histogram' + diff --git a/lib/logstash/web/controllers/search.rb b/lib/logstash/web/controllers/search.rb new file mode 100644 index 000000000..0e2d86991 --- /dev/null +++ b/lib/logstash/web/controllers/search.rb @@ -0,0 +1,60 @@ + +get '/search' do + # TODO(sissel): Refactor this to use the api_search + 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 # get '/search' + diff --git a/lib/logstash/web/controllers/static_files.rb b/lib/logstash/web/controllers/static_files.rb new file mode 100644 index 000000000..ed4f4c9da --- /dev/null +++ b/lib/logstash/web/controllers/static_files.rb @@ -0,0 +1,36 @@ + +# Mizuno can't serve static files from a jar +# https://github.com/matadon/mizuno/issues/9 +#if __FILE__ =~ /^file:.+!.+$/ + get '/js/*' do static_file end + get '/css/*' do static_file end + get '/media/*' do static_file end + get '/ws/*' do static_file end +#else + ## If here, we aren't running from a jar; safe to serve files + ## through the normal public handler. + #set :public, "#{File.dirname(__FILE__)}/public" +#end + +def static_file + # request.path_info is the full path of the request. + path = File.join(File.dirname(__FILE__), "..", "public", *request.path_info.split("/")) + p :static => path + if File.exists?(path) + ext = path.split(".").last + case ext + when "js"; content_type "application/javascript" + when "css"; content_type "text/css" + when "jpg"; content_type "image/jpeg" + when "jpeg"; content_type "image/jpeg" + when "png"; content_type "image/png" + when "gif"; content_type "image/gif" + end + + body File.new(path, "r").read + else + status 404 + content_type "text/plain" + body "File not found: #{path}" + end +end # def static_file diff --git a/lib/logstash/web/server.rb b/lib/logstash/web/server.rb index e9675a539..b79570786 100755 --- a/lib/logstash/web/server.rb +++ b/lib/logstash/web/server.rb @@ -16,9 +16,15 @@ require "rack" # gem rack require "mizuno" # gem mizuno require "sinatra/base" # gem sinatra -module LogStash::Web; end - class LogStash::Web::Server < Sinatra::Base + mime_type :html, "text/html" + mime_type :txt, "text/plain" + mime_type :json, "text/plain" # so browsers don't "download" when viewed + mime_type :javascript, "application/javascript" + mime_type :gif, "image/gif" + mime_type :jpg, "image/jpeg" + mime_type :png, "image/png" + #register Sinatra::Async helpers Sinatra::RequireParam # logstash/web/helpers/require_param @@ -29,9 +35,20 @@ class LogStash::Web::Server < Sinatra::Base use Rack::CommonLogger use Rack::ShowExceptions + # We could do 'use' here, but 'use' is for middleware and it seems difficult + # (intentionally?) to share between middlewares. We'd need to share the + # instances variables like @backend, etc. + # + # Load anything in controllers/ + Dir.glob(File.join(File.dirname(__FILE__), "controllers", "**", "*")).each do |path| + puts "Loading #{path}" + # TODO(sissel): This is pretty shitty. + eval(File.new(path).read, binding, path) + end + def initialize(settings={}) super - # TODO(sissel): Support alternate backends + # TODO(sissel): Make this better. backend_url = URI.parse(settings.backend_url) case backend_url.scheme @@ -49,37 +66,6 @@ class LogStash::Web::Server < Sinatra::Base end # backend_url.scheme end # def initialize - # Mizuno can't serve static files from a jar - # https://github.com/matadon/mizuno/issues/9 - #if __FILE__ =~ /^file:.+!.+$/ - get '/js/*' do static_file end - get '/css/*' do static_file end - get '/media/*' do static_file end - get '/ws/*' do static_file end - #else - ## If here, we aren't running from a jar; safe to serve files - ## through the normal public handler. - #set :public, "#{File.dirname(__FILE__)}/public" - #end - - def static_file - # request.path_info is the full path of the request. - path = File.join(File.dirname(__FILE__), "public", *request.path_info.split("/")) - if File.exists?(path) - ext = path.split(".").last - case ext - when "js"; headers "Content-Type" => "application/javascript" - when "css"; headers "Content-Type" => "text/css" - when "jpg"; headers "Content-Type" => "image/jpeg" - when "jpeg"; headers "Content-Type" => "image/jpeg" - when "png"; headers "Content-Type" => "image/png" - when "gif"; headers "Content-Type" => "image/gif" - end - - body File.new(path, "r").read - end - end # def static_file - get '/style.css' do headers "Content-Type" => "text/css; charset=utf8" body sass :style @@ -89,200 +75,6 @@ class LogStash::Web::Server < Sinatra::Base redirect "/search" end # '/' - get '/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 # get '/search' - - post '/api/search' do - api_search - end # post /api/search - - get '/api/search' do - api_search - end # get /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. - body({ "error" => @results.error_message }.to_json) - 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 - count - @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" }) - pretty = params.has_key?("pretty") - if pretty - body JSON.pretty_generate(@results.to_hash) - else - body @results.to_json - end - end # case params[:format] - end # @backend.search - end # def api_search - - get '/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") # default json - field = (params[:field] or "@timestamp") # default @timestamp - interval = (params[:interval] or 3600000).to_i # default 1 hour - @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 # get '/api/histogram' - get '/*' do status 404 if @error body "Invalid path." @@ -327,11 +119,13 @@ end opts.parse! if settings.daemonize - if Process.fork == nil - Process.setsid - else - exit(0) - end + $stderr.puts "Daemonizing is not supported. (JRuby has no 'fork')" + exit(1) + #if Process.fork == nil + #Process.setsid + #else + #exit(0) + #end end if settings.logfile @@ -345,9 +139,6 @@ elsif settings.daemonize STDERR.reopen(devnull) end -#Rack::Handler::Thin.run( Mizuno::HttpServer.run( - #Rack::CommonLogger.new( \ - #Rack::ShowExceptions.new( \ - LogStash::Web::Server.new(settings), - :Port => settings.port, :Host => settings.address) + LogStash::Web::Server.new(settings), + :port => settings.port, :host => settings.address)