Skip to content

Commit c9f50c4

Browse files
authored
feat: Add stats of client and pool to be accessible through agent (#4157)
1 parent bd24250 commit c9f50c4

File tree

14 files changed

+458
-182
lines changed

14 files changed

+458
-182
lines changed

docs/docs/api/Agent.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,9 @@ See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatche
7575
### `Agent.upgrade(options[, callback])`
7676

7777
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
78+
79+
### `Agent.stats()`
80+
81+
Returns an object of stats by origin in the format of `Record<string, TClientStats | TPoolStats>`
82+
83+
See [`PoolStats`](/docs/docs/api/PoolStats.md) and [`ClientStats`](/docs/docs/api/ClientStats.md).

docs/docs/api/ClientStats.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Class: ClientStats
2+
3+
Stats for a [Client](/docs/docs/api/Client.md).
4+
5+
## `new ClientStats(client)`
6+
7+
Arguments:
8+
9+
* **client** `Client` - Client from which to return stats.
10+
11+
## Instance Properties
12+
13+
### `ClientStats.connected`
14+
15+
Boolean if socket as open connection by this client.
16+
17+
### `ClientStats.pending`
18+
19+
Number of pending requests of this client.
20+
21+
### `ClientStats.running`
22+
23+
Number of currently active requests across this client.
24+
25+
### `ClientStats.size`
26+
27+
Number of active, pending, or queued requests of this clients.

lib/dispatcher/agent.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
const { InvalidArgumentError } = require('../core/errors')
4-
const { kClients, kRunning, kClose, kDestroy, kDispatch } = require('../core/symbols')
4+
const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
55
const DispatcherBase = require('./dispatcher-base')
66
const Pool = require('./pool')
77
const Client = require('./client')
@@ -110,6 +110,16 @@ class Agent extends DispatcherBase {
110110

111111
await Promise.all(destroyPromises)
112112
}
113+
114+
get stats () {
115+
const allClientStats = {}
116+
for (const client of this[kClients].values()) {
117+
if (client.stats) {
118+
allClientStats[client[kUrl].origin] = client.stats
119+
}
120+
}
121+
return allClientStats
122+
}
113123
}
114124

115125
module.exports = Agent

lib/dispatcher/client.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const assert = require('node:assert')
44
const net = require('node:net')
55
const http = require('node:http')
66
const util = require('../core/util.js')
7+
const { ClientStats } = require('../util/stats.js')
78
const { channels } = require('../core/diagnostics.js')
89
const Request = require('../core/request.js')
910
const DispatcherBase = require('./dispatcher-base')
@@ -260,6 +261,10 @@ class Client extends DispatcherBase {
260261
this[kResume](true)
261262
}
262263

264+
get stats () {
265+
return new ClientStats(this)
266+
}
267+
263268
get [kPending] () {
264269
return this[kQueue].length - this[kPendingIdx]
265270
}

lib/dispatcher/pool-base.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict'
22

3+
const { PoolStats } = require('../util/stats.js')
34
const DispatcherBase = require('./dispatcher-base')
45
const FixedQueue = require('./fixed-queue')
56
const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols')
6-
const PoolStats = require('./pool-stats')
77

88
const kClients = Symbol('clients')
99
const kNeedDrain = Symbol('needDrain')
@@ -16,7 +16,6 @@ const kOnConnectionError = Symbol('onConnectionError')
1616
const kGetDispatcher = Symbol('get dispatcher')
1717
const kAddClient = Symbol('add client')
1818
const kRemoveClient = Symbol('remove client')
19-
const kStats = Symbol('stats')
2019

