@loglayer/transport-http 1.1.1 → 1.1.2

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/dist/index.js CHANGED
@@ -1,348 +1,316 @@
1
- // src/errors.ts
1
+ import { LoggerlessTransport } from "@loglayer/transport";
2
+
3
+ //#region src/errors.ts
4
+ /**
5
+ * Error thrown when HTTP request fails
6
+ */
2
7
  var HttpTransportError = class extends Error {
3
- constructor(message, status, response) {
4
- super(message);
5
- this.status = status;
6
- this.response = response;
7
- this.name = "HttpTransportError";
8
- }
8
+ constructor(message, status, response) {
9
+ super(message);
10
+ this.status = status;
11
+ this.response = response;
12
+ this.name = "HttpTransportError";
13
+ }
9
14
  };
15
+ /**
16
+ * Error thrown when rate limit is exceeded
17
+ */
10
18
  var RateLimitError = class extends Error {
11
- constructor(message, retryAfter) {
12
- super(message);
13
- this.retryAfter = retryAfter;
14
- this.name = "RateLimitError";
15
- }
19
+ constructor(message, retryAfter) {
20
+ super(message);
21
+ this.retryAfter = retryAfter;
22
+ this.name = "RateLimitError";
23
+ }
16
24
  };
25
+ /**
26
+ * Error thrown when log entry exceeds size limits
27
+ */
17
28
  var LogSizeError = class extends Error {
18
- constructor(message, logEntry, size, limit) {
19
- super(message);
20
- this.logEntry = logEntry;
21
- this.size = size;
22
- this.limit = limit;
23
- this.name = "LogSizeError";
24
- }
29
+ constructor(message, logEntry, size, limit) {
30
+ super(message);
31
+ this.logEntry = logEntry;
32
+ this.size = size;
33
+ this.limit = limit;
34
+ this.name = "LogSizeError";
35
+ }
25
36
  };
26
37
 
