copperegg-revealmetrics 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -19
- data/lib/copperegg/revealmetrics.rb +17 -0
- data/lib/copperegg/revealmetrics/api.rb +28 -0
- data/lib/copperegg/revealmetrics/custom_dashboard.rb +142 -0
- data/lib/copperegg/revealmetrics/metric_group.rb +127 -0
- data/lib/copperegg/revealmetrics/metric_sample.rb +26 -0
- data/lib/copperegg/revealmetrics/mixins/persistence.rb +124 -0
- data/lib/copperegg/revealmetrics/tag.rb +97 -0
- data/lib/copperegg/revealmetrics/ver.rb +7 -0
- data/test/custom_dashboard_test.rb +28 -28
- data/test/metric_group_test.rb +11 -11
- data/test/metric_sample_test.rb +2 -2
- data/test/tag_test.rb +12 -12
- metadata +9 -9
- data/lib/copperegg.rb +0 -13
- data/lib/copperegg/api.rb +0 -24
- data/lib/copperegg/custom_dashboard.rb +0 -138
- data/lib/copperegg/metric_group.rb +0 -123
- data/lib/copperegg/metric_sample.rb +0 -22
- data/lib/copperegg/mixins/persistence.rb +0 -118
- data/lib/copperegg/tag.rb +0 -94
- data/lib/copperegg/ver.rb +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ab876f0e7a6bdf9ea7c22ebbca19da8e231a581
|
4
|
+
data.tar.gz: a726222785b11573ec23ac80f701b1535ad604d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f941a07e07e7f61b64ae58e177c597f935143e9cd121818d54d5736f9e6dd0c7f3467f77b496ed4e300d3e76eb58012880dfdf200b6e43e7734a9812fc997ea
|
7
|
+
data.tar.gz: 44b0d4aa25f277f5f343529f54743ccb123a1cbc2bd0cf1056c4a3dc9a59dae648e73f2ed878cb12de8b481055ffb725069e2f1415e270e5121ae4c5d9656a74
|
data/README.md
CHANGED
@@ -13,8 +13,8 @@ gem install copperegg-revealmetrics
|
|
13
13
|
|
14
14
|
Set up your API key:
|
15
15
|
``` ruby
|
16
|
-
require 'copperegg'
|
17
|
-
|
16
|
+
require 'copperegg/revealmetrics'
|
17
|
+
Copperegg::Revealmetrics::Api.apikey = "sdf87xxxxxxxxxxxxxxxxxxxxx" # from the web UI
|
18
18
|
```
|
19
19
|
|
20
20
|
## Metric Groups
|
@@ -22,19 +22,19 @@ CopperEgg::Api.apikey = "sdf87xxxxxxxxxxxxxxxxxxxxx" # from the web UI
|
|
22
22
|
#### Get a metric group:
|
23
23
|
|
24
24
|
``` ruby
|
25
|
-
metric_group =
|
25
|
+
metric_group = Copperegg::Revealmetrics::MetricGroup.find("my_metric_group")
|
26
26
|
metric_group.name
|
27
27
|
# => "my_metric_group"
|
28
28
|
metric_group.label
|
29
29
|
# => "My Metric Group"
|
30
30
|
metric_group.metrics
|
31
|
-
# => [#<
|
31
|
+
# => [#<Copperegg::Revealmetrics::MetricGroup::Metric:0x007fb43aab2570 @position=0, @type="ce_gauge", @name="metric1", @label="Metric 1", @unit="b">]
|
32
32
|
```
|
33
33
|
|
34
34
|
#### Create a metric group:
|
35
35
|
|
36
36
|
``` ruby
|
37
|
-
metric_group =
|
37
|
+
metric_group = Copperegg::Revealmetrics::MetricGroup.new(:name => "my_new_metric_group", :label => "Cool New Group Visible Name", :frequency => 60) # data is sent every 60 seconds
|
38
38
|
metric_group.metrics << {"type"=>"ce_gauge", "name"=>"active_connections", "unit"=>"Connections"}
|
39
39
|
metric_group.metrics << {"type"=>"ce_gauge", "name"=>"connections_accepts", "unit"=>"Connections"}
|
40
40
|
metric_group.metrics << {"type"=>"ce_gauge", "name"=>"connections_handled", "unit"=>"Connections"}
|
@@ -48,7 +48,7 @@ metric_group.save
|
|
48
48
|
If a metric group by the same name already exists, that one will rather than creating a new one. In addition, if the metric group was previously removed it will be restored.
|
49
49
|
|
50
50
|
```ruby
|
51
|
-
metric_group2 =
|
51
|
+
metric_group2 = Copperegg::Revealmetrics::MetricGroup.new(:name => "my_new_metric_group", :label => "New Group Version 2", :frequency => 60)
|
52
52
|
metric_group2.metrics << {"type"=>"ce_counter", "name"=>"active_connections", "unit"=>"Connections"}
|
53
53
|
metric_group2.save # this will perform an update to change the type of the metric 'active_connections' from 'ce_gauge' to 'ce_counter'
|
54
54
|
|
@@ -86,20 +86,20 @@ metric_group.delete
|
|
86
86
|
#### Post samples for a metric group
|
87
87
|
|
88
88
|
```ruby
|
89
|
-
|
89
|
+
Copperegg::Revealmetrics::MetricSample.save(metric_group.name, "custom_identifier1", Time.now.to_i, "active_connections" => 2601, "connections_accepts" => 154, "connections_handled" => 128, "connections_requested" => 1342, ...)
|
90
90
|
```
|
91
91
|
|
92
92
|
#### Get samples
|
93
93
|
|
94
94
|
```ruby
|
95
95
|
# Get the most recent samples for a single metric
|
96
|
-
|
96
|
+
Copperegg::Revealmetrics::MetricSample.samples(metric_group.name, "connections_accepts")
|
97
97
|
|
98
98
|
# Get the most recent samples for multiple metrics
|
99
|
-
|
99
|
+
Copperegg::Revealmetrics::MetricSample.samples(metric_group.name, ["connections_accepts", "connections_handled", "reading", "writing"])
|
100
100
|
|
101
101
|
# Specify a start time and duration
|
102
|
-
|
102
|
+
Copperegg::Revealmetrics::MetricSample.samples(metric_group.name, ["connections_accepts", "connections_handled", "reading", "writing"], :starttime => 4.hours.ago, :duration => 15.minutes)
|
103
103
|
```
|
104
104
|
|
105
105
|
The raw JSON response is returned as specified in the [API docs][sample_docs].
|
@@ -112,37 +112,37 @@ By default, the dashboard created will be named "_MetricGroupLabel_ Dashboard" a
|
|
112
112
|
|
113
113
|
```ruby
|
114
114
|
# Creates a dashboard named "My Metric Group Dashboard"
|
115
|
-
dashboard =
|
115
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.create(metric_group)
|
116
116
|
```
|
117
117
|
|
118
118
|
You can pass an option to specify the name of the dashboard.
|
119
119
|
|
120
120
|
```ruby
|
121
|
-
dashboard =
|
121
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.create(metric_group, :name => "Cloud Servers")
|
122
122
|
```
|
123
123
|
|
124
124
|
If a single identifier is specified, the dashboard will be created having one value widget per metric matching the single identifier.
|
125
125
|
|
126
126
|
```ruby
|
127
|
-
dashboard =
|
127
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.create(metric_group, :name => "Cloud Servers", :identifiers => "custom_identifier1")
|
128
128
|
```
|
129
129
|
|
130
130
|
If an array of identifiers is specified, the dashboard will be created having one timeline widget per metric matching each identifier.
|
131
131
|
|
132
132
|
```ruby
|
133
|
-
dashboard =
|
133
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.create(metric_group, :name => "Cloud Servers", :identifiers => ["custom_identifier1", "custom_identifier2"])
|
134
134
|
```
|
135
135
|
|
136
136
|
You can limit the widgets created by metric.
|
137
137
|
|
138
138
|
```ruby
|
139
|
-
dashboard =
|
139
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.create(metric_group, :name => "Cloud Servers", :identifiers => ["custom_identifier1", "custom_identifier2"], :metrics => ["reading", "writing", "waiting"])
|
140
140
|
```
|
141
141
|
|
142
142
|
#### Get a dashboard
|
143
143
|
|
144
144
|
```ruby
|
145
|
-
dashboard =
|
145
|
+
dashboard = Copperegg::Revealmetrics::CustomDashboard.find_by_name("My Metric Group Dashboard")
|
146
146
|
```
|
147
147
|
|
148
148
|
#### Delete a dashboard
|
@@ -158,14 +158,14 @@ dashboard.delete
|
|
158
158
|
#### Get all or specific tags
|
159
159
|
|
160
160
|
```ruby
|
161
|
-
tags_list =
|
162
|
-
tag =
|
161
|
+
tags_list = Copperegg::Revealmetrics::Tag.find
|
162
|
+
tag = Copperegg::Revealmetrics::Tag.find_by_name("my-tag")
|
163
163
|
```
|
164
164
|
|
165
165
|
#### Create a tag
|
166
166
|
|
167
167
|
```ruby
|
168
|
-
tag =
|
168
|
+
tag = Copperegg::Revealmetrics::Tag.new({:name => "my-tag"})
|
169
169
|
tag.objects = ["object-identifier-1", "object-identifier-2"]
|
170
170
|
tag.save
|
171
171
|
```
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
require "uri"
|
4
|
+
require "json/pure"
|
5
|
+
|
6
|
+
require "copperegg/revealmetrics/mixins/persistence"
|
7
|
+
require "copperegg/revealmetrics/metric_group"
|
8
|
+
require "copperegg/revealmetrics/custom_dashboard"
|
9
|
+
require "copperegg/revealmetrics/metric_sample"
|
10
|
+
require "copperegg/revealmetrics/tag"
|
11
|
+
require "copperegg/revealmetrics/api"
|
12
|
+
|
13
|
+
module Copperegg
|
14
|
+
module Revealmetrics
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Copperegg
|
2
|
+
module Revealmetrics
|
3
|
+
|
4
|
+
class Api
|
5
|
+
class << self
|
6
|
+
attr_accessor :apikey
|
7
|
+
attr_reader :ssl_verify_peer, :timeout
|
8
|
+
|
9
|
+
def host=(host)
|
10
|
+
@uri = URI.join(host, "/v2/revealmetrics/").to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def uri
|
14
|
+
@uri || "https://api.copperegg.com/v2/revealmetrics/"
|
15
|
+
end
|
16
|
+
|
17
|
+
def ssl_verify_peer=(boolean)
|
18
|
+
@ssl_verify_peer = boolean ? true : false
|
19
|
+
end
|
20
|
+
|
21
|
+
def timeout=(seconds)
|
22
|
+
@timeout = seconds.to_i
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Copperegg
|
2
|
+
module Revealmetrics
|
3
|
+
|
4
|
+
class CustomDashboard
|
5
|
+
include Copperegg::Revealmetrics::Mixins::Persistence
|
6
|
+
|
7
|
+
WIDGET_TYPES = %w(metric metric_list timeline)
|
8
|
+
WIDGET_STYLES = %w(value timeline both list values)
|
9
|
+
WIDGET_MATCHES = %w(select multi tag all)
|
10
|
+
|
11
|
+
resource "dashboards"
|
12
|
+
|
13
|
+
attr_accessor :name, :label, :data
|
14
|
+
|
15
|
+
def load_attributes(attributes)
|
16
|
+
@data = {"widgets" => {}, "order" => []}
|
17
|
+
attributes.each do |name, value|
|
18
|
+
if name.to_s == "id"
|
19
|
+
@id = value
|
20
|
+
elsif name.to_s == "data"
|
21
|
+
attributes[name].each do |data_name, data_value|
|
22
|
+
if data_name.to_s == "order"
|
23
|
+
data["order"] = data_value
|
24
|
+
else
|
25
|
+
data["widgets"] = data_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
elsif !respond_to?("#{name}=")
|
29
|
+
next
|
30
|
+
else
|
31
|
+
send "#{name}=", value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid?
|
37
|
+
@error = nil
|
38
|
+
if self.name.nil? || self.name.to_s.strip.empty?
|
39
|
+
@error = "Name can't be blank."
|
40
|
+
else
|
41
|
+
self.data["widgets"].values.each do |widget|
|
42
|
+
widget.each do |key, value|
|
43
|
+
if key.to_s == "type" && !WIDGET_TYPES.include?(value)
|
44
|
+
@error = "Invalid widget type #{value}."
|
45
|
+
elsif key.to_s == "style" && !WIDGET_STYLES.include?(value)
|
46
|
+
@error = "Invalid widget style #{value}."
|
47
|
+
elsif key.to_s == "match" && !WIDGET_MATCHES.include?(value)
|
48
|
+
@error = "Invalid widget match #{value}."
|
49
|
+
elsif key.to_s == "metric" && (!value.is_a?(Hash) || value.keys.size == 0)
|
50
|
+
@error = "Invalid widget metric. #{value}"
|
51
|
+
else
|
52
|
+
(widget["metric"] || widget[:metric]).each do |metric_group_name, metric_group_value|
|
53
|
+
if !metric_group_value.is_a?(Array)
|
54
|
+
@error = "Invalid widget metric. #{metric_group_value}"
|
55
|
+
elsif metric_group_value.length == 0
|
56
|
+
@error = "Invalid widget metric. #{metric_group_value}"
|
57
|
+
else
|
58
|
+
metric_group_value.each do |metric_data|
|
59
|
+
if !metric_data.is_a?(Array)
|
60
|
+
@error = "Invalid widget metric. #{metric_group_value}"
|
61
|
+
elsif metric_data.length < 2
|
62
|
+
@error = "Invalid widget metric. #{metric_group_value}"
|
63
|
+
elsif (/^\d+$/ =~ metric_data.first.to_s).nil?
|
64
|
+
@error = "Invalid widget metric. #{metric_group_value}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
match_param = widget["match_param"] || widget[:match_param]
|
72
|
+
if (widget["match"] || widget[:match]) != "all" && (match_param.nil? || match_param.to_s.strip.empty?)
|
73
|
+
@error = "Missing match parameter."
|
74
|
+
end
|
75
|
+
break if !@error.nil?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
@error.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_hash
|
82
|
+
set_data_order
|
83
|
+
self.instance_variables.reduce({}) do |memo, variable|
|
84
|
+
unless variable.to_s == "@error"
|
85
|
+
value = instance_variable_get(variable)
|
86
|
+
memo[variable.to_s.sub("@", "")] = value
|
87
|
+
end
|
88
|
+
memo
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class <<self
|
93
|
+
def create(*args)
|
94
|
+
options = args.last.class == Hash ? args.pop : {}
|
95
|
+
|
96
|
+
return super(args.first) if args.first.is_a?(Hash)
|
97
|
+
|
98
|
+
metric_group = args.first
|
99
|
+
raise ArgumentError.new("Copperegg::Revealmetrics::MetricGroup object expected") if !metric_group.is_a?(MetricGroup)
|
100
|
+
raise ArgumentError.new("Invalid metric group") if !metric_group.valid?
|
101
|
+
|
102
|
+
metrics = filter_metrics(metric_group, options[:metrics]).map { |name| metric_group.metrics.find { |metric| metric.name == name } }
|
103
|
+
identifiers = options[:identifiers].is_a?(Array) ? (options[:identifiers].empty? ? nil : options[:identifiers]) : (options[:identifier] ? [options[:identifiers]] : nil)
|
104
|
+
widget_match = identifiers.nil? ? "all" : (identifiers.size == 1 ? "select" : "multi")
|
105
|
+
widget_type = widget_match == "select" ? "metric" : "timeline"
|
106
|
+
widget_style = widget_type == "metric" ? "both" : "values"
|
107
|
+
name = options[:name] || "#{metric_group.label} Dashboard"
|
108
|
+
|
109
|
+
dashboard = new(:name => name)
|
110
|
+
metrics.each.with_index do |metric, i|
|
111
|
+
metric_data = [metric.position, metric.name]
|
112
|
+
metric_data.push("rate") if metric.type == "ce_counter" || metric.type == "ce_counter_f"
|
113
|
+
widget = {:type => widget_type, :style => widget_style, :match => widget_match, :metric => {metric_group.name => [metric_data]}}
|
114
|
+
widget[:match_param] = identifiers if identifiers
|
115
|
+
dashboard.data["widgets"][i.to_s] = widget
|
116
|
+
end
|
117
|
+
dashboard.save
|
118
|
+
dashboard
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_by_name(name)
|
122
|
+
find.detect { |dashboard| dashboard.name == name }
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def filter_metrics(metric_group, specified_metrics)
|
128
|
+
metrics = metric_group.metrics.map(&:name)
|
129
|
+
specified_metrics = specified_metrics.is_a?(Array) ? specified_metrics & metrics : (specified_metrics ? [specified_metrics] & metrics : [])
|
130
|
+
specified_metrics.empty? ? metrics : specified_metrics
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def set_data_order
|
137
|
+
@data["order"] = @data["widgets"].keys if @data["order"].empty?
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Copperegg
|
2
|
+
module Revealmetrics
|
3
|
+
|
4
|
+
class MetricGroup
|
5
|
+
include Copperegg::Revealmetrics::Mixins::Persistence
|
6
|
+
|
7
|
+
resource "metric_groups"
|
8
|
+
|
9
|
+
attr_accessor :name, :label, :frequency, :metrics
|
10
|
+
|
11
|
+
def load_attributes(attributes)
|
12
|
+
@metrics = []
|
13
|
+
attributes.each do |name, value|
|
14
|
+
if name.to_s == "id"
|
15
|
+
@id = value
|
16
|
+
elsif !respond_to?("#{name}=")
|
17
|
+
next
|
18
|
+
elsif value.to_s == "metrics"
|
19
|
+
@metrics = value.map { |v| Metric.new(v) }
|
20
|
+
else
|
21
|
+
send "#{name}=", value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
self.instance_variables.reduce({}) do |memo, variable|
|
28
|
+
value = instance_variable_get(variable)
|
29
|
+
if variable.to_s == "@metrics"
|
30
|
+
memo[variable.to_s.sub("@", "")] = value.map(&:to_hash)
|
31
|
+
elsif variable.to_s != "@error"
|
32
|
+
memo[variable.to_s.sub("@", "")] = value
|
33
|
+
end
|
34
|
+
memo
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def valid?
|
39
|
+
@error = nil
|
40
|
+
if self.name.nil? || self.name.to_s.strip.empty?
|
41
|
+
@error = "Name can't be blank."
|
42
|
+
elsif self.metrics.nil? || self.metrics.empty?
|
43
|
+
@error = "You must define at least one metric."
|
44
|
+
else
|
45
|
+
self.metrics = self.metrics.map { |metric| metric.is_a?(Hash) ? Metric.new(metric) : metric }
|
46
|
+
self.metrics.each do |metric|
|
47
|
+
if !metric.is_a?(Metric)
|
48
|
+
@error = "Metric expected."
|
49
|
+
break
|
50
|
+
elsif !metric.valid?
|
51
|
+
@error = metric.error
|
52
|
+
break
|
53
|
+
else
|
54
|
+
metric.send(:remove_instance_variable, :@position) if (metric.instance_variables.include?(:@position) || metric.instance_variables.include?("@position")) && !self.persisted?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@error.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def create
|
64
|
+
response = self.class.request(:request_type => "get", :id => self.name, :show_hidden => true)
|
65
|
+
if response.code == "200"
|
66
|
+
json = JSON.parse(response.body)
|
67
|
+
metric_group = self.class.new(json)
|
68
|
+
@id = self.name
|
69
|
+
# needs_update = self.label != metric_group.label || self.frequency != metric_group.frequency || self.metrics.length != metric_group.metrics.length || self.metrics.map(&:name).sort != metric_group.metrics.map {|m| m["name"]}.sort
|
70
|
+
if true #needs_update
|
71
|
+
self.class.request(self.to_hash.merge(:id => @id, :is_hidden => 0, :request_type => "put", :show_hidden => true))
|
72
|
+
else
|
73
|
+
response
|
74
|
+
end
|
75
|
+
else
|
76
|
+
self.class.request(self.to_hash.merge(:request_type => "post"))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Metric
|
81
|
+
TYPES = %w(ce_gauge ce_gauge_f ce_counter ce_counter_f)
|
82
|
+
|
83
|
+
attr_accessor :name, :label, :type, :unit
|
84
|
+
attr_reader :error, :position
|
85
|
+
|
86
|
+
def initialize(attributes={})
|
87
|
+
attributes.each do |name, value|
|
88
|
+
if name.to_s == "position"
|
89
|
+
@position = value
|
90
|
+
elsif !respond_to?("#{name}=")
|
91
|
+
next
|
92
|
+
else
|
93
|
+
send "#{name}=", value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_hash
|
99
|
+
self.instance_variables.reduce({}) do |memo, variable|
|
100
|
+
if variable.to_s != "@error"
|
101
|
+
value = instance_variable_get(variable)
|
102
|
+
memo[variable.to_s.sub("@", "")] = value
|
103
|
+
end
|
104
|
+
memo
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid?
|
109
|
+
valid = false
|
110
|
+
@error = nil
|
111
|
+
if self.name.nil? || self.name.to_s.strip.empty?
|
112
|
+
@error = "Metric name cannot be blank."
|
113
|
+
elsif self.type.nil? || self.type.to_s.strip.empty?
|
114
|
+
@error = "Metric type must be defined."
|
115
|
+
elsif !TYPES.include?(self.type)
|
116
|
+
@error = "Invalid metric type #{self.type}."
|
117
|
+
else
|
118
|
+
valid = true
|
119
|
+
remove_instance_variable(:@error)
|
120
|
+
end
|
121
|
+
valid
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|