Skip to content
This repository was archived by the owner on Aug 29, 2023. It is now read-only.

Commit c004357

Browse files
authored
feat!: add metrics (#223)
Uses new metrics interface from libp2p/js-libp2p-interfaces#310 to report useful connection metrics. Similar to #217 but it adds the listening host/port to the metrics name to allow multiple TCP listeners to report metrics separately. BREAKING CHANGE: requires metrics interface v4
1 parent 73240c4 commit c004357

File tree

4 files changed

+113
-13
lines changed

4 files changed

+113
-13
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
"stream-to-it": "^0.2.2"
149149
},
150150
"devDependencies": {
151+
"@libp2p/interface-metrics": "^4.0.0",
151152
"@libp2p/interface-mocks": "^7.0.1",
152153
"@libp2p/interface-transport-compliance-tests": "^3.0.0",
153154
"aegir": "^37.5.3",

src/index.ts

+38-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CreateListenerOptions, DialOptions, Listener, symbol, Transport } from
1111
import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
1212
import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net'
1313
import type { Connection } from '@libp2p/interface-connection'
14+
import type { CounterGroup, Metrics } from '@libp2p/interface-metrics'
1415

1516
const log = logger('libp2p:tcp')
1617

@@ -55,11 +56,36 @@ export interface TCPCreateListenerOptions extends CreateListenerOptions, TCPSock
5556

5657
}
5758

