matt 1.0.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.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/README.md +115 -0
- data/bin/matt +4 -0
- data/lib/matt.rb +39 -0
- data/lib/matt/command.rb +211 -0
- data/lib/matt/configuration.rb +69 -0
- data/lib/matt/datasource.rb +14 -0
- data/lib/matt/datasource/sql.rb +33 -0
- data/lib/matt/exporter.rb +10 -0
- data/lib/matt/exporter/sql.rb +90 -0
- data/lib/matt/measure.rb +25 -0
- data/lib/matt/support.rb +1 -0
- data/lib/matt/support/puts.rb +26 -0
- data/lib/matt/version.rb +8 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 712f1cd0be5efa12e62fe0491c0c14028b76a5e4edcdaf792bf8ab9652397218
|
4
|
+
data.tar.gz: e133df58cfba2501cdd806668ff6e21363150a32a9563a6fa554938dc2bfed5c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ae322c7f52f9bab450bb21781b5653bd87a6e40703fadfe2768cc56305508464945f411c15584b3702cfefc969baf2af0fe4e437b6b454355b934b7602d39920
|
7
|
+
data.tar.gz: d4ef94dc91c994bb4886a4b9c4beb0c863af6d62bc2c66c440fb0a594788640bca2c536bbe1f86a8f3e22ebb93bc357ef09e0e29c604256aa4f064b167ba33ef
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# Matt - A monitoring tool for data-driven business decisions
|
2
|
+
|
3
|
+
Matt is a CTO-oriented tool that helps capturing daily data points that are
|
4
|
+
relevant for conducting growth hacking and other data-driven decisions.
|
5
|
+
|
6
|
+
It consists of the following abstractions:
|
7
|
+
|
8
|
+
* Datasource: provides access to data from which measures are extracted
|
9
|
+
* Measure: standardized data vector for relevant decisions
|
10
|
+
* Exporter: a way to save measures for future analysis and visualization
|
11
|
+
|
12
|
+
## How to install Matt?
|
13
|
+
|
14
|
+
There are various usage scenario to use matt, according to your needs and
|
15
|
+
types of environment and datasources.
|
16
|
+
|
17
|
+
### As a commandline
|
18
|
+
|
19
|
+
To simply use Matt as a commandline and connect to local databases, simply
|
20
|
+
install and use the ruby gem:
|
21
|
+
|
22
|
+
```
|
23
|
+
gem install matt
|
24
|
+
matt --help
|
25
|
+
```
|
26
|
+
|
27
|
+
### As a docker image
|
28
|
+
|
29
|
+
If you don't have ruby installed and want to use docker instead, we provide a
|
30
|
+
docker image that has Matt as entrypoint.
|
31
|
+
|
32
|
+
```
|
33
|
+
docker run enspirit/matt --help
|
34
|
+
```
|
35
|
+
|
36
|
+
Please check docker's network documentation if you want Matt to connect to
|
37
|
+
databases running on your host.
|
38
|
+
|
39
|
+
### In a Gemfile
|
40
|
+
|
41
|
+
It may be useful to use Matt as part of a larger ruby gemset, for instance
|
42
|
+
because you need various ruby gems to connect to mysql, postgres, or whatever.
|
43
|
+
|
44
|
+
Then in your Gemfile:
|
45
|
+
|
46
|
+
```
|
47
|
+
source https://rubygems.org
|
48
|
+
|
49
|
+
gem "matt"
|
50
|
+
```
|
51
|
+
|
52
|
+
Then,
|
53
|
+
|
54
|
+
```
|
55
|
+
bundle exec matt --help
|
56
|
+
```
|
57
|
+
|
58
|
+
### As a docker base image + a Gemfile
|
59
|
+
|
60
|
+
If Matt is part of a larger docker/kubernetes infrastructure, you probably need
|
61
|
+
the last scenario while also having your own docker image for the component.
|
62
|
+
|
63
|
+
This is a good basis for a Dockerfile:
|
64
|
+
|
65
|
+
```
|
66
|
+
FROM enspirit/matt # or from ruby-3.0:alpine
|
67
|
+
|
68
|
+
COPY Gemfile .
|
69
|
+
RUN bundle install
|
70
|
+
|
71
|
+
ENTRYPOINT ['bundle', 'exec', 'matt']
|
72
|
+
```
|
73
|
+
|
74
|
+
Let's say you build that image as `my/matt`, you can now simply run matt as
|
75
|
+
follows:
|
76
|
+
|
77
|
+
```
|
78
|
+
docker run my/matt --help
|
79
|
+
```
|
80
|
+
|
81
|
+
In such a scenario, you may want to use Matt's version that exists in the
|
82
|
+
docker container instead without having to specify it in the Gemfile.
|
83
|
+
The following Gemfile hack works:
|
84
|
+
|
85
|
+
```
|
86
|
+
gem "matt", path: "/matt"
|
87
|
+
```
|
88
|
+
|
89
|
+
## Semantics versioning
|
90
|
+
|
91
|
+
Matt uses semantics versioning since 1.0.
|
92
|
+
|
93
|
+
The public API consists in:
|
94
|
+
|
95
|
+
* Matt public methods
|
96
|
+
* Matt::Configuration public methods
|
97
|
+
* Matt::Datasource public & protected methods exposed to including classes
|
98
|
+
* Matt::Exporter (same)
|
99
|
+
* Matt::Measure (same)
|
100
|
+
* `matt` commandline and its options
|
101
|
+
* `enspirit/matt` docker image with `/matt` and `/home/matt` behavior
|
102
|
+
|
103
|
+
## Contribute
|
104
|
+
|
105
|
+
Please use github issues and pull requests for all questions, bug reports,
|
106
|
+
and contributions. Don't hesitate to get in touch with us with an early code
|
107
|
+
spike if you plan to add non trivial features.
|
108
|
+
|
109
|
+
## Licence
|
110
|
+
|
111
|
+
This software is distributed by Enspirit SRL under a MIT Licence. Please
|
112
|
+
contact Bernard Lambeau (blambeau@gmail.com) with any question.
|
113
|
+
|
114
|
+
Enspirit (https://enspirit.be) and Klaro App (https://klaro.cards) are both
|
115
|
+
actively using and contributing to the library.
|
data/bin/matt
ADDED
data/lib/matt.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'json'
|
4
|
+
require 'sequel'
|
5
|
+
require 'bmg'
|
6
|
+
require 'path'
|
7
|
+
require 'bmg/sequel'
|
8
|
+
module Matt
|
9
|
+
|
10
|
+
def env(which, default = nil)
|
11
|
+
val = ENV.has_key?(which) ? ENV[which] : default
|
12
|
+
val = val.strip if val.is_a?(String)
|
13
|
+
val
|
14
|
+
end
|
15
|
+
module_function :env
|
16
|
+
|
17
|
+
def alltime_predicate
|
18
|
+
Predicate.tautology
|
19
|
+
end
|
20
|
+
module_function :alltime_predicate
|
21
|
+
|
22
|
+
def yesterday_predicate
|
23
|
+
Predicate.gte(:at, Date.today - 1) & Predicate.lt(:at, Date.today)
|
24
|
+
end
|
25
|
+
module_function :yesterday_predicate
|
26
|
+
|
27
|
+
def today_predicate
|
28
|
+
Predicate.gte(:at, Date.today) & Predicate.lt(:at, Date.today + 1)
|
29
|
+
end
|
30
|
+
module_function :today_predicate
|
31
|
+
|
32
|
+
end # module Matt
|
33
|
+
require_relative 'matt/version'
|
34
|
+
require_relative 'matt/support'
|
35
|
+
require_relative 'matt/datasource'
|
36
|
+
require_relative 'matt/measure'
|
37
|
+
require_relative 'matt/exporter'
|
38
|
+
require_relative 'matt/configuration'
|
39
|
+
require_relative 'matt/command'
|
data/lib/matt/command.rb
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
#/
|
2
|
+
#/ Matt - A monitoring tool for data-driven business decisions
|
3
|
+
#/ vVERSION - (c) Enspirit SRL, 2021 and beyond
|
4
|
+
#/
|
5
|
+
#/ Usage: matt [-f folder] command [options...] [args...]
|
6
|
+
#/
|
7
|
+
#/ show measure Show the datapoints of a given measure
|
8
|
+
#/ export measure* Export all (or specific) measure(s) to
|
9
|
+
#/ ping datasource* Ping all (or specific) datasource(s)
|
10
|
+
#/ their default exporters
|
11
|
+
#/
|
12
|
+
#/ -f FOLDER Use the folder as entry point (`pwd` by default)
|
13
|
+
#/ --all-time Do not restrict measures shown/exported
|
14
|
+
#/ --today Only show/export measures for today
|
15
|
+
#/ --yesterday Only show/export measures for yesterday's (default)
|
16
|
+
#/ --to=exporter,... Override the default exporters
|
17
|
+
#/ --json Use json when displaying measures on console
|
18
|
+
#/ --csv Use csv when displaying measures on console (default)
|
19
|
+
#/ -h, --help Show this help message
|
20
|
+
#/ --version Show matt version
|
21
|
+
#/
|
22
|
+
#/ Examples:
|
23
|
+
#/ matt show --all-time --json account_creations
|
24
|
+
#/ matt export --today --to=sql,prometheus account_creations
|
25
|
+
#/
|
26
|
+
require 'optparse'
|
27
|
+
module Matt
|
28
|
+
class Command
|
29
|
+
include Support::Puts
|
30
|
+
|
31
|
+
attr_accessor :exitcode
|
32
|
+
attr_accessor :output_format
|
33
|
+
attr_accessor :to
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@output_format = :csv
|
37
|
+
@to = nil
|
38
|
+
yield(self) if block_given?
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_configuration(&bl)
|
42
|
+
@on_configuration = bl
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def configuration
|
47
|
+
return @configuration if @configuration
|
48
|
+
self.configuration = Configuration.new(Path.pwd)
|
49
|
+
end
|
50
|
+
|
51
|
+
def configuration=(c)
|
52
|
+
@on_configuration.call(c) if @on_configuration
|
53
|
+
@configuration = c
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_configuration?
|
57
|
+
!!@configuration
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_argv(argv)
|
61
|
+
opt_parser.parse!(argv)
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(argv)
|
65
|
+
ok = false
|
66
|
+
catch :abort do
|
67
|
+
parse_argv(argv)
|
68
|
+
_call(argv)
|
69
|
+
ok = true
|
70
|
+
end
|
71
|
+
self.exitcode = ok ? 0 : 1
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def _call(argv)
|
77
|
+
if argv.empty?
|
78
|
+
puts_out opt_parser
|
79
|
+
abort
|
80
|
+
end
|
81
|
+
|
82
|
+
meth = :"do_#{argv.first}"
|
83
|
+
if self.respond_to?(meth, true)
|
84
|
+
send(meth, argv[1..-1])
|
85
|
+
else
|
86
|
+
puts_err "No such command #{argv.first}"
|
87
|
+
abort
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def do_show(argv)
|
92
|
+
argv_count!(argv, 1)
|
93
|
+
m = measure_exists!(argv.first)
|
94
|
+
data = m.full_data.restrict(configuration.at_predicate)
|
95
|
+
case of = output_format
|
96
|
+
when :json
|
97
|
+
puts_out JSON.pretty_generate(data)
|
98
|
+
when :csv
|
99
|
+
puts_out data.to_csv(configuration.csv_options)
|
100
|
+
else
|
101
|
+
puts_err "Unknown format #{of}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def do_ping(argv)
|
106
|
+
which_ones = if argv.empty?
|
107
|
+
configuration.datasources.to_h.values
|
108
|
+
else
|
109
|
+
argv.map{|arg| datasource_exists!(arg) }
|
110
|
+
end
|
111
|
+
which_ones.each do |d|
|
112
|
+
d.ping
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def do_export(argv)
|
117
|
+
argv_count!(argv, 1)
|
118
|
+
m = measure_exists!(argv.first)
|
119
|
+
data = m.full_data.restrict(configuration.at_predicate)
|
120
|
+
(@to || m.exporters).each do |e|
|
121
|
+
exporter = exporter_exists!(e)
|
122
|
+
exporter.export(m, data)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def do_help(*args)
|
127
|
+
file = __FILE__
|
128
|
+
exec "grep ^#/<'#{file}'|cut -c4-|sed s/VERSION/#{Matt::VERSION}/g"
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
def opt_parser
|
134
|
+
OptionParser.new do |opts|
|
135
|
+
opts.banner = "Usage: matt [options] COMMAND [args]"
|
136
|
+
opts.on("-f FOLDER") do |folder|
|
137
|
+
p = Path(folder)
|
138
|
+
if has_configuration?
|
139
|
+
puts_err "-f must be used before other configuration options"
|
140
|
+
abort
|
141
|
+
elsif p.exists? && p.directory?
|
142
|
+
self.configuration = Configuration.new(p)
|
143
|
+
else
|
144
|
+
puts_err "No such folder: #{folder}"
|
145
|
+
abort
|
146
|
+
end
|
147
|
+
end
|
148
|
+
opts.on("--all-time") do
|
149
|
+
self.configuration.at_predicate = Matt.alltime_predicate
|
150
|
+
end
|
151
|
+
opts.on("--yesterday") do
|
152
|
+
self.configuration.at_predicate = Matt.yesterday_predicate
|
153
|
+
end
|
154
|
+
opts.on("--today") do
|
155
|
+
self.configuration.at_predicate = Matt.today_predicate
|
156
|
+
end
|
157
|
+
opts.on("--to=EXPORTERS") do |exporters|
|
158
|
+
@to = (@to || []) + exporters.split(/\s*,\s*/).map{|e|
|
159
|
+
exporter_exists!(e.to_sym).name
|
160
|
+
}
|
161
|
+
end
|
162
|
+
opts.on("--json") do
|
163
|
+
self.output_format = :json
|
164
|
+
end
|
165
|
+
opts.on("--csv") do
|
166
|
+
self.output_format = :csv
|
167
|
+
end
|
168
|
+
opts.on('--version', "Show version number") do
|
169
|
+
puts_out "Matt v#{VERSION} - (c) Enspirit SRL"
|
170
|
+
abort
|
171
|
+
end
|
172
|
+
opts.on("-h", "--help", "Prints this help") do
|
173
|
+
do_help
|
174
|
+
abort
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def argv_count!(argv, n)
|
180
|
+
return if argv.size == n
|
181
|
+
puts_err "#{n} arguments expected, got #{argv.size}"
|
182
|
+
abort
|
183
|
+
end
|
184
|
+
|
185
|
+
def measure_exists!(name)
|
186
|
+
m = configuration.measures.send(name.to_sym)
|
187
|
+
return m if m
|
188
|
+
puts_err "No such measure #{name}"
|
189
|
+
abort
|
190
|
+
end
|
191
|
+
|
192
|
+
def exporter_exists!(name)
|
193
|
+
m = configuration.exporters.send(name.to_sym)
|
194
|
+
return m if m
|
195
|
+
puts_err "No such exporter #{name}"
|
196
|
+
abort
|
197
|
+
end
|
198
|
+
|
199
|
+
def datasource_exists!(name)
|
200
|
+
d = configuration.datasources.send(name.to_sym)
|
201
|
+
return d if d
|
202
|
+
puts_err "No such datasource #{name}"
|
203
|
+
abort
|
204
|
+
end
|
205
|
+
|
206
|
+
def abort
|
207
|
+
throw :abort
|
208
|
+
end
|
209
|
+
|
210
|
+
end # class Command
|
211
|
+
end # module Matt
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Matt
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
def initialize(folder)
|
5
|
+
@folder = folder
|
6
|
+
@stdout = $stdout
|
7
|
+
@stderr = $stderr
|
8
|
+
@csv_options = {
|
9
|
+
:write_headers => true
|
10
|
+
}
|
11
|
+
@at_predicate = Matt.yesterday_predicate
|
12
|
+
yield(self) if block_given?
|
13
|
+
end
|
14
|
+
attr_accessor :folder
|
15
|
+
attr_accessor :stdout, :stderr
|
16
|
+
attr_accessor :csv_options
|
17
|
+
attr_accessor :at_predicate
|
18
|
+
|
19
|
+
def datasources_folder
|
20
|
+
folder/"datasources"
|
21
|
+
end
|
22
|
+
|
23
|
+
def datasources
|
24
|
+
@datasources ||= load_dynamic_objects(datasources_folder, Matt::Datasource)
|
25
|
+
end
|
26
|
+
alias :ds :datasources
|
27
|
+
|
28
|
+
def measures_folder
|
29
|
+
folder/"measures"
|
30
|
+
end
|
31
|
+
|
32
|
+
def measures
|
33
|
+
@measures ||= load_dynamic_objects(measures_folder, Matt::Measure)
|
34
|
+
end
|
35
|
+
alias :ms :measures
|
36
|
+
|
37
|
+
def exporters_folder
|
38
|
+
folder/"exporters"
|
39
|
+
end
|
40
|
+
|
41
|
+
def exporters
|
42
|
+
@exporters ||= load_dynamic_objects(exporters_folder, Matt::Exporter)
|
43
|
+
end
|
44
|
+
alias :es :exporters
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def load_dynamic_objects(folder, expected_type)
|
49
|
+
struct = OpenStruct.new
|
50
|
+
folder.glob("*.rb").each_with_object(struct) do |file,memo|
|
51
|
+
obj = load_dynamic_object(file)
|
52
|
+
if obj.nil?
|
53
|
+
raise "Wrong file #{file}: eval returned nil"
|
54
|
+
elsif !obj.is_a?(expected_type)
|
55
|
+
raise "Wrong file #{file}: must include #{expected_type}"
|
56
|
+
else
|
57
|
+
obj.configuration = self
|
58
|
+
obj.name = file.basename.rm_ext.to_s
|
59
|
+
memo.send("#{obj.name}=", obj)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def load_dynamic_object(file)
|
65
|
+
self.instance_eval(file.read, file.to_s)
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class Configuration
|
69
|
+
end # module Matt
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Matt
|
2
|
+
module Datasource
|
3
|
+
include Support::Puts
|
4
|
+
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :configuration
|
7
|
+
|
8
|
+
def ping
|
9
|
+
raise NotImplementedError, "#{self} should implement `ping`"
|
10
|
+
end
|
11
|
+
|
12
|
+
end # module Datasource
|
13
|
+
end # module Matt
|
14
|
+
require_relative 'datasource/sql'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Matt
|
2
|
+
module Datasource
|
3
|
+
class Sql
|
4
|
+
include Matt::Datasource
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
@sequel_db = ::Sequel.connect(config)
|
9
|
+
end
|
10
|
+
attr_reader :config, :sequel_db
|
11
|
+
|
12
|
+
def ping
|
13
|
+
sequel_db.test_connection
|
14
|
+
puts "#{self} -- Ok."
|
15
|
+
rescue => ex
|
16
|
+
puts_err "#{self} -- Ko: #{ex.message}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
if config.is_a?(Hash)
|
21
|
+
"#{config[:host]}:#{config[:port]}/#{config[:database]}"
|
22
|
+
else
|
23
|
+
config.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def sequel(*args)
|
28
|
+
Bmg.sequel(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Matt
|
2
|
+
module Exporter
|
3
|
+
class Sql
|
4
|
+
include Matt::Exporter
|
5
|
+
|
6
|
+
def initialize(arg)
|
7
|
+
case @config = arg
|
8
|
+
when String
|
9
|
+
when Hash
|
10
|
+
when Sequel::Database
|
11
|
+
when Matt::Datasource::Sql
|
12
|
+
else
|
13
|
+
raise ArgumentError, "Unable to use `#{arg}` to create an Sql exporter"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
attr_reader :config
|
17
|
+
|
18
|
+
def sequel_db
|
19
|
+
@sequel_db ||= case config
|
20
|
+
when Sequel::Database then config
|
21
|
+
when Matt::Datasource::Sql then config.sequel_db
|
22
|
+
else Sequel.connect(config)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def export(measure, data)
|
27
|
+
now = Time.now
|
28
|
+
data = data.constants(:created_at => now)
|
29
|
+
table = ensure_table!(measure)
|
30
|
+
remove_old_data!(table, data)
|
31
|
+
insert_new_data!(table, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def remove_old_data!(table, data)
|
37
|
+
dates = data.project([:at]).map{|t| t[:at] }
|
38
|
+
table.where(:at => dates).delete
|
39
|
+
end
|
40
|
+
|
41
|
+
def insert_new_data!(table, data)
|
42
|
+
table.multi_insert(data.to_a)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ensure_table!(measure)
|
46
|
+
tb_name = measure.name.to_sym
|
47
|
+
fields = [:at] + measure.dimensions.keys + measure.metrics.keys
|
48
|
+
if sequel_db.table_exists?(tb_name)
|
49
|
+
align_table!(tb_name, measure, fields)
|
50
|
+
else
|
51
|
+
create_table!(tb_name, measure, fields)
|
52
|
+
end
|
53
|
+
sequel_db[tb_name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def align_table!(tb_name, measure, fields)
|
57
|
+
schema = sequel_db.schema(tb_name)
|
58
|
+
existing = schema.map{|s| s.first } - [:created_at]
|
59
|
+
missing = fields - existing
|
60
|
+
missing = measure.dimensions.merge(measure.metrics).select{|x|
|
61
|
+
missing.include?(x)
|
62
|
+
}
|
63
|
+
outdated = existing - fields
|
64
|
+
return if missing.empty? && outdated.empty?
|
65
|
+
sequel_db.alter_table(tb_name) do
|
66
|
+
outdated.each do |out|
|
67
|
+
drop_column(out)
|
68
|
+
end
|
69
|
+
missing.each_pair do |mis, type|
|
70
|
+
add_column(mis, type, null: true)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_table!(tb_name, measure, fields)
|
76
|
+
sequel_db.create_table(tb_name) do
|
77
|
+
column(:at, Date, :null => false)
|
78
|
+
column(:created_at, :timestamp, :null => false)
|
79
|
+
measure.dimensions.each do |name,type|
|
80
|
+
column(name, type, :null => true)
|
81
|
+
end
|
82
|
+
measure.metrics.each do |name,type|
|
83
|
+
column(name, type, :null => true)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end # class Sql
|
89
|
+
end # module Exporter
|
90
|
+
end # module Matt
|
data/lib/matt/measure.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Matt
|
2
|
+
module Measure
|
3
|
+
include Support::Puts
|
4
|
+
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :configuration
|
7
|
+
|
8
|
+
def dimensions
|
9
|
+
{}
|
10
|
+
end
|
11
|
+
|
12
|
+
def exporters
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
|
16
|
+
def metrics
|
17
|
+
raise NotImplementedError, "#{self} must implement `metrics`"
|
18
|
+
end
|
19
|
+
|
20
|
+
def ds
|
21
|
+
configuration.datasources
|
22
|
+
end
|
23
|
+
|
24
|
+
end # module Measure
|
25
|
+
end # module Matt
|
data/lib/matt/support.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'support/puts'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Matt
|
2
|
+
module Support
|
3
|
+
module Puts
|
4
|
+
|
5
|
+
def stdout
|
6
|
+
configuration.stdout
|
7
|
+
end
|
8
|
+
|
9
|
+
def stderr
|
10
|
+
configuration.stderr
|
11
|
+
end
|
12
|
+
|
13
|
+
def puts_out(*args, &bl)
|
14
|
+
return $stdout.puts(*args, &bl) unless configuration
|
15
|
+
configuration.stdout.puts(*args, &bl)
|
16
|
+
end
|
17
|
+
alias :puts :puts_out
|
18
|
+
|
19
|
+
def puts_err(*args, &bl)
|
20
|
+
return $stderr.puts(*args, &bl) unless configuration
|
21
|
+
configuration.stderr.puts(*args, &bl)
|
22
|
+
end
|
23
|
+
|
24
|
+
end # module Puts
|
25
|
+
end # module Support
|
26
|
+
end # module Matt
|
data/lib/matt/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: matt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bernard Lambeau
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: path
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bmg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.18.5
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0.19'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 0.18.5
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0.19'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: sequel
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '5.0'
|
60
|
+
- - "<"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '6.19'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '5.0'
|
70
|
+
- - "<"
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '6.19'
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: rake
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '13.0'
|
80
|
+
- - "<"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '14.0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13.0'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '14.0'
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rspec
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '3.10'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '4.0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.10'
|
110
|
+
- - "<"
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '4.0'
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: sqlite3
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 1.4.2
|
120
|
+
- - "<"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '2.0'
|
123
|
+
type: :development
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: 1.4.2
|
130
|
+
- - "<"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '2.0'
|
133
|
+
description: Matt help monitoring data points for better business decision.
|
134
|
+
email: blambeau@gmail.com
|
135
|
+
executables:
|
136
|
+
- matt
|
137
|
+
extensions: []
|
138
|
+
extra_rdoc_files: []
|
139
|
+
files:
|
140
|
+
- Gemfile
|
141
|
+
- README.md
|
142
|
+
- bin/matt
|
143
|
+
- lib/matt.rb
|
144
|
+
- lib/matt/command.rb
|
145
|
+
- lib/matt/configuration.rb
|
146
|
+
- lib/matt/datasource.rb
|
147
|
+
- lib/matt/datasource/sql.rb
|
148
|
+
- lib/matt/exporter.rb
|
149
|
+
- lib/matt/exporter/sql.rb
|
150
|
+
- lib/matt/measure.rb
|
151
|
+
- lib/matt/support.rb
|
152
|
+
- lib/matt/support/puts.rb
|
153
|
+
- lib/matt/version.rb
|
154
|
+
homepage: http://github.com/enspirit/matt
|
155
|
+
licenses:
|
156
|
+
- MIT
|
157
|
+
metadata: {}
|
158
|
+
post_install_message:
|
159
|
+
rdoc_options: []
|
160
|
+
require_paths:
|
161
|
+
- lib
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
requirements: []
|
173
|
+
rubygems_version: 3.2.15
|
174
|
+
signing_key:
|
175
|
+
specification_version: 4
|
176
|
+
summary: Matt help monitoring data points for better business decision.
|
177
|
+
test_files: []
|