hq-log-monitor-server 0.2.2 → 0.3.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/features/event-page.feature +67 -0
- data/features/{overview.feature → overview-page.feature} +2 -2
- data/features/support/env.rb +28 -0
- data/features/support/steps.rb +51 -27
- data/features/support/ui-steps.rb +50 -0
- data/lib/hq/log-monitor-server/do-checks.rb +116 -0
- data/lib/hq/log-monitor-server/event-page.rb +174 -0
- data/lib/hq/log-monitor-server/logic.rb +89 -0
- data/lib/hq/log-monitor-server/overview-page.rb +100 -0
- data/lib/hq/log-monitor-server/script.rb +12 -628
- data/lib/hq/log-monitor-server/service-host-page.rb +109 -0
- data/lib/hq/log-monitor-server/service-page.rb +112 -0
- data/lib/hq/log-monitor-server/submit-log-event.rb +58 -0
- metadata +70 -33
- checksums.yaml +0 -7
@@ -0,0 +1,67 @@
|
|
1
|
+
Feature: View and manipulate a single event
|
2
|
+
|
3
|
+
Background:
|
4
|
+
|
5
|
+
Given the log monitor server config:
|
6
|
+
"""
|
7
|
+
<log-monitor-server-config>
|
8
|
+
<server port="${port}"/>
|
9
|
+
<db host="${db-host}" port="${db-port}" name="${db-name}"/>
|
10
|
+
<icinga command-file="${command-file}">
|
11
|
+
<service name="service" icinga-host="host" icinga-service="service">
|
12
|
+
<type name="warning" level="warning"/>
|
13
|
+
<type name="critical" level="critical"/>
|
14
|
+
</service>
|
15
|
+
</icinga>
|
16
|
+
</log-monitor-server-config>
|
17
|
+
"""
|
18
|
+
|
19
|
+
And the time is 100
|
20
|
+
|
21
|
+
And I submit the following event:
|
22
|
+
"""
|
23
|
+
{
|
24
|
+
type: warning,
|
25
|
+
source: { class: class, host: host, service: service },
|
26
|
+
location: { file: logfile, line: 0 },
|
27
|
+
lines: {
|
28
|
+
before: [],
|
29
|
+
matching: WARNING blah,
|
30
|
+
after: [],
|
31
|
+
}
|
32
|
+
}
|
33
|
+
"""
|
34
|
+
|
35
|
+
Scenario: View an event
|
36
|
+
|
37
|
+
When I visit /event/${event-id}
|
38
|
+
|
39
|
+
Then I should see the event
|
40
|
+
And the event status should be "unseen"
|
41
|
+
|
42
|
+
And the summary new should be 1
|
43
|
+
And the summary total should be 1
|
44
|
+
And the summary new for type "warning" should be 1
|
45
|
+
And the summary total for type "warning" should be 1
|
46
|
+
|
47
|
+
Scenario: Mark as seen
|
48
|
+
|
49
|
+
Given the time is 200
|
50
|
+
|
51
|
+
When I visit /event/${event-id}
|
52
|
+
And I click "mark as seen"
|
53
|
+
|
54
|
+
Then I should see the event
|
55
|
+
And the event status should be "seen"
|
56
|
+
|
57
|
+
And the summary new should be 0
|
58
|
+
And the summary total should be 1
|
59
|
+
And the summary new for type "warning" should be 0
|
60
|
+
And the summary total for type "warning" should be 1
|
61
|
+
|
62
|
+
And icinga should receive:
|
63
|
+
"""
|
64
|
+
[100] PROCESS_SERVICE_CHECK_RESULT;host;service;0;OK no new events
|
65
|
+
[100] PROCESS_SERVICE_CHECK_RESULT;host;service;1;WARNING 1 warning
|
66
|
+
[200] PROCESS_SERVICE_CHECK_RESULT;host;service;0;OK no new events
|
67
|
+
"""
|
@@ -14,7 +14,7 @@ Feature: Log monitor server overview
|
|
14
14
|
|
15
15
|
Scenario: No events
|
16
16
|
|
17
|
-
When I visit
|
17
|
+
When I visit /
|
18
18
|
|
19
19
|
Then I should see no summaries
|
20
20
|
|
@@ -36,7 +36,7 @@ Feature: Log monitor server overview
|
|
36
36
|
},
|
37
37
|
"""
|
38
38
|
|
39
|
-
When I visit
|
39
|
+
When I visit /
|
40
40
|
|
41
41
|
Then I should see 1 summary
|
42
42
|
And the 1st summary should be:
|
data/features/support/env.rb
CHANGED
@@ -33,6 +33,34 @@ def mongo_db name
|
|
33
33
|
mongo_conn[mongo_db_name name]
|
34
34
|
end
|
35
35
|
|
36
|
+
def get_event event_id
|
37
|
+
|
38
|
+
db =
|
39
|
+
mongo_db("logMonitorServer")
|
40
|
+
|
41
|
+
event =
|
42
|
+
db["events"].find({
|
43
|
+
"_id" => event_id,
|
44
|
+
}).first
|
45
|
+
|
46
|
+
return event
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_summary source
|
51
|
+
|
52
|
+
db =
|
53
|
+
mongo_db("logMonitorServer")
|
54
|
+
|
55
|
+
summary =
|
56
|
+
db["summaries"].find({
|
57
|
+
"_id" => source,
|
58
|
+
}).first
|
59
|
+
|
60
|
+
return summary
|
61
|
+
|
62
|
+
end
|
63
|
+
|
36
64
|
After do
|
37
65
|
|
38
66
|
@db_names.each do
|
data/features/support/steps.rb
CHANGED
@@ -75,6 +75,16 @@ When /^I submit the following events?:$/ do
|
|
75
75
|
|
76
76
|
@submitted_events = events_data
|
77
77
|
|
78
|
+
# store information about the event (assuming it's the only one)
|
79
|
+
|
80
|
+
db = mongo_db("logMonitorServer")
|
81
|
+
event = db["events"].find.first
|
82
|
+
|
83
|
+
if event
|
84
|
+
@event_id = event["_id"]
|
85
|
+
@source = event["source"]
|
86
|
+
end
|
87
|
+
|
78
88
|
end
|
79
89
|
|
80
90
|
Then /^I should receive a (\d+) response$/ do
|
@@ -85,14 +95,15 @@ end
|
|
85
95
|
Then /^the event should be in the database$/ do
|
86
96
|
|
87
97
|
db = mongo_db("logMonitorServer")
|
88
|
-
|
89
98
|
event = db["events"].find.first
|
90
99
|
|
91
100
|
event.should_not be_nil
|
92
101
|
event["timestamp"].should be_a Time
|
102
|
+
event["status"].should be_a String
|
93
103
|
|
94
104
|
event.delete "_id"
|
95
105
|
event.delete "timestamp"
|
106
|
+
event.delete "status"
|
96
107
|
|
97
108
|
event.should == @submitted_events.first
|
98
109
|
|
@@ -103,56 +114,69 @@ Then /^the summary should show:$/ do
|
|
103
114
|
|
104
115
|
expected_summary = YAML.load expected_string
|
105
116
|
|
106
|
-
|
107
|
-
|
108
|
-
summary =
|
109
|
-
db["summaries"].find({
|
110
|
-
"_id" => expected_summary["_id"],
|
111
|
-
}).first
|
117
|
+
summary = get_summary expected_summary["_id"]
|
112
118
|
|
113
119
|
summary.should == expected_summary
|
114
120
|
|
115
121
|
end
|
116
122
|
|
117
|
-
|
123
|
+
Then /^the event status should be "(.*?)"$/ do
|
124
|
+
|expected_status|
|
125
|
+
|
126
|
+
event = get_event @event_id
|
127
|
+
|
128
|
+
event["status"].should == expected_status
|
118
129
|
|
119
|
-
When /^I visit the overview page$/ do
|
120
|
-
visit "/"
|
121
130
|
end
|
122
131
|
|
123
|
-
Then /^
|
124
|
-
|
132
|
+
Then /^the summary new should be (\d+)$/ do
|
133
|
+
|count_str|
|
134
|
+
|
135
|
+
summary = get_summary @source
|
136
|
+
|
137
|
+
summary["combined"]["new"].should == count_str.to_i
|
138
|
+
|
125
139
|
end
|
126
140
|
|
127
|
-
Then /^
|
141
|
+
Then /^the summary total should be (\d+)$/ do
|
128
142
|
|count_str|
|
129
|
-
|
130
|
-
|
143
|
+
|
144
|
+
summary = get_summary @source
|
145
|
+
|
146
|
+
summary["combined"]["total"].should == count_str.to_i
|
147
|
+
|
131
148
|
end
|
132
149
|
|
133
|
-
Then /^the (
|
134
|
-
|
|
150
|
+
Then /^the summary new for type "(.*?)" should be (\d+)$/ do
|
151
|
+
|type, count_str|
|
135
152
|
|
136
|
-
|
153
|
+
summary = get_summary @source
|
137
154
|
|
138
|
-
|
155
|
+
summary["types"][type]["new"].should == count_str.to_i
|
139
156
|
|
140
|
-
|
141
|
-
|row|
|
142
|
-
find(".#{row["name"]}").text.should == row["value"]
|
143
|
-
end
|
157
|
+
end
|
144
158
|
|
145
|
-
|
159
|
+
Then /^the summary total for type "(.*?)" should be (\d+)$/ do
|
160
|
+
|type, count_str|
|
161
|
+
|
162
|
+
summary = get_summary @source
|
163
|
+
|
164
|
+
summary["types"][type]["total"].should == count_str.to_i
|
146
165
|
|
147
166
|
end
|
148
167
|
|
149
168
|
Then /^icinga should receive:$/ do
|
150
|
-
|
|
169
|
+
|expected_commands|
|
151
170
|
|
152
171
|
command_contents =
|
153
172
|
File.new(@command_file).to_a.map { |line| line.strip }
|
154
173
|
|
155
|
-
|
156
|
-
|
174
|
+
expected_commands.split("\n").each do
|
175
|
+
|expected_command|
|
176
|
+
|
177
|
+
command_contents.should \
|
178
|
+
include expected_command
|
179
|
+
|
180
|
+
end
|
157
181
|
|
158
182
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
When /^I visit (\/.*)$/ do
|
2
|
+
|url_raw|
|
3
|
+
|
4
|
+
url = url_raw.gsub /\$\{([-a-z]+)\}/ do
|
5
|
+
var_name = $1.gsub "-", "_"
|
6
|
+
instance_variable_get "@#{var_name}"
|
7
|
+
end
|
8
|
+
|
9
|
+
visit url
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
When /^I click "(.*?)"$/ do
|
14
|
+
|target_str|
|
15
|
+
|
16
|
+
click_on target_str
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
Then /^I should see no summaries$/ do
|
21
|
+
page.should have_content "No events have been logged"
|
22
|
+
end
|
23
|
+
|
24
|
+
Then /^I should see (\d+) summar(?:y|ies)$/ do
|
25
|
+
|count_str|
|
26
|
+
count = count_str.to_i
|
27
|
+
find("#summaries").should have_css(".summary", :count => count)
|
28
|
+
end
|
29
|
+
|
30
|
+
Then /^the (\d+(?:st|nd|rd|th)) summary should be:$/ do
|
31
|
+
|index_str, fields|
|
32
|
+
|
33
|
+
index = index_str.to_i
|
34
|
+
|
35
|
+
within "#summaries" do
|
36
|
+
|
37
|
+
fields.hashes.each do
|
38
|
+
|row|
|
39
|
+
find(".#{row["name"]}").text.should == row["value"]
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
Then /^I should see the event$/ do
|
47
|
+
|
48
|
+
find("#event #id td").text.should == @event_id.to_s
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module HQ
|
2
|
+
module LogMonitorServer
|
3
|
+
|
4
|
+
class Script
|
5
|
+
|
6
|
+
def do_checks
|
7
|
+
@mutex.synchronize do
|
8
|
+
|
9
|
+
File.open @icinga_elem["command-file"], "a" do
|
10
|
+
|command_io|
|
11
|
+
|
12
|
+
summaries_by_service =
|
13
|
+
get_summaries_by_service
|
14
|
+
|
15
|
+
@icinga_elem.find("service").each do
|
16
|
+
|service_elem|
|
17
|
+
|
18
|
+
service_name = service_elem["name"]
|
19
|
+
|
20
|
+
critical_count = 0
|
21
|
+
warning_count = 0
|
22
|
+
unknown_count = 0
|
23
|
+
|
24
|
+
summaries =
|
25
|
+
summaries_by_service[service_name]
|
26
|
+
|
27
|
+
if summaries
|
28
|
+
summaries["types"].each do
|
29
|
+
|type_name, type_info|
|
30
|
+
|
31
|
+
type_elem =
|
32
|
+
service_elem.find_first("
|
33
|
+
type [@name = #{esc_xp type_name}]
|
34
|
+
")
|
35
|
+
|
36
|
+
if ! type_elem
|
37
|
+
unknown_count += type_info["new"]
|
38
|
+
elsif type_elem["level"] == "critical"
|
39
|
+
critical_count += type_info["new"]
|
40
|
+
elsif type_elem["level"] == "warning"
|
41
|
+
warning_count += type_info["new"]
|
42
|
+
else
|
43
|
+
unknown_count += type_info["new"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
status_int =
|
49
|
+
if critical_count > 0
|
50
|
+
2
|
51
|
+
elsif warning_count > 0
|
52
|
+
1
|
53
|
+
elsif unknown_count > 0
|
54
|
+
3
|
55
|
+
else
|
56
|
+
0
|
57
|
+
end
|
58
|
+
|
59
|
+
status_str =
|
60
|
+
if critical_count > 0
|
61
|
+
"CRITICAL"
|
62
|
+
elsif warning_count > 0
|
63
|
+
"WARNING"
|
64
|
+
elsif unknown_count > 0
|
65
|
+
"UNKNOWN"
|
66
|
+
else
|
67
|
+
"OK"
|
68
|
+
end
|
69
|
+
|
70
|
+
parts = []
|
71
|
+
|
72
|
+
if critical_count > 0
|
73
|
+
parts << "%d critical" % critical_count
|
74
|
+
end
|
75
|
+
|
76
|
+
if warning_count > 0
|
77
|
+
parts << "%d warning" % warning_count
|
78
|
+
end
|
79
|
+
|
80
|
+
if unknown_count > 0
|
81
|
+
parts << "%d unknown" % unknown_count
|
82
|
+
end
|
83
|
+
|
84
|
+
if parts.empty?
|
85
|
+
parts << "no new events"
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
command_io.print "[%s] %s\n" % [
|
90
|
+
Time.now.to_i,
|
91
|
+
[
|
92
|
+
"PROCESS_SERVICE_CHECK_RESULT",
|
93
|
+
service_elem["icinga-host"],
|
94
|
+
service_elem["icinga-service"],
|
95
|
+
status_int,
|
96
|
+
"%s %s" % [
|
97
|
+
status_str,
|
98
|
+
parts.join(", "),
|
99
|
+
]
|
100
|
+
].join(";"),
|
101
|
+
]
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
@next_check = Time.now + 60
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module HQ
|
2
|
+
module LogMonitorServer
|
3
|
+
|
4
|
+
class Script
|
5
|
+
|
6
|
+
def event_page env, context
|
7
|
+
|
8
|
+
req = Rack::Request.new env
|
9
|
+
|
10
|
+
# process form stuff
|
11
|
+
|
12
|
+
if req.request_method == "POST" \
|
13
|
+
&& req.params["mark-as-seen"]
|
14
|
+
|
15
|
+
mark_event_as_seen context[:event_id]
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# read from database
|
20
|
+
|
21
|
+
event =
|
22
|
+
@db["events"]
|
23
|
+
.find_one({
|
24
|
+
"_id" => BSON::ObjectId.from_string(context[:event_id]),
|
25
|
+
})
|
26
|
+
|
27
|
+
# set headers
|
28
|
+
|
29
|
+
headers = {}
|
30
|
+
|
31
|
+
headers["content-type"] = "text/html; charset=utf-8"
|
32
|
+
|
33
|
+
# create page
|
34
|
+
|
35
|
+
html = []
|
36
|
+
|
37
|
+
title =
|
38
|
+
"Event %s \u2014 Log monitor" % [
|
39
|
+
context[:event_id],
|
40
|
+
]
|
41
|
+
|
42
|
+
html << "<!DOCTYPE html>\n"
|
43
|
+
html << "<html>\n"
|
44
|
+
html << "<head>\n"
|
45
|
+
|
46
|
+
html << "<title>%s</title>\n" % [
|
47
|
+
esc_ht(title),
|
48
|
+
]
|
49
|
+
|
50
|
+
html << "</head>\n"
|
51
|
+
html << "<body>\n"
|
52
|
+
|
53
|
+
html << "<h1>%s</h1>\n" % [
|
54
|
+
esc_ht(title),
|
55
|
+
]
|
56
|
+
|
57
|
+
unless event
|
58
|
+
|
59
|
+
html << "<p>Event id not recognised</p>\n"
|
60
|
+
|
61
|
+
else
|
62
|
+
|
63
|
+
html << "<table id=\"event\">\n"
|
64
|
+
html << "<tbody>\n"
|
65
|
+
|
66
|
+
html << "<tr id=\"id\">\n"
|
67
|
+
html << "<th>ID</th>\n"
|
68
|
+
html << "<td>%s</td>\n" % [
|
69
|
+
esc_ht(event["_id"].to_s),
|
70
|
+
]
|
71
|
+
html << "</tr>\n"
|
72
|
+
|
73
|
+
html << "<tr>\n"
|
74
|
+
html << "<th>Timestamp</th>\n"
|
75
|
+
html << "<td>%s</td>\n" % [
|
76
|
+
esc_ht(event["timestamp"].to_s),
|
77
|
+
]
|
78
|
+
html << "</tr>\n"
|
79
|
+
|
80
|
+
html << "<tr>\n"
|
81
|
+
html << "<th>Service</th>\n"
|
82
|
+
html << "<td>%s</td>\n" % [
|
83
|
+
esc_ht(event["source"]["service"]),
|
84
|
+
]
|
85
|
+
html << "</tr>\n"
|
86
|
+
|
87
|
+
html << "<tr>\n"
|
88
|
+
html << "<th>Host</th>\n"
|
89
|
+
html << "<td>%s</td>\n" % [
|
90
|
+
esc_ht(event["source"]["host"]),
|
91
|
+
]
|
92
|
+
html << "</tr>\n"
|
93
|
+
|
94
|
+
html << "<tr>\n"
|
95
|
+
html << "<th>Class</th>\n"
|
96
|
+
html << "<td>%s</td>\n" % [
|
97
|
+
esc_ht(event["source"]["class"]),
|
98
|
+
]
|
99
|
+
html << "</tr>\n"
|
100
|
+
|
101
|
+
html << "<tr>\n"
|
102
|
+
html << "<th>Filename</th>\n"
|
103
|
+
html << "<td>%s</td>\n" % [
|
104
|
+
esc_ht(event["location"]["file"]),
|
105
|
+
]
|
106
|
+
html << "</tr>\n"
|
107
|
+
|
108
|
+
html << "<tr>\n"
|
109
|
+
html << "<th>Line number</th>\n"
|
110
|
+
html << "<td>%s</td>\n" % [
|
111
|
+
esc_ht((event["location"]["line"] + 1).to_s),
|
112
|
+
]
|
113
|
+
html << "</tr>\n"
|
114
|
+
|
115
|
+
unless event["lines"]["before"].empty?
|
116
|
+
|
117
|
+
html << "<tr>\n"
|
118
|
+
html << "<th>Before</th>\n"
|
119
|
+
html << "<td>%s</td>\n" % [
|
120
|
+
event["lines"]["before"]
|
121
|
+
.map { |line| esc_ht(line) }
|
122
|
+
.join("<br>")
|
123
|
+
]
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
html << "<tr>\n"
|
128
|
+
html << "<th>Matching</th>\n"
|
129
|
+
html << "<td>%s</td>\n" % [
|
130
|
+
esc_ht(event["lines"]["matching"]),
|
131
|
+
]
|
132
|
+
|
133
|
+
unless event["lines"]["after"].empty?
|
134
|
+
|
135
|
+
html << "<tr>\n"
|
136
|
+
html << "<th>Before</th>\n"
|
137
|
+
html << "<td>%s</td>\n" % [
|
138
|
+
event["lines"]["after"]
|
139
|
+
.map { |line| esc_ht(line) }
|
140
|
+
.join("<br>")
|
141
|
+
]
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
html << "</tbody>\n"
|
146
|
+
html << "</table>\n"
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
html << "<form method=\"post\">\n"
|
151
|
+
|
152
|
+
html <<
|
153
|
+
"<p>" +
|
154
|
+
"<input " +
|
155
|
+
"type=\"submit\" " +
|
156
|
+
"name=\"mark-as-seen\" " +
|
157
|
+
"value=\"mark as seen\">" +
|
158
|
+
"</p>\n"
|
159
|
+
|
160
|
+
html << "</form>\n"
|
161
|
+
|
162
|
+
html << "</body>\n"
|
163
|
+
html << "</html>\n"
|
164
|
+
|
165
|
+
# return
|
166
|
+
|
167
|
+
return 200, headers, html
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|