clarity 0.9.0
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/History.txt +4 -0
- data/Manifest.txt +37 -0
- data/PostInstall.txt +8 -0
- data/README.rdoc +56 -0
- data/Rakefile +27 -0
- data/bin/clarity +10 -0
- data/config/config.yml.sample +7 -0
- data/lib/clarity.rb +20 -0
- data/lib/clarity/cli.rb +80 -0
- data/lib/clarity/commands/command_builder.rb +56 -0
- data/lib/clarity/commands/tail_command_builder.rb +27 -0
- data/lib/clarity/parsers/hostname_parser.rb +43 -0
- data/lib/clarity/parsers/shop_parser.rb +48 -0
- data/lib/clarity/parsers/time_parser.rb +93 -0
- data/lib/clarity/renderers/log_renderer.rb +47 -0
- data/lib/clarity/server.rb +143 -0
- data/lib/clarity/server/basic_auth.rb +24 -0
- data/lib/clarity/server/chunk_http.rb +57 -0
- data/lib/clarity/server/mime_types.rb +23 -0
- data/public/images/spinner_big.gif +0 -0
- data/public/javascripts/app.js +154 -0
- data/public/stylesheets/app.css +54 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/commands/command_builder_test.rb +100 -0
- data/test/commands/tail_command_builder_test.rb +43 -0
- data/test/files/logfile.log +17 -0
- data/test/parsers/hostname_parser_test.rb +40 -0
- data/test/parsers/shop_parser_test.rb +54 -0
- data/test/parsers/time_parser_test.rb +84 -0
- data/test/test_helper.rb +3 -0
- data/test/test_string_scanner.rb +17 -0
- data/views/_header.html.erb +5 -0
- data/views/_toolbar.html.erb +89 -0
- data/views/error.html.erb +11 -0
- data/views/index.html.erb +9 -0
- metadata +134 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class LogRenderer
|
5
|
+
include ActionView::Helpers::TagHelper
|
6
|
+
include ActionView::Helpers::UrlHelper
|
7
|
+
|
8
|
+
Prefix = ""
|
9
|
+
Suffix = "<br/>\n"
|
10
|
+
TagOrder = [ :timestamp, :shop, :labels, :line ]
|
11
|
+
MarkTime = 60 * 5 # 5 minutes
|
12
|
+
|
13
|
+
def initialize()
|
14
|
+
@last_timestamp = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(elements = {})
|
18
|
+
@elements = elements
|
19
|
+
@tags = []
|
20
|
+
TagOrder.each do |tag|
|
21
|
+
if content = @elements.fetch(tag, nil)
|
22
|
+
method = ("tag_"+tag.to_s).to_sym
|
23
|
+
@tags << self.send(method, content)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@tags.empty? ? "" : Prefix + @tags.join(" ").to_s + Suffix
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def tag_timestamp(content, options = {})
|
32
|
+
content + " : "
|
33
|
+
end
|
34
|
+
|
35
|
+
def tag_line(content, options = {})
|
36
|
+
ERB::Util.h(content)
|
37
|
+
end
|
38
|
+
|
39
|
+
def tag_shop(content, options = {})
|
40
|
+
"[<a href='http://#{URI.escape(content)}'>#{content}</a>]"
|
41
|
+
end
|
42
|
+
|
43
|
+
def tag_labels(content, options = {})
|
44
|
+
"[#{content}]"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require File.dirname(__FILE__) + '/server/basic_auth'
|
3
|
+
require File.dirname(__FILE__) + '/server/mime_types'
|
4
|
+
require File.dirname(__FILE__) + '/server/chunk_http'
|
5
|
+
require File.dirname(__FILE__) + '/grep_renderer'
|
6
|
+
|
7
|
+
module Clarity
|
8
|
+
class NotFoundError < StandardError; end
|
9
|
+
class NotAuthenticatedError < StandardError; end
|
10
|
+
class InvalidParameterError < StandardError; end
|
11
|
+
|
12
|
+
module Server
|
13
|
+
include EventMachine::HttpServer
|
14
|
+
include Clarity::BasicAuth
|
15
|
+
include Clarity::ChunkHttp
|
16
|
+
|
17
|
+
attr_accessor :required_username, :required_password
|
18
|
+
attr_accessor :log_files
|
19
|
+
|
20
|
+
def self.run(options)
|
21
|
+
EventMachine::run do
|
22
|
+
EventMachine.epoll
|
23
|
+
EventMachine::start_server(options[:address], options[:port], self) do |a|
|
24
|
+
a.log_files = options[:log_files]
|
25
|
+
a.required_username = options[:username]
|
26
|
+
a.required_password = options[:password]
|
27
|
+
end
|
28
|
+
STDERR.puts "Listening #{options[:address]}:#{options[:port]}..."
|
29
|
+
STDERR.puts "Adding log files: #{options[:log_files].inspect}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_http_request
|
34
|
+
authenticate!
|
35
|
+
|
36
|
+
puts "action: #{path}"
|
37
|
+
puts "params: #{params.inspect}"
|
38
|
+
|
39
|
+
case path
|
40
|
+
when '/'
|
41
|
+
respond_with(200, welcome_page)
|
42
|
+
|
43
|
+
when '/perform'
|
44
|
+
if params.empty?
|
45
|
+
respond_with(200, welcome_page)
|
46
|
+
else
|
47
|
+
# get command
|
48
|
+
command = case params['tool']
|
49
|
+
when 'grep' then CommandBuilder.new(params).command
|
50
|
+
when 'tail' then TailCommandBuilder.new(params).command
|
51
|
+
else raise InvalidParameterError, "Invalid Tool parameter"
|
52
|
+
end
|
53
|
+
response = respond_with_chunks
|
54
|
+
response.chunk results_page # display page header
|
55
|
+
|
56
|
+
puts "Running: #{command}"
|
57
|
+
EventMachine::popen(command, GrepRenderer) do |grepper|
|
58
|
+
@grepper = grepper
|
59
|
+
@grepper.marker = 0
|
60
|
+
@grepper.params = params
|
61
|
+
@grepper.response = response
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
when '/test'
|
66
|
+
response = init_chunk_response
|
67
|
+
EventMachine::add_periodic_timer(1) do
|
68
|
+
response.chunk "Lorem ipsum dolor sit amet<br/>"
|
69
|
+
response.send_chunks
|
70
|
+
end
|
71
|
+
|
72
|
+
else
|
73
|
+
respond_with(200, public_file(path), :content_type => Mime.for(path))
|
74
|
+
end
|
75
|
+
|
76
|
+
rescue InvalidParameterError => e
|
77
|
+
respond_with(500, error_page(e))
|
78
|
+
rescue NotFoundError => e
|
79
|
+
respond_with(404, "<h1>Not Found</h1>")
|
80
|
+
rescue NotAuthenticatedError => e
|
81
|
+
puts "Could not authenticate user"
|
82
|
+
headers = { "WWW-Authenticate" => %(Basic realm="Clarity")}
|
83
|
+
respond_with(401, "HTTP Basic: Access denied.\n", :content_type => 'text/plain', :headers => headers)
|
84
|
+
end
|
85
|
+
|
86
|
+
def error_page(error)
|
87
|
+
@error = error
|
88
|
+
render "error.html.erb"
|
89
|
+
end
|
90
|
+
|
91
|
+
def welcome_page
|
92
|
+
render "index.html.erb"
|
93
|
+
end
|
94
|
+
|
95
|
+
def results_page
|
96
|
+
render "index.html.erb"
|
97
|
+
end
|
98
|
+
|
99
|
+
def unbind
|
100
|
+
return unless @grepper
|
101
|
+
kill_processes(@grepper.get_status.pid)
|
102
|
+
close_connection
|
103
|
+
end
|
104
|
+
|
105
|
+
def kill_processes(ppid)
|
106
|
+
return if ppid.nil?
|
107
|
+
all_pids = [ppid] + get_child_pids(ppid).flatten.uniq.compact
|
108
|
+
puts "=== pids are #{all_pids.inspect}"
|
109
|
+
all_pids.each do |pid|
|
110
|
+
Process.kill('TERM',pid.to_i)
|
111
|
+
puts "=== killing #{pid}"
|
112
|
+
end
|
113
|
+
rescue Exception => e
|
114
|
+
puts "!Error killing processes: #{e}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_child_pids(ppid)
|
118
|
+
out = `ps -opid,ppid | grep #{ppid.to_s}`
|
119
|
+
ids = out.split("\n").map {|line| $1 if line =~ /^\s*([0-9]+)\s.*/ }.compact
|
120
|
+
ids.delete(ppid.to_s)
|
121
|
+
if ids.empty?
|
122
|
+
ids
|
123
|
+
else
|
124
|
+
ids << ids.map {|id| get_child_pids(id) }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def authenticate!
|
131
|
+
login, pass = authentication_data
|
132
|
+
|
133
|
+
if (required_username && required_username != login) || (required_password && required_password != pass)
|
134
|
+
raise NotAuthenticatedError
|
135
|
+
end
|
136
|
+
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Clarity
|
2
|
+
module BasicAuth
|
3
|
+
|
4
|
+
def decode_credentials(request)
|
5
|
+
Base64.decode64(request).split.last
|
6
|
+
end
|
7
|
+
|
8
|
+
def user_name_and_password(request)
|
9
|
+
decode_credentials(request).split(/:/, 2)
|
10
|
+
end
|
11
|
+
|
12
|
+
def authentication_data
|
13
|
+
headers = @http_headers.split("\000")
|
14
|
+
auth_header = headers.detect {|head| head =~ /Authorization: / }
|
15
|
+
header = auth_header.nil? ? "" : auth_header.split("Authorization: Basic ").last
|
16
|
+
return (user_name_and_password(header) rescue ['', ''])
|
17
|
+
end
|
18
|
+
|
19
|
+
def authenticate!(http_header)
|
20
|
+
raise NotAuthenticatedError unless authenticate(http_header)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Clarity
|
2
|
+
|
3
|
+
module ChunkHttp
|
4
|
+
|
5
|
+
LeadIn = ' ' * 1024
|
6
|
+
|
7
|
+
def respond_with_chunks
|
8
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
9
|
+
response.status = 200
|
10
|
+
response.headers['Content-Type'] = 'text/html'
|
11
|
+
response.chunk LeadIn
|
12
|
+
response
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_with(status, content, options = {})
|
16
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
17
|
+
response.headers['Content-Type'] = options.fetch(:content_type, 'text/html')
|
18
|
+
response.headers['Cache-Control'] = 'private, max-age=0'
|
19
|
+
headers = options.fetch(:headers, {})
|
20
|
+
headers.each_pair {|h, v| response.headers[h] = v }
|
21
|
+
response.status = status
|
22
|
+
response.content = content
|
23
|
+
response.send_response
|
24
|
+
end
|
25
|
+
|
26
|
+
def render(view)
|
27
|
+
@toolbar = template("_toolbar.html.erb")
|
28
|
+
@content_for_header = template("_header.html.erb")
|
29
|
+
template(view)
|
30
|
+
end
|
31
|
+
|
32
|
+
def template(filename)
|
33
|
+
content = File.read( File.join(Clarity::Templates, filename) )
|
34
|
+
ERB.new(content).result(binding)
|
35
|
+
end
|
36
|
+
|
37
|
+
def public_file(filename)
|
38
|
+
File.read( File.join(Clarity::Public, filename) )
|
39
|
+
rescue Errno::ENOENT
|
40
|
+
raise NotFoundError
|
41
|
+
end
|
42
|
+
|
43
|
+
def logfiles
|
44
|
+
log_files.map {|f| Dir[f] }.flatten.compact.uniq.select{|f| File.file?(f) }.sort
|
45
|
+
end
|
46
|
+
|
47
|
+
def params
|
48
|
+
ENV['QUERY_STRING'].split('&').inject({}) {|p,s| k,v = s.split('=');p[k.to_s] = CGI.unescape(v.to_s);p}
|
49
|
+
end
|
50
|
+
|
51
|
+
def path
|
52
|
+
ENV["PATH_INFO"]
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Clarity
|
2
|
+
module Mime
|
3
|
+
def self.for(filename)
|
4
|
+
|
5
|
+
content_type = TYPES[File.extname(filename)]
|
6
|
+
content_type || 'text/plain'
|
7
|
+
end
|
8
|
+
|
9
|
+
TYPES = {
|
10
|
+
'.jpg' => 'image/jpg',
|
11
|
+
'.jpeg' => 'image/jpeg',
|
12
|
+
'.gif' => 'image/gif',
|
13
|
+
'.png' => 'image/png',
|
14
|
+
'.bmp' => 'image/bmp',
|
15
|
+
'.bitmap' => 'image/x-ms-bmp',
|
16
|
+
'.js' => 'application/javascript',
|
17
|
+
'.txt' => 'text/plain',
|
18
|
+
'.css' => 'text/css',
|
19
|
+
'.html' => 'text/html',
|
20
|
+
'.htm' => 'text/html'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
Binary file
|
@@ -0,0 +1,154 @@
|
|
1
|
+
/* ----------------------------------------------------------------------------
|
2
|
+
* Log Search Server JS
|
3
|
+
* by John Tajima
|
4
|
+
* requires jQuery 1.3.2
|
5
|
+
* ----------------------------------------------------------------------------
|
6
|
+
*/
|
7
|
+
|
8
|
+
|
9
|
+
var Search = {
|
10
|
+
search_form : 'search', // domId of the form
|
11
|
+
resultsId : 'results',
|
12
|
+
search_fields: [ 'term1', 'term2', 'term3' ], // domIds of search term fields
|
13
|
+
file_list : 'file-list', // domId of select for logfiles
|
14
|
+
logfiles : {}, // hash of log files
|
15
|
+
past_params : null, // recent request
|
16
|
+
url : '/perform',
|
17
|
+
scroll_fnId : null,
|
18
|
+
|
19
|
+
// initialize Search form
|
20
|
+
// { 'grep': [ log, files, for, grep], 'tail': [ 'log', 'files', 'for', 'tail']}
|
21
|
+
init: function(logfiles, params) {
|
22
|
+
this.logfiles = logfiles;
|
23
|
+
this.past_params = params;
|
24
|
+
|
25
|
+
this.bind_grep_tool();
|
26
|
+
this.bind_tail_tool();
|
27
|
+
this.bind_options();
|
28
|
+
|
29
|
+
if (!this.past_params) return; // return if no prev settings, nothing to set
|
30
|
+
|
31
|
+
// init tool selector
|
32
|
+
(this.past_params['tool'] == 'grep') ? $('#grep-label').trigger('click') : $('#tail-tool').trigger('click');
|
33
|
+
|
34
|
+
// init log file selector
|
35
|
+
$('#'+this.file_list).val(this.past_params['file']);
|
36
|
+
|
37
|
+
// init search fields
|
38
|
+
jQuery.each(this.search_fields, function(){
|
39
|
+
$('#'+this).val(Search.past_params[this]);
|
40
|
+
});
|
41
|
+
|
42
|
+
// advanced options usd?
|
43
|
+
// time was set - so show advanced options
|
44
|
+
if ((this.past_params['sh']) || (this.past_params['eh'])) {
|
45
|
+
this.showAdvanced();
|
46
|
+
if (this.past_params['sh']) {
|
47
|
+
jQuery.each(['sh', 'sm', 'ss'], function(){ $('#'+this).val(Search.past_params[this]) });
|
48
|
+
}
|
49
|
+
if (this.past_params['eh']) {
|
50
|
+
jQuery.each(['eh', 'em', 'es'], function(){ $('#'+this).val(Search.past_params[this]) });
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
},
|
55
|
+
|
56
|
+
// bind option selectors
|
57
|
+
bind_options: function() {
|
58
|
+
$('#auto-scroll').bind('change', function(){
|
59
|
+
Search.autoScroll(this.checked);
|
60
|
+
});
|
61
|
+
$('#auto-scroll').attr('checked', true).trigger('change'); // by default, turn on
|
62
|
+
},
|
63
|
+
|
64
|
+
// bind change grep tool
|
65
|
+
bind_grep_tool: function() {
|
66
|
+
$('#grep-tool').bind('change', function(e){
|
67
|
+
var newlist = ""
|
68
|
+
jQuery.each(Search.logfiles['grep'], function(){
|
69
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
70
|
+
});
|
71
|
+
$('#'+Search.file_list).html(newlist);
|
72
|
+
});
|
73
|
+
// watch clicking label as well
|
74
|
+
$('#grep-label').bind('click', function(e){
|
75
|
+
$('#grep-tool').attr('checked', 'checked').val('grep').trigger('change');
|
76
|
+
});
|
77
|
+
},
|
78
|
+
|
79
|
+
|
80
|
+
// bind change tail tool
|
81
|
+
bind_tail_tool: function() {
|
82
|
+
$('#tail-tool').bind('change', function(e){
|
83
|
+
var newlist = ""
|
84
|
+
jQuery.each(Search.logfiles['tail'], function(){
|
85
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
86
|
+
});
|
87
|
+
$('#'+ Search.file_list).html(newlist);
|
88
|
+
});
|
89
|
+
// watch clicking label as well
|
90
|
+
$('#tail-label').bind('click', function(e){
|
91
|
+
$('#tail-tool').attr('checked', 'checked').val('tail').trigger('change');
|
92
|
+
});
|
93
|
+
},
|
94
|
+
|
95
|
+
|
96
|
+
// clears the terms fields
|
97
|
+
clear: function() {
|
98
|
+
jQuery.each(this.search_fields, function(){
|
99
|
+
$('#'+this).val("");
|
100
|
+
});
|
101
|
+
},
|
102
|
+
|
103
|
+
showAdvanced: function() {
|
104
|
+
$('#enable-advanced').hide();
|
105
|
+
$('#disable-advanced').show();
|
106
|
+
$('.advanced-options').show();
|
107
|
+
},
|
108
|
+
|
109
|
+
hideAdvanced: function() {
|
110
|
+
this.clearAdvanced();
|
111
|
+
$('#enable-advanced').show();
|
112
|
+
$('#disable-advanced').hide();
|
113
|
+
$('.advanced-options').hide();
|
114
|
+
},
|
115
|
+
|
116
|
+
clearAdvanced: function() {
|
117
|
+
$('#advanced-options input').val("");
|
118
|
+
},
|
119
|
+
|
120
|
+
// gathers form elements and submits to proper url
|
121
|
+
submit: function() {
|
122
|
+
$('#'+this.search_form).submit();
|
123
|
+
$('#'+this.resultsId).html("Sending new query...");
|
124
|
+
},
|
125
|
+
|
126
|
+
//
|
127
|
+
// Misc utitilies
|
128
|
+
//
|
129
|
+
|
130
|
+
autoScroll: function(enabled) {
|
131
|
+
if (enabled == true) {
|
132
|
+
if (this.scroll_fnId) return; // already running
|
133
|
+
|
134
|
+
//console.log("scroll ON!")
|
135
|
+
window._currPos = 0; // init pos
|
136
|
+
this.scroll_fnId = setInterval( function(){
|
137
|
+
if (window._currPos < document.height) {
|
138
|
+
window.scrollTo(0, document.height);
|
139
|
+
window._currPos = document.height;
|
140
|
+
}
|
141
|
+
}, 100 );
|
142
|
+
} else {
|
143
|
+
if (!this.scroll_fnId) return;
|
144
|
+
//console.log("scroll off")
|
145
|
+
if (this.scroll_fnId) {
|
146
|
+
clearInterval(this.scroll_fnId);
|
147
|
+
window._currPost = 0;
|
148
|
+
this.scroll_fnId = null;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
};
|
154
|
+
|