@rsdk/db.mikro-orm 5.15.0-next.9 → 5.15.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/metrics/entity-operations.subscriber.d.ts +15 -0
  3. package/dist/metrics/entity-operations.subscriber.js +47 -0
  4. package/dist/metrics/entity-operations.subscriber.js.map +1 -0
  5. package/dist/metrics/flush-metrics.subscriber.d.ts +18 -0
  6. package/dist/metrics/flush-metrics.subscriber.js +37 -0
  7. package/dist/metrics/flush-metrics.subscriber.js.map +1 -0
  8. package/dist/metrics/identity-map.registry.d.ts +23 -0
  9. package/dist/metrics/identity-map.registry.js +41 -0
  10. package/dist/metrics/identity-map.registry.js.map +1 -0
  11. package/dist/metrics/knex-pool.d.ts +18 -0
  12. package/dist/metrics/knex-pool.js +28 -0
  13. package/dist/metrics/knex-pool.js.map +1 -0
  14. package/dist/metrics/mikro-orm.metrics.d.ts +7 -0
  15. package/dist/metrics/mikro-orm.metrics.js +51 -0
  16. package/dist/metrics/mikro-orm.metrics.js.map +1 -0
  17. package/dist/metrics/subscribers.d.ts +25 -0
  18. package/dist/metrics/subscribers.js +36 -0
  19. package/dist/metrics/subscribers.js.map +1 -0
  20. package/dist/mikro-orm.plugin.js +2 -0
  21. package/dist/mikro-orm.plugin.js.map +1 -1
  22. package/dist/modules/metrics.module.d.ts +32 -0
  23. package/dist/modules/metrics.module.js +101 -0
  24. package/dist/modules/metrics.module.js.map +1 -0
  25. package/package.json +2 -2
  26. package/src/metrics/entity-operations.subscriber.ts +60 -0
  27. package/src/metrics/flush-metrics.subscriber.ts +48 -0
  28. package/src/metrics/identity-map.registry.spec.ts +121 -0
  29. package/src/metrics/identity-map.registry.ts +48 -0
  30. package/src/metrics/knex-pool.spec.ts +38 -0
  31. package/src/metrics/knex-pool.ts +37 -0
  32. package/src/metrics/mikro-orm.metrics.ts +35 -0
  33. package/src/metrics/subscribers.spec.ts +164 -0
  34. package/src/metrics/subscribers.ts +40 -0
  35. package/src/mikro-orm.plugin.ts +2 -0
  36. package/src/modules/metrics.module.spec.ts +267 -0
  37. package/src/modules/metrics.module.ts +128 -0
