Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buddhi metric reporter service api #6

Merged
merged 17 commits into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 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 Down
19 changes: 18 additions & 1 deletion buddhi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,26 @@ $ curl http://127.0.0.1:8089/paths/backend?lines=1
```bash
$ curl http://127.0.0.1:8089/paths/amp?lines=1
"30dd63c1-4ee9-481e-b0ed-e7d10d4900c9","/1?user_key=2c6e8625ed43d064"
```
- POST **/report/amp**: Send traffic file and generate metric counter report.
```bash
$ curl -X POST --data-binary "@traffic.csv" http://127.0.0.1:8089/report/amp 2>/dev/null | jq '.'
{
"metrics": {
"24b08f30-403e-4be7-83d8-984d6e93b91a": 7,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A metric ID by itself does not identify a metric. It also needs the service ID.
If I remember correctly, Apisonator will let you create 2 different metrics with the same ID under different services.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct,

What format would be appropriate?

{
   "svc_a":{
       "metric_a_1": 3,
       "metric_a_2": 6,
        "metric_a_3": 8,
       }
   },
   "svc_b":{
       "metric_b_1": 1,
       "metric_b_2": 2,
        "metric_b_3": 7,
       }
   }
}

or CSV

"service", "metric", "counter"
"svc_a", "metric_a_1", 5
"svc_a", "metric_a_2", 5
"svc_a", "metric_a_3", 5
"svc_b", "metric_b_1", 5
"svc_b", "metric_b_2", 5
"svc_b", "metric_b_3", 5

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd return JSON in this case because it allows us to represent the service -> metric relationships

"406127d9-0db7-4c77-8971-405f75ae94aa": 5,
"7c1b7719-518c-45dc-9fd7-cbd56a7b921e": 2,
"ce116219-2b63-4b6b-abb5-d52167be17cd": 2,
"e28ae3f1-c3dd-43c9-8b66-52ea33b02e91": 6,
"a53593fa-5d79-477b-a364-d6d3e6718b96": 3,
"9cd322bd-ed72-490c-8a1b-cf1fc1831ce5": 2,
"426d164f-2c87-4434-9d10-e6728024f422": 1,
"1f47cdce-f802-46ce-bc11-8139f9d09612": 2
}
}
```

## Contributing
## 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
27 changes: 27 additions & 0 deletions buddhi/lib/amp/toolkit/buddhi/metric_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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)
CSV.parse(data).map(&method(:parse)).each_with_object({}) do |(service_id, path), hsh|
hsh.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 = { metrics: 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
22 changes: 22 additions & 0 deletions buddhi/spec/metric_reporter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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('base' => 4, 'metric_1' => 6, 'metric_3' => 1, 'metric_2' => 2)
end
end
end
Loading