diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 02b6bdb480..d38cac43d1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1 +1 @@
-{"packages/auto-tls":"1.0.2","packages/config":"1.0.0","packages/connection-encrypter-plaintext":"2.0.12","packages/connection-encrypter-tls":"2.0.12","packages/crypto":"5.0.8","packages/interface":"2.3.0","packages/interface-compliance-tests":"6.2.2","packages/interface-internal":"2.2.1","packages/kad-dht":"14.1.6","packages/keychain":"5.0.11","packages/libp2p":"2.4.2","packages/logger":"5.1.5","packages/metrics-devtools":"1.1.12","packages/metrics-prometheus":"4.2.10","packages/metrics-simple":"1.2.8","packages/multistream-select":"6.0.10","packages/peer-collections":"6.0.13","packages/peer-discovery-bootstrap":"11.0.16","packages/peer-discovery-mdns":"11.0.16","packages/peer-id":"5.0.9","packages/peer-record":"8.0.13","packages/peer-store":"11.0.13","packages/pnet":"2.0.16","packages/protocol-autonat":"2.0.15","packages/protocol-dcutr":"2.0.14","packages/protocol-echo":"2.1.5","packages/protocol-fetch":"2.0.14","packages/protocol-identify":"3.0.14","packages/protocol-perf":"4.0.16","packages/protocol-ping":"2.0.14","packages/pubsub":"10.0.14","packages/pubsub-floodsub":"10.1.14","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"11.0.16","packages/transport-circuit-relay-v2":"3.1.6","packages/transport-memory":"1.0.2","packages/transport-tcp":"10.0.14","packages/transport-webrtc":"5.0.22","packages/transport-websockets":"9.1.1","packages/transport-webtransport":"5.0.21","packages/upnp-nat":"3.0.3","packages/utils":"6.3.0"}
+{"packages/auto-tls":"1.0.2","packages/config":"1.0.0","packages/connection-encrypter-plaintext":"2.0.12","packages/connection-encrypter-tls":"2.0.12","packages/crypto":"5.0.8","packages/interface":"2.3.0","packages/interface-compliance-tests":"6.2.2","packages/interface-internal":"2.2.1","packages/kad-dht":"14.1.6","packages/keychain":"5.0.11","packages/libp2p":"2.4.2","packages/logger":"5.1.5","packages/metrics-devtools":"1.1.12","packages/metrics-opentelemetry":"0.0.0","packages/metrics-prometheus":"4.2.10","packages/metrics-simple":"1.2.8","packages/multistream-select":"6.0.10","packages/peer-collections":"6.0.13","packages/peer-discovery-bootstrap":"11.0.16","packages/peer-discovery-mdns":"11.0.16","packages/peer-id":"5.0.9","packages/peer-record":"8.0.13","packages/peer-store":"11.0.13","packages/pnet":"2.0.16","packages/protocol-autonat":"2.0.15","packages/protocol-dcutr":"2.0.14","packages/protocol-echo":"2.1.5","packages/protocol-fetch":"2.0.14","packages/protocol-identify":"3.0.14","packages/protocol-perf":"4.0.16","packages/protocol-ping":"2.0.14","packages/pubsub":"10.0.14","packages/pubsub-floodsub":"10.1.14","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"11.0.16","packages/transport-circuit-relay-v2":"3.1.6","packages/transport-memory":"1.0.2","packages/transport-tcp":"10.0.14","packages/transport-webrtc":"5.0.22","packages/transport-websockets":"9.1.1","packages/transport-webtransport":"5.0.21","packages/upnp-nat":"3.0.3","packages/utils":"6.3.0"}
diff --git a/.release-please.json b/.release-please.json
index 4ccf28091e..4ed6957a83 100644
--- a/.release-please.json
+++ b/.release-please.json
@@ -22,6 +22,7 @@
"packages/libp2p": {},
"packages/logger": {},
"packages/metrics-devtools": {},
+ "packages/metrics-opentelemetry": {},
"packages/metrics-prometheus": {},
"packages/metrics-simple": {},
"packages/multistream-select": {},
diff --git a/packages/metrics-opentelemetry/CODE_OF_CONDUCT.md b/packages/metrics-opentelemetry/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..6b0fa54c54
--- /dev/null
+++ b/packages/metrics-opentelemetry/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Contributor Code of Conduct
+
+This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
diff --git a/packages/metrics-opentelemetry/LICENSE-APACHE b/packages/metrics-opentelemetry/LICENSE-APACHE
new file mode 100644
index 0000000000..b09cd7856d
--- /dev/null
+++ b/packages/metrics-opentelemetry/LICENSE-APACHE
@@ -0,0 +1,201 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/metrics-opentelemetry/LICENSE-MIT b/packages/metrics-opentelemetry/LICENSE-MIT
new file mode 100644
index 0000000000..72dc60d84b
--- /dev/null
+++ b/packages/metrics-opentelemetry/LICENSE-MIT
@@ -0,0 +1,19 @@
+The MIT License (MIT)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/metrics-opentelemetry/README.md b/packages/metrics-opentelemetry/README.md
new file mode 100644
index 0000000000..50e323c09d
--- /dev/null
+++ b/packages/metrics-opentelemetry/README.md
@@ -0,0 +1,85 @@
+# @libp2p/opentelemetry-metrics
+
+[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
+[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
+[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p)
+[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amain)
+
+> Opentelemetry metrics gathering for libp2p
+
+# About
+
+
+
+Uses [OpenTelemetry](https://opentelemetry.io/) to store metrics and method
+traces in libp2p.
+
+## Example - Node.js
+
+Use with [OpenTelemetry Desktop Viewer](https://github.com/CtrlSpice/otel-desktop-viewer):
+
+```ts
+import { createLibp2p } from 'libp2p'
+import { openTelemetryMetrics } from '@libp2p/opentelemetry-metrics'
+import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
+import { NodeSDK } from '@opentelemetry/sdk-node'
+
+const sdk = new NodeSDK({
+ traceExporter: new OTLPTraceExporter({
+ url: 'http://127.0.0.1:4318/v1/traces'
+ }),
+ metricReader: new PrometheusExporter({
+ port: 9464
+ }),
+ serviceName: 'my-app'
+})
+sdk.start()
+
+const node = await createLibp2p({
+ // ... other options
+ metrics: openTelemetryMetrics()
+})
+```
+
+# Install
+
+```console
+$ npm i @libp2p/simple-metrics
+```
+
+## Browser `
+```
+
+# API Docs
+
+-
+
+# License
+
+Licensed under either of
+
+- Apache 2.0, ([LICENSE-APACHE](https://github.com/libp2p/js-libp2p/blob/main/packages/metrics-simple/LICENSE-APACHE) / )
+- MIT ([LICENSE-MIT](https://github.com/libp2p/js-libp2p/blob/main/packages/metrics-simple/LICENSE-MIT) / )
+
+# Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
diff --git a/packages/metrics-opentelemetry/package.json b/packages/metrics-opentelemetry/package.json
new file mode 100644
index 0000000000..99084cf684
--- /dev/null
+++ b/packages/metrics-opentelemetry/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "@libp2p/opentelemetry-metrics",
+ "version": "0.0.0",
+ "description": "Opentelemetry metrics gathering for libp2p",
+ "license": "Apache-2.0 OR MIT",
+ "homepage": "https://github.com/libp2p/js-libp2p/tree/main/packages/metrics-opentelemetry#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/libp2p/js-libp2p.git"
+ },
+ "bugs": {
+ "url": "https://github.com/libp2p/js-libp2p/issues"
+ },
+ "publishConfig": {
+ "access": "public",
+ "provenance": true
+ },
+ "type": "module",
+ "types": "./dist/src/index.d.ts",
+ "files": [
+ "src",
+ "dist",
+ "!dist/test",
+ "!**/*.tsbuildinfo"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/src/index.d.ts",
+ "import": "./dist/src/index.js"
+ }
+ },
+ "eslintConfig": {
+ "extends": "ipfs",
+ "parserOptions": {
+ "project": true,
+ "sourceType": "module"
+ }
+ },
+ "scripts": {
+ "clean": "aegir clean",
+ "lint": "aegir lint",
+ "dep-check": "aegir dep-check",
+ "doc-check": "aegir doc-check",
+ "build": "aegir build",
+ "test": "aegir test",
+ "test:node": "aegir test -t node --cov",
+ "test:electron-main": "aegir test -t electron-main"
+ },
+ "dependencies": {
+ "@libp2p/interface": "^2.3.0",
+ "@opentelemetry/api": "^1.9.0",
+ "it-foreach": "^2.1.1",
+ "it-stream-types": "^2.0.2"
+ },
+ "devDependencies": {
+ "aegir": "^45.0.5"
+ },
+ "browser": {
+ "./dist/src/system-metrics.js": "./dist/src/system-metrics.browser.js"
+ },
+ "sideEffects": false
+}
diff --git a/packages/metrics-opentelemetry/src/counter-group.ts b/packages/metrics-opentelemetry/src/counter-group.ts
new file mode 100644
index 0000000000..356c49df37
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/counter-group.ts
@@ -0,0 +1,38 @@
+import type { CounterGroup, StopTimer } from '@libp2p/interface'
+import type { UpDownCounter as OTelCounter } from '@opentelemetry/api'
+
+export class OpenTelemetryCounterGroup implements CounterGroup {
+ private readonly label: string
+ private readonly counter: OTelCounter
+
+ constructor (label: string, counter: OTelCounter) {
+ this.label = label
+ this.counter = counter
+ }
+
+ update (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ this.counter.add(value, {
+ [this.label]: key
+ })
+ })
+ }
+
+ increment (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ this.counter.add(value === true ? 1 : value, {
+ [this.label]: key
+ })
+ })
+ }
+
+ reset (): void {
+ // no-op
+ }
+
+ timer (key: string): StopTimer {
+ return () => {
+ // no-op
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/counter.ts b/packages/metrics-opentelemetry/src/counter.ts
new file mode 100644
index 0000000000..ab15babc93
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/counter.ts
@@ -0,0 +1,18 @@
+import type { Counter } from '@libp2p/interface'
+import type { Counter as OTelCounter } from '@opentelemetry/api'
+
+export class OpenTelemetryCounter implements Counter {
+ private readonly counter: OTelCounter
+
+ constructor (counter: OTelCounter) {
+ this.counter = counter
+ }
+
+ increment (value?: number): void {
+ this.counter.add(value ?? 1)
+ }
+
+ reset (): void {
+ // no-op
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/histogram-group.ts b/packages/metrics-opentelemetry/src/histogram-group.ts
new file mode 100644
index 0000000000..1586294902
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/histogram-group.ts
@@ -0,0 +1,34 @@
+import type { HistogramGroup, StopTimer } from '@libp2p/interface'
+import type { Histogram as OTelHistogram } from '@opentelemetry/api'
+
+export class OpenTelemetryHistogramGroup implements HistogramGroup {
+ private readonly label: string
+ private readonly histogram: OTelHistogram
+
+ constructor (label: string, histogram: OTelHistogram) {
+ this.label = label
+ this.histogram = histogram
+ }
+
+ observe (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ this.histogram.record(value, {
+ [this.label]: key
+ })
+ })
+ }
+
+ reset (): void {
+ this.histogram.record(0)
+ }
+
+ timer (key: string): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.histogram.record(Date.now() - start, {
+ [this.label]: key
+ })
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/histogram.ts b/packages/metrics-opentelemetry/src/histogram.ts
new file mode 100644
index 0000000000..9720d6f2cd
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/histogram.ts
@@ -0,0 +1,26 @@
+import type { Histogram, StopTimer } from '@libp2p/interface'
+import type { Histogram as OTelHistogram } from '@opentelemetry/api'
+
+export class OpenTelemetryHistogram implements Histogram {
+ private readonly histogram: OTelHistogram
+
+ constructor (histogram: OTelHistogram) {
+ this.histogram = histogram
+ }
+
+ observe (value: number): void {
+ this.histogram.record(value)
+ }
+
+ reset (): void {
+ this.histogram.record(0)
+ }
+
+ timer (): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.observe(Date.now() - start)
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/index.ts b/packages/metrics-opentelemetry/src/index.ts
new file mode 100644
index 0000000000..c0ec0035b0
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/index.ts
@@ -0,0 +1,557 @@
+/**
+ * @packageDocumentation
+ *
+ * Uses [OpenTelemetry](https://opentelemetry.io/) to store metrics and method
+ * traces in libp2p.
+ *
+ * @example Node.js
+ *
+ * Use with [OpenTelemetry Desktop Viewer](https://github.com/CtrlSpice/otel-desktop-viewer):
+ *
+ * ```ts
+ * import { createLibp2p } from 'libp2p'
+ * import { openTelemetryMetrics } from '@libp2p/opentelemetry-metrics'
+ * import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
+ * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
+ * import { NodeSDK } from '@opentelemetry/sdk-node'
+ *
+ * const sdk = new NodeSDK({
+ * traceExporter: new OTLPTraceExporter({
+ * url: 'http://127.0.0.1:4318/v1/traces'
+ * }),
+ * metricReader: new PrometheusExporter({
+ * port: 9464
+ * }),
+ * serviceName: 'my-app'
+ * })
+ * sdk.start()
+ *
+ * const node = await createLibp2p({
+ * // ... other options
+ * metrics: openTelemetryMetrics()
+ * })
+ * ```
+ */
+
+import { InvalidParametersError, serviceCapabilities } from '@libp2p/interface'
+import { trace, metrics, context, SpanStatusCode } from '@opentelemetry/api'
+import each from 'it-foreach'
+import { OpenTelemetryCounterGroup } from './counter-group.js'
+import { OpenTelemetryCounter } from './counter.js'
+import { OpenTelemetryHistogramGroup } from './histogram-group.js'
+import { OpenTelemetryHistogram } from './histogram.js'
+import { OpenTelemetryMetricGroup } from './metric-group.js'
+import { OpenTelemetryMetric } from './metric.js'
+import { OpenTelemetrySummaryGroup } from './summary-group.js'
+import { OpenTelemetrySummary } from './summary.js'
+import { collectSystemMetrics } from './system-metrics.js'
+import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, Metrics, CalculatedMetricOptions, MetricOptions, Counter, CounterGroup, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions, NodeInfo, TraceFunctionOptions, TraceGeneratorFunctionOptions, TraceAttributes } from '@libp2p/interface'
+import type { Span, Attributes } from '@opentelemetry/api'
+import type { Duplex } from 'it-stream-types'
+
+// see https://betterstack.com/community/guides/observability/opentelemetry-metrics-nodejs/#prerequisites
+
+export interface OpenTelemetryComponents {
+ nodeInfo: NodeInfo
+}
+
+export interface OpenTelemetryMetricsInit {
+ /**
+ * The app name used to create the tracer
+ *
+ * @default 'js-libp2p'
+ */
+ appName?: string
+
+ /**
+ * The app version used to create the tracer.
+ *
+ * The version number of the running version of libp2p is used as the default.
+ */
+ appVersion?: string
+
+ /**
+ * On Node.js platforms the current filesystem usage is reported as the metric
+ * `nodejs_fs_usage_bytes` using the `statfs` function from `node:fs` - the
+ * default location to stat is the current working directory, configured this
+ * location here
+ */
+ statfsLocation?: string
+
+ /**
+ * The meter name used for creating metrics
+ *
+ * @default 'js-libp2p'
+ */
+ meterName?: string
+}
+
+class OpenTelemetryMetrics implements Metrics {
+ private transferStats: Map
+ private readonly tracer: ReturnType
+ private readonly meterName: string
+
+ constructor (components: OpenTelemetryComponents, init?: OpenTelemetryMetricsInit) {
+ this.tracer = trace.getTracer(init?.appName ?? components.nodeInfo.name, init?.appVersion ?? components.nodeInfo.version)
+
+ // holds global and per-protocol sent/received stats
+ this.transferStats = new Map()
+ this.meterName = init?.meterName ?? components.nodeInfo.name
+
+ this.registerCounterGroup('libp2p_data_transfer_bytes_total', {
+ label: 'protocol',
+ calculate: () => {
+ const output: Record = {}
+
+ for (const [key, value] of this.transferStats.entries()) {
+ output[key] = value
+ }
+
+ // reset counts for next time
+ this.transferStats = new Map()
+
+ return output
+ }
+ })
+
+ collectSystemMetrics(this, init)
+ }
+
+ readonly [Symbol.toStringTag] = '@libp2p/metrics-opentelemetry'
+
+ readonly [serviceCapabilities]: string[] = [
+ '@libp2p/metrics'
+ ]
+
+ /**
+ * Increment the transfer stat for the passed key, making sure
+ * it exists first
+ */
+ _incrementValue (key: string, value: number): void {
+ const existing = this.transferStats.get(key) ?? 0
+
+ this.transferStats.set(key, existing + value)
+ }
+
+ /**
+ * Override the sink/source of the stream to count the bytes
+ * in and out
+ */
+ _track (stream: Duplex>, name: string): void {
+ const self = this
+
+ const sink = stream.sink
+ stream.sink = async function trackedSink (source) {
+ await sink(each(source, buf => {
+ self._incrementValue(`${name} sent`, buf.byteLength)
+ }))
+ }
+
+ const source = stream.source
+ stream.source = each(source, buf => {
+ self._incrementValue(`${name} received`, buf.byteLength)
+ })
+ }
+
+ trackMultiaddrConnection (maConn: MultiaddrConnection): void {
+ this._track(maConn, 'global')
+ }
+
+ trackProtocolStream (stream: Stream, connection: Connection): void {
+ if (stream.protocol == null) {
+ // protocol not negotiated yet, should not happen as the upgrader
+ // calls this handler after protocol negotiation
+ return
+ }
+
+ this._track(stream, stream.protocol)
+ }
+
+ registerMetric (name: string, opts: CalculatedMetricOptions): void
+ registerMetric (name: string, opts?: MetricOptions): Metric
+ registerMetric (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+
+ if (isCalculatedMetricOptions(opts)) {
+ const calculate = opts.calculate
+ const counter = meter.createObservableGauge(name, {
+ description: opts?.help ?? name
+ })
+ counter.addCallback(async (result) => {
+ result.observe(await calculate())
+ })
+
+ return
+ }
+
+ return new OpenTelemetryMetric(meter.createGauge(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerMetricGroup (name: string, opts: CalculatedMetricOptions>): void
+ registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup
+ registerMetricGroup (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+ const label = opts?.label ?? name
+
+ if (isCalculatedMetricOptions>>(opts)) {
+ const calculate = opts.calculate
+ const gauge = meter.createObservableGauge(name, {
+ description: opts?.help ?? name
+ })
+ gauge.addCallback(async (observable) => {
+ const observed = await calculate()
+
+ for (const [key, value] of Object.entries(observed)) {
+ observable.observe(value, {
+ [label]: key
+ })
+ }
+ })
+
+ return
+ }
+
+ return new OpenTelemetryMetricGroup(label, meter.createGauge(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerCounter (name: string, opts: CalculatedMetricOptions): void
+ registerCounter (name: string, opts?: MetricOptions): Counter
+ registerCounter (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+
+ if (isCalculatedMetricOptions(opts)) {
+ const calculate = opts.calculate
+ const counter = meter.createObservableCounter(name, {
+ description: opts?.help ?? name
+ })
+ counter.addCallback(async (result) => {
+ result.observe(await calculate())
+ })
+
+ return
+ }
+
+ return new OpenTelemetryCounter(meter.createCounter(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerCounterGroup (name: string, opts: CalculatedMetricOptions>): void
+ registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup
+ registerCounterGroup (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+ const label = opts?.label ?? name
+
+ if (isCalculatedMetricOptions>>(opts)) {
+ const values: Record = {}
+ const calculate = opts.calculate
+ const counter = meter.createObservableGauge(name, {
+ description: opts?.help ?? name
+ })
+ counter.addCallback(async (observable) => {
+ const observed = await calculate()
+
+ for (const [key, value] of Object.entries(observed)) {
+ if (values[key] == null) {
+ values[key] = 0
+ }
+
+ values[key] += value
+
+ observable.observe(values[key], {
+ [label]: key
+ })
+ }
+ })
+
+ return
+ }
+
+ return new OpenTelemetryCounterGroup(label, meter.createCounter(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerHistogram (name: string, opts: CalculatedHistogramOptions): void
+ registerHistogram (name: string, opts?: HistogramOptions): Histogram
+ registerHistogram (name: string, opts: CalculatedHistogramOptions | HistogramOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+
+ if (isCalculatedMetricOptions(opts)) {
+ return
+ }
+
+ return new OpenTelemetryHistogram(meter.createHistogram(name, {
+ advice: {
+ explicitBucketBoundaries: opts.buckets
+ },
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerHistogramGroup (name: string, opts: CalculatedHistogramOptions>): void
+ registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup
+ registerHistogramGroup (name: string, opts: CalculatedHistogramOptions | HistogramOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+ const label = opts?.label ?? name
+
+ if (isCalculatedMetricOptions>>(opts)) {
+ return
+ }
+
+ return new OpenTelemetryHistogramGroup(label, meter.createHistogram(name, {
+ advice: {
+ explicitBucketBoundaries: opts.buckets
+ },
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerSummary (name: string, opts: CalculatedSummaryOptions): void
+ registerSummary (name: string, opts?: SummaryOptions): Summary
+ registerSummary (name: string, opts: CalculatedSummaryOptions | SummaryOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+
+ if (isCalculatedMetricOptions(opts)) {
+ return
+ }
+
+ return new OpenTelemetrySummary(meter.createGauge(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ registerSummaryGroup (name: string, opts: CalculatedSummaryOptions>): void
+ registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup
+ registerSummaryGroup (name: string, opts: CalculatedSummaryOptions | SummaryOptions = {}): any {
+ if (name == null || name.trim() === '') {
+ throw new InvalidParametersError('Metric name is required')
+ }
+
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
+ const label = opts?.label ?? name
+
+ if (isCalculatedMetricOptions(opts)) {
+ return
+ }
+
+ return new OpenTelemetrySummaryGroup(label, meter.createGauge(name, {
+ description: opts?.help ?? name
+ }))
+ }
+
+ createTrace (): any {
+ return context.active()
+ }
+
+ traceFunction any> (name: string, fn: F, options?: TraceFunctionOptions, ReturnType>): F {
+ // @ts-expect-error returned function could be different to T
+ return (...args: Parameters): any => {
+ const optionsIndex = options?.optionsIndex ?? 0
+ // make sure we have an options object
+ const opts = {
+ ...(args[optionsIndex] ?? {})
+ }
+ args[optionsIndex] = opts
+
+ // skip tracing if no context is passed
+ if (opts.trace == null) {
+ return fn.apply(null, args)
+ }
+
+ const attributes = {}
+
+ // extract the parent context from the options object
+ const parentContext = opts.trace
+ const span = this.tracer.startSpan(name, {
+ attributes: options?.getAttributesFromArgs?.(args, attributes)
+ }, parentContext)
+
+ const childContext = trace.setSpan(parentContext, span)
+ opts.trace = childContext
+ let result: any
+
+ try {
+ result = context.with(childContext, fn, undefined, ...args)
+ } catch (err: any) {
+ span.recordException(err)
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
+ span.end()
+ throw err
+ }
+
+ if (isPromise(result)) {
+ return wrapPromise(result, span, attributes, options)
+ }
+
+ if (isGenerator(result)) {
+ return wrapGenerator(result, span, attributes, options)
+ }
+
+ if (isAsyncGenerator(result)) {
+ return wrapAsyncGenerator(result, span, attributes, options)
+ }
+
+ setAttributes(span, options?.getAttributesFromReturnValue?.(result, attributes))
+
+ span.setStatus({ code: SpanStatusCode.OK })
+ span.end()
+
+ return result
+ }
+ }
+}
+
+export function openTelemetryMetrics (init: OpenTelemetryMetricsInit = {}): (components: OpenTelemetryComponents) => Metrics {
+ return (components: OpenTelemetryComponents) => new OpenTelemetryMetrics(components, init)
+}
+
+function isPromise (obj?: any): obj is Promise {
+ return typeof obj?.then === 'function'
+}
+
+async function wrapPromise (promise: Promise, span: Span, attributes: TraceAttributes, options?: TraceFunctionOptions): Promise {
+ return promise
+ .then(res => {
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res, attributes))
+ span.setStatus({ code: SpanStatusCode.OK })
+ return res
+ })
+ .catch(err => {
+ span.recordException(err)
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
+ })
+ .finally(() => {
+ span.end()
+ })
+}
+
+function isGenerator (obj?: any): obj is Generator {
+ return obj?.[Symbol.iterator] != null
+}
+
+function wrapGenerator (gen: Generator, span: Span, attributes: TraceAttributes, options?: TraceGeneratorFunctionOptions): Generator {
+ const iter = gen[Symbol.iterator]()
+ let index = 0
+
+ const wrapped: Generator = {
+ next: () => {
+ try {
+ const res = iter.next()
+
+ if (res.done === true) {
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res.value, attributes))
+ span.setStatus({ code: SpanStatusCode.OK })
+ span.end()
+ } else {
+ setAttributes(span, options?.getAttributesFromYieldedValue?.(res.value, attributes, ++index))
+ }
+
+ return res
+ } catch (err: any) {
+ span.recordException(err)
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
+ span.end()
+
+ throw err
+ }
+ },
+ return: (value) => {
+ return iter.return(value)
+ },
+ throw: (err) => {
+ return iter.throw(err)
+ },
+ [Symbol.iterator]: () => {
+ return wrapped
+ }
+ }
+
+ return wrapped
+}
+
+function isAsyncGenerator (obj?: any): obj is AsyncGenerator {
+ return obj?.[Symbol.asyncIterator] != null
+}
+
+function wrapAsyncGenerator (gen: AsyncGenerator, span: Span, attributes: TraceAttributes, options?: TraceGeneratorFunctionOptions): AsyncGenerator {
+ const iter = gen[Symbol.asyncIterator]()
+ let index = 0
+
+ const wrapped: AsyncGenerator = {
+ next: async () => {
+ try {
+ const res = await iter.next()
+
+ if (res.done === true) {
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res.value, attributes))
+ span.setStatus({ code: SpanStatusCode.OK })
+ span.end()
+ } else {
+ setAttributes(span, options?.getAttributesFromYieldedValue?.(res.value, attributes, ++index))
+ }
+
+ return res
+ } catch (err: any) {
+ span.recordException(err)
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
+ span.end()
+
+ throw err
+ }
+ },
+ return: async (value) => {
+ return iter.return(value)
+ },
+ throw: async (err) => {
+ return iter.throw(err)
+ },
+ [Symbol.asyncIterator]: () => {
+ return wrapped
+ }
+ }
+
+ return wrapped
+}
+
+function isCalculatedMetricOptions (opts?: any): opts is T {
+ return opts?.calculate != null
+}
+
+function setAttributes (span: Span, attributes?: Attributes): void {
+ if (attributes != null) {
+ span.setAttributes(attributes)
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/metric-group.ts b/packages/metrics-opentelemetry/src/metric-group.ts
new file mode 100644
index 0000000000..004f7f7f6b
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/metric-group.ts
@@ -0,0 +1,69 @@
+import type { MetricGroup, StopTimer } from '@libp2p/interface'
+import type { Gauge } from '@opentelemetry/api'
+
+export class OpenTelemetryMetricGroup implements MetricGroup {
+ private readonly label: string
+ private readonly gauge: Gauge
+ private readonly lastValues: Record
+
+ constructor (label: string, gauge: Gauge) {
+ this.label = label
+ this.gauge = gauge
+ this.lastValues = {}
+ }
+
+ update (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ this.lastValues[key] = value
+ this.gauge.record(value, {
+ [this.label]: key
+ })
+ })
+ }
+
+ increment (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ if (this.lastValues[key] == null) {
+ this.lastValues[key] = 0
+ }
+
+ this.lastValues[key] += value === true ? 1 : value
+ this.gauge.record(this.lastValues[key], {
+ [this.label]: key
+ })
+ })
+ }
+
+ decrement (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ if (this.lastValues[key] == null) {
+ this.lastValues[key] = 0
+ }
+
+ this.lastValues[key] -= value === true ? 1 : value
+ this.gauge.record(this.lastValues[key], {
+ [this.label]: key
+ })
+ })
+ }
+
+ reset (): void {
+ Object.keys(this.lastValues).forEach(key => {
+ this.lastValues[key] = 0
+ this.gauge.record(0, {
+ [this.label]: key
+ })
+ })
+ }
+
+ timer (key: string): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.lastValues[key] = Date.now() - start
+ this.gauge.record(this.lastValues[key], {
+ [this.label]: key
+ })
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/metric.ts b/packages/metrics-opentelemetry/src/metric.ts
new file mode 100644
index 0000000000..963be34a4a
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/metric.ts
@@ -0,0 +1,44 @@
+import type { Metric, StopTimer } from '@libp2p/interface'
+import type { Gauge } from '@opentelemetry/api'
+
+export class OpenTelemetryMetric implements Metric {
+ private readonly gauge: Gauge
+ private lastValue: number
+
+ constructor (gauge: Gauge) {
+ this.gauge = gauge
+ this.lastValue = 0
+ this.update(0)
+ }
+
+ update (value: number): void {
+ this.lastValue = value
+ this.gauge.record(value, {
+ attrName: 'attrValue'
+ })
+ }
+
+ increment (value: number = 1): void {
+ this.lastValue += value
+ this.gauge.record(this.lastValue)
+ }
+
+ decrement (value: number = 1): void {
+ this.lastValue -= value
+ this.gauge.record(this.lastValue)
+ }
+
+ reset (): void {
+ this.gauge.record(0)
+ this.lastValue = 0
+ }
+
+ timer (): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.lastValue = Date.now() - start
+ this.gauge.record(this.lastValue)
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/summary-group.ts b/packages/metrics-opentelemetry/src/summary-group.ts
new file mode 100644
index 0000000000..5035999963
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/summary-group.ts
@@ -0,0 +1,43 @@
+import type { HistogramGroup, StopTimer } from '@libp2p/interface'
+import type { Gauge } from '@opentelemetry/api'
+
+export class OpenTelemetrySummaryGroup implements HistogramGroup {
+ private readonly label: string
+ private readonly gauge: Gauge
+ private readonly lastValues: Record
+
+ constructor (label: string, gauge: Gauge) {
+ this.label = label
+ this.gauge = gauge
+ this.lastValues = {}
+ }
+
+ observe (values: Record): void {
+ Object.entries(values).forEach(([key, value]) => {
+ this.lastValues[key] = value
+ this.gauge.record(value, {
+ [this.label]: key
+ })
+ })
+ }
+
+ reset (): void {
+ Object.keys(this.lastValues).forEach(key => {
+ this.lastValues[key] = 0
+ this.gauge.record(0, {
+ [this.label]: key
+ })
+ })
+ }
+
+ timer (key: string): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.lastValues[key] = Date.now() - start
+ this.gauge.record(this.lastValues[key], {
+ [this.label]: key
+ })
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/summary.ts b/packages/metrics-opentelemetry/src/summary.ts
new file mode 100644
index 0000000000..765b234502
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/summary.ts
@@ -0,0 +1,26 @@
+import type { StopTimer, Summary } from '@libp2p/interface'
+import type { Gauge } from '@opentelemetry/api'
+
+export class OpenTelemetrySummary implements Summary {
+ private readonly gauge: Gauge
+
+ constructor (gauge: Gauge) {
+ this.gauge = gauge
+ }
+
+ observe (value: number): void {
+ this.gauge.record(value)
+ }
+
+ reset (): void {
+ this.gauge.record(0)
+ }
+
+ timer (): StopTimer {
+ const start = Date.now()
+
+ return () => {
+ this.observe(Date.now() - start)
+ }
+ }
+}
diff --git a/packages/metrics-opentelemetry/src/system-metrics.browser.ts b/packages/metrics-opentelemetry/src/system-metrics.browser.ts
new file mode 100644
index 0000000000..56f73c95f0
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/system-metrics.browser.ts
@@ -0,0 +1,3 @@
+export function collectSystemMetrics (): void {
+
+}
diff --git a/packages/metrics-opentelemetry/src/system-metrics.ts b/packages/metrics-opentelemetry/src/system-metrics.ts
new file mode 100644
index 0000000000..a6ab63b78b
--- /dev/null
+++ b/packages/metrics-opentelemetry/src/system-metrics.ts
@@ -0,0 +1,504 @@
+import { readdirSync, readFileSync } from 'node:fs'
+import { statfs } from 'node:fs/promises'
+import { totalmem } from 'node:os'
+import { monitorEventLoopDelay, PerformanceObserver, constants as PerfHooksConstants } from 'node:perf_hooks'
+import { getHeapSpaceStatistics } from 'node:v8'
+import type { Metrics } from '@libp2p/interface'
+
+export interface SystemMetricsOptions {
+ statfsLocation?: string
+}
+
+export function collectSystemMetrics (metrics: Metrics, init?: SystemMetricsOptions): void {
+ metrics.registerMetricGroup('nodejs_memory_usage_bytes', {
+ label: 'memory',
+ calculate: () => {
+ return {
+ ...process.memoryUsage()
+ }
+ }
+ })
+ const totalMemoryMetric = metrics.registerMetric('nodejs_memory_total_bytes')
+ totalMemoryMetric.update(totalmem())
+
+ metrics.registerMetricGroup('nodejs_fs_usage_bytes', {
+ label: 'filesystem',
+ calculate: async () => {
+ const stats = await statfs(init?.statfsLocation ?? process.cwd())
+ const total = stats.bsize * stats.blocks
+ const available = stats.bsize * stats.bavail
+
+ return {
+ total,
+ free: stats.bsize * stats.bfree,
+ available,
+ used: (available / total) * 100
+ }
+ }
+ })
+
+ collectProcessCPUMetrics(metrics)
+ collectProcessStartTime(metrics)
+ collectMemoryHeap(metrics)
+ collectOpenFileDescriptors(metrics)
+ collectMaxFileDescriptors(metrics)
+ collectEventLoopStats(metrics)
+ collectProcessResources(metrics)
+ collectProcessHandles(metrics)
+ collectProcessRequests(metrics)
+ collectHeapSizeAndUsed(metrics)
+ collectHeapSpacesSizeAndUsed(metrics)
+ collectNodeVersion(metrics)
+ collectGcStats(metrics)
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processCpuTotal.js
+ */
+function collectProcessCPUMetrics (metrics: Metrics): void {
+ let lastCpuUsage = process.cpuUsage()
+ const cpuUserSecondsTotal = metrics.registerCounter('process_cpu_user_seconds_total', {
+ help: 'Total user CPU time spent in seconds.'
+ })
+ const cpuSystemSecondsTotal = metrics.registerCounter('process_cpu_system_seconds_total', {
+ help: 'Total system CPU time spent in seconds.'
+ })
+
+ metrics.registerCounter('process_cpu_seconds_total', {
+ help: 'Total user and system CPU time spent in seconds.',
+ calculate: () => {
+ const cpuUsage = process.cpuUsage()
+ const userUsageMicros = cpuUsage.user - lastCpuUsage.user
+ const systemUsageMicros = cpuUsage.system - lastCpuUsage.system
+ lastCpuUsage = cpuUsage
+
+ cpuUserSecondsTotal.increment(userUsageMicros / 1e6)
+ cpuSystemSecondsTotal.increment(systemUsageMicros / 1e6)
+
+ return (userUsageMicros + systemUsageMicros) / 1e6
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processStartTime.js
+ */
+function collectProcessStartTime (metrics: Metrics): void {
+ const metric = metrics.registerMetric('process_start_time_seconds', {
+ help: 'Start time of the process since unix epoch in seconds.'
+ })
+
+ metric.update(Math.round(Date.now() / 1000 - process.uptime()))
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/osMemoryHeap.js
+ */
+function collectMemoryHeap (metrics: Metrics): void {
+ metrics.registerMetric('process_resident_memory_bytes', {
+ help: 'Resident memory size in bytes.',
+ calculate: () => {
+ try {
+ return process.memoryUsage().rss
+ } catch {}
+ return 0
+ }
+ })
+ metrics.registerMetric('process_virtual_memory_bytes', {
+ help: 'Virtual memory size in bytes.',
+ calculate: () => {
+ // this involves doing sync io in prom-client so skip it
+ // https://github.com/siimon/prom-client/blob/c1d76c5d497ef803f6bd90c56c713c3fa811c3e0/lib/metrics/osMemoryHeapLinux.js#L53C5-L54C52
+ return 0
+ }
+ })
+ metrics.registerMetric('process_heap_bytes', {
+ help: 'Process heap size in bytes.',
+ calculate: () => {
+ try {
+ return process.memoryUsage().heapTotal
+ } catch {}
+ return 0
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processOpenFileDescriptors.js
+ */
+function collectOpenFileDescriptors (metrics: Metrics): void {
+ if (process.platform !== 'linux') {
+ return
+ }
+
+ metrics.registerMetric('process_open_fds', {
+ help: 'Number of open file descriptors.',
+ calculate: () => {
+ try {
+ const fds = readdirSync('/proc/self/fd')
+ // Minus 1 to not count the fd that was used by readdirSync(),
+ // it's now closed.
+ return fds.length - 1
+ } catch {}
+
+ return 0
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processMaxFileDescriptors.js
+ */
+function collectMaxFileDescriptors (metrics: Metrics): void {
+ let maxFds: number | undefined
+
+ // This will fail if a linux-like procfs is not available.
+ try {
+ const limits = readFileSync('/proc/self/limits', 'utf8')
+ const lines = limits.split('\n')
+ for (const line of lines) {
+ if (line.startsWith('Max open files')) {
+ const parts = line.split(/ +/)
+ maxFds = Number(parts[1])
+ break
+ }
+ }
+ } catch {
+ return
+ }
+
+ if (maxFds == null) {
+ return
+ }
+
+ const metric = metrics.registerMetric('process_max_fds', {
+ help: 'Maximum number of open file descriptors.'
+ })
+ metric.update(maxFds)
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/eventLoopLag.js
+ */
+function collectEventLoopStats (metrics: Metrics): void {
+ try {
+ const histogram = monitorEventLoopDelay()
+ histogram.enable()
+
+ metrics.registerMetric('nodejs_eventloop_lag_seconds', {
+ help: 'Lag of event loop in seconds.',
+ calculate: async () => {
+ const start = process.hrtime()
+
+ return new Promise(resolve => {
+ setImmediate(() => {
+ const delta = process.hrtime(start)
+ const nanosec = delta[0] * 1e9 + delta[1]
+ const seconds = nanosec / 1e9
+
+ lagMin.update(histogram.min / 1e9)
+ lagMax.update(histogram.max / 1e9)
+ lagMean.update(histogram.mean / 1e9)
+ lagStddev.update(histogram.stddev / 1e9)
+ lagP50.update(histogram.percentile(50) / 1e9)
+ lagP90.update(histogram.percentile(90) / 1e9)
+ lagP99.update(histogram.percentile(99) / 1e9)
+
+ histogram.reset()
+
+ resolve(seconds)
+ })
+ })
+ }
+ })
+ const lagMin = metrics.registerMetric('nodejs_eventloop_lag_min_seconds', {
+ help: 'The minimum recorded event loop delay.'
+ })
+ const lagMax = metrics.registerMetric('nodejs_eventloop_lag_max_seconds', {
+ help: 'The maximum recorded event loop delay.'
+ })
+ const lagMean = metrics.registerMetric('nodejs_eventloop_lag_mean_seconds', {
+ help: 'The mean of the recorded event loop delays.'
+ })
+ const lagStddev = metrics.registerMetric('nodejs_eventloop_lag_stddev_seconds', {
+ help: 'The standard deviation of the recorded event loop delays.'
+ })
+ const lagP50 = metrics.registerMetric('nodejs_eventloop_lag_p50_seconds', {
+ help: 'The 50th percentile of the recorded event loop delays.'
+ })
+ const lagP90 = metrics.registerMetric('nodejs_eventloop_lag_p90_seconds', {
+ help: 'The 90th percentile of the recorded event loop delays.'
+ })
+ const lagP99 = metrics.registerMetric('nodejs_eventloop_lag_p99_seconds', {
+ help: 'The 99th percentile of the recorded event loop delays.'
+ })
+ } catch (err: any) {
+ if (err.code === 'ERR_NOT_IMPLEMENTED') {
+ return // Bun
+ }
+
+ throw err
+ }
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processResources.js
+ */
+function collectProcessResources (metrics: Metrics): void {
+ // Don't do anything if the function does not exist in previous nodes (exists in node@17.3.0)
+ if (typeof process.getActiveResourcesInfo !== 'function') {
+ return
+ }
+
+ metrics.registerMetricGroup('nodejs_active_resources', {
+ help: 'Number of active resources that are currently keeping the event loop alive, grouped by async resource type.',
+ label: 'type',
+ calculate: () => {
+ const resources = process.getActiveResourcesInfo()
+
+ const data: Record = {}
+
+ for (let i = 0; i < resources.length; i++) {
+ const resource = resources[i]
+
+ if (Object.hasOwn(data, resource)) {
+ data[resource] += 1
+ } else {
+ data[resource] = 1
+ }
+ }
+
+ return data
+ }
+ })
+
+ metrics.registerMetric('nodejs_active_resources_total', {
+ help: 'Total number of active resources.',
+ calculate: () => {
+ const resources = process.getActiveResourcesInfo()
+
+ return resources.length
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processHandles.js
+ */
+function collectProcessHandles (metrics: Metrics): void {
+ // Don't do anything if the function is removed in later nodes (exists in node@6-12...)
+ // @ts-expect-error not part of the public API
+ if (typeof process._getActiveHandles !== 'function') {
+ return
+ }
+
+ metrics.registerMetricGroup('nodejs_active_handles', {
+ help: 'Number of active libuv handles grouped by handle type. Every handle type is C++ class name.',
+ label: 'type',
+ calculate: () => {
+ // @ts-expect-error not part of the public API
+ const resources = process._getActiveHandles()
+
+ const data: Record = {}
+
+ for (let i = 0; i < resources.length; i++) {
+ const listElement = resources[i]
+
+ if (listElement == null || typeof listElement.constructor === 'undefined') {
+ continue
+ }
+
+ if (Object.hasOwnProperty.call(data, listElement.constructor.name)) {
+ data[listElement.constructor.name] += 1
+ } else {
+ data[listElement.constructor.name] = 1
+ }
+ }
+
+ return data
+ }
+ })
+
+ metrics.registerMetric('nodejs_active_handles_total', {
+ help: 'Total number of active handles.',
+ calculate: () => {
+ // @ts-expect-error not part of the public API
+ const resources = process._getActiveHandles()
+
+ return resources.length
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/processRequests.js
+ */
+function collectProcessRequests (metrics: Metrics): void {
+ // Don't do anything if the function is removed in later nodes (exists in node@6)
+ // @ts-expect-error not part of the public API
+ if (typeof process._getActiveRequests !== 'function') {
+ return
+ }
+
+ metrics.registerMetricGroup('nodejs_active_requests', {
+ help: 'Number of active libuv requests grouped by request type. Every request type is C++ class name.',
+ label: 'type',
+ calculate: () => {
+ // @ts-expect-error not part of the public API
+ const resources = process._getActiveRequests()
+
+ const data: Record = {}
+
+ for (let i = 0; i < resources.length; i++) {
+ const listElement = resources[i]
+
+ if (listElement == null || typeof listElement.constructor === 'undefined') {
+ continue
+ }
+
+ if (Object.hasOwnProperty.call(data, listElement.constructor.name)) {
+ data[listElement.constructor.name] += 1
+ } else {
+ data[listElement.constructor.name] = 1
+ }
+ }
+
+ return data
+ }
+ })
+
+ metrics.registerMetric('nodejs_active_requests_total', {
+ help: 'Total number of active requests.',
+ calculate: () => {
+ // @ts-expect-error not part of the public API
+ const resources = process._getActiveRequests()
+
+ return resources.length
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/heapSizeAndUsed.js
+ */
+function collectHeapSizeAndUsed (metrics: Metrics): void {
+ const heapSizeUsed = metrics.registerMetric('nodejs_heap_size_used_bytes', {
+ help: 'Process heap size used from Node.js in bytes.'
+ })
+ const externalMemUsed = metrics.registerMetric('nodejs_external_memory_bytes', {
+ help: 'Node.js external memory size in bytes.'
+ })
+
+ metrics.registerMetric('nodejs_heap_size_total_bytes', {
+ help: 'Process heap size from Node.js in bytes.',
+ calculate: () => {
+ try {
+ const memUsage = process.memoryUsage()
+
+ heapSizeUsed.update(memUsage.heapUsed)
+ if (memUsage.external !== undefined) {
+ externalMemUsed.update(memUsage.external)
+ }
+
+ return memUsage.heapTotal
+ } catch {}
+
+ return 0
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/heapSpacesSizeAndUsed.js
+ */
+function collectHeapSpacesSizeAndUsed (metrics: Metrics): void {
+ try {
+ getHeapSpaceStatistics()
+ } catch (err: any) {
+ if (err.code === 'ERR_NOT_IMPLEMENTED') {
+ return // Bun
+ }
+ throw err
+ }
+
+ const used = metrics.registerMetricGroup('nodejs_heap_space_size_used_bytes', {
+ help: 'Process heap space size used from Node.js in bytes.',
+ label: 'space'
+ })
+ const available = metrics.registerMetricGroup('nodejs_heap_space_size_available_bytes', {
+ help: 'Process heap space size available from Node.js in bytes.',
+ label: 'space'
+ })
+
+ metrics.registerMetricGroup('nodejs_heap_space_size_total_bytes', {
+ help: 'Process heap space size total from Node.js in bytes.',
+ label: 'space',
+ calculate: () => {
+ const data: Record = {}
+
+ for (const space of getHeapSpaceStatistics()) {
+ const spaceName = space.space_name.substr(0, space.space_name.indexOf('_space'))
+
+ used.update({
+ [spaceName]: space.space_used_size
+ })
+
+ available.update({
+ [spaceName]: space.space_available_size
+ })
+
+ data[spaceName] = space.space_size
+ }
+
+ return data
+ }
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/version.js
+ */
+function collectNodeVersion (metrics: Metrics): void {
+ const metric = metrics.registerMetricGroup('nodejs_version_info', {
+ help: 'Node.js version info.'
+ })
+
+ const version = process.version
+ const versionSegments = version.slice(1).split('.').map(Number)
+
+ // @ts-expect-error use internal API to get same result as prom-client
+ metric.gauge.record(1, {
+ version,
+ major: versionSegments[0],
+ minor: versionSegments[1],
+ patch: versionSegments[2]
+ })
+}
+
+/**
+ * @see https://github.com/siimon/prom-client/blob/master/lib/metrics/gc.js
+ */
+function collectGcStats (metrics: Metrics): void {
+ const histogram = metrics.registerHistogramGroup('nodejs_gc_duration_seconds_bucket', {
+ buckets: [0.001, 0.01, 0.1, 1, 2, 5],
+ label: 'kind'
+ })
+
+ const kinds: string[] = []
+ kinds[PerfHooksConstants.NODE_PERFORMANCE_GC_MAJOR] = 'major'
+ kinds[PerfHooksConstants.NODE_PERFORMANCE_GC_MINOR] = 'minor'
+ kinds[PerfHooksConstants.NODE_PERFORMANCE_GC_INCREMENTAL] = 'incremental'
+ kinds[PerfHooksConstants.NODE_PERFORMANCE_GC_WEAKCB] = 'weakcb'
+
+ const obs = new PerformanceObserver(list => {
+ const entry = list.getEntries()[0]
+ // @ts-expect-error types are incomplete
+ const kind = kinds[entry.detail.kind]
+ // Convert duration from milliseconds to seconds
+ histogram.observe({
+ [kind]: entry.duration / 1000
+ })
+ })
+
+ obs.observe({ entryTypes: ['gc'] })
+}
diff --git a/packages/metrics-opentelemetry/test/index.spec.ts b/packages/metrics-opentelemetry/test/index.spec.ts
new file mode 100644
index 0000000000..8419deae7a
--- /dev/null
+++ b/packages/metrics-opentelemetry/test/index.spec.ts
@@ -0,0 +1,8 @@
+import { expect } from 'aegir/chai'
+
+describe('opentelemetry-metrics', () => {
+ it('should wrap a method', async () => {
+ // TODO: tests
+ expect(true).to.be.true()
+ })
+})
diff --git a/packages/metrics-opentelemetry/tsconfig.json b/packages/metrics-opentelemetry/tsconfig.json
new file mode 100644
index 0000000000..7f69ee51fe
--- /dev/null
+++ b/packages/metrics-opentelemetry/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "aegir/src/config/tsconfig.aegir.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": [
+ "src",
+ "test"
+, "../../otel-test.js" ],
+ "references": [
+ {
+ "path": "../interface"
+ },
+ {
+ "path": "../logger"
+ }
+ ]
+}
diff --git a/packages/metrics-opentelemetry/typedoc.json b/packages/metrics-opentelemetry/typedoc.json
new file mode 100644
index 0000000000..f599dc728d
--- /dev/null
+++ b/packages/metrics-opentelemetry/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "entryPoints": [
+ "./src/index.ts"
+ ]
+}