Skip to content

Commit

Permalink
Add :metrics option to capture headers (#625)
Browse files Browse the repository at this point in the history
* Add :withFaunaMetrics option to capture headers

* rename 'headers' to 'metrics'

* Add `queryWithMetrics()` to align w JVM driver

* wip

* fix node backward compatibility issue

* pass metrics option into query query options

* add tests

* remove legacy metrics

* Update src/Client.js

Co-authored-by: Cleve Stuart <90649124+cleve-fauna@users.noreply.github.com>

* Remove legacy metric headers

* drop legacy metrics

* documentation

* add comments

* update readme

* Return metrics as Int

* Update docs

* Update src/Client.js

Co-authored-by: Cleve Stuart <90649124+cleve-fauna@users.noreply.github.com>

* bugfix

* add test

* add test

Co-authored-by: Henry Ball <henry.ball@fauna.com>
Co-authored-by: Cleve Stuart <90649124+cleve-fauna@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 29, 2022
1 parent ee6f53f commit 2dd164e
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 14 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,34 @@ createP.then(function(response) {
`response` is a JSON object containing the FaunaDB response. See the JSDocs for
`faunadb.Client`.

The `:metrics` option is used during instantiation to create a client that also
returns usage information about the queries issued to FaunaDB.

```javascript
let client = new faunadb.Client({
secret: 'YOUR_FAUNADB_SECRET',
metrics: true
})
```

The `response` object is shaped differently for clients when the `:metrics` option
is/not set. The default client setting returns the `response` body at root level.
When the client is configured to return `:metrics`, the `response` object is
structured as follows:

```javascript
{
value: { ... }, // structured response body
metrics: {
x-compute-ops: XX,
x-byte-read-ops: XX,
x-byte-write-ops: XX,
x-query-time: XX,
x-txn-retries: XX
} // usage data
}
```
Metrics returned in the response will be of `Int` data type.
#### Pagination Helpers

This driver contains helpers to provide a simpler API for consuming paged
Expand Down
42 changes: 41 additions & 1 deletion src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ var values = require('./values')
* which has the highest priority and overrides the option passed into the Client constructor.
* @param {?boolean} options.checkNewVersion
* Enabled by default. Prints a message to the terminal when a newer driver is available.
* @param {?boolean} options.metrics
* Disabled by default. Controls whether or not query metrics are returned.
*/
function Client(options) {
var http2SessionIdleTime = getHttp2SessionIdleTime()
Expand All @@ -182,6 +184,7 @@ function Client(options) {
queryTimeout: null,
http2SessionIdleTime: http2SessionIdleTime.value,
checkNewVersion: false,
metrics: false,
})

if (http2SessionIdleTime.shouldOverride) {
Expand All @@ -191,6 +194,7 @@ function Client(options) {
this._observer = options.observer
this._http = new http.HttpClient(options)
this.stream = stream.StreamAPI(this)
this._globalQueryOptions = { metrics: options.metrics }
}

/**
Expand All @@ -212,6 +216,7 @@ Client.apiVersion = packageJson.apiVersion
* @return {external:Promise<Object>} FaunaDB response object.
*/
Client.prototype.query = function(expression, options) {
options = Object.assign({}, this._globalQueryOptions, options)
return this._execute('POST', '', query.wrap(expression), null, options)
}

Expand Down Expand Up @@ -281,6 +286,24 @@ Client.prototype.close = function(opts) {
return this._http.close(opts)
}

/**
* Executes a query via the FaunaDB Query API.
* See the [docs](https://app.fauna.com/documentation/reference/queryapi),
* and the query functions in this documentation.
* @param expression {module:query~ExprArg}
* The query to execute. Created from {@link module:query} functions.
* @param {?Object} options
* Object that configures the current query, overriding FaunaDB client options.
* @param {?string} options.secret FaunaDB secret (see [Reference Documentation](https://app.fauna.com/documentation/intro/security))
* @return {external:Promise<Object>} {value, metrics} An object containing the FaunaDB response object and the list of query metrics incurred by the request.
*/
Client.prototype.queryWithMetrics = function(expression, options) {
options = Object.assign({}, this._globalQueryOptions, options, {
metrics: true,
})
return this._execute('POST', '', query.wrap(expression), null, options)
}

Client.prototype._execute = function(method, path, data, query, options) {
query = util.defaults(query, null)

Expand Down Expand Up @@ -327,7 +350,24 @@ Client.prototype._execute = function(method, path, data, query, options) {
)
self._handleRequestResult(response, result, options)

return responseObject['resource']
const metricsHeaders = [
'x-compute-ops',
'x-byte-read-ops',
'x-byte-write-ops',
'x-query-time',
'x-txn-retries',
]

if (options && options.metrics) {
return {
value: responseObject['resource'],
metrics: Object.fromEntries(Array.from(Object.entries(response.headers)).
filter( ([k,v]) => metricsHeaders.includes(k) ).
map(([ k,v ]) => [k, parseInt(v)])
)}
} else {
return responseObject['resource']
}
})
}

Expand Down
10 changes: 7 additions & 3 deletions src/types/Client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export interface ClientConfig {
fetch?: typeof fetch
http2SessionIdleTime?: number
checkNewVersion?: boolean
metrics?: boolean
}

export interface QueryOptions
extends Partial<
Pick<ClientConfig, 'secret' | 'queryTimeout' | 'fetch' | 'observer'>
Pick<
ClientConfig,
'secret' | 'queryTimeout' | 'fetch' | 'observer' | 'metrics'
>
> {
signal?: AbortSignal
}
signal?: AbortSignal
}

type StreamFn<T> = (
expr: Expr,
Expand Down
54 changes: 44 additions & 10 deletions test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@ describe('Client', () => {
expect(client._http._baseUrl.endsWith(':0')).toBeFalsy()
})

test('returns query metrics if the metrics flag is set', async () => {
var metricsClient = util.getClient({ metrics: true })
const response = await metricsClient.query(query.Add(1, 1))
assertMetric(response.metrics, 'x-compute-ops')
assertMetric(response.metrics, 'x-byte-read-ops')
assertMetric(response.metrics, 'x-byte-write-ops')
assertMetric(response.metrics, 'x-query-time')
assertMetric(response.metrics, 'x-txn-retries')
})

test('returns query metrics if using the queryWithMetrics function', async () => {
const response = await client.queryWithMetrics(query.Add(1, 1))
assertMetric(response.metrics, 'x-compute-ops')
assertMetric(response.metrics, 'x-byte-read-ops')
assertMetric(response.metrics, 'x-byte-write-ops')
assertMetric(response.metrics, 'x-query-time')
assertMetric(response.metrics, 'x-txn-retries')
})

test('query metrics response has correct structure', async () => {
const response = await client.queryWithMetrics(query.Add(1, 1))
expect(Object.keys(response).sort()).
toEqual(['metrics', 'value'])
})

test('client with metrics returns expected value', async () => {
const response = await client.queryWithMetrics(query.Add(1, 1))
expect(response.value).toEqual(2)
})

test('paginates', () => {
return createDocument().then(function(document) {
return client.paginate(document.ref).each(function(page) {
Expand Down Expand Up @@ -79,12 +109,12 @@ describe('Client', () => {

test('extract response headers from observer', () => {
var assertResults = function(result) {
assertHeader(result.responseHeaders, 'x-read-ops')
assertHeader(result.responseHeaders, 'x-write-ops')
assertHeader(result.responseHeaders, 'x-storage-bytes-read')
assertHeader(result.responseHeaders, 'x-storage-bytes-write')
assertHeader(result.responseHeaders, 'x-query-bytes-in')
assertHeader(result.responseHeaders, 'x-query-bytes-out')
assertObserverStats(result.responseHeaders, 'x-read-ops')
assertObserverStats(result.responseHeaders, 'x-write-ops')
assertObserverStats(result.responseHeaders, 'x-storage-bytes-read')
assertObserverStats(result.responseHeaders, 'x-storage-bytes-write')
assertObserverStats(result.responseHeaders, 'x-query-bytes-in')
assertObserverStats(result.responseHeaders, 'x-query-bytes-out')

expect(result.endTime).toBeGreaterThan(result.startTime)
}
Expand Down Expand Up @@ -386,12 +416,16 @@ describe('Client', () => {
requiredKeys.every(key => driverEnvHeader.includes(key))
).toBeDefined()
})

})

function assertHeader(headers, name) {
expect(headers[name]).not.toBeNull()
expect(parseInt(headers[name])).toBeGreaterThanOrEqual(0)
function assertObserverStats(metrics, name) {
expect(metrics[name]).not.toBeNull()
expect(parseInt(metrics[name])).toBeGreaterThanOrEqual(0)
}

function assertMetric(metrics, name) {
expect(metrics[name]).not.toBeNull()
expect(metrics[name]).toBeGreaterThanOrEqual(0)
}

function createDocument() {
Expand Down

0 comments on commit 2dd164e

Please sign in to comment.