Skip to content

Commit

Permalink
fix: adding .metrics property and circuit breaker metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
leftieFriele committed Nov 4, 2024
1 parent 6f14c0a commit e8f969a
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 19 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ For a complete list of options, consult the [undici documentation](https://undic

Closes the client and it's connections.

## Metrics

The expose metrics on the circuit breaking behaviour.

| name | type | description |
|------------------------------|---------|-------------------------------------------------------------------------------------------|
| `http_client_breaker_events` | counter | Counters on events exposed by the circuit breaker library,<br/>see [opossum] for details |


[@metrics/metric]: https://github.com/metrics-js/metric '@metrics/metric'
[abslog]: https://github.com/trygve-lie/abslog 'abslog'
[undici]: https://undici.nodejs.org/
Expand Down
32 changes: 32 additions & 0 deletions lib/http-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Agent, request, interceptors } from 'undici';
import createError from 'http-errors';
import Opossum from 'opossum';
import abslog from 'abslog';
import Metrics from '@metrics/client';

/**
* @typedef HttpClientOptions
Expand All @@ -22,10 +23,12 @@ export default class HttpClient {
#abortController;
#agent;
#breaker;
#breakerCounter;
#followRedirects;
// eslint-disable-next-line no-unused-private-class-members
#hasFallback = false;
#logger;
#metrics = new Metrics();
#throwOn400;
#throwOn500;

Expand Down Expand Up @@ -66,6 +69,27 @@ export default class HttpClient {
timeout,
});

this.#breakerCounter = this.#metrics.counter({
name: 'http_client_breaker_events',
description: 'Metrics on breaker events',
});
for (const eventName of this.#breaker.eventNames()) {
//@ts-ignore
const event = eventName;
//@ts-ignore
this.#breaker.on(event, (e) => {
let obj = Array.isArray(e) ? e[0] : e;
this.#breakerCounter.inc({
labels: {
//@ts-ignore
name: event,
...(obj && obj.path && { path: obj.path }),
...(obj && obj.origin && { origin: obj.origin }),
},
});
});
}

this.#agent = new Agent({
keepAliveMaxTimeout,
keepAliveTimeout,
Expand All @@ -84,6 +108,14 @@ export default class HttpClient {
}
}

/**
*
* @returns {import('@metrics/client')}
*/
get metrics() {
return this.#metrics;
}

async #request(options = {}) {
if (this.#followRedirects || options.redirectable) {
const { redirect } = interceptors;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@podium/typescript-config": "1.0.0",
"@types/node": "20.17.3",
"@types/opossum": "8.1.8",
"@types/readable-stream": "4.0.15",
"concurrently": "9.0.1",
"cronometro": "4.0.0",
"eslint": "9.13.0",
Expand Down
103 changes: 84 additions & 19 deletions tests/http-client.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import test from 'node:test';
import test, { after, before } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';

Expand Down Expand Up @@ -155,23 +155,29 @@ await test('http-client - abort controller', async (t) => {
});

await test('http-client - redirects', async (t) => {
const to = http.createServer(async (request, response) => {
response.writeHead(200);
response.end();
let to, from;
after(function () {
from.close();
to.close();
});
to.listen(3033, host);

const from = http.createServer(async (request, response) => {
if (request.url === '/redirect') {
response.setHeader('location', 'http://localhost:3033');
}
response.writeHead(301);
response.end();
before(function () {
to = http.createServer(async (request, response) => {
response.writeHead(200);
response.end();
});
to.listen(3033, host);
from = http.createServer(async (request, response) => {
if (request.url === '/redirect') {
response.setHeader('location', 'http://localhost:3033');
}
response.writeHead(301);
response.end();
});
from.listen(port, host);
});
from.listen(port, host);

await t.test('can follow redirects', async () => {
const client = new HttpClient({ threshold: 50, followRedirects: true });
const client = new HttpClient({ followRedirects: true });
const response = await client.request({
method: 'GET',
origin: `http://${host}:${port}`,
Expand All @@ -189,8 +195,6 @@ await test('http-client - redirects', async (t) => {
});
assert.strictEqual(response.statusCode, 301);
});
from.close();
to.close();
});

await test('http-client - circuit breaker behaviour', async (t) => {
Expand Down Expand Up @@ -250,7 +254,68 @@ await test('http-client - circuit breaker behaviour', async (t) => {
assert.strictEqual(response.statusCode, 200);
await afterEach(client);
});
// await t.test('has a .metrics property', async () => {
// const client = new HttpClient({ threshold: 50, reset: breakerReset });
// });
});

await test('http-client: metrics', async (t) => {
await t.test('has a .metrics property', () => {
const client = new HttpClient();
assert.notEqual(client.metrics, undefined);
});
await t.test('metric on open breakers', async () => {
beforeEach();
const client = new HttpClient({
threshold: 1,
reset: 10,
timeout: 10000,
throwOn400: false,
throwOn500: false,
});
const metrics = [];
client.metrics.on('data', (metric) => {
metrics.push(metric);
});
client.metrics.on('end', () => {
assert.strictEqual(metrics.length, 6, metrics);

Check failure on line 278 in tests/http-client.test.js

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 20)

Argument of type 'any[]' is not assignable to parameter of type 'string | Error'.

Check failure on line 278 in tests/http-client.test.js

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest, 22)

Argument of type 'any[]' is not assignable to parameter of type 'string | Error'.

Check failure on line 278 in tests/http-client.test.js

View workflow job for this annotation

GitHub Actions / build (macOS-latest, 20)

Argument of type 'any[]' is not assignable to parameter of type 'string | Error'.

Check failure on line 278 in tests/http-client.test.js

View workflow job for this annotation

GitHub Actions / build (macOS-latest, 22)

Argument of type 'any[]' is not assignable to parameter of type 'string | Error'.
assert.strictEqual(metrics[0].name, 'http_client_breaker_events');
assert.strictEqual(metrics[0].type, 2);
assert.strictEqual(metrics[0].labels[0].value, 'fire');
assert.strictEqual(metrics[0].labels[1].value, '/not-found');

assert.strictEqual(metrics[1].name, 'http_client_breaker_events');
assert.strictEqual(metrics[1].type, 2);
assert.strictEqual(metrics[1].labels[0].value, 'failure');

assert.strictEqual(metrics[2].name, 'http_client_breaker_events');
assert.strictEqual(metrics[2].type, 2);
assert.strictEqual(metrics[2].labels[0].value, 'open');

assert.strictEqual(metrics[3].name, 'http_client_breaker_events');
assert.strictEqual(metrics[3].type, 2);
assert.strictEqual(metrics[3].labels[0].value, 'fire');

assert.strictEqual(metrics[4].name, 'http_client_breaker_events');
assert.strictEqual(metrics[4].type, 2);
assert.strictEqual(metrics[4].labels[0].value, 'close');
});
try {
// Make the circuit open
await client.request({
path: '/not-found',
origin: 'http://not.found.host:3003',
method: 'GET',
});
// eslint-disable-next-line no-unused-vars
} catch (_) {
/* empty */
}
await wait(15);
// Wait for circuit to reset, before using the client again.
await client.request({
path: '/',
origin: 'http://localhost:3003',
method: 'GET',
});
client.metrics.push(null);
await afterEach(client);
});
});

0 comments on commit e8f969a

Please sign in to comment.