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