s3db.js 6.2.0 → 7.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/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +12105 -19396
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +12090 -19393
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +12103 -19398
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -38
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +159 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import Plugin from "./plugin.class.js";
|
|
2
|
+
import tryFn from "../concerns/try-fn.js";
|
|
3
|
+
|
|
4
|
+
export class MetricsPlugin extends Plugin {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.config = {
|
|
8
|
+
collectPerformance: options.collectPerformance !== false,
|
|
9
|
+
collectErrors: options.collectErrors !== false,
|
|
10
|
+
collectUsage: options.collectUsage !== false,
|
|
11
|
+
retentionDays: options.retentionDays || 30,
|
|
12
|
+
flushInterval: options.flushInterval || 60000, // 1 minute
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.metrics = {
|
|
17
|
+
operations: {
|
|
18
|
+
insert: { count: 0, totalTime: 0, errors: 0 },
|
|
19
|
+
update: { count: 0, totalTime: 0, errors: 0 },
|
|
20
|
+
delete: { count: 0, totalTime: 0, errors: 0 },
|
|
21
|
+
get: { count: 0, totalTime: 0, errors: 0 },
|
|
22
|
+
list: { count: 0, totalTime: 0, errors: 0 },
|
|
23
|
+
count: { count: 0, totalTime: 0, errors: 0 }
|
|
24
|
+
},
|
|
25
|
+
resources: {},
|
|
26
|
+
errors: [],
|
|
27
|
+
performance: [],
|
|
28
|
+
startTime: new Date().toISOString()
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.flushTimer = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async setup(database) {
|
|
35
|
+
this.database = database;
|
|
36
|
+
if (process.env.NODE_ENV === 'test') return;
|
|
37
|
+
|
|
38
|
+
const [ok, err] = await tryFn(async () => {
|
|
39
|
+
const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
|
|
40
|
+
name: 'metrics',
|
|
41
|
+
attributes: {
|
|
42
|
+
id: 'string|required',
|
|
43
|
+
type: 'string|required', // 'operation', 'error', 'performance'
|
|
44
|
+
resourceName: 'string',
|
|
45
|
+
operation: 'string',
|
|
46
|
+
count: 'number|required',
|
|
47
|
+
totalTime: 'number|required',
|
|
48
|
+
errors: 'number|required',
|
|
49
|
+
avgTime: 'number|required',
|
|
50
|
+
timestamp: 'string|required',
|
|
51
|
+
metadata: 'json'
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
this.metricsResource = ok1 ? metricsResource : database.resources.metrics;
|
|
55
|
+
|
|
56
|
+
const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
|
|
57
|
+
name: 'error_logs',
|
|
58
|
+
attributes: {
|
|
59
|
+
id: 'string|required',
|
|
60
|
+
resourceName: 'string|required',
|
|
61
|
+
operation: 'string|required',
|
|
62
|
+
error: 'string|required',
|
|
63
|
+
timestamp: 'string|required',
|
|
64
|
+
metadata: 'json'
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
this.errorsResource = ok2 ? errorsResource : database.resources.error_logs;
|
|
68
|
+
|
|
69
|
+
const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
|
|
70
|
+
name: 'performance_logs',
|
|
71
|
+
attributes: {
|
|
72
|
+
id: 'string|required',
|
|
73
|
+
resourceName: 'string|required',
|
|
74
|
+
operation: 'string|required',
|
|
75
|
+
duration: 'number|required',
|
|
76
|
+
timestamp: 'string|required',
|
|
77
|
+
metadata: 'json'
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
this.performanceResource = ok3 ? performanceResource : database.resources.performance_logs;
|
|
81
|
+
});
|
|
82
|
+
if (!ok) {
|
|
83
|
+
// Resources might already exist
|
|
84
|
+
this.metricsResource = database.resources.metrics;
|
|
85
|
+
this.errorsResource = database.resources.error_logs;
|
|
86
|
+
this.performanceResource = database.resources.performance_logs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Install hooks for all resources except metrics resources
|
|
90
|
+
this.installMetricsHooks();
|
|
91
|
+
|
|
92
|
+
// Disable flush timer during tests to avoid side effects
|
|
93
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
94
|
+
this.startFlushTimer();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async start() {
|
|
99
|
+
// Plugin is ready
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async stop() {
|
|
103
|
+
// Stop flush timer and flush remaining metrics
|
|
104
|
+
if (this.flushTimer) {
|
|
105
|
+
clearInterval(this.flushTimer);
|
|
106
|
+
this.flushTimer = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Don't flush metrics during tests
|
|
110
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
111
|
+
await this.flushMetrics();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
installMetricsHooks() {
|
|
116
|
+
// Only hook into non-metrics resources
|
|
117
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
118
|
+
if (['metrics', 'error_logs', 'performance_logs'].includes(resource.name)) {
|
|
119
|
+
continue; // Skip metrics resources to avoid recursion
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.installResourceHooks(resource);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Hook into database proxy for new resources
|
|
126
|
+
this.database._createResource = this.database.createResource;
|
|
127
|
+
this.database.createResource = async function (...args) {
|
|
128
|
+
const resource = await this._createResource(...args);
|
|
129
|
+
if (this.plugins?.metrics && !['metrics', 'error_logs', 'performance_logs'].includes(resource.name)) {
|
|
130
|
+
this.plugins.metrics.installResourceHooks(resource);
|
|
131
|
+
}
|
|
132
|
+
return resource;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
installResourceHooks(resource) {
|
|
137
|
+
// Store original methods
|
|
138
|
+
resource._insert = resource.insert;
|
|
139
|
+
resource._update = resource.update;
|
|
140
|
+
resource._delete = resource.delete;
|
|
141
|
+
resource._deleteMany = resource.deleteMany;
|
|
142
|
+
resource._get = resource.get;
|
|
143
|
+
resource._getMany = resource.getMany;
|
|
144
|
+
resource._getAll = resource.getAll;
|
|
145
|
+
resource._list = resource.list;
|
|
146
|
+
resource._listIds = resource.listIds;
|
|
147
|
+
resource._count = resource.count;
|
|
148
|
+
resource._page = resource.page;
|
|
149
|
+
|
|
150
|
+
// Hook insert operations
|
|
151
|
+
resource.insert = async function (...args) {
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
const [ok, err, result] = await tryFn(() => resource._insert(...args));
|
|
154
|
+
this.recordOperation(resource.name, 'insert', Date.now() - startTime, !ok);
|
|
155
|
+
if (!ok) this.recordError(resource.name, 'insert', err);
|
|
156
|
+
if (!ok) throw err;
|
|
157
|
+
return result;
|
|
158
|
+
}.bind(this);
|
|
159
|
+
|
|
160
|
+
// Hook update operations
|
|
161
|
+
resource.update = async function (...args) {
|
|
162
|
+
const startTime = Date.now();
|
|
163
|
+
const [ok, err, result] = await tryFn(() => resource._update(...args));
|
|
164
|
+
this.recordOperation(resource.name, 'update', Date.now() - startTime, !ok);
|
|
165
|
+
if (!ok) this.recordError(resource.name, 'update', err);
|
|
166
|
+
if (!ok) throw err;
|
|
167
|
+
return result;
|
|
168
|
+
}.bind(this);
|
|
169
|
+
|
|
170
|
+
// Hook delete operations
|
|
171
|
+
resource.delete = async function (...args) {
|
|
172
|
+
const startTime = Date.now();
|
|
173
|
+
const [ok, err, result] = await tryFn(() => resource._delete(...args));
|
|
174
|
+
this.recordOperation(resource.name, 'delete', Date.now() - startTime, !ok);
|
|
175
|
+
if (!ok) this.recordError(resource.name, 'delete', err);
|
|
176
|
+
if (!ok) throw err;
|
|
177
|
+
return result;
|
|
178
|
+
}.bind(this);
|
|
179
|
+
|
|
180
|
+
// Hook deleteMany operations
|
|
181
|
+
resource.deleteMany = async function (...args) {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
const [ok, err, result] = await tryFn(() => resource._deleteMany(...args));
|
|
184
|
+
this.recordOperation(resource.name, 'delete', Date.now() - startTime, !ok);
|
|
185
|
+
if (!ok) this.recordError(resource.name, 'delete', err);
|
|
186
|
+
if (!ok) throw err;
|
|
187
|
+
return result;
|
|
188
|
+
}.bind(this);
|
|
189
|
+
|
|
190
|
+
// Hook get operations
|
|
191
|
+
resource.get = async function (...args) {
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
const [ok, err, result] = await tryFn(() => resource._get(...args));
|
|
194
|
+
this.recordOperation(resource.name, 'get', Date.now() - startTime, !ok);
|
|
195
|
+
if (!ok) this.recordError(resource.name, 'get', err);
|
|
196
|
+
if (!ok) throw err;
|
|
197
|
+
return result;
|
|
198
|
+
}.bind(this);
|
|
199
|
+
|
|
200
|
+
// Hook getMany operations
|
|
201
|
+
resource.getMany = async function (...args) {
|
|
202
|
+
const startTime = Date.now();
|
|
203
|
+
const [ok, err, result] = await tryFn(() => resource._getMany(...args));
|
|
204
|
+
this.recordOperation(resource.name, 'get', Date.now() - startTime, !ok);
|
|
205
|
+
if (!ok) this.recordError(resource.name, 'get', err);
|
|
206
|
+
if (!ok) throw err;
|
|
207
|
+
return result;
|
|
208
|
+
}.bind(this);
|
|
209
|
+
|
|
210
|
+
// Hook getAll operations
|
|
211
|
+
resource.getAll = async function (...args) {
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
const [ok, err, result] = await tryFn(() => resource._getAll(...args));
|
|
214
|
+
this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
|
|
215
|
+
if (!ok) this.recordError(resource.name, 'list', err);
|
|
216
|
+
if (!ok) throw err;
|
|
217
|
+
return result;
|
|
218
|
+
}.bind(this);
|
|
219
|
+
|
|
220
|
+
// Hook list operations
|
|
221
|
+
resource.list = async function (...args) {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
const [ok, err, result] = await tryFn(() => resource._list(...args));
|
|
224
|
+
this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
|
|
225
|
+
if (!ok) this.recordError(resource.name, 'list', err);
|
|
226
|
+
if (!ok) throw err;
|
|
227
|
+
return result;
|
|
228
|
+
}.bind(this);
|
|
229
|
+
|
|
230
|
+
// Hook listIds operations
|
|
231
|
+
resource.listIds = async function (...args) {
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
const [ok, err, result] = await tryFn(() => resource._listIds(...args));
|
|
234
|
+
this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
|
|
235
|
+
if (!ok) this.recordError(resource.name, 'list', err);
|
|
236
|
+
if (!ok) throw err;
|
|
237
|
+
return result;
|
|
238
|
+
}.bind(this);
|
|
239
|
+
|
|
240
|
+
// Hook count operations
|
|
241
|
+
resource.count = async function (...args) {
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const [ok, err, result] = await tryFn(() => resource._count(...args));
|
|
244
|
+
this.recordOperation(resource.name, 'count', Date.now() - startTime, !ok);
|
|
245
|
+
if (!ok) this.recordError(resource.name, 'count', err);
|
|
246
|
+
if (!ok) throw err;
|
|
247
|
+
return result;
|
|
248
|
+
}.bind(this);
|
|
249
|
+
|
|
250
|
+
// Hook page operations
|
|
251
|
+
resource.page = async function (...args) {
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
const [ok, err, result] = await tryFn(() => resource._page(...args));
|
|
254
|
+
this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
|
|
255
|
+
if (!ok) this.recordError(resource.name, 'list', err);
|
|
256
|
+
if (!ok) throw err;
|
|
257
|
+
return result;
|
|
258
|
+
}.bind(this);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
recordOperation(resourceName, operation, duration, isError) {
|
|
262
|
+
// Update global metrics
|
|
263
|
+
if (this.metrics.operations[operation]) {
|
|
264
|
+
this.metrics.operations[operation].count++;
|
|
265
|
+
this.metrics.operations[operation].totalTime += duration;
|
|
266
|
+
if (isError) {
|
|
267
|
+
this.metrics.operations[operation].errors++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update resource-specific metrics
|
|
272
|
+
if (!this.metrics.resources[resourceName]) {
|
|
273
|
+
this.metrics.resources[resourceName] = {
|
|
274
|
+
insert: { count: 0, totalTime: 0, errors: 0 },
|
|
275
|
+
update: { count: 0, totalTime: 0, errors: 0 },
|
|
276
|
+
delete: { count: 0, totalTime: 0, errors: 0 },
|
|
277
|
+
get: { count: 0, totalTime: 0, errors: 0 },
|
|
278
|
+
list: { count: 0, totalTime: 0, errors: 0 },
|
|
279
|
+
count: { count: 0, totalTime: 0, errors: 0 }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this.metrics.resources[resourceName][operation]) {
|
|
284
|
+
this.metrics.resources[resourceName][operation].count++;
|
|
285
|
+
this.metrics.resources[resourceName][operation].totalTime += duration;
|
|
286
|
+
if (isError) {
|
|
287
|
+
this.metrics.resources[resourceName][operation].errors++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Record performance data if enabled
|
|
292
|
+
if (this.config.collectPerformance) {
|
|
293
|
+
this.metrics.performance.push({
|
|
294
|
+
resourceName,
|
|
295
|
+
operation,
|
|
296
|
+
duration,
|
|
297
|
+
timestamp: new Date().toISOString()
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
recordError(resourceName, operation, error) {
|
|
303
|
+
if (!this.config.collectErrors) return;
|
|
304
|
+
|
|
305
|
+
this.metrics.errors.push({
|
|
306
|
+
resourceName,
|
|
307
|
+
operation,
|
|
308
|
+
error: error.message,
|
|
309
|
+
stack: error.stack,
|
|
310
|
+
timestamp: new Date().toISOString()
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
startFlushTimer() {
|
|
315
|
+
if (this.flushTimer) {
|
|
316
|
+
clearInterval(this.flushTimer);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Only start timer if flushInterval is greater than 0
|
|
320
|
+
if (this.config.flushInterval > 0) {
|
|
321
|
+
this.flushTimer = setInterval(() => {
|
|
322
|
+
this.flushMetrics().catch(console.error);
|
|
323
|
+
}, this.config.flushInterval);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async flushMetrics() {
|
|
328
|
+
if (!this.metricsResource) return;
|
|
329
|
+
|
|
330
|
+
const [ok, err] = await tryFn(async () => {
|
|
331
|
+
// Use empty metadata during tests to avoid header issues
|
|
332
|
+
const metadata = process.env.NODE_ENV === 'test' ? {} : { global: 'true' };
|
|
333
|
+
const perfMetadata = process.env.NODE_ENV === 'test' ? {} : { perf: 'true' };
|
|
334
|
+
const errorMetadata = process.env.NODE_ENV === 'test' ? {} : { error: 'true' };
|
|
335
|
+
const resourceMetadata = process.env.NODE_ENV === 'test' ? {} : { resource: 'true' };
|
|
336
|
+
|
|
337
|
+
// Flush operation metrics
|
|
338
|
+
for (const [operation, data] of Object.entries(this.metrics.operations)) {
|
|
339
|
+
if (data.count > 0) {
|
|
340
|
+
await this.metricsResource.insert({
|
|
341
|
+
id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
342
|
+
type: 'operation',
|
|
343
|
+
resourceName: 'global',
|
|
344
|
+
operation,
|
|
345
|
+
count: data.count,
|
|
346
|
+
totalTime: data.totalTime,
|
|
347
|
+
errors: data.errors,
|
|
348
|
+
avgTime: data.count > 0 ? data.totalTime / data.count : 0,
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
metadata
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Flush resource-specific metrics
|
|
356
|
+
for (const [resourceName, operations] of Object.entries(this.metrics.resources)) {
|
|
357
|
+
for (const [operation, data] of Object.entries(operations)) {
|
|
358
|
+
if (data.count > 0) {
|
|
359
|
+
await this.metricsResource.insert({
|
|
360
|
+
id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
361
|
+
type: 'operation',
|
|
362
|
+
resourceName,
|
|
363
|
+
operation,
|
|
364
|
+
count: data.count,
|
|
365
|
+
totalTime: data.totalTime,
|
|
366
|
+
errors: data.errors,
|
|
367
|
+
avgTime: data.count > 0 ? data.totalTime / data.count : 0,
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
metadata: resourceMetadata
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Flush performance logs
|
|
376
|
+
if (this.config.collectPerformance && this.metrics.performance.length > 0) {
|
|
377
|
+
for (const perf of this.metrics.performance) {
|
|
378
|
+
await this.performanceResource.insert({
|
|
379
|
+
id: `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
380
|
+
resourceName: perf.resourceName,
|
|
381
|
+
operation: perf.operation,
|
|
382
|
+
duration: perf.duration,
|
|
383
|
+
timestamp: perf.timestamp,
|
|
384
|
+
metadata: perfMetadata
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Flush error logs
|
|
390
|
+
if (this.config.collectErrors && this.metrics.errors.length > 0) {
|
|
391
|
+
for (const error of this.metrics.errors) {
|
|
392
|
+
await this.errorsResource.insert({
|
|
393
|
+
id: `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
394
|
+
resourceName: error.resourceName,
|
|
395
|
+
operation: error.operation,
|
|
396
|
+
error: error.error,
|
|
397
|
+
stack: error.stack,
|
|
398
|
+
timestamp: error.timestamp,
|
|
399
|
+
metadata: errorMetadata
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Reset metrics after flushing
|
|
405
|
+
this.resetMetrics();
|
|
406
|
+
});
|
|
407
|
+
if (!ok) {
|
|
408
|
+
console.error('Failed to flush metrics:', err);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
resetMetrics() {
|
|
413
|
+
// Reset operation metrics
|
|
414
|
+
for (const operation of Object.keys(this.metrics.operations)) {
|
|
415
|
+
this.metrics.operations[operation] = { count: 0, totalTime: 0, errors: 0 };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Reset resource metrics
|
|
419
|
+
for (const resourceName of Object.keys(this.metrics.resources)) {
|
|
420
|
+
for (const operation of Object.keys(this.metrics.resources[resourceName])) {
|
|
421
|
+
this.metrics.resources[resourceName][operation] = { count: 0, totalTime: 0, errors: 0 };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Clear performance and error arrays
|
|
426
|
+
this.metrics.performance = [];
|
|
427
|
+
this.metrics.errors = [];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Utility methods
|
|
431
|
+
async getMetrics(options = {}) {
|
|
432
|
+
const {
|
|
433
|
+
type = 'operation',
|
|
434
|
+
resourceName,
|
|
435
|
+
operation,
|
|
436
|
+
startDate,
|
|
437
|
+
endDate,
|
|
438
|
+
limit = 100,
|
|
439
|
+
offset = 0
|
|
440
|
+
} = options;
|
|
441
|
+
|
|
442
|
+
if (!this.metricsResource) return [];
|
|
443
|
+
|
|
444
|
+
const allMetrics = await this.metricsResource.getAll();
|
|
445
|
+
|
|
446
|
+
let filtered = allMetrics.filter(metric => {
|
|
447
|
+
if (type && metric.type !== type) return false;
|
|
448
|
+
if (resourceName && metric.resourceName !== resourceName) return false;
|
|
449
|
+
if (operation && metric.operation !== operation) return false;
|
|
450
|
+
if (startDate && new Date(metric.timestamp) < new Date(startDate)) return false;
|
|
451
|
+
if (endDate && new Date(metric.timestamp) > new Date(endDate)) return false;
|
|
452
|
+
return true;
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Sort by timestamp descending
|
|
456
|
+
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
457
|
+
|
|
458
|
+
return filtered.slice(offset, offset + limit);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async getErrorLogs(options = {}) {
|
|
462
|
+
if (!this.errorsResource) return [];
|
|
463
|
+
|
|
464
|
+
const {
|
|
465
|
+
resourceName,
|
|
466
|
+
operation,
|
|
467
|
+
startDate,
|
|
468
|
+
endDate,
|
|
469
|
+
limit = 100,
|
|
470
|
+
offset = 0
|
|
471
|
+
} = options;
|
|
472
|
+
|
|
473
|
+
const allErrors = await this.errorsResource.getAll();
|
|
474
|
+
|
|
475
|
+
let filtered = allErrors.filter(error => {
|
|
476
|
+
if (resourceName && error.resourceName !== resourceName) return false;
|
|
477
|
+
if (operation && error.operation !== operation) return false;
|
|
478
|
+
if (startDate && new Date(error.timestamp) < new Date(startDate)) return false;
|
|
479
|
+
if (endDate && new Date(error.timestamp) > new Date(endDate)) return false;
|
|
480
|
+
return true;
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Sort by timestamp descending
|
|
484
|
+
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
485
|
+
|
|
486
|
+
return filtered.slice(offset, offset + limit);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async getPerformanceLogs(options = {}) {
|
|
490
|
+
if (!this.performanceResource) return [];
|
|
491
|
+
|
|
492
|
+
const {
|
|
493
|
+
resourceName,
|
|
494
|
+
operation,
|
|
495
|
+
startDate,
|
|
496
|
+
endDate,
|
|
497
|
+
limit = 100,
|
|
498
|
+
offset = 0
|
|
499
|
+
} = options;
|
|
500
|
+
|
|
501
|
+
const allPerformance = await this.performanceResource.getAll();
|
|
502
|
+
|
|
503
|
+
let filtered = allPerformance.filter(perf => {
|
|
504
|
+
if (resourceName && perf.resourceName !== resourceName) return false;
|
|
505
|
+
if (operation && perf.operation !== operation) return false;
|
|
506
|
+
if (startDate && new Date(perf.timestamp) < new Date(startDate)) return false;
|
|
507
|
+
if (endDate && new Date(perf.timestamp) > new Date(endDate)) return false;
|
|
508
|
+
return true;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Sort by timestamp descending
|
|
512
|
+
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
513
|
+
|
|
514
|
+
return filtered.slice(offset, offset + limit);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async getStats() {
|
|
518
|
+
const now = new Date();
|
|
519
|
+
const startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000)); // Last 24 hours
|
|
520
|
+
|
|
521
|
+
const [metrics, errors, performance] = await Promise.all([
|
|
522
|
+
this.getMetrics({ startDate: startDate.toISOString() }),
|
|
523
|
+
this.getErrorLogs({ startDate: startDate.toISOString() }),
|
|
524
|
+
this.getPerformanceLogs({ startDate: startDate.toISOString() })
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
// Calculate summary statistics
|
|
528
|
+
const stats = {
|
|
529
|
+
period: '24h',
|
|
530
|
+
totalOperations: 0,
|
|
531
|
+
totalErrors: errors.length,
|
|
532
|
+
avgResponseTime: 0,
|
|
533
|
+
operationsByType: {},
|
|
534
|
+
resources: {},
|
|
535
|
+
uptime: {
|
|
536
|
+
startTime: this.metrics.startTime,
|
|
537
|
+
duration: now.getTime() - new Date(this.metrics.startTime).getTime()
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Aggregate metrics
|
|
542
|
+
for (const metric of metrics) {
|
|
543
|
+
if (metric.type === 'operation') {
|
|
544
|
+
stats.totalOperations += metric.count;
|
|
545
|
+
|
|
546
|
+
if (!stats.operationsByType[metric.operation]) {
|
|
547
|
+
stats.operationsByType[metric.operation] = {
|
|
548
|
+
count: 0,
|
|
549
|
+
errors: 0,
|
|
550
|
+
avgTime: 0
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
stats.operationsByType[metric.operation].count += metric.count;
|
|
555
|
+
stats.operationsByType[metric.operation].errors += metric.errors;
|
|
556
|
+
|
|
557
|
+
// Calculate weighted average
|
|
558
|
+
const current = stats.operationsByType[metric.operation];
|
|
559
|
+
const totalCount = current.count;
|
|
560
|
+
const newAvg = ((current.avgTime * (totalCount - metric.count)) + metric.totalTime) / totalCount;
|
|
561
|
+
current.avgTime = newAvg;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Calculate overall average response time
|
|
566
|
+
const totalTime = metrics.reduce((sum, m) => sum + m.totalTime, 0);
|
|
567
|
+
const totalCount = metrics.reduce((sum, m) => sum + m.count, 0);
|
|
568
|
+
stats.avgResponseTime = totalCount > 0 ? totalTime / totalCount : 0;
|
|
569
|
+
|
|
570
|
+
return stats;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async cleanupOldData() {
|
|
574
|
+
const cutoffDate = new Date();
|
|
575
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
|
|
576
|
+
|
|
577
|
+
// Clean up old metrics
|
|
578
|
+
if (this.metricsResource) {
|
|
579
|
+
const oldMetrics = await this.getMetrics({ endDate: cutoffDate.toISOString() });
|
|
580
|
+
for (const metric of oldMetrics) {
|
|
581
|
+
await this.metricsResource.delete(metric.id);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Clean up old error logs
|
|
586
|
+
if (this.errorsResource) {
|
|
587
|
+
const oldErrors = await this.getErrorLogs({ endDate: cutoffDate.toISOString() });
|
|
588
|
+
for (const error of oldErrors) {
|
|
589
|
+
await this.errorsResource.delete(error.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Clean up old performance logs
|
|
594
|
+
if (this.performanceResource) {
|
|
595
|
+
const oldPerformance = await this.getPerformanceLogs({ endDate: cutoffDate.toISOString() });
|
|
596
|
+
for (const perf of oldPerformance) {
|
|
597
|
+
await this.performanceResource.delete(perf.id);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export default MetricsPlugin;
|