main
ts 376 lines 11.2 KB
Raw
1 /**
2 * @packageDocumentation
3 *
4 * Configure your browser-based libp2p node with DevTools metrics:
5 *
6 * ```typescript
7 * import { createLibp2p } from 'libp2p'
8 * import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
9 *
10 * const node = await createLibp2p({
11 * metrics: inspectorMetrics()
12 * })
13 * ```
14 *
15 * Then use the [DevTools plugin](https://github.com/ipfs-shipyard/js-libp2p-devtools)
16 * for Chrome or Firefox to inspect the state of your running node.
17 */
18
19 import { serviceCapabilities, start, stop } from '@libp2p/interface'
20 import { simpleMetrics } from '@libp2p/simple-metrics'
21 import { pipe } from 'it-pipe'
22 import { pushable } from 'it-pushable'
23 import { rpc } from 'it-rpc'
24 import { base64 } from 'multiformats/bases/base64'
25 import manifest from '../package.json' with { type: 'json' }
26 import { messages } from './messages/index.js'
27 import { valueCodecs } from './rpc/index.js'
28 import { metricsRpc } from './rpc/rpc.js'
29 import { debounce } from './utils/debounce.js'
30 import { getPeers } from './utils/get-peers.js'
31 import { getSelf } from './utils/get-self.js'
32 import type { Messages } from './messages/index.js'
33 import type { InspectorRPC } from './rpc/index.js'
34 import type { ComponentLogger, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, PeerStore, Stream, ContentRouting, PeerRouting, TypedEventTarget, Startable, NodeInfo } from '@libp2p/interface'
35 import type { TransportManager, Registrar, ConnectionManager, AddressManager } from '@libp2p/interface-internal'
36 import type { Pushable } from 'it-pushable'
37 import type { RPC } from 'it-rpc'
38
39 export * from './rpc/index.js'
40
41 export const SOURCE_CLIENT = '@ipshipyard/libp2p-inspector-metrics:client'
42 export const SOURCE_METRICS = '@ipshipyard/libp2p-inspector-metrics:metrics'
43 export const LIBP2P_INSPECTOR_METRICS_KEY = '________ipshipyard_libp2p_inspector_metrics'
44
45 // let inspector know we are here
46 Object.defineProperty(globalThis, LIBP2P_INSPECTOR_METRICS_KEY, {
47 value: true,
48 enumerable: false,
49 writable: false
50 })
51
52 // don't wait for inspector RPC forever
53 const RPC_TIMEOUT = 10_000
54
55 /**
56 * Sent by the client to discover basic node info & versions
57 */
58 export interface IdentifyMessage {
59 source: typeof SOURCE_CLIENT
60 type: 'libp2p-identify'
61 }
62
63 /**
64 * Sent to the client to let it know basic node info & versions
65 */
66 export interface IdentifyResponse {
67 source: typeof SOURCE_METRICS
68 type: 'libp2p-identify',
69 name: string
70 version: string
71 userAgent: string
72 inspector: string
73 }
74
75 /**
76 * Invoke a method on the libp2p object
77 */
78 export interface RPCMessage {
79 source: typeof SOURCE_CLIENT | typeof SOURCE_METRICS
80 type: 'libp2p-rpc'
81
82 /**
83 * The RPC message encoded as a multibase string
84 */
85 message: string
86 }
87
88 /**
89 * Messages that are sent from the inspector to the client
90 */
91 export type InspectorMessage = IdentifyResponse | RPCMessage
92
93 /**
94 * Messages that are sent from the client to the inspector
95 */
96 export type ClientMessage = IdentifyMessage | RPCMessage
97
98 export interface Address {
99 /**
100 * The multiaddr this address represents
101 */
102 multiaddr: string
103
104 /**
105 * If `true`, this multiaddr came from a signed peer record
106 */
107 isCertified?: boolean
108
109 /**
110 * If `true`, the current node has an active connection to this peer via this
111 * address
112 */
113 isConnected?: boolean
114 }
115
116 export interface InspectorMetricsInit {
117 /**
118 * How often to pass metrics to the DevTools panel
119 */
120 intervalMs?: number
121
122 /**
123 * When used under Node.js this is the mDNS service tag that is advertised
124 *
125 * @default '_libp2p_inspector_metrics._tcp.local'
126 */
127 serviceTag?: string
128
129 /**
130 * How to accept/publish RPC messages.
131 *
132 * `window` - uses the [window.postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
133 * API so communicate with a browser plugin (only available in browsers).
134 *
135 * `mdns` - start a TCP socket and advertise it to local connections via the
136 * `_libp2p_inspector_metrics._tcp.local` service tag (not available in
137 * browsers)
138 *
139 * `libp2p` - accept incoming libp2p streams with the `/libp2p/devtools/1.0.0`
140 * protocol
141 *
142 * Defaults to `window` in browsers and `mdns` in Node.js
143 *
144 * @default 'window|mdns'
145 */
146 rpc?: 'mdns' | 'window' | 'libp2p'
147 }
148
149 export interface InspectorMetricsComponents {
150 logger: ComponentLogger
151 events: TypedEventTarget<Libp2pEvents>
152 peerId: PeerId
153 transportManager: TransportManager
154 registrar: Registrar
155 connectionManager: ConnectionManager
156 peerStore: PeerStore
157 nodeInfo: NodeInfo
158 contentRouting: ContentRouting
159 peerRouting: PeerRouting
160 addressManager: AddressManager
161 }
162
163 class InspectorMetrics implements Metrics, Startable {
164 private readonly log: Logger
165 private readonly components: InspectorMetricsComponents
166 private readonly simpleMetrics: Metrics
167 private readonly intervalMs?: number
168 private readonly rpcQueue: Pushable<Uint8Array>
169 private readonly rpc: RPC
170 private readonly inspector: InspectorRPC
171 private readonly messages: Messages
172
173 constructor (components: InspectorMetricsComponents, init: InspectorMetricsInit = {}) {
174 this.log = components.logger.forComponent('libp2p:devtools-metrics')
175 this.intervalMs = init?.intervalMs
176 this.components = components
177
178 // create RPC endpoint
179 this.rpcQueue = pushable()
180 this.rpc = rpc({
181 valueCodecs
182 })
183 this.inspector = this.rpc.createClient('inspector', {
184 timeout: RPC_TIMEOUT
185 })
186 this.messages = messages(components, init)
187
188 // collect information on current peers and sent it to the dev tools panel
189 this.onPeersUpdate = debounce(this.onPeersUpdate.bind(this), 1000)
190 this.onSelfUpdate = debounce(this.onSelfUpdate.bind(this), 1000)
191 this.onIncomingMessage = this.onIncomingMessage.bind(this)
192
193 // collect metrics
194 this.simpleMetrics = simpleMetrics({
195 intervalMs: this.intervalMs,
196 onMetrics: (metrics) => {
197 this.inspector.safeDispatchEvent('metrics', {
198 detail: metrics
199 }).catch(err => {
200 this.log.error('error sending metrics', err)
201 })
202 }
203 })(components)
204 }
205
206 readonly [Symbol.toStringTag] = '@ipshipyard/libp2p-inspector-metrics'
207
208 readonly [serviceCapabilities]: string[] = [
209 '@libp2p/metrics'
210 ]
211
212 trackMultiaddrConnection (maConn: MultiaddrConnection): void {
213 this.simpleMetrics.trackMultiaddrConnection(maConn)
214 }
215
216 trackProtocolStream (stream: Stream): void {
217 this.simpleMetrics.trackProtocolStream(stream)
218 }
219
220 registerMetric (name: any, options: any): any {
221 return this.simpleMetrics.registerMetric(name, options)
222 }
223
224 registerMetricGroup (name: any, options: any): any {
225 return this.simpleMetrics.registerMetricGroup(name, options)
226 }
227
228 registerCounter (name: any, options: any): any {
229 return this.simpleMetrics.registerCounter(name, options)
230 }
231
232 registerCounterGroup (name: any, options: any): any {
233 return this.simpleMetrics.registerCounterGroup(name, options)
234 }
235
236 registerHistogram (name: any, options: any): any {
237 return this.simpleMetrics.registerHistogram(name, options)
238 }
239
240 registerHistogramGroup (name: any, options: any): any {
241 return this.simpleMetrics.registerHistogramGroup(name, options)
242 }
243
244 registerSummary (name: any, options: any): any {
245 return this.simpleMetrics.registerSummary(name, options)
246 }
247
248 registerSummaryGroup (name: any, options: any): any {
249 return this.simpleMetrics.registerSummaryGroup(name, options)
250 }
251
252 createTrace (): any {
253 return this.simpleMetrics.createTrace()
254 }
255
256 traceFunction <T extends (...args: any[]) => any> (name: string, fn: T, options?: any): T {
257 return this.simpleMetrics.traceFunction(name, fn, options)
258 }
259
260 async start (): Promise<void> {
261 // send peer updates
262 this.components.events.addEventListener('peer:connect', this.onPeersUpdate)
263 this.components.events.addEventListener('peer:disconnect', this.onPeersUpdate)
264 this.components.events.addEventListener('peer:identify', this.onPeersUpdate)
265 this.components.events.addEventListener('peer:update', this.onPeersUpdate)
266
267 // send node status updates
268 this.components.events.addEventListener('self:peer:update', this.onSelfUpdate)
269
270 // process incoming messages from devtools
271 this.messages.addEventListener('message', this.onIncomingMessage)
272
273 // create rpc target
274 this.rpc.createTarget('metrics', metricsRpc(this.components))
275
276 // start metrics
277 await start(this.simpleMetrics, this.messages)
278
279 // send RPC messages
280 Promise.resolve()
281 .then(async () => {
282 await pipe(
283 this.rpcQueue,
284 this.rpc,
285 async source => {
286 for await (const buf of source) {
287 this.messages.postMessage({
288 source: SOURCE_METRICS,
289 type: 'libp2p-rpc',
290 message: base64.encode(buf)
291 })
292 }
293 }
294 )
295 })
296 .catch(err => {
297 this.log.error('error while reading RPC messages', err)
298 })
299 }
300
301 async stop (): Promise<void> {
302 this.messages.removeEventListener('message', this.onIncomingMessage)
303 this.components.events.removeEventListener('self:peer:update', this.onSelfUpdate)
304 this.components.events.removeEventListener('peer:connect', this.onPeersUpdate)
305 this.components.events.removeEventListener('peer:disconnect', this.onPeersUpdate)
306 this.components.events.removeEventListener('peer:identify', this.onPeersUpdate)
307 this.components.events.removeEventListener('peer:update', this.onPeersUpdate)
308 await stop(this.simpleMetrics, this.messages)
309 }
310
311 private onIncomingMessage (event: MessageEvent<ClientMessage>): void {
312 // Only accept messages from same frame
313 // @ts-expect-error types are wonky
314 if (event.source !== this.messages) {
315 return
316 }
317
318 const message = event.data
319
320 // Only accept messages of correct format (our messages)
321 if (message?.source !== SOURCE_CLIENT) {
322 return
323 }
324
325 // respond to identify message without invoking RPC since it will be used by
326 // the client to understand what version of RPC we support
327
328 if (message.type === 'libp2p-identify') {
329 // @ts-expect-error wat
330 this.messages.postMessage({
331 source: SOURCE_METRICS,
332 type: 'libp2p-identify',
333 name: this.components.nodeInfo.name,
334 version: this.components.nodeInfo.version,
335 userAgent: this.components.nodeInfo.userAgent,
336 inspector: manifest.version
337 })
338
339 return
340 }
341
342 if (message.type === 'libp2p-rpc') {
343 this.rpcQueue.push(base64.decode(message.message))
344 }
345 }
346
347 private onSelfUpdate (): void {
348 Promise.resolve()
349 .then(async () => {
350 await this.inspector.safeDispatchEvent('self', {
351 detail: await getSelf(this.components)
352 })
353 })
354 .catch(err => {
355 this.log.error('error sending peers message', err)
356 })
357 }
358
359 private onPeersUpdate (): void {
360 Promise.resolve()
361 .then(async () => {
362 await this.inspector.safeDispatchEvent('peers', {
363 detail: await getPeers(this.components, this.log)
364 })
365 })
366 .catch(err => {
367 this.log.error('error sending peers message', err)
368 })
369 }
370 }
371
372 export function inspectorMetrics (init?: Partial<InspectorMetricsInit>): (components: InspectorMetricsComponents) => Metrics {
373 return (components) => {
374 return new InspectorMetrics(components, init)
375 }
376 }