clarity 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|