logstash-lite 0.2.20110206003603 → 0.2.20110329105411
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/logstash-test +6 -0
- data/lib/logstash/filters/grok.rb +16 -2
- data/lib/logstash/inputs/amqp.rb +20 -11
- data/lib/logstash/namespace.rb +1 -0
- data/lib/logstash/outputs/amqp.rb +31 -11
- data/lib/logstash/outputs/elasticsearch.rb +43 -19
- data/lib/logstash/search/base.rb +39 -0
- data/lib/logstash/search/elasticsearch.rb +196 -0
- data/lib/logstash/search/facetresult.rb +25 -0
- data/lib/logstash/search/facetresult/entry.rb +6 -0
- data/lib/logstash/search/facetresult/histogram.rb +21 -0
- data/lib/logstash/search/query.rb +35 -0
- data/lib/logstash/search/result.rb +39 -0
- data/lib/logstash/search/twitter.rb +90 -0
- data/lib/logstash/web/helpers/require_param.rb +17 -0
- data/lib/logstash/web/public/js/logstash.js +81 -13
- data/lib/logstash/web/public/media/construction.gif +0 -0
- data/lib/logstash/web/public/media/throbber.gif +0 -0
- data/lib/logstash/web/public/media/truckconstruction.gif +0 -0
- data/lib/logstash/web/server.rb +170 -37
- data/lib/logstash/web/views/layout.haml +1 -1
- data/lib/logstash/web/views/search/ajax.haml +23 -17
- data/lib/logstash/web/views/search/error.haml +1 -1
- data/lib/logstash/web/views/search/error.txt.erb +4 -0
- data/lib/logstash/web/views/search/results.haml +3 -0
- data/lib/logstash/web/views/search/results.txt.erb +3 -4
- data/lib/logstash/web/views/style.sass +7 -1
- metadata +18 -6
- data/lib/logstash/web/lib/elasticsearch.rb +0 -85
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/logging"
|
4
|
+
|
5
|
+
class LogStash::Search::FacetResult
|
6
|
+
# Array of LogStash::Search::FacetResult::Entry
|
7
|
+
attr_accessor :results
|
8
|
+
|
9
|
+
# How long this query took, in seconds (or fractions of).
|
10
|
+
attr_accessor :duration
|
11
|
+
|
12
|
+
# Error message, if any.
|
13
|
+
attr_accessor :error_message
|
14
|
+
|
15
|
+
def initialize(settings={})
|
16
|
+
@results = []
|
17
|
+
@duration = nil
|
18
|
+
@error_message = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def error?
|
22
|
+
return !@error_message.nil?
|
23
|
+
end
|
24
|
+
end # class LogStash::Search::FacetResult
|
25
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
require "json"
|
3
|
+
require "logstash/search/facetresult/entry"
|
4
|
+
|
5
|
+
class LogStash::Search::FacetResult::Histogram < LogStash::Search::FacetResult::Entry
|
6
|
+
# The name or key for this result.
|
7
|
+
attr_accessor :key
|
8
|
+
attr_accessor :mean
|
9
|
+
attr_accessor :total
|
10
|
+
attr_accessor :count
|
11
|
+
|
12
|
+
# sometimes a parent call to to_json calls us with args?
|
13
|
+
def to_json(*args)
|
14
|
+
return {
|
15
|
+
"key" => @key,
|
16
|
+
"mean" => @mean,
|
17
|
+
"total" => @total,
|
18
|
+
"count" => @count,
|
19
|
+
}.to_json
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "logstash/namespace"
|
2
|
+
require "logstash/logging"
|
3
|
+
|
4
|
+
class LogStash::Search::Query
|
5
|
+
# The query string
|
6
|
+
attr_accessor :query_string
|
7
|
+
|
8
|
+
# The offset to start at (like SQL's SELECT ... OFFSET n)
|
9
|
+
attr_accessor :offset
|
10
|
+
|
11
|
+
# The max number of results to return. (like SQL's SELECT ... LIMIT n)
|
12
|
+
attr_accessor :count
|
13
|
+
|
14
|
+
# New query object.
|
15
|
+
#
|
16
|
+
# 'settings' should be a hash containing:
|
17
|
+
#
|
18
|
+
# * :query_string - a string query for searching
|
19
|
+
# * :offset - (optional, default 0) offset to search from
|
20
|
+
# * :count - (optional, default 50) max number of results to return
|
21
|
+
def initialize(settings)
|
22
|
+
@query_string = settings[:query_string]
|
23
|
+
@offset = settings[:offset] || 0
|
24
|
+
@count = settings[:count] || 50
|
25
|
+
end
|
26
|
+
|
27
|
+
# Class method. Parses a query string and returns
|
28
|
+
# a LogStash::Search::Query instance
|
29
|
+
def self.parse(query_string)
|
30
|
+
# TODO(sissel): I would prefer not to invent my own query language.
|
31
|
+
# Can we be similar to Lucene, SQL, or other query languages?
|
32
|
+
return self.new(:query_string => query_string)
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class LogStash::Search::Query
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "logstash/namespace"
|
2
|
+
require "logstash/logging"
|
3
|
+
|
4
|
+
class LogStash::Search::Result
|
5
|
+
# Array of LogStash::Event of results
|
6
|
+
attr_accessor :events
|
7
|
+
|
8
|
+
# How long this query took, in seconds (or fractions of).
|
9
|
+
attr_accessor :duration
|
10
|
+
|
11
|
+
# Offset in search
|
12
|
+
attr_accessor :offset
|
13
|
+
|
14
|
+
# Total records matched by this query, regardless of offset/count in query.
|
15
|
+
attr_accessor :total
|
16
|
+
|
17
|
+
# Error message, if any.
|
18
|
+
attr_accessor :error_message
|
19
|
+
|
20
|
+
def initialize(settings={})
|
21
|
+
@events = []
|
22
|
+
@duration = nil
|
23
|
+
@error_message = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def error?
|
27
|
+
return !@error_message.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json
|
31
|
+
return {
|
32
|
+
"events" => @events,
|
33
|
+
"duration" => @duration,
|
34
|
+
"offset" => @offset,
|
35
|
+
"total" => @total,
|
36
|
+
}.to_json
|
37
|
+
end # def to_json
|
38
|
+
end # class LogStash::Search::Result
|
39
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "em-http-request"
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/logging"
|
4
|
+
require "logstash/event"
|
5
|
+
require "logstash/search/base"
|
6
|
+
require "logstash/search/query"
|
7
|
+
require "logstash/search/result"
|
8
|
+
require "logstash/search/facetresult"
|
9
|
+
require "logstash/search/facetresult/histogram"
|
10
|
+
|
11
|
+
class LogStash::Search::Twitter < LogStash::Search::Base
|
12
|
+
public
|
13
|
+
def initialize(settings={})
|
14
|
+
@host = (settings[:host] || "search.twitter.com")
|
15
|
+
@port = (settings[:port] || 80).to_i
|
16
|
+
@logger = LogStash::Logger.new(STDOUT)
|
17
|
+
end
|
18
|
+
|
19
|
+
public
|
20
|
+
def search(query)
|
21
|
+
raise "No block given for search call." if !block_given?
|
22
|
+
if query.is_a?(String)
|
23
|
+
query = LogStash::Search::Query.parse(query)
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO(sissel): only search a specific index?
|
27
|
+
http = EventMachine::HttpRequest.new("http://#{@host}:#{@port}/search.json?q=#{URI.escape(query.query_string)}&rpp=#{URI.escape(query.count) rescue query.count}")
|
28
|
+
|
29
|
+
@logger.info(["Query", query])
|
30
|
+
|
31
|
+
start_time = Time.now
|
32
|
+
req = http.get
|
33
|
+
|
34
|
+
result = LogStash::Search::Result.new
|
35
|
+
req.callback do
|
36
|
+
data = JSON.parse(req.response)
|
37
|
+
result.duration = Time.now - start_time
|
38
|
+
|
39
|
+
hits = (data["results"] || nil) rescue nil
|
40
|
+
|
41
|
+
if hits.nil? or !data["error"].nil?
|
42
|
+
# Use the error message if any, otherwise, return the whole
|
43
|
+
# data object as json as the error message for debugging later.
|
44
|
+
result.error_message = (data["error"] rescue false) || data.to_json
|
45
|
+
yield result
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
hits.each do |hit|
|
50
|
+
hit["@message"] = hit["text"]
|
51
|
+
hit["@timestamp"] = hit["created_at"]
|
52
|
+
hit.delete("text")
|
53
|
+
end
|
54
|
+
|
55
|
+
@logger.info(["Got search results",
|
56
|
+
{ :query => query.query_string, :duration => data["duration"],
|
57
|
+
:result_count => hits.size }])
|
58
|
+
|
59
|
+
if req.response_header.status != 200
|
60
|
+
result.error_message = data["error"] || req.inspect
|
61
|
+
@error = data["error"] || req.inspect
|
62
|
+
end
|
63
|
+
|
64
|
+
# We want to yield a list of LogStash::Event objects.
|
65
|
+
hits.each do |hit|
|
66
|
+
result.events << LogStash::Event.new(hit)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Total hits this search could find if not limited
|
70
|
+
result.total = hits.size
|
71
|
+
result.offset = 0
|
72
|
+
|
73
|
+
yield result
|
74
|
+
end
|
75
|
+
|
76
|
+
req.errback do
|
77
|
+
@logger.warn(["Query failed", query, req, req.response])
|
78
|
+
result.duration = Time.now - start_time
|
79
|
+
result.error_message = req.response
|
80
|
+
|
81
|
+
yield result
|
82
|
+
end
|
83
|
+
end # def search
|
84
|
+
|
85
|
+
def histogram(query, field, interval=nil)
|
86
|
+
# Nothing to histogram.
|
87
|
+
result = LogStash::Search::FacetResult.new
|
88
|
+
yield result
|
89
|
+
end
|
90
|
+
end # class LogStash::Search::ElasticSearch
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
module RequireParam
|
5
|
+
def require_param(*fields)
|
6
|
+
missing = []
|
7
|
+
fields.each do |field|
|
8
|
+
if params[field].nil?
|
9
|
+
missing << field
|
10
|
+
end
|
11
|
+
end
|
12
|
+
return missing
|
13
|
+
end # def require_param
|
14
|
+
end # module RequireParam
|
15
|
+
|
16
|
+
helpers RequireParam
|
17
|
+
end # module Sinatra
|
@@ -1,4 +1,6 @@
|
|
1
1
|
(function() {
|
2
|
+
// TODO(sissel): Write something that will use history.pushState and fall back
|
3
|
+
// to document.location.hash madness.
|
2
4
|
|
3
5
|
var logstash = {
|
4
6
|
params: {
|
@@ -6,21 +8,79 @@
|
|
6
8
|
count: 50,
|
7
9
|
},
|
8
10
|
|
9
|
-
search: function(query) {
|
11
|
+
search: function(query, options) {
|
10
12
|
if (query == undefined || query == "") {
|
11
13
|
return;
|
12
14
|
}
|
13
|
-
|
15
|
+
|
16
|
+
/* Default options */
|
17
|
+
if (typeof(options) == 'undefined') {
|
18
|
+
options = { graph: true };
|
19
|
+
}
|
14
20
|
|
15
21
|
var display_query = query.replace("<", "<").replace(">", ">")
|
16
|
-
$("#querystatus").html("Loading query '" + display_query + "'")
|
22
|
+
$("#querystatus, #results h1").html("Loading query '" + display_query + "' (offset:" + logstash.params.offset + ", count:" + logstash.params.count + ") <img class='throbber' src='/media/construction.gif'>")
|
17
23
|
//console.log(logstash.params)
|
18
24
|
logstash.params.q = query;
|
19
25
|
document.location.hash = escape(JSON.stringify(logstash.params));
|
20
|
-
|
26
|
+
|
27
|
+
/* Load the search results */
|
28
|
+
$("#results").load("/api/search?format=html", logstash.params);
|
29
|
+
|
30
|
+
if (options.graph != false) {
|
31
|
+
/* Load the default histogram graph */
|
32
|
+
logstash.params.interval = 3600000; /* 1 hour, default */
|
33
|
+
logstash.histogram();
|
34
|
+
} /* if options.graph != false */
|
21
35
|
$("#query").val(logstash.params.q);
|
22
36
|
}, /* search */
|
23
37
|
|
38
|
+
histogram: function(tries) {
|
39
|
+
if (typeof(tries) == 'undefined') {
|
40
|
+
tries = 7;
|
41
|
+
}
|
42
|
+
|
43
|
+
/* GeoCities mode on the graph while waiting ...
|
44
|
+
* This won't likely survive 1.0, but it's fun for now... */
|
45
|
+
$("#visual").html("<center><img src='/media/truckconstruction.gif'><center>");
|
46
|
+
|
47
|
+
jQuery.getJSON("/api/histogram", logstash.params, function(histogram, text, jqxhr) {
|
48
|
+
/* Load the data into the graph */
|
49
|
+
var flot_data = [];
|
50
|
+
// histogram is an array of { "key": ..., "count": ... }
|
51
|
+
for (var i in histogram) {
|
52
|
+
flot_data.push([parseInt(histogram[i]["key"]), histogram[i]["count"]])
|
53
|
+
}
|
54
|
+
logstash.plot(flot_data, logstash.params.interval);
|
55
|
+
//console.log(histogram);
|
56
|
+
|
57
|
+
/* Try to be intelligent about how we choose the histogram interval.
|
58
|
+
* If there are too few data points, try a smaller interval.
|
59
|
+
* If there are too many data points, try a larger interval.
|
60
|
+
* Give up after a few tries and go with the last result.
|
61
|
+
*
|
62
|
+
* This queries the backend several times, but should be reasonably
|
63
|
+
* speedy as this behaves roughly as a binary search. */
|
64
|
+
//if (flot_data.length < 6 && flot_data.length > 0 && tries > 0) {
|
65
|
+
//console.log("Histogram bucket " + logstash.params.interval + " has only " + flot_data.length + " data points, trying smaller...");
|
66
|
+
//logstash.params.interval /= 2;
|
67
|
+
//if (logstash.params.interval < 1000) {
|
68
|
+
//tries = 0; /* stop trying, too small... */
|
69
|
+
//logstash.plot(flot_data, logstash.params.interval);
|
70
|
+
//return;
|
71
|
+
//}
|
72
|
+
//logstash.histogram(tries - 1);
|
73
|
+
//} else if (flot_data.length > 50 && tries > 0) {
|
74
|
+
//console.log("Histogram bucket " + logstash.params.interval + " too many (" + flot_data.length + ") data points, trying larger interval...");
|
75
|
+
//logstash.params.interval *= 2;
|
76
|
+
//logstash.histogram(tries - 1);
|
77
|
+
//} else {
|
78
|
+
//console.log("Histo:" + logstash.params.interval);
|
79
|
+
//logstash.plot(flot_data, logstash.params.interval);
|
80
|
+
//}
|
81
|
+
});
|
82
|
+
},
|
83
|
+
|
24
84
|
parse_params: function(href) {
|
25
85
|
var query = href.replace(/^[^?]*\?/, "");
|
26
86
|
if (query == href) {
|
@@ -48,14 +108,15 @@
|
|
48
108
|
logstash.search(newquery.trim());
|
49
109
|
}, /* appendquery */
|
50
110
|
|
51
|
-
plot: function(data) {
|
111
|
+
plot: function(data, interval) {
|
52
112
|
var target = $("#visual");
|
113
|
+
target.css("display", "block");
|
53
114
|
var plot = $.plot(target,
|
54
115
|
[ { /* data */
|
55
116
|
data: data,
|
56
117
|
bars: {
|
57
118
|
show: true,
|
58
|
-
barWidth:
|
119
|
+
barWidth: interval,
|
59
120
|
}
|
60
121
|
} ],
|
61
122
|
{ /* options */
|
@@ -67,8 +128,12 @@
|
|
67
128
|
target.bind("plotclick", function(e, pos, item) {
|
68
129
|
if (item) {
|
69
130
|
start = logstash.ms_to_iso8601(item.datapoint[0]);
|
70
|
-
end = logstash.ms_to_iso8601(item.datapoint[0] +
|
131
|
+
end = logstash.ms_to_iso8601(item.datapoint[0] + interval);
|
71
132
|
|
133
|
+
/* Clicking on the graph means a new search, means
|
134
|
+
* we probably don't want to keep the old offset since
|
135
|
+
* the search results will change. */
|
136
|
+
logstash.params.offset = 0;
|
72
137
|
logstash.appendquery("@timestamp:[" + start + " TO " + end + "]");
|
73
138
|
}
|
74
139
|
});
|
@@ -125,13 +190,16 @@
|
|
125
190
|
for (var p in params) {
|
126
191
|
logstash.params[p] = params[p];
|
127
192
|
}
|
128
|
-
logstash.search(logstash.params.q)
|
193
|
+
logstash.search(logstash.params.q, { graph: false })
|
129
194
|
return false;
|
130
195
|
});
|
131
196
|
|
132
197
|
var result_row_selector = "table.results tr.event";
|
133
198
|
$(result_row_selector).live("click", function() {
|
134
|
-
var data =
|
199
|
+
var data = $("td.message", this).data("full");
|
200
|
+
if (typeof(data) == "string") {
|
201
|
+
data = JSON.parse(data);
|
202
|
+
}
|
135
203
|
|
136
204
|
/* Apply template to the dialog */
|
137
205
|
var query = $("#query").val().replace(/^\s+|\s+$/g, "")
|
@@ -155,8 +223,8 @@
|
|
155
223
|
|
156
224
|
/* TODO(sissel): recurse through the data */
|
157
225
|
var fields = new Array();
|
158
|
-
for (var i in data
|
159
|
-
var value = data
|
226
|
+
for (var i in data["@fields"]) {
|
227
|
+
var value = data["@fields"][i]
|
160
228
|
if (/^[, ]*$/.test(value)) {
|
161
229
|
continue; /* Skip empty data fields */
|
162
230
|
}
|
@@ -166,9 +234,9 @@
|
|
166
234
|
fields.push( { type: "field", field: i, value: value })
|
167
235
|
}
|
168
236
|
|
169
|
-
for (var i in data
|
237
|
+
for (var i in data) {
|
170
238
|
if (i == "@fields") continue;
|
171
|
-
var value = data
|
239
|
+
var value = data[i]
|
172
240
|
if (!(value instanceof Array)) {
|
173
241
|
value = [value];
|
174
242
|
}
|
Binary file
|
Binary file
|
Binary file
|
data/lib/logstash/web/server.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# I don't want folks to have to learn to use yet another tool (rackup)
|
3
|
+
# just to launch logstash-web. So let's work like a standard ruby
|
4
|
+
# executable.
|
2
5
|
##rackup -Ilib:../lib -s thin
|
3
6
|
|
4
7
|
$:.unshift("%s/../lib" % File.dirname(__FILE__))
|
@@ -6,22 +9,49 @@ $:.unshift(File.dirname(__FILE__))
|
|
6
9
|
|
7
10
|
require "eventmachine"
|
8
11
|
require "json"
|
9
|
-
require "
|
12
|
+
require "logstash/search/elasticsearch"
|
13
|
+
require "logstash/search/query"
|
10
14
|
require "logstash/namespace"
|
11
15
|
require "rack"
|
12
16
|
require "rubygems"
|
13
17
|
require "sinatra/async"
|
18
|
+
require "logstash/web/helpers/require_param"
|
14
19
|
|
15
20
|
class EventMachine::ConnectionError < RuntimeError; end
|
21
|
+
module LogStash::Web; end
|
16
22
|
|
17
23
|
class LogStash::Web::Server < Sinatra::Base
|
18
24
|
register Sinatra::Async
|
25
|
+
helpers Sinatra::RequireParam # logstash/web/helpers/require_param
|
26
|
+
|
19
27
|
set :haml, :format => :html5
|
20
28
|
set :logging, true
|
21
29
|
set :public, "#{File.dirname(__FILE__)}/public"
|
22
30
|
set :views, "#{File.dirname(__FILE__)}/views"
|
23
|
-
elasticsearch = LogStash::Web::ElasticSearch.new
|
24
31
|
|
32
|
+
use Rack::CommonLogger
|
33
|
+
#use Rack::ShowExceptions
|
34
|
+
|
35
|
+
def initialize(settings={})
|
36
|
+
super
|
37
|
+
# TODO(sissel): Support alternate backends
|
38
|
+
backend_url = URI.parse(settings.backend_url)
|
39
|
+
|
40
|
+
case backend_url.scheme
|
41
|
+
when "elasticsearch"
|
42
|
+
@backend = LogStash::Search::ElasticSearch.new(
|
43
|
+
:host => backend_url.host,
|
44
|
+
:port => backend_url.port
|
45
|
+
)
|
46
|
+
when "twitter"
|
47
|
+
require "logstash/search/twitter"
|
48
|
+
@backend = LogStash::Search::Twitter.new(
|
49
|
+
:host => backend_url.host,
|
50
|
+
:port => backend_url.port
|
51
|
+
)
|
52
|
+
end # backend_url.scheme
|
53
|
+
end # def initialize
|
54
|
+
|
25
55
|
aget '/style.css' do
|
26
56
|
headers "Content-Type" => "text/css; charset=utf8"
|
27
57
|
body sass :style
|
@@ -32,8 +62,11 @@ class LogStash::Web::Server < Sinatra::Base
|
|
32
62
|
end # '/'
|
33
63
|
|
34
64
|
aget '/search' do
|
35
|
-
result_callback = proc do
|
65
|
+
result_callback = proc do |results|
|
36
66
|
status 500 if @error
|
67
|
+
@results = results
|
68
|
+
|
69
|
+
p :got => results
|
37
70
|
|
38
71
|
params[:format] ||= "html"
|
39
72
|
case params[:format]
|
@@ -48,10 +81,10 @@ class LogStash::Web::Server < Sinatra::Base
|
|
48
81
|
body erb :"search/results.txt", :layout => false
|
49
82
|
when "json"
|
50
83
|
headers({"Content-Type" => "text/plain" })
|
84
|
+
# TODO(sissel): issue/30 - needs refactoring here.
|
51
85
|
hits = @hits.collect { |h| h["_source"] }
|
52
86
|
response = {
|
53
87
|
"hits" => hits,
|
54
|
-
"facets" => (@results["facets"] rescue nil),
|
55
88
|
}
|
56
89
|
|
57
90
|
response["error"] = @error if @error
|
@@ -63,43 +96,79 @@ class LogStash::Web::Server < Sinatra::Base
|
|
63
96
|
# have javascript enabled, we need to show the results in
|
64
97
|
# case a user doesn't have javascript.
|
65
98
|
if params[:q] and params[:q] != ""
|
66
|
-
|
67
|
-
|
68
|
-
|
99
|
+
query = LogStash::Search::Query.new(
|
100
|
+
:query_string => params[:q],
|
101
|
+
:offset => params[:offset],
|
102
|
+
:count => params[:count]
|
103
|
+
)
|
104
|
+
|
105
|
+
@backend.search(query) do |results|
|
106
|
+
p :got => results
|
69
107
|
begin
|
70
|
-
result_callback.call
|
108
|
+
result_callback.call results
|
71
109
|
rescue => e
|
72
|
-
|
110
|
+
p :exception => e
|
73
111
|
end
|
74
|
-
end #
|
112
|
+
end # @backend.search
|
75
113
|
else
|
76
|
-
|
77
|
-
|
78
|
-
|
114
|
+
results = LogStash::Search::Result.new(
|
115
|
+
:events => [],
|
116
|
+
:error_message => "No query given"
|
117
|
+
)
|
118
|
+
result_callback.call results
|
79
119
|
end
|
80
120
|
end # aget '/search'
|
81
121
|
|
82
|
-
apost '/search
|
122
|
+
apost '/api/search' do
|
123
|
+
api_search
|
124
|
+
end # apost /api/search
|
125
|
+
|
126
|
+
aget '/api/search' do
|
127
|
+
api_search
|
128
|
+
end # aget /api/search
|
129
|
+
|
130
|
+
def api_search
|
131
|
+
|
83
132
|
headers({"Content-Type" => "text/html" })
|
84
133
|
count = params["count"] = (params["count"] or 50).to_i
|
85
134
|
offset = params["offset"] = (params["offset"] or 0).to_i
|
86
|
-
|
135
|
+
format = (params[:format] or "json")
|
136
|
+
|
137
|
+
query = LogStash::Search::Query.new(
|
138
|
+
:query_string => params[:q],
|
139
|
+
:offset => offset,
|
140
|
+
:count => count
|
141
|
+
)
|
142
|
+
|
143
|
+
@backend.search(query) do |results|
|
87
144
|
@results = results
|
88
|
-
if @results.
|
89
|
-
|
145
|
+
if @results.error?
|
146
|
+
status 500
|
147
|
+
case format
|
148
|
+
when "html"
|
149
|
+
headers({"Content-Type" => "text/html" })
|
150
|
+
body haml :"search/error", :layout => !request.xhr?
|
151
|
+
when "text"
|
152
|
+
headers({"Content-Type" => "text/plain" })
|
153
|
+
body erb :"search/error.txt", :layout => false
|
154
|
+
when "txt"
|
155
|
+
headers({"Content-Type" => "text/plain" })
|
156
|
+
body erb :"search/error.txt", :layout => false
|
157
|
+
when "json"
|
158
|
+
headers({"Content-Type" => "text/plain" })
|
159
|
+
# TODO(sissel): issue/30 - needs refactoring here.
|
160
|
+
if @results.error?
|
161
|
+
body({ "error" => @results.error_message }.to_json)
|
162
|
+
else
|
163
|
+
body @results.to_json
|
164
|
+
end
|
165
|
+
end # case params[:format]
|
90
166
|
next
|
91
167
|
end
|
92
168
|
|
93
|
-
@
|
94
|
-
@total = (@results
|
95
|
-
|
96
|
-
begin
|
97
|
-
@results["facets"]["by_hour"]["entries"].each do |entry|
|
98
|
-
@graphpoints << [entry["key"], entry["count"]]
|
99
|
-
end
|
100
|
-
rescue => e
|
101
|
-
puts e
|
102
|
-
end
|
169
|
+
@events = @results.events
|
170
|
+
@total = (@results.total rescue 0)
|
171
|
+
count = @results.events.size
|
103
172
|
|
104
173
|
if count and offset
|
105
174
|
if @total > (count + offset)
|
@@ -115,7 +184,7 @@ class LogStash::Web::Server < Sinatra::Base
|
|
115
184
|
next_params["offset"] = [offset + count, @total - count].min
|
116
185
|
@next_href = "?" + next_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
117
186
|
last_params = next_params.clone
|
118
|
-
last_params["offset"] = @total -
|
187
|
+
last_params["offset"] = @total - count
|
119
188
|
@last_href = "?" + last_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
120
189
|
end
|
121
190
|
|
@@ -124,24 +193,83 @@ class LogStash::Web::Server < Sinatra::Base
|
|
124
193
|
prev_params["offset"] = [offset - count, 0].max
|
125
194
|
@prev_href = "?" + prev_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
126
195
|
|
127
|
-
if prev_params["offset"] > 0
|
196
|
+
#if prev_params["offset"] > 0
|
128
197
|
first_params = prev_params.clone
|
129
198
|
first_params["offset"] = 0
|
130
199
|
@first_href = "?" + first_params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
131
|
-
end
|
200
|
+
#end
|
132
201
|
end
|
133
202
|
|
134
|
-
|
135
|
-
|
136
|
-
|
203
|
+
# TODO(sissel): make a helper function taht goes hash -> cgi querystring
|
204
|
+
@refresh_href = "?" + params.collect { |k,v| [URI.escape(k.to_s), URI.escape(v.to_s)].join("=") }.join("&")
|
205
|
+
|
206
|
+
case format
|
207
|
+
when "html"
|
208
|
+
headers({"Content-Type" => "text/html" })
|
209
|
+
body haml :"search/ajax", :layout => !request.xhr?
|
210
|
+
when "text"
|
211
|
+
headers({"Content-Type" => "text/plain" })
|
212
|
+
body erb :"search/results.txt", :layout => false
|
213
|
+
when "txt"
|
214
|
+
headers({"Content-Type" => "text/plain" })
|
215
|
+
body erb :"search/results.txt", :layout => false
|
216
|
+
when "json"
|
217
|
+
headers({"Content-Type" => "text/plain" })
|
218
|
+
# TODO(sissel): issue/30 - needs refactoring here.
|
219
|
+
response = @results
|
220
|
+
body response.to_json
|
221
|
+
end # case params[:format]
|
222
|
+
end # @backend.search
|
223
|
+
end # def api_search
|
224
|
+
|
225
|
+
aget '/api/histogram' do
|
226
|
+
headers({"Content-Type" => "text/plain" })
|
227
|
+
missing = require_param(:q)
|
228
|
+
if !missing.empty?
|
229
|
+
status 500
|
230
|
+
body({ "error" => "Missing requiremed parameters",
|
231
|
+
"missing" => missing }.to_json)
|
232
|
+
next
|
233
|
+
end # if !missing.empty?
|
234
|
+
|
235
|
+
format = (params[:format] or "json") # default json
|
236
|
+
field = (params[:field] or "@timestamp") # default @timestamp
|
237
|
+
interval = (params[:interval] or 3600000).to_i # default 1 hour
|
238
|
+
@backend.histogram(params[:q], field, interval) do |results|
|
239
|
+
@results = results
|
240
|
+
if @results.error?
|
241
|
+
status 500
|
242
|
+
body({ "error" => @results.error_message }.to_json)
|
243
|
+
next
|
244
|
+
end
|
245
|
+
|
246
|
+
begin
|
247
|
+
a = results.results.to_json
|
248
|
+
rescue => e
|
249
|
+
status 500
|
250
|
+
body e.inspect
|
251
|
+
p :exception => e
|
252
|
+
p e
|
253
|
+
raise e
|
254
|
+
end
|
255
|
+
status 200
|
256
|
+
body a
|
257
|
+
end # @backend.search
|
258
|
+
end # aget '/api/histogram'
|
259
|
+
|
260
|
+
aget '/*' do
|
261
|
+
status 404 if @error
|
262
|
+
body "Invalid path."
|
263
|
+
end # aget /*
|
137
264
|
end # class LogStash::Web::Server
|
138
265
|
|
139
266
|
require "optparse"
|
140
|
-
Settings = Struct.new(:daemonize, :logfile, :address, :port)
|
267
|
+
Settings = Struct.new(:daemonize, :logfile, :address, :port, :backend_url)
|
141
268
|
settings = Settings.new
|
142
269
|
|
143
|
-
settings.address
|
144
|
-
settings.port
|
270
|
+
settings.address = "0.0.0.0"
|
271
|
+
settings.port = 9292
|
272
|
+
settings.backend_url = "elasticsearch://localhost:9200/"
|
145
273
|
|
146
274
|
progname = File.basename($0)
|
147
275
|
|
@@ -163,6 +291,11 @@ opts = OptionParser.new do |opts|
|
|
163
291
|
opts.on("-p", "--port PORT", "Port on which to start webserver. Default is 9292.") do |port|
|
164
292
|
settings.port = port.to_i
|
165
293
|
end
|
294
|
+
|
295
|
+
opts.on("-b", "--backend URL",
|
296
|
+
"The backend URL to use. Default is elasticserach://localhost:9200/") do |url|
|
297
|
+
settings.backend_url = url
|
298
|
+
end
|
166
299
|
end
|
167
300
|
|
168
301
|
opts.parse!
|
@@ -189,5 +322,5 @@ end
|
|
189
322
|
Rack::Handler::Thin.run(
|
190
323
|
Rack::CommonLogger.new( \
|
191
324
|
Rack::ShowExceptions.new( \
|
192
|
-
LogStash::Web::Server.new)),
|
325
|
+
LogStash::Web::Server.new(settings))),
|
193
326
|
:Port => settings.port, :Host => settings.address)
|