27
- // src/HttpTransport.ts
28
- import { LoggerlessTransport } from "@loglayer/transport";
29
-
30
- // src/utils.ts
38
+ //#endregion
39
+ //#region src/utils.ts
40
+ /**
41
+ * Compresses data using gzip compression
42
+ */
31
43
  async function compressData(data) {
32
- const encoder = new TextEncoder();
33
- const dataBytes = encoder.encode(data);
34
- if (typeof CompressionStream !== "undefined") {
35
- const stream = new CompressionStream("gzip");
36
- const writer = stream.writable.getWriter();
37
- const reader = stream.readable.getReader();
38
- await writer.write(dataBytes);
39
- await writer.close();
40
- const chunks = [];
41
- while (true) {
42
- const { done, value } = await reader.read();
43
- if (done) break;
44
- chunks.push(value);
45
- }
46
- const totalLength = chunks.reduce((acc, chunk) => acc + 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
- throw new Error("Gzip compression not supported in this environment");
44
+ const dataBytes = new TextEncoder().encode(data);
45
+ if (typeof CompressionStream !== "undefined") {
46
+ const stream = new CompressionStream("gzip");
47
+ const writer = stream.writable.getWriter();
48
+ const reader = stream.readable.getReader();
49
+ await writer.write(dataBytes);
50
+ await writer.close();
51
+ const chunks = [];
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+ chunks.push(value);
56
+ }
57
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
58
+ const result = new Uint8Array(totalLength);
59
+ let offset = 0;
60
+ for (const chunk of chunks) {
61
+ result.set(chunk, offset);
62
+ offset += chunk.length;
63
+ }
64
+ return result;
65
+ }
66
+ throw new Error("Gzip compression not supported in this environment");
56
67
  }
68
+ /**
69
+ * Sends HTTP request with retry logic and rate limiting
70
+ */
57
71
  async function sendWithRetry(url, method, headers, payload, maxRetries, retryDelay, respectRateLimit = true, onDebugReqRes, onError) {
58
- let lastError;
59
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
60
- try {
61
- const response = await fetch(url, {
62
- method,
63
- headers,
64
- body: payload
65
- });
66
- if (onDebugReqRes) {
67
- try {
68
- const responseBody = await response.clone().text();
69
- const responseHeaders = {};
70
- response.headers.forEach((value, key) => {
71
- responseHeaders[key] = value;
72
- });
73
- onDebugReqRes({
74
- req: {
75
- url,
76
- method,
77
- headers,
78
- body: payload
79
- },
80
- res: {
81
- status: response.status,
82
- statusText: response.statusText,
83
- headers: responseHeaders,
84
- body: responseBody
85
- }
86
- });
87
- } catch (debugError) {
88
- if (onError) {
89
- onError(new Error(`Debug callback error: ${debugError}`));
90
- }
91
- }
92
- }
93
- if (response.status === 429 && respectRateLimit) {
94
- const retryAfter = response.headers.get("retry-after");
95
- const waitTime = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : retryDelay;
96
- throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);
97
- }
98
- if (response.status < 200 || response.status >= 300) {
99
- if (onError) {
100
- onError(new Error(`HTTP request failed with status ${response.status}: ${response.statusText}`));
101
- }
102
- }
103
- if (!response.ok) {
104
- throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);
105
- }
106
- return response;
107
- } catch (error) {
108
- lastError = error;
109
- if (error instanceof RateLimitError && respectRateLimit) {
110
- await new Promise((resolve) => setTimeout(resolve, error.retryAfter));
111
- continue;
112
- }
113
- if (attempt === maxRetries) {
114
- throw error;
115
- }
116
- const waitTime = retryDelay * 2 ** attempt;
117
- await new Promise((resolve) => setTimeout(resolve, waitTime));
118
- }
119
- }
120
- throw lastError;
72
+ let lastError;
73
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
74
+ const response = await fetch(url, {
75
+ method,
76
+ headers,
77
+ body: payload
78
+ });
79
+ if (onDebugReqRes) try {
80
+ const responseBody = await response.clone().text();
81
+ const responseHeaders = {};
82
+ response.headers.forEach((value, key) => {
83
+ responseHeaders[key] = value;
84
+ });
85
+ onDebugReqRes({
86
+ req: {
87
+ url,
88
+ method,
89
+ headers,
90
+ body: payload
91
+ },
92
+ res: {
93
+ status: response.status,
94
+ statusText: response.statusText,
95
+ headers: responseHeaders,
96
+ body: responseBody
97
+ }
98
+ });
99
+ } catch (debugError) {
100
+ if (onError) onError(/* @__PURE__ */ new Error(`Debug callback error: ${debugError}`));
101
+ }
102
+ if (response.status === 429 && respectRateLimit) {
103
+ const retryAfter = response.headers.get("retry-after");
104
+ const waitTime = retryAfter ? Number.parseInt(retryAfter, 10) * 1e3 : retryDelay;
105
+ throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);
106
+ }
107
+ if (response.status < 200 || response.status >= 300) {
108
+ if (onError) onError(/* @__PURE__ */ new Error(`HTTP request failed with status ${response.status}: ${response.statusText}`));
109
+ }
110
+ if (!response.ok) throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);
111
+ return response;
112
+ } catch (error) {
113
+ lastError = error;
114
+ if (error instanceof RateLimitError && respectRateLimit) {
115
+ await new Promise((resolve) => setTimeout(resolve, error.retryAfter));
116
+ continue;
117
+ }
118
+ if (attempt === maxRetries) throw error;
119
+ const waitTime = retryDelay * 2 ** attempt;
120
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
121
+ }
122
+ throw lastError;
121
123
  }
122
124
 
