Skip to content

Commit

Permalink
Merge pull request #6 from 3scale/buddhi-metric-counter-api
Browse files Browse the repository at this point in the history
Buddhi metric reporter service api
  • Loading branch information
eguzki authored Jun 6, 2018
2 parents ec13d24 + b33675e commit b8ae4f2
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 87 deletions.
4 changes: 3 additions & 1 deletion buddhi/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ IMAGE_NAME ?= perftest-toolkit:$(IMAGE_TAG)
REGISTRY ?= quay.io/3scale

build:
rake build -v
bundle exec rake build -v
docker build -t $(IMAGE_NAME) .

push:
Expand All @@ -22,3 +22,5 @@ test:
clean:
bundle clean
rm -rf vendor
rm -rf pkg
rm -rf coverage
52 changes: 50 additions & 2 deletions buddhi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,58 @@ $ curl http://127.0.0.1:8089/paths/backend?lines=1
- **/paths/amp**: CSV formatted file with **Host, Path*^** header. Add *lines* query param for any number of valid random requests.
```bash
$ curl http://127.0.0.1:8089/paths/amp?lines=1
"30dd63c1-4ee9-481e-b0ed-e7d10d4900c9","/1?user_key=2c6e8625ed43d064"
"53f07c14-e35e-4bfa-b0b1-9d3a993fad14.benchmark.3sca.net","/1?app_id=6641c22185bbf204&app_key=3d0112323ceef116"
```
- POST **/report/amp**: Send traffic file and generate metric counter report.

## Contributing
Report shows usage for every metric involved in traffic.

Format
```
{ service_id => { metric_id: counter} }
```

Traffic information can be generated using */paths/amp* endpoint as follows:

```bash
$ curl http://127.0.0.1:8089/paths/amp?lines=5 2>/dev/null > traffic.csv
$ cat traffic.csv
"53f07c14-e35e-4bfa-b0b1-9d3a993fad14.benchmark.3sca.net","/1?app_id=ddfa9a8842a3822e&app_key=73418183a69b027a"
"e75ef4f7-54da-4ec6-a4b2-33a163764385.benchmark.3sca.net","/1?app_id=5e4618aa57d801cd&app_key=fe4db52e5e86668f"
"e75ef4f7-54da-4ec6-a4b2-33a163764385.benchmark.3sca.net","/11?app_id=ceeeb23abfd0adfd&app_key=fbdfae99a587811e"
"31b75b9b-fbb4-4223-8736-b93c34676f04.benchmark.3sca.net","/1?user_key=aa5736e41a3888db"
"e75ef4f7-54da-4ec6-a4b2-33a163764385.benchmark.3sca.net","/111?app_id=ca2f8ff8b0a8707c&app_key=4b349db5bb77b9db"
```

Then, metric report can be generated requesting */report/amp* endpoint as follows:

```bash
$ curl -X POST --data-binary "@traffic.csv" http://127.0.0.1:8089/report/amp 2>/dev/null
{"53f07c14-e35e-4bfa-b0b1-9d3a993fad14":{"6527c16b-dfeb-46e7-93d8-4eef0a6abbe3":1,"0dfb13fa-410e-4394-9fad-f0b785e1e680":1},"e75ef4f7-54da-4ec6-a4b2-33a163764385":{"dabdb86c-5344-4ff5-b8c7-65740357ecc6":6,"439402ef-ee9a-4c2a-856e-59928a3cef10":3,"e7265d03-efa7-4641-80b6-6d4a0d44713b":2,"937ebcef-1afd-4498-91a7-696c069f4668":1},"31b75b9b-fbb4-4223-8736-b93c34676f04":{"267c1777-53f7-4568-b9c0-2af28571a1dc":1,"2641b733-2030-4fac-9248-c1b8d3a4f02b":1}}
```

Pretty printed