59+
export interface TCPComponents {
60+
metrics?: Metrics
61+
}
62+
63+
export interface TCPMetrics {
64+
dialerEvents: CounterGroup
65+
listenerEvents: CounterGroup
66+
}
67+
5868
class TCP implements Transport {
5969
private readonly opts: TCPOptions
70+
private readonly metrics?: TCPMetrics
71+
private readonly components: TCPComponents
6072

61-
constructor (options: TCPOptions = {}) {
73+
constructor (components: TCPComponents, options: TCPOptions = {}) {
6274
this.opts = options
75+
this.components = components
76+
77+
if (components.metrics != null) {
78+
this.metrics = {
79+
dialerEvents: components.metrics.registerCounterGroup('libp2p_tcp_dialer_errors_total', {
80+
label: 'event',
81+
help: 'Total count of TCP dialer errors by error type'
82+
}),
83+
listenerEvents: components.metrics.registerCounterGroup('libp2p_tcp_listener_errors_total', {
84+
label: 'event',
85+
help: 'Total count of TCP listener errors by error type'
86+
})
87+
}
88+
}
6389
}
6490

6591
get [symbol] (): true {
@@ -84,7 +110,8 @@ class TCP implements Transport {
84110
remoteAddr: ma,
85111
signal: options.signal,
86112
socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
87-
socketCloseTimeout: this.opts.socketCloseTimeout
113+
socketCloseTimeout: this.opts.socketCloseTimeout,
114+
metrics: this.metrics?.dialerEvents
88115
})
89116
log('new outbound connection %s', maConn.remoteAddr)
90117
const conn = await options.upgrader.upgradeOutbound(maConn)
@@ -107,12 +134,14 @@ class TCP implements Transport {
107134

108135
const onError = (err: Error) => {
109136
err.message = `connection error ${cOptsStr}: ${err.message}`
137+
this.metrics?.dialerEvents.increment({ error: true })
110138

111139
done(err)
112140
}
113141

114142
const onTimeout = () => {
115143
log('connection timeout %s', cOptsStr)
144+
this.metrics?.dialerEvents.increment({ timeout: true })
116145

117146
const err = errCode(new Error(`connection timeout after ${Date.now() - start}ms`), 'ERR_CONNECT_TIMEOUT')
118147
// Note: this will result in onError() being called
@@ -121,11 +150,13 @@ class TCP implements Transport {
121150

122151
const onConnect = () => {
123152
log('connection opened %j', cOpts)
153+
this.metrics?.dialerEvents.increment({ connect: true })
124154
done()
125155
}
126156

127157
const onAbort = () => {
128158
log('connection aborted %j', cOpts)
159+
this.metrics?.dialerEvents.increment({ abort: true })
129160
rawSocket.destroy()
130161
done(new AbortError())
131162
}
@@ -166,7 +197,8 @@ class TCP implements Transport {
166197
...options,
167198
maxConnections: this.opts.maxConnections,
168199
socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout,
169-
socketCloseTimeout: this.opts.socketCloseTimeout
200+
socketCloseTimeout: this.opts.socketCloseTimeout,
201+
metrics: this.components.metrics
170202
})
171203
}
172204

@@ -190,8 +222,8 @@ class TCP implements Transport {
190222
}
191223
}
192224

193-
export function tcp (init: TCPOptions = {}): (components?: any) => Transport {
194-
return () => {
195-
return new TCP(init)
225+
export function tcp (init: TCPOptions = {}): (components?: TCPComponents) => Transport {
226+
return (components: TCPComponents = {}) => {
227+
return new TCP(components, init)
196228
}
197229
}

src/listener.ts

+67-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { MultiaddrConnection, Connection } from '@libp2p/interface-connecti
1111
import type { Upgrader, Listener, ListenerEvents } from '@libp2p/interface-transport'
1212
import type { Multiaddr } from '@multiformats/multiaddr'
1313
import type { TCPCreateListenerOptions } from './index.js'
14+
import type { CounterGroup, Metric, Metrics } from '@libp2p/interface-metrics'
1415

1516
const log = logger('libp2p:tcp:listener')
1617

@@ -31,6 +32,16 @@ interface Context extends TCPCreateListenerOptions {
3132
socketInactivityTimeout?: number
3233
socketCloseTimeout?: number
3334
maxConnections?: number
35+
metrics?: Metrics
36+
}
37+
38+
const SERVER_STATUS_UP = 1
39+
const SERVER_STATUS_DOWN = 0
40+
41+
export interface TCPListenerMetrics {
42+
status: Metric
43+
errors: CounterGroup
44+
events: CounterGroup
3445
}
3546

3647
type Status = {started: false} | {started: true, listeningAddr: Multiaddr, peerId: string | null }
@@ -39,8 +50,8 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
3950
private readonly server: net.Server
4051
/** Keep track of open connections to destroy in case of timeout */
4152
private readonly connections = new Set<MultiaddrConnection>()
42-
4353
private status: Status = { started: false }
54+
private metrics?: TCPListenerMetrics
4455

4556
constructor (private readonly context: Context) {
4657
super()
@@ -57,26 +68,75 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
5768
}
5869

5970
this.server
60-
.on('listening', () => this.dispatchEvent(new CustomEvent('listening')))
61-
.on('error', err => this.dispatchEvent(new CustomEvent<Error>('error', { detail: err })))
62-
.on('close', () => this.dispatchEvent(new CustomEvent('close')))
71+
.on('listening', () => {
72+
if (context.metrics != null) {
73+
// we are listening, register metrics for our port
74+
const address = this.server.address()
75+
let addr: string
76+
77+
if (address == null) {
78+
addr = 'unknown'
79+
} else if (typeof address === 'string') {
80+
// unix socket
81+
addr = address
82+
} else {
83+
addr = `${address.address}:${address.port}`
84+
}
85+
86+
context.metrics?.registerMetric(`libp2p_tcp_connections_${addr}_count`, {
87+
help: 'Current active connections in TCP listener',
88+
calculate: () => {
89+
return this.connections.size
90+
}
91+
})
92+
93+
this.metrics = {
94+
status: context.metrics.registerMetric(`libp2p_tcp_${addr}_server_status`, {
95+
help: 'Current status of the TCP server'
96+
}),
97+
errors: context.metrics.registerCounterGroup(`libp2p_tcp_${addr}_server_errors_total`, {
98+
label: 'error',
99+
help: 'Total count of TCP listener errors by error type'
100+
}),
101+
events: context.metrics.registerCounterGroup(`libp2p_tcp_$${addr}_socket_events`, {
102+
label: 'event',
103+
help: 'Total count of TCP socket events by event'
104+
})
105+
}
106+
107+
this.metrics?.status.update(SERVER_STATUS_UP)
108+
}
109+
110+
this.dispatchEvent(new CustomEvent('listening'))
111+
})
112+
.on('error', err => {
113+
this.metrics?.errors.increment({ listen_error: true })
114+
this.dispatchEvent(new CustomEvent<Error>('error', { detail: err }))
115+
})
116+
.on('close', () => {
117+
this.metrics?.status.update(SERVER_STATUS_DOWN)
118+
this.dispatchEvent(new CustomEvent('close'))
119+
})
63120
}
64121

65122
private onSocket (socket: net.Socket) {
66123
// Avoid uncaught errors caused by unstable connections
67124
socket.on('error', err => {
68125
log('socket error', err)
126+
this.metrics?.events.increment({ error: true })
69127
})
70128

71129
let maConn: MultiaddrConnection
72130
try {
73131
maConn = toMultiaddrConnection(socket, {
74132
listeningAddr: this.status.started ? this.status.listeningAddr : undefined,
75133
socketInactivityTimeout: this.context.socketInactivityTimeout,
76-
socketCloseTimeout: this.context.socketCloseTimeout
134+
socketCloseTimeout: this.context.socketCloseTimeout,
135+
metrics: this.metrics?.events
77136
})
78137
} catch (err) {
79138
log.error('inbound connection failed', err)
139+
this.metrics?.errors.increment({ inbound_to_connection: true })
80140
return
81141
}
82142

@@ -99,6 +159,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
99159
})
100160
.catch(async err => {
101161
log.error('inbound connection failed', err)
162+
this.metrics?.errors.increment({ inbound_upgrade: true })
102163

103164
await attemptClose(maConn)
104165
})
@@ -111,6 +172,7 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
111172
attemptClose(maConn)
112173
.catch(err => {
113174
log.error('closing inbound connection failed', err)
175+
this.metrics?.errors.increment({ inbound_closing_failed: true })
114176
})
115177
}
116178
}

