elasticsearch-watcher 0.0.1
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/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +166 -0
- data/Rakefile +99 -0
- data/elasticsearch-watcher.gemspec +40 -0
- data/lib/elasticsearch-watcher.rb +1 -0
- data/lib/elasticsearch/watcher.rb +37 -0
- data/lib/elasticsearch/watcher/api/actions/ack_watch.rb +27 -0
- data/lib/elasticsearch/watcher/api/actions/delete_watch.rb +29 -0
- data/lib/elasticsearch/watcher/api/actions/execute_watch.rb +27 -0
- data/lib/elasticsearch/watcher/api/actions/get_watch.rb +31 -0
- data/lib/elasticsearch/watcher/api/actions/info.rb +23 -0
- data/lib/elasticsearch/watcher/api/actions/put_watch.rb +29 -0
- data/lib/elasticsearch/watcher/api/actions/restart.rb +24 -0
- data/lib/elasticsearch/watcher/api/actions/start.rb +23 -0
- data/lib/elasticsearch/watcher/api/actions/stats.rb +23 -0
- data/lib/elasticsearch/watcher/api/actions/stop.rb +23 -0
- data/lib/elasticsearch/watcher/version.rb +5 -0
- data/test/test_helper.rb +87 -0
- data/test/unit/ack_watch_test.rb +26 -0
- data/test/unit/delete_watch_test.rb +26 -0
- data/test/unit/execute_watch_test.rb +26 -0
- data/test/unit/get_watch_test.rb +26 -0
- data/test/unit/info_test.rb +26 -0
- data/test/unit/put_watch_test.rb +26 -0
- data/test/unit/restart_test.rb +26 -0
- data/test/unit/start_test.rb +26 -0
- data/test/unit/stats_test.rb +26 -0
- data/test/unit/stop_test.rb +26 -0
- metadata +310 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 997c4ba2c5e12dbd058aa6ce597770019f7f6fbd
|
4
|
+
data.tar.gz: 4b9088c48307f47f57b56426abb7030f7b8c2cfa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5852020cbae6eb416ebdf4ed1ca99d0d4271d3dcd6324772a182575bb22fe8a6d388d96a706b12d6c2ca5b1aee0dfc3a507cb993b3b37428079050e34f749ea3
|
7
|
+
data.tar.gz: 2d1327265ba961cda13400847caa802b37bdaf78029994db88fe6c1cd61b402d6b9b97a20273fc91e2bb1b5de90e66f66d98864fd54d165ae53453e3fb7d9bce
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2015 Elasticsearch
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# Elasticsearch::Watcher
|
2
|
+
|
3
|
+
This library provides Ruby API for the [_Watcher_](https://www.elastic.co/products/watcher) plugin.
|
4
|
+
|
5
|
+
Please refer to the [_Watcher_ documentation](http://www.elastic.co/guide/en/watcher/current/index.html)
|
6
|
+
for information about the plugin.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install the package from [Rubygems](https://rubygems.org):
|
11
|
+
|
12
|
+
gem install elasticsearch-watcher
|
13
|
+
|
14
|
+
To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://gembundler.com):
|
15
|
+
|
16
|
+
gem 'elasticsearch-watcher', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git'
|
17
|
+
|
18
|
+
or install it from a source code checkout:
|
19
|
+
|
20
|
+
git clone https://github.com/elasticsearch/elasticsearch-ruby.git
|
21
|
+
cd elasticsearch-ruby/elasticsearch-watcher
|
22
|
+
bundle install
|
23
|
+
rake install
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
The documentation for the Ruby API methods is available at <http://www.rubydoc.info/gems/elasticsearch-watcher>.
|
28
|
+
|
29
|
+
A comprehensive example of registering a watch, triggering the actions, and getting information
|
30
|
+
about the watch execution is quoted below.
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'elasticsearch'
|
34
|
+
require 'elasticsearch/watcher'
|
35
|
+
|
36
|
+
client = Elasticsearch::Client.new url: 'http://localhost:9200', log: true
|
37
|
+
client.transport.logger.formatter = proc do |severity, datetime, progname, msg| "\e[2m#{msg}\e[0m\n" end
|
38
|
+
|
39
|
+
# Delete the Watcher and test indices
|
40
|
+
#
|
41
|
+
client.indices.delete index: ['alerts', 'test', '.watches', '.watch_history*'], ignore: 404
|
42
|
+
|
43
|
+
# Print information about the Watcher plugin
|
44
|
+
#
|
45
|
+
puts "Watcher #{client.watcher.info['version']['number']}"
|
46
|
+
|
47
|
+
# Register a new watch
|
48
|
+
#
|
49
|
+
client.watcher.put_watch id: 'error_500', body: {
|
50
|
+
# Label the watch
|
51
|
+
#
|
52
|
+
metadata: { tags: ['errors'] },
|
53
|
+
|
54
|
+
# Run the watch every 10 seconds
|
55
|
+
#
|
56
|
+
trigger: { schedule: { interval: '10s' } },
|
57
|
+
|
58
|
+
# Search for at least 3 documents matching the condition
|
59
|
+
#
|
60
|
+
condition: { script: { inline: 'ctx.payload.hits.total > 3' } },
|
61
|
+
|
62
|
+
# Throttle the watch execution for 30 seconds
|
63
|
+
#
|
64
|
+
throttle_period: '30s',
|
65
|
+
|
66
|
+
# The search request to execute
|
67
|
+
#
|
68
|
+
input: { search: {
|
69
|
+
request: {
|
70
|
+
indices: ['test'],
|
71
|
+
body: {
|
72
|
+
query: {
|
73
|
+
filtered: {
|
74
|
+
query: { match: { status: 500 } },
|
75
|
+
filter: { range: { timestamp: { from: '{{ctx.trigger.scheduled_time}}||-5m', to: '{{ctx.trigger.triggered_time}}' } } }
|
76
|
+
}
|
77
|
+
},
|
78
|
+
# Return statistics about different hosts
|
79
|
+
#
|
80
|
+
aggregations: {
|
81
|
+
hosts: { terms: { field: 'host' } }
|
82
|
+
}
|
83
|
+
}}}},
|
84
|
+
|
85
|
+
# The actions to perform
|
86
|
+
#
|
87
|
+
actions: {
|
88
|
+
send_email: {
|
89
|
+
transform: {
|
90
|
+
# Transform the data for the template
|
91
|
+
#
|
92
|
+
script: 'return [ total: ctx.payload.hits.total, hosts: ctx.payload.aggregations.hosts.buckets.collect { [ host: it.key, errors: it.doc_count ] }, errors: ctx.payload.hits.hits.collect { it._source } ];'
|
93
|
+
},
|
94
|
+
email: { to: 'alerts@example.com',
|
95
|
+
subject: '[ALERT] {{ctx.watch_id}}',
|
96
|
+
body: "Received {{ctx.payload.total}} error documents in the last 5 minutes.\n\nHosts:\n\n{{#ctx.payload.hosts}}* {{host}} ({{errors}})\n{{/ctx.payload.hosts}}",
|
97
|
+
attach_data: true }
|
98
|
+
},
|
99
|
+
index_payload: {
|
100
|
+
# Transform the data to be stored
|
101
|
+
#
|
102
|
+
transform: { script: 'return [ watch_id: ctx.watch_id, payload: ctx.payload ]' },
|
103
|
+
index: { index: 'alerts', doc_type: 'alert' }
|
104
|
+
},
|
105
|
+
ping_webhook: {
|
106
|
+
webhook: {
|
107
|
+
method: 'POST',
|
108
|
+
host: 'localhost',
|
109
|
+
port: 4567,
|
110
|
+
path: '/',
|
111
|
+
body: %q|{"watch_id" : "{{ctx.watch_id}}", "payload" : "{{ctx.payload}}"}| }
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
# Index documents to trigger the watch
|
117
|
+
#
|
118
|
+
5.times do
|
119
|
+
client.index index: 'test', type: 'd',
|
120
|
+
body: { timestamp: Time.now.utc.iso8601, status: 500, host: "10.0.0.#{rand(1..3)}" }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Wait a bit...
|
124
|
+
#
|
125
|
+
print "Waiting 30 seconds..."
|
126
|
+
$i=0; while $i < 30 do
|
127
|
+
sleep(1); print('.'); $i+=1
|
128
|
+
end; puts "\n"
|
129
|
+
|
130
|
+
# Display information about watch execution
|
131
|
+
#
|
132
|
+
puts '='*80, ""
|
133
|
+
client.search(index: '.watch_history*', q: 'watch_id:error_500', sort: 'timestamp:asc')['hits']['hits'].each do |r|
|
134
|
+
puts "#{r['_id']} : #{r['_source']['state']}"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Delete the watch
|
138
|
+
#
|
139
|
+
puts "Deleting the watch..."
|
140
|
+
client.watcher.delete_watch id: 'error_500', master_timeout: '30s', force: true
|
141
|
+
```
|
142
|
+
|
143
|
+
You can run a simple [Sinatra](https://github.com/sinatra/sinatra/) server
|
144
|
+
to test the `webhook` action with the following Ruby code:
|
145
|
+
|
146
|
+
```bash
|
147
|
+
ruby -r sinatra -r json -e 'post("/") { json = JSON.parse(request.body.read); puts %Q~Received #{json["watch_id"]} with payload: #{json["payload"]}~ }'
|
148
|
+
```
|
149
|
+
|
150
|
+
## License
|
151
|
+
|
152
|
+
This software is licensed under the Apache 2 license, quoted below.
|
153
|
+
|
154
|
+
Copyright (c) 2015 Elasticsearch <http://www.elasticsearch.org>
|
155
|
+
|
156
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
157
|
+
you may not use this file except in compliance with the License.
|
158
|
+
You may obtain a copy of the License at
|
159
|
+
|
160
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
161
|
+
|
162
|
+
Unless required by applicable law or agreed to in writing, software
|
163
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
164
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
165
|
+
See the License for the specific language governing permissions and
|
166
|
+
limitations under the License.
|
data/Rakefile
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
__current__ = Pathname( File.expand_path('..', __FILE__) )
|
5
|
+
|
6
|
+
task(:default) { system "rake --tasks" }
|
7
|
+
task :test => 'test:unit'
|
8
|
+
|
9
|
+
# ----- Test tasks ------------------------------------------------------------
|
10
|
+
|
11
|
+
require 'rake/testtask'
|
12
|
+
namespace :test do
|
13
|
+
Rake::TestTask.new(:unit) do |test|
|
14
|
+
test.libs << 'lib' << 'test'
|
15
|
+
test.test_files = FileList["test/unit/**/*_test.rb"]
|
16
|
+
# test.verbose = true
|
17
|
+
# test.warning = true
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Run integration tests"
|
21
|
+
task :integration do
|
22
|
+
unless ENV['TEST_REST_API_SPEC']
|
23
|
+
puts "[!] Please export the TEST_REST_API_SPEC variable with a path to the Watcher YAML tests"
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Define the task
|
28
|
+
t = Rake::TestTask.new(:integration) do |test|
|
29
|
+
test.libs << 'lib' << 'test'
|
30
|
+
test.test_files = FileList["../elasticsearch-api/test/integration/yaml_test_runner.rb", "test/integration/**/*_test.rb"]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Run the task
|
34
|
+
args = [t.ruby_opts_string, t.run_code, t.file_list_string, t.option_list].join(' ')
|
35
|
+
|
36
|
+
ruby args do |ok, status|
|
37
|
+
if !ok && status.respond_to?(:signaled?) && status.signaled?
|
38
|
+
raise SignalException.new(status.termsig)
|
39
|
+
elsif !ok
|
40
|
+
fail "Command failed with status (#{status.exitstatus}): " + "[ruby #{args}]"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Run unit and integration tests"
|
46
|
+
task :all do
|
47
|
+
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
|
48
|
+
Rake::Task['test:unit'].invoke
|
49
|
+
Rake::Task['test:integration'].invoke
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "Initialize or update the repository with integration tests"
|
53
|
+
task :update do
|
54
|
+
sh "git --git-dir=#{__current__.join('tmp/elasticsearch-watcher/.git')} --work-tree=#{__current__.join('tmp/elasticsearch-watcher')} fetch origin --verbose"
|
55
|
+
begin
|
56
|
+
puts %x[git --git-dir=#{__current__.join('tmp/elasticsearch-watcher/.git')} --work-tree=#{__current__.join('tmp/elasticsearch-watcher')} pull --verbose]
|
57
|
+
rescue Exception => @exception
|
58
|
+
@failed = true
|
59
|
+
end
|
60
|
+
|
61
|
+
if @failed || !$?.success?
|
62
|
+
STDERR.puts "", "[!] Error while pulling -- #{@exception}"
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "\n", "CHANGES:", '-'*80
|
66
|
+
sh "git --git-dir=#{__current__.join('tmp/elasticsearch-watcher/.git')} --work-tree=#{__current__.join('tmp/elasticsearch-watcher')} log --oneline ORIG_HEAD..HEAD | cat", :verbose => false
|
67
|
+
end
|
68
|
+
|
69
|
+
namespace :cluster do
|
70
|
+
desc "Start Elasticsearch nodes for tests"
|
71
|
+
task :start do
|
72
|
+
$LOAD_PATH << File.expand_path('../../elasticsearch-transport/lib', __FILE__) << File.expand_path('../test', __FILE__)
|
73
|
+
require 'elasticsearch/extensions/test/cluster'
|
74
|
+
Elasticsearch::Extensions::Test::Cluster.start
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Stop Elasticsearch nodes for tests"
|
78
|
+
task :stop do
|
79
|
+
$LOAD_PATH << File.expand_path('../../elasticsearch-transport/lib', __FILE__) << File.expand_path('../test', __FILE__)
|
80
|
+
require 'elasticsearch/extensions/test/cluster'
|
81
|
+
Elasticsearch::Extensions::Test::Cluster.stop
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# ----- Documentation tasks ---------------------------------------------------
|
87
|
+
|
88
|
+
require 'yard'
|
89
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
90
|
+
t.options = %w| --embed-mixins --markup=markdown |
|
91
|
+
end
|
92
|
+
|
93
|
+
# ----- Code analysis tasks ---------------------------------------------------
|
94
|
+
|
95
|
+
require 'cane/rake_task'
|
96
|
+
Cane::RakeTask.new(:quality) do |cane|
|
97
|
+
cane.abc_max = 15
|
98
|
+
cane.no_style = true
|
99
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elasticsearch/watcher/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "elasticsearch-watcher"
|
8
|
+
s.version = Elasticsearch::Watcher::VERSION
|
9
|
+
s.authors = ["Karel Minarik"]
|
10
|
+
s.email = ["karel.minarik@elasticsearch.org"]
|
11
|
+
s.description = %q{Ruby Integrations for Elasticsearch Watcher plugin (WIP)}
|
12
|
+
s.summary = s.description
|
13
|
+
s.homepage = ""
|
14
|
+
s.license = "Apache 2"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split($/)
|
17
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency "elasticsearch-api"
|
22
|
+
|
23
|
+
s.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
s.add_development_dependency "rake"
|
25
|
+
|
26
|
+
s.add_development_dependency "elasticsearch"
|
27
|
+
s.add_development_dependency "elasticsearch-extensions"
|
28
|
+
|
29
|
+
s.add_development_dependency 'shoulda-context'
|
30
|
+
s.add_development_dependency 'activesupport'
|
31
|
+
s.add_development_dependency 'turn'
|
32
|
+
s.add_development_dependency 'mocha'
|
33
|
+
s.add_development_dependency 'minitest', '~> 4.0'
|
34
|
+
s.add_development_dependency 'minitest-reporters'
|
35
|
+
s.add_development_dependency 'simplecov'
|
36
|
+
s.add_development_dependency 'simplecov-rcov'
|
37
|
+
s.add_development_dependency 'yard'
|
38
|
+
s.add_development_dependency 'cane'
|
39
|
+
s.add_development_dependency 'pry'
|
40
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'elasticsearch/watcher'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "elasticsearch/watcher/version"
|
2
|
+
|
3
|
+
Dir[ File.expand_path('../watcher/api/actions/**/*.rb', __FILE__) ].each { |f| require f }
|
4
|
+
|
5
|
+
module Elasticsearch
|
6
|
+
module Watcher
|
7
|
+
def self.included(base)
|
8
|
+
base.__send__ :include, Elasticsearch::API::Watcher
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Elasticsearch
|
14
|
+
module API
|
15
|
+
module Watcher
|
16
|
+
module Actions; end
|
17
|
+
|
18
|
+
# Client for the "watcher" namespace (includes the {Watcher::Actions} methods)
|
19
|
+
#
|
20
|
+
class WatcherClient
|
21
|
+
include Elasticsearch::API::Common::Client,
|
22
|
+
Elasticsearch::API::Common::Client::Base,
|
23
|
+
Elasticsearch::API::Watcher::Actions
|
24
|
+
end
|
25
|
+
|
26
|
+
# Proxy method for {WatcherClient}, available in the receiving object
|
27
|
+
#
|
28
|
+
def watcher
|
29
|
+
@watcher ||= WatcherClient.new(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Elasticsearch::API.__send__ :include, Elasticsearch::API::Watcher
|
36
|
+
|
37
|
+
Elasticsearch::Transport::Client.__send__ :include, Elasticsearch::API::Watcher if defined?(Elasticsearch::Transport::Client)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module API
|
3
|
+
module Watcher
|
4
|
+
module Actions
|
5
|
+
|
6
|
+
# Throttle the execution of the watch by acknowledging it
|
7
|
+
#
|
8
|
+
# @option arguments [String] :id Watch ID (*Required*)
|
9
|
+
#
|
10
|
+
# @see http://www.elastic.co/guide/en/watcher/current/appendix-api-ack-watch.html
|
11
|
+
#
|
12
|
+
def ack_watch(arguments={})
|
13
|
+
raise ArgumentError, "Required argument 'id' missing" unless arguments[:id]
|
14
|
+
valid_params = [
|
15
|
+
:master_timeout
|
16
|
+
]
|
17
|
+
method = 'PUT'
|
18
|
+
path = "_watcher/watch/#{arguments[:id]}/_ack"
|
19
|
+
params = {}
|
20
|
+
body = nil
|
21
|
+
|
22
|
+
perform_request(method, path, params, body).body
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module API
|
3
|
+
module Watcher
|
4
|
+
module Actions
|
5
|
+
|
6
|
+
# Delete a specific watch
|
7
|
+
#
|
8
|
+
# @option arguments [String] :id Watch ID (*Required*)
|
9
|
+
# @option arguments [Boolean] :force Ignore any locks on the watch and force the execution
|
10
|
+
#
|
11
|
+
# @see http://www.elastic.co/guide/en/watcher/current/appendix-api-delete-watch.html
|
12
|
+
#
|
13
|
+
def delete_watch(arguments={})
|
14
|
+
raise ArgumentError, "Required argument 'id' missing" unless arguments[:id]
|
15
|
+
valid_params = [
|
16
|
+
:master_timeout,
|
17
|
+
:force
|
18
|
+
]
|
19
|
+
method = 'DELETE'
|
20
|
+
path = "_watcher/watch/#{arguments[:id]}"
|
21
|
+
params = {}
|
22
|
+
body = nil
|
23
|
+
|
24
|
+
perform_request(method, path, params, body).body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|