logstash-lite 0.2.20110206003603 → 0.2.20110329105411
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|