```json
{
"53f07c14-e35e-4bfa-b0b1-9d3a993fad14": {
"6527c16b-dfeb-46e7-93d8-4eef0a6abbe3": 1,
"0dfb13fa-410e-4394-9fad-f0b785e1e680": 1
},
"e75ef4f7-54da-4ec6-a4b2-33a163764385": {
"dabdb86c-5344-4ff5-b8c7-65740357ecc6": 6,
"439402ef-ee9a-4c2a-856e-59928a3cef10": 3,
"e7265d03-efa7-4641-80b6-6d4a0d44713b": 2,
"937ebcef-1afd-4498-91a7-696c069f4668": 1
},
"31b75b9b-fbb4-4223-8736-b93c34676f04": {
"267c1777-53f7-4568-b9c0-2af28571a1dc": 1,
"2641b733-2030-4fac-9248-c1b8d3a4f02b": 1
}
}
```

## Development

## Run unit tests

Expand Down
1 change: 1 addition & 0 deletions buddhi/lib/amp/toolkit/buddhi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
require 'amp/toolkit/buddhi/simple/test_plan'
require 'amp/toolkit/buddhi/simple/factory'
require 'amp/toolkit/buddhi/cli'
require 'amp/toolkit/buddhi/metric_reporter'
require 'amp/toolkit/buddhi/main'
3 changes: 2 additions & 1 deletion buddhi/lib/amp/toolkit/buddhi/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ module Buddhi
def self.main
opts = AMP::Toolkit::Buddhi::CLI.run
test_plan = AMP::Toolkit::Buddhi::Factory.call opts
metric_report = AMP::Toolkit::Buddhi::MetricReporter.new test_plan
AMP::Toolkit::Buddhi::Backend.run test_plan
AMP::Toolkit::Buddhi::Server.run test_plan
AMP::Toolkit::Buddhi::Server.run test_plan, metric_report
end
end
end
Expand Down
30 changes: 30 additions & 0 deletions buddhi/lib/amp/toolkit/buddhi/metric_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'csv'

module AMP
module Toolkit
module Buddhi
class MetricReporter
attr_reader :test_plan
def initialize(test_plan)
@test_plan = test_plan
end

def report(data)
# hash default value will always be one and the same object
# initializing with a block like that,
# hash['key'].merge!(some_hash) will keep updates in hash
CSV.parse(data).map(&method(:parse)).each_with_object(Hash.new { |hash, key| hash[key] = {} }) do |(service_id, path), acc|
acc[service_id].merge!(test_plan.metric_report(service_id, path)) { |_, old, new| old + new }
end
end

private

def parse(row)
host, full_path = row.map(&:strip)
[host.split('.')[0], URI(full_path).path]
end
end
end
end
end
40 changes: 6 additions & 34 deletions buddhi/lib/amp/toolkit/buddhi/saas/test_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,15 @@ class TestPlan
BACKEND_N_METRIC_ARY = ([0] * 20 + [1] * 70 + [2] * 7 + [3] * 3).freeze
AMP_N_METRIC_ARY = ([1] * 70 + [2] * 20 + [3] * 10).freeze

def apicast_service_info(id)
return unless @services.key? id
apicast_service_obj(@services[id]) { |_, idx| proxy_pattern(idx + 1) }
def amp_uri_path
proxy_pattern(AMP_N_METRIC_ARY.sample)
end

def amp_path
host, path = amp_path_sample
%("#{host}","#{path}")
end

def amp_path_sample
service = @services.values.sample
app_key = service[:application_keys].sample
app_id_auth = app_auth_params app_key
uri = amp_uri(proxy_pattern(AMP_N_METRIC_ARY.sample), app_id_auth)
[hosts_for(service[:id]).first, "#{uri.path}?#{uri.query}"]
end

def proxy_pattern(n)
format('/%<path>s', path: '1' * n)
end

def backend_path
service = @services.values.sample
app_key = service[:application_keys].sample
metrics = service[:metrics].values
app_id_auth = app_auth_params app_key

query = {
provider_key: service[:provider_key],
service_id: service[:id]
}.merge(app_id_auth)

