Microservice Usage Logging with Openresty and Google BigQuery
Tags: nginx, openresty, bigquery
Published: 2022-06-14 22:42:11

---
title: Microservice Usage Logging with Openresty and Google BigQuery
published: true
date: 2017-04-21 00:00:00 UTC
tags: nginx, openresty, bigquery
canonical_url: https://justin.poehnelt.com/posts/microservice-usage-logginging-with-openresty-and-google-bigquery/
---

Here at Descartes Labs, we have been using the microservice architecture in building out our platform. If you are unfamiliar with microservices, they are a collection of independent services often communicating over HTTP or GRPC. Check out Martin Fowler’s 2014 [article](https://martinfowler.com/articles/microservices.html) for more information.

[![Microservices at Descartes Labs](https://justin.poehnelt.com/images/microservices-1EOecTj8ug-200.jpeg)](https://justin.poehnelt.com/images/microservices-1EOecTj8ug-600.jpeg)

Microservices at Descartes Labs

## Logging bytes sent

The key requirement of our logging, specifically usage logging, is to capture the number of bytes sent to the user since we are providing API access to our entire corpus of satellite imagery. This most closely matches our costs, such as egress, and can give us an indication of the underlying value provided to the customer.

The problem in NGINX and with chunked responses from upstream services is that getting the `$bytes_sent` from NGINX can only occur in the logging phase. Alternatively, the `body_filter_by_lua*` could be used to track the bytes from each chunk, but that is definitely a second option due to the added complexity.

The first thing to try, which DOES NOT WORK, is the following:

```
log_by_lua_block {
    local payload = {
        bytes = tonumber(ngx.var.bytes_sent),
        status = tonumber(ngx.var.status),
        timestamp = ngx.now() 
        ... -- more values
    }
    -- post payload to favorite backend for timeseries analysis
}

```

There are two issues with the above. The critical issue is that the Lua cosocket for nonblocking IO is not available in the logging phase. The second is we want to do this in batch.

## Using a detached thread

The solution we have implemented involves using a detached thread on each NGINX worker and a shared thread safe buffer.

[![OpenResty architecture](https://justin.poehnelt.com/images/architecture-diagram-VMzP_YOA0Y-200.jpeg)](https://justin.poehnelt.com/images/architecture-diagram-VMzP_YOA0Y-600.jpeg)

OpenResty architecture

The NGINX Lua blocks look like the following.

```
lua_shared_dict usage_logging 10m;

init_worker_by_lua_block {
    local Logging = require "descarteslabs.logging"
    l = Logging.new()
    l:watch(ngx.shared.usage_logging)
}

location = /test {
    proxy_pass service;
    log_by_lua_block {
        local payload = {
            bytes = tonumber(ngx.var.bytes_sent),
            status = tonumber(ngx.var.status),
            timestamp = ngx.now() 
            ... -- more values
        }
        local Logging = require "descarteslabs.logging"
        l = Logging.save(ngx.shared.usage_logging, payload)
    }    
}
```

## Checking the buffer

The [ngx.timer.at](http://ngx.timer.at/) mechanism makes it trivial to watch this buffer. The only trick is that each worker will be watching the buffer, so some randomness should be added.

```
local check_function
check_function = function(premature)
    if not premature then
        while true do
            local requests = self:get(size)
            pcall(_M.save, self, rows)

            if #rows < size then
                break
            end
        end

        local ok, err = ngx.timer.at(delay(), check_function)
        if not ok then
            log(ERR, "failed to create timer: ", err)
            return
        end
    end
end
```

## Using BigQuery

As a primarily Google Cloud Platform customer, we have a custom Lua client for many of the Google Cloud APIs, such as Cloud Storage, BigQuery, and Stackdriver. For this particular use case, we are trying out BigQuery. Our query looks something like this:

```
SELECT 
  COUNT(*) as calls,
  SUM(bytes) as bytes, 
  DATETIME_TRUNC(DATETIME(timestamp), `second` ) as w
FROM `project.dataset.table`
WHERE
  status=200 
GROUP BY
  w
ORDER BY 
  w desc
LIMIT 100
```

Which outputs this table:

[![Google BigQuery Results Using Vegeta for Load Testing](https://justin.poehnelt.com/images/bigquery-table-output-w47vX3NkvB-200.jpeg)](https://justin.poehnelt.com/images/bigquery-table-output-w47vX3NkvB-400.jpeg)

Google BigQuery Results Using Vegeta for Load Testing

Having reached this point, it is trivial to add a few fields to group by, such as customer or service, and provide billing with varying windows and granularity.