2120
class PoolBase extends DispatcherBase {
2221
constructor () {
@@ -67,8 +66,6 @@ class PoolBase extends DispatcherBase {
6766
this[kOnConnectionError] = (origin, targets, err) => {
6867
pool.emit('connectionError', origin, [pool, ...targets], err)
6968
}
70-
71-
this[kStats] = new PoolStats(this)
7269
}
7370

7471
get [kBusy] () {
@@ -108,7 +105,7 @@ class PoolBase extends DispatcherBase {
108105
}
109106

110107
get stats () {
111-
return this[kStats]
108+
return new PoolStats(this)
112109
}
113110

114111
async [kClose] () {

lib/dispatcher/pool-stats.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

lib/util/stats.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict'
2+
3+
const {
4+
kConnected,
5+
kPending,
6+
kRunning,
7+
kSize,
8+
kFree,
9+
kQueued
10+
} = require('../core/symbols')
11+
12+
class ClientStats {
13+
constructor (client) {
14+
this.connected = client[kConnected]
15+
this.pending = client[kPending]
16+
this.running = client[kRunning]
17+
this.size = client[kSize]
18+
}
19+
}
20+
21+
class PoolStats {
22+
constructor (pool) {
23+
this.connected = pool[kConnected]
24+
this.free = pool[kFree]
25+
this.pending = pool[kPending]
26+
this.queued = pool[kQueued]
27+
this.running = pool[kRunning]
28+
this.size = pool[kSize]
29+
}
30+
}
31+
32+
module.exports = { ClientStats, PoolStats }

test/client.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,3 +2166,33 @@ test('\\n in Method', async (t) => {
21662166
t.strictEqual(err.message, 'invalid request method')
21672167
})
21682168
})
2169+
2170+
test('stats', async (t) => {
2171+
t = tspl(t, { plan: 3 })
2172+
2173+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
2174+
t.strictEqual('/', req.url)
2175+
t.strictEqual('GET', req.method)
2176+
t.strictEqual(`localhost:${server.address().port}`, req.headers.host)
2177+
res.setHeader('Content-Type', 'text/plain')
2178+
res.end('hello')
2179+
})
2180+
after(() => server.close())
2181+
2182+
server.listen(0, () => {
2183+
const client = new Client(`http://localhost:${server.address().port}`)
2184+
after(() => client.close())
2185+
2186+
client.request({
2187+
path: '/',
2188+
method: 'GET'
2189+
}, (err, data) => {
2190+
t.ifError(err)
2191+
t.strictEqual(client.stats.connected, true)
2192+
t.strictEqual(client.stats.pending, 1)
2193+
t.strictEqual(client.stats.running, 1)
2194+
})
2195+
})
2196+
2197+
await t.completed
2198+
})

test/node-test/agent.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,3 +808,40 @@ test('the dispatcher is truly global', t => {
808808
assert.ok(require.resolve('../../index.js') in require.cache)
809809
assert.strictEqual(agent, undiciFresh.getGlobalDispatcher())
810810
})
811+
812+
test('stats', async t => {
813+
const p = tspl(t, { plan: 7 })
814+
const wanted = 'payload'
815+
816+
const server = http.createServer({ joinDuplicateHeaders: true }, (req, res) => {
817+
p.strictEqual('/', req.url)
818+
p.strictEqual('GET', req.method)
819+
res.end(wanted)
820+
})
821+
822+
t.after(closeServerAsPromise(server))
823+
824+
const dispatcher = new Agent({
825+
connect: {
826+
servername: 'agent1'
827+
}
828+
})
829+
830+
server.listen(0, () => {
831+
request(`http://localhost:${server.address().port}`, { dispatcher })
832+
.then(({ statusCode, headers, body }) => {
833+
p.strictEqual(statusCode, 200)
834+
const originForStats = `http://localhost:${server.address().port}`
835+
const agentStats = dispatcher.stats[originForStats]
836+
p.strictEqual(agentStats.connected, 1)
837+
p.strictEqual(agentStats.pending, 0)
838+
p.strictEqual(agentStats.running, 0)
839+
p.strictEqual(agentStats.size, 0)
840+
})
841+
.catch(err => {
842+
p.fail(err)
843+
})
844+
})
845+
846+
await p.completed
847+
})

test/pool.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,3 +1150,35 @@ test('pool destroy fails queued requests', async (t) => {
11501150
})
11511151
await t.completed
11521152
})
1153+
1154+
test('stats', async (t) => {
1155+
t = tspl(t, { plan: 11 })
1156+
1157+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
1158+
t.strictEqual('/', req.url)
1159+
t.strictEqual('GET', req.method)
1160+
res.setHeader('content-type', 'text/plain')
1161+
res.end('hello')
1162+
})
1163+
after(() => server.close())
1164+
1165+
server.listen(0, async () => {
1166+
const client = new Pool(`http://localhost:${server.address().port}`)
1167+
after(() => client.destroy())
1168+
1169+
t.strictEqual(client[kUrl].origin, `http://localhost:${server.address().port}`)
1170+
1171+
client.request({ path: '/', method: 'GET' }, (err, { statusCode, headers, body }) => {
1172+
t.ifError(err)
1173+
t.strictEqual(statusCode, 200)
1174+
t.strictEqual(client.stats.connected, 1)
1175+
t.strictEqual(client.stats.free, 0)
1176+
t.strictEqual(client.stats.pending, 0)
1177+
t.strictEqual(client.stats.queued, 0)
1178+
t.strictEqual(client.stats.running, 1)
1179+
t.strictEqual(client.stats.size, 1)
1180+
})
1181+
})
1182+
1183+
await t.completed
1184+
})

0 commit comments

Comments
 (0)