Skip to content

Why MetricsJS?

With Metrics JS, it’s possible to instrument a Node.js module, measuring the things you feel are important such as operation timing or counts, without needing to know which system the metrics will be collected into. It’s up to the final consumer to decide whether the metrics will be pushed to Prometheus, Data Dog or something else.

Resilient design

MetricsJS is designed in a way that’s immune to the problem posed by a global registry.

The problem we solve

Take for instance this simplified implementation of a metrics class:

const REGISTRY = new Map();
module.exports = class MyMetricsClient {
constructor() {
super();
}
counter(opts = {}) {
}
histogram(opts = {}) {
}
metrics() {
return REGISTRY.entries();
}
};

This approach works as long as there is only ever one version of this module. Why? Let’s look at how npm and other package managers handles dependencies.

Imagine you have a dependency tree somewhat like this:

node_modules/
├── my-metrics-client@1.2.2
└── @my-corp/
├── login/
| └── my-metrics-client@1.2.2
└── health-checks/
└── my-metrics-client@1.2.2

Your app is not the only one using my-metrics-client. What happens when installing is that npm is that the shared dependencies will be flattened:

node_modules/
├── my-metrics-client@1.2.2
└── @my-corp/
├── login/
└── health-checks/

So far, so good. All code that produces metrics use the same version, and push metrics to the same shared REGISTRY singleton.

Problems start happening when your dependency tree looks like this:

node_modules/
├── my-metrics-client@1.2.2
└── @my-corp/
├── login/
| └── my-metrics-client@1.0.2
└── health-checks/
└── my-metrics-client@1.4.0

At this point, my-metrics-client can’t be flattened to one shared instance. Each of the @my-corp modules that are producing metrics will not add their entries to your app’s REGISTRY. You will have no access to the metrics generated by your dependencies.

Solving dependency mismatch

MetricsJS solves the problem by treating metrics as a stream of data.

In MetricsJS we define a standard metric object:

const metric = {
description: '',
timestamp: -1,
source: '',
labels: [],
value: -1,
time: -1,
meta: {},
type: 0,
name: '',
};

Each producer of metrics has its own instance of the metrics client, which mimics the API of prom-client. The metrics client is also Node stream. Each method on the client produces a metric object on the stream.

Back to the dependency tree, it doesn’t matter if it looks like this:

node_modules/
├── @metrics/client@1.2.2
└── @my-corp/
├── login/
| └── @metrics/client@1.0.2
└── health-checks/
└── @metrics/client@1.4.0

All the modules in @my-corp expose their metrics stream. The app composes the streams, and has access to all metrics.