1 | import * as express from 'express';
|
2 | import * as prometheus from 'prom-client';
|
3 | import { TypedError } from 'typed-error';
|
4 |
|
5 | import * as Debug from 'debug';
|
6 | const debug = Debug('node-metrics-gatherer');
|
7 |
|
8 | import { collectAPIMetrics } from './collectors/api/collect';
|
9 |
|
10 | import {
|
11 | AuthTestFunc,
|
12 | ConstructorMap,
|
13 | CustomParams,
|
14 | Kind,
|
15 | LabelSet,
|
16 | MetricConstructor,
|
17 | MetricsMap,
|
18 | MetricsMetaMap,
|
19 | } from './types';
|
20 |
|
21 | export class MetricsGathererError extends TypedError {}
|
22 |
|
23 | const constructors: ConstructorMap = {
|
24 | gauge: new MetricConstructor(prometheus.Gauge),
|
25 | counter: new MetricConstructor(prometheus.Counter),
|
26 | summary: new MetricConstructor(prometheus.Summary),
|
27 | histogram: new MetricConstructor(prometheus.Histogram),
|
28 | };
|
29 |
|
30 | interface Describer {
|
31 | [kind: string]: (
|
32 | name: string,
|
33 | help: string,
|
34 | customParams?: CustomParams,
|
35 | ) => void;
|
36 | }
|
37 |
|
38 | export class MetricsGatherer {
|
39 | public internalErrorCount: number;
|
40 | public meta: MetricsMetaMap;
|
41 | private metrics: MetricsMap;
|
42 | public describe: Describer;
|
43 | public client: any;
|
44 |
|
45 | constructor() {
|
46 | this.initState();
|
47 | this.setupDescribe();
|
48 | this.client = prometheus;
|
49 | }
|
50 |
|
51 | private initState() {
|
52 | try {
|
53 | this.metrics = {
|
54 | gauge: {},
|
55 | counter: {},
|
56 | histogram: {},
|
57 | summary: {},
|
58 | };
|
59 | this.meta = {};
|
60 | this.internalErrorCount = 0;
|
61 | } catch (e) {
|
62 | this.err(e);
|
63 | }
|
64 | }
|
65 |
|
66 | private setupDescribe() {
|
67 | this.describe = {};
|
68 | for (const kind of ['gauge', 'counter', 'histogram', 'summary'] as const) {
|
69 | this.describe[kind] = (
|
70 | name: string,
|
71 | help: string,
|
72 | customParams: CustomParams = {},
|
73 | ) => {
|
74 | if (this.meta[name]) {
|
75 | throw new MetricsGathererError(
|
76 | `tried to describe metric "${name}" twice`,
|
77 | );
|
78 | } else {
|
79 | this.meta[name] = {
|
80 | kind,
|
81 | help,
|
82 | customParams,
|
83 | };
|
84 | }
|
85 | };
|
86 | }
|
87 | }
|
88 |
|
89 |
|
90 | public gauge(name: string, val: number, labels: LabelSet = {}) {
|
91 | try {
|
92 | this.ensureExists('gauge', name, labels);
|
93 | this.metrics.gauge[name].set(labels, val);
|
94 | } catch (e) {
|
95 | this.err(e);
|
96 | }
|
97 | }
|
98 |
|
99 |
|
100 | public inc(name: string, val: number = 1, labels: LabelSet = {}) {
|
101 | try {
|
102 |
|
103 |
|
104 |
|
105 | const kind = /.+_total$/.test(name) ? 'counter' : 'gauge';
|
106 | this.ensureExists(kind, name, labels);
|
107 | if (!this.checkMetricType(name, ['gauge', 'counter'])) {
|
108 | throw new MetricsGathererError(
|
109 | `Tried to increment non-gauge, non-counter metric ${name}`,
|
110 | );
|
111 | }
|
112 | if (this.meta[name].kind === 'gauge') {
|
113 | this.metrics.gauge[name].inc(labels, val);
|
114 | } else {
|
115 | this.metrics.counter[name].inc(labels, val);
|
116 | }
|
117 | } catch (e) {
|
118 | this.err(e);
|
119 | }
|
120 | }
|
121 |
|
122 |
|
123 | public dec(name: string, val: number = 1, labels: LabelSet = {}) {
|
124 | try {
|
125 |
|
126 | this.ensureExists('gauge', name, labels);
|
127 | if (!this.checkMetricType(name, ['gauge'])) {
|
128 | throw new MetricsGathererError(
|
129 | `Tried to decrement non-gauge metric ${name}`,
|
130 | );
|
131 | }
|
132 | this.metrics.gauge[name].dec(labels, val);
|
133 | } catch (e) {
|
134 | this.err(e);
|
135 | }
|
136 | }
|
137 |
|
138 |
|
139 | public counter(name: string, val: number = 1, labels: LabelSet = {}) {
|
140 | try {
|
141 | this.ensureExists('counter', name, labels);
|
142 | this.metrics.counter[name].inc(labels, val);
|
143 | } catch (e) {
|
144 | this.err(e);
|
145 | }
|
146 | }
|
147 |
|
148 |
|
149 | public summary(
|
150 | name: string,
|
151 | val: number,
|
152 | labels: LabelSet = {},
|
153 | customParams: CustomParams = {},
|
154 | ) {
|
155 | try {
|
156 | this.ensureExists('summary', name, labels, customParams);
|
157 | this.metrics.summary[name].observe(labels, val);
|
158 | } catch (e) {
|
159 | this.err(e);
|
160 | }
|
161 | }
|
162 |
|
163 |
|
164 | public histogram(
|
165 | name: string,
|
166 | val: number,
|
167 | labels: LabelSet = {},
|
168 | customParams: CustomParams = {},
|
169 | ) {
|
170 | try {
|
171 | this.ensureExists('histogram', name, labels, customParams);
|
172 | this.metrics.histogram[name].observe(labels, val);
|
173 | } catch (e) {
|
174 | this.err(e);
|
175 | }
|
176 | }
|
177 |
|
178 |
|
179 | public histogramSummary(name: string, val: number, labels: LabelSet = {}) {
|
180 | try {
|
181 | this.histogram(`${name}_hist`, val, labels);
|
182 | this.summary(`${name}_summary`, val, labels);
|
183 | } catch (e) {
|
184 | this.err(e);
|
185 | }
|
186 | }
|
187 |
|
188 |
|
189 | public checkMetricType(name: string, kinds: string[]) {
|
190 | try {
|
191 | return kinds.includes(this.meta[name].kind);
|
192 | } catch (e) {
|
193 | this.err(e);
|
194 | }
|
195 | }
|
196 |
|
197 | public getMetric<T extends string = string>(
|
198 | name: string,
|
199 | ): prometheus.Metric<T> | undefined {
|
200 | if (this.meta[name]) {
|
201 | return this.metrics[this.meta[name].kind][name];
|
202 | }
|
203 | }
|
204 |
|
205 | public exists(name: string) {
|
206 | return this.getMetric(name) != null;
|
207 | }
|
208 |
|
209 |
|
210 | private ensureExists(
|
211 | kind: Kind,
|
212 | name: string,
|
213 | labels: LabelSet = {},
|
214 | customParams: CustomParams = {},
|
215 | ) {
|
216 | try {
|
217 |
|
218 | if (this.exists(name)) {
|
219 | return;
|
220 | }
|
221 |
|
222 | if (!this.meta[name]) {
|
223 | this.describe[kind](name, `undescribed ${kind} metric`, {
|
224 | labelNames: Object.keys(labels),
|
225 | ...customParams,
|
226 | });
|
227 | } else if (this.meta[name].kind !== kind) {
|
228 |
|
229 | throw new MetricsGathererError(
|
230 | `tried to use ${name} twice - first as ` +
|
231 | `${this.meta[name].kind}, then as ${kind}`,
|
232 | );
|
233 | }
|
234 |
|
235 | this.metrics[kind][name] = constructors[kind].create({
|
236 | name,
|
237 | help: this.meta[name].help,
|
238 | labelNames: Object.keys(labels),
|
239 | ...customParams,
|
240 | ...this.meta[name].customParams,
|
241 | });
|
242 | } catch (e) {
|
243 | this.err(e);
|
244 | }
|
245 | }
|
246 |
|
247 |
|
248 | public reset(name?: string) {
|
249 | try {
|
250 | if (!name) {
|
251 | prometheus.register.resetMetrics();
|
252 | } else {
|
253 | const metric = this.getMetric(name);
|
254 | if (metric) {
|
255 | metric.reset();
|
256 | }
|
257 | }
|
258 | } catch (e) {
|
259 | this.err(e);
|
260 | }
|
261 | }
|
262 |
|
263 |
|
264 |
|
265 | public exportOn(
|
266 | port: number,
|
267 | path: string = '/metrics',
|
268 | requestHandler?: express.Handler,
|
269 | ) {
|
270 | const app = express();
|
271 | app.use(path, requestHandler ?? this.requestHandler());
|
272 | app.listen(port);
|
273 | }
|
274 |
|
275 |
|
276 | public requestHandler(authTest?: AuthTestFunc): express.Handler {
|
277 | return (req: express.Request, res: express.Response) => {
|
278 | if (authTest && !authTest(req)) {
|
279 | return res.status(403).send();
|
280 | }
|
281 | res.writeHead(200, { 'Content-Type': 'text/plain' });
|
282 | res.end(prometheus.register.metrics());
|
283 | };
|
284 | }
|
285 |
|
286 |
|
287 |
|
288 | public aggregateRequestHandler(authTest?: AuthTestFunc): express.Handler {
|
289 | const aggregatorRegistry = new prometheus.AggregatorRegistry();
|
290 | return (req, res) => {
|
291 | if (authTest && !authTest(req)) {
|
292 | return res.status(403).send();
|
293 | }
|
294 | aggregatorRegistry
|
295 | .clusterMetrics()
|
296 | .then((metrics: string) => {
|
297 | res.set('Content-Type', aggregatorRegistry.contentType);
|
298 | res.send(metrics);
|
299 | })
|
300 | .catch((err: Error) => {
|
301 | this.err(err);
|
302 | res.status(500).send();
|
303 | });
|
304 | };
|
305 | }
|
306 |
|
307 |
|
308 | public collectDefaultMetrics() {
|
309 | prometheus.collectDefaultMetrics();
|
310 | }
|
311 |
|
312 |
|
313 | public collectAPIMetrics(app: express.Application): express.Application {
|
314 | app.use(collectAPIMetrics(this));
|
315 | return app;
|
316 | }
|
317 |
|
318 |
|
319 | public output(): string {
|
320 | try {
|
321 | return prometheus.register.metrics();
|
322 | } catch (e) {
|
323 | this.err(e);
|
324 | return '';
|
325 | }
|
326 | }
|
327 |
|
328 |
|
329 | public clear() {
|
330 | try {
|
331 | prometheus.register.clear();
|
332 | this.initState();
|
333 | } catch (e) {
|
334 | this.err(e);
|
335 | }
|
336 | }
|
337 |
|
338 | private err(e: Error) {
|
339 | debug(e);
|
340 | this.internalErrorCount++;
|
341 | }
|
342 | }
|