@@ -0,0 +1,267 @@
1
+ import type { MikroORM } from '@mikro-orm/core';
2
+ import { EventManager, EventType } from '@mikro-orm/core';
3
+ import type { DbMetricsConfig } from '@rsdk/db';
4
+ import type {
5
+ DbClientMetrics,
6
+ DbMetricProvider,
7
+ EntityOperationsRecorder,
8
+ } from '@rsdk/db/dist/metrics/plugin-kit';
9
+ import {
10
+ DB_CLIENT_METRICS_TOKEN,
11
+ dbMetricsModuleProviders,
12
+ } from '@rsdk/db/dist/metrics/plugin-kit';
13
+ import type { ILogger } from '@rsdk/logging';
14
+
15
+ import type {
16
+ MikroOrmFlushChangesets,
17
+ MikroOrmFlushDuration,
18
+ } from '../metrics/mikro-orm.metrics';
19
+
20
+ import { MikroOrmMetricsModule } from './metrics.module';
21
+
22
+ const noop = (): void => {};
23
+
24
+ /**
25
+ * Все члены, которые проверяет и использует инструментирование tarn
26
+ */
27
+ const tarnPool = (): { on: jest.Mock } & Record<string, unknown> => ({
28
+ max: 1,
29
+ numUsed: () => 0,
30
+ numFree: () => 0,
31
+ numPendingAcquires: () => 0,
32
+ destroy: noop,
33
+ on: jest.fn(),
34
+ removeListener: noop,
35
+ free: [],
36
+ used: [],
37
+ });
38
+
39
+ const pgPool = (): object => ({
40
+ totalCount: 0,
41
+ idleCount: 0,
42
+ waitingCount: 0,
43
+ options: { max: 10 },
44
+ connect: noop,
45
+ on: noop,
46
+ removeListener: noop,
47
+ });
48
+
49
+ /**
50
+ * Пул tarn без счётчиков ожидания и свободных соединений
51
+ */
52
+ const partialPool = (): object => ({ max: 1, numUsed: () => 0, on: noop });
53
+
54
+ const config = {
55
+ isEnabled: true,
56
+ slowQueryThreshold: { millis: () => 500 },
57
+ ormEntityOperationsTotalEnabled: false,
58
+ } as unknown as DbMetricsConfig;
59
+
60
+ /**
61
+ * `DbClientMetrics` из провайдера по токену, так же как его получает Nest:
62
+ * фабрика провайдера с включённым конфигом и `APP_NAME`
63
+ */
64
+ const clientMetrics = (appName: string): DbClientMetrics => {
65
+ const provider = dbMetricsModuleProviders().find(
66
+ (
67
+ candidate,
68
+ ): candidate is Extract<
69
+ DbMetricProvider,
70
+ { provide: typeof DB_CLIENT_METRICS_TOKEN }
71
+ > => candidate.provide === DB_CLIENT_METRICS_TOKEN,
72
+ );
73
+ const client = provider?.useFactory(config, appName);
74
+
75
+ if (!client) {
76
+ throw new Error('The DbClientMetrics provider gave no client');
77
+ }
78
+
79
+ return client;
80
+ };
81
+
82
+ /**
83
+ * MikroORM, у которого `getKnex()` отдаёт `knex`. Без `knex` у соединения
84
+ * нет `getKnex`, как у драйверов не на SQL
85
+ */
86
+ const orm = (knex?: object): MikroORM =>
87
+ ({
88
+ em: {
89
+ getConnection: () => (knex ? { getKnex: () => knex } : {}),
90
+ getEventManager: () => new EventManager([]),
91
+ },
92
+ }) as unknown as MikroORM;
93
+
94
+ describe('MikroOrmMetricsModule pool instrumentation', () => {
95
+ const logger = { warn: jest.fn(), info: jest.fn() };
96
+ let module: MikroOrmMetricsModule | undefined;
97
+
98
+ const init = (
99
+ source: MikroORM,
100
+ client: DbClientMetrics | null = clientMetrics('app'),
101
+ ): void => {
102
+ module = new MikroOrmMetricsModule(
103
+ source,
104
+ client,
105
+ null,
106
+ null,
107
+ null,
108
+ logger as unknown as ILogger,
109
+ );
110
+ module.onModuleInit();
111
+ };
112
+
113
+ afterEach(() => {
114
+ module?.onApplicationShutdown();
115
+ module = undefined;
116
+ logger.warn.mockReset();
117
+ logger.info.mockReset();
118
+ });
119
+
120
+ it('instruments the knex pool', () => {
121
+ const pool = tarnPool();
122
+
123
+ init(orm({ client: { dialect: 'postgresql', pool } }));
124
+
125
+ expect(pool.on).toHaveBeenCalled();
126
+ expect(logger.warn).not.toHaveBeenCalled();
127
+ expect(logger.info).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it.each([
131
+ ['a pg-like pool', pgPool()],
132
+ ['a partial pool', partialPool()],
133
+ ])('warns once without info when the knex pool is %s', (_title, pool) => {
134
+ expect(() =>
135
+ init(orm({ client: { dialect: 'postgresql', pool } })),
136
+ ).not.toThrow();
137
+ expect(logger.warn).toHaveBeenCalledTimes(1);
138
+ expect(logger.warn).toHaveBeenCalledWith(
139
+ expect.stringContaining('app__mikro-orm: expected a tarn pool'),
140
+ );
141
+ expect(logger.info).not.toHaveBeenCalled();
142
+ });
143
+
144
+ // Имя приложения модуль не передаёт: его знает `DbClientMetrics`,
145
+ // которому провайдер внедряет `APP_NAME`
146
+ it('labels the pool with the application name of the injected client', () => {
147
+ init(
148
+ orm({ client: { dialect: 'postgresql', pool: partialPool() } }),
149
+ clientMetrics('other'),
150
+ );
151
+
152
+ expect(logger.warn).toHaveBeenCalledWith(
153
+ expect.stringContaining('other__mikro-orm: expected a tarn pool'),
154
+ );
155
+ });
156
+
157
+ it.each([
158
+ ['postgresql', true],
159
+ ['mysql', false],
160
+ ])(
161
+ 'passes only the resource and instrumentQueries %s: %s',
162
+ (dialect, instrumentQueries) => {
163
+ const client = clientMetrics('app');
164
+ const instrumentPool = jest.spyOn(client, 'instrumentPool');
165
+ const pool = tarnPool();
166
+
167
+ init(orm({ client: { dialect, pool } }), client);
168
+
169
+ expect(instrumentPool.mock.calls).toEqual([
170
+ ['tarn', pool, 'mikro-orm', logger, { instrumentQueries }],
171
+ ]);
172
+ },
173
+ );
174
+
175
+ it('instruments nothing without an injected client', () => {
176
+ const pool = tarnPool();
177
+
178
+ init(orm({ client: { dialect: 'postgresql', pool } }), null);
179
+
180
+ expect(pool.on).not.toHaveBeenCalled();
181
+ expect(logger.warn).not.toHaveBeenCalled();
182
+ expect(logger.info).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it('logs info without a warning when there is no knex', () => {
186
+ init(orm());
187
+
188
+ expect(logger.warn).not.toHaveBeenCalled();
189
+ expect(logger.info).toHaveBeenCalledWith(
190
+ 'Pool metrics are not supported for this driver',
191
+ );
192
+ });
193
+ });
194
+
195
+ describe('MikroOrmMetricsModule injected metrics', () => {
196
+ const logger = { warn: jest.fn(), info: jest.fn() };
197
+ const duration = { observe: jest.fn() };
198
+ const changesets = { observe: jest.fn() };
199
+ const record = jest.fn();
200
+
201
+ const flushArgs = {
202
+ uow: {
203
+ getChangeSets: (): object[] => [
204
+ { type: 'create', meta: { className: 'Cat' } },
205
+ ],
206
+ },
207
+ };
208
+
209
+ const create = (
210
+ events: EventManager,
211
+ client: DbClientMetrics | null,
212
+ ): MikroOrmMetricsModule =>
213
+ new MikroOrmMetricsModule(
214
+ {
215
+ em: { getConnection: () => ({}), getEventManager: () => events },
216
+ } as unknown as MikroORM,
217
+ client,
218
+ { record } as unknown as EntityOperationsRecorder,
219
+ duration as unknown as MikroOrmFlushDuration,
220
+ changesets as unknown as MikroOrmFlushChangesets,
221
+ logger as unknown as ILogger,
222
+ );
223
+
224
+ const flush = async (events: EventManager): Promise<void> => {
225
+ await events.dispatchEvent(EventType.beforeFlush, flushArgs as never);
226
+ await events.dispatchEvent(EventType.afterFlush, flushArgs as never);
227
+ };
228
+
229
+ beforeEach(() => {
230
+ duration.observe.mockClear();
231
+ changesets.observe.mockClear();
232
+ record.mockClear();
233
+ });
234
+
235
+ it('writes flush and entity operations to the injected metrics until shutdown', async () => {
236
+ const events = new EventManager([]);
237
+ const module = create(events, clientMetrics('app'));
238
+
239
+ module.onModuleInit();
240
+ await flush(events);
241
+
242
+ expect(duration.observe).toHaveBeenCalledWith(expect.any(Number));
243
+ expect(changesets.observe).toHaveBeenCalledWith(1);
244
+ expect(record).toHaveBeenCalledWith('Cat', 'insert');
245
+
246
+ module.onApplicationShutdown();
247
+ await flush(events);
248
+
249
+ expect(duration.observe).toHaveBeenCalledTimes(1);
250
+ expect(changesets.observe).toHaveBeenCalledTimes(1);
251
+ expect(record).toHaveBeenCalledTimes(1);
252
+ });
253
+
254
+ it('uses no injected metric without an injected client', async () => {
255
+ const events = new EventManager([]);
256
+ const module = create(events, null);
257
+
258
+ module.onModuleInit();
259
+ await flush(events);
260
+
261
+ expect(duration.observe).not.toHaveBeenCalled();
262
+ expect(changesets.observe).not.toHaveBeenCalled();
263
+ expect(record).not.toHaveBeenCalled();
264
+
265
+ module.onApplicationShutdown();
266
+ });
267
+ });
@@ -0,0 +1,128 @@
1
+ import { MikroORM } from '@mikro-orm/core';
2
+ import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
3
+ import { Inject, Module } from '@nestjs/common';
4
+ import { InjectLogger } from '@rsdk/core';
5
+ import type {
6
+ DbClientMetrics,
7
+ Detach,
8
+ EntityOperationsRecorder,
9
+ } from '@rsdk/db/dist/metrics/plugin-kit';
10
+ import {
11
+ DB_CLIENT_METRICS_TOKEN,
12
+ DbMetricsModuleBase,
13
+ dbMetricsModuleProviders,
14
+ ENTITY_OPERATIONS_RECORDER_TOKEN,
15
+ } from '@rsdk/db/dist/metrics/plugin-kit';
16
+ import { ILogger } from '@rsdk/logging';
17
+
18
+ import { EntityOperationsSubscriber } from '../metrics/entity-operations.subscriber';
19
+ import { FlushMetricsSubscriber } from '../metrics/flush-metrics.subscriber';
20
+ import {
21
+ IdentityMapRegistry,
22
+ identityMapSize,
23
+ } from '../metrics/identity-map.registry';
24
+ import { findKnexPool } from '../metrics/knex-pool';
25
+ import {
26
+ MikroOrmFlushChangesets,
27
+ MikroOrmFlushDuration,
28
+ MikroOrmIdentityMapSize,
29
+ } from '../metrics/mikro-orm.metrics';
30
+ import { registerSubscriber } from '../metrics/subscribers';
31
+
32
+ /**
33
+ * Подключает метрики БД к MikroORM: пул knex, flush, identity map и
34
+ * операции над сущностями. Выключатель `DB_METRICS_ENABLED`, внедрение
35
+ * метрик через DI (`DbClientMetrics` и `EntityOperationsRecorder` — по
36
+ * токенам из `plugin-kit`) и итог при остановке приложения описаны в
37
+ * {@link DbMetricsModuleBase}. Значение метки `pool` собирает ядро из
38
+ * `APP_NAME` и ресурса из {@link findKnexPool}.
39
+ *
40
+ * При остановке subscriber-ы выключаются. Удалить их из EventManager нельзя:
41
+ * публичного способа нет, поэтому они остаются зарегистрированными
42
+ * у закрытого ORM, но ничего не пишут
43
+ */
44
+ @Module({
45
+ providers: dbMetricsModuleProviders([
46
+ MikroOrmFlushDuration,
47
+ MikroOrmFlushChangesets,
48
+ MikroOrmIdentityMapSize,
49
+ ]),
50
+ })
51
+ export class MikroOrmMetricsModule
52
+ extends DbMetricsModuleBase
53
+ implements OnModuleInit, OnApplicationShutdown
54
+ {
55
+ /**
56
+ * @param flushDuration `null`, только когда метрики выключены, а тогда
57
+ * `attach` не вызывается; то же для `flushChangesets`
58
+ */
59
+ constructor(
60
+ private readonly orm: MikroORM,
61
+ @Inject(DB_CLIENT_METRICS_TOKEN) client: DbClientMetrics | null,
62
+ @Inject(ENTITY_OPERATIONS_RECORDER_TOKEN)
63
+ entityOperations: EntityOperationsRecorder | null,
64
+ @Inject(MikroOrmFlushDuration)
65
+ private readonly flushDuration: MikroOrmFlushDuration | null,
66
+ @Inject(MikroOrmFlushChangesets)
67
+ private readonly flushChangesets: MikroOrmFlushChangesets | null,
68
+ @InjectLogger(MikroOrmMetricsModule) private readonly logger: ILogger,
69
+ ) {
70
+ super(client, entityOperations);
71
+ }
72
+
73
+ protected attach(
74
+ add: (detach?: Detach) => void,
75
+ client: DbClientMetrics,
76
+ ): void {
77
+ this.instrumentPool(add, client);
78
+ this.subscribe(add);
79
+ add(IdentityMapRegistry.add(() => identityMapSize(this.orm)));
80
+ }
81
+
82
+ private instrumentPool(
83
+ add: (detach?: Detach) => void,
84
+ client: DbClientMetrics,
85
+ ): void {
86
+ const knexPool = findKnexPool(this.orm);
87
+ if (!knexPool) {
88
+ this.logger.info('Pool metrics are not supported for this driver');
89
+ return;
90
+ }
91
+
92
+ const { resource, pool, isPostgres } = knexPool;
93
+
94
+ // Обёртка `query` рассчитана на соединения `pg`, а они в пуле knex
95
+ // только у диалекта postgresql
96
+
97
+ add(
98
+ client.instrumentPool('tarn', pool, resource, this.logger, {
99
+ instrumentQueries: isPostgres,
100
+ }),
101
+ );
102
+ }
103
+
104
+ private subscribe(add: (detach?: Detach) => void): void {
105
+ const events = this.orm.em.getEventManager();
106
+
107
+ if (this.flushDuration && this.flushChangesets) {
108
+ add(
109
+ registerSubscriber(
110
+ events,
111
+ new FlushMetricsSubscriber({
112
+ duration: this.flushDuration,
113
+ changesets: this.flushChangesets,
114
+ }),
115
+ ),
116
+ );
117
+ }
118
+
119
+ if (this.entityOperations) {
120
+ add(
121
+ registerSubscriber(
122
+ events,
123
+ new EntityOperationsSubscriber(this.entityOperations),
124
+ ),
125
+ );
126
+ }
127
+ }
128
+ }