sabisu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +191 -0
- data/README.md +72 -0
- data/bin/sabisu +39 -0
- data/lib/sabisu.rb +5 -0
- data/lib/sabisu/config.rb +116 -0
- data/lib/sabisu/event.rb +184 -0
- data/lib/sabisu/public/css/base.css +32 -0
- data/lib/sabisu/public/css/bootstrap.min.css +9 -0
- data/lib/sabisu/public/css/events.css +194 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.svg +228 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/lib/sabisu/public/js/sabisu.js +893 -0
- data/lib/sabisu/public/js/typeahead.js +1718 -0
- data/lib/sabisu/routes/api.rb +38 -0
- data/lib/sabisu/routes/client.rb +13 -0
- data/lib/sabisu/routes/events.rb +9 -0
- data/lib/sabisu/routes/init.rb +4 -0
- data/lib/sabisu/routes/sensu.rb +44 -0
- data/lib/sabisu/sensu.rb +67 -0
- data/lib/sabisu/server.rb +110 -0
- data/lib/sabisu/templates/events.haml +268 -0
- data/lib/sabisu/templates/layout.haml +85 -0
- data/lib/sabisu/templates/login.haml +22 -0
- data/lib/sabisu/version.rb +4 -0
- metadata +284 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# api routes
|
2
|
+
module Sabisu
|
3
|
+
# server class
|
4
|
+
class Server
|
5
|
+
get '/api/events' do
|
6
|
+
params = request.env['rack.request.query_hash']
|
7
|
+
events = Event.all(params)
|
8
|
+
JSON.pretty_generate(events)
|
9
|
+
end
|
10
|
+
|
11
|
+
get '/api/events/search' do
|
12
|
+
params = request.env['rack.request.query_hash']
|
13
|
+
if params.key?('query')
|
14
|
+
query = params['query']
|
15
|
+
params.delete('query')
|
16
|
+
else
|
17
|
+
return 'Must supply \'query\' parameter'
|
18
|
+
end
|
19
|
+
events = Event.search(query, params)
|
20
|
+
JSON.pretty_generate(events)
|
21
|
+
end
|
22
|
+
|
23
|
+
get '/api/events/stale' do
|
24
|
+
params = request.env['rack.request.query_hash']
|
25
|
+
stale = Event.stale(params)
|
26
|
+
JSON.pretty_generate(stale: stale)
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/api/events/changes' do
|
30
|
+
params = request.env['rack.request.query_hash']
|
31
|
+
JSON.pretty_generate(CURRENT_DB.changes(params))
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/api/configuration/fields' do
|
35
|
+
JSON.pretty_generate(FIELDS)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# client routes
|
2
|
+
module Sabisu
|
3
|
+
# server class
|
4
|
+
class Server
|
5
|
+
get '/clients/:client' do
|
6
|
+
haml :client, locals: { client: params[:client] }
|
7
|
+
end
|
8
|
+
|
9
|
+
get '/clients/:client/:check' do
|
10
|
+
haml :client_check, locals: { client: params[:client], check: params[:check] }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# sensu routes
|
2
|
+
module Sabisu
|
3
|
+
# server class
|
4
|
+
class Server
|
5
|
+
def sensu(request)
|
6
|
+
sensu = Sensu.new
|
7
|
+
tmp_path = request.path_info.split('/')
|
8
|
+
tmp_path.delete_at(1)
|
9
|
+
path = tmp_path.join('/')
|
10
|
+
opts = {
|
11
|
+
path: path,
|
12
|
+
method: request.request_method,
|
13
|
+
ssl: API_SSL
|
14
|
+
}
|
15
|
+
begin
|
16
|
+
opts[:payload] = JSON.parse(request.body.read) if request.post?
|
17
|
+
rescue StandardError
|
18
|
+
puts "unable to parse: #{request.body.read}"
|
19
|
+
end
|
20
|
+
sensu.request(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
route :get, :post, '/sensu/stashes' do
|
24
|
+
res = sensu(request)
|
25
|
+
status res.code
|
26
|
+
headers 'content-type' => 'application/json'
|
27
|
+
body res.body
|
28
|
+
end
|
29
|
+
|
30
|
+
delete '/sensu/stashes/*' do
|
31
|
+
res = sensu(request)
|
32
|
+
status res.code
|
33
|
+
headers 'content-type' => 'application/json'
|
34
|
+
body res.body
|
35
|
+
end
|
36
|
+
|
37
|
+
post '/sensu/resolve' do
|
38
|
+
res = sensu(request)
|
39
|
+
status res.code
|
40
|
+
headers 'content-type' => 'application/json'
|
41
|
+
body res.body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/sabisu/sensu.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
## Heavily borrowed from sensu-cli
|
2
|
+
## https://github.com/agent462/sensu-cli/blob/master/lib/sensu-cli/api.rb
|
3
|
+
|
4
|
+
require 'net/https'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# make api requests to sensu
|
8
|
+
module Sabisu
|
9
|
+
class Server
|
10
|
+
# sensu passthrough api
|
11
|
+
class Sensu
|
12
|
+
def request(opts)
|
13
|
+
http = Net::HTTP.new(API_URL, API_PORT)
|
14
|
+
http.read_timeout = 15
|
15
|
+
http.open_timeout = 5
|
16
|
+
if API_SSL == true
|
17
|
+
http.use_ssl = true
|
18
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
19
|
+
end
|
20
|
+
proxy_header = { 'api-proxy' => 'true' }
|
21
|
+
case opts[:method].upcase
|
22
|
+
when 'GET'
|
23
|
+
req = Net::HTTP::Get.new(opts[:path], proxy_header)
|
24
|
+
when 'DELETE'
|
25
|
+
req = Net::HTTP::Delete.new(opts[:path], proxy_header)
|
26
|
+
when 'POST'
|
27
|
+
req = Net::HTTP::Post.new(
|
28
|
+
opts[:path],
|
29
|
+
proxy_header.merge!('Content-Type' => 'application/json')
|
30
|
+
)
|
31
|
+
req.body = opts[:payload].to_json
|
32
|
+
end
|
33
|
+
req.basic_auth(API_USER, API_PASSWORD) unless API_USER.nil? && API_PASSWORD.nil?
|
34
|
+
http.request(req)
|
35
|
+
end
|
36
|
+
|
37
|
+
def response(code, body, command = nil)
|
38
|
+
case code
|
39
|
+
when '200'
|
40
|
+
JSON.parse(body)
|
41
|
+
when '201'
|
42
|
+
puts 'The stash has been created.' if command == 'stashes' || command == 'silence'
|
43
|
+
when '202'
|
44
|
+
puts 'The item was submitted for processing.'
|
45
|
+
when '204'
|
46
|
+
puts 'Sensu is healthy' if command == 'health'
|
47
|
+
if command == 'aggregates' || command == 'stashes'
|
48
|
+
puts 'The item was successfully deleted.'
|
49
|
+
end
|
50
|
+
when '400'
|
51
|
+
puts 'The payload is malformed.'.color(:red)
|
52
|
+
when '401'
|
53
|
+
puts 'The request requires user authentication.'.color(:red)
|
54
|
+
when '404'
|
55
|
+
puts 'The item does not exist.'.color(:cyan)
|
56
|
+
else
|
57
|
+
if command == 'health'
|
58
|
+
puts 'Sensu is not healthy.'.color(:red)
|
59
|
+
else
|
60
|
+
puts 'There was an error while trying to complete your request. ' +
|
61
|
+
"Response code: #{code}".color(:red)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
gem 'thin'
|
2
|
+
gem 'sinatra', '1.4.4'
|
3
|
+
|
4
|
+
# load gems
|
5
|
+
require 'sinatra'
|
6
|
+
require 'sinatra/base'
|
7
|
+
require 'sinatra/multi_route'
|
8
|
+
require 'couchrest'
|
9
|
+
require 'restclient'
|
10
|
+
require 'cgi'
|
11
|
+
require 'json'
|
12
|
+
require 'pp'
|
13
|
+
|
14
|
+
# load version number
|
15
|
+
require_relative 'version'
|
16
|
+
|
17
|
+
module Sabisu
|
18
|
+
# run sabisu as a server
|
19
|
+
class Server < Sinatra::Base
|
20
|
+
register Sinatra::MultiRoute
|
21
|
+
|
22
|
+
# load classes
|
23
|
+
require_relative 'sensu'
|
24
|
+
require_relative 'event'
|
25
|
+
|
26
|
+
# load configuration settings
|
27
|
+
require_relative 'config'
|
28
|
+
|
29
|
+
# load routes
|
30
|
+
require_relative 'routes/init'
|
31
|
+
|
32
|
+
before '/:name' do
|
33
|
+
unless params[:name] == 'login'
|
34
|
+
session[:url] = request.fullpath
|
35
|
+
force_session_auth
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
before '/api/*' do
|
40
|
+
protected!
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/' do
|
44
|
+
redirect '/login'
|
45
|
+
end
|
46
|
+
|
47
|
+
get '/login' do
|
48
|
+
if logged_in?
|
49
|
+
redirect '/events'
|
50
|
+
else
|
51
|
+
haml :login, locals: { remember_me: session[:remember_me] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
post '/login' do
|
56
|
+
if validate(params[:username], params['password'])
|
57
|
+
session[:logged_in] = true
|
58
|
+
session[:username] = params[:username]
|
59
|
+
session[:remember_me] = params[:username] if params[:remember_me] == 'on'
|
60
|
+
session[:url] = '/events' unless session.key?(:url)
|
61
|
+
redirect session[:url]
|
62
|
+
else
|
63
|
+
haml :login, locals: { message: 'Incorrect username and/or password' }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
get '/logout' do
|
68
|
+
clear_session
|
69
|
+
redirect '/login'
|
70
|
+
end
|
71
|
+
|
72
|
+
helpers do
|
73
|
+
def validate(username, password)
|
74
|
+
username == UI_USERNAME && password == UI_PASSWORD
|
75
|
+
end
|
76
|
+
|
77
|
+
def logged_in?
|
78
|
+
NOAUTH == true || (session[:logged_in] == true && session[:username])
|
79
|
+
end
|
80
|
+
|
81
|
+
def force_session_auth
|
82
|
+
if logged_in?
|
83
|
+
return true
|
84
|
+
else
|
85
|
+
redirect '/login'
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear_session
|
91
|
+
session.clear
|
92
|
+
end
|
93
|
+
|
94
|
+
def authorized?
|
95
|
+
@auth ||= Rack::Auth::Basic::Request.new(request.env)
|
96
|
+
@auth.provided? &&
|
97
|
+
@auth.basic? &&
|
98
|
+
@auth.credentials &&
|
99
|
+
@auth.credentials == [UI_USERNAME, UI_PASSWORD]
|
100
|
+
end
|
101
|
+
|
102
|
+
def protected!
|
103
|
+
unless authorized? || logged_in?
|
104
|
+
response['WWW-Authenticate'] = "Basic realm='Sabisu requires authentication'"
|
105
|
+
throw(:halt, [401, "Not authorized\n"])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
%link{:rel => 'stylesheet', :href => '/css/events.css'}
|
2
|
+
%script{:src => '/js/typeahead.js'}
|
3
|
+
%script{:src => 'https://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js'}
|
4
|
+
|
5
|
+
#eventsController.row{'ng-controller' => 'eventsController'}
|
6
|
+
-# last update time in corner
|
7
|
+
%span#corner_status
|
8
|
+
|
9
|
+
-# search box (and sort dropdown)
|
10
|
+
#search.col-md-12.pull-right.main-content
|
11
|
+
.panel
|
12
|
+
#search_field.collapse.in{:role => 'search'}
|
13
|
+
.panel-body
|
14
|
+
%form.form-inline{:role => 'search'}
|
15
|
+
#search_form_group.form-group.col-md-9
|
16
|
+
%input#search_input.form-control{:type => 'text', :placeholder => 'Search', 'ng-model' => 'search_field', 'search-typeahead' => ''}
|
17
|
+
%small.help-block
|
18
|
+
%a{'data-toggle' => 'modal', 'data-target' => '#howdoisearch'} How do I search?
|
19
|
+
.form-group.col-md-2
|
20
|
+
%select#sort.form-control{'ng-model' => 'sort_field'}
|
21
|
+
%option{disabled: true} Sort by
|
22
|
+
%option{:value => 'client'} client
|
23
|
+
%option{:value => '-client'} client (descending)
|
24
|
+
%option{:value => 'check'} check
|
25
|
+
%option{:value => '-check'} check (descending)
|
26
|
+
%option{:value => 'status'} status
|
27
|
+
%option{:value => '-status'} status (descending)
|
28
|
+
%option{:value => 'age', :selected => 'selected'} age
|
29
|
+
%option{:value => '-age'} age (descending)
|
30
|
+
%option{:value => 'issued'} issued
|
31
|
+
%option{:value => '-issued'} issued (descending)
|
32
|
+
%option{:value => 'output'} output
|
33
|
+
%option{:value => '-output'} output (descending)
|
34
|
+
%input.btn.btn-success.pull-right{:type => 'submit', :value => 'Search', 'ng-click' => 'updateParams()'}
|
35
|
+
|
36
|
+
-# left sidebar with stats
|
37
|
+
#stats.col-md-3.pull-left.main-content
|
38
|
+
.panel
|
39
|
+
#stats_field.collapse.in{:role => 'stats'}
|
40
|
+
.panel-body{'ng-hide' => 'events.length == 0 || events_spin'}
|
41
|
+
-# stats by status
|
42
|
+
#stats_status{'ng-hide' => 'events_spin'}
|
43
|
+
#totals
|
44
|
+
%span.label.label-warning.pointer{'ng-click' => 'appendQuery(1, "status", false)'}= ' - '
|
45
|
+
%span.label.label-danger.pointer{'ng-click' => 'appendQuery(2, "status", false)'}= ' - '
|
46
|
+
%span.label.label-info.pointer{'ng-click' => 'appendQuery(3, "status", false)'}= ' - '
|
47
|
+
|
48
|
+
-# stats by field
|
49
|
+
#stats_by_count{'ng-repeat' => '(name, stat) in stats'}
|
50
|
+
%h5
|
51
|
+
{{name | uppercase }} stats
|
52
|
+
%table.table.table-striped{'ng-hide' => 'events_spin'}
|
53
|
+
%tr
|
54
|
+
%td {{name}} name
|
55
|
+
%td Quantity
|
56
|
+
%tr{'ng-repeat' => 's in stat | slice:0:10'}
|
57
|
+
%td.col-md-3
|
58
|
+
%span.appendQuery{'ng-click' => 'appendQuery(s[0], name)'}
|
59
|
+
{{s[0]}}
|
60
|
+
%td.col-md-1.text-center
|
61
|
+
{{s[1]}}
|
62
|
+
|
63
|
+
-# all events
|
64
|
+
#events.col-md-9.pull-right.main-content{:style => 'margin-bottom: 100px'}
|
65
|
+
.panel
|
66
|
+
#events_field.collapse.in{:role => 'events'}
|
67
|
+
%ul.list-group
|
68
|
+
-# events options
|
69
|
+
.progress.progress-striped.active{'ng-show' => 'events_spin', :style => 'margin-top: 50px;'}
|
70
|
+
.progress-bar.progress-bar-success{:role => 'progressbar', 'aria-valuenow' => '100', 'aria-valuemin' => '0', 'aria-valuemax' => '100', :style => 'width: 100%'}
|
71
|
+
%span.sr-only getting events...
|
72
|
+
.text-center{'ng-hide' => 'events.length || events_spin'} No Events
|
73
|
+
.pull-right{'ng-hide' => 'events.length == 0 || events_spin'}
|
74
|
+
%span.view View
|
75
|
+
%select#limit{'ng-model' => 'limit_field'}
|
76
|
+
%option{:value => '10'} 10
|
77
|
+
%option{:value => '20'} 20
|
78
|
+
%option{:value => '50', :selected => "selected"} 50
|
79
|
+
%option{:value => '100'} 100
|
80
|
+
%option{:value => '200'} 200
|
81
|
+
%a#events-accordian-toggle.text-right.pull-left{'ng-click' => 'bulkToggleDetails()', 'ng-hide' => 'events.length == 0 || events_spin'} {{bulk}} all details
|
82
|
+
|
83
|
+
-# an event
|
84
|
+
%br
|
85
|
+
%li.list-group-item{'ng-repeat' => 'event in events'}
|
86
|
+
.row
|
87
|
+
.panel
|
88
|
+
-# event header
|
89
|
+
.panel-heading.flat-heading.event_line{:panel => '{{event.client.name}}/{{event.check.name}}'}
|
90
|
+
.col-md-2
|
91
|
+
%h4
|
92
|
+
%span.label.pull-right.pointer{'class' => 'label-{{event.color}}', 'ng-click' => 'appendQuery(event.check.status, "status", false)'} {{event.wstatus}}
|
93
|
+
%h5.event_title.pull-left
|
94
|
+
%span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.client.silenced', 'ng-click' => 'updateSilenceDetails(event.client.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
|
95
|
+
%span.appendQuery{'ng-click' => 'appendQuery(event.client.name, "client")'}
|
96
|
+
{{event.client.name}}
|
97
|
+
%span= ' / '
|
98
|
+
%span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.check.silenced', 'ng-click' => 'updateSilenceDetails(event.check.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
|
99
|
+
%span.appendQuery{'ng-click' => 'appendQuery(event.check.name, "check")'}
|
100
|
+
{{event.check.name}}
|
101
|
+
%small.text-muted.muted-output
|
102
|
+
\- {{event.check.output}}
|
103
|
+
%h5
|
104
|
+
%span.glyphicon.glyphicon-collapse-down.toggleBtnIcon.pull-right{'ng-click' => 'toggleDetails(event.id)'}
|
105
|
+
|
106
|
+
.panel-body.panel-collapse.collapse{:class => '{{event.showdetails}}', :id => '{{event.id}}'}
|
107
|
+
-# event actions
|
108
|
+
.dropdown.actions.pull-right
|
109
|
+
%button.btn.btn-link.dropdown-toggle{'data-toggle' => 'dropdown'}
|
110
|
+
Actions
|
111
|
+
%span.caret
|
112
|
+
%ul.dropdown-menu
|
113
|
+
%li
|
114
|
+
%a{:href => '#', 'ng-click' => 'resolveEvent(event.client.name, event.check.name)'} Resolve
|
115
|
+
%li.divider
|
116
|
+
%li.dropdown-header Silence
|
117
|
+
%li
|
118
|
+
%a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Client
|
119
|
+
%li
|
120
|
+
%a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name, event.check.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Check
|
121
|
+
|
122
|
+
-# event attributes
|
123
|
+
%dl.dl-horizontal.col-md-5.pull-left
|
124
|
+
%dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.left'} {{attr[0]}}
|
125
|
+
%dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
|
126
|
+
%dl.dl-horizontal.col-md-5.pull-left
|
127
|
+
%dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.right'} {{attr[0]}}
|
128
|
+
%dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
|
129
|
+
|
130
|
+
-# event output
|
131
|
+
.col-md-12.pull-left.output
|
132
|
+
{{event.check.output}}
|
133
|
+
|
134
|
+
-# mini silence dialog (for seeing existing silences)
|
135
|
+
#silence_window_mini.silence_window.modal.fade
|
136
|
+
.modal-dialog
|
137
|
+
.modal-content
|
138
|
+
.modal-header
|
139
|
+
Silence Details for {{silencePath}}
|
140
|
+
%button.btn.btn-link.btn-xs.pull-right.close_popover{:type => 'button', 'data-dismiss' => 'modal'}
|
141
|
+
%span.glyphicon.glyphicon-remove
|
142
|
+
close
|
143
|
+
|
144
|
+
.modal-body
|
145
|
+
%dl.dl-horizontal
|
146
|
+
%dt{'ng-show' => 'silenceCreated'} Created
|
147
|
+
%dd{'ng-show' => 'silenceCreated'} {{silenceCreated}}
|
148
|
+
|
149
|
+
%dt{'ng-show' => 'silenceOwner'} Owner
|
150
|
+
%dd{'ng-show' => 'silenceOwner'} {{silenceOwner}}
|
151
|
+
|
152
|
+
%dt{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} Expires
|
153
|
+
%dd{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} {{silenceExpires}}
|
154
|
+
|
155
|
+
%dt{'ng-show' => 'silenceReason'} Reason
|
156
|
+
%dd{'ng-show' => 'silenceReason'} {{silenceReason}}
|
157
|
+
|
158
|
+
.modal-footer
|
159
|
+
%button.deleteSilenceBtn.btn.btn-danger.btn-sm.pull-right{:type => 'button', 'ng-click' => "deleteSilence('{{silencePath}}')"}
|
160
|
+
%span.glyphicon.glyphicon-remove
|
161
|
+
Delete
|
162
|
+
|
163
|
+
-# regular silence dialog (for creating a new silence)
|
164
|
+
#silence_window.modal.fade
|
165
|
+
.modal-dialog
|
166
|
+
.modal-content
|
167
|
+
.modal-header
|
168
|
+
%button.close.pull-right{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
|
169
|
+
%h4.modal-title Silence {{silencePath}}
|
170
|
+
.modal-body
|
171
|
+
%form#silence_form.form-horizontal{:role => 'form'}
|
172
|
+
.form-group.silence_owner.has-feedback
|
173
|
+
%label.col-sm-2.control-label{:for => 'owner'} Owner*
|
174
|
+
.col-sm-10
|
175
|
+
%input#owner.form-control{:type => 'text', :placeholder => 'Joe Smith'}
|
176
|
+
.form-group.silence_reason.has-feedback
|
177
|
+
%label.col-sm-2.control-label{:for => 'reason'} Reason*
|
178
|
+
.col-sm-10
|
179
|
+
%textarea#reason.form-control{:placeholder => 'Enter reason here', :rows => '3'}
|
180
|
+
.form-group
|
181
|
+
%label.col-sm-2.control-label{:for => 'expires'} Expiration*
|
182
|
+
.col-sm-10
|
183
|
+
.radio
|
184
|
+
%label
|
185
|
+
%input#resolve{:type => 'radio', :name => 'expiration', :value => 'resolve', :checked => ''}
|
186
|
+
On resolve
|
187
|
+
%span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"On resolve" will cause this silence to be deleted once the check clears for check, clients require all checks to clear. A minimum of 1 hour is enforced.'}
|
188
|
+
.radio
|
189
|
+
%label
|
190
|
+
%input#timer{:type => 'radio', :name => 'expiration', :value => 'timer'}
|
191
|
+
%span.pull-left Timer
|
192
|
+
.col-sm-3.silence_timer_val.has-feedback
|
193
|
+
%input#timer_val.input-sm.form-control{:type => 'text', :name => 'expirationl', :placeholder => ''}
|
194
|
+
%span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Timer" will cause this silence to be deleted in a predetermined time span. The expires value should be inputted as a number followed by a single character. m = minutes, h = hours, d = days, w = weeks. (examples 15m, 2h, 1d, 5w)'}
|
195
|
+
.radio
|
196
|
+
%label
|
197
|
+
%input#never{:type => 'radio', :name => 'expiration', :value => 'never'}
|
198
|
+
Never
|
199
|
+
%span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Never" will cause this silence to, surprise surprise, never expire. It will remain silence until yourself or someone else manually deletes the silence. It is highly discourage to permanently silence anything. Its extremely rare that that is ever the correct action.'}
|
200
|
+
.modal-footer
|
201
|
+
%button.btn.btn-default.pull-right{:type => 'submit', 'ng-click' => 'saveSilence()'} Create
|
202
|
+
|
203
|
+
-# help dialog
|
204
|
+
#howdoisearch.modal.fade
|
205
|
+
.modal-dialog
|
206
|
+
.modal-content
|
207
|
+
.modal-header
|
208
|
+
%button.close{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
|
209
|
+
%h4.modal-title How Do I Search?
|
210
|
+
.modal-body
|
211
|
+
%h4 Available Fields
|
212
|
+
%p.well.well-sm
|
213
|
+
{{event_fields_name | joinBy : ', '}}
|
214
|
+
%h4 Query Syntax
|
215
|
+
%h5 Examples
|
216
|
+
%table.table.table-striped
|
217
|
+
%tr
|
218
|
+
%td Desired Result
|
219
|
+
%td Query
|
220
|
+
%tr
|
221
|
+
%td For clients STARTING with "web"
|
222
|
+
%td client:web*
|
223
|
+
%tr
|
224
|
+
%td For checks ENDING with "process"
|
225
|
+
%td check:process
|
226
|
+
%tr
|
227
|
+
%td For checks MATCHING with "disk_usage"
|
228
|
+
%td check:"disk_usage"
|
229
|
+
%tr
|
230
|
+
%td For statuses warning or critical
|
231
|
+
%td status:[1 TO 2]
|
232
|
+
%tr
|
233
|
+
%td For "web" clients and critical
|
234
|
+
%td client:web* AND status:2
|
235
|
+
%tr
|
236
|
+
%td Search for everything, except checks that start with "disk"
|
237
|
+
%td *:* AND NOT check:disk*
|
238
|
+
%tr
|
239
|
+
%td For "web" or "dbs" clients and status is unknown
|
240
|
+
%td client:(web* OR db*) AND status:3
|
241
|
+
%tr
|
242
|
+
%td Full text search for "failure"
|
243
|
+
%td failure
|
244
|
+
%tr
|
245
|
+
%td Events since Jan 1, 2014
|
246
|
+
%td issued:[1388534400 TO Infinity]
|
247
|
+
%p
|
248
|
+
The Cloudant search query syntax is based on the
|
249
|
+
%a{:href => 'http://lucene.apache.org/core/4_2_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description'}
|
250
|
+
Lucene
|
251
|
+
syntax. Search queries take the form of "name:value" (unless the name is omitted, in which case they hit the default field).
|
252
|
+
%p
|
253
|
+
Queries over multiple fields can be logically combined and groups and fields can be grouped. The available logical operators are: "AND", "+", "OR", "NOT" and "-", and are case sensitive. Range queries can run over strings or numbers.
|
254
|
+
%p
|
255
|
+
If you want a fuzzy search you can run a query with "~" to find terms like the search term, for instance "look~" will find terms book and took.
|
256
|
+
%p
|
257
|
+
You can also increase the importance of a search term by using the boost character "^". This makes matches containing the term more relevant, e.g. cloudant "data layer"^4 will make results containing "data layer" 4 times more relevant. The default boost value is 1. Boost values must be positive, but can be less than 1 (e.g. 0.5 to reduce importance).
|
258
|
+
%p
|
259
|
+
Wild card searches are supported, for both single ("?") and multiple ("*") character searches. "dat?" would match date and data, "dat*" would match date, data, database, dates etc. Wildcards must come after a search term, you cannot do a query like "*base".
|
260
|
+
|
261
|
+
%p
|
262
|
+
The following characters require escaping if you want to search on them
|
263
|
+
|
264
|
+
%p.well.well-sm
|
265
|
+
+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
|
266
|
+
|
267
|
+
.modal-footer
|
268
|
+
%button.btn.btn-default{:type => 'button', 'data-dismiss' => 'modal'} Close
|