mouth 0.8.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/.gitignore +7 -0
- data/Capfile +26 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +138 -0
- data/Rakefile +19 -0
- data/TODO +32 -0
- data/bin/mouth +77 -0
- data/bin/mouth-console +18 -0
- data/bin/mouth-endoscope +18 -0
- data/lib/mouth.rb +61 -0
- data/lib/mouth/dashboard.rb +25 -0
- data/lib/mouth/endoscope.rb +120 -0
- data/lib/mouth/endoscope/public/222222_256x240_icons_icons.png +0 -0
- data/lib/mouth/endoscope/public/application.css +464 -0
- data/lib/mouth/endoscope/public/application.js +938 -0
- data/lib/mouth/endoscope/public/backbone.js +1158 -0
- data/lib/mouth/endoscope/public/d3.js +4707 -0
- data/lib/mouth/endoscope/public/d3.time.js +687 -0
- data/lib/mouth/endoscope/public/jquery-ui-1.8.16.custom.min.js +177 -0
- data/lib/mouth/endoscope/public/jquery.js +4 -0
- data/lib/mouth/endoscope/public/json2.js +480 -0
- data/lib/mouth/endoscope/public/keymaster.js +163 -0
- data/lib/mouth/endoscope/public/linen.js +46 -0
- data/lib/mouth/endoscope/public/seven.css +68 -0
- data/lib/mouth/endoscope/public/seven.js +291 -0
- data/lib/mouth/endoscope/public/underscore.js +931 -0
- data/lib/mouth/endoscope/views/dashboard.erb +67 -0
- data/lib/mouth/graph.rb +58 -0
- data/lib/mouth/instrument.rb +56 -0
- data/lib/mouth/record.rb +72 -0
- data/lib/mouth/runner.rb +89 -0
- data/lib/mouth/sequence.rb +284 -0
- data/lib/mouth/source.rb +76 -0
- data/lib/mouth/sucker.rb +235 -0
- data/lib/mouth/version.rb +3 -0
- data/mouth.gemspec +28 -0
- data/test/sequence_test.rb +163 -0
- data/test/sucker_test.rb +55 -0
- data/test/test_helper.rb +5 -0
- metadata +167 -0
data/.gitignore
ADDED
data/Capfile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
load 'deploy'
|
2
|
+
|
3
|
+
# Simply way to deploy the app if you're developing it
|
4
|
+
# I will probably remove this file later.
|
5
|
+
set :application, "mouth"
|
6
|
+
default_run_options[:pty] = true
|
7
|
+
|
8
|
+
set :scm, :none
|
9
|
+
role :app, "sugar"
|
10
|
+
|
11
|
+
task :omg do
|
12
|
+
res = `gem build mouth.gemspec`
|
13
|
+
if res =~ /Success/
|
14
|
+
home = "#{capture('pwd').strip}/mouth"
|
15
|
+
file = res.match(/File: (.+)$/)[1]
|
16
|
+
run "mkdir -p ~/mouth"
|
17
|
+
loc = "#{home}/#{file}"
|
18
|
+
upload file, loc
|
19
|
+
sudo "gem install #{loc}"
|
20
|
+
run "mouth-endoscope -K"
|
21
|
+
run "mouth --pidfile #{home}/mouth.pid -K"
|
22
|
+
run "sleep 5"
|
23
|
+
run "mouth-endoscope --mongohost 10.36.103.70"
|
24
|
+
run "mouth --pidfile #{home}/mouth.pid --logfile #{home}/mouth.log -H 10.18.23.135 -P 8889 --mongohost 10.36.103.70"
|
25
|
+
end
|
26
|
+
end
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Jonathan Novak - http://github.com/cypriss
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Mouth
|
2
|
+
|
3
|
+
Mouth is a Ruby daemon that collects stats via UDP and stores them in Mongo. It comes with a modern UI that allows you to view graphs and create dashboards of these statistics. Mouth is very similar to [StatsD](https://github.com/etsy/statsd) + [Graphite](http://graphite.wikidot.com/) + [Graphene](http://jondot.github.com/graphene/).
|
4
|
+
|
5
|
+
|
6
|
+
## Why
|
7
|
+
|
8
|
+
Why duplicate effort of the excellent StatsD / Graphite packages? I wanted a graphing and monitoring tool that offered:
|
9
|
+
|
10
|
+
|
11
|
+
* **Accessible data**. Because the metrics are stored in Mongo, they are very accessible to any scripts you want to write to read and act on your data.
|
12
|
+
* **Easy installation**. Mouth depends on just Ruby and Mongo, both incredibly easy to install.
|
13
|
+
* **Modern, friendly UI**. Mouth graphs are beautiful and easy to work with. Exploring data and building dashboards are a joy.
|
14
|
+
|
15
|
+
## Kinds of Metrics
|
16
|
+
|
17
|
+
There are two kinds of metrics currently: counters and timers. Both are stored in a time series with minute granularity.
|
18
|
+
|
19
|
+
* **Counters**:
|
20
|
+
* how many occurances of something happen per minute?
|
21
|
+
* Can be sampled
|
22
|
+
* Standard usage: Mouth.increment("myapp.happenings")
|
23
|
+
* Advanced usage: Mouth.increment("myapp.happenings", 1, 0.1) # 1/10 sample rate. UDP packets are sent 1 in 10 times, but count for 1 * 10 each.
|
24
|
+
* Gives you a time series of happenings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
|
25
|
+
* **Timers**:
|
26
|
+
* How long does something take?
|
27
|
+
* Standard usage: Mouth.measure("myapp.occurances", 3.3) # This occurance took 3.3 ms to occur
|
28
|
+
* Standard usage 2: Mouth.measure("myapp.occurances") { do_occurance() }
|
29
|
+
* Gives you a time series of occurance timings by minute: {"myapp.happenings" => [1, 2, 5, 20, ...]}
|
30
|
+
* **Gauges**: (Coming soon)
|
31
|
+
|
32
|
+
## Installation on OSX
|
33
|
+
|
34
|
+
Install mouth:
|
35
|
+
|
36
|
+
gem install mouth
|
37
|
+
|
38
|
+
Install MongoDB if you haven't:
|
39
|
+
|
40
|
+
brew install mongodb
|
41
|
+
|
42
|
+
Start collector:
|
43
|
+
|
44
|
+
mouth
|
45
|
+
|
46
|
+
Start web UI:
|
47
|
+
|
48
|
+
mouth-endoscope
|
49
|
+
|
50
|
+
Record a metric:
|
51
|
+
|
52
|
+
ruby -e 'require "mouth"; require "mouth/instrument"; Mouth.increment("gorets")'
|
53
|
+
|
54
|
+
To load the web UI, go to http://0.0.0.0:5678/ (or whatever port got chosen -- see the Terminal). Click 'Add Graph' in the lower right-hand corner.
|
55
|
+
|
56
|
+
## Installation in Production
|
57
|
+
|
58
|
+
You'll want to follow the general gist of what you did for OSX, but make sure to specify your hosts, ports, and log locations.
|
59
|
+
NOTE: there is no config file -- all options are via command-line.
|
60
|
+
|
61
|
+
sudo gem install mouth
|
62
|
+
mouth --pidfile /path/to/log/mouth.pid --logfile /path/to/log/mouth.log -H x.x.x.x -P 8889 --mongohost y.y.y.y --verbosity 1
|
63
|
+
mouth-endoscope --mongohost x.x.x.x
|
64
|
+
|
65
|
+
## Instrumenting Your Application to Record Metrics
|
66
|
+
|
67
|
+
There are many ways to instrument your application:
|
68
|
+
|
69
|
+
### Using the mouth gem
|
70
|
+
|
71
|
+
Mouth comes with a built-in facility to instrument your apps:
|
72
|
+
|
73
|
+
require 'mouth'
|
74
|
+
require 'mouth/instrument'
|
75
|
+
|
76
|
+
Mouth.server = "0.0.0.0:8889"
|
77
|
+
Mouth.increment('hello.world')
|
78
|
+
Mouth.measure('hello.happening', 42.9)
|
79
|
+
|
80
|
+
### Using mouth-instrument
|
81
|
+
|
82
|
+
mouth-instrument is a lightweight gem that doesn't have the baggage of the various gems that come with mouth. Its usage is nearly identical:
|
83
|
+
|
84
|
+
gem install mouth-instrument
|
85
|
+
|
86
|
+
require 'mouth-instrument'
|
87
|
+
|
88
|
+
Mouth.server = "0.0.0.0:8889"
|
89
|
+
Mouth.increment('hello.world')
|
90
|
+
Mouth.measure('hello.happening', 42.9)
|
91
|
+
|
92
|
+
### Using any StatsD instrumentation
|
93
|
+
|
94
|
+
Mouth is StatsD compatible -- if you've instrumented your application to record StatsD metrics, it should work on Mouth. Just replace your StatsD server with a mouth process.
|
95
|
+
|
96
|
+
## Accessing Your Data Via Scripts
|
97
|
+
|
98
|
+
You can access and act on your metrics quite easily.
|
99
|
+
|
100
|
+
require 'mouth'
|
101
|
+
require 'mouth/sequence'
|
102
|
+
|
103
|
+
Mouth.host = "0.0.0.0:8889"
|
104
|
+
Sequence.new("exceptions.app", :kind => :counter).sequence
|
105
|
+
# => [4, 9, 0, ...]
|
106
|
+
|
107
|
+
Sequence.new("app.requests", :kind => :timer, :granularity_in_minutes => 15, :start_time => Time.now - 86400, :end_time => Time.now).sequence
|
108
|
+
# => [{:count => 3, :min => 1, :max => 30, :mean => 17.0, :sum => 51.0, :median => 20, :stddev => 12.02}, ...]
|
109
|
+
|
110
|
+
## Tech
|
111
|
+
|
112
|
+
* **Ruby** - 1.9.2+ is required. Ruby was chosen because many Ruby shops already have it deployed as part of their infrastucture. By putting everything in Ruby, node + python aren't needed.
|
113
|
+
* **MongoDB** - Mouth stores metrics in Mongo. Mongo was chosen for 3 reasons:
|
114
|
+
* It's very easy to install and get going
|
115
|
+
* It has drivers for everything, so getting at your data is super easy
|
116
|
+
* It's schemaless design is fairly good for storing time series metrics.
|
117
|
+
* **EventMachine** - Mouth is powered by EM, the Ruby way of doing nonblocking IO.
|
118
|
+
* **Sinatra** - The web UI is served with a simple Sinatra app.
|
119
|
+
* **Backbone.js** - The web UI is powered by Backbone.js
|
120
|
+
* **D3.js** - The graphs are powered by D3.js
|
121
|
+
|
122
|
+
|
123
|
+
## Contributing
|
124
|
+
|
125
|
+
You're interested in contributing to Mouth? *AWESOME*. Here are the basic steps:
|
126
|
+
|
127
|
+
fork Mouth from here: http://github.com/cypriss/mouth
|
128
|
+
|
129
|
+
1. Clone your fork
|
130
|
+
2. Hack away
|
131
|
+
3. If you are adding new functionality, document it in the README
|
132
|
+
4. If necessary, rebase your commits into logical chunks, without errors
|
133
|
+
5. Push the branch up to GitHub
|
134
|
+
6. Send a pull request to the cypriss/mouth project.
|
135
|
+
|
136
|
+
## Thanks
|
137
|
+
|
138
|
+
Thanks to UserVoice.com for sponsoring this project. Thanks to the [StatsD](https://github.com/etsy/statsd) project for massive inspiration. Other contributors: https://github.com/cypriss/mouth/graphs/contributors
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'rake/testtask'
|
13
|
+
Rake::TestTask.new(:test) do |test|
|
14
|
+
test.libs << 'lib' << 'test'
|
15
|
+
test.pattern = 'test/*_test.rb'
|
16
|
+
test.verbose = true
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => :test
|
data/TODO
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Next
|
2
|
+
- Cookie-ize the timespan settings
|
3
|
+
- Re implement graph autoplacement
|
4
|
+
- Better backend impl of source list
|
5
|
+
- Figure out how dashboards can scroll
|
6
|
+
- Show period averages / rates
|
7
|
+
- Full keyboard navigation
|
8
|
+
- Build in some mechanisms to diagnose mongo connection issues
|
9
|
+
- Auto refreshing source list (every minute?)
|
10
|
+
- More Tests
|
11
|
+
- Figure out time zones
|
12
|
+
|
13
|
+
Someday
|
14
|
+
- Picker:
|
15
|
+
- When you re-pick sources, it should scroll to the existing picked items
|
16
|
+
- Selected rows should also be highlighted in addition to checked
|
17
|
+
- should show a little sparkline or some sense of how old the data is
|
18
|
+
- Source picker textbox filter
|
19
|
+
- Manual refresh source list icon
|
20
|
+
- Graphing
|
21
|
+
- Restructure charting code to not remove everything when updating with new data
|
22
|
+
- Plugin system
|
23
|
+
- 'Dashboards' should be a module
|
24
|
+
- 'Explorer' module
|
25
|
+
- 'Monitor' module
|
26
|
+
|
27
|
+
Bugs:
|
28
|
+
- When picker is open, changing dashboards should hide picker
|
29
|
+
- tooltip spawns multiple copies on large datasets
|
30
|
+
|
31
|
+
Refactorings:
|
32
|
+
- reconcile naming of 'sources' sometimes being array of strings and sometimes array of objects
|
data/bin/mouth
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'mouth'
|
7
|
+
require 'mouth/runner'
|
8
|
+
require 'mouth/sucker'
|
9
|
+
|
10
|
+
options = {
|
11
|
+
:verbosity => 0,
|
12
|
+
:host => "localhost",
|
13
|
+
:port => 8889
|
14
|
+
}
|
15
|
+
|
16
|
+
parser = OptionParser.new do |op|
|
17
|
+
op.banner = "Usage: mouth [options]"
|
18
|
+
|
19
|
+
op.separator ""
|
20
|
+
op.separator "Options:"
|
21
|
+
|
22
|
+
##
|
23
|
+
## Daemonization / Logging options
|
24
|
+
##
|
25
|
+
|
26
|
+
op.on("--pidfile PATH", "DO YOU WANT ME TO WRITE A PIDFILE SOMEWHERE FOR U?") do |pid_file|
|
27
|
+
options[:pid_file] = pid_file
|
28
|
+
end
|
29
|
+
|
30
|
+
op.on("--logfile PATH", "I'LL POOP OUT LOGS HERE FOR U") do |log_file|
|
31
|
+
options[:log_file] = log_file
|
32
|
+
end
|
33
|
+
|
34
|
+
op.on("-v", "--verbosity LEVEL", "HOW MUCH POOP DO U WANT IN UR LOGS? [LEVEL=0:errors,1:some,2:lots of poop]") do |verbosity|
|
35
|
+
options[:verbosity] = verbosity.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
op.on("-K", "--kill", "SHUT THE MOUTH") do
|
39
|
+
options[:kill] = true
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
## Socket to suck from
|
44
|
+
##
|
45
|
+
|
46
|
+
op.on("-H", "--host HOST", "I SUCK ON THIS NETWORK INTERFACE") do |host|
|
47
|
+
options[:host] = host
|
48
|
+
end
|
49
|
+
|
50
|
+
op.on("-P", "--port PORT", "I SUCK FROM THIS HOLE") do |port|
|
51
|
+
options[:port] = port.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
## Mongo
|
56
|
+
##
|
57
|
+
op.on("--mongodb DATABASE", "STORE SUCKINGS IN THIS DB") do |mongo_db|
|
58
|
+
options[:mongo_db] = mongo_db
|
59
|
+
end
|
60
|
+
|
61
|
+
# NOTE: this option can be given multiple times for a replica set
|
62
|
+
op.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO") do |mongo_host|
|
63
|
+
options[:mongo_hosts] ||= []
|
64
|
+
options[:mongo_hosts] << mongo_host
|
65
|
+
end
|
66
|
+
|
67
|
+
op.on("-h", "--help", "I WANT MOAR HALP") do
|
68
|
+
puts op
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
|
72
|
+
op.separator ""
|
73
|
+
end
|
74
|
+
|
75
|
+
parser.parse!
|
76
|
+
|
77
|
+
Mouth::Runner.new(options).run!
|
data/bin/mouth-console
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Running bin/mouth-console will put you in an IRB session with the mouth files loaded. Convenient for development and experimentation.
|
4
|
+
|
5
|
+
require 'irb'
|
6
|
+
require 'irb/completion'
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
9
|
+
|
10
|
+
require 'mouth'
|
11
|
+
require 'mouth/sequence'
|
12
|
+
require 'mouth/record'
|
13
|
+
require 'mouth/graph'
|
14
|
+
require 'mouth/dashboard'
|
15
|
+
require 'mouth/source'
|
16
|
+
require 'mouth/instrument'
|
17
|
+
|
18
|
+
IRB.start
|
data/bin/mouth-endoscope
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'vegas'
|
6
|
+
require 'mouth'
|
7
|
+
require 'mouth/endoscope'
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'thin'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
14
|
+
Vegas::Runner.new(Mouth::Endoscope, 'mouth-endoscope') do |runner, opts, app|
|
15
|
+
opts.on("--mongohost HOSTPORT", "STORE SUCKINGS IN THIS MONGO") do |mongo_host|
|
16
|
+
Mouth.mongo_host = mongo_host
|
17
|
+
end
|
18
|
+
end
|
data/lib/mouth.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Since the gem can be used in many ways (sucker daemon, sinatra app, instrument, ad-hoc, don't include anything)
|
2
|
+
require 'mouth/version'
|
3
|
+
|
4
|
+
module Mouth
|
5
|
+
class << self
|
6
|
+
attr_accessor :logger
|
7
|
+
attr_accessor :mongo_host
|
8
|
+
|
9
|
+
# Returns a mongo connection (NOT an em-mongo connection)
|
10
|
+
def mongo
|
11
|
+
@mongo ||= begin
|
12
|
+
require 'mongo' # require mongo here, as opposed to the top, because we don't want mongo included in the reactor (use em-mongo for that)
|
13
|
+
Mongo::Connection.new(self.mongo_host || "localhost").db("mouth")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def collection(collection_name)
|
18
|
+
@collections ||= {}
|
19
|
+
@collections[collection_name] ||= begin
|
20
|
+
c = mongo.collection(collection_name)
|
21
|
+
c.ensure_index([["t", 1]], {:background => true, :unique => true})
|
22
|
+
c
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def mongo_collection_name(namespace)
|
27
|
+
"mouth_#{namespace}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def sanitize_namespace(key)
|
31
|
+
key.gsub(/\s+/, '_').gsub(/\//, '-').gsub(/[^a-zA-Z_\-0-9]/, '')
|
32
|
+
end
|
33
|
+
|
34
|
+
def sanitize_metric(key)
|
35
|
+
key.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9\-_\/]/, '')
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parses a key into two parts: namespace, and metric. Also sanitizes each field
|
39
|
+
# Returns an array of values: [namespace, metric]
|
40
|
+
# eg,
|
41
|
+
# parse_key("Ticket.process_new_ticket") # => ["Ticket", "process_new_ticket"]
|
42
|
+
# parse_key("Forum List.other! stuff.ok") # => ["Forum_List", "other_stuff-ok"]
|
43
|
+
# parse_key("no_namespace") # => ["default", "no_namespace"]
|
44
|
+
def parse_key(key)
|
45
|
+
parts = key.split(".")
|
46
|
+
namespace = nil
|
47
|
+
metric = nil
|
48
|
+
if parts.length > 1
|
49
|
+
namespace = parts.shift
|
50
|
+
metric = parts.join("-")
|
51
|
+
else
|
52
|
+
namespace = "default"
|
53
|
+
metric = parts.shift
|
54
|
+
end
|
55
|
+
|
56
|
+
[sanitize_namespace(namespace), sanitize_metric(metric)]
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mouth
|
2
|
+
|
3
|
+
# A dashboard has these fields:
|
4
|
+
# id
|
5
|
+
# name
|
6
|
+
# todo: order
|
7
|
+
class Dashboard < Record
|
8
|
+
|
9
|
+
def all_attributes
|
10
|
+
self.attributes.tap {|attrs| attrs[:graphs] = graphs.collect(&:all_attributes) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy
|
14
|
+
self.graphs.each(&:destroy)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# An array of graphs
|
19
|
+
def graphs
|
20
|
+
@graphs ||= begin
|
21
|
+
Graph.for_dashboard(self.attributes[:id])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|