BACKEND_N_METRIC_ARY.sample.times do |idx|
query["usage[#{metrics[idx][:name]}]".to_sym] = 1
def backend_metric_usage(service)
# first metric is the parent 'hits'
service[:metrics].values[1..BACKEND_N_METRIC_ARY.sample].each do |metric|
yield metric
end
backend_uri(query)
end
end
end
Expand Down
14 changes: 10 additions & 4 deletions buddhi/lib/amp/toolkit/buddhi/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ module AMP
module Toolkit
module Buddhi
class Server
attr_reader :test_plan, :services_info
attr_reader :test_plan, :services_info, :metric_report

def initialize(test_plan)
def initialize(test_plan, metric_report)
@test_plan = test_plan
@metric_report = metric_report
@server = WEBrick::HTTPServer.new Port: test_plan.http_port
@server.mount_proc '/admin/api/services.json', method(:services)
@server.mount_proc '/admin/api/services/', method(:service)
@server.mount_proc '/paths/amp', method(:amp_paths)
@server.mount_proc '/paths/backend', method(:backend_paths)
@server.mount_proc '/report/amp', method(:amp_report)
end

def start
Expand Down Expand Up @@ -47,8 +49,12 @@ def path(req, res, test_plan_method)
res.body = Array.new(num_lines) { test_plan_method.call }.join("\n")
end

def self.run(test_plan)
new(test_plan).start
def amp_report(req, res)
res.body = metric_report.report(req.body).to_json
end

def self.run(test_plan, metric_report)
new(test_plan, metric_report).start
end
end
end
Expand Down
37 changes: 4 additions & 33 deletions buddhi/lib/amp/toolkit/buddhi/simple/test_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,13 @@ module Simple
class TestPlan
include Buddhi::TestPlan

AMP_URL_PATH_TEMPLATE = '/%<metric>s/some-request'.freeze

def apicast_service_info(id)
return unless @services.key? id
apicast_service_obj(@services[id]) { |metric, _| "/#{metric[:name]}/" }
end

def amp_path
host, path = amp_path_sample
%("#{host}","#{path}")
end

def amp_path_sample
service = @services.values.first
# first metric is the parent 'hits'
metric = service[:metrics].values.drop(1).first
app_key = service[:application_keys].first
app_id_auth = app_auth_params app_key

uri = amp_uri(format(AMP_URL_PATH_TEMPLATE, metric: metric[:name]), app_id_auth)
[hosts_for(service[:id]).first, "#{uri.path}?#{uri.query}"]
def amp_uri_path
proxy_pattern(1)
end

def backend_path
service = @services.values.first
def backend_metric_usage(service)
# first metric is the parent 'hits'
metric = service[:metrics].values.drop(1).first
app_key = service[:application_keys].first
app_id_auth = app_auth_params app_key

query = {
provider_key: service[:provider_key],
service_id: service[:id]
}.merge(app_id_auth)
query["usage[#{metric[:name]}]".to_sym] = 1
backend_uri(query)
yield service[:metrics].values.drop(1).first
end
end
end
Expand Down
56 changes: 54 additions & 2 deletions buddhi/lib/amp/toolkit/buddhi/test_plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Toolkit
module Buddhi
# TestPlan Interface
module TestPlan
%i[apicast_service_info amp_path backend_path].each do |method_name|
%i[amp_uri_path backend_metric_usage].each do |method_name|
define_method(method_name) do
raise 'Not Implemented'
end
Expand Down Expand Up @@ -88,6 +88,11 @@ def backend_uri(query_params)
"#{uri.path}?#{uri.query}"
end

def apicast_service_info(id)
return unless @services.key? id
apicast_service_obj @services[id]
end

def apicast_service_obj(service)
{
id: service[:id],
Expand All @@ -104,7 +109,7 @@ def apicast_service_obj(service)
proxy_rules: service[:metrics].values.drop(1).each_with_index.map do |metric, idx|
{
http_method: 'GET',
pattern: yield(metric, idx),
pattern: proxy_pattern(idx + 1),
metric_system_name: metric[:name],
delta: 1
}
Expand All @@ -113,9 +118,56 @@ def apicast_service_obj(service)
}
end

