@lokalise/prisma-utils 3.0.1 → 3.2.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/README.md +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/plugins/CollectionScheduler.d.ts +12 -0
- package/dist/plugins/CollectionScheduler.js +23 -0
- package/dist/plugins/MetricsCollector.d.ts +31 -0
- package/dist/plugins/MetricsCollector.js +138 -0
- package/dist/plugins/prismaMetricsPlugin.d.ts +21 -0
- package/dist/plugins/prismaMetricsPlugin.js +52 -0
- package/package.json +15 -13
package/README.md
CHANGED
|
@@ -14,3 +14,14 @@ const result: Either<unknown, [Item, Segment]> = await prismaTransaction(prisma,
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
This implementation will retry the transaction on P2034 error, which satisfies Prisma recommendations for distributed databases such as CockroachDB.
|
|
17
|
+
|
|
18
|
+
### Prisma metrics plugin
|
|
19
|
+
|
|
20
|
+
Plugin to collect and send metrics to prometheus. Prisma metrics will be added to our app metrics.
|
|
21
|
+
|
|
22
|
+
Add the plugin to your Fastify instance by registering it with the following options:
|
|
23
|
+
|
|
24
|
+
- `isEnabled`;
|
|
25
|
+
- `collectionOptions` (by default we collect metrics `every 5 seconds`) to override default collector behaviour
|
|
26
|
+
|
|
27
|
+
Once the plugin has been added to your Fastify instance and loaded, we will start collection prisma metrics.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export type * from './types';
|
|
|
2
2
|
export * from './errors';
|
|
3
3
|
export { prismaTransaction } from './prismaTransaction';
|
|
4
4
|
export { prismaClientFactory } from './prismaClientFactory';
|
|
5
|
+
export { prismaMetricsPlugin, type PrismaMetricsPluginOptions } from './plugins/prismaMetricsPlugin';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.prismaClientFactory = exports.prismaTransaction = void 0;
|
|
3
|
+
exports.prismaMetricsPlugin = exports.prismaClientFactory = exports.prismaTransaction = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
tslib_1.__exportStar(require("./errors"), exports);
|
|
6
6
|
var prismaTransaction_1 = require("./prismaTransaction");
|
|
7
7
|
Object.defineProperty(exports, "prismaTransaction", { enumerable: true, get: function () { return prismaTransaction_1.prismaTransaction; } });
|
|
8
8
|
var prismaClientFactory_1 = require("./prismaClientFactory");
|
|
9
9
|
Object.defineProperty(exports, "prismaClientFactory", { enumerable: true, get: function () { return prismaClientFactory_1.prismaClientFactory; } });
|
|
10
|
+
var prismaMetricsPlugin_1 = require("./plugins/prismaMetricsPlugin");
|
|
11
|
+
Object.defineProperty(exports, "prismaMetricsPlugin", { enumerable: true, get: function () { return prismaMetricsPlugin_1.prismaMetricsPlugin; } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type CollectionScheduler = {
|
|
2
|
+
start: () => void;
|
|
3
|
+
stop: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare class PromiseBasedCollectionScheduler implements CollectionScheduler {
|
|
6
|
+
private active;
|
|
7
|
+
private readonly collectionIntervalInMs;
|
|
8
|
+
private readonly collect;
|
|
9
|
+
constructor(collectionIntervalInMs: number, collect: () => Promise<void>);
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PromiseBasedCollectionScheduler = void 0;
|
|
4
|
+
const promises_1 = require("node:timers/promises");
|
|
5
|
+
class PromiseBasedCollectionScheduler {
|
|
6
|
+
active = true;
|
|
7
|
+
collectionIntervalInMs;
|
|
8
|
+
collect;
|
|
9
|
+
constructor(collectionIntervalInMs, collect) {
|
|
10
|
+
this.collectionIntervalInMs = collectionIntervalInMs;
|
|
11
|
+
this.collect = collect;
|
|
12
|
+
}
|
|
13
|
+
async start() {
|
|
14
|
+
while (this.active) {
|
|
15
|
+
await this.collect();
|
|
16
|
+
await (0, promises_1.setTimeout)(this.collectionIntervalInMs);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
stop() {
|
|
20
|
+
this.active = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.PromiseBasedCollectionScheduler = PromiseBasedCollectionScheduler;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PrismaClient } from '@prisma/client';
|
|
2
|
+
import type { FastifyBaseLogger } from 'fastify';
|
|
3
|
+
import * as prometheus from 'prom-client';
|
|
4
|
+
export type PrometheusMetricsDefinitions = {
|
|
5
|
+
counters: Record<string, prometheus.Counter<'prisma' | 'connection-pool'>>;
|
|
6
|
+
gauges: Record<string, prometheus.Gauge<'prisma' | 'connection-pool'>>;
|
|
7
|
+
histograms: Record<string, prometheus.Histogram<'prisma' | 'connection-pool'>>;
|
|
8
|
+
names: string[];
|
|
9
|
+
};
|
|
10
|
+
export type MetricCollectorOptions = {
|
|
11
|
+
metricsPrefix: string;
|
|
12
|
+
};
|
|
13
|
+
export declare class MetricsCollector {
|
|
14
|
+
private readonly prisma;
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly registry;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
private metrics?;
|
|
19
|
+
constructor(prisma: PrismaClient, options: MetricCollectorOptions, registry: prometheus.Registry, logger: FastifyBaseLogger);
|
|
20
|
+
/**
|
|
21
|
+
* Updates metrics for prisma
|
|
22
|
+
*/
|
|
23
|
+
collect(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Stops the metrics collection and cleans up resources
|
|
26
|
+
*/
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
private registerMetrics;
|
|
29
|
+
private getRegisteredMetrics;
|
|
30
|
+
private getJsonMetrics;
|
|
31
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsCollector = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const prometheus = tslib_1.__importStar(require("prom-client"));
|
|
6
|
+
function registerMetrics(_prefix, jsonMetrics) {
|
|
7
|
+
const metrics = {
|
|
8
|
+
counters: {},
|
|
9
|
+
gauges: {},
|
|
10
|
+
histograms: {},
|
|
11
|
+
names: [],
|
|
12
|
+
};
|
|
13
|
+
for (const metric of jsonMetrics.counters) {
|
|
14
|
+
metrics.counters[metric.key] = new prometheus.Counter({
|
|
15
|
+
name: metric.key,
|
|
16
|
+
help: metric.description,
|
|
17
|
+
labelNames: ['prisma', 'connection_pool'],
|
|
18
|
+
});
|
|
19
|
+
metrics.names.push(metric.key);
|
|
20
|
+
}
|
|
21
|
+
for (const metric of jsonMetrics.gauges) {
|
|
22
|
+
metrics.gauges[metric.key] = new prometheus.Gauge({
|
|
23
|
+
name: metric.key,
|
|
24
|
+
help: metric.description,
|
|
25
|
+
labelNames: ['prisma', 'connection_pool'],
|
|
26
|
+
});
|
|
27
|
+
metrics.names.push(metric.key);
|
|
28
|
+
}
|
|
29
|
+
for (const metric of jsonMetrics.histograms) {
|
|
30
|
+
metrics.histograms[metric.key] = new prometheus.Histogram({
|
|
31
|
+
name: metric.key,
|
|
32
|
+
help: metric.description,
|
|
33
|
+
buckets: metric.value.buckets.filter((bucket) => bucket[1] === 0).map((bucket) => bucket[0]),
|
|
34
|
+
labelNames: ['prisma', 'connection_pool'],
|
|
35
|
+
});
|
|
36
|
+
metrics.names.push(metric.key);
|
|
37
|
+
}
|
|
38
|
+
return metrics;
|
|
39
|
+
}
|
|
40
|
+
function getMetricKeys(_prefix, jsonMetrics) {
|
|
41
|
+
const metricKeys = [];
|
|
42
|
+
metricKeys.push(...jsonMetrics.counters.map((metric) => metric.key));
|
|
43
|
+
metricKeys.push(...jsonMetrics.gauges.map((metric) => metric.key));
|
|
44
|
+
metricKeys.push(...jsonMetrics.histograms.map((metric) => metric.key));
|
|
45
|
+
return metricKeys;
|
|
46
|
+
}
|
|
47
|
+
class MetricsCollector {
|
|
48
|
+
prisma;
|
|
49
|
+
options;
|
|
50
|
+
registry;
|
|
51
|
+
logger;
|
|
52
|
+
metrics;
|
|
53
|
+
constructor(prisma, options, registry, logger) {
|
|
54
|
+
this.prisma = prisma;
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.registry = registry;
|
|
57
|
+
this.logger = logger;
|
|
58
|
+
this.registerMetrics(this.registry, this.options).then((result) => {
|
|
59
|
+
this.metrics = result;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Updates metrics for prisma
|
|
64
|
+
*/
|
|
65
|
+
async collect() {
|
|
66
|
+
if (!this.metrics) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const jsonMetrics = await this.getJsonMetrics();
|
|
71
|
+
for (const counterMetric of jsonMetrics.counters) {
|
|
72
|
+
// we need to reset counter since prisma returns already the accumulated counter value
|
|
73
|
+
this.metrics.counters[counterMetric.key].reset();
|
|
74
|
+
this.metrics.counters[counterMetric.key].inc(counterMetric.value);
|
|
75
|
+
}
|
|
76
|
+
for (const gaugeMetric of jsonMetrics.gauges) {
|
|
77
|
+
this.metrics.gauges[gaugeMetric.key].set(gaugeMetric.value);
|
|
78
|
+
}
|
|
79
|
+
for (const histogramMetric of jsonMetrics.histograms) {
|
|
80
|
+
this.metrics.histograms[histogramMetric.key].observe(histogramMetric.value.count);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
/* c8 ignore start */
|
|
85
|
+
this.logger.error(err);
|
|
86
|
+
}
|
|
87
|
+
/* c8 ignore stop */
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Stops the metrics collection and cleans up resources
|
|
91
|
+
*/
|
|
92
|
+
async dispose() { }
|
|
93
|
+
async registerMetrics(registry, { metricsPrefix }) {
|
|
94
|
+
const jsonMetrics = await this.getJsonMetrics();
|
|
95
|
+
const metricNames = getMetricKeys(metricsPrefix, jsonMetrics);
|
|
96
|
+
// If metrics are already registered, just return them to avoid triggering a Prometheus error
|
|
97
|
+
const existingMetrics = this.getRegisteredMetrics(registry, metricNames);
|
|
98
|
+
if (existingMetrics) {
|
|
99
|
+
/* c8 ignore start */
|
|
100
|
+
return existingMetrics;
|
|
101
|
+
}
|
|
102
|
+
/* c8 ignore stop */
|
|
103
|
+
return registerMetrics(metricsPrefix, jsonMetrics);
|
|
104
|
+
}
|
|
105
|
+
getRegisteredMetrics(registry, metricNames) {
|
|
106
|
+
// If metrics are already registered, just return them to avoid triggering a Prometheus error
|
|
107
|
+
if (!metricNames.length || !registry.getSingleMetric(metricNames[0])) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
/* c8 ignore start */
|
|
111
|
+
const retrievedMetrics = registry.getMetricsAsArray();
|
|
112
|
+
const returnValue = {
|
|
113
|
+
counters: {},
|
|
114
|
+
histograms: {},
|
|
115
|
+
gauges: {},
|
|
116
|
+
};
|
|
117
|
+
for (const metric of retrievedMetrics) {
|
|
118
|
+
if (!metricNames.includes(metric.name)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (metric.type.toString() === 'counter') {
|
|
122
|
+
returnValue.counters[metric.name] = metric;
|
|
123
|
+
}
|
|
124
|
+
if (metric.type.toString() === 'gauge') {
|
|
125
|
+
returnValue.gauges[metric.name] = metric;
|
|
126
|
+
}
|
|
127
|
+
if (metric.type.toString() === 'histogram') {
|
|
128
|
+
returnValue.histograms[metric.name] = metric;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return returnValue;
|
|
132
|
+
/* c8 ignore stop */
|
|
133
|
+
}
|
|
134
|
+
getJsonMetrics() {
|
|
135
|
+
return this.prisma.$metrics.json();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.MetricsCollector = MetricsCollector;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from 'fastify';
|
|
2
|
+
import 'fastify-metrics';
|
|
3
|
+
import type { PrismaClient } from '@prisma/client';
|
|
4
|
+
import type { MetricCollectorOptions } from './MetricsCollector';
|
|
5
|
+
declare module 'fastify' {
|
|
6
|
+
interface FastifyInstance {
|
|
7
|
+
prismaMetrics: {
|
|
8
|
+
collect: () => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export type PrismaMetricsPluginOptions = {
|
|
13
|
+
prisma: PrismaClient;
|
|
14
|
+
collectionOptions?: {
|
|
15
|
+
type: 'interval';
|
|
16
|
+
intervalInMs: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'manual';
|
|
19
|
+
};
|
|
20
|
+
} & Partial<MetricCollectorOptions>;
|
|
21
|
+
export declare const prismaMetricsPlugin: FastifyPluginCallback<PrismaMetricsPluginOptions>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prismaMetricsPlugin = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
require("fastify-metrics");
|
|
6
|
+
const fastify_plugin_1 = tslib_1.__importDefault(require("fastify-plugin"));
|
|
7
|
+
const CollectionScheduler_1 = require("./CollectionScheduler");
|
|
8
|
+
const MetricsCollector_1 = require("./MetricsCollector");
|
|
9
|
+
function plugin(fastify, pluginOptions, next) {
|
|
10
|
+
if (!fastify.metrics) {
|
|
11
|
+
return next(new Error('No Prometheus Client found, Prisma metrics plugin requires `fastify-metrics` plugin to be registered'));
|
|
12
|
+
}
|
|
13
|
+
const options = {
|
|
14
|
+
collectionOptions: {
|
|
15
|
+
type: 'interval',
|
|
16
|
+
intervalInMs: 5000,
|
|
17
|
+
},
|
|
18
|
+
...pluginOptions,
|
|
19
|
+
metricsPrefix: 'prisma',
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const collector = new MetricsCollector_1.MetricsCollector(options.prisma, options, fastify.metrics.client.register, fastify.log);
|
|
23
|
+
const collectFn = async () => await collector.collect();
|
|
24
|
+
let scheduler;
|
|
25
|
+
if (options.collectionOptions.type === 'interval') {
|
|
26
|
+
scheduler = new CollectionScheduler_1.PromiseBasedCollectionScheduler(options.collectionOptions.intervalInMs, collectFn);
|
|
27
|
+
// Void is set so the scheduler can run indefinitely
|
|
28
|
+
void scheduler.start();
|
|
29
|
+
}
|
|
30
|
+
fastify.addHook('onClose', async () => {
|
|
31
|
+
if (scheduler) {
|
|
32
|
+
scheduler.stop();
|
|
33
|
+
}
|
|
34
|
+
await collector.dispose();
|
|
35
|
+
});
|
|
36
|
+
fastify.decorate('prismaMetrics', {
|
|
37
|
+
collect: collectFn,
|
|
38
|
+
});
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
/* c8 ignore start */
|
|
43
|
+
return next(err instanceof Error
|
|
44
|
+
? err
|
|
45
|
+
: new Error('Unknown error in prisma-metrics-plugin', { cause: err }));
|
|
46
|
+
/* c8 ignore stop */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.prismaMetricsPlugin = (0, fastify_plugin_1.default)(plugin, {
|
|
50
|
+
fastify: '5.x',
|
|
51
|
+
name: 'prisma-metrics-plugin',
|
|
52
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokalise/prisma-utils",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": ["dist", "README.md", "LICENSE.md"],
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"homepage": "https://github.com/lokalise/shared-ts-libs",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "
|
|
12
|
+
"url": "https://github.com/lokalise/shared-ts-libs.git"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "rimraf dist && npm run db:update-client && tsc",
|
|
@@ -31,22 +31,24 @@
|
|
|
31
31
|
"postversion": "biome check --write package.json"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@lokalise/node-core": "^
|
|
34
|
+
"@lokalise/node-core": "^13.1.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@prisma/client": "^5.
|
|
38
|
-
"prisma": "^5.
|
|
37
|
+
"@prisma/client": "^5.0.0",
|
|
38
|
+
"prisma": "^5.0.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "^1.
|
|
42
|
-
"@lokalise/biome-config": "^1.
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
41
|
+
"@biomejs/biome": "^1.9.4",
|
|
42
|
+
"@lokalise/biome-config": "^1.5.0",
|
|
43
|
+
"@lokalise/backend-http-client": "^2.3.0",
|
|
44
|
+
"@lokalise/fastify-extras": "^25.0.0",
|
|
45
|
+
"@prisma/client": "^5.21.1",
|
|
46
|
+
"@vitest/coverage-v8": "^2.1.3",
|
|
45
47
|
"cross-env": "^7.0.3",
|
|
46
48
|
"dotenv-cli": "^7.4.1",
|
|
47
|
-
"prisma": "^5.
|
|
48
|
-
"rimraf": "^
|
|
49
|
-
"typescript": "5.
|
|
50
|
-
"vitest": "^2.
|
|
49
|
+
"prisma": "^5.21.1",
|
|
50
|
+
"rimraf": "^6.0.1",
|
|
51
|
+
"typescript": "5.6.3",
|
|
52
|
+
"vitest": "^2.1.3"
|
|
51
53
|
}
|
|
52
54
|
}
|