src/socket-to-conn.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import errCode from 'err-code'
99
import type { Socket } from 'net'
1010
import type { Multiaddr } from '@multiformats/multiaddr'
1111
import type { MultiaddrConnection } from '@libp2p/interface-connection'
12+
import type { CounterGroup } from '@libp2p/interface-metrics'
1213

1314
const log = logger('libp2p:tcp:socket')
1415

@@ -19,14 +20,15 @@ interface ToConnectionOptions {
1920
signal?: AbortSignal
2021
socketInactivityTimeout?: number
2122
socketCloseTimeout?: number
23+
metrics?: CounterGroup
2224
}
2325

2426
/**
2527
* Convert a socket into a MultiaddrConnection
2628
* https://github.com/libp2p/interface-transport#multiaddrconnection
2729
*/
28-
export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOptions) => {
29-
options = options ?? {}
30+
export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions) => {
31+
const metrics = options.metrics
3032
const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT
3133
const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT
3234

@@ -61,6 +63,7 @@ export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOpti
6163
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
6264
socket.setTimeout(inactivityTimeout, () => {
6365
log('%s socket read timeout', lOptsStr)
66+
metrics?.increment({ timeout: true })
6467

6568
// only destroy with an error if the remote has not sent the FIN message
6669
let err: Error | undefined
@@ -75,6 +78,7 @@ export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOpti
7578

7679
socket.once('close', () => {
7780
log('%s socket read timeout', lOptsStr)
81+
metrics?.increment({ close: true })
7882

7983
// In instances where `close` was not explicitly called,
8084
// such as an iterable stream ending, ensure we have set the close
@@ -88,6 +92,7 @@ export const toMultiaddrConnection = (socket: Socket, options?: ToConnectionOpti
8892
// the remote sent a FIN packet which means no more data will be sent
8993
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
9094
log('socket ended', maConn.remoteAddr.toString())
95+
metrics?.increment({ end: true })
9196
})
9297

9398
const maConn: MultiaddrConnection = {

0 commit comments

Comments
 (0)