@visulima/pail 3.1.0 → 3.2.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/CHANGELOG.md +59 -0
- package/LICENSE.md +3 -407
- package/README.md +298 -0
- package/dist/constants.d.ts +37 -0
- package/dist/index.browser.d.ts +47 -12
- package/dist/index.browser.js +12 -1
- package/dist/index.server.d.ts +65 -40
- package/dist/index.server.js +533 -2
- package/dist/interactive/index.d.ts +2 -28
- package/dist/interactive/index.js +2 -1
- package/dist/interactive/interactive-manager.d.ts +108 -0
- package/dist/interactive/interactive-stream-hook.d.ts +68 -0
- package/dist/object-tree.d.ts +65 -7
- package/dist/object-tree.js +89 -2
- package/dist/packem_shared/AbstractJsonReporter-intFdT_A.js +204 -0
- package/dist/packem_shared/InteractiveManager-CZ85hGNW.js +172 -0
- package/dist/packem_shared/InteractiveStreamHook-DiSubbJ1.js +21 -0
- package/dist/packem_shared/JsonReporter-C0AXk99i.js +58 -0
- package/dist/packem_shared/JsonReporter-DcM2LBX9.js +28 -0
- package/dist/packem_shared/PrettyReporter-BFWaYP_J.js +222 -0
- package/dist/packem_shared/PrettyReporter-CuLLKr6-.js +169 -0
- package/dist/packem_shared/abstract-pretty-reporter-DMPDCslJ.js +50 -0
- package/dist/packem_shared/constants-DfDr4MHC.js +119 -0
- package/dist/packem_shared/format-label-Btft2KGP.js +1194 -0
- package/dist/packem_shared/get-longest-label-C9PWeyKq.js +9 -0
- package/dist/packem_shared/index-BomQ3E6J.js +650 -0
- package/dist/packem_shared/index-DqKWykfa.js +1146 -0
- package/dist/packem_shared/interactive-stream-hook-DG4BtN12.js +141 -0
- package/dist/packem_shared/pail.browser-CPjQrsyy.js +1427 -0
- package/dist/packem_shared/write-console-log-based-on-level-DBmRYXpj.js +14 -0
- package/dist/packem_shared/write-stream-BG8fhcs3.js +6 -0
- package/dist/pail.browser.d.ts +412 -0
- package/dist/pail.server.d.ts +233 -0
- package/dist/processor/caller/caller-processor.d.ts +40 -7
- package/dist/processor/caller/caller-processor.js +59 -1
- package/dist/processor/caller/get-caller-filename.d.ts +23 -0
- package/dist/processor/message-formatter-processor.d.ts +44 -9
- package/dist/processor/message-formatter-processor.js +67 -1
- package/dist/processor/opentelemetry-processor.d.ts +70 -0
- package/dist/processor/opentelemetry-processor.js +52 -0
- package/dist/processor/redact-processor.d.ts +39 -8
- package/dist/processor/redact-processor.js +30 -1
- package/dist/progress-bar.d.ts +75 -15
- package/dist/progress-bar.js +404 -1
- package/dist/reporter/file/json-file-reporter.d.ts +39 -20
- package/dist/reporter/file/json-file-reporter.js +136 -4
- package/dist/reporter/file/utils/rotating-file-stream.d.ts +48 -0
- package/dist/reporter/http/abstract-http-reporter.d.ts +215 -0
- package/dist/reporter/http/abstract-http-reporter.js +435 -0
- package/dist/reporter/http/http-reporter.d.ts +39 -0
- package/dist/reporter/http/http-reporter.edge-light.d.ts +40 -0
- package/dist/reporter/http/http-reporter.edge-light.js +651 -0
- package/dist/reporter/http/http-reporter.js +13 -0
- package/dist/reporter/http/utils/compression.d.ts +7 -0
- package/dist/reporter/http/utils/log-size-error.d.ts +30 -0
- package/dist/reporter/http/utils/retry.d.ts +27 -0
- package/dist/reporter/json/abstract-json-reporter.d.ts +61 -0
- package/dist/reporter/json/index.browser.d.ts +3 -13
- package/dist/reporter/json/index.browser.js +2 -1
- package/dist/reporter/json/index.d.ts +3 -16
- package/dist/reporter/json/index.js +2 -1
- package/dist/reporter/json/json-reporter.browser.d.ts +40 -0
- package/dist/reporter/json/json-reporter.server.d.ts +50 -0
- package/dist/reporter/pretty/abstract-pretty-reporter.d.ts +83 -0
- package/dist/reporter/pretty/index.browser.d.ts +2 -13
- package/dist/reporter/pretty/index.browser.js +1 -1
- package/dist/reporter/pretty/index.d.ts +2 -25
- package/dist/reporter/pretty/index.js +1 -1
- package/dist/reporter/pretty/pretty-reporter.browser.d.ts +36 -0
- package/dist/reporter/pretty/pretty-reporter.server.d.ts +70 -0
- package/dist/reporter/raw/raw-reporter.browser.d.ts +5 -0
- package/dist/reporter/raw/raw-reporter.server.d.ts +13 -0
- package/dist/reporter/simple/simple-reporter.server.d.ts +10 -14
- package/dist/reporter/simple/simple-reporter.server.js +186 -8
- package/dist/reporter/utils/default-inspector-config.d.ts +3 -0
- package/dist/reporter/utils/format-label.d.ts +3 -0
- package/dist/spinner.d.ts +170 -104
- package/dist/spinner.js +2150 -1
- package/dist/types.d.ts +241 -0
- package/dist/utils/ansi-escapes.d.ts +4 -0
- package/dist/utils/arrayify.d.ts +2 -0
- package/dist/utils/get-longest-badge.d.ts +4 -0
- package/dist/utils/get-longest-label.d.ts +4 -0
- package/dist/utils/merge-types.d.ts +4 -0
- package/dist/utils/stream/safe-stream-handler.d.ts +21 -0
- package/dist/utils/write-console-log-based-on-level.d.ts +4 -0
- package/dist/utils/write-stream.d.ts +2 -0
- package/package.json +53 -4
- package/dist/packem_shared/AbstractJsonReporter-UftN6CIL.js +0 -1
- package/dist/packem_shared/InteractiveManager-CgmJyW9x.js +0 -3
- package/dist/packem_shared/InteractiveStreamHook-NtJu71aN.js +0 -1
- package/dist/packem_shared/JsonReporter-DTBtHNaD.js +0 -2
- package/dist/packem_shared/JsonReporter-Dl4m0xZe.js +0 -1
- package/dist/packem_shared/PrettyReporter-Bns0ZWLy.js +0 -12
- package/dist/packem_shared/PrettyReporter-CGKSTI7X.js +0 -5
- package/dist/packem_shared/abstract-json-reporter-CPsNkpz8.d.ts +0 -22
- package/dist/packem_shared/abstract-pretty-reporter-CUtSm20r.js +0 -1
- package/dist/packem_shared/abstract-pretty-reporter-DB2G-qlI.d.ts +0 -28
- package/dist/packem_shared/constants-DKfCaSUR.js +0 -1
- package/dist/packem_shared/format-label-CpyyTBom.js +0 -26
- package/dist/packem_shared/get-longest-label-B0NrI-o2.js +0 -1
- package/dist/packem_shared/index-CysYvHXs.js +0 -8
- package/dist/packem_shared/index-D9hWq9ka.js +0 -1
- package/dist/packem_shared/index.d-BR1GjZri.d.ts +0 -53
- package/dist/packem_shared/index.d-oxZvg_y7.d.ts +0 -20
- package/dist/packem_shared/interactive-stream-hook-CeVo4Kth.js +0 -2
- package/dist/packem_shared/pail.browser-BmHoDvEA.js +0 -19
- package/dist/packem_shared/pail.browser-CmWcqnn9.d.ts +0 -64
- package/dist/packem_shared/types-DVzG8TWL.d.ts +0 -95
- package/dist/packem_shared/write-console-log-based-on-level-BP95fgQZ.js +0 -1
- package/dist/packem_shared/write-stream-CD8XFv1L.js +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { LiteralUnion } from "type-fest";
|
|
2
|
+
import type { ExtendedRfc5424LogLevels, StringifyAwareReporter } from "../../types.d.ts";
|
|
3
|
+
import type { AbstractJsonReporterOptions } from "../json/abstract-json-reporter.d.ts";
|
|
4
|
+
import { AbstractJsonReporter } from "../json/abstract-json-reporter.d.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for the HTTP reporter.
|
|
7
|
+
*/
|
|
8
|
+
export type AbstractHttpReporterOptions = AbstractJsonReporterOptions & {
|
|
9
|
+
/**
|
|
10
|
+
* Content type for batch log requests. User-specified headers take precedence.
|
|
11
|
+
* @default "application/json"
|
|
12
|
+
*/
|
|
13
|
+
batchContentType?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Field name to wrap batch entries in when batchMode is "field" (e.g., "batch" for Logflare)
|
|
16
|
+
* @default undefined
|
|
17
|
+
*/
|
|
18
|
+
batchFieldName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Batch mode for sending multiple log entries
|
|
21
|
+
* - "delimiter": Join entries with a delimiter (default)
|
|
22
|
+
* - "field": Wrap entries in an object with a field name
|
|
23
|
+
* - "array": Send entries as a plain JSON array
|
|
24
|
+
* @default "delimiter"
|
|
25
|
+
*/
|
|
26
|
+
batchMode?: "delimiter" | "field" | "array";
|
|
27
|
+
/**
|
|
28
|
+
* Delimiter to use between log entries in batch mode
|
|
29
|
+
* @default "\n"
|
|
30
|
+
*/
|
|
31
|
+
batchSendDelimiter?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Timeout in milliseconds for sending batches regardless of size
|
|
34
|
+
* @default 5000
|
|
35
|
+
*/
|
|
36
|
+
batchSendTimeout?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Number of log entries to batch before sending
|
|
39
|
+
* @default 100
|
|
40
|
+
*/
|
|
41
|
+
batchSize?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Whether to use gzip compression
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
compression?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Content type for single log requests. User-specified headers take precedence.
|
|
49
|
+
* @default "application/json"
|
|
50
|
+
*/
|
|
51
|
+
contentType?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Whether to enable Edge Runtime compatibility mode
|
|
54
|
+
* When enabled, TextEncoder and compression are disabled
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
edgeCompat?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether to enable batch sending
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
enableBatchSend?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Headers to include in the request. Can be an object or a function that returns headers.
|
|
65
|
+
*/
|
|
66
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
67
|
+
/**
|
|
68
|
+
* Maximum size of a single log entry in bytes
|
|
69
|
+
* @default 1048576 (1MB)
|
|
70
|
+
*/
|
|
71
|
+
maxLogSize?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Maximum size of the payload (uncompressed) in bytes
|
|
74
|
+
* @default 5242880 (5MB)
|
|
75
|
+
*/
|
|
76
|
+
maxPayloadSize?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Number of retry attempts before giving up
|
|
79
|
+
* @default 3
|
|
80
|
+
*/
|
|
81
|
+
maxRetries?: number;
|
|
82
|
+
/**
|
|
83
|
+
* HTTP method to use for requests
|
|
84
|
+
* @default "POST"
|
|
85
|
+
*/
|
|
86
|
+
method?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Optional callback for debugging log entries before they are sent
|
|
89
|
+
*/
|
|
90
|
+
onDebug?: (entry: Record<string, unknown>) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Optional callback for debugging HTTP requests and responses
|
|
93
|
+
*/
|
|
94
|
+
onDebugRequestResponse?: (requestResponse: {
|
|
95
|
+
req: {
|
|
96
|
+
body: string | Uint8Array;
|
|
97
|
+
headers: Record<string, string>;
|
|
98
|
+
method: string;
|
|
99
|
+
url: string;
|
|
100
|
+
};
|
|
101
|
+
res: {
|
|
102
|
+
body: string;
|
|
103
|
+
headers: Record<string, string>;
|
|
104
|
+
status: number;
|
|
105
|
+
statusText: string;
|
|
106
|
+
};
|
|
107
|
+
}) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Optional callback for error handling
|
|
110
|
+
*/
|
|
111
|
+
onError?: (error: Error) => void;
|
|
112
|
+
/**
|
|
113
|
+
* Function to transform log data into the payload format.
|
|
114
|
+
* By default, uses the JSON stringified log entry.
|
|
115
|
+
*/
|
|
116
|
+
payloadTemplate?: (data: {
|
|
117
|
+
data?: Record<string, unknown>;
|
|
118
|
+
logLevel: string;
|
|
119
|
+
message: string;
|
|
120
|
+
}) => string;
|
|
121
|
+
/**
|
|
122
|
+
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
123
|
+
* @default true
|
|
124
|
+
*/
|
|
125
|
+
respectRateLimit?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Base delay between retries in milliseconds
|
|
128
|
+
* @default 1000
|
|
129
|
+
*/
|
|
130
|
+
retryDelay?: number;
|
|
131
|
+
/**
|
|
132
|
+
* The URL to send logs to
|
|
133
|
+
*/
|
|
134
|
+
url: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Abstract HTTP Reporter.
|
|
138
|
+
*
|
|
139
|
+
* Base class for HTTP-based reporters that sends logs to HTTP endpoints.
|
|
140
|
+
* Supports batching, compression, retries, and rate limiting.
|
|
141
|
+
* @template L - The log level type
|
|
142
|
+
*/
|
|
143
|
+
export declare abstract class AbstractHttpReporter<L extends string = string> extends AbstractJsonReporter<L> implements StringifyAwareReporter<L> {
|
|
144
|
+
protected url: string;
|
|
145
|
+
protected method: string;
|
|
146
|
+
protected headers: Record<string, string> | (() => Record<string, string>);
|
|
147
|
+
protected contentType: string;
|
|
148
|
+
protected batchContentType: string;
|
|
149
|
+
protected onError?: (error: Error) => void;
|
|
150
|
+
protected onDebug?: (entry: Record<string, unknown>) => void;
|
|
151
|
+
protected onDebugRequestResponse?: (requestResponse: {
|
|
152
|
+
req: {
|
|
153
|
+
body: string | Uint8Array;
|
|
154
|
+
headers: Record<string, string>;
|
|
155
|
+
method: string;
|
|
156
|
+
url: string;
|
|
157
|
+
};
|
|
158
|
+
res: {
|
|
159
|
+
body: string;
|
|
160
|
+
headers: Record<string, string>;
|
|
161
|
+
status: number;
|
|
162
|
+
statusText: string;
|
|
163
|
+
};
|
|
164
|
+
}) => void;
|
|
165
|
+
protected payloadTemplate: (data: {
|
|
166
|
+
data?: Record<string, unknown>;
|
|
167
|
+
logLevel: string;
|
|
168
|
+
message: string;
|
|
169
|
+
}) => string;
|
|
170
|
+
protected compression: boolean;
|
|
171
|
+
protected maxRetries: number;
|
|
172
|
+
protected retryDelay: number;
|
|
173
|
+
protected respectRateLimit: boolean;
|
|
174
|
+
protected enableBatchSend: boolean;
|
|
175
|
+
protected batchSize: number;
|
|
176
|
+
protected batchSendTimeout: number;
|
|
177
|
+
protected batchSendDelimiter: string;
|
|
178
|
+
protected batchMode: "delimiter" | "field" | "array";
|
|
179
|
+
protected batchFieldName?: string;
|
|
180
|
+
protected maxLogSize: number;
|
|
181
|
+
protected maxPayloadSize: number;
|
|
182
|
+
protected edgeCompat: boolean;
|
|
183
|
+
protected batchQueue: string[];
|
|
184
|
+
protected batchTimeout?: ReturnType<typeof setTimeout>;
|
|
185
|
+
protected isProcessingBatch: boolean;
|
|
186
|
+
protected currentBatchSize: number;
|
|
187
|
+
/**
|
|
188
|
+
* Creates a new instance of AbstractHttpReporter.
|
|
189
|
+
* @param config Configuration options for the reporter
|
|
190
|
+
*/
|
|
191
|
+
constructor(config: AbstractHttpReporterOptions);
|
|
192
|
+
/**
|
|
193
|
+
* Processes and ships log entries to the HTTP endpoint.
|
|
194
|
+
* @param message The JSON-formatted log message
|
|
195
|
+
* @param logLevel The log level of the message
|
|
196
|
+
* @protected
|
|
197
|
+
*/
|
|
198
|
+
protected _log(message: string, logLevel: LiteralUnion<ExtendedRfc5424LogLevels, L>): void;
|
|
199
|
+
/**
|
|
200
|
+
* Adds a payload to the batch queue and triggers sending if conditions are met.
|
|
201
|
+
*/
|
|
202
|
+
protected addToBatch(payload: string, logEntrySize: number): void;
|
|
203
|
+
/**
|
|
204
|
+
* Processes the current batch and sends it to the HTTP endpoint.
|
|
205
|
+
*/
|
|
206
|
+
protected processBatch(): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* Sends a batch of payloads to the HTTP endpoint.
|
|
209
|
+
*/
|
|
210
|
+
protected sendBatch(batch: string[]): Promise<void>;
|
|
211
|
+
/**
|
|
212
|
+
* Sends a single payload to the HTTP endpoint.
|
|
213
|
+
*/
|
|
214
|
+
protected sendPayload(payload: string, contentType?: string): Promise<void>;
|
|
215
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
Buffer
|
|
22
|
+
} = __cjs_getBuiltinModule("node:buffer");
|
|
23
|
+
import { AbstractJsonReporter } from '../../packem_shared/AbstractJsonReporter-intFdT_A.js';
|
|
24
|
+
const {
|
|
25
|
+
gzipSync
|
|
26
|
+
} = __cjs_getBuiltinModule("node:zlib");
|
|
27
|
+
|
|
28
|
+
const compressData = async (data) => {
|
|
29
|
+
const CompressionStreamClass = globalThis.CompressionStream;
|
|
30
|
+
if (CompressionStreamClass) {
|
|
31
|
+
const stream = new CompressionStreamClass("gzip");
|
|
32
|
+
const writer = stream.writable.getWriter();
|
|
33
|
+
const reader = stream.readable.getReader();
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const chunks = [];
|
|
36
|
+
writer.write(encoder.encode(data));
|
|
37
|
+
writer.close();
|
|
38
|
+
let done = false;
|
|
39
|
+
while (!done) {
|
|
40
|
+
const result2 = await reader.read();
|
|
41
|
+
done = result2.done ?? false;
|
|
42
|
+
if (result2.value) {
|
|
43
|
+
chunks.push(result2.value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const totalLength = chunks.reduce((accumulator, chunk) => accumulator + chunk.length, 0);
|
|
47
|
+
const result = new Uint8Array(totalLength);
|
|
48
|
+
let offset = 0;
|
|
49
|
+
for (const chunk of chunks) {
|
|
50
|
+
result.set(chunk, offset);
|
|
51
|
+
offset += chunk.length;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
return gzipSync(data);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class LogSizeError extends Error {
|
|
59
|
+
/**
|
|
60
|
+
* The log entry data that caused the error
|
|
61
|
+
*/
|
|
62
|
+
logData;
|
|
63
|
+
/**
|
|
64
|
+
* The actual size of the log entry in bytes
|
|
65
|
+
*/
|
|
66
|
+
actualSize;
|
|
67
|
+
/**
|
|
68
|
+
* The maximum allowed size in bytes
|
|
69
|
+
*/
|
|
70
|
+
maxSize;
|
|
71
|
+
/**
|
|
72
|
+
* Creates a new LogSizeError instance.
|
|
73
|
+
* @param message Descriptive error message explaining the size violation
|
|
74
|
+
* @param logData The log entry data that caused the error
|
|
75
|
+
* @param actualSize Size of the log entry in bytes
|
|
76
|
+
* @param maxSize Maximum allowed size in bytes
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* throw new LogSizeError("Log too large", logData, 2000000, 1000000);
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
constructor(message, logData, actualSize, maxSize) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "LogSizeError";
|
|
85
|
+
this.logData = logData;
|
|
86
|
+
this.actualSize = actualSize;
|
|
87
|
+
this.maxSize = maxSize;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const calculateBackoffDelay = (baseDelay, attempt) => baseDelay * 2 ** attempt;
|
|
92
|
+
const sleep = (delay) => new Promise((resolve) => {
|
|
93
|
+
setTimeout(resolve, delay);
|
|
94
|
+
});
|
|
95
|
+
const prepareRequestBody = (body) => {
|
|
96
|
+
if (typeof body === "string") {
|
|
97
|
+
return body;
|
|
98
|
+
}
|
|
99
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
100
|
+
};
|
|
101
|
+
const processResponse = async (response, url, method, headers, body, onDebugRequestResponse, onError) => {
|
|
102
|
+
const responseHeaders = {};
|
|
103
|
+
response.headers.forEach((value, key) => {
|
|
104
|
+
responseHeaders[key] = value;
|
|
105
|
+
});
|
|
106
|
+
const responseBody = await response.text();
|
|
107
|
+
const requestResponse = {
|
|
108
|
+
req: {
|
|
109
|
+
body,
|
|
110
|
+
headers,
|
|
111
|
+
method,
|
|
112
|
+
url
|
|
113
|
+
},
|
|
114
|
+
res: {
|
|
115
|
+
body: responseBody,
|
|
116
|
+
headers: responseHeaders,
|
|
117
|
+
status: response.status,
|
|
118
|
+
statusText: response.statusText
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
if (onDebugRequestResponse) {
|
|
122
|
+
onDebugRequestResponse(requestResponse);
|
|
123
|
+
}
|
|
124
|
+
if (response.ok) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (response.status < 500 && response.status !== 429) {
|
|
128
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
129
|
+
if (onError) {
|
|
130
|
+
onError(error);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
const handleRateLimit = (response, respectRateLimit, retryDelay, attempt, maxRetries) => {
|
|
137
|
+
if (response.status === 429 && respectRateLimit && attempt < maxRetries) {
|
|
138
|
+
const retryAfter = response.headers.get("retry-after");
|
|
139
|
+
return retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : calculateBackoffDelay(retryDelay, attempt);
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
};
|
|
143
|
+
const handleServerError = (response, retryDelay, attempt, maxRetries) => {
|
|
144
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
145
|
+
return calculateBackoffDelay(retryDelay, attempt);
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
};
|
|
149
|
+
const calculateRetryDelay = (response, respectRateLimit, retryDelay, attempt, maxRetries) => {
|
|
150
|
+
const rateLimitDelay = handleRateLimit(response, respectRateLimit, retryDelay, attempt, maxRetries);
|
|
151
|
+
if (rateLimitDelay !== void 0) {
|
|
152
|
+
return rateLimitDelay;
|
|
153
|
+
}
|
|
154
|
+
return handleServerError(response, retryDelay, attempt, maxRetries);
|
|
155
|
+
};
|
|
156
|
+
const handleRetryError = async (error, attempt, maxRetries, retryDelay, onError) => {
|
|
157
|
+
if (attempt < maxRetries) {
|
|
158
|
+
const delay = calculateBackoffDelay(retryDelay, attempt);
|
|
159
|
+
await sleep(delay);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (onError) {
|
|
163
|
+
onError(error);
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
};
|
|
167
|
+
const sendWithRetry = async (url, method, headers, body, maxRetries, retryDelay, respectRateLimit, onDebugRequestResponse, onError) => {
|
|
168
|
+
let attempt = 0;
|
|
169
|
+
while (attempt <= maxRetries) {
|
|
170
|
+
try {
|
|
171
|
+
const requestBody = prepareRequestBody(body);
|
|
172
|
+
const response = await fetch(url, {
|
|
173
|
+
body: requestBody,
|
|
174
|
+
headers,
|
|
175
|
+
method
|
|
176
|
+
});
|
|
177
|
+
const shouldRetry = await processResponse(response, url, method, headers, body, onDebugRequestResponse, onError);
|
|
178
|
+
if (!shouldRetry) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const retryDelayValue = calculateRetryDelay(response, respectRateLimit, retryDelay, attempt, maxRetries);
|
|
182
|
+
if (retryDelayValue !== void 0) {
|
|
183
|
+
await sleep(retryDelayValue);
|
|
184
|
+
attempt += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
189
|
+
if (onError) {
|
|
190
|
+
onError(error);
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const shouldRetry = await handleRetryError(error, attempt, maxRetries, retryDelay, onError);
|
|
197
|
+
if (shouldRetry) {
|
|
198
|
+
attempt += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
class AbstractHttpReporter extends AbstractJsonReporter {
|
|
207
|
+
url;
|
|
208
|
+
method;
|
|
209
|
+
headers;
|
|
210
|
+
contentType;
|
|
211
|
+
batchContentType;
|
|
212
|
+
onError;
|
|
213
|
+
onDebug;
|
|
214
|
+
onDebugRequestResponse;
|
|
215
|
+
payloadTemplate;
|
|
216
|
+
compression;
|
|
217
|
+
maxRetries;
|
|
218
|
+
retryDelay;
|
|
219
|
+
respectRateLimit;
|
|
220
|
+
enableBatchSend;
|
|
221
|
+
batchSize;
|
|
222
|
+
batchSendTimeout;
|
|
223
|
+
batchSendDelimiter;
|
|
224
|
+
batchMode;
|
|
225
|
+
batchFieldName;
|
|
226
|
+
maxLogSize;
|
|
227
|
+
maxPayloadSize;
|
|
228
|
+
edgeCompat;
|
|
229
|
+
// Batch management
|
|
230
|
+
batchQueue = [];
|
|
231
|
+
batchTimeout;
|
|
232
|
+
isProcessingBatch = false;
|
|
233
|
+
currentBatchSize = 0;
|
|
234
|
+
// Track uncompressed size of current batch
|
|
235
|
+
/**
|
|
236
|
+
* Creates a new instance of AbstractHttpReporter.
|
|
237
|
+
* @param config Configuration options for the reporter
|
|
238
|
+
*/
|
|
239
|
+
constructor(config) {
|
|
240
|
+
super(config);
|
|
241
|
+
this.url = config.url;
|
|
242
|
+
this.method = config.method ?? "POST";
|
|
243
|
+
this.headers = config.headers ?? {};
|
|
244
|
+
this.contentType = config.contentType ?? "application/json";
|
|
245
|
+
this.batchContentType = config.batchContentType ?? "application/json";
|
|
246
|
+
this.onError = config.onError;
|
|
247
|
+
this.onDebug = config.onDebug;
|
|
248
|
+
this.onDebugRequestResponse = config.onDebugRequestResponse;
|
|
249
|
+
this.payloadTemplate = config.payloadTemplate ?? ((data) => JSON.stringify(data));
|
|
250
|
+
this.compression = config.compression ?? false;
|
|
251
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
252
|
+
this.retryDelay = config.retryDelay ?? 1e3;
|
|
253
|
+
this.respectRateLimit = config.respectRateLimit ?? true;
|
|
254
|
+
this.enableBatchSend = config.enableBatchSend ?? true;
|
|
255
|
+
this.batchSize = config.batchSize ?? 100;
|
|
256
|
+
this.batchSendTimeout = config.batchSendTimeout ?? 5e3;
|
|
257
|
+
this.batchSendDelimiter = config.batchSendDelimiter ?? "\n";
|
|
258
|
+
this.batchMode = config.batchMode ?? "delimiter";
|
|
259
|
+
this.batchFieldName = config.batchFieldName;
|
|
260
|
+
if (this.batchMode === "field" && !this.batchFieldName) {
|
|
261
|
+
throw new Error("batchFieldName is required when batchMode is 'field'");
|
|
262
|
+
}
|
|
263
|
+
this.maxLogSize = config.maxLogSize ?? 1048576;
|
|
264
|
+
this.maxPayloadSize = config.maxPayloadSize ?? 5242880;
|
|
265
|
+
this.edgeCompat = config.edgeCompat ?? false;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Processes and ships log entries to the HTTP endpoint.
|
|
269
|
+
* @param message The JSON-formatted log message
|
|
270
|
+
* @param logLevel The log level of the message
|
|
271
|
+
* @protected
|
|
272
|
+
*/
|
|
273
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
274
|
+
_log(message, logLevel) {
|
|
275
|
+
try {
|
|
276
|
+
const logData = JSON.parse(message);
|
|
277
|
+
const logLevelString = logLevel;
|
|
278
|
+
const messageString = logData.message;
|
|
279
|
+
const payloadTemplate = {
|
|
280
|
+
logLevel: logLevelString,
|
|
281
|
+
message: messageString ?? ""
|
|
282
|
+
};
|
|
283
|
+
if (logData.data) {
|
|
284
|
+
payloadTemplate["data"] = logData.data;
|
|
285
|
+
} else if (logData.context) {
|
|
286
|
+
payloadTemplate["data"] = logData.context;
|
|
287
|
+
} else {
|
|
288
|
+
const rest = { ...logData };
|
|
289
|
+
delete rest.message;
|
|
290
|
+
if (Object.keys(rest).length > 0) {
|
|
291
|
+
payloadTemplate["data"] = rest;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const payload = this.payloadTemplate(payloadTemplate);
|
|
295
|
+
if (this.onDebug) {
|
|
296
|
+
this.onDebug({ data: payloadTemplate.data, logLevel: logLevelString, message: messageString });
|
|
297
|
+
}
|
|
298
|
+
const logEntrySize = this.edgeCompat || typeof TextEncoder === "undefined" ? Buffer.byteLength(payload, "utf8") : new TextEncoder().encode(payload).length;
|
|
299
|
+
if (logEntrySize > this.maxLogSize) {
|
|
300
|
+
const error = new LogSizeError(
|
|
301
|
+
`Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,
|
|
302
|
+
{ data: payloadTemplate.data, logLevel: logLevelString, message: messageString },
|
|
303
|
+
logEntrySize,
|
|
304
|
+
this.maxLogSize
|
|
305
|
+
);
|
|
306
|
+
if (this.onError) {
|
|
307
|
+
this.onError(error);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (this.enableBatchSend) {
|
|
312
|
+
this.addToBatch(payload, logEntrySize);
|
|
313
|
+
} else {
|
|
314
|
+
this.sendPayload(payload).catch((error) => {
|
|
315
|
+
if (this.onError) {
|
|
316
|
+
this.onError(error);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (this.onError) {
|
|
322
|
+
this.onError(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Adds a payload to the batch queue and triggers sending if conditions are met.
|
|
328
|
+
*/
|
|
329
|
+
addToBatch(payload, logEntrySize) {
|
|
330
|
+
const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;
|
|
331
|
+
const payloadSizeThreshold = this.maxPayloadSize * 0.9;
|
|
332
|
+
if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {
|
|
333
|
+
this.processBatch();
|
|
334
|
+
}
|
|
335
|
+
this.batchQueue.push(payload);
|
|
336
|
+
this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;
|
|
337
|
+
if (!this.batchTimeout) {
|
|
338
|
+
this.batchTimeout = setTimeout(() => {
|
|
339
|
+
this.processBatch();
|
|
340
|
+
}, this.batchSendTimeout);
|
|
341
|
+
}
|
|
342
|
+
if (this.batchQueue.length >= this.batchSize) {
|
|
343
|
+
this.processBatch();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Processes the current batch and sends it to the HTTP endpoint.
|
|
348
|
+
*/
|
|
349
|
+
async processBatch() {
|
|
350
|
+
if (this.isProcessingBatch || this.batchQueue.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.isProcessingBatch = true;
|
|
354
|
+
if (this.batchTimeout) {
|
|
355
|
+
clearTimeout(this.batchTimeout);
|
|
356
|
+
this.batchTimeout = void 0;
|
|
357
|
+
}
|
|
358
|
+
const batch = this.batchQueue.splice(0, this.batchSize);
|
|
359
|
+
this.currentBatchSize = 0;
|
|
360
|
+
for (let i = 0; i < this.batchQueue.length; i += 1) {
|
|
361
|
+
const payload = this.batchQueue[i];
|
|
362
|
+
const payloadSize = this.edgeCompat || typeof TextEncoder === "undefined" ? Buffer.byteLength(payload, "utf8") : new TextEncoder().encode(payload).length;
|
|
363
|
+
this.currentBatchSize += payloadSize + (i < this.batchQueue.length - 1 ? this.batchSendDelimiter.length : 0);
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await this.sendBatch(batch);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (this.onError) {
|
|
369
|
+
this.onError(error);
|
|
370
|
+
}
|
|
371
|
+
} finally {
|
|
372
|
+
this.isProcessingBatch = false;
|
|
373
|
+
if (this.batchQueue.length > 0) {
|
|
374
|
+
this.processBatch();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Sends a batch of payloads to the HTTP endpoint.
|
|
380
|
+
*/
|
|
381
|
+
async sendBatch(batch) {
|
|
382
|
+
let batchPayload;
|
|
383
|
+
switch (this.batchMode) {
|
|
384
|
+
case "array": {
|
|
385
|
+
const batchEntries = batch.map((payload) => JSON.parse(payload));
|
|
386
|
+
batchPayload = JSON.stringify(batchEntries);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case "field": {
|
|
390
|
+
const fieldEntries = batch.map((payload) => JSON.parse(payload));
|
|
391
|
+
const batchObject = this.batchFieldName ? { [this.batchFieldName]: fieldEntries } : {};
|
|
392
|
+
batchPayload = JSON.stringify(batchObject);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
default: {
|
|
396
|
+
batchPayload = batch.join(this.batchSendDelimiter);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
await this.sendPayload(batchPayload, this.batchContentType);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Sends a single payload to the HTTP endpoint.
|
|
404
|
+
*/
|
|
405
|
+
async sendPayload(payload, contentType) {
|
|
406
|
+
const headers = typeof this.headers === "function" ? this.headers() : { ...this.headers };
|
|
407
|
+
if (!headers["content-type"]) {
|
|
408
|
+
headers["content-type"] = contentType ?? this.contentType;
|
|
409
|
+
}
|
|
410
|
+
let finalPayload = payload;
|
|
411
|
+
if (this.compression && !this.edgeCompat) {
|
|
412
|
+
try {
|
|
413
|
+
finalPayload = await compressData(payload);
|
|
414
|
+
headers["content-encoding"] = "gzip";
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (this.onError) {
|
|
417
|
+
this.onError(new Error(`Compression failed: ${error}`));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
await sendWithRetry(
|
|
422
|
+
this.url,
|
|
423
|
+
this.method,
|
|
424
|
+
headers,
|
|
425
|
+
finalPayload,
|
|
426
|
+
this.maxRetries,
|
|
427
|
+
this.retryDelay,
|
|
428
|
+
this.respectRateLimit,
|
|
429
|
+
this.onDebugRequestResponse,
|
|
430
|
+
this.onError
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { AbstractHttpReporter };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AbstractHttpReporterOptions } from "./abstract-http-reporter.d.ts";
|
|
2
|
+
import { AbstractHttpReporter } from "./abstract-http-reporter.d.ts";
|
|
3
|
+
/**
|
|
4
|
+
* HTTP Reporter.
|
|
5
|
+
*
|
|
6
|
+
* A reporter that sends logs to HTTP endpoints.
|
|
7
|
+
* Supports batching, compression, retries, and rate limiting.
|
|
8
|
+
* Works in both Node.js server and browser environments.
|
|
9
|
+
* @template L - The log level type
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createPail } from "@visulima/pail";
|
|
13
|
+
* import { HttpReporter } from "@visulima/pail/reporter/http";
|
|
14
|
+
*
|
|
15
|
+
* const logger = createPail({
|
|
16
|
+
* reporters: [
|
|
17
|
+
* new HttpReporter({
|
|
18
|
+
* url: "https://api.example.com/logs",
|
|
19
|
+
* method: "POST",
|
|
20
|
+
* headers: {
|
|
21
|
+
* "Authorization": "Bearer token"
|
|
22
|
+
* },
|
|
23
|
+
* enableBatchSend: true,
|
|
24
|
+
* batchSize: 100
|
|
25
|
+
* })
|
|
26
|
+
* ]
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* logger.info("Application started", { version: "1.0.0" });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare class HttpReporter<L extends string = string> extends AbstractHttpReporter<L> {
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new HTTP Reporter instance.
|
|
35
|
+
* @param options Configuration options for HTTP reporting
|
|
36
|
+
*/
|
|
37
|
+
constructor(options: AbstractHttpReporterOptions);
|
|
38
|
+
}
|
|
39
|
+
export default HttpReporter;
|