def proxy_pattern(n)
format('/%<path>s', path: '1' * n)
end

def amp_path
service = @services.values.sample
app_key = service[:application_keys].sample
app_id_auth = app_auth_params app_key
uri = amp_uri(amp_uri_path, app_id_auth)
host = hosts_for(service[:id]).first
path = "#{uri.path}?#{uri.query}"
%("#{host}","#{path}")
end

def amp_uri(path, query_params)
URI::HTTP.build(path: path, query: URI.encode_www_form(query_params))
end

def backend_path
service = @services.values.sample
app_key = service[:application_keys].sample
app_id_auth = app_auth_params app_key

query = {
provider_key: service[:provider_key],
service_id: service[:id]
}.merge(app_id_auth)

backend_metric_usage(service) do |metric|
query["usage[#{metric[:name]}]".to_sym] = 1
end

backend_uri(query)
end

def metric_report(service_id, path)
return {} unless @services.key? service_id
service = @services[service_id]
parent_metric = service[:metrics].values.first
proxy_rules = apicast_service_obj(@services[service_id])[:proxy][:proxy_rules]
matching_rules = proxy_rules.select { |r| filter_matching_rule(r, path) }
matching_rules.each_with_object(Hash.new(0)) do |rule, acc|
acc[parent_metric[:name]] += 1
acc[rule[:metric_system_name]] += rule[:delta]
end
end

def filter_matching_rule(rule, path)
!/#{rule[:pattern]}/.match(path).nil?
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion buddhi/spec/main_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
RSpec.describe AMP::Toolkit::Buddhi do
let(:opts) { { testplan: 'some_plan' } }
let(:test_plan) { { test_plan: 1 } }
let(:metric_reporter) { {} }

before do
expect(AMP::Toolkit::Buddhi::CLI).to receive(:run).and_return(opts)
expect(AMP::Toolkit::Buddhi::Factory).to receive(:call).with(opts).and_return(test_plan)
expect(AMP::Toolkit::Buddhi::MetricReporter).to receive(:new).with(test_plan).and_return(metric_reporter)
expect(AMP::Toolkit::Buddhi::Backend).to receive(:run).with(test_plan)
expect(AMP::Toolkit::Buddhi::Server).to receive(:run).with(test_plan)
expect(AMP::Toolkit::Buddhi::Server).to receive(:run).with(test_plan, metric_reporter)
end

it 'main' do
Expand Down
32 changes: 32 additions & 0 deletions buddhi/spec/metric_reporter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'amp/toolkit/buddhi'
RSpec.describe AMP::Toolkit::Buddhi::MetricReporter do
let(:test_plan) { instance_double(AMP::Toolkit::Buddhi::Simple::TestPlan) }
subject { described_class.new test_plan }
describe 'report' do
it 'should return empty hash when empty input' do
expect(test_plan).not_to receive(:metric_report)
expect(subject.report('')).to eq({})
end

it 'should return aggregated results' do
data = %("a533908aa896.benchmark.3sca.net","/1?app_id=4cc&app_key=fcc"
"cb0a88b8da15.benchmark.3sca.net","/1?app_id=74ac&app_key=22"
"a533908aa896.benchmark.3sca.net","/11?app_id=6ba&app_key=ff"
)
expect(test_plan).to receive(:metric_report).with('a533908aa896', '/1').and_return('base' => 1, 'metric_1' => 3)
expect(test_plan).to receive(:metric_report).with('cb0a88b8da15', '/1').and_return('base' => 1, 'metric_2' => 2)
expect(test_plan).to receive(:metric_report).with('a533908aa896', '/11').and_return('base' => 2, 'metric_1' => 3, 'metric_3' => 1)
expect(subject.report(data)).to eq(
'a533908aa896' => {
'base' => 3,
'metric_1' => 6,
'metric_3' => 1
},
'cb0a88b8da15' => {
'base' => 1,
'metric_2' => 2
}
)
end
end
end
Loading

0 comments on commit b8ae4f2

Please sign in to comment.