@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.
- package/CHANGELOG.md +4 -0
- package/dist/metrics/entity-operations.subscriber.d.ts +15 -0
- package/dist/metrics/entity-operations.subscriber.js +47 -0
- package/dist/metrics/entity-operations.subscriber.js.map +1 -0
- package/dist/metrics/flush-metrics.subscriber.d.ts +18 -0
- package/dist/metrics/flush-metrics.subscriber.js +37 -0
- package/dist/metrics/flush-metrics.subscriber.js.map +1 -0
- package/dist/metrics/identity-map.registry.d.ts +23 -0
- package/dist/metrics/identity-map.registry.js +41 -0
- package/dist/metrics/identity-map.registry.js.map +1 -0
- package/dist/metrics/knex-pool.d.ts +18 -0
- package/dist/metrics/knex-pool.js +28 -0
- package/dist/metrics/knex-pool.js.map +1 -0
- package/dist/metrics/mikro-orm.metrics.d.ts +7 -0
- package/dist/metrics/mikro-orm.metrics.js +51 -0
- package/dist/metrics/mikro-orm.metrics.js.map +1 -0
- package/dist/metrics/subscribers.d.ts +25 -0
- package/dist/metrics/subscribers.js +36 -0
- package/dist/metrics/subscribers.js.map +1 -0
- package/dist/mikro-orm.plugin.js +2 -0
- package/dist/mikro-orm.plugin.js.map +1 -1
- package/dist/modules/metrics.module.d.ts +32 -0
- package/dist/modules/metrics.module.js +101 -0
- package/dist/modules/metrics.module.js.map +1 -0
- package/package.json +2 -2
- package/src/metrics/entity-operations.subscriber.ts +60 -0
- package/src/metrics/flush-metrics.subscriber.ts +48 -0
- package/src/metrics/identity-map.registry.spec.ts +121 -0
- package/src/metrics/identity-map.registry.ts +48 -0
- package/src/metrics/knex-pool.spec.ts +38 -0
- package/src/metrics/knex-pool.ts +37 -0
- package/src/metrics/mikro-orm.metrics.ts +35 -0
- package/src/metrics/subscribers.spec.ts +164 -0
- package/src/metrics/subscribers.ts +40 -0
- package/src/mikro-orm.plugin.ts +2 -0
- package/src/modules/metrics.module.spec.ts +267 -0
- package/src/modules/metrics.module.ts +128 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeSet,
|
|
3
|
+
EventArgs,
|
|
4
|
+
EventSubscriber,
|
|
5
|
+
FlushEventArgs,
|
|
6
|
+
} from '@mikro-orm/core';
|
|
7
|
+
import type {
|
|
8
|
+
EntityOperation,
|
|
9
|
+
EntityOperationsRecorder,
|
|
10
|
+
} from '@rsdk/db/dist/metrics/plugin-kit';
|
|
11
|
+
|
|
12
|
+
import { DetachableSubscriber } from './subscribers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ключи — значения `ChangeSetType`. Строками, а не через enum: в MikroORM 5
|
|
16
|
+
* нет части значений (`update_early`, `delete_early`)
|
|
17
|
+
*/
|
|
18
|
+
const CHANGESET_OPERATIONS: Record<string, EntityOperation | undefined> = {
|
|
19
|
+
create: 'insert',
|
|
20
|
+
update: 'update',
|
|
21
|
+
update_early: 'update',
|
|
22
|
+
delete: 'delete',
|
|
23
|
+
delete_early: 'delete',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Пишет `orm_entity_operations_total`: загрузки по onLoad, изменения
|
|
28
|
+
* по changesets после flush. Подписка на onLoad заставляет MikroORM
|
|
29
|
+
* отправлять событие для каждой загруженной сущности, поэтому subscriber
|
|
30
|
+
* регистрируется только при включённой метрике
|
|
31
|
+
*/
|
|
32
|
+
export class EntityOperationsSubscriber
|
|
33
|
+
extends DetachableSubscriber
|
|
34
|
+
implements EventSubscriber
|
|
35
|
+
{
|
|
36
|
+
constructor(private readonly recorder: EntityOperationsRecorder) {
|
|
37
|
+
super();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onLoad({ meta }: EventArgs<object>): void {
|
|
41
|
+
if (!this.attached) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.recorder.record(meta.className, 'load');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
afterFlush({ uow }: FlushEventArgs): void {
|
|
49
|
+
if (!this.attached) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const changeSet of uow.getChangeSets() as ChangeSet<object>[]) {
|
|
54
|
+
const op = CHANGESET_OPERATIONS[changeSet.type];
|
|
55
|
+
if (op) {
|
|
56
|
+
this.recorder.record(changeSet.meta.className, op);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EventSubscriber, FlushEventArgs } from '@mikro-orm/core';
|
|
2
|
+
import type { HistogramLike } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
3
|
+
import { now, secondsSince } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
4
|
+
|
|
5
|
+
import { DetachableSubscriber } from './subscribers';
|
|
6
|
+
|
|
7
|
+
export interface FlushMetrics {
|
|
8
|
+
duration: HistogramLike;
|
|
9
|
+
changesets: HistogramLike;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Меряет длительность flush и число changesets в нём.
|
|
14
|
+
* Упавший flush не доходит до afterFlush и не учитывается
|
|
15
|
+
*/
|
|
16
|
+
export class FlushMetricsSubscriber
|
|
17
|
+
extends DetachableSubscriber
|
|
18
|
+
implements EventSubscriber
|
|
19
|
+
{
|
|
20
|
+
private readonly startedAt = new WeakMap<object, number>();
|
|
21
|
+
|
|
22
|
+
constructor(private readonly metrics: FlushMetrics) {
|
|
23
|
+
super();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeFlush({ uow }: FlushEventArgs): void {
|
|
27
|
+
if (!this.attached) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.startedAt.set(uow, now());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterFlush({ uow }: FlushEventArgs): void {
|
|
35
|
+
if (!this.attached) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const startedAt = this.startedAt.get(uow);
|
|
40
|
+
if (startedAt === undefined) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.startedAt.delete(uow);
|
|
45
|
+
this.metrics.duration.observe(secondsSince(startedAt));
|
|
46
|
+
this.metrics.changesets.observe(uow.getChangeSets().length);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { IdentityMap } from '@mikro-orm/core';
|
|
2
|
+
import { register } from '@rsdk/core';
|
|
3
|
+
import type { DbMetricsConfig } from '@rsdk/db';
|
|
4
|
+
import { dbMetricsModuleProviders } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
5
|
+
|
|
6
|
+
import { countIdentityMap, IdentityMapRegistry } from './identity-map.registry';
|
|
7
|
+
import { MikroOrmIdentityMapSize } from './mikro-orm.metrics';
|
|
8
|
+
|
|
9
|
+
class Account {}
|
|
10
|
+
|
|
11
|
+
const scrapeIdentityMapSize = async (): Promise<number | undefined> => {
|
|
12
|
+
const metrics = await register.metrics();
|
|
13
|
+
const line = metrics
|
|
14
|
+
.split('\n')
|
|
15
|
+
.find((x) => x.startsWith('mikro_orm_identity_map_size '));
|
|
16
|
+
|
|
17
|
+
return line === undefined ? undefined : Number(line.split(' ')[1]);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('IdentityMapRegistry', () => {
|
|
21
|
+
const detach: (() => void)[] = [];
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
// Метрику регистрирует провайдер с включёнными метриками, как в модуле
|
|
25
|
+
// метрик MikroORM
|
|
26
|
+
const provider = dbMetricsModuleProviders([MikroOrmIdentityMapSize]).find(
|
|
27
|
+
({ provide }) => provide === MikroOrmIdentityMapSize,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(
|
|
31
|
+
provider?.useFactory({ isEnabled: true } as DbMetricsConfig, 'spec'),
|
|
32
|
+
).toBeInstanceOf(MikroOrmIdentityMapSize);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
for (const remove of detach.splice(0)) {
|
|
37
|
+
remove();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('sums sources and forgets removed ones', () => {
|
|
42
|
+
const removeFirst = IdentityMapRegistry.add(() => 2);
|
|
43
|
+
const removeSecond = IdentityMapRegistry.add(() => 3);
|
|
44
|
+
|
|
45
|
+
expect(IdentityMapRegistry.total()).toBe(5);
|
|
46
|
+
|
|
47
|
+
removeFirst();
|
|
48
|
+
expect(IdentityMapRegistry.total()).toBe(3);
|
|
49
|
+
|
|
50
|
+
removeSecond();
|
|
51
|
+
expect(IdentityMapRegistry.total()).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does nothing on a second detach', () => {
|
|
55
|
+
const remove = IdentityMapRegistry.add(() => 2);
|
|
56
|
+
|
|
57
|
+
detach.push(IdentityMapRegistry.add(() => 3));
|
|
58
|
+
|
|
59
|
+
remove();
|
|
60
|
+
remove();
|
|
61
|
+
|
|
62
|
+
expect(IdentityMapRegistry.total()).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('skips a source that throws or returns a non-number', () => {
|
|
66
|
+
detach.push(
|
|
67
|
+
IdentityMapRegistry.add(() => {
|
|
68
|
+
throw new Error('orm is closed');
|
|
69
|
+
}),
|
|
70
|
+
IdentityMapRegistry.add(() => undefined as unknown as number),
|
|
71
|
+
IdentityMapRegistry.add(() => 3),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(IdentityMapRegistry.total()).toBe(3);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not fail scrape when one source throws', async () => {
|
|
78
|
+
detach.push(
|
|
79
|
+
IdentityMapRegistry.add(() => {
|
|
80
|
+
throw new RangeError('Maximum call stack size exceeded');
|
|
81
|
+
}),
|
|
82
|
+
IdentityMapRegistry.add(() => 2),
|
|
83
|
+
IdentityMapRegistry.add(() => 5),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await expect(register.metrics()).resolves.toEqual(expect.any(String));
|
|
87
|
+
expect(await scrapeIdentityMapSize()).toBe(7);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('countIdentityMap', () => {
|
|
92
|
+
it('counts a real identity map with hundreds of thousands of entities of one class', async () => {
|
|
93
|
+
const size = 250_000;
|
|
94
|
+
const map = new IdentityMap();
|
|
95
|
+
const store = new Map<string, object>();
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < size; i++) {
|
|
98
|
+
store.set(String(i), {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Публичный store() требует управляемую сущность с __helper; заполняем
|
|
102
|
+
// внутреннее хранилище напрямую
|
|
103
|
+
(map as unknown as { registry: Map<unknown, unknown> }).registry.set(
|
|
104
|
+
Account,
|
|
105
|
+
store,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Проверяем, что при таком размере старый способ через keys() падает
|
|
109
|
+
expect(() => map.keys()).toThrow(RangeError);
|
|
110
|
+
|
|
111
|
+
expect(countIdentityMap(map)).toBe(size);
|
|
112
|
+
|
|
113
|
+
const remove = IdentityMapRegistry.add(() => countIdentityMap(map));
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
expect(await scrapeIdentityMapSize()).toBe(size);
|
|
117
|
+
} finally {
|
|
118
|
+
remove();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MikroORM } from '@mikro-orm/core';
|
|
2
|
+
import type { Detach } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
3
|
+
import { ScrapeSourceRegistry } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
4
|
+
|
|
5
|
+
export type IdentityMapSize = () => number;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Число сущностей в identity map. Считается обходом, а не через `keys()`:
|
|
9
|
+
* `keys()` собирает массив через `push(...spread)` и на сотнях тысяч
|
|
10
|
+
* сущностей одного класса падает с `RangeError` (переполнение стека)
|
|
11
|
+
*/
|
|
12
|
+
export const countIdentityMap = (map: Iterable<unknown>): number => {
|
|
13
|
+
let size = 0;
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
16
|
+
for (const _ of map) {
|
|
17
|
+
size++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return size;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Размер identity map глобального EntityManager
|
|
25
|
+
*/
|
|
26
|
+
export const identityMapSize = (orm: MikroORM): number =>
|
|
27
|
+
countIdentityMap(orm.em.getUnitOfWork(false).getIdentityMap());
|
|
28
|
+
|
|
29
|
+
const isSize = (value: unknown): value is number =>
|
|
30
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
31
|
+
|
|
32
|
+
const registry = new ScrapeSourceRegistry(isSize);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* EntityManager-ы, размер identity map которых gauge читает при scrape.
|
|
36
|
+
* Метрика живёт на весь процесс, а MikroORM — вместе с приложением.
|
|
37
|
+
* Источник, который бросил исключение или вернул не размер (не конечное
|
|
38
|
+
* число ≥ 0), в сумму не попадает: см. `ScrapeSourceRegistry`
|
|
39
|
+
*/
|
|
40
|
+
export class IdentityMapRegistry {
|
|
41
|
+
static add(source: IdentityMapSize): Detach {
|
|
42
|
+
return registry.add(source);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static total(): number {
|
|
46
|
+
return registry.snapshots().reduce((total, size) => total + size, 0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MikroORM } from '@mikro-orm/core';
|
|
2
|
+
|
|
3
|
+
import { findKnexPool } from './knex-pool';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MikroORM, у которого `getKnex()` отдаёт `knex`. Без `knex` у соединения
|
|
7
|
+
* нет `getKnex`, как у драйверов не на SQL
|
|
8
|
+
*/
|
|
9
|
+
const orm = (knex?: object): MikroORM =>
|
|
10
|
+
({
|
|
11
|
+
em: {
|
|
12
|
+
getConnection: () => (knex ? { getKnex: () => knex } : {}),
|
|
13
|
+
},
|
|
14
|
+
}) as unknown as MikroORM;
|
|
15
|
+
|
|
16
|
+
describe('findKnexPool', () => {
|
|
17
|
+
it('finds the pool and names its resource mikro-orm', () => {
|
|
18
|
+
const pool = {};
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
findKnexPool(orm({ client: { dialect: 'postgresql', pool } })),
|
|
22
|
+
).toEqual({ resource: 'mikro-orm', pool, isPostgres: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('marks a pool of another dialect as not postgres', () => {
|
|
26
|
+
expect(
|
|
27
|
+
findKnexPool(orm({ client: { dialect: 'mysql', pool: {} } }))?.isPostgres,
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
['knex', undefined],
|
|
33
|
+
['a client', {}],
|
|
34
|
+
['a pool', { client: { dialect: 'postgresql' } }],
|
|
35
|
+
])('returns undefined when there is no %s', (_title, knex) => {
|
|
36
|
+
expect(findKnexPool(orm(knex))).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MikroORM } from '@mikro-orm/core';
|
|
2
|
+
|
|
3
|
+
export interface KnexPool {
|
|
4
|
+
resource: string;
|
|
5
|
+
pool: unknown;
|
|
6
|
+
isPostgres: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface KnexLike {
|
|
10
|
+
client?: { dialect?: string; pool?: unknown };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Пул knex основного соединения. Knex есть только у SQL-драйверов, и пул
|
|
15
|
+
* у него — tarn при любом диалекте. Без knex или без пула результат
|
|
16
|
+
* `undefined`.
|
|
17
|
+
*
|
|
18
|
+
* Здесь пул только находится: что это действительно пул tarn, проверяет
|
|
19
|
+
* `DbClientMetrics.instrumentPool`.
|
|
20
|
+
*
|
|
21
|
+
* `resource` — вторая часть значения метки `pool`, всегда `mikro-orm`. Имя
|
|
22
|
+
* приложения из `APP_NAME` к нему добавляет `DbClientMetrics`
|
|
23
|
+
*/
|
|
24
|
+
export const findKnexPool = (orm: MikroORM): KnexPool | undefined => {
|
|
25
|
+
const connection = orm.em.getConnection() as { getKnex?: () => KnexLike };
|
|
26
|
+
const client = connection.getKnex?.().client;
|
|
27
|
+
|
|
28
|
+
if (!client?.pool) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
resource: 'mikro-orm',
|
|
34
|
+
pool: client.pool,
|
|
35
|
+
isPostgres: client.dialect === 'postgresql',
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Gauge, Histogram } from '@rsdk/core';
|
|
2
|
+
import { DbMetric, QUERY_BUCKETS } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
3
|
+
|
|
4
|
+
import { IdentityMapRegistry } from './identity-map.registry';
|
|
5
|
+
|
|
6
|
+
const CHANGESET_BUCKETS: readonly number[] = [
|
|
7
|
+
1, 5, 10, 25, 50, 100, 250, 500, 1000,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
@DbMetric('histogram', {
|
|
11
|
+
name: 'mikro_orm_flush_duration_seconds',
|
|
12
|
+
help: 'Duration of EntityManager flush',
|
|
13
|
+
description: 'Длительность em.flush()',
|
|
14
|
+
buckets: QUERY_BUCKETS,
|
|
15
|
+
})
|
|
16
|
+
export class MikroOrmFlushDuration extends Histogram {}
|
|
17
|
+
|
|
18
|
+
@DbMetric('histogram', {
|
|
19
|
+
name: 'mikro_orm_flush_changesets',
|
|
20
|
+
help: 'Number of changesets per flush',
|
|
21
|
+
description: 'Число changesets за один flush',
|
|
22
|
+
buckets: CHANGESET_BUCKETS,
|
|
23
|
+
})
|
|
24
|
+
export class MikroOrmFlushChangesets extends Histogram {}
|
|
25
|
+
|
|
26
|
+
@DbMetric('gauge', {
|
|
27
|
+
name: 'mikro_orm_identity_map_size',
|
|
28
|
+
help: 'Number of entities in the global EntityManager identity map',
|
|
29
|
+
description:
|
|
30
|
+
'Размер identity map глобального EntityManager. Рост означает, что забыли fork или clear',
|
|
31
|
+
collect(this: Gauge): void {
|
|
32
|
+
this.set(IdentityMapRegistry.total());
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
export class MikroOrmIdentityMapSize extends Gauge {}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { EventArgs, FlushEventArgs } from '@mikro-orm/core';
|
|
2
|
+
import { EventManager, EventType } from '@mikro-orm/core';
|
|
3
|
+
import type { EntityOperationsRecorder } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
4
|
+
|
|
5
|
+
import { EntityOperationsSubscriber } from './entity-operations.subscriber';
|
|
6
|
+
import { FlushMetricsSubscriber } from './flush-metrics.subscriber';
|
|
7
|
+
import { registerSubscriber } from './subscribers';
|
|
8
|
+
|
|
9
|
+
const flushArgs = (types: string[]): FlushEventArgs =>
|
|
10
|
+
({
|
|
11
|
+
uow: {
|
|
12
|
+
getChangeSets: () =>
|
|
13
|
+
types.map((type) => ({ type, meta: { className: 'Cat' } })),
|
|
14
|
+
},
|
|
15
|
+
}) as unknown as FlushEventArgs;
|
|
16
|
+
|
|
17
|
+
const loadArgs = {
|
|
18
|
+
meta: { className: 'Cat' },
|
|
19
|
+
} as unknown as EventArgs<object>;
|
|
20
|
+
|
|
21
|
+
describe('FlushMetricsSubscriber', () => {
|
|
22
|
+
const duration = { observe: jest.fn() };
|
|
23
|
+
const changesets = { observe: jest.fn() };
|
|
24
|
+
let subscriber: FlushMetricsSubscriber;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
duration.observe.mockClear();
|
|
28
|
+
changesets.observe.mockClear();
|
|
29
|
+
subscriber = new FlushMetricsSubscriber({ duration, changesets });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('measures flush between beforeFlush and afterFlush', () => {
|
|
33
|
+
const args = flushArgs(['create', 'update']);
|
|
34
|
+
|
|
35
|
+
subscriber.beforeFlush(args);
|
|
36
|
+
subscriber.afterFlush(args);
|
|
37
|
+
|
|
38
|
+
expect(duration.observe).toHaveBeenCalledWith(expect.any(Number));
|
|
39
|
+
expect(changesets.observe).toHaveBeenCalledWith(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('records nothing without beforeFlush', () => {
|
|
43
|
+
subscriber.afterFlush(flushArgs(['create']));
|
|
44
|
+
|
|
45
|
+
expect(duration.observe).not.toHaveBeenCalled();
|
|
46
|
+
expect(changesets.observe).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('records nothing after detach', () => {
|
|
50
|
+
const args = flushArgs(['create']);
|
|
51
|
+
|
|
52
|
+
subscriber.detach();
|
|
53
|
+
subscriber.beforeFlush(args);
|
|
54
|
+
subscriber.afterFlush(args);
|
|
55
|
+
|
|
56
|
+
expect(duration.observe).not.toHaveBeenCalled();
|
|
57
|
+
expect(changesets.observe).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('records nothing for a flush started before detach and finished after it', () => {
|
|
61
|
+
const args = flushArgs(['create']);
|
|
62
|
+
|
|
63
|
+
subscriber.beforeFlush(args);
|
|
64
|
+
subscriber.detach();
|
|
65
|
+
subscriber.afterFlush(args);
|
|
66
|
+
|
|
67
|
+
expect(duration.observe).not.toHaveBeenCalled();
|
|
68
|
+
expect(changesets.observe).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('EntityOperationsSubscriber', () => {
|
|
73
|
+
const record = jest.fn();
|
|
74
|
+
let subscriber: EntityOperationsSubscriber;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
record.mockClear();
|
|
78
|
+
subscriber = new EntityOperationsSubscriber({
|
|
79
|
+
record,
|
|
80
|
+
} as unknown as EntityOperationsRecorder);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('maps changesets to operations', () => {
|
|
84
|
+
subscriber.afterFlush(
|
|
85
|
+
flushArgs([
|
|
86
|
+
'create',
|
|
87
|
+
'update',
|
|
88
|
+
'update_early',
|
|
89
|
+
'delete',
|
|
90
|
+
'delete_early',
|
|
91
|
+
'unknown',
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(record.mock.calls).toEqual([
|
|
96
|
+
['Cat', 'insert'],
|
|
97
|
+
['Cat', 'update'],
|
|
98
|
+
['Cat', 'update'],
|
|
99
|
+
['Cat', 'delete'],
|
|
100
|
+
['Cat', 'delete'],
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('counts a load', () => {
|
|
105
|
+
subscriber.onLoad(loadArgs);
|
|
106
|
+
|
|
107
|
+
expect(record).toHaveBeenCalledWith('Cat', 'load');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('records nothing after detach', () => {
|
|
111
|
+
subscriber.detach();
|
|
112
|
+
subscriber.onLoad(loadArgs);
|
|
113
|
+
subscriber.afterFlush(flushArgs(['create', 'update', 'delete']));
|
|
114
|
+
|
|
115
|
+
expect(record).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('registerSubscriber', () => {
|
|
120
|
+
const record = jest.fn();
|
|
121
|
+
|
|
122
|
+
beforeEach(() => record.mockClear());
|
|
123
|
+
|
|
124
|
+
it('registers the subscriber in EventManager and Detach turns it off', async () => {
|
|
125
|
+
const events = new EventManager([]);
|
|
126
|
+
const detach = registerSubscriber(
|
|
127
|
+
events,
|
|
128
|
+
new EntityOperationsSubscriber({
|
|
129
|
+
record,
|
|
130
|
+
} as unknown as EntityOperationsRecorder),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await events.dispatchEvent(EventType.afterFlush, flushArgs(['create']));
|
|
134
|
+
|
|
135
|
+
expect(record.mock.calls).toEqual([['Cat', 'insert']]);
|
|
136
|
+
|
|
137
|
+
detach();
|
|
138
|
+
await events.dispatchEvent(EventType.afterFlush, flushArgs(['create']));
|
|
139
|
+
|
|
140
|
+
expect(record).toHaveBeenCalledTimes(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('keeps the subscriber off on a second detach', async () => {
|
|
144
|
+
const events = new EventManager([]);
|
|
145
|
+
const detach = registerSubscriber(
|
|
146
|
+
events,
|
|
147
|
+
new EntityOperationsSubscriber({
|
|
148
|
+
record,
|
|
149
|
+
} as unknown as EntityOperationsRecorder),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
detach();
|
|
153
|
+
expect(() => detach()).not.toThrow();
|
|
154
|
+
await events.dispatchEvent(EventType.afterFlush, flushArgs(['create']));
|
|
155
|
+
|
|
156
|
+
expect(record).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not treat detach, detached and attached as hooks', () => {
|
|
160
|
+
expect(Object.keys(EventType)).not.toContain('detach');
|
|
161
|
+
expect(Object.keys(EventType)).not.toContain('detached');
|
|
162
|
+
expect(Object.keys(EventType)).not.toContain('attached');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EventManager, EventSubscriber } from '@mikro-orm/core';
|
|
2
|
+
import type { Detach } from '@rsdk/db/dist/metrics/plugin-kit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Subscriber метрик, которого можно выключить: у EventManager нет способа
|
|
6
|
+
* удалить подписчика. Каждый хук наследника первым делом проверяет `attached`.
|
|
7
|
+
* Выключить можно только через {@link DetachableSubscriber.detach}, включить
|
|
8
|
+
* обратно нельзя.
|
|
9
|
+
*
|
|
10
|
+
* `detach`, `detached` и `attached` не совпадают ни с одним ключом
|
|
11
|
+
* `EventType`, поэтому MikroORM не принимает их за хуки. `EventSubscriber`
|
|
12
|
+
* реализуют наследники: у него все поля необязательные, и класс без хуков
|
|
13
|
+
* TypeScript не примет
|
|
14
|
+
*/
|
|
15
|
+
export abstract class DetachableSubscriber {
|
|
16
|
+
private detached = false;
|
|
17
|
+
|
|
18
|
+
protected get attached(): boolean {
|
|
19
|
+
return !this.detached;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
detach(): void {
|
|
23
|
+
this.detached = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Регистрирует subscriber в EventManager. Detach не удаляет subscriber из
|
|
29
|
+
* EventManager, а выключает его: subscriber остаётся зарегистрированным,
|
|
30
|
+
* но больше ничего не пишет. Повторный вызов ничего не меняет:
|
|
31
|
+
* {@link DetachableSubscriber.detach} только ставит флаг
|
|
32
|
+
*/
|
|
33
|
+
export const registerSubscriber = (
|
|
34
|
+
events: EventManager,
|
|
35
|
+
subscriber: DetachableSubscriber & EventSubscriber,
|
|
36
|
+
): Detach => {
|
|
37
|
+
events.registerSubscriber(subscriber);
|
|
38
|
+
|
|
39
|
+
return () => subscriber.detach();
|
|
40
|
+
};
|
package/src/mikro-orm.plugin.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { LoggerFactory } from '@rsdk/logging';
|
|
|
20
20
|
import _ from 'lodash';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
|
|
23
|
+
import { MikroOrmMetricsModule } from './modules/metrics.module';
|
|
23
24
|
import { MikroOrmPluginConfig } from './providers/mikro-orm.config';
|
|
24
25
|
import { MikroOrmAdapter } from './providers/mikro-orm-logger.adapter';
|
|
25
26
|
import { MikroOrmErrorTransformer } from './mikro-orm.errors-transformer';
|
|
@@ -188,6 +189,7 @@ export class MikroOrmPlugin implements PlatformAppPlugin {
|
|
|
188
189
|
.value();
|
|
189
190
|
},
|
|
190
191
|
}),
|
|
192
|
+
MikroOrmMetricsModule,
|
|
191
193
|
];
|
|
192
194
|
}
|
|
193
195
|
|