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

[example] adds example for collecting response metrics #404

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions examples/response-metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Reporting metrics on responses

APIcast supports metrics on requests out of the box. However, sometimes you need to capture metrics on the responses you're returning to the user.

For example, let's say you want to record how many documents a user is retrieving through an API call - it's not possible to record this through the request because we don't know how many documents are available until the request has been processed by our application.

To carry this out, you'll need to capture data from the response on it's way out of the 3scale gateway.

This is possible by creating a custom module and overriding the `post_action` function of `apicast`.

## How it works

There are two code snippets that do the following:

1. `apicast_response_metrics.lua` is a custom module (see [this example](https://github.com/3scale/apicast/tree/master/examples/custom-module) for more info) that overrides the default APIcast's post_action phase handler to include the following logic:
* It checks if the request was for a specific path, i.e. the path we wish to collect metrics on - in this case `/v1/documents`
* It extracts a custom header `x-document-count` from the response (which was added in the application code)
* It calls a custom path `/report_metric`, passing the `document_count` and `user_key` to a record the metric
2. `response_metrics.conf` is a configuration file that should be added to `apicast.d` directory to be included in the configuration.
* It contains `/report_metric` that is used to make the `POST` request to report custom metrics to 3scale

## How it works

You'll need to set up a custom metric in your 3scale Admin Portal. In this example we have a custom metric `document_count` already set up.

See the 3scale documentation on how to [Create new metric](https://support.3scale.net/docs/access-control/api-definition-methods-metrics).

## Adding the customization to APIcast

**Note:** the example commands are supposed to be run from the root of the local copy of the `apicast` repository.

### Native APIcast

Place `apicast_response_metrics.lua` to `apicast/src`, and `response_metrics.conf` to `apicast/apicast.d` and start APIcast:

```
THREESCALE_PORTAL_ENDPOINT=https://ACCESS-TOKEN@ACCOUNT-admin.3scale.net APICAST_MODULE=apicast_response_metrics bin/apicast
```

### Docker

Attach the above files as volumes to the container and set `APICAST_MODULE` environment variable.

```
docker run --name apicast --rm -p 8080:8080 -v $(pwd)/examples/response-metrics/apicast_response_metrics.lua:/opt/app-root/src/src/apicast_response_metrics.lua:ro -v $(pwd)/examples/response-metrics/response_metrics.conf:/opt/app-root/src/apicast.d/response_metrics.conf:ro -e THREESCALE_PORTAL_ENDPOINT=https://ACCESS-TOKEN@ACCOUNT-admin.3scale.net -e APICAST_MODULE=apicast_response_metrics quay.io/3scale/apicast:master
```
66 changes: 66 additions & 0 deletions examples/response-metrics/apicast_response_metrics.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
--- Custom APICAST_MODULE which overrides post_action from original
--- https://github.com/3scale/apicast/blob/3.0-stable/apicast/src/apicast.lua

-- load and initialize the parent module
local apicast = require('apicast').new()

local _M = { _VERSION = '3.0.0', _NAME = 'APIcast with response metrics' }
local mt = { __index = setmetatable(_M, { __index = apicast }) }

function _M.new()
return setmetatable({}, mt)
end

local function send_count_metrics()
-- get the document count from the custom response header
local document_count = ngx.resp.get_headers()['x-document-count'];
local user_key = ngx.req.get_headers()['user_key'];

-- only report metrics if the response was a success and the custom header exists
if ngx.status == ngx.HTTP_OK and document_count then
ngx.log(ngx.INFO, '[3scale-metrics] sending metric to 3scale document-count: ', document_count)
local report = ngx.location.capture("/report_metric",
Copy link
Member

Choose a reason for hiding this comment

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

@nburkley as @mikz pointed out it would be better to do this in lua. You could make 2 requests here to 3scale, first an authorise and then a report. This is done in the post_action phase and doesn't impact the latency on the client request. If the authorize returns a 409 you can allow the call through but if it returns a 403 then the call should be rejected, then you can report based on the authorize response. The authorize already exists in the backend_client so you would just need to add the report call there.

Copy link
Author

Choose a reason for hiding this comment

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

Sure, I can do that - I was trying to avoid modifying the backend_clint code - hence the call to an nginx conf.

I didn't have any auth-checks as I as going with the assumption that if the request made it thorugh our stack successfully it was already authorized - I can add this though.

{
args = {
user_key = user_key,
metric_name = 'document_count',
count = document_count
},
copy_all_vars = true
}
);

if report.status == ngx.HTTP_ACCEPTED then
ngx.log(ngx.INFO, '[3scale-metrics] document_count metric succeeded ', report.status)
else
ngx.log(ngx.WARN, '[3scale-metrics] document_count metric update failed. status: ', report.status)
end
end
end

function _M:post_action()
local request_id = ngx.var.original_request_id
local post_action_proxy = self.post_action_proxy

if not post_action_proxy then
return nil, 'not initialized'
end

-- send custom metrics if this is a document search
if ngx.var.request_uri:match '^/v1/documents' then
Copy link
Member

Choose a reason for hiding this comment

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

Better if this regex is not hardcoded. One suggestion could be to define another environment variable that consists of the regex pattern(s) to match against. APICAST_REPORT_METRIC_MATCHER for example... or at the very least define a variable with the regex pattern outside the scope of the function which can then be used anywhere within this custom module.

Copy link
Author

Choose a reason for hiding this comment

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

Sure, make sense, I'll tidy this up.

send_count_metrics()
end

local p = ngx.ctx.proxy or post_action_proxy[request_id]

post_action_proxy[request_id] = nil

if p then
return p:post_action()
else
ngx.log(ngx.INFO, 'could not find proxy for request id: ', request_id)
return nil, 'no proxy for request'
end
end

return _M
24 changes: 24 additions & 0 deletions examples/response-metrics/response_metrics.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
location = /report_metric {
internal;
resolver 8.8.8.8;
Copy link
Author

Choose a reason for hiding this comment

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

Ideally, we would not have to define a resolver here, but unlike the other conf blocks, the default resolver doesn't appear to be picked up here.


set $user_key $arg_user_key;
set $metric_name $arg_metric_name;
set $document_count $arg_count;
set $user_credentials transactions[0][user_key]=$user_key;
set $metrics transactions[0][usage][$metric_name]=$document_count;
set $reporting_url https://su1.3scale.net:443;
Copy link
Author

Choose a reason for hiding this comment

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

We tried using $backend_endpoint here, but it's value was "backend_upstream" which resulted in a peering lookup error failed to set current backend peer: missing peers while connecting to upstream.

set $path /transactions.xml?$backend_authentication_type=$backend_authentication_value&service_id=$service_id&$metrics&$user_credentials;

proxy_pass_request_headers off;
proxy_http_version 1.1;
proxy_set_header Host "$backend_host";
proxy_set_header User-Agent "$user_agent";
proxy_set_header X-3scale-User-Agent "$deployment";
proxy_set_header X-3scale-Version "$version";
proxy_set_header Connection "";
proxy_set_header Content-Type "application/x-www-form-urlencoded";
proxy_method POST;

proxy_pass $reporting_url$path;
}