influxer 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -2
- data/.travis.yml +3 -2
- data/Changelog.md +19 -0
- data/Gemfile +0 -13
- data/MIT-LICENSE +1 -1
- data/README.md +51 -6
- data/gemfiles/rails42.gemfile +7 -0
- data/influxer.gemspec +7 -5
- data/lib/influxer/client.rb +46 -0
- data/lib/influxer/config.rb +10 -0
- data/lib/influxer/metrics/fanout.rb +53 -0
- data/lib/influxer/metrics/metrics.rb +33 -5
- data/lib/influxer/metrics/relation/fanout_query.rb +33 -0
- data/lib/influxer/metrics/relation/time_query.rb +73 -0
- data/lib/influxer/metrics/relation.rb +177 -39
- data/lib/influxer/metrics/scoping/default.rb +28 -0
- data/lib/influxer/metrics/scoping/named.rb +18 -0
- data/lib/influxer/metrics/scoping.rb +56 -0
- data/lib/influxer/model.rb +3 -1
- data/lib/influxer/version.rb +1 -1
- data/spec/client_spec.rb +34 -0
- data/spec/dummy/app/metrics/testo_metrics.rb +1 -1
- data/spec/dummy/app/models/testo.rb +2 -0
- data/spec/dummy/config/application.rb +1 -0
- data/spec/dummy/config/environments/test.rb +0 -1
- data/spec/metrics/fanout_spec.rb +46 -0
- data/spec/metrics/metrics_spec.rb +42 -7
- data/spec/metrics/relation_spec.rb +199 -51
- data/spec/metrics/scoping_spec.rb +68 -0
- data/spec/model/testo_spec.rb +12 -3
- data/spec/spec_helper.rb +1 -1
- metadata +78 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bb445897c491a860d94249f174bc02e9ad6b61f
|
4
|
+
data.tar.gz: 011778f550be687a6d1503618f98ec70dd454c2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85e6922159fc728186d5b3a0736ff1c9950f1af0a1d70f8452e91f6e1759e2508a1e1566d9f55482ffefe62a01b39d2843aea81aec8e5382217e015291b26dd5
|
7
|
+
data.tar.gz: 232e5a9108e36d1378134a1e26fbc9d25367a569cae4a3f7ab752c2c48f3da5ea47e395019032fd2896cc6ed90421c6e8b07283b94690453e2d8ec120b0fe144
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
language: ruby
|
2
|
+
cache: bundler
|
2
3
|
rvm:
|
3
4
|
- 2.0.0
|
4
5
|
- 2.1
|
@@ -14,8 +15,8 @@ matrix:
|
|
14
15
|
- rvm: 2.1
|
15
16
|
gemfile: gemfiles/rails40.gemfile
|
16
17
|
|
17
|
-
- rvm: 2.
|
18
|
+
- rvm: 2.1
|
18
19
|
gemfile: gemfiles/rails41.gemfile
|
19
20
|
|
20
21
|
- rvm: 2.1
|
21
|
-
gemfile: gemfiles/
|
22
|
+
gemfile: gemfiles/rails42.gemfile
|
data/Changelog.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
## 0.1.0
|
2
|
+
- Add logs
|
3
|
+
- Add `foreign_key` param to `has_metrics` options
|
4
|
+
|
5
|
+
## 0.1.0-rc
|
6
|
+
- Fix `Relation#to_a` (now returns array of points correctrly)
|
7
|
+
- Fix fanout queries with array args (now use `merge(Regexp)`)
|
8
|
+
|
9
|
+
## 0.1.0-alpha
|
10
|
+
- Add `time` method to Relation to group by time with constants (`:hour`, `:day`, etc) and fill support
|
11
|
+
- Series names now properly quoted with double-quotes
|
12
|
+
- Using regexps, ranges and arrays within `where` clause
|
13
|
+
- `where.not(...)` support
|
14
|
+
- Add `past` and `since` methods
|
15
|
+
- Add `merge` method and support for regexp series
|
16
|
+
- Add `delete_all` support
|
17
|
+
- Add cache support (using `Rails.cache`)
|
18
|
+
- Scopes (default and named)
|
19
|
+
- Support for fanout series
|
data/Gemfile
CHANGED
@@ -1,16 +1,3 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
|
-
|
3
|
-
# Declare your gem's dependencies in influxer.gemspec.
|
4
|
-
# Bundler will treat runtime dependencies like base dependencies, and
|
5
|
-
# development dependencies will be added by default to the :development group.
|
6
|
-
gem 'sqlite3'
|
7
|
-
gem 'pry-byebug'
|
8
2
|
gemspec
|
9
3
|
|
10
|
-
# Declare any dependencies that are still in development here instead of in
|
11
|
-
# your gemspec. These might include edge Rails or gems from your path or
|
12
|
-
# Git. Remember to move these dependencies to your gemspec before releasing
|
13
|
-
# your gem to rubygems.org.
|
14
|
-
|
15
|
-
# To use debugger
|
16
|
-
# gem 'debugger'
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -2,14 +2,59 @@
|
|
2
2
|
|
3
3
|
## Influxer
|
4
4
|
|
5
|
-
|
5
|
+
ActiveRecord-like wrapper for [influxdb-ruby](https://github.com/influxdb/influxdb-ruby) with many useful features, such as:
|
6
|
+
- Familar query language (use `select`, `where`, `not`, `group` etc).
|
7
|
+
- Support for Regex conditions: `where(page_id: /^home\/.*/) #=> select * ... where page_id=~/^home\/.*/`.
|
8
|
+
- Special query methods for InfluxDB:
|
9
|
+
- `time` - group by time (e.g. `Metrics.time(:hour) => # select * ... group by time(1h)`);
|
10
|
+
- `past` - get only points for last hour/minute/whatever (e.g. `Metrics.past(:day) => # select * ... where time > now() - 1d`);
|
11
|
+
- `since` - get only points since date (e.g. `Metrics.since(Time.utc(2014,12,31)) => # select * ... where time > 1419984000s`);
|
12
|
+
- `merge` - merge series.
|
13
|
+
- Scopes support
|
14
|
+
```
|
15
|
+
class Metrics < Influxer::Metrics
|
16
|
+
default_scope -> { time(:hour).limit(1000) }
|
17
|
+
scope :unlimited, -> { limit(nil) }
|
18
|
+
scope :by_account, -> (id) { where(account_id: id) if id.present? }
|
19
|
+
end
|
6
20
|
|
7
|
-
|
21
|
+
Metrics.by_account(1)
|
22
|
+
# => select * from "metrics" group by time(1h) where account_id=1 limit 1000
|
8
23
|
|
9
|
-
|
24
|
+
Metrics.unlimited.by_account(1).time(:week)
|
25
|
+
# => select * from "metrics" group by time(1w) where account_id=1
|
10
26
|
|
11
|
-
|
27
|
+
```
|
28
|
+
- Support for handling fanout series as one metrics.
|
29
|
+
```
|
30
|
+
class Metrics < Influxer::Metrics
|
31
|
+
fanout :account, :user, :page
|
32
|
+
end
|
12
33
|
|
13
|
-
|
34
|
+
Metrics.where(account: 1)
|
35
|
+
# => select * from "metrics_account_1"
|
14
36
|
|
15
|
-
|
37
|
+
|
38
|
+
Metrics.where(page: 'home').where(user: 12)
|
39
|
+
# => select * from "metrics_user_12_page_home"
|
40
|
+
|
41
|
+
Metrics.where(page: /(home|faq)/).where(account: 1).where(user: 12)
|
42
|
+
# => select * from /^metrics_account_1_user_12_page_(home|faq)$/
|
43
|
+
|
44
|
+
```
|
45
|
+
- Integrate with your model:
|
46
|
+
```
|
47
|
+
class UserVisits < Influxer::Metrics
|
48
|
+
end
|
49
|
+
|
50
|
+
class User < ActiveRecord::Base
|
51
|
+
has_metrics :visits
|
52
|
+
end
|
53
|
+
|
54
|
+
user = User.find(1)
|
55
|
+
user.visits.write(page_id: 'home')
|
56
|
+
#=> < creates point {user_id: 1, page_id: 'home'} in 'user_visits' series >
|
57
|
+
|
58
|
+
user.visits.where(page_id: 'home')
|
59
|
+
#=> select * from user_visits where page_id='home'
|
60
|
+
```
|
data/influxer.gemspec
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
$:.push File.expand_path("../lib", __FILE__)
|
2
2
|
|
3
|
-
# Maintain your gem's version:
|
4
3
|
require "influxer/version"
|
5
4
|
|
6
|
-
# Describe your gem and declare its dependencies:
|
7
5
|
Gem::Specification.new do |s|
|
8
6
|
s.name = "influxer"
|
9
7
|
s.version = Influxer::VERSION
|
10
8
|
s.authors = ["Vlad Dem"]
|
11
9
|
s.email = ["dementiev.vm@gmail.com"]
|
12
10
|
s.homepage = "http://github.com/palkan/influxer"
|
13
|
-
s.summary = "InfluxDB
|
11
|
+
s.summary = "InfluxDB for Rails"
|
14
12
|
s.description = "InfluxDB the Rails way"
|
15
13
|
s.license = "MIT"
|
16
14
|
|
@@ -22,6 +20,10 @@ Gem::Specification.new do |s|
|
|
22
20
|
|
23
21
|
s.add_development_dependency "timecop"
|
24
22
|
s.add_development_dependency "simplecov", ">= 0.3.8"
|
25
|
-
|
26
|
-
s.add_development_dependency
|
23
|
+
|
24
|
+
s.add_development_dependency 'sqlite3'
|
25
|
+
s.add_development_dependency 'pry'
|
26
|
+
s.add_development_dependency 'pry-byebug'
|
27
|
+
s.add_development_dependency "rspec", "~> 3.1.0"
|
28
|
+
s.add_development_dependency "rspec-rails", "~> 3.1.0"
|
27
29
|
end
|
data/lib/influxer/client.rb
CHANGED
@@ -3,7 +3,53 @@ require 'influxdb'
|
|
3
3
|
module Influxer
|
4
4
|
class Client < ::InfluxDB::Client
|
5
5
|
def initialize
|
6
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
6
7
|
super Influxer.config.database, Influxer.config.as_json.symbolize_keys!
|
7
8
|
end
|
9
|
+
|
10
|
+
def cached_query(sql)
|
11
|
+
log(sql) do
|
12
|
+
unless Influxer.config.cache == false
|
13
|
+
Rails.cache.fetch(normalized_cache_key(sql), cache_options(sql)) { self.query(sql) }
|
14
|
+
else
|
15
|
+
self.query(sql)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def log(sql)
|
23
|
+
return yield unless logger.debug?
|
24
|
+
|
25
|
+
_start = Time.now
|
26
|
+
res = yield
|
27
|
+
_duration = (Time.now - _start)*1000
|
28
|
+
|
29
|
+
name = "InfluxDB SQL (#{_duration.round(1)}ms)"
|
30
|
+
|
31
|
+
# bold black name and blue query string
|
32
|
+
msg = "\e[1m\e[30m#{name}\e[0m \e[34m#{sql}\e[0m"
|
33
|
+
logger.debug msg
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache_options(sql=nil)
|
38
|
+
options = Influxer.config.cache.dup
|
39
|
+
# if sql contains 'now()' set expires to 1 minute or :cache_now_for value of config.cache if defined
|
40
|
+
if sql =~ /\snow\(\)/
|
41
|
+
options[:expires_in] = options[:cache_now_for] || 60
|
42
|
+
end
|
43
|
+
options
|
44
|
+
end
|
45
|
+
|
46
|
+
# add prefix; remove whitespaces
|
47
|
+
def normalized_cache_key(sql)
|
48
|
+
"influxer:#{sql.gsub(/\s*/, '')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger
|
52
|
+
Rails.logger
|
53
|
+
end
|
8
54
|
end
|
9
55
|
end
|
data/lib/influxer/config.rb
CHANGED
@@ -7,6 +7,7 @@ module Influxer
|
|
7
7
|
:password,
|
8
8
|
:use_ssl,
|
9
9
|
:async,
|
10
|
+
:cache,
|
10
11
|
:retry,
|
11
12
|
:time_precision,
|
12
13
|
:initial_delay,
|
@@ -20,6 +21,7 @@ module Influxer
|
|
20
21
|
@port = 8083
|
21
22
|
@use_ssl = false
|
22
23
|
@async = true
|
24
|
+
@cache = false
|
23
25
|
@retry = false
|
24
26
|
@time_precision = 's'
|
25
27
|
@max_delay = 30
|
@@ -42,6 +44,14 @@ module Influxer
|
|
42
44
|
config.each do |key, val|
|
43
45
|
self.send("#{key}=",val)
|
44
46
|
end
|
47
|
+
|
48
|
+
# we want pass @cache value as options to cache store, so we want it to be a Hash
|
49
|
+
if @cache == true
|
50
|
+
@cache = {}
|
51
|
+
elsif @cache != false
|
52
|
+
# cache keys should be symbols to work as Rails.cache options
|
53
|
+
@cache.symbolize_keys!
|
54
|
+
end
|
45
55
|
end
|
46
56
|
end
|
47
57
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Influxer
|
2
|
+
module Fanout
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :fanouts, :fanouts_by_name, :fanout_options
|
7
|
+
self.fanouts = []
|
8
|
+
self.fanouts_by_name = {} # to use within `fanout?`
|
9
|
+
self.fanout_options = {delimeter: "_"}
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Define fanouts for metrics as array of keys.
|
14
|
+
# Order of keys is important.
|
15
|
+
# Fanout delimeter can be set with 'delimiter' option (defaults to '_').
|
16
|
+
#
|
17
|
+
# class MyMetrics < Influxer::Metrics
|
18
|
+
# set_series "my_points"
|
19
|
+
# fanout :account_id, :user_id
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyMetrics.where(user_id: 1).where(account_id: 10)
|
23
|
+
# # select * from my_points_account_id_10_user_id_1
|
24
|
+
#
|
25
|
+
# class MyMetrics < Influxer::Metrics
|
26
|
+
# set_series "my_points"
|
27
|
+
# fanout :account_id, :user_id, delimiter: "."
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# MyMetrics.where(user_id: 1).where(account_id: 10).where("req_time > 1000")
|
31
|
+
# # select * from my_points.account_id.10.user_id.1 where req_time > 1000
|
32
|
+
|
33
|
+
def fanout(*args, **hargs)
|
34
|
+
self.fanout_options = self.fanout_options.merge hargs
|
35
|
+
|
36
|
+
names = args.map(&:to_s) # convert all to strings (because args can be both Symbols and Strings)
|
37
|
+
|
38
|
+
self.fanouts = (self.fanouts+names).uniq
|
39
|
+
|
40
|
+
names_hash = {}
|
41
|
+
names.each do |name|
|
42
|
+
names_hash[name] = 1
|
43
|
+
end
|
44
|
+
|
45
|
+
self.fanouts_by_name = self.fanouts_by_name.merge names_hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def fanout?(key)
|
49
|
+
self.fanouts_by_name.key?(key.to_s)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'influxer/metrics/scoping'
|
2
|
+
require 'influxer/metrics/fanout'
|
3
|
+
|
1
4
|
module Influxer
|
2
5
|
class MetricsError < StandardError; end
|
3
6
|
class MetricsInvalid < MetricsError; end
|
@@ -7,10 +10,13 @@ module Influxer
|
|
7
10
|
include ActiveModel::Validations
|
8
11
|
extend ActiveModel::Callbacks
|
9
12
|
|
13
|
+
include Influxer::Scoping
|
14
|
+
include Influxer::Fanout
|
15
|
+
|
10
16
|
define_model_callbacks :write
|
11
17
|
|
12
18
|
class << self
|
13
|
-
delegate :select, :where, :group, :limit, :delete_all, to: :all
|
19
|
+
delegate :select, :where, :group, :merge, :time, :past, :since, :limit, :fill, :delete_all, to: :all
|
14
20
|
|
15
21
|
def attributes(*attrs)
|
16
22
|
attrs.each do |name|
|
@@ -32,14 +38,14 @@ module Influxer
|
|
32
38
|
if args.empty?
|
33
39
|
matches = self.to_s.match(/^(.*)Metrics$/)
|
34
40
|
if matches.nil?
|
35
|
-
@series = self.to_s.underscore
|
41
|
+
@series = self.superclass.respond_to?(:series) ? self.superclass.series : self.to_s.underscore
|
36
42
|
else
|
37
43
|
@series = matches[1].split("::").join("_").underscore
|
38
44
|
end
|
39
45
|
elsif args.first.is_a?(Proc)
|
40
46
|
@series = args.first
|
41
47
|
else
|
42
|
-
@series = args
|
48
|
+
@series = args
|
43
49
|
end
|
44
50
|
end
|
45
51
|
|
@@ -48,7 +54,11 @@ module Influxer
|
|
48
54
|
end
|
49
55
|
|
50
56
|
def all
|
51
|
-
|
57
|
+
if current_scope
|
58
|
+
current_scope.clone
|
59
|
+
else
|
60
|
+
default_scoped
|
61
|
+
end
|
52
62
|
end
|
53
63
|
end
|
54
64
|
|
@@ -83,13 +93,31 @@ module Influxer
|
|
83
93
|
end
|
84
94
|
|
85
95
|
def series
|
86
|
-
|
96
|
+
quote_series(self.class.series)
|
87
97
|
end
|
88
98
|
|
89
99
|
def client
|
90
100
|
Influxer.client
|
91
101
|
end
|
92
102
|
|
103
|
+
def quote_series(val)
|
104
|
+
case val
|
105
|
+
when Regexp
|
106
|
+
val.inspect
|
107
|
+
when Proc
|
108
|
+
quote_series(self.class.series.call(self))
|
109
|
+
when Array
|
110
|
+
if val.length > 1
|
111
|
+
"merge(#{ val.map{ |s| quote_series(s) }.join(',') })"
|
112
|
+
else
|
113
|
+
quote_series(val.first)
|
114
|
+
end
|
115
|
+
else
|
116
|
+
'"'+val.to_s.gsub(/\"/){ %q{\"} }+'"'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
93
120
|
attributes :time
|
121
|
+
|
94
122
|
end
|
95
123
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Influxer
|
2
|
+
module FanoutQuery
|
3
|
+
# Instance methods are included to Relation
|
4
|
+
def build_fanout(key, val)
|
5
|
+
@values[:has_fanout] = true
|
6
|
+
if val.is_a?(Regexp)
|
7
|
+
@values[:fanout_rxp] = true
|
8
|
+
fanout_values[key.to_s] = val.inspect[1..-2]
|
9
|
+
else
|
10
|
+
fanout_values[key.to_s] = val.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_series_name
|
15
|
+
if @values[:has_fanout] == true
|
16
|
+
fan_parts = [@instance.series[1..-2]]
|
17
|
+
@klass.fanouts.each do |name|
|
18
|
+
if fanout_values.key?(name)
|
19
|
+
fan_parts << name
|
20
|
+
fan_parts << fanout_values[name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
if @values[:fanout_rxp] == true
|
24
|
+
"merge(/^#{ fan_parts.join( @klass.fanout_options[:delimeter] ) }$/)"
|
25
|
+
else
|
26
|
+
@instance.quote_series(fan_parts.join(@klass.fanout_options[:delimeter]))
|
27
|
+
end
|
28
|
+
else
|
29
|
+
@instance.series
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Influxer
|
2
|
+
module TimeQuery
|
3
|
+
TIME_ALIASES = {
|
4
|
+
hour: '1h',
|
5
|
+
minute: '1m',
|
6
|
+
second: '1s',
|
7
|
+
ms: '1u',
|
8
|
+
week: '1w',
|
9
|
+
day: '1d',
|
10
|
+
month: '30d'
|
11
|
+
}
|
12
|
+
|
13
|
+
# Add group value to relation. To be used instead of `group("time(...)").
|
14
|
+
# Accepts symbols and strings.
|
15
|
+
#
|
16
|
+
# You can set fill value within options.
|
17
|
+
#
|
18
|
+
# Metrics.time(:hour)
|
19
|
+
# # select * from metrics group by time(1h)
|
20
|
+
#
|
21
|
+
# Metrics.time("4d", fill: 0)
|
22
|
+
# # select * from metrics group by time(4d) fill(0)
|
23
|
+
|
24
|
+
def time(val, options={})
|
25
|
+
if val.is_a?(Symbol)
|
26
|
+
@values[:time] = TIME_ALIASES[val] || '1'+val.to_s
|
27
|
+
else
|
28
|
+
@values[:time] = val
|
29
|
+
end
|
30
|
+
|
31
|
+
unless options[:fill].nil?
|
32
|
+
fill( (options[:fill] == :null) ? 'null' : options[:fill].to_i)
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Shortcut to define time interval with regard to current time.
|
38
|
+
# Accepts symbols and numbers.
|
39
|
+
#
|
40
|
+
# Metrics.past(:hour)
|
41
|
+
# # select * from metrics where time > now() - 1h
|
42
|
+
#
|
43
|
+
# Metrics.past(:d)
|
44
|
+
# # select * from metrics where time > now() - 1d
|
45
|
+
#
|
46
|
+
# Metrics.past(2.days)
|
47
|
+
# # select * from metrics where time > now() - 172800s
|
48
|
+
|
49
|
+
def past(val)
|
50
|
+
case val
|
51
|
+
when Symbol
|
52
|
+
where("time > now() - #{ TIME_ALIASES[val] || ('1'+val.to_s) }")
|
53
|
+
when String
|
54
|
+
where("time > now() - #{val}")
|
55
|
+
else
|
56
|
+
where("time > now() - #{val.to_i}s")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Shortcut to define start point of the time interval.
|
61
|
+
# Accepts DateTime objects.
|
62
|
+
#
|
63
|
+
# Metrics.since(1.day.ago) # assume that now is 2014-12-31 12:00:00 UTC
|
64
|
+
# # select * from metrics where time > 1420027200s
|
65
|
+
#
|
66
|
+
# Metrics.since(Time.local(2014,12,31))
|
67
|
+
# # select * from metrics where time > 1419984000s
|
68
|
+
|
69
|
+
def since(val)
|
70
|
+
where("time > #{val.to_i}s")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|