adonisjs-pulse 0.0.1
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.md +9 -0
- package/README.md +119 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +47 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +5 -0
- package/build/providers/pulse_provider.d.ts +16 -0
- package/build/providers/pulse_provider.js +64 -0
- package/build/resources/views/components/card-header.edge +30 -0
- package/build/resources/views/components/card.edge +8 -0
- package/build/resources/views/components/no-results.edge +3 -0
- package/build/resources/views/components/pulse.edge +79 -0
- package/build/resources/views/components/scroll.edge +3 -0
- package/build/resources/views/components/table.edge +3 -0
- package/build/resources/views/components/td.edge +4 -0
- package/build/resources/views/components/th.edge +4 -0
- package/build/resources/views/components/thead.edge +3 -0
- package/build/resources/views/components/theme-switcher.edge +48 -0
- package/build/resources/views/dashboard.edge +7 -0
- package/build/resources/views/livewire/cache.edge +111 -0
- package/build/resources/views/livewire/exceptions.edge +48 -0
- package/build/resources/views/livewire/period-selector.edge +11 -0
- package/build/resources/views/livewire/servers.edge +86 -0
- package/build/resources/views/livewire/slow-queries.edge +50 -0
- package/build/resources/views/livewire/slow-requests.edge +62 -0
- package/build/resources/views/livewire/usage.edge +63 -0
- package/build/services/pulse.d.ts +3 -0
- package/build/services/pulse.js +6 -0
- package/build/src/entry.d.ts +24 -0
- package/build/src/entry.js +67 -0
- package/build/src/livewire/cache.d.ts +4 -0
- package/build/src/livewire/cache.js +33 -0
- package/build/src/livewire/card.d.ts +22 -0
- package/build/src/livewire/card.js +96 -0
- package/build/src/livewire/exceptions.d.ts +4 -0
- package/build/src/livewire/exceptions.js +29 -0
- package/build/src/livewire/period_selector.d.ts +5 -0
- package/build/src/livewire/period_selector.js +16 -0
- package/build/src/livewire/servers.d.ts +4 -0
- package/build/src/livewire/servers.js +36 -0
- package/build/src/livewire/slow_queries.d.ts +5 -0
- package/build/src/livewire/slow_queries.js +37 -0
- package/build/src/livewire/slow_requests.d.ts +5 -0
- package/build/src/livewire/slow_requests.js +37 -0
- package/build/src/pulse.d.ts +44 -0
- package/build/src/pulse.js +224 -0
- package/build/src/recorders/cache_interactions.d.ts +34 -0
- package/build/src/recorders/cache_interactions.js +80 -0
- package/build/src/recorders/exceptions.d.ts +8 -0
- package/build/src/recorders/exceptions.js +81 -0
- package/build/src/recorders/index.d.ts +7 -0
- package/build/src/recorders/index.js +6 -0
- package/build/src/recorders/recorder.d.ts +24 -0
- package/build/src/recorders/recorder.js +45 -0
- package/build/src/recorders/servers.d.ts +24 -0
- package/build/src/recorders/servers.js +133 -0
- package/build/src/recorders/slow_queries.d.ts +8 -0
- package/build/src/recorders/slow_queries.js +27 -0
- package/build/src/recorders/slow_requests.d.ts +11 -0
- package/build/src/recorders/slow_requests.js +80 -0
- package/build/src/storage/database_storage.d.ts +20 -0
- package/build/src/storage/database_storage.js +357 -0
- package/build/src/types.d.ts +91 -0
- package/build/src/types.js +3 -0
- package/build/src/value.d.ts +9 -0
- package/build/src/value.js +20 -0
- package/build/stubs/config.stub +58 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/stubs/migration.stub +58 -0
- package/package.json +100 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { lazy, url } from 'adonisjs-livewire';
|
|
8
|
+
import { Card } from './card.js';
|
|
9
|
+
let SlowQueries = class SlowQueries extends Card {
|
|
10
|
+
orderBy = 'slowest';
|
|
11
|
+
async render() {
|
|
12
|
+
const aggregateData = await this.aggregate('slow_query', ['max', 'count'], this.orderBy === 'count' ? 'count' : 'max');
|
|
13
|
+
const slowQueries = aggregateData
|
|
14
|
+
.values()
|
|
15
|
+
.map((row) => {
|
|
16
|
+
const [sql] = JSON.parse(row.key);
|
|
17
|
+
return {
|
|
18
|
+
sql,
|
|
19
|
+
slowest: row.max,
|
|
20
|
+
count: row.count,
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
.toArray();
|
|
24
|
+
return await this.view.render('pulse::livewire/slow-queries', {
|
|
25
|
+
period: this.periodForHumans(),
|
|
26
|
+
config: { threshold: 1_000 },
|
|
27
|
+
queries: slowQueries,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
__decorate([
|
|
32
|
+
url('slow-queries')
|
|
33
|
+
], SlowQueries.prototype, "orderBy", void 0);
|
|
34
|
+
SlowQueries = __decorate([
|
|
35
|
+
lazy()
|
|
36
|
+
], SlowQueries);
|
|
37
|
+
export { SlowQueries };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { lazy, url } from 'adonisjs-livewire';
|
|
8
|
+
import { Card } from './card.js';
|
|
9
|
+
let SlowRequests = class SlowRequests extends Card {
|
|
10
|
+
orderBy = 'slowest';
|
|
11
|
+
async render() {
|
|
12
|
+
const aggregateData = await this.aggregate('slow_request', ['max', 'count'], this.orderBy === 'count' ? 'count' : 'max');
|
|
13
|
+
const slowRequests = Array.from(aggregateData.entries()).map(([key, row]) => {
|
|
14
|
+
const [method, uri, action] = JSON.parse(key);
|
|
15
|
+
return {
|
|
16
|
+
method,
|
|
17
|
+
uri,
|
|
18
|
+
action,
|
|
19
|
+
slowest: row.max ?? 0,
|
|
20
|
+
count: row.count ?? 0,
|
|
21
|
+
threshold: 1_000,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return await this.view.render('pulse::livewire/slow-requests', {
|
|
25
|
+
period: this.periodForHumans(),
|
|
26
|
+
config: { threshold: 1_000 },
|
|
27
|
+
requests: slowRequests,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
__decorate([
|
|
32
|
+
url('slow-requests')
|
|
33
|
+
], SlowRequests.prototype, "orderBy", void 0);
|
|
34
|
+
SlowRequests = __decorate([
|
|
35
|
+
lazy()
|
|
36
|
+
], SlowRequests);
|
|
37
|
+
export { SlowRequests };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Database } from '@adonisjs/lucid/database';
|
|
2
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
3
|
+
import { Entry } from './entry.js';
|
|
4
|
+
import { Value } from './value.js';
|
|
5
|
+
import type { AggregationType, PulseConfig, PulseStorage, RecordCallback, AggregateResult, AggregateTypesResult, StoredValue, ResolvedPulseConfig, RecorderEntry } from './types.js';
|
|
6
|
+
import type { Recorder } from './recorders/recorder.js';
|
|
7
|
+
export declare class Pulse {
|
|
8
|
+
protected db: Database;
|
|
9
|
+
protected entries: Entry[];
|
|
10
|
+
protected values: Value[];
|
|
11
|
+
protected lazy: RecordCallback[];
|
|
12
|
+
protected storage: PulseStorage | null;
|
|
13
|
+
protected config: ResolvedPulseConfig;
|
|
14
|
+
protected ingesting: boolean;
|
|
15
|
+
protected ingestTimer: ReturnType<typeof setInterval> | null;
|
|
16
|
+
protected recorders: Recorder[];
|
|
17
|
+
constructor(db: Database, config?: Partial<PulseConfig>);
|
|
18
|
+
get enabled(): boolean;
|
|
19
|
+
enable(): this;
|
|
20
|
+
disable(): this;
|
|
21
|
+
registerRecorders(app: ApplicationService): Promise<void>;
|
|
22
|
+
protected registerRecorder(app: ApplicationService, entry: RecorderEntry): Promise<void>;
|
|
23
|
+
record(entry: Entry): this;
|
|
24
|
+
set(value: Value): this;
|
|
25
|
+
setLazy(type: string, key: string, callback: () => string | Promise<string>): this;
|
|
26
|
+
recordLazy(callback: RecordCallback): this;
|
|
27
|
+
ingest(): Promise<void>;
|
|
28
|
+
protected shouldTrim(): boolean;
|
|
29
|
+
trim(): Promise<void>;
|
|
30
|
+
purge(types?: string[]): Promise<void>;
|
|
31
|
+
getValues(type: string, keys?: string[]): Promise<StoredValue[]>;
|
|
32
|
+
aggregate(type: string, aggregates: AggregationType[], interval: number, orderBy?: AggregationType, direction?: 'asc' | 'desc', limit?: number): Promise<Map<string, AggregateResult>>;
|
|
33
|
+
graph(types: string[], aggregate: AggregationType, interval: number): Promise<Map<number, Map<string, number>>>;
|
|
34
|
+
aggregateTypes(types: string[], aggregate: AggregationType, interval: number, orderBy?: string, direction?: 'asc' | 'desc', limit?: number): Promise<AggregateTypesResult[]>;
|
|
35
|
+
aggregateTotal(types: string[], aggregate: AggregationType, interval: number): Promise<Record<string, number>>;
|
|
36
|
+
startIngesting(): this;
|
|
37
|
+
stopIngesting(): this;
|
|
38
|
+
flush(): Promise<void>;
|
|
39
|
+
getConfig(): ResolvedPulseConfig;
|
|
40
|
+
getRecorders(): Recorder[];
|
|
41
|
+
pendingEntries(): number;
|
|
42
|
+
pendingValues(): number;
|
|
43
|
+
pendingLazy(): number;
|
|
44
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Entry } from './entry.js';
|
|
2
|
+
import { Value } from './value.js';
|
|
3
|
+
import { DatabaseStorage } from './storage/database_storage.js';
|
|
4
|
+
function resolveConfig(config) {
|
|
5
|
+
return {
|
|
6
|
+
enabled: config.enabled ?? true,
|
|
7
|
+
ingestInterval: config.ingest?.interval ?? 5000,
|
|
8
|
+
trimLotteryChance: [config.ingest?.lottery?.chance ?? 1, config.ingest?.lottery?.outOf ?? 1000],
|
|
9
|
+
connection: config.storage?.connection,
|
|
10
|
+
retain: {
|
|
11
|
+
entries: 604800,
|
|
12
|
+
values: 604800,
|
|
13
|
+
aggregates: 7776000,
|
|
14
|
+
},
|
|
15
|
+
recorders: config.recorders ?? [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export class Pulse {
|
|
19
|
+
db;
|
|
20
|
+
entries = [];
|
|
21
|
+
values = [];
|
|
22
|
+
lazy = [];
|
|
23
|
+
storage = null;
|
|
24
|
+
config;
|
|
25
|
+
ingesting = false;
|
|
26
|
+
ingestTimer = null;
|
|
27
|
+
recorders = [];
|
|
28
|
+
constructor(db, config = {}) {
|
|
29
|
+
this.db = db;
|
|
30
|
+
this.config = resolveConfig(config);
|
|
31
|
+
this.storage = new DatabaseStorage(db, this.config);
|
|
32
|
+
}
|
|
33
|
+
get enabled() {
|
|
34
|
+
return this.config.enabled;
|
|
35
|
+
}
|
|
36
|
+
enable() {
|
|
37
|
+
this.config.enabled = true;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
disable() {
|
|
41
|
+
this.config.enabled = false;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
async registerRecorders(app) {
|
|
45
|
+
for (const entry of this.config.recorders) {
|
|
46
|
+
await this.registerRecorder(app, entry);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async registerRecorder(app, entry) {
|
|
50
|
+
try {
|
|
51
|
+
const { recorder: lazyRecorder, ...options } = entry;
|
|
52
|
+
const module = await lazyRecorder();
|
|
53
|
+
const RecorderClass = module.default;
|
|
54
|
+
const recorder = new RecorderClass(options);
|
|
55
|
+
recorder.setApp(app).setPulse(this);
|
|
56
|
+
await recorder.register();
|
|
57
|
+
this.recorders.push(recorder);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error('Failed to register recorder:', error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
record(entry) {
|
|
64
|
+
if (!this.config.enabled) {
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
this.entries.push(entry);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
set(value) {
|
|
71
|
+
if (!this.config.enabled) {
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
this.values.push(value);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
setLazy(type, key, callback) {
|
|
78
|
+
if (!this.config.enabled) {
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
this.lazy.push(async () => {
|
|
82
|
+
const value = await callback();
|
|
83
|
+
return new Value(Date.now(), type, key, value);
|
|
84
|
+
});
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
recordLazy(callback) {
|
|
88
|
+
if (!this.config.enabled) {
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
this.lazy.push(callback);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
async ingest() {
|
|
95
|
+
if (!this.config.enabled || !this.storage || this.ingesting) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.ingesting = true;
|
|
99
|
+
try {
|
|
100
|
+
const entries = [...this.entries];
|
|
101
|
+
const values = [...this.values];
|
|
102
|
+
const lazyCallbacks = [...this.lazy];
|
|
103
|
+
this.entries = [];
|
|
104
|
+
this.values = [];
|
|
105
|
+
this.lazy = [];
|
|
106
|
+
for (const callback of lazyCallbacks) {
|
|
107
|
+
try {
|
|
108
|
+
const result = await callback();
|
|
109
|
+
if (result === null) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(result)) {
|
|
113
|
+
for (const item of result) {
|
|
114
|
+
if (item instanceof Entry) {
|
|
115
|
+
entries.push(item);
|
|
116
|
+
}
|
|
117
|
+
else if (item instanceof Value) {
|
|
118
|
+
values.push(item);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (result instanceof Entry) {
|
|
123
|
+
entries.push(result);
|
|
124
|
+
}
|
|
125
|
+
else if (result instanceof Value) {
|
|
126
|
+
values.push(result);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore errors in lazy callbacks
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (entries.length > 0 || values.length > 0) {
|
|
134
|
+
await this.storage.store(entries, values);
|
|
135
|
+
}
|
|
136
|
+
if (this.shouldTrim()) {
|
|
137
|
+
await this.trim();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
this.ingesting = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
shouldTrim() {
|
|
145
|
+
const [chance, outOf] = this.config.trimLotteryChance;
|
|
146
|
+
return Math.random() * outOf < chance;
|
|
147
|
+
}
|
|
148
|
+
async trim() {
|
|
149
|
+
if (!this.storage) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
await this.storage.trim();
|
|
153
|
+
}
|
|
154
|
+
async purge(types) {
|
|
155
|
+
if (!this.storage) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await this.storage.purge(types);
|
|
159
|
+
}
|
|
160
|
+
async getValues(type, keys) {
|
|
161
|
+
if (!this.storage) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
return this.storage.values(type, keys);
|
|
165
|
+
}
|
|
166
|
+
async aggregate(type, aggregates, interval, orderBy, direction = 'desc', limit) {
|
|
167
|
+
if (!this.storage) {
|
|
168
|
+
return new Map();
|
|
169
|
+
}
|
|
170
|
+
return this.storage.aggregate(type, aggregates, interval, orderBy, direction, limit);
|
|
171
|
+
}
|
|
172
|
+
async graph(types, aggregate, interval) {
|
|
173
|
+
if (!this.storage) {
|
|
174
|
+
return new Map();
|
|
175
|
+
}
|
|
176
|
+
return this.storage.graph(types, aggregate, interval);
|
|
177
|
+
}
|
|
178
|
+
async aggregateTypes(types, aggregate, interval, orderBy, direction = 'desc', limit) {
|
|
179
|
+
if (!this.storage) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return this.storage.aggregateTypes(types, aggregate, interval, orderBy, direction, limit);
|
|
183
|
+
}
|
|
184
|
+
async aggregateTotal(types, aggregate, interval) {
|
|
185
|
+
if (!this.storage) {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
return this.storage.aggregateTotal(types, aggregate, interval);
|
|
189
|
+
}
|
|
190
|
+
startIngesting() {
|
|
191
|
+
if (this.ingestTimer) {
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
this.ingestTimer = setInterval(() => {
|
|
195
|
+
this.ingest().catch(() => { });
|
|
196
|
+
}, this.config.ingestInterval);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
stopIngesting() {
|
|
200
|
+
if (this.ingestTimer) {
|
|
201
|
+
clearInterval(this.ingestTimer);
|
|
202
|
+
this.ingestTimer = null;
|
|
203
|
+
}
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
async flush() {
|
|
207
|
+
await this.ingest();
|
|
208
|
+
}
|
|
209
|
+
getConfig() {
|
|
210
|
+
return this.config;
|
|
211
|
+
}
|
|
212
|
+
getRecorders() {
|
|
213
|
+
return this.recorders;
|
|
214
|
+
}
|
|
215
|
+
pendingEntries() {
|
|
216
|
+
return this.entries.length;
|
|
217
|
+
}
|
|
218
|
+
pendingValues() {
|
|
219
|
+
return this.values.length;
|
|
220
|
+
}
|
|
221
|
+
pendingLazy() {
|
|
222
|
+
return this.lazy.length;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Recorder } from './recorder.js';
|
|
2
|
+
interface CacheHitEvent {
|
|
3
|
+
key: string;
|
|
4
|
+
value: any;
|
|
5
|
+
store: string;
|
|
6
|
+
layer: 'l1' | 'l2';
|
|
7
|
+
graced: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface CacheMissEvent {
|
|
10
|
+
key: string;
|
|
11
|
+
store: string;
|
|
12
|
+
}
|
|
13
|
+
interface CacheWrittenEvent {
|
|
14
|
+
key: string;
|
|
15
|
+
value: any;
|
|
16
|
+
store: string;
|
|
17
|
+
}
|
|
18
|
+
interface CacheDeletedEvent {
|
|
19
|
+
key: string;
|
|
20
|
+
store: string;
|
|
21
|
+
}
|
|
22
|
+
interface CacheClearedEvent {
|
|
23
|
+
store: string;
|
|
24
|
+
}
|
|
25
|
+
export default class CacheInteractions extends Recorder {
|
|
26
|
+
register(): Promise<void>;
|
|
27
|
+
protected recordHit(payload: CacheHitEvent): void;
|
|
28
|
+
protected recordMiss(payload: CacheMissEvent): void;
|
|
29
|
+
protected recordWritten(payload: CacheWrittenEvent): void;
|
|
30
|
+
protected recordDeleted(payload: CacheDeletedEvent): void;
|
|
31
|
+
protected recordCleared(payload: CacheClearedEvent): void;
|
|
32
|
+
protected group(key: string): string;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Recorder } from './recorder.js';
|
|
2
|
+
import { Entry } from '../entry.js';
|
|
3
|
+
export default class CacheInteractions extends Recorder {
|
|
4
|
+
async register() {
|
|
5
|
+
if (!this.enabled) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (!this.app.container.hasBinding('cache.manager')) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const emitter = await this.app.container.make('emitter');
|
|
12
|
+
emitter.on('cache:hit', (payload) => {
|
|
13
|
+
this.recordHit(payload);
|
|
14
|
+
});
|
|
15
|
+
emitter.on('cache:miss', (payload) => {
|
|
16
|
+
this.recordMiss(payload);
|
|
17
|
+
});
|
|
18
|
+
emitter.on('cache:written', (payload) => {
|
|
19
|
+
this.recordWritten(payload);
|
|
20
|
+
});
|
|
21
|
+
emitter.on('cache:deleted', (payload) => {
|
|
22
|
+
this.recordDeleted(payload);
|
|
23
|
+
});
|
|
24
|
+
emitter.on('cache:cleared', (payload) => {
|
|
25
|
+
this.recordCleared(payload);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
recordHit(payload) {
|
|
29
|
+
if (!this.shouldSample()) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (this.shouldIgnore(payload.key)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.pulse.record(new Entry(Date.now(), 'cache_hit', this.group(payload.key)).count());
|
|
36
|
+
}
|
|
37
|
+
recordMiss(payload) {
|
|
38
|
+
if (!this.shouldSample()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (this.shouldIgnore(payload.key)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.pulse.record(new Entry(Date.now(), 'cache_miss', this.group(payload.key)).count());
|
|
45
|
+
}
|
|
46
|
+
recordWritten(payload) {
|
|
47
|
+
if (!this.shouldSample()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (this.shouldIgnore(payload.key)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.pulse.record(new Entry(Date.now(), 'cache_write', this.group(payload.key)).count());
|
|
54
|
+
}
|
|
55
|
+
recordDeleted(payload) {
|
|
56
|
+
if (!this.shouldSample()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (this.shouldIgnore(payload.key)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.pulse.record(new Entry(Date.now(), 'cache_delete', this.group(payload.key)).count());
|
|
63
|
+
}
|
|
64
|
+
recordCleared(payload) {
|
|
65
|
+
if (!this.shouldSample()) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.pulse.record(new Entry(Date.now(), 'cache_clear', payload.store).count());
|
|
69
|
+
}
|
|
70
|
+
group(key) {
|
|
71
|
+
const groups = this.options.groups ?? {};
|
|
72
|
+
for (const [pattern, replacement] of Object.entries(groups)) {
|
|
73
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
74
|
+
if (regex.test(key)) {
|
|
75
|
+
return key.replace(regex, replacement);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import { Recorder } from './recorder.js';
|
|
3
|
+
export default class Exceptions extends Recorder {
|
|
4
|
+
register(): Promise<void>;
|
|
5
|
+
protected record(error: any, ctx: HttpContext): void;
|
|
6
|
+
protected getLocation(error: Error): string;
|
|
7
|
+
recordException(error: Error, ctx?: HttpContext): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ExceptionHandler } from '@adonisjs/core/http';
|
|
2
|
+
import { Recorder } from './recorder.js';
|
|
3
|
+
import { Entry } from '../entry.js';
|
|
4
|
+
import { ReflectionClass } from 'unreflect';
|
|
5
|
+
export default class Exceptions extends Recorder {
|
|
6
|
+
async register() {
|
|
7
|
+
if (!this.enabled) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const server = await this.app.container.make('server');
|
|
11
|
+
const record = (error, ctx) => {
|
|
12
|
+
this.record(error, ctx);
|
|
13
|
+
};
|
|
14
|
+
const reflected = new ReflectionClass(server);
|
|
15
|
+
const resolvedErrorHandler = await reflected.getPropertyValue('#resolvedErrorHandler');
|
|
16
|
+
reflected.setProperty('#resolvedErrorHandler', await this.app.container.make(class extends ExceptionHandler {
|
|
17
|
+
async handle(error, ctx) {
|
|
18
|
+
record(error, ctx);
|
|
19
|
+
return resolvedErrorHandler.handle(error, ctx);
|
|
20
|
+
}
|
|
21
|
+
async report(error, ctx) {
|
|
22
|
+
return resolvedErrorHandler.report(error, ctx);
|
|
23
|
+
}
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
record(error, ctx) {
|
|
27
|
+
if (!this.shouldSample()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const name = error.name || error.constructor?.name || 'Error';
|
|
31
|
+
const message = error.message || '';
|
|
32
|
+
const className = `${name}: ${message}`;
|
|
33
|
+
if (this.shouldIgnore(className)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const location = this.getLocation(error);
|
|
37
|
+
this.pulse.record(new Entry(Date.now(), 'exception', JSON.stringify([className, location]), 1).count());
|
|
38
|
+
const auth = ctx.auth;
|
|
39
|
+
if (auth?.user) {
|
|
40
|
+
this.pulse.record(new Entry(Date.now(), 'user_exception', String(auth.user.id), 1).count());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getLocation(error) {
|
|
44
|
+
if (!error.stack) {
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
const lines = error.stack.split('\n');
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const match = line.match(/at\s+(?:.*?\s+)?\(?(.+?):(\d+):(\d+)\)?/);
|
|
50
|
+
if (match) {
|
|
51
|
+
const [, file, lineNum, column] = match;
|
|
52
|
+
if (!file.includes('node_modules')) {
|
|
53
|
+
return `${file}:${lineNum}:${column}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
recordException(error, ctx) {
|
|
60
|
+
if (!this.enabled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!this.shouldSample()) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const name = error.name || error.constructor?.name || 'Error';
|
|
67
|
+
const message = error.message || '';
|
|
68
|
+
const className = `${name}: ${message}`;
|
|
69
|
+
if (this.shouldIgnore(className)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const location = this.getLocation(error);
|
|
73
|
+
this.pulse.record(new Entry(Date.now(), 'exception', JSON.stringify([className, location]), 1).count());
|
|
74
|
+
if (ctx) {
|
|
75
|
+
const auth = ctx.auth;
|
|
76
|
+
if (auth?.user) {
|
|
77
|
+
this.pulse.record(new Entry(Date.now(), 'user_exception', String(auth.user.id), 1).count());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Recorder } from './recorder.js';
|
|
2
|
+
export type { RecorderOptions } from './recorder.js';
|
|
3
|
+
export { default as SlowQueries } from './slow_queries.js';
|
|
4
|
+
export { default as SlowRequests } from './slow_requests.js';
|
|
5
|
+
export { default as Exceptions } from './exceptions.js';
|
|
6
|
+
export { default as Servers } from './servers.js';
|
|
7
|
+
export { default as CacheInteractions } from './cache_interactions.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Recorder } from './recorder.js';
|
|
2
|
+
export { default as SlowQueries } from './slow_queries.js';
|
|
3
|
+
export { default as SlowRequests } from './slow_requests.js';
|
|
4
|
+
export { default as Exceptions } from './exceptions.js';
|
|
5
|
+
export { default as Servers } from './servers.js';
|
|
6
|
+
export { default as CacheInteractions } from './cache_interactions.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
import type { Pulse } from '../pulse.js';
|
|
3
|
+
export interface RecorderOptions {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
sampleRate?: number;
|
|
6
|
+
threshold?: number;
|
|
7
|
+
ignore?: string[];
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
export declare abstract class Recorder {
|
|
11
|
+
protected options: RecorderOptions;
|
|
12
|
+
protected pulse: Pulse;
|
|
13
|
+
protected app: ApplicationService;
|
|
14
|
+
constructor(options?: RecorderOptions);
|
|
15
|
+
get enabled(): boolean;
|
|
16
|
+
get sampleRate(): number;
|
|
17
|
+
get threshold(): number;
|
|
18
|
+
get ignore(): string[];
|
|
19
|
+
setApp(app: ApplicationService): this;
|
|
20
|
+
setPulse(pulse: Pulse): this;
|
|
21
|
+
abstract register(): void | Promise<void>;
|
|
22
|
+
protected shouldSample(): boolean;
|
|
23
|
+
protected shouldIgnore(value: string): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class Recorder {
|
|
2
|
+
options;
|
|
3
|
+
pulse;
|
|
4
|
+
app;
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
}
|
|
8
|
+
get enabled() {
|
|
9
|
+
return this.options.enabled ?? true;
|
|
10
|
+
}
|
|
11
|
+
get sampleRate() {
|
|
12
|
+
return this.options.sampleRate ?? 1;
|
|
13
|
+
}
|
|
14
|
+
get threshold() {
|
|
15
|
+
return this.options.threshold ?? 0;
|
|
16
|
+
}
|
|
17
|
+
get ignore() {
|
|
18
|
+
return this.options.ignore ?? [];
|
|
19
|
+
}
|
|
20
|
+
setApp(app) {
|
|
21
|
+
this.app = app;
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
setPulse(pulse) {
|
|
25
|
+
this.pulse = pulse;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
shouldSample() {
|
|
29
|
+
return Math.random() <= this.sampleRate;
|
|
30
|
+
}
|
|
31
|
+
shouldIgnore(value) {
|
|
32
|
+
for (const pattern of this.ignore) {
|
|
33
|
+
if (pattern.startsWith('/') && pattern.endsWith('/')) {
|
|
34
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
35
|
+
if (regex.test(value)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (value.includes(pattern)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Recorder, RecorderOptions } from './recorder.js';
|
|
2
|
+
export interface ServersOptions extends RecorderOptions {
|
|
3
|
+
serverName?: string;
|
|
4
|
+
directories?: string[];
|
|
5
|
+
}
|
|
6
|
+
export default class Servers extends Recorder {
|
|
7
|
+
protected serverName: string;
|
|
8
|
+
protected directories: string[];
|
|
9
|
+
protected interval: ReturnType<typeof setInterval> | null;
|
|
10
|
+
constructor(options?: ServersOptions);
|
|
11
|
+
register(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
record(): void;
|
|
14
|
+
protected getCpuUsage(): number;
|
|
15
|
+
protected getMemoryUsage(): {
|
|
16
|
+
used: number;
|
|
17
|
+
total: number;
|
|
18
|
+
};
|
|
19
|
+
protected getStorageUsage(): {
|
|
20
|
+
directory: string;
|
|
21
|
+
used: number;
|
|
22
|
+
total: number;
|
|
23
|
+
}[];
|
|
24
|
+
}
|