Skip to content

Commit 2615e72

Browse files
authored
feat: add last fetch and last update (#190)
* feat: add last fetch and last update Adds prometheus gauges for when we last fetched (no matter if 200 or 304) and when we last updated (only updated when status==200) Fixes: #187 * docs: add prometheus endpoint to docs
1 parent 5113d4c commit 2615e72

File tree

9 files changed

+1601
-1020
lines changed

9 files changed

+1601
-1020
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,15 @@ Connection: keep-alive
506506
Keep-Alive: timeout=5
507507
```
508508

509+
**Prometheus endpoint**
510+
511+
The proxy has a prometheus metrics endpoint available at http://localhost:3000/proxy/internal-backstage/prometheus
512+
With the following metrics
513+
* `unleash_proxy_up` a [counter](https://prometheus.io/docs/concepts/metric_types/#counter) which is set to 1 when proxy is running
514+
* `last_metrics_update_epoch_timestamp_ms` a [gauge](https://prometheus.io/docs/concepts/metric_types/#gauge) set to the epoch timestamp in ms when the proxy last received a feature update
515+
* `last_metrics_fetch_epoch_timestamp_ms` a [gauge](https://prometheus.io/docs/concepts/metric_types/#gauge) set to the epoch timestamp in ms when the proxy last checked for updates
516+
517+
509518
### Run with Node.js:
510519

511520
**STEP 1: Install dependency**

package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"express": "^4.18.2",
4545
"json-schema-to-ts": "^2.3.0",
4646
"openapi-types": "^11.0.0",
47+
"prom-client": "^15.1.3",
4748
"qs": "^6.9.7",
4849
"type-is": "^1.6.18",
4950
"unleash-client": "^6.1.1"
@@ -54,7 +55,7 @@
5455
"@types/compression": "^1.7.2",
5556
"@types/cors": "^2.8.12",
5657
"@types/express": "^4.17.13",
57-
"@types/jest": "^27.5.0",
58+
"@types/jest": "^29.5.12",
5859
"@types/node": "18.7.14",
5960
"@types/supertest": "^2.0.12",
6061
"@types/type-is": "^1.6.3",
@@ -67,12 +68,12 @@
6768
"eslint-config-prettier": "^8.5.0",
6869
"eslint-plugin-import": "^2.26.0",
6970
"eslint-plugin-prettier": "^4.0.0",
70-
"jest": "^28.0.3",
71+
"jest": "^29.7.0",
7172
"prettier": "^2.6.2",
7273
"semver": "^7.3.7",
7374
"supertest": "^6.2.3",
74-
"ts-jest": "^28.0.1",
75-
"ts-node-dev": "^1.1.8",
75+
"ts-jest": "^29.2.4",
76+
"ts-node-dev": "^2.0.0",
7677
"typescript": "^4.6.4"
7778
},
7879
"prettier": {

src/client.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import EventEmitter from 'events';
2-
import { Context, Unleash, Variant } from 'unleash-client';
2+
import { Context, Unleash, UnleashEvents, Variant } from 'unleash-client';
33
import { FeatureInterface } from 'unleash-client/lib/feature';
44
import Metrics from 'unleash-client/lib/metrics';
55
import { getDefaultVariant } from 'unleash-client/lib/variant';
66
import { IProxyConfig } from './config';
77
import { Logger } from './logger';
8+
import { lastMetricsFetch, lastMetricsUpdate } from './prom-metrics';
89

910
export type FeatureToggleStatus = {
1011
name: string;
@@ -77,6 +78,14 @@ class Client extends EventEmitter implements IClient {
7778
this.ready = true;
7879
this.metrics.start();
7980
});
81+
this.unleash.on(UnleashEvents.Unchanged, () => {
82+
lastMetricsFetch.set(new Date().getTime());
83+
});
84+
this.unleash.on(UnleashEvents.Changed, () => {
85+
const updatedAt = new Date().getTime();
86+
lastMetricsFetch.set(updatedAt);
87+
lastMetricsUpdate.set(updatedAt);
88+
});
8089
}
8190

8291
setUnleashApiToken(unleashApiToken: string): void {

src/prom-metrics/createCounter.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Counter as PromCounter, type CounterConfiguration } from 'prom-client';
2+
3+
/**
4+
* A wrapped instance of prom-client's Counter, overriding some of its methods for enhanced functionality and type-safety.
5+
*/
6+
export type Counter<T extends string = string> = {
7+
counter: PromCounter<T>;
8+
labels: (labels: Record<T, string | number>) => PromCounter.Internal;
9+
inc: (value?: number | undefined) => void;
10+
increment: (labels: Record<T, string | number>, value?: number) => void;
11+
};
12+
13+
/**
14+
* Creates a wrapped instance of prom-client's Counter, overriding some of its methods for enhanced functionality and type-safety.
15+
*
16+
* @param options - The configuration options for the Counter, as defined in prom-client's CounterConfiguration.
17+
* See prom-client documentation for detailed options: https://github.com/siimon/prom-client#counter
18+
* @returns An object containing the wrapped Counter instance and custom methods.
19+
*/
20+
export const createCounter = <T extends string>(
21+
options: CounterConfiguration<T>,
22+
): Counter<T> => {
23+
/**
24+
* The underlying instance of prom-client's Counter.
25+
*/
26+
const counter = new PromCounter<T>(options);
27+
28+
/**
29+
* Applies given labels to the counter. Labels are key-value pairs.
30+
* This method wraps the original Counter's labels method for additional type-safety, requiring all configured labels to be specified.
31+
*
32+
* @param labels - An object where keys are label names and values are the label values.
33+
* @returns The Counter instance with the applied labels, allowing for method chaining.
34+
*/
35+
// eslint-disable-next-line @typescript-eslint/no-shadow
36+
const labels = (labels: Record<T, string | number>) =>
37+
counter.labels(labels);
38+
39+
/**
40+
* Increments the counter by a specified value or by 1 if no value is provided.
41+
* Wraps the original Counter's inc method.
42+
*
43+
* @param value - (Optional) The value to increment the counter by. If not provided, defaults to 1.
44+
*/
45+
const inc = (value?: number | undefined) => counter.inc(value);
46+
47+
/**
48+
* A convenience method that combines setting labels and incrementing the counter.
49+
* Useful for incrementing with labels in a single call.
50+
*
51+
* @param labels - An object where keys are label names and values are the label values.
52+
* @param value - (Optional) The value to increment the counter by. If not provided, defaults to 1.
53+
*/
54+
// eslint-disable-next-line @typescript-eslint/no-shadow
55+
const increment = (labels: Record<T, string | number>, value?: number) =>
56+
counter.labels(labels).inc(value);
57+
58+
return {
59+
counter,
60+
labels,
61+
inc,
62+
increment,
63+
};
64+
};

src/prom-metrics/createGauge.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Gauge as PromGauge, type GaugeConfiguration } from 'prom-client';
2+
3+
/**
4+
* A wrapped instance of prom-client's Gauge, overriding some of its methods for enhanced functionality and type-safety.
5+
*/
6+
export type Gauge<T extends string = string> = {
7+
gauge: PromGauge<T>;
8+
labels: (labels: Record<T, string | number>) => PromGauge.Internal<T>;
9+
reset: () => void;
10+
set: (value: number) => void;
11+
};
12+
13+
export const createGauge = <T extends string>(
14+
options: GaugeConfiguration<T>,
15+
): Gauge<T> => {
16+
/**
17+
* The underlying instance of prom-client's Gauge.
18+
*/
19+
const gauge = new PromGauge(options);
20+
21+
/**
22+
* Applies given labels to the gauge. Labels are key-value pairs.
23+
* This method wraps the original Gauge's labels method for additional type-safety, requiring all configured labels to be specified.
24+
*
25+
* @param labels - An object where keys are label names and values are the label values.
26+
* @returns The Gauge instance with the applied labels, allowing for method chaining.
27+
*/
28+
// eslint-disable-next-line @typescript-eslint/no-shadow
29+
const labels = (labels: Record<T, string | number>) => gauge.labels(labels);
30+
31+
/**
32+
* Resets the gauge value.
33+
* Wraps the original Gauge's reset method.
34+
*/
35+
const reset = () => gauge.reset();
36+
37+
/**
38+
* Sets the gauge to a specified value.
39+
* Wraps the original Gauge's set method.
40+
*
41+
* @param value - The value to set the gauge to.
42+
*/
43+
const set = (value: number) => gauge.set(value);
44+
45+
return {
46+
gauge,
47+
labels,
48+
reset,
49+
set,
50+
};
51+
};

src/prom-metrics/index.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createGauge, type Gauge } from './createGauge';
2+
import { createCounter } from './createCounter';
3+
4+
export * from './createCounter';
5+
export * from './createGauge';
6+
7+
export const lastMetricsUpdate: Gauge = createGauge({
8+
name: 'last_metrics_update_epoch_timestamp_ms',
9+
help: 'An epoch timestamp (in milliseconds) set to when our unleash-client last got an update from upstream Unleash',
10+
});
11+
12+
export const lastMetricsFetch: Gauge = createGauge({
13+
name: 'last_metrics_fetch_epoch_timestamp_ms',
14+
help: 'An epoch timestamp (in milliseconds) set to when our unleash-client last checked (regardless if there was an update or not)',
15+
});
16+
17+
createCounter({
18+
name: 'unleash_proxy_up',
19+
help: 'Indication that the service is up.',
20+
}).inc(1);

0 commit comments

Comments
 (0)