123
- // src/HttpTransport.ts
125
+ //#endregion
126
+ //#region src/HttpTransport.ts
127
+ /**
128
+ * HttpTransport is responsible for sending logs to any HTTP endpoint.
129
+ * It supports batching, compression, retries, and rate limiting.
130
+ *
131
+ * Features:
132
+ * - Configurable HTTP method and headers
133
+ * - Custom payload template function
134
+ * - Gzip compression support
135
+ * - Retry logic with exponential backoff
136
+ * - Rate limiting support
137
+ * - Batch sending with configurable size and timeout
138
+ * - Error and debug callbacks
139
+ * - Log size validation
140
+ * - Payload size tracking for batching
141
+ */
124
142
  var HttpTransport = class extends LoggerlessTransport {
125
- url;
126
- method;
127
- headers;
128
- contentType;
129
- batchContentType;
130
- onError;
131
- onDebug;
132
- onDebugReqRes;
133
- payloadTemplate;
134
- compression;
135
- maxRetries;
136
- retryDelay;
137
- respectRateLimit;
138
- enableBatchSend;
139
- batchSize;
140
- batchSendTimeout;
141
- batchSendDelimiter;
142
- batchMode;
143
- batchFieldName;
144
- maxLogSize;
145
- maxPayloadSize;
146
- enableNextJsEdgeCompat;
147
- // Batch management
148
- batchQueue = [];
149
- batchTimeout;
150
- isProcessingBatch = false;
151
- currentBatchSize = 0;
152
- // Track uncompressed size of current batch
153
- /**
154
- * Creates a new instance of HttpTransport.
155
- *
156
- * @param config - Configuration options for the transport
157
- */
158
- constructor(config) {
159
- super(config);
160
- this.url = config.url;
161
- this.method = config.method ?? "POST";
162
- this.headers = config.headers ?? {};
163
- this.contentType = config.contentType ?? "application/json";
164
- this.batchContentType = config.batchContentType ?? "application/json";
165
- this.onError = config.onError;
166
- this.onDebug = config.onDebug;
167
- this.onDebugReqRes = config.onDebugReqRes;
168
- this.payloadTemplate = config.payloadTemplate;
169
- this.compression = config.compression ?? false;
170
- this.maxRetries = config.maxRetries ?? 3;
171
- this.retryDelay = config.retryDelay ?? 1e3;
172
- this.respectRateLimit = config.respectRateLimit ?? true;
173
- this.enableBatchSend = config.enableBatchSend ?? true;
174
- this.batchSize = config.batchSize ?? 100;
175
- this.batchSendTimeout = config.batchSendTimeout ?? 5e3;
176
- this.batchSendDelimiter = config.batchSendDelimiter ?? "\n";
177
- this.batchMode = config.batchMode ?? "delimiter";
178
- this.batchFieldName = config.batchFieldName;
179
- if (this.batchMode === "field" && !this.batchFieldName) {
180
- throw new Error("batchFieldName is required when batchMode is 'field'");
181
- }
182
- this.maxLogSize = config.maxLogSize ?? 1048576;
183
- this.maxPayloadSize = config.maxPayloadSize ?? 5242880;
184
- this.enableNextJsEdgeCompat = config.enableNextJsEdgeCompat ?? false;
185
- }
186
- /**
187
- * Processes and ships log entries to the HTTP endpoint.
188
- *
189
- * @param params - Log parameters including level, messages, and metadata
190
- * @returns The original messages array
191
- */
192
- shipToLogger({ logLevel, messages, data, hasData }) {
193
- try {
194
- const message = messages.join(" ");
195
- const payloadTemplate = {
196
- logLevel,
197
- message
198
- };
199
- if (hasData) {
200
- payloadTemplate["data"] = data;
201
- }
202
- const payload = this.payloadTemplate(payloadTemplate);
203
- if (this.onDebug) {
204
- this.onDebug({ logLevel, message, data });
205
- }
206
- let logEntrySize;
207
- if (this.enableNextJsEdgeCompat || typeof TextEncoder === "undefined") {
208
- logEntrySize = Buffer.byteLength(payload, "utf8");
209
- } else {
210
- logEntrySize = new TextEncoder().encode(payload).length;
211
- }
212
- if (logEntrySize > this.maxLogSize) {
213
- const error = new LogSizeError(
214
- `Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,
215
- { logLevel, message, data },
216
- logEntrySize,
217
- this.maxLogSize
218
- );
219
- if (this.onError) {
220
- this.onError(error);
221
- }
222
- return messages;
223
- }
224
- if (this.enableBatchSend) {
225
- this.addToBatch(payload, logEntrySize);
226
- } else {
227
- this.sendPayload(payload).catch((err) => {
228
- if (this.onError) {
229
- this.onError(err);
230
- }
231
- });
232
- }
233
- } catch (error) {
234
- if (this.onError) {
235
- this.onError(error);
236
- }
237
- }
238
- return messages;
239
- }
240
- /**
241
- * Adds a payload to the batch queue and triggers sending if conditions are met
242
- */
243
- addToBatch(payload, logEntrySize) {
244
- const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;
245
- const payloadSizeThreshold = this.maxPayloadSize * 0.9;
246
- if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {
247
- this.processBatch();
248
- }
249
- this.batchQueue.push(payload);
250
- this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;
251
- if (!this.batchTimeout) {
252
- this.batchTimeout = setTimeout(() => {
253
- this.processBatch();
254
- }, this.batchSendTimeout);
255
- }
256
- if (this.batchQueue.length >= this.batchSize) {
257
- this.processBatch();
258
- }
259
- }
260
- /**
261
- * Processes the current batch and sends it to the HTTP endpoint
262
- */
263
- async processBatch() {
264
- if (this.isProcessingBatch || this.batchQueue.length === 0) {
265
- return;
266
- }
267
- this.isProcessingBatch = true;
268
- if (this.batchTimeout) {
269
- clearTimeout(this.batchTimeout);
270
- this.batchTimeout = void 0;
271
- }
272
- const batch = this.batchQueue.splice(0, this.batchSize);
273
- this.currentBatchSize = 0;
274
- try {
275
- await this.sendBatch(batch);
276
- } catch (error) {
277
- if (this.onError) {
278
- this.onError(error);
279
- }
280
- } finally {
281
- this.isProcessingBatch = false;
282
- if (this.batchQueue.length > 0) {
283
- this.processBatch();
284
- }
285
- }
286
- }
287
- /**
288
- * Sends a batch of payloads to the HTTP endpoint
289
- */
290
- async sendBatch(batch) {
291
- let batchPayload;
292
- switch (this.batchMode) {
293
- case "array": {
294
- const batchEntries = batch.map((payload) => JSON.parse(payload));
295
- batchPayload = JSON.stringify(batchEntries);
296
- break;
297
- }
298
- case "field": {
299
- const fieldEntries = batch.map((payload) => JSON.parse(payload));
300
- const batchObject = { [this.batchFieldName]: fieldEntries };
301
- batchPayload = JSON.stringify(batchObject);
302
- break;
303
- }
304
- default:
305
- batchPayload = batch.join(this.batchSendDelimiter);
306
- break;
307
- }
308
- await this.sendPayload(batchPayload, this.batchContentType);
309
- }
310
- /**
311
- * Sends a single payload to the HTTP endpoint
312
- */
313
- async sendPayload(payload, contentType) {
314
- const headers = typeof this.headers === "function" ? this.headers() : { ...this.headers };
315
- if (!headers["content-type"]) {
316
- headers["content-type"] = contentType ?? this.contentType;
317
- }
318
- let finalPayload = payload;
319
- if (this.compression && !this.enableNextJsEdgeCompat) {
320
- try {
321
- finalPayload = await compressData(payload);
322
- headers["content-encoding"] = "gzip";
323
- } catch (error) {
324
- if (this.onError) {
325
- this.onError(new Error(`Compression failed: ${error}`));
326
- }
327
- }
328
- }
329
- await sendWithRetry(
330
- this.url,
331
- this.method,
332
- headers,
333
- finalPayload,
334
- this.maxRetries,
335
- this.retryDelay,
336
- this.respectRateLimit,
337
- this.onDebugReqRes,
338
- this.onError
339
- );
340
- }
341
- };
342
- export {
343
- HttpTransport,
344
- HttpTransportError,
345
- LogSizeError,
346
- RateLimitError
143
+ url;
144
+ method;
145
+ headers;
146
+ contentType;
147
+ batchContentType;
148
+ onError;
149
+ onDebug;
150
+ onDebugReqRes;
151
+ payloadTemplate;
152
+ compression;
153
+ maxRetries;
154
+ retryDelay;
155
+ respectRateLimit;
156
+ enableBatchSend;
157
+ batchSize;
158
+ batchSendTimeout;
159
+ batchSendDelimiter;
160
+ batchMode;
161
+ batchFieldName;
162
+ maxLogSize;
163
+ maxPayloadSize;
164
+ enableNextJsEdgeCompat;
165
+ batchQueue = [];
166
+ batchTimeout;
167
+ isProcessingBatch = false;
168
+ currentBatchSize = 0;
169
+ /**
170
+ * Creates a new instance of HttpTransport.
171
+ *
172
+ * @param config - Configuration options for the transport
173
+ */
174
+ constructor(config) {
175
+ super(config);
176
+ this.url = config.url;
177
+ this.method = config.method ?? "POST";
178
+ this.headers = config.headers ?? {};
179
+ this.contentType = config.contentType ?? "application/json";
180
+ this.batchContentType = config.batchContentType ?? "application/json";
181
+ this.onError = config.onError;
182
+ this.onDebug = config.onDebug;
183
+ this.onDebugReqRes = config.onDebugReqRes;
184
+ this.payloadTemplate = config.payloadTemplate;
185
+ this.compression = config.compression ?? false;
186
+ this.maxRetries = config.maxRetries ?? 3;
187
+ this.retryDelay = config.retryDelay ?? 1e3;
188
+ this.respectRateLimit = config.respectRateLimit ?? true;
189
+ this.enableBatchSend = config.enableBatchSend ?? true;
190
+ this.batchSize = config.batchSize ?? 100;
191
+ this.batchSendTimeout = config.batchSendTimeout ?? 5e3;
192
+ this.batchSendDelimiter = config.batchSendDelimiter ?? "\n";
193
+ this.batchMode = config.batchMode ?? "delimiter";
194
+ this.batchFieldName = config.batchFieldName;
195
+ if (this.batchMode === "field" && !this.batchFieldName) throw new Error("batchFieldName is required when batchMode is 'field'");
196
+ this.maxLogSize = config.maxLogSize ?? 1048576;
197
+ this.maxPayloadSize = config.maxPayloadSize ?? 5242880;
198
+ this.enableNextJsEdgeCompat = config.enableNextJsEdgeCompat ?? false;
199
+ }
200
+ /**
201
+ * Processes and ships log entries to the HTTP endpoint.
202
+ *
203
+ * @param params - Log parameters including level, messages, and metadata
204
+ * @returns The original messages array
205
+ */
206
+ shipToLogger({ logLevel, messages, data, hasData }) {
207
+ try {
208
+ const message = messages.join(" ");
209
+ const payloadTemplate = {
210
+ logLevel,
211
+ message
212
+ };
213
+ if (hasData) payloadTemplate["data"] = data;
214
+ const payload = this.payloadTemplate(payloadTemplate);
215
+ if (this.onDebug) this.onDebug({
216
+ logLevel,
217
+ message,
218
+ data
219
+ });
220
+ let logEntrySize;
221
+ if (this.enableNextJsEdgeCompat || typeof TextEncoder === "undefined") logEntrySize = Buffer.byteLength(payload, "utf8");
222
+ else logEntrySize = new TextEncoder().encode(payload).length;
223
+ if (logEntrySize > this.maxLogSize) {
224
+ const error = new LogSizeError(`Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`, {
225
+ logLevel,
226
+ message,
227
+ data
228
+ }, logEntrySize, this.maxLogSize);
229
+ if (this.onError) this.onError(error);
230
+ return messages;
231
+ }
232
+ if (this.enableBatchSend) this.addToBatch(payload, logEntrySize);
233
+ else this.sendPayload(payload).catch((err) => {
234
+ if (this.onError) this.onError(err);
235
+ });
236
+ } catch (error) {
237
+ if (this.onError) this.onError(error);
238
+ }
239
+ return messages;
240
+ }
241
+ /**
242
+ * Adds a payload to the batch queue and triggers sending if conditions are met
243
+ */
244
+ addToBatch(payload, logEntrySize) {
245
+ if (this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length > this.maxPayloadSize * .9 && this.batchQueue.length > 0) this.processBatch();
246
+ this.batchQueue.push(payload);
247
+ this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;
248
+ if (!this.batchTimeout) this.batchTimeout = setTimeout(() => {
249
+ this.processBatch();
250
+ }, this.batchSendTimeout);
251
+ if (this.batchQueue.length >= this.batchSize) this.processBatch();
252
+ }
253
+ /**
254
+ * Processes the current batch and sends it to the HTTP endpoint
255
+ */
256
+ async processBatch() {
257
+ if (this.isProcessingBatch || this.batchQueue.length === 0) return;
258
+ this.isProcessingBatch = true;
259
+ if (this.batchTimeout) {
260
+ clearTimeout(this.batchTimeout);
261
+ this.batchTimeout = void 0;
262
+ }
263
+ const batch = this.batchQueue.splice(0, this.batchSize);
264
+ this.currentBatchSize = 0;
265
+ try {
266
+ await this.sendBatch(batch);
267
+ } catch (error) {
268
+ if (this.onError) this.onError(error);
269
+ } finally {
270
+ this.isProcessingBatch = false;
271
+ if (this.batchQueue.length > 0) this.processBatch();
272
+ }
273
+ }
274
+ /**
275
+ * Sends a batch of payloads to the HTTP endpoint
276
+ */
277
+ async sendBatch(batch) {
278
+ let batchPayload;
279
+ switch (this.batchMode) {
280
+ case "array": {
281
+ const batchEntries = batch.map((payload) => JSON.parse(payload));
282
+ batchPayload = JSON.stringify(batchEntries);
283
+ break;
284
+ }
285
+ case "field": {
286
+ const fieldEntries = batch.map((payload) => JSON.parse(payload));
287
+ const batchObject = { [this.batchFieldName]: fieldEntries };
288
+ batchPayload = JSON.stringify(batchObject);
289
+ break;
290
+ }
291
+ default:
292
+ batchPayload = batch.join(this.batchSendDelimiter);
293
+ break;
294
+ }
295
+ await this.sendPayload(batchPayload, this.batchContentType);
296
+ }
297
+ /**
298
+ * Sends a single payload to the HTTP endpoint
299
+ */
300
+ async sendPayload(payload, contentType) {
301
+ const headers = typeof this.headers === "function" ? this.headers() : { ...this.headers };
302
+ if (!headers["content-type"]) headers["content-type"] = contentType ?? this.contentType;
303
+ let finalPayload = payload;
304
+ if (this.compression && !this.enableNextJsEdgeCompat) try {
305
+ finalPayload = await compressData(payload);
306
+ headers["content-encoding"] = "gzip";
307
+ } catch (error) {
308
+ if (this.onError) this.onError(/* @__PURE__ */ new Error(`Compression failed: ${error}`));
309
+ }
310
+ await sendWithRetry(this.url, this.method, headers, finalPayload, this.maxRetries, this.retryDelay, this.respectRateLimit, this.onDebugReqRes, this.onError);
311
+ }
347
312
  };
313
+
314
+ //#endregion
315
+ export { HttpTransport, HttpTransportError, LogSizeError, RateLimitError };
348
316
  //# sourceMappingURL=index.js.map