@tryghost/prometheus-metrics 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/build/MetricsServer.d.ts +24 -0
- package/build/MetricsServer.js +54 -0
- package/build/PrometheusClient.d.ts +148 -0
- package/build/PrometheusClient.js +349 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +18 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2025 Ghost Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Metrics Server
|
|
2
|
+
|
|
3
|
+
A standalone server for exporting prometheus metrics from Ghost
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Develop
|
|
10
|
+
|
|
11
|
+
This is a monorepo package.
|
|
12
|
+
|
|
13
|
+
Follow the instructions for the top-level repo.
|
|
14
|
+
1. `git clone` this repo & `cd` into it as usual
|
|
15
|
+
2. Run `yarn` to install top-level dependencies.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Test
|
|
20
|
+
|
|
21
|
+
- `yarn lint` run just eslint
|
|
22
|
+
- `yarn test` run lint and tests
|
|
23
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import stoppable from 'stoppable';
|
|
3
|
+
type ServerConfig = {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class MetricsServer {
|
|
8
|
+
private serverConfig;
|
|
9
|
+
private handler;
|
|
10
|
+
private app;
|
|
11
|
+
private httpServer;
|
|
12
|
+
private isShuttingDown;
|
|
13
|
+
constructor({ serverConfig, handler }: {
|
|
14
|
+
serverConfig: ServerConfig;
|
|
15
|
+
handler: express.Handler;
|
|
16
|
+
});
|
|
17
|
+
start(): Promise<{
|
|
18
|
+
app: express.Application;
|
|
19
|
+
httpServer: stoppable.StoppableServer;
|
|
20
|
+
}>;
|
|
21
|
+
stop(): Promise<void>;
|
|
22
|
+
shutdown(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MetricsServer = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("@tryghost/debug"));
|
|
8
|
+
const express_1 = __importDefault(require("express"));
|
|
9
|
+
const stoppable_1 = __importDefault(require("stoppable"));
|
|
10
|
+
const debug = (0, debug_1.default)('metrics-server');
|
|
11
|
+
class MetricsServer {
|
|
12
|
+
serverConfig;
|
|
13
|
+
handler;
|
|
14
|
+
app;
|
|
15
|
+
httpServer;
|
|
16
|
+
isShuttingDown;
|
|
17
|
+
constructor({ serverConfig, handler }) {
|
|
18
|
+
// initialize local variables
|
|
19
|
+
this.serverConfig = serverConfig;
|
|
20
|
+
this.handler = handler;
|
|
21
|
+
this.app = null;
|
|
22
|
+
this.httpServer = null;
|
|
23
|
+
this.isShuttingDown = false;
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
// start the server
|
|
27
|
+
debug('Starting metrics server');
|
|
28
|
+
this.app = (0, express_1.default)();
|
|
29
|
+
this.app.get('/metrics', this.handler);
|
|
30
|
+
const httpServer = this.app.listen(this.serverConfig.port, this.serverConfig.host, () => {
|
|
31
|
+
debug(`Metrics server listening at ${this.serverConfig.host}:${this.serverConfig.port}`);
|
|
32
|
+
});
|
|
33
|
+
this.httpServer = (0, stoppable_1.default)(httpServer, 0);
|
|
34
|
+
process.on('SIGINT', () => this.shutdown());
|
|
35
|
+
process.on('SIGTERM', () => this.shutdown());
|
|
36
|
+
return { app: this.app, httpServer: this.httpServer };
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
// stop the server
|
|
40
|
+
debug('Stopping metrics server');
|
|
41
|
+
if (this.httpServer && this.httpServer.listening) {
|
|
42
|
+
await this.httpServer.stop();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async shutdown() {
|
|
46
|
+
if (this.isShuttingDown) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.isShuttingDown = true;
|
|
50
|
+
await this.stop();
|
|
51
|
+
this.isShuttingDown = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.MetricsServer = MetricsServer;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import client from 'prom-client';
|
|
3
|
+
import type { Metric, MetricObjectWithValues, MetricValue } from 'prom-client';
|
|
4
|
+
import type { Knex } from 'knex';
|
|
5
|
+
type PrometheusClientConfig = {
|
|
6
|
+
register?: client.Registry;
|
|
7
|
+
pushgateway?: {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
url?: string;
|
|
10
|
+
interval?: number;
|
|
11
|
+
jobName?: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A client for exporting metrics to Prometheus, based on prom-client
|
|
16
|
+
*/
|
|
17
|
+
export declare class PrometheusClient {
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new PrometheusClient instance
|
|
20
|
+
* @param prometheusConfig - The configuration for the PrometheusClient
|
|
21
|
+
*/
|
|
22
|
+
constructor(prometheusConfig?: PrometheusClientConfig, logger?: any);
|
|
23
|
+
client: typeof client;
|
|
24
|
+
gateway: client.Pushgateway<client.RegistryContentType> | undefined;
|
|
25
|
+
queries: Map<string, () => void>;
|
|
26
|
+
acquires: Map<number, () => void>;
|
|
27
|
+
creates: Map<number, () => void>;
|
|
28
|
+
private config;
|
|
29
|
+
private prefix;
|
|
30
|
+
private pushInterval;
|
|
31
|
+
private pushRetries;
|
|
32
|
+
private logger;
|
|
33
|
+
/**
|
|
34
|
+
* Initializes the prometheus client, setting up the pushgateway if enabled
|
|
35
|
+
*/
|
|
36
|
+
init(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Pushes metrics to the pushgateway, if enabled
|
|
39
|
+
*/
|
|
40
|
+
pushMetrics(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Shuts down the prometheus client cleanly
|
|
43
|
+
*/
|
|
44
|
+
stop(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Tells prom-client to collect default metrics
|
|
47
|
+
* Only called once on init
|
|
48
|
+
*/
|
|
49
|
+
collectDefaultMetrics(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Handles metrics requests to serve the /metrics endpoint
|
|
52
|
+
* @param req - The request object
|
|
53
|
+
* @param res - The response object
|
|
54
|
+
*/
|
|
55
|
+
handleMetricsRequest(req: Request, res: Response): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Returns the metrics from the registry as a string
|
|
58
|
+
*/
|
|
59
|
+
getMetrics(): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Returns the metrics from the registry as a JSON object
|
|
62
|
+
*
|
|
63
|
+
* Particularly useful for testing
|
|
64
|
+
*/
|
|
65
|
+
getMetricsAsJSON(): Promise<object[]>;
|
|
66
|
+
getMetricsAsArray(): Promise<object[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Returns the content type for the metrics
|
|
69
|
+
*/
|
|
70
|
+
getContentType(): "text/plain; version=0.0.4; charset=utf-8";
|
|
71
|
+
/**
|
|
72
|
+
* Returns a single metric from the registry
|
|
73
|
+
* @param name - The name of the metric
|
|
74
|
+
* @returns The metric
|
|
75
|
+
*/
|
|
76
|
+
getMetric(name: string): Metric | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the metric object of a single metric, if it exists
|
|
79
|
+
* @param name - The name of the metric
|
|
80
|
+
* @returns The values of the metric
|
|
81
|
+
*/
|
|
82
|
+
getMetricObject(name: string): Promise<MetricObjectWithValues<MetricValue<string>> | undefined>;
|
|
83
|
+
getMetricValues(name: string): Promise<MetricValue<string>[] | undefined>;
|
|
84
|
+
/**
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Registers a counter metric
|
|
89
|
+
* @param name - The name of the metric
|
|
90
|
+
* @param help - The help text for the metric
|
|
91
|
+
* @param labelNames - The names of the labels for the metric
|
|
92
|
+
* @returns The counter metric
|
|
93
|
+
*/
|
|
94
|
+
registerCounter({ name, help, labelNames }: {
|
|
95
|
+
name: string;
|
|
96
|
+
help: string;
|
|
97
|
+
labelNames?: string[];
|
|
98
|
+
}): client.Counter;
|
|
99
|
+
/**
|
|
100
|
+
* Registers a gauge metric
|
|
101
|
+
* @param name - The name of the metric
|
|
102
|
+
* @param help - The help text for the metric
|
|
103
|
+
* @param collect - The collect function to use for the gauge
|
|
104
|
+
* @returns The gauge metric
|
|
105
|
+
*/
|
|
106
|
+
registerGauge({ name, help, collect }: {
|
|
107
|
+
name: string;
|
|
108
|
+
help: string;
|
|
109
|
+
collect?: () => void;
|
|
110
|
+
}): client.Gauge;
|
|
111
|
+
/**
|
|
112
|
+
* Registers a summary metric
|
|
113
|
+
* @param name - The name of the metric
|
|
114
|
+
* @param help - The help text for the metric
|
|
115
|
+
* @param percentiles - The percentiles to calculate for the summary
|
|
116
|
+
* @param collect - The collect function to use for the summary
|
|
117
|
+
* @returns The summary metric
|
|
118
|
+
*/
|
|
119
|
+
registerSummary({ name, help, percentiles, maxAgeSeconds, ageBuckets, pruneAgedBuckets, collect }: {
|
|
120
|
+
name: string;
|
|
121
|
+
help: string;
|
|
122
|
+
percentiles?: number[];
|
|
123
|
+
maxAgeSeconds?: number;
|
|
124
|
+
ageBuckets?: number;
|
|
125
|
+
pruneAgedBuckets?: boolean;
|
|
126
|
+
collect?: () => void;
|
|
127
|
+
}): client.Summary;
|
|
128
|
+
/**
|
|
129
|
+
* Registers a histogram metric
|
|
130
|
+
* @param name - The name of the metric
|
|
131
|
+
* @param help - The help text for the metric
|
|
132
|
+
* @param buckets - The buckets to calculate for the histogram
|
|
133
|
+
* @param collect - The collect function to use for the histogram
|
|
134
|
+
* @returns The histogram metric
|
|
135
|
+
*/
|
|
136
|
+
registerHistogram({ name, help, buckets }: {
|
|
137
|
+
name: string;
|
|
138
|
+
help: string;
|
|
139
|
+
buckets: number[];
|
|
140
|
+
collect?: () => void;
|
|
141
|
+
}): client.Histogram;
|
|
142
|
+
/**
|
|
143
|
+
* Instruments the knex connection pool and queries
|
|
144
|
+
* @param knexInstance - The knex instance
|
|
145
|
+
*/
|
|
146
|
+
instrumentKnex(knexInstance: Knex): void;
|
|
147
|
+
}
|
|
148
|
+
export {};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PrometheusClient = void 0;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const prom_client_1 = __importDefault(require("prom-client"));
|
|
9
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
10
|
+
/**
|
|
11
|
+
* A client for exporting metrics to Prometheus, based on prom-client
|
|
12
|
+
*/
|
|
13
|
+
class PrometheusClient {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new PrometheusClient instance
|
|
16
|
+
* @param prometheusConfig - The configuration for the PrometheusClient
|
|
17
|
+
*/
|
|
18
|
+
constructor(prometheusConfig = {}, logger = logging_1.default) {
|
|
19
|
+
this.config = prometheusConfig;
|
|
20
|
+
this.client = prom_client_1.default;
|
|
21
|
+
this.prefix = 'ghost_';
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
client;
|
|
25
|
+
gateway; // public for testing
|
|
26
|
+
queries = new Map();
|
|
27
|
+
acquires = new Map();
|
|
28
|
+
creates = new Map();
|
|
29
|
+
config;
|
|
30
|
+
prefix;
|
|
31
|
+
pushInterval;
|
|
32
|
+
pushRetries = 0;
|
|
33
|
+
logger;
|
|
34
|
+
/**
|
|
35
|
+
* Initializes the prometheus client, setting up the pushgateway if enabled
|
|
36
|
+
*/
|
|
37
|
+
init() {
|
|
38
|
+
this.collectDefaultMetrics();
|
|
39
|
+
if (this.config.pushgateway?.enabled) {
|
|
40
|
+
const gatewayUrl = this.config.pushgateway.url || 'http://localhost:9091';
|
|
41
|
+
const interval = this.config.pushgateway.interval || 15000;
|
|
42
|
+
this.gateway = new prom_client_1.default.Pushgateway(gatewayUrl, {
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
agent: new http_1.default.Agent({
|
|
45
|
+
keepAlive: true,
|
|
46
|
+
maxSockets: 1
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
this.pushMetrics();
|
|
50
|
+
this.pushInterval = setInterval(() => {
|
|
51
|
+
this.pushMetrics();
|
|
52
|
+
}, interval);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pushes metrics to the pushgateway, if enabled
|
|
57
|
+
*/
|
|
58
|
+
async pushMetrics() {
|
|
59
|
+
if (this.pushRetries >= 3) {
|
|
60
|
+
this.logger.error('Failed to push metrics to pushgateway 3 times in a row, giving up');
|
|
61
|
+
this.stop();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.config.pushgateway?.enabled && this.gateway) {
|
|
65
|
+
const jobName = this.config.pushgateway?.jobName || 'ghost';
|
|
66
|
+
try {
|
|
67
|
+
await this.gateway.pushAdd({ jobName });
|
|
68
|
+
this.logger.debug('Metrics pushed to pushgateway - jobName: ', jobName);
|
|
69
|
+
this.pushRetries = 0;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
let error;
|
|
73
|
+
if (typeof err === 'object' && err !== null && 'code' in err) {
|
|
74
|
+
error = 'Error pushing metrics to pushgateway: ' + err.code;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
error = 'Error pushing metrics to pushgateway: Unknown error';
|
|
78
|
+
}
|
|
79
|
+
this.logger.error(error);
|
|
80
|
+
this.pushRetries = this.pushRetries + 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Shuts down the prometheus client cleanly
|
|
86
|
+
*/
|
|
87
|
+
stop() {
|
|
88
|
+
// Clear the push interval
|
|
89
|
+
if (this.pushInterval) {
|
|
90
|
+
clearInterval(this.pushInterval);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Tells prom-client to collect default metrics
|
|
95
|
+
* Only called once on init
|
|
96
|
+
*/
|
|
97
|
+
collectDefaultMetrics() {
|
|
98
|
+
this.client.collectDefaultMetrics({ prefix: this.prefix });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Handles metrics requests to serve the /metrics endpoint
|
|
102
|
+
* @param req - The request object
|
|
103
|
+
* @param res - The response object
|
|
104
|
+
*/
|
|
105
|
+
async handleMetricsRequest(req, res) {
|
|
106
|
+
try {
|
|
107
|
+
res.set('Content-Type', this.getContentType());
|
|
108
|
+
res.end(await this.getMetrics());
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof Error && err.message) {
|
|
112
|
+
res.status(500).end(err.message);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
res.status(500).end('Unknown error');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns the metrics from the registry as a string
|
|
121
|
+
*/
|
|
122
|
+
async getMetrics() {
|
|
123
|
+
return this.client.register.metrics();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Returns the metrics from the registry as a JSON object
|
|
127
|
+
*
|
|
128
|
+
* Particularly useful for testing
|
|
129
|
+
*/
|
|
130
|
+
async getMetricsAsJSON() {
|
|
131
|
+
return this.client.register.getMetricsAsJSON();
|
|
132
|
+
}
|
|
133
|
+
async getMetricsAsArray() {
|
|
134
|
+
return this.client.register.getMetricsAsArray();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns the content type for the metrics
|
|
138
|
+
*/
|
|
139
|
+
getContentType() {
|
|
140
|
+
return this.client.register.contentType;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns a single metric from the registry
|
|
144
|
+
* @param name - The name of the metric
|
|
145
|
+
* @returns The metric
|
|
146
|
+
*/
|
|
147
|
+
getMetric(name) {
|
|
148
|
+
if (!name.startsWith(this.prefix)) {
|
|
149
|
+
name = `${this.prefix}${name}`;
|
|
150
|
+
}
|
|
151
|
+
return this.client.register.getSingleMetric(name);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Returns the metric object of a single metric, if it exists
|
|
155
|
+
* @param name - The name of the metric
|
|
156
|
+
* @returns The values of the metric
|
|
157
|
+
*/
|
|
158
|
+
async getMetricObject(name) {
|
|
159
|
+
const metric = this.getMetric(name);
|
|
160
|
+
if (!metric) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return await metric.get();
|
|
164
|
+
}
|
|
165
|
+
async getMetricValues(name) {
|
|
166
|
+
const metricObject = await this.getMetricObject(name);
|
|
167
|
+
if (!metricObject) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return metricObject.values;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
*
|
|
174
|
+
*/
|
|
175
|
+
/**
|
|
176
|
+
* Registers a counter metric
|
|
177
|
+
* @param name - The name of the metric
|
|
178
|
+
* @param help - The help text for the metric
|
|
179
|
+
* @param labelNames - The names of the labels for the metric
|
|
180
|
+
* @returns The counter metric
|
|
181
|
+
*/
|
|
182
|
+
registerCounter({ name, help, labelNames = [] }) {
|
|
183
|
+
return new this.client.Counter({
|
|
184
|
+
name: `${this.prefix}${name}`,
|
|
185
|
+
help,
|
|
186
|
+
labelNames
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Registers a gauge metric
|
|
191
|
+
* @param name - The name of the metric
|
|
192
|
+
* @param help - The help text for the metric
|
|
193
|
+
* @param collect - The collect function to use for the gauge
|
|
194
|
+
* @returns The gauge metric
|
|
195
|
+
*/
|
|
196
|
+
registerGauge({ name, help, collect }) {
|
|
197
|
+
return new this.client.Gauge({
|
|
198
|
+
name: `${this.prefix}${name}`,
|
|
199
|
+
help,
|
|
200
|
+
collect
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Registers a summary metric
|
|
205
|
+
* @param name - The name of the metric
|
|
206
|
+
* @param help - The help text for the metric
|
|
207
|
+
* @param percentiles - The percentiles to calculate for the summary
|
|
208
|
+
* @param collect - The collect function to use for the summary
|
|
209
|
+
* @returns The summary metric
|
|
210
|
+
*/
|
|
211
|
+
registerSummary({ name, help, percentiles, maxAgeSeconds, ageBuckets, pruneAgedBuckets, collect }) {
|
|
212
|
+
return new this.client.Summary({
|
|
213
|
+
name: `${this.prefix}${name}`,
|
|
214
|
+
help,
|
|
215
|
+
percentiles: percentiles || [0.5, 0.9, 0.99],
|
|
216
|
+
maxAgeSeconds,
|
|
217
|
+
ageBuckets,
|
|
218
|
+
pruneAgedBuckets,
|
|
219
|
+
collect
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Registers a histogram metric
|
|
224
|
+
* @param name - The name of the metric
|
|
225
|
+
* @param help - The help text for the metric
|
|
226
|
+
* @param buckets - The buckets to calculate for the histogram
|
|
227
|
+
* @param collect - The collect function to use for the histogram
|
|
228
|
+
* @returns The histogram metric
|
|
229
|
+
*/
|
|
230
|
+
registerHistogram({ name, help, buckets }) {
|
|
231
|
+
return new this.client.Histogram({
|
|
232
|
+
name: `${this.prefix}${name}`,
|
|
233
|
+
help,
|
|
234
|
+
buckets: buckets
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Utility functions for creating custom metrics
|
|
238
|
+
/**
|
|
239
|
+
* Instruments the knex connection pool and queries
|
|
240
|
+
* @param knexInstance - The knex instance
|
|
241
|
+
*/
|
|
242
|
+
instrumentKnex(knexInstance) {
|
|
243
|
+
// Create some gauges for tracking the connection pool
|
|
244
|
+
this.registerGauge({
|
|
245
|
+
name: `db_connection_pool_max`,
|
|
246
|
+
help: 'The maximum number of connections allowed in the pool',
|
|
247
|
+
collect() {
|
|
248
|
+
this.set(knexInstance.client.pool.max);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
this.registerGauge({
|
|
252
|
+
name: `db_connection_pool_min`,
|
|
253
|
+
help: 'The minimum number of connections allowed in the pool',
|
|
254
|
+
collect() {
|
|
255
|
+
this.set(knexInstance.client.pool.min);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
this.registerGauge({
|
|
259
|
+
name: `db_connection_pool_active`,
|
|
260
|
+
help: 'The number of active connections to the database, which can be in use or idle',
|
|
261
|
+
collect() {
|
|
262
|
+
this.set(knexInstance.client.pool.numUsed() + knexInstance.client.pool.numFree());
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
this.registerGauge({
|
|
266
|
+
name: `db_connection_pool_used`,
|
|
267
|
+
help: 'The number of connections currently in use by the database',
|
|
268
|
+
collect() {
|
|
269
|
+
this.set(knexInstance.client.pool.numUsed());
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
this.registerGauge({
|
|
273
|
+
name: `db_connection_pool_idle`,
|
|
274
|
+
help: 'The number of active connections currently idle in pool',
|
|
275
|
+
collect() {
|
|
276
|
+
this.set(knexInstance.client.pool.numFree());
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
this.registerGauge({
|
|
280
|
+
name: `db_connection_pool_pending_acquires`,
|
|
281
|
+
help: 'The number of connections currently waiting to be acquired from the pool',
|
|
282
|
+
collect() {
|
|
283
|
+
this.set(knexInstance.client.pool.numPendingAcquires());
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
this.registerGauge({
|
|
287
|
+
name: `db_connection_pool_pending_creates`,
|
|
288
|
+
help: 'The number of connections currently waiting to be created',
|
|
289
|
+
collect() {
|
|
290
|
+
this.set(knexInstance.client.pool.numPendingCreates());
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const queryDurationSummary = this.registerSummary({
|
|
294
|
+
name: `db_query_duration_seconds`,
|
|
295
|
+
help: 'Summary of the duration of knex database queries in seconds',
|
|
296
|
+
percentiles: [0.5, 0.9, 0.99],
|
|
297
|
+
maxAgeSeconds: 60,
|
|
298
|
+
ageBuckets: 6,
|
|
299
|
+
pruneAgedBuckets: false
|
|
300
|
+
});
|
|
301
|
+
const acquireDurationSummary = this.registerSummary({
|
|
302
|
+
name: `db_connection_acquire_duration_seconds`,
|
|
303
|
+
help: 'Summary of the duration of acquiring a connection from the pool in seconds',
|
|
304
|
+
percentiles: [0.5, 0.9, 0.99],
|
|
305
|
+
maxAgeSeconds: 60,
|
|
306
|
+
ageBuckets: 6,
|
|
307
|
+
pruneAgedBuckets: false
|
|
308
|
+
});
|
|
309
|
+
const createDurationSummary = this.registerSummary({
|
|
310
|
+
name: `db_connection_create_duration_seconds`,
|
|
311
|
+
help: 'Summary of the duration of creating a connection in seconds',
|
|
312
|
+
percentiles: [0.5, 0.9, 0.99],
|
|
313
|
+
maxAgeSeconds: 60,
|
|
314
|
+
ageBuckets: 6,
|
|
315
|
+
pruneAgedBuckets: false
|
|
316
|
+
});
|
|
317
|
+
knexInstance.on('query', (query) => {
|
|
318
|
+
// Add the query to the map
|
|
319
|
+
this.queries.set(query.__knexQueryUid, queryDurationSummary.startTimer());
|
|
320
|
+
});
|
|
321
|
+
knexInstance.on('query-response', (err, query) => {
|
|
322
|
+
this.queries.get(query.__knexQueryUid)?.();
|
|
323
|
+
this.queries.delete(query.__knexQueryUid);
|
|
324
|
+
});
|
|
325
|
+
knexInstance.client.pool.on('createRequest', (eventId) => {
|
|
326
|
+
this.creates.set(eventId, createDurationSummary.startTimer());
|
|
327
|
+
});
|
|
328
|
+
knexInstance.client.pool.on('createSuccess', (eventId) => {
|
|
329
|
+
this.creates.get(eventId)?.();
|
|
330
|
+
this.creates.delete(eventId);
|
|
331
|
+
});
|
|
332
|
+
knexInstance.client.pool.on('createFail', (eventId) => {
|
|
333
|
+
this.creates.get(eventId)?.();
|
|
334
|
+
this.creates.delete(eventId);
|
|
335
|
+
});
|
|
336
|
+
knexInstance.client.pool.on('acquireRequest', (eventId) => {
|
|
337
|
+
this.acquires.set(eventId, acquireDurationSummary.startTimer());
|
|
338
|
+
});
|
|
339
|
+
knexInstance.client.pool.on('acquireSuccess', (eventId) => {
|
|
340
|
+
this.acquires.get(eventId)?.();
|
|
341
|
+
this.acquires.delete(eventId);
|
|
342
|
+
});
|
|
343
|
+
knexInstance.client.pool.on('acquireFail', (eventId) => {
|
|
344
|
+
this.acquires.get(eventId)?.();
|
|
345
|
+
this.acquires.delete(eventId);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
exports.PrometheusClient = PrometheusClient;
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./MetricsServer"), exports);
|
|
18
|
+
__exportStar(require("./PrometheusClient"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tryghost/prometheus-metrics",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/prometheus-metrics",
|
|
5
|
+
"author": "Ghost Foundation",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "build/index.js",
|
|
10
|
+
"types": "build/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "tsc --watch --preserveWatchOutput --sourceMap",
|
|
13
|
+
"build": "yarn build:ts",
|
|
14
|
+
"build:ts": "tsc",
|
|
15
|
+
"prepare": "tsc",
|
|
16
|
+
"test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --90 --reporter text --reporter cobertura mocha -r ts-node/register './test/**/*.test.ts'",
|
|
17
|
+
"test": "yarn test:types && yarn test:unit",
|
|
18
|
+
"test:types": "tsc --noEmit",
|
|
19
|
+
"lint:code": "eslint src/ --ext .ts --cache",
|
|
20
|
+
"lint": "yarn lint:code && yarn lint:test",
|
|
21
|
+
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"build"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/express": "4.17.21",
|
|
28
|
+
"@types/sinon": "17.0.4",
|
|
29
|
+
"@types/stoppable": "1.1.3",
|
|
30
|
+
"@types/supertest": "6.0.3",
|
|
31
|
+
"c8": "10.1.2",
|
|
32
|
+
"knex": "2.4.2",
|
|
33
|
+
"mocha": "10.8.2",
|
|
34
|
+
"nock": "13.5.6",
|
|
35
|
+
"sinon": "19.0.5",
|
|
36
|
+
"supertest": "7.0.0",
|
|
37
|
+
"ts-node": "10.9.2",
|
|
38
|
+
"typescript": "5.8.3"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@tryghost/logging": "2.4.21",
|
|
42
|
+
"express": "4.21.2",
|
|
43
|
+
"prom-client": "15.1.3",
|
|
44
|
+
"stoppable": "1.1.0"
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "422803e3f596e81f0321c328680e08d8221af5b4"
|
|
47
|
+
}
|