@spinajs/log-common 2.0.480 → 2.0.482
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -4
- package/lib/cjs/BatchQueue.d.ts +127 -0
- package/lib/cjs/BatchQueue.d.ts.map +1 -0
- package/lib/cjs/BatchQueue.js +126 -0
- package/lib/cjs/BatchQueue.js.map +1 -0
- package/lib/cjs/filters/filter.d.ts +27 -0
- package/lib/cjs/filters/filter.d.ts.map +1 -0
- package/lib/cjs/filters/filter.js +20 -0
- package/lib/cjs/filters/filter.js.map +1 -0
- package/lib/cjs/filters/whenRepeated.d.ts +52 -0
- package/lib/cjs/filters/whenRepeated.d.ts.map +1 -0
- package/lib/cjs/filters/whenRepeated.js +98 -0
- package/lib/cjs/filters/whenRepeated.js.map +1 -0
- package/lib/cjs/format.d.ts +20 -0
- package/lib/cjs/format.d.ts.map +1 -0
- package/lib/cjs/format.js +75 -0
- package/lib/cjs/format.js.map +1 -0
- package/lib/cjs/index.d.ts +260 -21
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +207 -33
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/perf.d.ts +109 -0
- package/lib/cjs/perf.d.ts.map +1 -0
- package/lib/cjs/perf.js +162 -0
- package/lib/cjs/perf.js.map +1 -0
- package/lib/cjs/persistence.d.ts +19 -0
- package/lib/cjs/persistence.d.ts.map +1 -0
- package/lib/cjs/persistence.js +137 -0
- package/lib/cjs/persistence.js.map +1 -0
- package/lib/cjs/serializers.d.ts +77 -0
- package/lib/cjs/serializers.d.ts.map +1 -0
- package/lib/cjs/serializers.js +215 -0
- package/lib/cjs/serializers.js.map +1 -0
- package/lib/mjs/BatchQueue.d.ts +127 -0
- package/lib/mjs/BatchQueue.d.ts.map +1 -0
- package/lib/mjs/BatchQueue.js +122 -0
- package/lib/mjs/BatchQueue.js.map +1 -0
- package/lib/mjs/filters/filter.d.ts +27 -0
- package/lib/mjs/filters/filter.d.ts.map +1 -0
- package/lib/mjs/filters/filter.js +16 -0
- package/lib/mjs/filters/filter.js.map +1 -0
- package/lib/mjs/filters/whenRepeated.d.ts +52 -0
- package/lib/mjs/filters/whenRepeated.d.ts.map +1 -0
- package/lib/mjs/filters/whenRepeated.js +95 -0
- package/lib/mjs/filters/whenRepeated.js.map +1 -0
- package/lib/mjs/format.d.ts +20 -0
- package/lib/mjs/format.d.ts.map +1 -0
- package/lib/mjs/format.js +72 -0
- package/lib/mjs/format.js.map +1 -0
- package/lib/mjs/index.d.ts +260 -21
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +201 -10
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/perf.d.ts +109 -0
- package/lib/mjs/perf.d.ts.map +1 -0
- package/lib/mjs/perf.js +157 -0
- package/lib/mjs/perf.js.map +1 -0
- package/lib/mjs/persistence.d.ts +19 -0
- package/lib/mjs/persistence.d.ts.map +1 -0
- package/lib/mjs/persistence.js +132 -0
- package/lib/mjs/persistence.js.map +1 -0
- package/lib/mjs/serializers.d.ts +77 -0
- package/lib/mjs/serializers.d.ts.map +1 -0
- package/lib/mjs/serializers.js +208 -0
- package/lib/mjs/serializers.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +8 -9
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export class BatchQueue {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.options = options;
|
|
4
|
+
this.buffer = [];
|
|
5
|
+
this.stopped = false;
|
|
6
|
+
/**
|
|
7
|
+
* The currently running flush, or null when idle. Used both as the
|
|
8
|
+
* re-entrancy guard ( coalesce concurrent flush() calls ) and to let
|
|
9
|
+
* forceFlush()/shutdown() wait for an in-flight flush before draining.
|
|
10
|
+
*/
|
|
11
|
+
this.inFlight = null;
|
|
12
|
+
this.timer = setInterval(() => void this.flush(), this.options.flushIntervalMs);
|
|
13
|
+
// Do not keep the event loop / process alive just for the flush tick.
|
|
14
|
+
this.timer.unref?.();
|
|
15
|
+
}
|
|
16
|
+
/** Current number of buffered items. */
|
|
17
|
+
get size() {
|
|
18
|
+
return this.buffer.length;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Add an item to the buffer.
|
|
22
|
+
*
|
|
23
|
+
* If the queue has been shut down this is a no-op that resolves immediately.
|
|
24
|
+
* When the buffer reaches `maxBatch` a flush is triggered and its promise is
|
|
25
|
+
* returned, so a caller MAY await a size-triggered flush; otherwise resolves
|
|
26
|
+
* immediately.
|
|
27
|
+
*/
|
|
28
|
+
enqueue(item) {
|
|
29
|
+
if (this.stopped) {
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
this.buffer.push(item);
|
|
33
|
+
this.enforceCap();
|
|
34
|
+
if (this.size >= this.options.maxBatch) {
|
|
35
|
+
return this.flush();
|
|
36
|
+
}
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Put items back at the FRONT of the buffer ( then enforce the cap ). Owners
|
|
41
|
+
* use this to re-queue a batch whose `onFlush` failed, without dropping it.
|
|
42
|
+
*/
|
|
43
|
+
requeueFront(items) {
|
|
44
|
+
this.buffer = [...items, ...this.buffer];
|
|
45
|
+
this.enforceCap();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Enforce the hard `maxQueue` cap by dropping the OLDEST items. Fires
|
|
49
|
+
* `onOverflow(droppedItems)` with the spliced-out items when anything is dropped.
|
|
50
|
+
*/
|
|
51
|
+
enforceCap() {
|
|
52
|
+
if (this.buffer.length > this.options.maxQueue) {
|
|
53
|
+
const dropCount = this.buffer.length - this.options.maxQueue;
|
|
54
|
+
const droppedItems = this.buffer.splice(0, dropCount);
|
|
55
|
+
this.options.onOverflow?.(droppedItems);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Flush the entire buffer in a single `onFlush` call.
|
|
60
|
+
*
|
|
61
|
+
* Re-entrancy: if a flush is already in flight, that same promise is returned
|
|
62
|
+
* ( no second `onFlush` ). If the buffer is empty, resolves immediately.
|
|
63
|
+
* Otherwise the buffer is snapshotted and cleared synchronously, `onFlush`
|
|
64
|
+
* runs ( optionally raced against `exportTimeoutMs` ), and the in-flight guard
|
|
65
|
+
* is cleared in `finally` regardless of success or failure. The returned
|
|
66
|
+
* promise settles with `onFlush`'s outcome so an awaiting owner can catch a
|
|
67
|
+
* rejection and requeue.
|
|
68
|
+
*/
|
|
69
|
+
flush() {
|
|
70
|
+
if (this.inFlight) {
|
|
71
|
+
return this.inFlight;
|
|
72
|
+
}
|
|
73
|
+
if (this.buffer.length === 0) {
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
}
|
|
76
|
+
const batch = this.buffer;
|
|
77
|
+
this.buffer = [];
|
|
78
|
+
const run = this.options.exportTimeoutMs !== undefined ? this.withTimeout(this.options.onFlush(batch), this.options.exportTimeoutMs) : this.options.onFlush(batch);
|
|
79
|
+
// Store as the guard and clear it once settled ( success OR failure ) so the
|
|
80
|
+
// next flush can proceed. The promise is still returned to the caller so the
|
|
81
|
+
// rejection propagates for owner-side handling.
|
|
82
|
+
this.inFlight = run.finally(() => {
|
|
83
|
+
this.inFlight = null;
|
|
84
|
+
});
|
|
85
|
+
return this.inFlight;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Race a flush promise against a timeout that rejects after `ms`. The timeout
|
|
89
|
+
* timer is cleared ( and unref()'d ) so it neither leaks nor keeps the loop alive.
|
|
90
|
+
*/
|
|
91
|
+
withTimeout(promise, ms) {
|
|
92
|
+
let timeout;
|
|
93
|
+
const timer = new Promise((_resolve, reject) => {
|
|
94
|
+
timeout = setTimeout(() => reject(new Error(`BatchQueue flush timed out after ${ms}ms`)), ms);
|
|
95
|
+
timeout.unref?.();
|
|
96
|
+
});
|
|
97
|
+
return Promise.race([promise, timer]).finally(() => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Flush any buffered items and await completion. If a flush is already in
|
|
103
|
+
* flight, wait for it first, then flush any remainder that accumulated. Since
|
|
104
|
+
* a single `flush()` drains the ENTIRE buffer, one follow-up flush suffices.
|
|
105
|
+
*/
|
|
106
|
+
async forceFlush() {
|
|
107
|
+
if (this.inFlight) {
|
|
108
|
+
await this.inFlight;
|
|
109
|
+
}
|
|
110
|
+
await this.flush();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Stop the periodic timer and perform a best-effort final flush of buffered
|
|
114
|
+
* items. After shutdown, `enqueue` is a no-op that resolves.
|
|
115
|
+
*/
|
|
116
|
+
async shutdown() {
|
|
117
|
+
this.stopped = true;
|
|
118
|
+
clearInterval(this.timer);
|
|
119
|
+
await this.forceFlush();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=BatchQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BatchQueue.js","sourceRoot":"","sources":["../../src/BatchQueue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ILogEntry } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Common shape of a configured filter entry. `type` is the DI string name the
|
|
4
|
+
* filter is registered under ( eg. "LevelFilter" ); all other keys are the
|
|
5
|
+
* filter's own free-form options and are read by the concrete filter.
|
|
6
|
+
*/
|
|
7
|
+
export interface ILogFilterOptions {
|
|
8
|
+
type: string;
|
|
9
|
+
[k: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Base class for a composable log filter. Filters are DI-registered by string
|
|
13
|
+
* name ( just like log targets ) via `@Injectable("<Name>")` and resolved per
|
|
14
|
+
* logger from `logger.filters` config. They run IN ORDER inside the logger's
|
|
15
|
+
* `write()`: each filter may pass the entry through ( optionally mutating it ),
|
|
16
|
+
* or return `null` to DROP it entirely.
|
|
17
|
+
*
|
|
18
|
+
* Kept a plain, dependency-light abstract class ( no SyncService etc. ) so the
|
|
19
|
+
* contract stays browser-safe and cheap to instantiate.
|
|
20
|
+
*/
|
|
21
|
+
export declare abstract class LogFilter {
|
|
22
|
+
protected options?: ILogFilterOptions | undefined;
|
|
23
|
+
constructor(options?: ILogFilterOptions | undefined);
|
|
24
|
+
/** Return the ( possibly modified ) entry to keep, or null to DROP it. */
|
|
25
|
+
abstract apply(entry: ILogEntry): ILogEntry | null;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../../src/filters/filter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,8BAAsB,SAAS;IACjB,SAAS,CAAC,OAAO,CAAC,EAAE,iBAAiB;gBAA3B,OAAO,CAAC,EAAE,iBAAiB,YAAA;IAEjD,0EAA0E;aAC1D,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;CAC1D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for a composable log filter. Filters are DI-registered by string
|
|
3
|
+
* name ( just like log targets ) via `@Injectable("<Name>")` and resolved per
|
|
4
|
+
* logger from `logger.filters` config. They run IN ORDER inside the logger's
|
|
5
|
+
* `write()`: each filter may pass the entry through ( optionally mutating it ),
|
|
6
|
+
* or return `null` to DROP it entirely.
|
|
7
|
+
*
|
|
8
|
+
* Kept a plain, dependency-light abstract class ( no SyncService etc. ) so the
|
|
9
|
+
* contract stays browser-safe and cheap to instantiate.
|
|
10
|
+
*/
|
|
11
|
+
export class LogFilter {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../../src/filters/filter.ts"],"names":[],"mappings":"AAeA;;;;;;;;;GASG;AACH,MAAM,OAAgB,SAAS;IAC7B,YAAsB,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;CAItD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ILogEntry } from "../index.js";
|
|
2
|
+
import { LogFilter, ILogFilterOptions } from "./filter.js";
|
|
3
|
+
export interface IWhenRepeatedOptions extends Partial<ILogFilterOptions> {
|
|
4
|
+
/**
|
|
5
|
+
* Suppression window in SECONDS. Identical entries logged within this window
|
|
6
|
+
* ( from the first occurrence ) are collapsed into one. Default is 10.
|
|
7
|
+
*/
|
|
8
|
+
timeout?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Hard cap on the number of tracked keys. The internal map is self-pruning:
|
|
11
|
+
* expired records are dropped first, then the oldest, so memory stays bounded
|
|
12
|
+
* even under many distinct messages. Default is 1024.
|
|
13
|
+
*/
|
|
14
|
+
maxKeys?: number;
|
|
15
|
+
}
|
|
16
|
+
interface IRepeatRecord {
|
|
17
|
+
count: number;
|
|
18
|
+
windowStart: number;
|
|
19
|
+
level: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Port of NLog's WhenRepeatedFilter. Collapses identical, repeated log entries
|
|
23
|
+
* within a timeout window into a single emitted entry, appending a ` (xN)`
|
|
24
|
+
* suppressed-count when logging of that key resumes. This stops a hot loop or a
|
|
25
|
+
* flapping error from flooding console / file / Loki and burying real signal.
|
|
26
|
+
*
|
|
27
|
+
* Known tradeoff: there is no background timer. A residual suppressed count is
|
|
28
|
+
* flushed on the NEXT matching entry ( window expired or higher severity ), NOT
|
|
29
|
+
* when the window simply elapses with no further activity. This keeps the filter
|
|
30
|
+
* dependency-free; the full filter pipeline in a later phase can layer a timed
|
|
31
|
+
* flush on top.
|
|
32
|
+
*/
|
|
33
|
+
export declare class WhenRepeatedFilter extends LogFilter {
|
|
34
|
+
protected now: () => number;
|
|
35
|
+
protected timeoutMs: number;
|
|
36
|
+
protected maxKeys: number;
|
|
37
|
+
protected records: Map<string, IRepeatRecord>;
|
|
38
|
+
constructor(options?: IWhenRepeatedOptions, now?: () => number);
|
|
39
|
+
protected keyOf(entry: ILogEntry): string;
|
|
40
|
+
/**
|
|
41
|
+
* Returns the entry to EMIT ( possibly with an injected suppressed-count ) or
|
|
42
|
+
* `null` to SUPPRESS.
|
|
43
|
+
*/
|
|
44
|
+
apply(entry: ILogEntry): ILogEntry | null;
|
|
45
|
+
/**
|
|
46
|
+
* Keeps the tracked-key map bounded. Drops expired records first, then the
|
|
47
|
+
* oldest by `windowStart` until at/under the cap.
|
|
48
|
+
*/
|
|
49
|
+
protected prune(now: number): void;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=whenRepeated.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whenRepeated.d.ts","sourceRoot":"","sources":["../../../src/filters/whenRepeated.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,oBAAqB,SAAQ,OAAO,CAAC,iBAAiB,CAAC;IACtE;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,qBAEa,kBAAmB,SAAQ,SAAS;IAKH,SAAS,CAAC,GAAG,EAAE,MAAM,MAAM;IAJvE,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAa;gBAE9C,OAAO,CAAC,EAAE,oBAAoB,EAAY,GAAG,GAAE,MAAM,MAAyB;IAM1F,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM;IAIzC;;;OAGG;IACI,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;IA8BhD;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAuBnC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Injectable, NewInstance } from "@spinajs/di";
|
|
11
|
+
import { LogFilter } from "./filter.js";
|
|
12
|
+
/**
|
|
13
|
+
* Port of NLog's WhenRepeatedFilter. Collapses identical, repeated log entries
|
|
14
|
+
* within a timeout window into a single emitted entry, appending a ` (xN)`
|
|
15
|
+
* suppressed-count when logging of that key resumes. This stops a hot loop or a
|
|
16
|
+
* flapping error from flooding console / file / Loki and burying real signal.
|
|
17
|
+
*
|
|
18
|
+
* Known tradeoff: there is no background timer. A residual suppressed count is
|
|
19
|
+
* flushed on the NEXT matching entry ( window expired or higher severity ), NOT
|
|
20
|
+
* when the window simply elapses with no further activity. This keeps the filter
|
|
21
|
+
* dependency-free; the full filter pipeline in a later phase can layer a timed
|
|
22
|
+
* flush on top.
|
|
23
|
+
*/
|
|
24
|
+
let WhenRepeatedFilter = class WhenRepeatedFilter extends LogFilter {
|
|
25
|
+
constructor(options, now = () => Date.now()) {
|
|
26
|
+
super(options);
|
|
27
|
+
this.now = now;
|
|
28
|
+
this.records = new Map();
|
|
29
|
+
this.timeoutMs = (options?.timeout ?? 10) * 1000;
|
|
30
|
+
this.maxKeys = options?.maxKeys ?? 1024;
|
|
31
|
+
}
|
|
32
|
+
keyOf(entry) {
|
|
33
|
+
return `${entry.Level}|${entry.Variables.logger}|${entry.Variables.message}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns the entry to EMIT ( possibly with an injected suppressed-count ) or
|
|
37
|
+
* `null` to SUPPRESS.
|
|
38
|
+
*/
|
|
39
|
+
apply(entry) {
|
|
40
|
+
const key = this.keyOf(entry);
|
|
41
|
+
const now = this.now();
|
|
42
|
+
const record = this.records.get(key);
|
|
43
|
+
if (!record) {
|
|
44
|
+
// first occurrence of this key - emit and start tracking
|
|
45
|
+
this.records.set(key, { count: 0, windowStart: now, level: entry.Level });
|
|
46
|
+
this.prune(now);
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
if (now - record.windowStart < this.timeoutMs && entry.Level <= record.level) {
|
|
50
|
+
// still inside the window and not a higher severity - suppress
|
|
51
|
+
record.count++;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
// window expired OR a higher-severity entry arrived - emit and reset. If we
|
|
55
|
+
// suppressed anything, inject the count into the ( freshly built per call,
|
|
56
|
+
// so safe to mutate ) message string.
|
|
57
|
+
if (record.count > 0) {
|
|
58
|
+
entry.Variables.message = `${entry.Variables.message} (x${record.count})`;
|
|
59
|
+
}
|
|
60
|
+
this.records.set(key, { count: 0, windowStart: now, level: entry.Level });
|
|
61
|
+
this.prune(now);
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Keeps the tracked-key map bounded. Drops expired records first, then the
|
|
66
|
+
* oldest by `windowStart` until at/under the cap.
|
|
67
|
+
*/
|
|
68
|
+
prune(now) {
|
|
69
|
+
if (this.records.size <= this.maxKeys) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const [k, r] of this.records) {
|
|
73
|
+
if (now - r.windowStart >= this.timeoutMs) {
|
|
74
|
+
this.records.delete(k);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (this.records.size <= this.maxKeys) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const byAge = [...this.records.entries()].sort((a, b) => a[1].windowStart - b[1].windowStart);
|
|
81
|
+
for (const [k] of byAge) {
|
|
82
|
+
if (this.records.size <= this.maxKeys) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
this.records.delete(k);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
WhenRepeatedFilter = __decorate([
|
|
90
|
+
Injectable("WhenRepeatedFilter"),
|
|
91
|
+
NewInstance(),
|
|
92
|
+
__metadata("design:paramtypes", [Object, Function])
|
|
93
|
+
], WhenRepeatedFilter);
|
|
94
|
+
export { WhenRepeatedFilter };
|
|
95
|
+
//# sourceMappingURL=whenRepeated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whenRepeated.js","sourceRoot":"","sources":["../../../src/filters/whenRepeated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, platform-free clone of Node's `util.format`.
|
|
3
|
+
*
|
|
4
|
+
* Supports the format specifiers the framework actually uses:
|
|
5
|
+
* %s - String
|
|
6
|
+
* %d - Number ( integer )
|
|
7
|
+
* %i - parseInt
|
|
8
|
+
* %f - parseFloat
|
|
9
|
+
* %j - JSON
|
|
10
|
+
* %o - Object ( JSON.stringify )
|
|
11
|
+
* %O - Object ( JSON.stringify )
|
|
12
|
+
* %% - literal percent sign ( consumes no argument )
|
|
13
|
+
*
|
|
14
|
+
* Any arguments left over after the specifiers are consumed are appended,
|
|
15
|
+
* space-separated. Objects are rendered via JSON.stringify ( circular refs
|
|
16
|
+
* fall back to String() ). This avoids pulling in Node's `util` module so the
|
|
17
|
+
* logger is usable in the browser.
|
|
18
|
+
*/
|
|
19
|
+
export declare function format(f?: any, ...args: any[]): string;
|
|
20
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CA0CtD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* Minimal, platform-free clone of Node's `util.format`.
|
|
4
|
+
*
|
|
5
|
+
* Supports the format specifiers the framework actually uses:
|
|
6
|
+
* %s - String
|
|
7
|
+
* %d - Number ( integer )
|
|
8
|
+
* %i - parseInt
|
|
9
|
+
* %f - parseFloat
|
|
10
|
+
* %j - JSON
|
|
11
|
+
* %o - Object ( JSON.stringify )
|
|
12
|
+
* %O - Object ( JSON.stringify )
|
|
13
|
+
* %% - literal percent sign ( consumes no argument )
|
|
14
|
+
*
|
|
15
|
+
* Any arguments left over after the specifiers are consumed are appended,
|
|
16
|
+
* space-separated. Objects are rendered via JSON.stringify ( circular refs
|
|
17
|
+
* fall back to String() ). This avoids pulling in Node's `util` module so the
|
|
18
|
+
* logger is usable in the browser.
|
|
19
|
+
*/
|
|
20
|
+
export function format(f, ...args) {
|
|
21
|
+
if (typeof f !== 'string') {
|
|
22
|
+
// no format string: join everything space-separated
|
|
23
|
+
return [f, ...args].map((a) => inspect(a)).join(' ');
|
|
24
|
+
}
|
|
25
|
+
let argIndex = 0;
|
|
26
|
+
const str = f.replace(/%[sdifjoO%]/g, (match) => {
|
|
27
|
+
if (match === '%%') {
|
|
28
|
+
return '%';
|
|
29
|
+
}
|
|
30
|
+
if (argIndex >= args.length) {
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
const arg = args[argIndex++];
|
|
34
|
+
switch (match) {
|
|
35
|
+
case '%s':
|
|
36
|
+
return typeof arg === 'object' && arg !== null ? inspect(arg) : String(arg);
|
|
37
|
+
case '%d':
|
|
38
|
+
return String(Number(arg));
|
|
39
|
+
case '%i':
|
|
40
|
+
return String(parseInt(arg, 10));
|
|
41
|
+
case '%f':
|
|
42
|
+
return String(parseFloat(arg));
|
|
43
|
+
case '%j':
|
|
44
|
+
case '%o':
|
|
45
|
+
case '%O':
|
|
46
|
+
return inspect(arg);
|
|
47
|
+
default:
|
|
48
|
+
return match;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
if (argIndex >= args.length) {
|
|
52
|
+
return str;
|
|
53
|
+
}
|
|
54
|
+
// append remaining args space-separated
|
|
55
|
+
const rest = args.slice(argIndex).map((a) => (typeof a === 'string' ? a : inspect(a)));
|
|
56
|
+
return [str, ...rest].join(' ');
|
|
57
|
+
}
|
|
58
|
+
function inspect(value) {
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'object' && value !== null) {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/format.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,MAAM,CAAC,CAAO,EAAE,GAAG,IAAW;IAC5C,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,oDAAoD;QACpD,OAAO,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,KAAa,EAAU,EAAE;QAC9D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,GAAG,CAAC;QACb,CAAC;QAED,IAAI,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7B,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,IAAI;gBACP,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9E,KAAK,IAAI;gBACP,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC7B,KAAK,IAAI;gBACP,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAU,EAAE,EAAE,CAAC,CAAC,CAAC;YAC1C,KAAK,IAAI;gBACP,OAAO,MAAM,CAAC,UAAU,CAAC,GAAU,CAAC,CAAC,CAAC;YACxC,KAAK,IAAI,CAAC;YACV,KAAK,IAAI,CAAC;YACV,KAAK,IAAI;gBACP,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;YACtB;gBACE,OAAO,KAAK,CAAC;QACjB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,wCAAwC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,OAAO,CAAC,KAAU;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
|