| 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 | } |