webstats 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.textile +81 -0
- data/Rakefile +23 -0
- data/VERSION.yml +4 -0
- data/bin/webstats +1 -0
- data/bin/webstats_email_notifier +1 -0
- data/bin/webstats_growl_notifier +1 -0
- data/clients/email_notifier/README.textile +13 -0
- data/clients/email_notifier/email_notifier.rb +66 -0
- data/clients/growl_notifier/Growl.rb +125 -0
- data/clients/growl_notifier/README.textile +13 -0
- data/clients/growl_notifier/growl_notifier.rb +41 -0
- data/clients/simple_notifier.rb +106 -0
- data/server/data_providers/cpu_info.rb +74 -0
- data/server/data_providers/disk_activity.rb +52 -0
- data/server/data_providers/disk_usage.c +29 -0
- data/server/data_providers/disk_usage.rb +54 -0
- data/server/data_providers/extconf.rb +5 -0
- data/server/data_providers/mem_info.rb +54 -0
- data/server/data_providers/url_monitor.rb +71 -0
- data/server/webstats.rb +269 -0
- data/webstats.gemspec +57 -0
- metadata +86 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Brenton Fletcher (http://i.bloople.net i@bloople.net)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
h1. Webstats
|
2
|
+
|
3
|
+
Webstats is a server and clients that monitors your servers performance (CPU usage, memory usage, disk usage, disk activity, url load time) and allows you to get notifications on your computer when the server is having problems, as well as showing (http://kimag.es/share/91090734.png) the current performance stats for the server on a web page.
|
4
|
+
|
5
|
+
Webstats has 2 components: the _server_ and one or more _clients_.
|
6
|
+
|
7
|
+
h2. Server
|
8
|
+
|
9
|
+
The server runs on the computer whose performance you want to monitor; the server application's job is to (1) monitor the computer's performance and (2) report these performance statistics through a webserver that the server application runs.
|
10
|
+
|
11
|
+
See Installation below for install instructions.
|
12
|
+
|
13
|
+
The server application executes in the background and starts a tiny webserver on port 9970. You can view the performance stats for your server by going to http://<server's hostname>:9970/, for example if my server was under the domain name bloople.net, I would go to http://bloople.net:9970/. The statistics on this page automatically update every 5 seconds. You may need to open port 9970 if you have a firewall on your server. Note that the various clients get their data via the webserver the server application runs, just like the built-in stats page.
|
14
|
+
|
15
|
+
The statistic provided by the server application are:
|
16
|
+
|
17
|
+
* CPU usage and load average
|
18
|
+
* Memory (RAM) usage, with free, free - buffers, and total counts
|
19
|
+
* Disk usage by mount point
|
20
|
+
* Disk activity for reads and writes
|
21
|
+
* URL monitoring, to load URL's you specify to ensure they work
|
22
|
+
|
23
|
+
The server application will warn you of any of these statistics indicate a problem on the server - for example if you've have < 5MB free RAM for the last 12 seconds, that indicates a potential problem on your server that needs looking at. If you're looking at the performance statistics web page, the warnings will be presented by higlighting the perf stat that's causing the warning; when you're using a client application, it will present warnings to you as it sees fit.
|
24
|
+
|
25
|
+
You can configure at what thresholds the warnings get generated, as well as the URL's to monitor, by editing ~/.webstats. This file is generated the first time you run webstats, so it's pre-filled with sensible defaults; but you can change them as you see fit.
|
26
|
+
|
27
|
+
You can prevent public access to your webstats by invoking the server application with a single argument (e.g. <code>ruby webstats.rb <password to use></code>). Then, when you visit the webstats, you'll be prompted for a username and password; the username is always 'webstats', and the password is the password specified just above.
|
28
|
+
|
29
|
+
The server application requires Linux kernel 2.6+, will not work in *BSD or OS X; the server application requires only Ruby; it does not rely on rubygems or any external libraries that aren't included with Ruby; the server application uses very little RAM, approximately 10MB.
|
30
|
+
|
31
|
+
All the client applications depend on the server application being run to work.
|
32
|
+
|
33
|
+
h2. Clients
|
34
|
+
|
35
|
+
If all you want to do is be able to view the performance statistics for your server in a web browser, then you don't need to use any of these clients; going to http://<server's hostname>:9970/ will do just fine. But if you want to have Growl or email notification when something goes wrong, without having to look at a web page all the time, then you'll want one of the client applications that come with Webstats.
|
36
|
+
|
37
|
+
There are currently two client applications available:
|
38
|
+
|
39
|
+
* An email notifier; documentation is available in clients/email_notifier/README.textile
|
40
|
+
* A Growl notifier; documentation is available in clients/growl_notifier/README.textile
|
41
|
+
|
42
|
+
The notifiers that ship with Webstats can be started in 2 ways; if you have installed webstats via rubygems, run:
|
43
|
+
|
44
|
+
<pre><code>~$ webstats_<notifier_name></code></pre>
|
45
|
+
|
46
|
+
For example, to run the email notifier, you can run webstats_email_notifier. If you installed webstats outside of rubygems, first change directory into the directory containing webstats, then run:
|
47
|
+
<pre><code>~/webstats$ ruby clients/<notifier_name>/<notifier_name>.rb</code></pre>
|
48
|
+
|
49
|
+
Many notifiers can also be run on the server, in the same ruby process as teh webstats server itself. To do this, change any settings as required by the documentation for the notifier, then add the notifier name (e.g. email_notifier) to the list of clients under webstats > clients in the ~/.webstats file
|
50
|
+
|
51
|
+
h2. Installation
|
52
|
+
|
53
|
+
You can now install webstats using rubygems; this saves you a bit of hassle. To install webstats on the server via rubygems, ensure github gems are in your gems sources; then run:
|
54
|
+
<pre><code>~$ sudo gem install bloopletech-webstats</code></pre>
|
55
|
+
|
56
|
+
To run the server application, run:
|
57
|
+
<pre><code>~$ webstats</code></pre>
|
58
|
+
|
59
|
+
The server application will start in the background.
|
60
|
+
|
61
|
+
If you don't want to use rubygems, then follow the below instructions:
|
62
|
+
To install the server app, ssh into the computer you want to monitor. then run:
|
63
|
+
<pre><code>~$ git clone git://github.com/bloopletech/webstats.git
|
64
|
+
~$ cd webstats
|
65
|
+
~/webstats$ cd server/data_providers
|
66
|
+
~/webstats/server/data_providers$ ruby extconf.rb
|
67
|
+
~/webstats/server/data_providers$ make
|
68
|
+
</code></pre>
|
69
|
+
|
70
|
+
To run the server app, ssh into the computer you want to monitor. then run:
|
71
|
+
<pre><code>~$ cd webstats
|
72
|
+
~/webstats$ ruby server/webstats.rb
|
73
|
+
</code></pre>
|
74
|
+
|
75
|
+
h2. Todo
|
76
|
+
|
77
|
+
* More client applications.
|
78
|
+
* Extend server to work on *BSD (including OS X).
|
79
|
+
* Make webstats server optionally install itself to run at boot.
|
80
|
+
* Make client applications optionally install themselves at boot, where suitable.
|
81
|
+
* (Maybe) Move all server code into it's own module and make code work without running the web server, so you can use it to just grab stats in your own code.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |s|
|
6
|
+
s.name = %q{webstats}
|
7
|
+
s.version = "0.1.0"
|
8
|
+
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.authors = ["Brenton Fletcher"]
|
11
|
+
s.date = Date.today.strftime("%Y-%m-%d")
|
12
|
+
s.description = s.summary = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
|
13
|
+
s.email = %q{i@bloople.net}
|
14
|
+
s.files = Dir['**/*'].reject { |fn| fn =~ /(\.o|\.so|\.bundle|Makefile|\.gem)$/ }
|
15
|
+
s.executables = ['webstats', 'webstats_growl_notifier', 'webstats_email_notifier']
|
16
|
+
s.extensions = ["server/data_providers/extconf.rb"]
|
17
|
+
s.has_rdoc = false
|
18
|
+
s.homepage = %q{http://github.com/bloopletech/webstats}
|
19
|
+
s.require_paths = [""]
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
23
|
+
end
|
data/VERSION.yml
ADDED
data/bin/webstats
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../server/webstats'
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../clients/email_notifier/email_notifier'
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../clients/growl_notifier/growl_notifier'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
h1. Email notifier for Webstats
|
2
|
+
|
3
|
+
The email notifier montiors one or more servers and emails you whenever the server is in danger or could be in danger soon - for example, running out of hard drive space.
|
4
|
+
|
5
|
+
h2. Usage
|
6
|
+
|
7
|
+
Run the notifier by running webstats_email_notifier or ruby <webstats_install_dir>/clients/email_notifier/email_notifier.rb on the terminal.
|
8
|
+
|
9
|
+
It'll create a template of the settings you'll need to change in ~/.webstats_client - you'll need to set the recipient address for the emails, you may need to change the mail server settings - you can supply these settings: address, port, domain, username, password, authentication (one of :plain, :login, or :cram_md5).
|
10
|
+
|
11
|
+
You'll also need to add some URL's for the email notifier to monitor - these are the URL's who statistics will be monitored. You will be notified when there is a warning or danger situation for a URL, as well as if one of the URL's can not be loaded. The URL's should the the hostnames of the servers you want to monitor, along with the correct port number (e.g. http://bloople.net:9970/).
|
12
|
+
|
13
|
+
Once that's done, run the notifier again and it will begin monitoring.
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../simple_notifier'
|
2
|
+
daemonize!
|
3
|
+
|
4
|
+
class EmailNotifier < SimpleNotifier
|
5
|
+
def initialize(settings = {}, read_config = true)
|
6
|
+
@name = 'email_notifier'
|
7
|
+
@no_settings_message = "Please edit ~/.webstats_clients and add some URLs to monitor and an email address to notfiy on"
|
8
|
+
|
9
|
+
super({ 'recipient' => '', 'mail_server' => { 'address' => 'localhost', 'domain' => 'localhost', 'port' => 25 } }.merge(settings), read_config)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def send_mail(subject, message)
|
14
|
+
sm = @settings[:mail_server]
|
15
|
+
|
16
|
+
msg = <<END_OF_MESSAGE
|
17
|
+
From: Webstats Email Notifier <#{@settings[:recipient]}>
|
18
|
+
To: #{@settings[:recipient]} <#{@settings[:recipient]}>
|
19
|
+
Subject: #{subject}
|
20
|
+
Date: #{Time.now.rfc2822}
|
21
|
+
Message-Id: <#{Time.now.to_i}.#{rand(10000000)}@#{sm[:domain]}>
|
22
|
+
|
23
|
+
#{message}
|
24
|
+
END_OF_MESSAGE
|
25
|
+
|
26
|
+
Net::SMTP.start(sm[:address], sm[:port], sm[:domain], sm[:username], sm[:password], sm[:authentication]) { |smtp| smtp.send_message msg, @settings[:recipient], @settings[:recipient] }
|
27
|
+
end
|
28
|
+
|
29
|
+
def failed_url(url, password, exception)
|
30
|
+
send_mail("Webstats Notification - Cannot load Webstats data", "Could not load #{url}#{!password.nil? ? " with password #{password}" : ""}, error was #{exception.message}. Will try again in 60 seconds.")
|
31
|
+
sleep(60)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def notify
|
36
|
+
messages = []
|
37
|
+
has_warnings = has_dangers = false
|
38
|
+
|
39
|
+
@settings[:urls].each do |url|
|
40
|
+
if !url[:bad].empty? and (url[:changed] or url[:time_past])
|
41
|
+
has_warnings = !url[:warnings].empty?
|
42
|
+
has_dangers = !url[:dangers].empty?
|
43
|
+
|
44
|
+
title = []
|
45
|
+
title << "Danger" if has_dangers
|
46
|
+
title << "Warnings" if has_warnings
|
47
|
+
title = title.join(" & ")
|
48
|
+
|
49
|
+
warnings_text = !url[:warnings].empty? ? "Warnings for #{url[:bad].select { |(k, v)| v['status'] == 'warning' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
|
50
|
+
danger_text = !url[:dangers].empty? ? "Dangerous situation for #{url[:bad].select { |(k, v)| v['status'] == 'danger' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
|
51
|
+
|
52
|
+
host = URI.parse(url[:url]).host
|
53
|
+
messages << "#{title} for #{host}\n#{"-" * (title.length + host.length + 5)}\n\n#{[danger_text, warnings_text].compact.join("\n")}\n\nCheck statistics online at #{url[:url]}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
title = []
|
58
|
+
title << "Danger" if has_dangers
|
59
|
+
title << "Warnings" if has_warnings
|
60
|
+
|
61
|
+
send_mail("Webstats Notification - #{title.join(" & ")}", messages.join("\n\n\n")) unless messages.empty?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
s = EmailNotifier.new()
|
66
|
+
s.start
|
@@ -0,0 +1,125 @@
|
|
1
|
+
### Copyright
|
2
|
+
#
|
3
|
+
# Copyright 2004 Thomas Kollbach <dev@bitfever.de>
|
4
|
+
#
|
5
|
+
# Released under the BSD license.
|
6
|
+
#
|
7
|
+
### Description
|
8
|
+
#
|
9
|
+
# A ruby class that enables posting notifications to the Growl daemon.
|
10
|
+
# See <http://growl.info> for more information.
|
11
|
+
#
|
12
|
+
# Requires RubyCocoa (http://www.fobj.com/rubycocoa/) and Ruby 1.8
|
13
|
+
# (http://ruby-lang.org).
|
14
|
+
#
|
15
|
+
### Versions
|
16
|
+
#
|
17
|
+
# v0.1- 25.11.2004 - Initial version, this is less more then a ruby translation of
|
18
|
+
# the python bindings
|
19
|
+
#
|
20
|
+
# TODO: transform this into a ruby-module, so it is usable as a mixin
|
21
|
+
# for ruby-scripts
|
22
|
+
#
|
23
|
+
### Usage
|
24
|
+
#
|
25
|
+
# Here is a short example how to use this in a script
|
26
|
+
#
|
27
|
+
# n = GrowlNotifier.new('bla',['Foo'],nil,OSX::NSWorkspace.sharedWorkspace().iconForFileType_('unknown'))
|
28
|
+
# n.register()
|
29
|
+
#
|
30
|
+
# n.notify('Foo', 'Test Notification', 'Blah blah blah')
|
31
|
+
#
|
32
|
+
###
|
33
|
+
|
34
|
+
require 'osx/cocoa'
|
35
|
+
|
36
|
+
$priority = {"Very Low" => -2,
|
37
|
+
"Moderate" => -1,
|
38
|
+
"Normal" => 0,
|
39
|
+
"High" => 1,
|
40
|
+
"Emergency"=> 2
|
41
|
+
}
|
42
|
+
|
43
|
+
|
44
|
+
class GrowlNotifier
|
45
|
+
# A class that abstracts the process of registering and posting
|
46
|
+
# notifications to the Growl daemon.
|
47
|
+
#
|
48
|
+
# `appName': The name of the application
|
49
|
+
# `notifications': an array of notifications - default is an empty array
|
50
|
+
#
|
51
|
+
# `defaultNotifications': optional - defaults to the value of
|
52
|
+
# `notifications'
|
53
|
+
# `appIcon' is also optional but defaults to a senseless icon so
|
54
|
+
# so you are higly encouraged to pass it along.
|
55
|
+
#
|
56
|
+
|
57
|
+
def initialize(appName='GrowlNotifier', notifications=[], defaultNotifications=nil, appIcon=nil)
|
58
|
+
@appName = appName
|
59
|
+
@notifications = notifications
|
60
|
+
@defaultNotifications = defaultNotifications
|
61
|
+
@appIcon = appIcon
|
62
|
+
end #initialize
|
63
|
+
|
64
|
+
def register
|
65
|
+
if @appIcon == nil then
|
66
|
+
@appIcon = OSX::NSWorkspace.sharedWorkspace().iconForFileType_("txt")
|
67
|
+
end
|
68
|
+
if @defaultNotifications == nil then
|
69
|
+
@defaultNotifications = @notifications
|
70
|
+
end
|
71
|
+
|
72
|
+
regData = {
|
73
|
+
'ApplicationName'=>@appName,
|
74
|
+
'AllNotifications'=> OSX::NSArray.arrayWithArray(@notifications),
|
75
|
+
'DefaultNotifications'=> OSX::NSArray.arrayWithArray(@defaultNotifications),
|
76
|
+
'ApplicationIcon'=> @appIcon.TIFFRepresentation
|
77
|
+
}
|
78
|
+
|
79
|
+
dict = OSX::NSDictionary.dictionaryWithDictionary(regData)
|
80
|
+
notifyCenter = OSX::NSDistributedNotificationCenter.defaultCenter
|
81
|
+
|
82
|
+
notifyCenter.postNotificationName_object_userInfo_deliverImmediately_("GrowlApplicationRegistrationNotification", nil, dict, true)
|
83
|
+
end #register
|
84
|
+
|
85
|
+
def notify(noteType, title, description, icon=nil, appIcon=nil, sticky=false, priority=nil)
|
86
|
+
# Post a notification to the Growl daemon.
|
87
|
+
#
|
88
|
+
# `noteType' is the name of the notification that is being posted.
|
89
|
+
# `title' is the user-visible title for this notification.
|
90
|
+
# `description' is the user-visible description of this notification.
|
91
|
+
# `icon' is an optional icon for this notification. It defaults to
|
92
|
+
# `@applicationIcon'.
|
93
|
+
# `appIcon' is an optional icon for the sending application.
|
94
|
+
# `sticky' is a boolean controlling whether the notification is sticky.
|
95
|
+
|
96
|
+
|
97
|
+
@notifications << noteType
|
98
|
+
if icon == nil then icon = @appIcon end
|
99
|
+
|
100
|
+
notification = {'NotificationName'=> noteType,
|
101
|
+
'ApplicationName'=> @appName,
|
102
|
+
'NotificationTitle'=> title,
|
103
|
+
'NotificationDescription'=> description,
|
104
|
+
'NotificationIcon'=> icon.TIFFRepresentation()}
|
105
|
+
|
106
|
+
unless appIcon == nil
|
107
|
+
notification['NotificationAppIcon'] = appicon.TIFFRepresentation
|
108
|
+
end
|
109
|
+
|
110
|
+
if sticky
|
111
|
+
notification['NotificationSticky'] = OSX::NSNumber.numberWithBool_(true)
|
112
|
+
end
|
113
|
+
|
114
|
+
unless priority == nil
|
115
|
+
notification['NotificationPriority'] = OSX::NSNumber.numberWithInt_(priority)
|
116
|
+
end
|
117
|
+
|
118
|
+
d = OSX::NSDictionary.dictionaryWithDictionary_(notification)
|
119
|
+
|
120
|
+
notCenter = OSX::NSDistributedNotificationCenter.defaultCenter()
|
121
|
+
notCenter.postNotificationName_object_userInfo_deliverImmediately_('GrowlNotification', nil, d, true)
|
122
|
+
|
123
|
+
end #notify
|
124
|
+
end #class growlnotifier
|
125
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
h1. Growl notifer for Webstats
|
2
|
+
|
3
|
+
The Growl notifier runs in the background and monitors the server; if the server goes under high CPU load for more than a few seconds, or if the server is nearly out of memory, you'll get a Growl notification saying what the problem is. This way, you can just set and forget, knowing that Webstats will report any problems.
|
4
|
+
|
5
|
+
Thy works on OS X only, with RubyCocoa installed.
|
6
|
+
|
7
|
+
.h2 Usage
|
8
|
+
|
9
|
+
Run the notifier by running webstats_growl_notifier or ruby <webstats_install_dir>/clients/growl_notifier/growl_notifier.rb on the terminal.
|
10
|
+
|
11
|
+
The first time you run the growl notifier, it will create a template of the settings you need to edit at ~/.webstats_clients; you must edit this file to set the URL's for the growl notifier to monitor. The URL's should the the hostnames of the servers you want to monitor, along with the correct port number (e.g. http://bloople.net:9970/).
|
12
|
+
|
13
|
+
Once you've edited the configuration file to add your URL's, run the notifier again; the Growl notifier will then run in the background and notify you od any warnings or danger situations on the server.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../simple_notifier'
|
2
|
+
daemonize!
|
3
|
+
require File.dirname(__FILE__) + '/Growl.rb'
|
4
|
+
|
5
|
+
class WGrowlNotifier < SimpleNotifier
|
6
|
+
def initialize(settings = {}, read_config = true)
|
7
|
+
@name = "growl_notifier"
|
8
|
+
@no_settings_message = "Please edit ~/.webstats_clients and add some URLs to monitor"
|
9
|
+
|
10
|
+
@g = GrowlNotifier.new("Webstats", ['Webstats Notification'], nil, OSX::NSWorkspace.sharedWorkspace().iconForFileType_('unknown'))
|
11
|
+
@g.register
|
12
|
+
|
13
|
+
super(settings, read_config)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def failed_url(url, password, exception)
|
18
|
+
@g.notify "Webstats Notification", "Cannot load Webstats data", "Could not load #{url}#{!password.nil? ? " with password #{password}" : ""}, error was #{exception.message}. Will try again in 60 seconds."
|
19
|
+
sleep(60)
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def notify
|
24
|
+
@settings[:urls].each do |url|
|
25
|
+
if !url[:bad].empty? and (url[:changed] or url[:time_past])
|
26
|
+
title = []
|
27
|
+
title << "Danger" unless url[:dangers].empty?
|
28
|
+
title << "Warnings" unless url[:warnings].empty?
|
29
|
+
title = title.join(" & ") + " for host #{URI.parse(url[:url]).host}"
|
30
|
+
|
31
|
+
warnings_text = !url[:warnings].empty? ? "Warnings for #{url[:bad].select { |(k, v)| v['status'] == 'warning' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
|
32
|
+
danger_text = !url[:dangers].empty? ? "Dangerous situation for #{url[:bad].select { |(k, v)| v['status'] == 'danger' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
|
33
|
+
|
34
|
+
@g.notify "Webstats Notification", title, [danger_text, warnings_text].compact.join(" "), nil, nil, true, (!url[:dangers].empty? ? 2 : 1)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
s = WGrowlNotifier.new()
|
41
|
+
s.start
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'thread'
|
5
|
+
require 'time'
|
6
|
+
require 'net/smtp'
|
7
|
+
|
8
|
+
def daemonize!
|
9
|
+
return if $DAEMONIZE == false
|
10
|
+
if $DEBUG
|
11
|
+
Thread.abort_on_exception
|
12
|
+
else
|
13
|
+
if (pid = fork)
|
14
|
+
Signal.trap('HUP', 'IGNORE')
|
15
|
+
Process.detach(pid)
|
16
|
+
exit
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Hash
|
22
|
+
alias_method :undecorated_get, :[] unless method_defined?(:undecorated_get)
|
23
|
+
def [](key)
|
24
|
+
undecorated_get(key) or undecorated_get(key.is_a?(String) ? key.to_sym : key.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class SimpleNotifier
|
29
|
+
def initialize(settings = {}, read_config = true)
|
30
|
+
@name ||= "notifier"
|
31
|
+
@default_settings ||= {}
|
32
|
+
|
33
|
+
if read_config
|
34
|
+
config_file_path = File.expand_path("~/.webstats_clients")
|
35
|
+
|
36
|
+
@settings = {}
|
37
|
+
|
38
|
+
if File.exists?(config_file_path)
|
39
|
+
@settings = YAML.load(IO.read(config_file_path))
|
40
|
+
@settings ||= {}
|
41
|
+
end
|
42
|
+
|
43
|
+
unless @settings.key?(@name)
|
44
|
+
@settings[@name] = { 'urls' => [{ 'url' => 'http://localhost:9970/', 'password' => nil }] }.merge(@default_settings).merge(settings)
|
45
|
+
File.open(config_file_path, "w") { |f| YAML.dump(@settings, f) }
|
46
|
+
|
47
|
+
puts @no_settings_message
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
@settings = @settings[@name]
|
52
|
+
else
|
53
|
+
@settings = settings
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def start
|
58
|
+
@settings[:urls].each do |url|
|
59
|
+
url[:mutex] = Mutex.new
|
60
|
+
Thread.new do
|
61
|
+
url[:mutex].synchronize { url.merge!({ :meta_info => make_request(URI.join(url[:url], "information"), url[:password]), :last_time => 0 }) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
while(true)
|
66
|
+
threads = []
|
67
|
+
@settings[:urls].each do |url|
|
68
|
+
threads << Thread.new do
|
69
|
+
url[:mutex].synchronize do
|
70
|
+
url[:data] = make_request(URI.join(url[:url], "update"), url[:password])
|
71
|
+
url[:bad] = url[:data].sort { |a, b| b[1]['importance'].to_f <=> a[1]['importance'].to_f }.select { |(k, v)| !v['status'].nil? && v['status'] != '' }
|
72
|
+
url[:last_warnings] = url[:warnings] || []
|
73
|
+
url[:warnings] = url[:bad].select { |(k, v)| v['status'] == 'warning' }
|
74
|
+
url[:has_warnings]= !url[:warnings].empty?
|
75
|
+
url[:last_dangers] = url[:dangers] || []
|
76
|
+
url[:dangers] = url[:bad].select { |(k, v)| v['status'] == 'danger' }
|
77
|
+
url[:has_dangers] = !url[:dangers].empty?
|
78
|
+
url[:changed] = (!url[:warnings].empty? || !url[:dangers].empty?) && (!url.key?(:changed) or (url[:warnings].length > url[:last_warnings].length) or (url[:dangers].length > url[:last_dangers].length))
|
79
|
+
url[:time_past] = url[:last_time] != 0 && (Time.now - url[:last_time]) > 60
|
80
|
+
url[:last_time] = Time.now if url[:changed] || url[:last_time].nil? || url[:time_past]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
threads.each { |t| t.join }
|
85
|
+
notify
|
86
|
+
sleep(10)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def make_request(url, password)
|
92
|
+
while(true)
|
93
|
+
begin
|
94
|
+
Net::HTTP.start(url.host, url.port) { |http|
|
95
|
+
http.read_timeout = http.open_timeout = 15
|
96
|
+
req = Net::HTTP::Get.new(url.request_uri)
|
97
|
+
req.basic_auth 'webstats', password unless password.nil?
|
98
|
+
return YAML.load(http.request(req).body)
|
99
|
+
}
|
100
|
+
rescue Exception => e
|
101
|
+
return nil unless failed_url(url, password, e)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class DataProviders::CpuInfo
|
2
|
+
def initialize(settings)
|
3
|
+
@settings = self.class.default_settings.merge(settings)
|
4
|
+
|
5
|
+
@readings = []
|
6
|
+
@mutex = Mutex.new
|
7
|
+
|
8
|
+
@thread = Thread.new do
|
9
|
+
last_time = last_user = last_nice = last_system = last_idle = last_iowait = 0
|
10
|
+
first_time = true
|
11
|
+
while(true)
|
12
|
+
time = (Time.new.to_f * 1000).to_i
|
13
|
+
|
14
|
+
user, nice, system, idle, iowait, crap = IO.readlines("/proc/stat").first.split(' ', 6).map { |i| i.to_i }
|
15
|
+
|
16
|
+
if first_time
|
17
|
+
first_time = false
|
18
|
+
else
|
19
|
+
temp_user = (user - last_user)
|
20
|
+
temp_nice = (nice - last_nice)
|
21
|
+
temp_system = (system - last_system)
|
22
|
+
temp_idle = (idle - last_idle)
|
23
|
+
temp_iowait = (iowait - last_iowait)
|
24
|
+
|
25
|
+
@mutex.synchronize do
|
26
|
+
@readings.unshift((temp_user + temp_nice + temp_system) / (temp_idle.to_f < 1 ? 1 : temp_idle.to_f))
|
27
|
+
@readings.pop while @readings.length > 5
|
28
|
+
end
|
29
|
+
end
|
30
|
+
last_user = user
|
31
|
+
last_nice = nice
|
32
|
+
last_system = system
|
33
|
+
last_idle = idle
|
34
|
+
last_iowait = iowait
|
35
|
+
last_time = time
|
36
|
+
sleep(@settings[:update_rate])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def get
|
42
|
+
out = { :usage => 0 }
|
43
|
+
@mutex.synchronize do
|
44
|
+
unless @readings.empty?
|
45
|
+
out[:usage] = @readings.first
|
46
|
+
out[:status] = 'warning' unless @readings.detect { |r| out[:usage] < @settings[:usage_warning_level] }
|
47
|
+
out[:status] = 'danger' unless @readings.detect { |r| out[:usage] < @settings[:usage_danger_level] }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
out[:loadavg_1], out[:loadavg_5], out[:loadavg_15] = IO.readlines("/proc/loadavg").first.split(' ', 4).map { |v| v.to_f }
|
51
|
+
out
|
52
|
+
end
|
53
|
+
|
54
|
+
def renderer
|
55
|
+
information.merge({ :contents => %{
|
56
|
+
sc.innerHTML = "<div class='major_figure'><span class='title'>Usage</span><span class='figure'>" + data_source['usage'] + "</span><span class='unit'>%</span></div>" +
|
57
|
+
"<div class='major_figure'><span class='title'>Load average</span><span class='figure'>" + data_source['loadavg_1'] +
|
58
|
+
"</span><span class='unit'>1m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_5'] +
|
59
|
+
"</span><span class='unit'>5m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_15'] + "</span><span class='unit'>15m</span></div>";
|
60
|
+
} })
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.default_settings
|
64
|
+
{ :update_rate => 2.5, :usage_warning_level => 95, :usage_danger_level => 99.5 }
|
65
|
+
end
|
66
|
+
|
67
|
+
def information
|
68
|
+
{ :name => "CPU Info", :in_sentence => 'CPU load', :importance => 100 }
|
69
|
+
end
|
70
|
+
|
71
|
+
def kill
|
72
|
+
@thread.kill
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class DataProviders::DiskActivity
|
2
|
+
def initialize(settings)
|
3
|
+
@settings = self.class.default_settings.merge(settings)
|
4
|
+
|
5
|
+
@reads_sec = 0
|
6
|
+
@writes_sec = 0
|
7
|
+
|
8
|
+
@thread = Thread.new do
|
9
|
+
last_time = last_reads = last_writes = 0
|
10
|
+
first_time = true
|
11
|
+
while(true)
|
12
|
+
time = (Time.new.to_f * 1000).to_i
|
13
|
+
|
14
|
+
reads, writes = IO.readlines("/proc/diskstats").map { |l| parts = l.split; [parts[5].to_i, parts[9].to_i] }.inject([0, 0]) { |sum, vals| [sum[0] + vals[0], sum[1] + vals[1]] }
|
15
|
+
|
16
|
+
if first_time
|
17
|
+
first_time = false
|
18
|
+
else
|
19
|
+
@reads_sec = ((reads - last_reads) / ((time - last_time).to_f / 1000.0)) * 512
|
20
|
+
@writes_sec = ((writes - last_writes) / ((time - last_time).to_f / 1000.0)) * 512
|
21
|
+
end
|
22
|
+
last_reads = reads
|
23
|
+
last_writes = writes
|
24
|
+
last_time = time
|
25
|
+
sleep(@settings[:update_rate])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get
|
31
|
+
{ :reads => @reads_sec / 1024.0 / 1024.0, :writes => @writes_sec / 1024.0 / 1024.0 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def renderer
|
35
|
+
information.merge({ :contents => %{
|
36
|
+
sc.innerHTML = "<div class='major_figure'><span class='title'>Reads</span><span class='figure'>" + data_source['reads'] + "</span><span class='unit'>mb/s</span></div>" +
|
37
|
+
"<div class='major_figure'><span class='title'>Writes</span><span class='figure'>" + data_source['writes'] + "</span><span class='unit'>mb/s</span></div>";
|
38
|
+
} })
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.default_settings
|
42
|
+
{ :update_rate => 2.5 }
|
43
|
+
end
|
44
|
+
|
45
|
+
def information
|
46
|
+
{ :name => "Disk Activity", :in_sentence => "Disk Activity", :importance => 70 }
|
47
|
+
end
|
48
|
+
|
49
|
+
def kill
|
50
|
+
@thread.kill
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <sys/statvfs.h>
|
3
|
+
|
4
|
+
static VALUE get_disk_usage(VALUE self, VALUE mount_point)
|
5
|
+
{
|
6
|
+
char* mount_point_str = RSTRING(mount_point)->ptr;
|
7
|
+
|
8
|
+
VALUE out_hash = rb_hash_new();
|
9
|
+
rb_hash_aset(out_hash, rb_str_new2("free"), Qnil);
|
10
|
+
rb_hash_aset(out_hash, rb_str_new2("total"), Qnil);
|
11
|
+
|
12
|
+
struct statvfs result;
|
13
|
+
|
14
|
+
if(statvfs(mount_point_str, &result) == 0)
|
15
|
+
{
|
16
|
+
rb_hash_aset(out_hash, rb_str_new2("free"), LL2NUM((long long)result.f_bavail * (long long)result.f_frsize));
|
17
|
+
rb_hash_aset(out_hash, rb_str_new2("total"), LL2NUM((long long)result.f_blocks * (long long)result.f_frsize));
|
18
|
+
}
|
19
|
+
|
20
|
+
return out_hash;
|
21
|
+
}
|
22
|
+
|
23
|
+
void Init_disk_usage()
|
24
|
+
{
|
25
|
+
VALUE mDataProviders = rb_const_get(rb_cObject, rb_intern("DataProviders"));
|
26
|
+
VALUE cDiskUsage = rb_const_get(mDataProviders, rb_intern("DiskUsage"));
|
27
|
+
|
28
|
+
rb_define_method(cDiskUsage, "get_disk_usage", get_disk_usage, 1);
|
29
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class DataProviders::DiskUsage
|
2
|
+
def initialize(settings)
|
3
|
+
@settings = self.class.default_settings.merge(settings)
|
4
|
+
end
|
5
|
+
|
6
|
+
def get
|
7
|
+
out = { :mounts => [] }
|
8
|
+
|
9
|
+
mtab = IO.readlines("/etc/mtab").sort_by { |l| l.split[1] }
|
10
|
+
mtab.map do |mp|
|
11
|
+
parts = mp.split
|
12
|
+
next unless parts[3].split(",").detect { |p| p == "rw" }
|
13
|
+
du = get_disk_usage(parts[1])
|
14
|
+
out[:mounts] << [parts[1], du] unless du['total'] == 0 or (du['total'] > 5242880 && ((du['total'] - du['free']) <= 1048576))
|
15
|
+
end
|
16
|
+
|
17
|
+
out[:mounts].map do |mp|
|
18
|
+
mp[1]['free'] /= (1024.0 * 1024)
|
19
|
+
mp[1]['total'] /= (1024.0 * 1024)
|
20
|
+
out[:status] = "warning" if mp[1]['free'] < @settings[:warning_threshold] and mp[1]['total'] > @settings[:warning_minimum_mount_point_size] and out[:status] != 'danger'
|
21
|
+
out[:status] = "danger" if mp[1]['free'] < @settings[:danger_threshold] and mp[1]['total'] > @settings[:danger_minimum_mount_point_size]
|
22
|
+
end
|
23
|
+
|
24
|
+
out
|
25
|
+
end
|
26
|
+
|
27
|
+
def renderer
|
28
|
+
information.merge({ :contents => %{
|
29
|
+
var temp = "";
|
30
|
+
for(var i = 0; i < data_source['mounts'].length; i++)
|
31
|
+
{
|
32
|
+
var mpd = data_source['mounts'][i][1];
|
33
|
+
temp += "<div class='major_figure'><span class='title'>" + data_source['mounts'][i][0] + "</span><span class='figure'>" + mpd['free'] +
|
34
|
+
"</span><span class='unit'>mb free</span><span class='divider'>/</span><span class='figure'>" + mpd['total'] +
|
35
|
+
"</span><span class='unit'>mb total</span></div>";
|
36
|
+
}
|
37
|
+
|
38
|
+
sc.innerHTML = temp;
|
39
|
+
} })
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.default_settings
|
43
|
+
{ :warning_threshold => 50, :danger_threshold => 10, :warning_minimum_mount_point_size => 100, :danger_minimum_mount_point_size => 20 }
|
44
|
+
end
|
45
|
+
|
46
|
+
def information
|
47
|
+
{ :name => "Disk Usage by Mount Point", :in_sentence => "Disk Usage", :importance => 80 }
|
48
|
+
end
|
49
|
+
|
50
|
+
def kill
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require File.dirname(__FILE__) + '/disk_usage.so'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class DataProviders::MemInfo
|
2
|
+
def initialize(settings)
|
3
|
+
@settings = self.class.default_settings.merge(settings)
|
4
|
+
|
5
|
+
@readings = []
|
6
|
+
@mutex = Mutex.new
|
7
|
+
|
8
|
+
@thread = Thread.new do
|
9
|
+
while(true)
|
10
|
+
out = {}
|
11
|
+
out[:total], out[:free], out[:buffers], out[:cached] = IO.readlines("/proc/meminfo")[0..4].map { |l| l =~ /^.*?\: +(.*?) kB$/; $1.to_i / 1024.0 }
|
12
|
+
out[:free_total] = out[:free] + out[:buffers] + out[:cached]
|
13
|
+
|
14
|
+
@mutex.synchronize do
|
15
|
+
@readings.unshift(out)
|
16
|
+
@readings.pop while @readings.length > 5
|
17
|
+
end
|
18
|
+
sleep(@settings[:update_rate])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get
|
24
|
+
out = { :total => 0, :free => 0, :buffers => 0, :cached => 0, :free_total => 0 }
|
25
|
+
@mutex.synchronize do
|
26
|
+
unless @readings.empty?
|
27
|
+
out = @readings.first.dup
|
28
|
+
out[:status] = 'warning' unless @readings.detect { |r| r[:free] > 5 }
|
29
|
+
out[:status] = 'danger' unless @readings.detect { |r| r[:free_total] > 1 }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
out
|
33
|
+
end
|
34
|
+
|
35
|
+
def renderer
|
36
|
+
information.merge({ :contents => %{
|
37
|
+
sc.innerHTML = "<div class='major_figure'><span class='title'>Free</span><span class='figure'>" + data_source['free'] + "</span><span class='unit'>mb</span></div>" +
|
38
|
+
"<div class='major_figure'><span class='title'>Free -buffers/cache</span><span class='figure'>" + data_source['free_total'] + "</span><span class='unit'>mb</span></div>" +
|
39
|
+
"<div class='major_figure'><span class='title'>Total</span><span class='figure'>" + data_source['total'] + "</span><span class='unit'>mb</span></div>";
|
40
|
+
} })
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.default_settings
|
44
|
+
{ :update_rate => 2.5 }
|
45
|
+
end
|
46
|
+
|
47
|
+
def information
|
48
|
+
{ :name => "Memory Info", :in_sentence => 'Memory Usage', :importance => 90 }
|
49
|
+
end
|
50
|
+
|
51
|
+
def kill
|
52
|
+
@thread.kill
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
class DataProviders::UrlMonitor
|
4
|
+
def initialize(settings)
|
5
|
+
@settings = self.class.default_settings.merge(settings)
|
6
|
+
|
7
|
+
@readings = {}
|
8
|
+
|
9
|
+
@mutex = Mutex.new
|
10
|
+
|
11
|
+
@thread = Thread.new do
|
12
|
+
while(true)
|
13
|
+
@settings[:urls].sort.each do |url|
|
14
|
+
@mutex.synchronize { @readings[url] = { :response_time => -1, :works => (@readings.key?(url) && @readings[url][:works] == :failed ? :failed : :waiting) } }
|
15
|
+
duration = 0
|
16
|
+
works = :failed
|
17
|
+
begin
|
18
|
+
uri = URI.parse(url)
|
19
|
+
start = Time.now
|
20
|
+
Net::HTTP.start(uri.host, uri.port) { |http|
|
21
|
+
http.read_timeout = http.open_timeout = @settings[:danger_response_time_threshold]
|
22
|
+
raise Exception unless [200, 201, 202, 203, 204, 205, 206, 301, 302, 304].include? http.get(uri.path).code.to_i
|
23
|
+
}
|
24
|
+
|
25
|
+
duration = Time.now - start
|
26
|
+
works = :works
|
27
|
+
rescue Exception => e
|
28
|
+
end
|
29
|
+
@mutex.synchronize { @readings[url] = { :response_time => duration * 1000, :works => works } }
|
30
|
+
end
|
31
|
+
sleep(@settings[:update_rate])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def get
|
38
|
+
out = {}
|
39
|
+
@mutex.synchronize { out[:urls] = @readings.to_a.sort_by { |e| e[0] } }
|
40
|
+
out[:urls].each do |(url, info)|
|
41
|
+
out[:status] = 'warning' if (info[:works] == :failed) or info[:response_time] > @settings[:warning_response_time_threshold] * 1000 and !out[:status] == 'danger'
|
42
|
+
out[:status] = 'danger' if (info[:works] == :failed) or info[:response_time] > @settings[:danger_response_time_threshold] * 1000
|
43
|
+
end
|
44
|
+
out
|
45
|
+
end
|
46
|
+
|
47
|
+
def renderer
|
48
|
+
information.merge({ :contents => %{
|
49
|
+
var temp = "";
|
50
|
+
for(var i = 0; i < data_source['urls'].length; i++)
|
51
|
+
{
|
52
|
+
var ud = data_source['urls'][i][1];
|
53
|
+
temp += "<div class='major_figure'><span class='title'>" + data_source['urls'][i][0] + "</span><span class='figure'>" +
|
54
|
+
(ud['works'] == 'failed' ? 'Failed</span>' : (ud['works'] == 'waiting' ? 'Waiting</span>' : ud['response_time'] + "</span><span class='unit'>ms</span>")) + "</div>";
|
55
|
+
}
|
56
|
+
|
57
|
+
sc.innerHTML = temp;
|
58
|
+
} })
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.default_settings
|
62
|
+
{ :update_rate => 30, :warning_response_time_threshold => 5, :danger_response_time_threshold => 15, :urls => ['http://localhost/'] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def information
|
66
|
+
{ :name => "URL Monitor", :in_sentence => "URL Monitor", :importance => 60 }
|
67
|
+
end
|
68
|
+
|
69
|
+
def kill
|
70
|
+
end
|
71
|
+
end
|
data/server/webstats.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
#NO GEM DEPENDENCIES FTW
|
2
|
+
require 'webrick'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
if $DEBUG
|
6
|
+
Thread.abort_on_exception
|
7
|
+
else
|
8
|
+
if (pid = fork)
|
9
|
+
Signal.trap('HUP', 'IGNORE')
|
10
|
+
Process.detach(pid)
|
11
|
+
exit
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Thread.new do
|
16
|
+
while(true)
|
17
|
+
sleep(300)
|
18
|
+
GC.start
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class NilClass
|
23
|
+
def to_json; "null"; end
|
24
|
+
end
|
25
|
+
|
26
|
+
class TrueClass
|
27
|
+
def to_json; "true"; end
|
28
|
+
end
|
29
|
+
|
30
|
+
class FalseClass
|
31
|
+
def to_json; "false"; end
|
32
|
+
end
|
33
|
+
|
34
|
+
class String
|
35
|
+
def underscore
|
36
|
+
self.gsub(/::/, '/').
|
37
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
38
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
39
|
+
tr("-", "_").
|
40
|
+
downcase
|
41
|
+
end
|
42
|
+
alias_method :to_json, :inspect
|
43
|
+
end
|
44
|
+
|
45
|
+
class Numeric
|
46
|
+
def formatted(precision = 1)
|
47
|
+
rounded_number = (Float(self) * (10 ** precision)).round.to_f / 10 ** precision
|
48
|
+
parts = ("%01.#{precision}f" % rounded_number).to_s.split('.')
|
49
|
+
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
|
50
|
+
parts.join(".")
|
51
|
+
end
|
52
|
+
alias_method :to_json, :inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
class Array
|
56
|
+
def formatted!
|
57
|
+
each_with_index do |v, i|
|
58
|
+
if v.is_a? Numeric
|
59
|
+
self[i] = v.formatted
|
60
|
+
elsif v.is_a? Hash or v.is_a? Array
|
61
|
+
self[i] = self[i].dup.formatted!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def symbolize_keys!
|
67
|
+
each_with_index { |v, i| self[i] = self[i].dup.symbolize_keys! if v.is_a? Hash }
|
68
|
+
end
|
69
|
+
|
70
|
+
def stringify_keys!
|
71
|
+
each_with_index { |v, i| self[i] = self[i].dup.stringify_keys! if v.is_a? Hash }
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_json
|
75
|
+
"[#{map { |e| e.to_json }.join(', ')}]"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Hash
|
80
|
+
def formatted!
|
81
|
+
each_pair do |k, v|
|
82
|
+
if v.is_a? Numeric
|
83
|
+
self[k] = v.formatted
|
84
|
+
elsif v.is_a? Hash or v.is_a? Array
|
85
|
+
self[k] = self[k].dup.formatted!
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def symbolize_keys!
|
91
|
+
keys.each { |key| self[key.to_sym] = delete(key) }
|
92
|
+
each_pair { |k, v| self[k] = self[k].dup.symbolize_keys! if v.is_a? Hash }
|
93
|
+
end
|
94
|
+
|
95
|
+
def stringify_keys!
|
96
|
+
keys.each { |key| self[key.to_s] = delete(key) }
|
97
|
+
each_pair { |k, v| self[k] = self[k].dup.stringify_keys! if v.is_a? Hash }
|
98
|
+
end
|
99
|
+
|
100
|
+
alias_method :undecorated_get, :[] unless method_defined?(:undecorated_get)
|
101
|
+
def [](key)
|
102
|
+
undecorated_get(key) or undecorated_get(key.is_a?(String) ? key.to_sym : key.to_s)
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_json
|
106
|
+
arr = []
|
107
|
+
each_pair { |k, v| arr << "#{k.to_json}: #{v.to_json}" }
|
108
|
+
"{#{arr.join(', ')}}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Symbol
|
113
|
+
def to_json
|
114
|
+
to_s.inspect
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module DataProviders
|
119
|
+
DATA_SOURCES_CLASSES = {}
|
120
|
+
DATA_SOURCES = {}
|
121
|
+
def self.preload
|
122
|
+
Dir.glob("#{File.dirname(__FILE__)}/data_providers/*.rb").each { |file| load file unless file =~ /extconf.rb$/ }
|
123
|
+
DataProviders.constants.each do |c|
|
124
|
+
c = DataProviders.const_get(c)
|
125
|
+
DATA_SOURCES_CLASSES[c.to_s.gsub(/^DataProviders::/, '').underscore] = c if c.is_a? Class
|
126
|
+
end
|
127
|
+
end
|
128
|
+
def self.setup(settings)
|
129
|
+
DATA_SOURCES_CLASSES.each_pair { |k, v| DATA_SOURCES[k] = v.new(settings[k]) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
DataProviders.preload
|
134
|
+
|
135
|
+
WEBSTATS_PATH = File.expand_path("~/.webstats")
|
136
|
+
|
137
|
+
$settings = {}
|
138
|
+
|
139
|
+
if File.exists?(WEBSTATS_PATH)
|
140
|
+
$settings = YAML.load(IO.read(WEBSTATS_PATH)).symbolize_keys!
|
141
|
+
else
|
142
|
+
$settings['webstats'] = { 'password' => nil, 'clients' => [] }
|
143
|
+
DataProviders::DATA_SOURCES_CLASSES.each_pair { |k, v| $settings[k.to_s] = v.default_settings.stringify_keys! }
|
144
|
+
File.open(WEBSTATS_PATH, "w") { |f| YAML.dump($settings, f) }
|
145
|
+
end
|
146
|
+
|
147
|
+
DataProviders.setup($settings)
|
148
|
+
|
149
|
+
class Webstats < WEBrick::HTTPServlet::AbstractServlet
|
150
|
+
def do_GET(req, res)
|
151
|
+
WEBrick::HTTPAuth.basic_auth(req, res, "Webstats") { |u, p| u == 'webstats' and p == $settings[:webstats][:password] } unless $settings[:webstats][:password].nil?
|
152
|
+
|
153
|
+
body = ""
|
154
|
+
if req.path_info == '/'
|
155
|
+
body << <<-EOF
|
156
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
157
|
+
<html>
|
158
|
+
<head>
|
159
|
+
<title>Webstats</title>
|
160
|
+
<style type="text/css">
|
161
|
+
* { margin: 0; padding: 0; font-family: "Lucida Grande", Helvetica, Arial, sans-serif; font-size: 100%; }
|
162
|
+
body { font-size: 95%; }
|
163
|
+
p { margin: 0 0 1em 0; }
|
164
|
+
|
165
|
+
h1 { margin: 1em; }
|
166
|
+
h1 span { font-size: 160%; font-weight: bold; }
|
167
|
+
.source .danger { background-color: #FF0D33; }
|
168
|
+
.source .warning { background-color: #F1FF28; }
|
169
|
+
|
170
|
+
.source { width: 500px; border: 1px solid #000000; margin: 1em; }
|
171
|
+
.source h2 { padding: 0 0.8em 0 0.8em; background-color: #C98300; }
|
172
|
+
.source h2 span { font-size: 130%; font-weight: bold; padding: 0.2em 0; display: block; }
|
173
|
+
.source .source_contents { padding: 0.8em; }
|
174
|
+
.source .title { padding-right: 0.5em; }
|
175
|
+
.source .major_figure { font-size: 130%; margin: 0.3em 0; }
|
176
|
+
.source .major_figure .figure { font-size: 120%; font-weight: bold; font-family: Georgia, serif; }
|
177
|
+
.source .major_figure .unit { font-family: Georgia, serif; font-size: 70%; }
|
178
|
+
.source .minor_figure { font-family: Georgia, serif; }
|
179
|
+
.source .divider { margin-left: 0.2em; margin-right: 0.2em; font-weight: normal; }
|
180
|
+
</style>
|
181
|
+
<script type="text/javascript">
|
182
|
+
var http = null;
|
183
|
+
|
184
|
+
function getLatest()
|
185
|
+
{
|
186
|
+
http.open("get", "/update", true);
|
187
|
+
http.send(null);
|
188
|
+
}
|
189
|
+
|
190
|
+
window.onload = function()
|
191
|
+
{
|
192
|
+
http = !!(window.attachEvent && !window.opera) ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
|
193
|
+
|
194
|
+
http.onreadystatechange = function()
|
195
|
+
{
|
196
|
+
if(http.readyState == 4)
|
197
|
+
{
|
198
|
+
var results = eval("(" + http.responseText + ")");
|
199
|
+
if(!results) return;
|
200
|
+
EOF
|
201
|
+
|
202
|
+
DataProviders::DATA_SOURCES.each_pair do |k, v|
|
203
|
+
body << %{var data_source = results['#{k}']; var sc = document.getElementById('source_contents_#{k}'); sc.className = "source_contents " + (data_source['status'] ? data_source['status'] : ''); #{v.renderer[:contents]}\n}
|
204
|
+
end
|
205
|
+
|
206
|
+
body << <<-EOF
|
207
|
+
}
|
208
|
+
};
|
209
|
+
|
210
|
+
window.setInterval("getLatest()", 5000);
|
211
|
+
getLatest();
|
212
|
+
}
|
213
|
+
</script>
|
214
|
+
</head>
|
215
|
+
<body id="body">
|
216
|
+
<div id="main">
|
217
|
+
<h1><span>Stats for #{req.host}</span></h1>
|
218
|
+
EOF
|
219
|
+
DataProviders::DATA_SOURCES.sort { |a, b| b[1].information[:importance] <=> a[1].information[:importance] }.each do |(k, v)|
|
220
|
+
r = v.renderer
|
221
|
+
body << %{<div class="source" id="source_#{k}"><h2><span>#{r[:name]}</span></h2><div class="source_contents" id="source_contents_#{k}">Loading...</div></div>}
|
222
|
+
end
|
223
|
+
|
224
|
+
body << <<-EOF
|
225
|
+
</div>
|
226
|
+
</body>
|
227
|
+
</html>
|
228
|
+
EOF
|
229
|
+
elsif req.path_info == '/update'
|
230
|
+
out = {}
|
231
|
+
DataProviders::DATA_SOURCES.each_pair do |k, v|
|
232
|
+
out[k] = v.get.dup
|
233
|
+
end
|
234
|
+
|
235
|
+
out.formatted!
|
236
|
+
|
237
|
+
body << out.to_json
|
238
|
+
elsif req.path_info == '/information'
|
239
|
+
out = {}
|
240
|
+
DataProviders::DATA_SOURCES.each_pair { |k, v| out[k] = v.information }
|
241
|
+
body << out.to_json
|
242
|
+
end
|
243
|
+
|
244
|
+
res.body = body
|
245
|
+
res['Content-Type'] = "text/html"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
threads = []
|
250
|
+
|
251
|
+
s = WEBrick::HTTPServer.new(:Port => 9970, :Logger => WEBrick::Log.new(nil, 0), :AccessLog => WEBrick::Log.new(nil, 0))
|
252
|
+
|
253
|
+
death = proc do
|
254
|
+
s.shutdown
|
255
|
+
DataProviders::DATA_SOURCES.each_pair { |k, v| v.kill }
|
256
|
+
threads.each { |t| t.kill }
|
257
|
+
end
|
258
|
+
trap("INT", death)
|
259
|
+
trap("TERM", death)
|
260
|
+
|
261
|
+
s.mount("/", Webstats)
|
262
|
+
threads << Thread.new { s.start }
|
263
|
+
|
264
|
+
if $settings['webstats'].key?(:clients) and !$settings['webstats'][:clients].nil?
|
265
|
+
$DAEMONIZE = false
|
266
|
+
$settings['webstats'][:clients].each { |name| threads << Thread.new { require "#{File.dirname(__FILE__)}/../clients/#{name}/#{name}.rb" } }
|
267
|
+
end
|
268
|
+
|
269
|
+
threads.first.join
|
data/webstats.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{webstats}
|
5
|
+
s.version = "0.10.5"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Brenton Fletcher"]
|
9
|
+
s.date = %q{2009-06-27}
|
10
|
+
s.description = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
|
11
|
+
s.email = %q{i@bloople.net}
|
12
|
+
s.executables = ["webstats", "webstats_growl_notifier", "webstats_email_notifier"]
|
13
|
+
s.extensions = ["server/data_providers/extconf.rb"]
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.textile"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"LICENSE",
|
20
|
+
"README.textile",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION.yml",
|
23
|
+
"bin/webstats",
|
24
|
+
"bin/webstats_email_notifier",
|
25
|
+
"bin/webstats_growl_notifier",
|
26
|
+
"clients/email_notifier/README.textile",
|
27
|
+
"clients/email_notifier/email_notifier.rb",
|
28
|
+
"clients/growl_notifier/Growl.rb",
|
29
|
+
"clients/growl_notifier/README.textile",
|
30
|
+
"clients/growl_notifier/growl_notifier.rb",
|
31
|
+
"clients/simple_notifier.rb",
|
32
|
+
"server/data_providers/cpu_info.rb",
|
33
|
+
"server/data_providers/disk_activity.rb",
|
34
|
+
"server/data_providers/disk_usage.c",
|
35
|
+
"server/data_providers/disk_usage.rb",
|
36
|
+
"server/data_providers/extconf.rb",
|
37
|
+
"server/data_providers/mem_info.rb",
|
38
|
+
"server/data_providers/url_monitor.rb",
|
39
|
+
"server/webstats.rb",
|
40
|
+
"webstats.gemspec"
|
41
|
+
]
|
42
|
+
s.homepage = %q{http://github.com/bloopletech/webstats}
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.require_paths = [""]
|
45
|
+
s.rubygems_version = %q{1.3.1}
|
46
|
+
s.summary = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
|
47
|
+
|
48
|
+
if s.respond_to? :specification_version then
|
49
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
50
|
+
s.specification_version = 2
|
51
|
+
|
52
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
53
|
+
else
|
54
|
+
end
|
55
|
+
else
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: webstats
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Brenton Fletcher
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-25 00:00:00 +10:30
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.
|
22
|
+
email: i@bloople.net
|
23
|
+
executables:
|
24
|
+
- webstats
|
25
|
+
- webstats_growl_notifier
|
26
|
+
- webstats_email_notifier
|
27
|
+
extensions:
|
28
|
+
- server/data_providers/extconf.rb
|
29
|
+
extra_rdoc_files:
|
30
|
+
- LICENSE
|
31
|
+
- README.textile
|
32
|
+
files:
|
33
|
+
- LICENSE
|
34
|
+
- README.textile
|
35
|
+
- Rakefile
|
36
|
+
- VERSION.yml
|
37
|
+
- bin/webstats
|
38
|
+
- bin/webstats_email_notifier
|
39
|
+
- bin/webstats_growl_notifier
|
40
|
+
- clients/email_notifier/README.textile
|
41
|
+
- clients/email_notifier/email_notifier.rb
|
42
|
+
- clients/growl_notifier/Growl.rb
|
43
|
+
- clients/growl_notifier/README.textile
|
44
|
+
- clients/growl_notifier/growl_notifier.rb
|
45
|
+
- clients/simple_notifier.rb
|
46
|
+
- server/data_providers/cpu_info.rb
|
47
|
+
- server/data_providers/disk_activity.rb
|
48
|
+
- server/data_providers/disk_usage.c
|
49
|
+
- server/data_providers/disk_usage.rb
|
50
|
+
- server/data_providers/extconf.rb
|
51
|
+
- server/data_providers/mem_info.rb
|
52
|
+
- server/data_providers/url_monitor.rb
|
53
|
+
- server/webstats.rb
|
54
|
+
- webstats.gemspec
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/bloopletech/webstats
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- ""
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.3.6
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.
|
85
|
+
test_files: []
|
86
|
+
|