@loglayer/transport-http 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Theo Gravity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # HTTP Transport for LogLayer
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/%40loglayer%2Ftransport-http)](https://www.npmjs.com/package/@loglayer/transport-http)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/%40loglayer%2Ftransport-http)](https://www.npmjs.com/package/@loglayer/transport-http)
5
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
6
+
7
+ An HTTP transport for the [LogLayer](https://loglayer.dev) logging library.
8
+
9
+ Ships logs to any HTTP endpoint with support for batching, compression, retries, and rate limiting. Features include:
10
+ - Configurable HTTP method and headers
11
+ - Custom payload template function
12
+ - Gzip compression support
13
+ - Retry logic with exponential backoff
14
+ - Rate limiting support
15
+ - Batch sending with configurable size and timeout
16
+ - Error and debug callbacks
17
+ - Log size validation and payload size tracking
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install loglayer @loglayer/transport-http serialize-error
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```typescript
28
+ import { LogLayer } from 'loglayer'
29
+ import { HttpTransport } from "@loglayer/transport-http"
30
+ import { serializeError } from "serialize-error";
31
+
32
+ const log = new LogLayer({
33
+ errorSerializer: serializeError,
34
+ transport: new HttpTransport({
35
+ url: "https://api.example.com/logs",
36
+ method: "POST", // optional, defaults to POST
37
+ headers: {
38
+ "Authorization": "Bearer YOUR_API_KEY",
39
+ "Content-Type": "application/json"
40
+ },
41
+ // Or use a function for dynamic headers
42
+ // headers: () => ({
43
+ // "Authorization": `Bearer ${getApiKey()}`,
44
+ // "Content-Type": "application/json"
45
+ // }),
46
+ contentType: "application/json", // optional, defaults to application/json
47
+ batchContentType: "application/x-ndjson", // optional, defaults to application/json
48
+ payloadTemplate: ({ logLevel, message, data }) =>
49
+ JSON.stringify({
50
+ timestamp: new Date().toISOString(),
51
+ level: logLevel,
52
+ message,
53
+ metadata: data,
54
+ }),
55
+ compression: true, // optional, defaults to false
56
+ maxRetries: 3, // optional, defaults to 3
57
+ retryDelay: 1000, // optional, defaults to 1000
58
+ respectRateLimit: true, // optional, defaults to true
59
+ enableBatchSend: true, // optional, defaults to true
60
+ batchSize: 100, // optional, defaults to 100
61
+ batchSendTimeout: 5000, // optional, defaults to 5000ms
62
+ batchSendDelimiter: "\n", // optional, defaults to "\n"
63
+ maxLogSize: 1048576, // optional, defaults to 1MB
64
+ maxPayloadSize: 5242880, // optional, defaults to 5MB
65
+ enableNextJsEdgeCompat: false, // optional, defaults to false
66
+ onError: (err) => {
67
+ console.error('Failed to send logs:', err);
68
+ },
69
+ onDebug: (entry) => {
70
+ console.log('Log entry being sent:', entry);
71
+ },
72
+ })
73
+ })
74
+
75
+ // Use the logger
76
+ log.info("This is a test message");
77
+ log.withMetadata({ userId: "123" }).error("User not found");
78
+ ```
79
+
80
+ ## Vibe code notice
81
+
82
+ 99% of this code was vibe-coded using Cursor and in the agent `auto` mode with some supervision.
83
+
84
+ See the [Prompts](PROMPTS.md) file for the prompts used.
85
+
86
+ ## Documentation
87
+
88
+ For more details, visit [https://loglayer.dev/transports/http](https://loglayer.dev/transports/http)
package/dist/index.cjs ADDED
@@ -0,0 +1,277 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } var _class;// src/HttpTransport.ts
2
+ var _transport = require('@loglayer/transport');
3
+ 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
+ }
10
+ };
11
+ var RateLimitError = class extends Error {
12
+ constructor(message, retryAfter) {
13
+ super(message);
14
+ this.retryAfter = retryAfter;
15
+ this.name = "RateLimitError";
16
+ }
17
+ };
18
+ 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
+ }
26
+ };
27
+ 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");
52
+ }
53
+ async function sendWithRetry(url, method, headers, payload, maxRetries, retryDelay, respectRateLimit = true) {
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 (response.status === 429 && respectRateLimit) {
63
+ const retryAfter = response.headers.get("retry-after");
64
+ const waitTime = retryAfter ? Number.parseInt(retryAfter) * 1e3 : retryDelay;
65
+ throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);
66
+ }
67
+ if (!response.ok) {
68
+ throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);
69
+ }
70
+ return response;
71
+ } catch (error) {
72
+ lastError = error;
73
+ if (error instanceof RateLimitError && respectRateLimit) {
74
+ await new Promise((resolve) => setTimeout(resolve, error.retryAfter));
75
+ continue;
76
+ }
77
+ if (attempt === maxRetries) {
78
+ throw error;
79
+ }
80
+ const waitTime = retryDelay * 2 ** attempt;
81
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
82
+ }
83
+ }
84
+ throw lastError;
85
+ }
86
+ var HttpTransport = (_class = class extends _transport.LoggerlessTransport {
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+ // Batch management
107
+ __init() {this.batchQueue = []}
108
+
109
+ __init2() {this.isProcessingBatch = false}
110
+ __init3() {this.currentBatchSize = 0}
111
+ // Track uncompressed size of current batch
112
+ /**
113
+ * Creates a new instance of HttpTransport.
114
+ *
115
+ * @param config - Configuration options for the transport
116
+ */
117
+ constructor(config) {
118
+ super(config);_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);;
119
+ this.url = config.url;
120
+ this.method = _nullishCoalesce(config.method, () => ( "POST"));
121
+ this.headers = _nullishCoalesce(config.headers, () => ( {}));
122
+ this.contentType = _nullishCoalesce(config.contentType, () => ( "application/json"));
123
+ this.batchContentType = _nullishCoalesce(config.batchContentType, () => ( "application/json"));
124
+ this.onError = config.onError;
125
+ this.onDebug = config.onDebug;
126
+ this.payloadTemplate = config.payloadTemplate;
127
+ this.compression = _nullishCoalesce(config.compression, () => ( false));
128
+ this.maxRetries = _nullishCoalesce(config.maxRetries, () => ( 3));
129
+ this.retryDelay = _nullishCoalesce(config.retryDelay, () => ( 1e3));
130
+ this.respectRateLimit = _nullishCoalesce(config.respectRateLimit, () => ( true));
131
+ this.enableBatchSend = _nullishCoalesce(config.enableBatchSend, () => ( true));
132
+ this.batchSize = _nullishCoalesce(config.batchSize, () => ( 100));
133
+ this.batchSendTimeout = _nullishCoalesce(config.batchSendTimeout, () => ( 5e3));
134
+ this.batchSendDelimiter = _nullishCoalesce(config.batchSendDelimiter, () => ( "\n"));
135
+ this.maxLogSize = _nullishCoalesce(config.maxLogSize, () => ( 1048576));
136
+ this.maxPayloadSize = _nullishCoalesce(config.maxPayloadSize, () => ( 5242880));
137
+ this.enableNextJsEdgeCompat = _nullishCoalesce(config.enableNextJsEdgeCompat, () => ( false));
138
+ }
139
+ /**
140
+ * Processes and ships log entries to the HTTP endpoint.
141
+ *
142
+ * @param params - Log parameters including level, messages, and metadata
143
+ * @returns The original messages array
144
+ */
145
+ shipToLogger({ logLevel, messages, data, hasData }) {
146
+ try {
147
+ const message = messages.join(" ");
148
+ const payload = this.payloadTemplate({
149
+ logLevel,
150
+ message,
151
+ data
152
+ });
153
+ if (this.onDebug) {
154
+ this.onDebug({ logLevel, message, data });
155
+ }
156
+ let logEntrySize;
157
+ if (this.enableNextJsEdgeCompat || typeof TextEncoder === "undefined") {
158
+ logEntrySize = Buffer.byteLength(payload, "utf8");
159
+ } else {
160
+ logEntrySize = new TextEncoder().encode(payload).length;
161
+ }
162
+ if (logEntrySize > this.maxLogSize) {
163
+ const error = new LogSizeError(
164
+ `Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,
165
+ { logLevel, message, data },
166
+ logEntrySize,
167
+ this.maxLogSize
168
+ );
169
+ if (this.onError) {
170
+ this.onError(error);
171
+ }
172
+ return messages;
173
+ }
174
+ if (this.enableBatchSend) {
175
+ this.addToBatch(payload, logEntrySize);
176
+ } else {
177
+ this.sendPayload(payload).catch((err) => {
178
+ if (this.onError) {
179
+ this.onError(err);
180
+ }
181
+ });
182
+ }
183
+ } catch (error) {
184
+ if (this.onError) {
185
+ this.onError(error);
186
+ }
187
+ }
188
+ return messages;
189
+ }
190
+ /**
191
+ * Adds a payload to the batch queue and triggers sending if conditions are met
192
+ */
193
+ addToBatch(payload, logEntrySize) {
194
+ const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;
195
+ const payloadSizeThreshold = this.maxPayloadSize * 0.9;
196
+ if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {
197
+ this.processBatch();
198
+ }
199
+ this.batchQueue.push(payload);
200
+ this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;
201
+ if (!this.batchTimeout) {
202
+ this.batchTimeout = setTimeout(() => {
203
+ this.processBatch();
204
+ }, this.batchSendTimeout);
205
+ }
206
+ if (this.batchQueue.length >= this.batchSize) {
207
+ this.processBatch();
208
+ }
209
+ }
210
+ /**
211
+ * Processes the current batch and sends it to the HTTP endpoint
212
+ */
213
+ async processBatch() {
214
+ if (this.isProcessingBatch || this.batchQueue.length === 0) {
215
+ return;
216
+ }
217
+ this.isProcessingBatch = true;
218
+ if (this.batchTimeout) {
219
+ clearTimeout(this.batchTimeout);
220
+ this.batchTimeout = void 0;
221
+ }
222
+ const batch = this.batchQueue.splice(0, this.batchSize);
223
+ this.currentBatchSize = 0;
224
+ try {
225
+ await this.sendBatch(batch);
226
+ } catch (error) {
227
+ if (this.onError) {
228
+ this.onError(error);
229
+ }
230
+ } finally {
231
+ this.isProcessingBatch = false;
232
+ if (this.batchQueue.length > 0) {
233
+ this.processBatch();
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Sends a batch of payloads to the HTTP endpoint
239
+ */
240
+ async sendBatch(batch) {
241
+ const batchPayload = batch.join(this.batchSendDelimiter);
242
+ await this.sendPayload(batchPayload, this.batchContentType);
243
+ }
244
+ /**
245
+ * Sends a single payload to the HTTP endpoint
246
+ */
247
+ async sendPayload(payload, contentType) {
248
+ const headers = typeof this.headers === "function" ? this.headers() : { ...this.headers };
249
+ if (!headers["content-type"]) {
250
+ headers["content-type"] = _nullishCoalesce(contentType, () => ( this.contentType));
251
+ }
252
+ let finalPayload = payload;
253
+ if (this.compression && !this.enableNextJsEdgeCompat) {
254
+ try {
255
+ finalPayload = await compressData(payload);
256
+ headers["content-encoding"] = "gzip";
257
+ } catch (error) {
258
+ if (this.onError) {
259
+ this.onError(new Error(`Compression failed: ${error}`));
260
+ }
261
+ }
262
+ }
263
+ await sendWithRetry(
264
+ this.url,
265
+ this.method,
266
+ headers,
267
+ finalPayload,
268
+ this.maxRetries,
269
+ this.retryDelay,
270
+ this.respectRateLimit
271
+ );
272
+ }
273
+ }, _class);
274
+
275
+
276
+ exports.HttpTransport = HttpTransport;
277
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/loglayer/loglayer/packages/transports/http/dist/index.cjs","../src/HttpTransport.ts"],"names":[],"mappings":"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","file":"/home/runner/work/loglayer/loglayer/packages/transports/http/dist/index.cjs","sourcesContent":[null,"import type { LoggerlessTransportConfig, LogLayerTransportParams } from \"@loglayer/transport\";\nimport { LoggerlessTransport } from \"@loglayer/transport\";\n\n/**\n * Error thrown when HTTP request fails\n */\nclass HttpTransportError extends Error {\n constructor(\n message: string,\n public status?: number,\n public response?: Response,\n ) {\n super(message);\n this.name = \"HttpTransportError\";\n }\n}\n\n/**\n * Error thrown when rate limit is exceeded\n */\nclass RateLimitError extends Error {\n constructor(\n message: string,\n public retryAfter: number,\n ) {\n super(message);\n this.name = \"RateLimitError\";\n }\n}\n\n/**\n * Error thrown when log entry exceeds size limits\n */\nclass LogSizeError extends Error {\n constructor(\n message: string,\n public logEntry: Record<string, any>,\n public size: number,\n public limit: number,\n ) {\n super(message);\n this.name = \"LogSizeError\";\n }\n}\n\n/**\n * Configuration options for the HTTP transport.\n */\nexport interface HttpTransportConfig extends LoggerlessTransportConfig {\n /**\n * The URL to send logs to\n */\n url: string;\n /**\n * HTTP method to use for requests\n * @default \"POST\"\n */\n method?: string;\n /**\n * Headers to include in the request. Can be an object or a function that returns headers.\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n /**\n * Content type for single log requests. User-specified headers take precedence.\n * @default \"application/json\"\n */\n contentType?: string;\n /**\n * Content type for batch log requests. User-specified headers take precedence.\n * @default \"application/json\"\n */\n batchContentType?: string;\n /**\n * Optional callback for error handling\n */\n onError?: (err: Error) => void;\n /**\n * Optional callback for debugging log entries before they are sent\n */\n onDebug?: (entry: Record<string, any>) => void;\n /**\n * Function to transform log data into the payload format\n */\n payloadTemplate: (data: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n /**\n * Whether to use gzip compression\n * @default false\n */\n compression?: boolean;\n /**\n * Number of retry attempts before giving up\n * @default 3\n */\n maxRetries?: number;\n /**\n * Base delay between retries in milliseconds\n * @default 1000\n */\n retryDelay?: number;\n /**\n * Whether to respect rate limiting by waiting when a 429 response is received\n * @default true\n */\n respectRateLimit?: boolean;\n /**\n * Whether to enable batch sending\n * @default true\n */\n enableBatchSend?: boolean;\n /**\n * Number of log entries to batch before sending\n * @default 100\n */\n batchSize?: number;\n /**\n * Timeout in milliseconds for sending batches regardless of size\n * @default 5000\n */\n batchSendTimeout?: number;\n /**\n * Delimiter to use between log entries in batch mode\n * @default \"\\n\"\n */\n batchSendDelimiter?: string;\n /**\n * Maximum size of a single log entry in bytes\n * @default 1048576 (1MB)\n */\n maxLogSize?: number;\n /**\n * Maximum size of the payload (uncompressed) in bytes\n * @default 5242880 (5MB)\n */\n maxPayloadSize?: number;\n /**\n * Whether to enable Next.js Edge Runtime compatibility mode\n * When enabled, TextEncoder and compression are disabled\n * @default false\n */\n enableNextJsEdgeCompat?: boolean;\n}\n\n/**\n * Compresses data using gzip compression\n */\nasync function compressData(data: string): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const dataBytes = encoder.encode(data);\n\n // Use the CompressionStream API if available (modern browsers)\n if (typeof CompressionStream !== \"undefined\") {\n const stream = new CompressionStream(\"gzip\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n\n await writer.write(dataBytes);\n await writer.close();\n\n const chunks: Uint8Array[] = [];\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n }\n\n // Fallback for Node.js or environments without CompressionStream\n // In a real implementation, you might want to use a library like 'zlib' for Node.js\n throw new Error(\"Gzip compression not supported in this environment\");\n}\n\n/**\n * Sends HTTP request with retry logic and rate limiting\n */\nasync function sendWithRetry(\n url: string,\n method: string,\n headers: Record<string, string>,\n payload: string | Uint8Array,\n maxRetries: number,\n retryDelay: number,\n respectRateLimit = true,\n): Promise<Response> {\n let lastError: Error;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, {\n method,\n headers,\n body: payload,\n });\n\n // Handle rate limiting\n if (response.status === 429 && respectRateLimit) {\n const retryAfter = response.headers.get(\"retry-after\");\n const waitTime = retryAfter ? Number.parseInt(retryAfter) * 1000 : retryDelay;\n\n throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);\n }\n\n // Handle other errors\n if (!response.ok) {\n throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);\n }\n\n return response;\n } catch (error) {\n lastError = error as Error;\n\n // If it's a rate limit error and we should respect it, wait and retry\n if (error instanceof RateLimitError && respectRateLimit) {\n await new Promise((resolve) => setTimeout(resolve, error.retryAfter));\n continue;\n }\n\n // If it's the last attempt, throw the error\n if (attempt === maxRetries) {\n throw error;\n }\n\n // Wait before retrying with exponential backoff\n const waitTime = retryDelay * 2 ** attempt;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n }\n }\n\n throw lastError!;\n}\n\n/**\n * HttpTransport is responsible for sending logs to any HTTP endpoint.\n * It supports batching, compression, retries, and rate limiting.\n *\n * Features:\n * - Configurable HTTP method and headers\n * - Custom payload template function\n * - Gzip compression support\n * - Retry logic with exponential backoff\n * - Rate limiting support\n * - Batch sending with configurable size and timeout\n * - Error and debug callbacks\n * - Log size validation\n * - Payload size tracking for batching\n */\nexport class HttpTransport extends LoggerlessTransport {\n private url: string;\n private method: string;\n private headers: Record<string, string> | (() => Record<string, string>);\n private contentType: string;\n private batchContentType: string;\n private onError?: (err: Error) => void;\n private onDebug?: (entry: Record<string, any>) => void;\n private payloadTemplate: (data: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n private compression: boolean;\n private maxRetries: number;\n private retryDelay: number;\n private respectRateLimit: boolean;\n private enableBatchSend: boolean;\n private batchSize: number;\n private batchSendTimeout: number;\n private batchSendDelimiter: string;\n private maxLogSize: number;\n private maxPayloadSize: number;\n private enableNextJsEdgeCompat: boolean;\n\n // Batch management\n private batchQueue: string[] = [];\n private batchTimeout?: NodeJS.Timeout;\n private isProcessingBatch = false;\n private currentBatchSize = 0; // Track uncompressed size of current batch\n\n /**\n * Creates a new instance of HttpTransport.\n *\n * @param config - Configuration options for the transport\n */\n constructor(config: HttpTransportConfig) {\n super(config);\n\n this.url = config.url;\n this.method = config.method ?? \"POST\";\n this.headers = config.headers ?? {};\n this.contentType = config.contentType ?? \"application/json\";\n this.batchContentType = config.batchContentType ?? \"application/json\";\n this.onError = config.onError;\n this.onDebug = config.onDebug;\n this.payloadTemplate = config.payloadTemplate;\n this.compression = config.compression ?? false;\n this.maxRetries = config.maxRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.respectRateLimit = config.respectRateLimit ?? true;\n this.enableBatchSend = config.enableBatchSend ?? true;\n this.batchSize = config.batchSize ?? 100;\n this.batchSendTimeout = config.batchSendTimeout ?? 5000;\n this.batchSendDelimiter = config.batchSendDelimiter ?? \"\\n\";\n this.maxLogSize = config.maxLogSize ?? 1048576; // 1MB\n this.maxPayloadSize = config.maxPayloadSize ?? 5242880; // 5MB\n this.enableNextJsEdgeCompat = config.enableNextJsEdgeCompat ?? false;\n }\n\n /**\n * Processes and ships log entries to the HTTP endpoint.\n *\n * @param params - Log parameters including level, messages, and metadata\n * @returns The original messages array\n */\n shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[] {\n try {\n const message = messages.join(\" \");\n const payload = this.payloadTemplate({\n logLevel,\n message,\n data,\n });\n\n if (this.onDebug) {\n this.onDebug({ logLevel, message, data });\n }\n\n // Check log entry size\n let logEntrySize: number;\n\n if (this.enableNextJsEdgeCompat || typeof TextEncoder === \"undefined\") {\n // Fallback for environments without TextEncoder (like Next.js Edge Runtime)\n logEntrySize = Buffer.byteLength(payload, \"utf8\");\n } else {\n logEntrySize = new TextEncoder().encode(payload).length;\n }\n\n if (logEntrySize > this.maxLogSize) {\n const error = new LogSizeError(\n `Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,\n { logLevel, message, data },\n logEntrySize,\n this.maxLogSize,\n );\n\n if (this.onError) {\n this.onError(error);\n }\n return messages;\n }\n\n if (this.enableBatchSend) {\n this.addToBatch(payload, logEntrySize);\n } else {\n // Send immediately\n this.sendPayload(payload).catch((err) => {\n if (this.onError) {\n this.onError(err);\n }\n });\n }\n } catch (error) {\n if (this.onError) {\n this.onError(error as Error);\n }\n }\n\n return messages;\n }\n\n /**\n * Adds a payload to the batch queue and triggers sending if conditions are met\n */\n private addToBatch(payload: string, logEntrySize: number): void {\n // Check if adding this entry would exceed payload size limit\n const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;\n const payloadSizeThreshold = this.maxPayloadSize * 0.9; // 90% of max payload size\n\n // Force send if adding this entry would exceed 90% of max payload size\n if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {\n this.processBatch();\n }\n\n this.batchQueue.push(payload);\n this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;\n\n // Start batch timeout if not already running\n if (!this.batchTimeout) {\n this.batchTimeout = setTimeout(() => {\n this.processBatch();\n }, this.batchSendTimeout);\n }\n\n // Send immediately if batch size is reached\n if (this.batchQueue.length >= this.batchSize) {\n this.processBatch();\n }\n }\n\n /**\n * Processes the current batch and sends it to the HTTP endpoint\n */\n private async processBatch(): Promise<void> {\n if (this.isProcessingBatch || this.batchQueue.length === 0) {\n return;\n }\n\n this.isProcessingBatch = true;\n\n // Clear the timeout\n if (this.batchTimeout) {\n clearTimeout(this.batchTimeout);\n this.batchTimeout = undefined;\n }\n\n // Get the current batch\n const batch = this.batchQueue.splice(0, this.batchSize);\n\n // Reset batch size counter\n this.currentBatchSize = 0;\n\n try {\n await this.sendBatch(batch);\n } catch (error) {\n if (this.onError) {\n this.onError(error as Error);\n }\n } finally {\n this.isProcessingBatch = false;\n\n // If there are more items in the queue, process them\n if (this.batchQueue.length > 0) {\n this.processBatch();\n }\n }\n }\n\n /**\n * Sends a batch of payloads to the HTTP endpoint\n */\n private async sendBatch(batch: string[]): Promise<void> {\n const batchPayload = batch.join(this.batchSendDelimiter);\n await this.sendPayload(batchPayload, this.batchContentType);\n }\n\n /**\n * Sends a single payload to the HTTP endpoint\n */\n private async sendPayload(payload: string, contentType?: string): Promise<void> {\n // Get headers\n const headers: Record<string, string> = typeof this.headers === \"function\" ? this.headers() : { ...this.headers };\n\n // Set content type - user headers take precedence\n if (!headers[\"content-type\"]) {\n headers[\"content-type\"] = contentType ?? this.contentType;\n }\n\n let finalPayload: string | Uint8Array = payload;\n\n // Apply compression if enabled and not in Next.js Edge compatibility mode\n if (this.compression && !this.enableNextJsEdgeCompat) {\n try {\n finalPayload = await compressData(payload);\n headers[\"content-encoding\"] = \"gzip\";\n } catch (error) {\n // If compression fails, fall back to uncompressed\n if (this.onError) {\n this.onError(new Error(`Compression failed: ${error}`));\n }\n }\n }\n\n await sendWithRetry(\n this.url,\n this.method,\n headers,\n finalPayload,\n this.maxRetries,\n this.retryDelay,\n this.respectRateLimit,\n );\n }\n}\n"]}
@@ -0,0 +1,173 @@
1
+ import { LoggerlessTransportConfig, LoggerlessTransport, LogLayerTransportParams } from '@loglayer/transport';
2
+
3
+ /**
4
+ * Configuration options for the HTTP transport.
5
+ */
6
+ interface HttpTransportConfig extends LoggerlessTransportConfig {
7
+ /**
8
+ * The URL to send logs to
9
+ */
10
+ url: string;
11
+ /**
12
+ * HTTP method to use for requests
13
+ * @default "POST"
14
+ */
15
+ method?: string;
16
+ /**
17
+ * Headers to include in the request. Can be an object or a function that returns headers.
18
+ */
19
+ headers?: Record<string, string> | (() => Record<string, string>);
20
+ /**
21
+ * Content type for single log requests. User-specified headers take precedence.
22
+ * @default "application/json"
23
+ */
24
+ contentType?: string;
25
+ /**
26
+ * Content type for batch log requests. User-specified headers take precedence.
27
+ * @default "application/json"
28
+ */
29
+ batchContentType?: string;
30
+ /**
31
+ * Optional callback for error handling
32
+ */
33
+ onError?: (err: Error) => void;
34
+ /**
35
+ * Optional callback for debugging log entries before they are sent
36
+ */
37
+ onDebug?: (entry: Record<string, any>) => void;
38
+ /**
39
+ * Function to transform log data into the payload format
40
+ */
41
+ payloadTemplate: (data: {
42
+ logLevel: string;
43
+ message: string;
44
+ data?: Record<string, any>;
45
+ }) => string;
46
+ /**
47
+ * Whether to use gzip compression
48
+ * @default false
49
+ */
50
+ compression?: boolean;
51
+ /**
52
+ * Number of retry attempts before giving up
53
+ * @default 3
54
+ */
55
+ maxRetries?: number;
56
+ /**
57
+ * Base delay between retries in milliseconds
58
+ * @default 1000
59
+ */
60
+ retryDelay?: number;
61
+ /**
62
+ * Whether to respect rate limiting by waiting when a 429 response is received
63
+ * @default true
64
+ */
65
+ respectRateLimit?: boolean;
66
+ /**
67
+ * Whether to enable batch sending
68
+ * @default true
69
+ */
70
+ enableBatchSend?: boolean;
71
+ /**
72
+ * Number of log entries to batch before sending
73
+ * @default 100
74
+ */
75
+ batchSize?: number;
76
+ /**
77
+ * Timeout in milliseconds for sending batches regardless of size
78
+ * @default 5000
79
+ */
80
+ batchSendTimeout?: number;
81
+ /**
82
+ * Delimiter to use between log entries in batch mode
83
+ * @default "\n"
84
+ */
85
+ batchSendDelimiter?: string;
86
+ /**
87
+ * Maximum size of a single log entry in bytes
88
+ * @default 1048576 (1MB)
89
+ */
90
+ maxLogSize?: number;
91
+ /**
92
+ * Maximum size of the payload (uncompressed) in bytes
93
+ * @default 5242880 (5MB)
94
+ */
95
+ maxPayloadSize?: number;
96
+ /**
97
+ * Whether to enable Next.js Edge Runtime compatibility mode
98
+ * When enabled, TextEncoder and compression are disabled
99
+ * @default false
100
+ */
101
+ enableNextJsEdgeCompat?: boolean;
102
+ }
103
+ /**
104
+ * HttpTransport is responsible for sending logs to any HTTP endpoint.
105
+ * It supports batching, compression, retries, and rate limiting.
106
+ *
107
+ * Features:
108
+ * - Configurable HTTP method and headers
109
+ * - Custom payload template function
110
+ * - Gzip compression support
111
+ * - Retry logic with exponential backoff
112
+ * - Rate limiting support
113
+ * - Batch sending with configurable size and timeout
114
+ * - Error and debug callbacks
115
+ * - Log size validation
116
+ * - Payload size tracking for batching
117
+ */
118
+ declare class HttpTransport extends LoggerlessTransport {
119
+ private url;
120
+ private method;
121
+ private headers;
122
+ private contentType;
123
+ private batchContentType;
124
+ private onError?;
125
+ private onDebug?;
126
+ private payloadTemplate;
127
+ private compression;
128
+ private maxRetries;
129
+ private retryDelay;
130
+ private respectRateLimit;
131
+ private enableBatchSend;
132
+ private batchSize;
133
+ private batchSendTimeout;
134
+ private batchSendDelimiter;
135
+ private maxLogSize;
136
+ private maxPayloadSize;
137
+ private enableNextJsEdgeCompat;
138
+ private batchQueue;
139
+ private batchTimeout?;
140
+ private isProcessingBatch;
141
+ private currentBatchSize;
142
+ /**
143
+ * Creates a new instance of HttpTransport.
144
+ *
145
+ * @param config - Configuration options for the transport
146
+ */
147
+ constructor(config: HttpTransportConfig);
148
+ /**
149
+ * Processes and ships log entries to the HTTP endpoint.
150
+ *
151
+ * @param params - Log parameters including level, messages, and metadata
152
+ * @returns The original messages array
153
+ */
154
+ shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[];
155
+ /**
156
+ * Adds a payload to the batch queue and triggers sending if conditions are met
157
+ */
158
+ private addToBatch;
159
+ /**
160
+ * Processes the current batch and sends it to the HTTP endpoint
161
+ */
162
+ private processBatch;
163
+ /**
164
+ * Sends a batch of payloads to the HTTP endpoint
165
+ */
166
+ private sendBatch;
167
+ /**
168
+ * Sends a single payload to the HTTP endpoint
169
+ */
170
+ private sendPayload;
171
+ }
172
+
173
+ export { HttpTransport, type HttpTransportConfig };
@@ -0,0 +1,173 @@
1
+ import { LoggerlessTransportConfig, LoggerlessTransport, LogLayerTransportParams } from '@loglayer/transport';
2
+
3
+ /**
4
+ * Configuration options for the HTTP transport.
5
+ */
6
+ interface HttpTransportConfig extends LoggerlessTransportConfig {
7
+ /**
8
+ * The URL to send logs to
9
+ */
10
+ url: string;
11
+ /**
12
+ * HTTP method to use for requests
13
+ * @default "POST"
14
+ */
15
+ method?: string;
16
+ /**
17
+ * Headers to include in the request. Can be an object or a function that returns headers.
18
+ */
19
+ headers?: Record<string, string> | (() => Record<string, string>);
20
+ /**
21
+ * Content type for single log requests. User-specified headers take precedence.
22
+ * @default "application/json"
23
+ */
24
+ contentType?: string;
25
+ /**
26
+ * Content type for batch log requests. User-specified headers take precedence.
27
+ * @default "application/json"
28
+ */
29
+ batchContentType?: string;
30
+ /**
31
+ * Optional callback for error handling
32
+ */
33
+ onError?: (err: Error) => void;
34
+ /**
35
+ * Optional callback for debugging log entries before they are sent
36
+ */
37
+ onDebug?: (entry: Record<string, any>) => void;
38
+ /**
39
+ * Function to transform log data into the payload format
40
+ */
41
+ payloadTemplate: (data: {
42
+ logLevel: string;
43
+ message: string;
44
+ data?: Record<string, any>;
45
+ }) => string;
46
+ /**
47
+ * Whether to use gzip compression
48
+ * @default false
49
+ */
50
+ compression?: boolean;
51
+ /**
52
+ * Number of retry attempts before giving up
53
+ * @default 3
54
+ */
55
+ maxRetries?: number;
56
+ /**
57
+ * Base delay between retries in milliseconds
58
+ * @default 1000
59
+ */
60
+ retryDelay?: number;
61
+ /**
62
+ * Whether to respect rate limiting by waiting when a 429 response is received
63
+ * @default true
64
+ */
65
+ respectRateLimit?: boolean;
66
+ /**
67
+ * Whether to enable batch sending
68
+ * @default true
69
+ */
70
+ enableBatchSend?: boolean;
71
+ /**
72
+ * Number of log entries to batch before sending
73
+ * @default 100
74
+ */
75
+ batchSize?: number;
76
+ /**
77
+ * Timeout in milliseconds for sending batches regardless of size
78
+ * @default 5000
79
+ */
80
+ batchSendTimeout?: number;
81
+ /**
82
+ * Delimiter to use between log entries in batch mode
83
+ * @default "\n"
84
+ */
85
+ batchSendDelimiter?: string;
86
+ /**
87
+ * Maximum size of a single log entry in bytes
88
+ * @default 1048576 (1MB)
89
+ */
90
+ maxLogSize?: number;
91
+ /**
92
+ * Maximum size of the payload (uncompressed) in bytes
93
+ * @default 5242880 (5MB)
94
+ */
95
+ maxPayloadSize?: number;
96
+ /**
97
+ * Whether to enable Next.js Edge Runtime compatibility mode
98
+ * When enabled, TextEncoder and compression are disabled
99
+ * @default false
100
+ */
101
+ enableNextJsEdgeCompat?: boolean;
102
+ }
103
+ /**
104
+ * HttpTransport is responsible for sending logs to any HTTP endpoint.
105
+ * It supports batching, compression, retries, and rate limiting.
106
+ *
107
+ * Features:
108
+ * - Configurable HTTP method and headers
109
+ * - Custom payload template function
110
+ * - Gzip compression support
111
+ * - Retry logic with exponential backoff
112
+ * - Rate limiting support
113
+ * - Batch sending with configurable size and timeout
114
+ * - Error and debug callbacks
115
+ * - Log size validation
116
+ * - Payload size tracking for batching
117
+ */
118
+ declare class HttpTransport extends LoggerlessTransport {
119
+ private url;
120
+ private method;
121
+ private headers;
122
+ private contentType;
123
+ private batchContentType;
124
+ private onError?;
125
+ private onDebug?;
126
+ private payloadTemplate;
127
+ private compression;
128
+ private maxRetries;
129
+ private retryDelay;
130
+ private respectRateLimit;
131
+ private enableBatchSend;
132
+ private batchSize;
133
+ private batchSendTimeout;
134
+ private batchSendDelimiter;
135
+ private maxLogSize;
136
+ private maxPayloadSize;
137
+ private enableNextJsEdgeCompat;
138
+ private batchQueue;
139
+ private batchTimeout?;
140
+ private isProcessingBatch;
141
+ private currentBatchSize;
142
+ /**
143
+ * Creates a new instance of HttpTransport.
144
+ *
145
+ * @param config - Configuration options for the transport
146
+ */
147
+ constructor(config: HttpTransportConfig);
148
+ /**
149
+ * Processes and ships log entries to the HTTP endpoint.
150
+ *
151
+ * @param params - Log parameters including level, messages, and metadata
152
+ * @returns The original messages array
153
+ */
154
+ shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[];
155
+ /**
156
+ * Adds a payload to the batch queue and triggers sending if conditions are met
157
+ */
158
+ private addToBatch;
159
+ /**
160
+ * Processes the current batch and sends it to the HTTP endpoint
161
+ */
162
+ private processBatch;
163
+ /**
164
+ * Sends a batch of payloads to the HTTP endpoint
165
+ */
166
+ private sendBatch;
167
+ /**
168
+ * Sends a single payload to the HTTP endpoint
169
+ */
170
+ private sendPayload;
171
+ }
172
+
173
+ export { HttpTransport, type HttpTransportConfig };
package/dist/index.js ADDED
@@ -0,0 +1,277 @@
1
+ // src/HttpTransport.ts
2
+ import { LoggerlessTransport } from "@loglayer/transport";
3
+ 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
+ }
10
+ };
11
+ var RateLimitError = class extends Error {
12
+ constructor(message, retryAfter) {
13
+ super(message);
14
+ this.retryAfter = retryAfter;
15
+ this.name = "RateLimitError";
16
+ }
17
+ };
18
+ 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
+ }
26
+ };
27
+ 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");
52
+ }
53
+ async function sendWithRetry(url, method, headers, payload, maxRetries, retryDelay, respectRateLimit = true) {
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 (response.status === 429 && respectRateLimit) {
63
+ const retryAfter = response.headers.get("retry-after");
64
+ const waitTime = retryAfter ? Number.parseInt(retryAfter) * 1e3 : retryDelay;
65
+ throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);
66
+ }
67
+ if (!response.ok) {
68
+ throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);
69
+ }
70
+ return response;
71
+ } catch (error) {
72
+ lastError = error;
73
+ if (error instanceof RateLimitError && respectRateLimit) {
74
+ await new Promise((resolve) => setTimeout(resolve, error.retryAfter));
75
+ continue;
76
+ }
77
+ if (attempt === maxRetries) {
78
+ throw error;
79
+ }
80
+ const waitTime = retryDelay * 2 ** attempt;
81
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
82
+ }
83
+ }
84
+ throw lastError;
85
+ }
86
+ var HttpTransport = class extends LoggerlessTransport {
87
+ url;
88
+ method;
89
+ headers;
90
+ contentType;
91
+ batchContentType;
92
+ onError;
93
+ onDebug;
94
+ payloadTemplate;
95
+ compression;
96
+ maxRetries;
97
+ retryDelay;
98
+ respectRateLimit;
99
+ enableBatchSend;
100
+ batchSize;
101
+ batchSendTimeout;
102
+ batchSendDelimiter;
103
+ maxLogSize;
104
+ maxPayloadSize;
105
+ enableNextJsEdgeCompat;
106
+ // Batch management
107
+ batchQueue = [];
108
+ batchTimeout;
109
+ isProcessingBatch = false;
110
+ currentBatchSize = 0;
111
+ // Track uncompressed size of current batch
112
+ /**
113
+ * Creates a new instance of HttpTransport.
114
+ *
115
+ * @param config - Configuration options for the transport
116
+ */
117
+ constructor(config) {
118
+ super(config);
119
+ this.url = config.url;
120
+ this.method = config.method ?? "POST";
121
+ this.headers = config.headers ?? {};
122
+ this.contentType = config.contentType ?? "application/json";
123
+ this.batchContentType = config.batchContentType ?? "application/json";
124
+ this.onError = config.onError;
125
+ this.onDebug = config.onDebug;
126
+ this.payloadTemplate = config.payloadTemplate;
127
+ this.compression = config.compression ?? false;
128
+ this.maxRetries = config.maxRetries ?? 3;
129
+ this.retryDelay = config.retryDelay ?? 1e3;
130
+ this.respectRateLimit = config.respectRateLimit ?? true;
131
+ this.enableBatchSend = config.enableBatchSend ?? true;
132
+ this.batchSize = config.batchSize ?? 100;
133
+ this.batchSendTimeout = config.batchSendTimeout ?? 5e3;
134
+ this.batchSendDelimiter = config.batchSendDelimiter ?? "\n";
135
+ this.maxLogSize = config.maxLogSize ?? 1048576;
136
+ this.maxPayloadSize = config.maxPayloadSize ?? 5242880;
137
+ this.enableNextJsEdgeCompat = config.enableNextJsEdgeCompat ?? false;
138
+ }
139
+ /**
140
+ * Processes and ships log entries to the HTTP endpoint.
141
+ *
142
+ * @param params - Log parameters including level, messages, and metadata
143
+ * @returns The original messages array
144
+ */
145
+ shipToLogger({ logLevel, messages, data, hasData }) {
146
+ try {
147
+ const message = messages.join(" ");
148
+ const payload = this.payloadTemplate({
149
+ logLevel,
150
+ message,
151
+ data
152
+ });
153
+ if (this.onDebug) {
154
+ this.onDebug({ logLevel, message, data });
155
+ }
156
+ let logEntrySize;
157
+ if (this.enableNextJsEdgeCompat || typeof TextEncoder === "undefined") {
158
+ logEntrySize = Buffer.byteLength(payload, "utf8");
159
+ } else {
160
+ logEntrySize = new TextEncoder().encode(payload).length;
161
+ }
162
+ if (logEntrySize > this.maxLogSize) {
163
+ const error = new LogSizeError(
164
+ `Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,
165
+ { logLevel, message, data },
166
+ logEntrySize,
167
+ this.maxLogSize
168
+ );
169
+ if (this.onError) {
170
+ this.onError(error);
171
+ }
172
+ return messages;
173
+ }
174
+ if (this.enableBatchSend) {
175
+ this.addToBatch(payload, logEntrySize);
176
+ } else {
177
+ this.sendPayload(payload).catch((err) => {
178
+ if (this.onError) {
179
+ this.onError(err);
180
+ }
181
+ });
182
+ }
183
+ } catch (error) {
184
+ if (this.onError) {
185
+ this.onError(error);
186
+ }
187
+ }
188
+ return messages;
189
+ }
190
+ /**
191
+ * Adds a payload to the batch queue and triggers sending if conditions are met
192
+ */
193
+ addToBatch(payload, logEntrySize) {
194
+ const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;
195
+ const payloadSizeThreshold = this.maxPayloadSize * 0.9;
196
+ if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {
197
+ this.processBatch();
198
+ }
199
+ this.batchQueue.push(payload);
200
+ this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;
201
+ if (!this.batchTimeout) {
202
+ this.batchTimeout = setTimeout(() => {
203
+ this.processBatch();
204
+ }, this.batchSendTimeout);
205
+ }
206
+ if (this.batchQueue.length >= this.batchSize) {
207
+ this.processBatch();
208
+ }
209
+ }
210
+ /**
211
+ * Processes the current batch and sends it to the HTTP endpoint
212
+ */
213
+ async processBatch() {
214
+ if (this.isProcessingBatch || this.batchQueue.length === 0) {
215
+ return;
216
+ }
217
+ this.isProcessingBatch = true;
218
+ if (this.batchTimeout) {
219
+ clearTimeout(this.batchTimeout);
220
+ this.batchTimeout = void 0;
221
+ }
222
+ const batch = this.batchQueue.splice(0, this.batchSize);
223
+ this.currentBatchSize = 0;
224
+ try {
225
+ await this.sendBatch(batch);
226
+ } catch (error) {
227
+ if (this.onError) {
228
+ this.onError(error);
229
+ }
230
+ } finally {
231
+ this.isProcessingBatch = false;
232
+ if (this.batchQueue.length > 0) {
233
+ this.processBatch();
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Sends a batch of payloads to the HTTP endpoint
239
+ */
240
+ async sendBatch(batch) {
241
+ const batchPayload = batch.join(this.batchSendDelimiter);
242
+ await this.sendPayload(batchPayload, this.batchContentType);
243
+ }
244
+ /**
245
+ * Sends a single payload to the HTTP endpoint
246
+ */
247
+ async sendPayload(payload, contentType) {
248
+ const headers = typeof this.headers === "function" ? this.headers() : { ...this.headers };
249
+ if (!headers["content-type"]) {
250
+ headers["content-type"] = contentType ?? this.contentType;
251
+ }
252
+ let finalPayload = payload;
253
+ if (this.compression && !this.enableNextJsEdgeCompat) {
254
+ try {
255
+ finalPayload = await compressData(payload);
256
+ headers["content-encoding"] = "gzip";
257
+ } catch (error) {
258
+ if (this.onError) {
259
+ this.onError(new Error(`Compression failed: ${error}`));
260
+ }
261
+ }
262
+ }
263
+ await sendWithRetry(
264
+ this.url,
265
+ this.method,
266
+ headers,
267
+ finalPayload,
268
+ this.maxRetries,
269
+ this.retryDelay,
270
+ this.respectRateLimit
271
+ );
272
+ }
273
+ };
274
+ export {
275
+ HttpTransport
276
+ };
277
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/HttpTransport.ts"],"sourcesContent":["import type { LoggerlessTransportConfig, LogLayerTransportParams } from \"@loglayer/transport\";\nimport { LoggerlessTransport } from \"@loglayer/transport\";\n\n/**\n * Error thrown when HTTP request fails\n */\nclass HttpTransportError extends Error {\n constructor(\n message: string,\n public status?: number,\n public response?: Response,\n ) {\n super(message);\n this.name = \"HttpTransportError\";\n }\n}\n\n/**\n * Error thrown when rate limit is exceeded\n */\nclass RateLimitError extends Error {\n constructor(\n message: string,\n public retryAfter: number,\n ) {\n super(message);\n this.name = \"RateLimitError\";\n }\n}\n\n/**\n * Error thrown when log entry exceeds size limits\n */\nclass LogSizeError extends Error {\n constructor(\n message: string,\n public logEntry: Record<string, any>,\n public size: number,\n public limit: number,\n ) {\n super(message);\n this.name = \"LogSizeError\";\n }\n}\n\n/**\n * Configuration options for the HTTP transport.\n */\nexport interface HttpTransportConfig extends LoggerlessTransportConfig {\n /**\n * The URL to send logs to\n */\n url: string;\n /**\n * HTTP method to use for requests\n * @default \"POST\"\n */\n method?: string;\n /**\n * Headers to include in the request. Can be an object or a function that returns headers.\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n /**\n * Content type for single log requests. User-specified headers take precedence.\n * @default \"application/json\"\n */\n contentType?: string;\n /**\n * Content type for batch log requests. User-specified headers take precedence.\n * @default \"application/json\"\n */\n batchContentType?: string;\n /**\n * Optional callback for error handling\n */\n onError?: (err: Error) => void;\n /**\n * Optional callback for debugging log entries before they are sent\n */\n onDebug?: (entry: Record<string, any>) => void;\n /**\n * Function to transform log data into the payload format\n */\n payloadTemplate: (data: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n /**\n * Whether to use gzip compression\n * @default false\n */\n compression?: boolean;\n /**\n * Number of retry attempts before giving up\n * @default 3\n */\n maxRetries?: number;\n /**\n * Base delay between retries in milliseconds\n * @default 1000\n */\n retryDelay?: number;\n /**\n * Whether to respect rate limiting by waiting when a 429 response is received\n * @default true\n */\n respectRateLimit?: boolean;\n /**\n * Whether to enable batch sending\n * @default true\n */\n enableBatchSend?: boolean;\n /**\n * Number of log entries to batch before sending\n * @default 100\n */\n batchSize?: number;\n /**\n * Timeout in milliseconds for sending batches regardless of size\n * @default 5000\n */\n batchSendTimeout?: number;\n /**\n * Delimiter to use between log entries in batch mode\n * @default \"\\n\"\n */\n batchSendDelimiter?: string;\n /**\n * Maximum size of a single log entry in bytes\n * @default 1048576 (1MB)\n */\n maxLogSize?: number;\n /**\n * Maximum size of the payload (uncompressed) in bytes\n * @default 5242880 (5MB)\n */\n maxPayloadSize?: number;\n /**\n * Whether to enable Next.js Edge Runtime compatibility mode\n * When enabled, TextEncoder and compression are disabled\n * @default false\n */\n enableNextJsEdgeCompat?: boolean;\n}\n\n/**\n * Compresses data using gzip compression\n */\nasync function compressData(data: string): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const dataBytes = encoder.encode(data);\n\n // Use the CompressionStream API if available (modern browsers)\n if (typeof CompressionStream !== \"undefined\") {\n const stream = new CompressionStream(\"gzip\");\n const writer = stream.writable.getWriter();\n const reader = stream.readable.getReader();\n\n await writer.write(dataBytes);\n await writer.close();\n\n const chunks: Uint8Array[] = [];\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n const result = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.length;\n }\n\n return result;\n }\n\n // Fallback for Node.js or environments without CompressionStream\n // In a real implementation, you might want to use a library like 'zlib' for Node.js\n throw new Error(\"Gzip compression not supported in this environment\");\n}\n\n/**\n * Sends HTTP request with retry logic and rate limiting\n */\nasync function sendWithRetry(\n url: string,\n method: string,\n headers: Record<string, string>,\n payload: string | Uint8Array,\n maxRetries: number,\n retryDelay: number,\n respectRateLimit = true,\n): Promise<Response> {\n let lastError: Error;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, {\n method,\n headers,\n body: payload,\n });\n\n // Handle rate limiting\n if (response.status === 429 && respectRateLimit) {\n const retryAfter = response.headers.get(\"retry-after\");\n const waitTime = retryAfter ? Number.parseInt(retryAfter) * 1000 : retryDelay;\n\n throw new RateLimitError(`Rate limit exceeded. Retry after ${waitTime}ms`, waitTime);\n }\n\n // Handle other errors\n if (!response.ok) {\n throw new HttpTransportError(`HTTP ${response.status}: ${response.statusText}`, response.status, response);\n }\n\n return response;\n } catch (error) {\n lastError = error as Error;\n\n // If it's a rate limit error and we should respect it, wait and retry\n if (error instanceof RateLimitError && respectRateLimit) {\n await new Promise((resolve) => setTimeout(resolve, error.retryAfter));\n continue;\n }\n\n // If it's the last attempt, throw the error\n if (attempt === maxRetries) {\n throw error;\n }\n\n // Wait before retrying with exponential backoff\n const waitTime = retryDelay * 2 ** attempt;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n }\n }\n\n throw lastError!;\n}\n\n/**\n * HttpTransport is responsible for sending logs to any HTTP endpoint.\n * It supports batching, compression, retries, and rate limiting.\n *\n * Features:\n * - Configurable HTTP method and headers\n * - Custom payload template function\n * - Gzip compression support\n * - Retry logic with exponential backoff\n * - Rate limiting support\n * - Batch sending with configurable size and timeout\n * - Error and debug callbacks\n * - Log size validation\n * - Payload size tracking for batching\n */\nexport class HttpTransport extends LoggerlessTransport {\n private url: string;\n private method: string;\n private headers: Record<string, string> | (() => Record<string, string>);\n private contentType: string;\n private batchContentType: string;\n private onError?: (err: Error) => void;\n private onDebug?: (entry: Record<string, any>) => void;\n private payloadTemplate: (data: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n private compression: boolean;\n private maxRetries: number;\n private retryDelay: number;\n private respectRateLimit: boolean;\n private enableBatchSend: boolean;\n private batchSize: number;\n private batchSendTimeout: number;\n private batchSendDelimiter: string;\n private maxLogSize: number;\n private maxPayloadSize: number;\n private enableNextJsEdgeCompat: boolean;\n\n // Batch management\n private batchQueue: string[] = [];\n private batchTimeout?: NodeJS.Timeout;\n private isProcessingBatch = false;\n private currentBatchSize = 0; // Track uncompressed size of current batch\n\n /**\n * Creates a new instance of HttpTransport.\n *\n * @param config - Configuration options for the transport\n */\n constructor(config: HttpTransportConfig) {\n super(config);\n\n this.url = config.url;\n this.method = config.method ?? \"POST\";\n this.headers = config.headers ?? {};\n this.contentType = config.contentType ?? \"application/json\";\n this.batchContentType = config.batchContentType ?? \"application/json\";\n this.onError = config.onError;\n this.onDebug = config.onDebug;\n this.payloadTemplate = config.payloadTemplate;\n this.compression = config.compression ?? false;\n this.maxRetries = config.maxRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.respectRateLimit = config.respectRateLimit ?? true;\n this.enableBatchSend = config.enableBatchSend ?? true;\n this.batchSize = config.batchSize ?? 100;\n this.batchSendTimeout = config.batchSendTimeout ?? 5000;\n this.batchSendDelimiter = config.batchSendDelimiter ?? \"\\n\";\n this.maxLogSize = config.maxLogSize ?? 1048576; // 1MB\n this.maxPayloadSize = config.maxPayloadSize ?? 5242880; // 5MB\n this.enableNextJsEdgeCompat = config.enableNextJsEdgeCompat ?? false;\n }\n\n /**\n * Processes and ships log entries to the HTTP endpoint.\n *\n * @param params - Log parameters including level, messages, and metadata\n * @returns The original messages array\n */\n shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[] {\n try {\n const message = messages.join(\" \");\n const payload = this.payloadTemplate({\n logLevel,\n message,\n data,\n });\n\n if (this.onDebug) {\n this.onDebug({ logLevel, message, data });\n }\n\n // Check log entry size\n let logEntrySize: number;\n\n if (this.enableNextJsEdgeCompat || typeof TextEncoder === \"undefined\") {\n // Fallback for environments without TextEncoder (like Next.js Edge Runtime)\n logEntrySize = Buffer.byteLength(payload, \"utf8\");\n } else {\n logEntrySize = new TextEncoder().encode(payload).length;\n }\n\n if (logEntrySize > this.maxLogSize) {\n const error = new LogSizeError(\n `Log entry exceeds maximum size of ${this.maxLogSize} bytes. Size: ${logEntrySize} bytes`,\n { logLevel, message, data },\n logEntrySize,\n this.maxLogSize,\n );\n\n if (this.onError) {\n this.onError(error);\n }\n return messages;\n }\n\n if (this.enableBatchSend) {\n this.addToBatch(payload, logEntrySize);\n } else {\n // Send immediately\n this.sendPayload(payload).catch((err) => {\n if (this.onError) {\n this.onError(err);\n }\n });\n }\n } catch (error) {\n if (this.onError) {\n this.onError(error as Error);\n }\n }\n\n return messages;\n }\n\n /**\n * Adds a payload to the batch queue and triggers sending if conditions are met\n */\n private addToBatch(payload: string, logEntrySize: number): void {\n // Check if adding this entry would exceed payload size limit\n const payloadSizeWithEntry = this.currentBatchSize + logEntrySize + this.batchSendDelimiter.length;\n const payloadSizeThreshold = this.maxPayloadSize * 0.9; // 90% of max payload size\n\n // Force send if adding this entry would exceed 90% of max payload size\n if (payloadSizeWithEntry > payloadSizeThreshold && this.batchQueue.length > 0) {\n this.processBatch();\n }\n\n this.batchQueue.push(payload);\n this.currentBatchSize += logEntrySize + this.batchSendDelimiter.length;\n\n // Start batch timeout if not already running\n if (!this.batchTimeout) {\n this.batchTimeout = setTimeout(() => {\n this.processBatch();\n }, this.batchSendTimeout);\n }\n\n // Send immediately if batch size is reached\n if (this.batchQueue.length >= this.batchSize) {\n this.processBatch();\n }\n }\n\n /**\n * Processes the current batch and sends it to the HTTP endpoint\n */\n private async processBatch(): Promise<void> {\n if (this.isProcessingBatch || this.batchQueue.length === 0) {\n return;\n }\n\n this.isProcessingBatch = true;\n\n // Clear the timeout\n if (this.batchTimeout) {\n clearTimeout(this.batchTimeout);\n this.batchTimeout = undefined;\n }\n\n // Get the current batch\n const batch = this.batchQueue.splice(0, this.batchSize);\n\n // Reset batch size counter\n this.currentBatchSize = 0;\n\n try {\n await this.sendBatch(batch);\n } catch (error) {\n if (this.onError) {\n this.onError(error as Error);\n }\n } finally {\n this.isProcessingBatch = false;\n\n // If there are more items in the queue, process them\n if (this.batchQueue.length > 0) {\n this.processBatch();\n }\n }\n }\n\n /**\n * Sends a batch of payloads to the HTTP endpoint\n */\n private async sendBatch(batch: string[]): Promise<void> {\n const batchPayload = batch.join(this.batchSendDelimiter);\n await this.sendPayload(batchPayload, this.batchContentType);\n }\n\n /**\n * Sends a single payload to the HTTP endpoint\n */\n private async sendPayload(payload: string, contentType?: string): Promise<void> {\n // Get headers\n const headers: Record<string, string> = typeof this.headers === \"function\" ? this.headers() : { ...this.headers };\n\n // Set content type - user headers take precedence\n if (!headers[\"content-type\"]) {\n headers[\"content-type\"] = contentType ?? this.contentType;\n }\n\n let finalPayload: string | Uint8Array = payload;\n\n // Apply compression if enabled and not in Next.js Edge compatibility mode\n if (this.compression && !this.enableNextJsEdgeCompat) {\n try {\n finalPayload = await compressData(payload);\n headers[\"content-encoding\"] = \"gzip\";\n } catch (error) {\n // If compression fails, fall back to uncompressed\n if (this.onError) {\n this.onError(new Error(`Compression failed: ${error}`));\n }\n }\n }\n\n await sendWithRetry(\n this.url,\n this.method,\n headers,\n finalPayload,\n this.maxRetries,\n this.retryDelay,\n this.respectRateLimit,\n );\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@loglayer/transport-http",
3
+ "description": "HTTP transport for the LogLayer logging library.",
4
+ "version": "1.0.1",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/loglayer/loglayer.git",
23
+ "directory": "packages/transports/http"
24
+ },
25
+ "author": "Theo Gravity <theo@suteki.nu>",
26
+ "keywords": [
27
+ "logging",
28
+ "log",
29
+ "loglayer",
30
+ "http",
31
+ "transport"
32
+ ],
33
+ "dependencies": {
34
+ "@loglayer/transport": "2.2.1"
35
+ },
36
+ "devDependencies": {
37
+ "dotenv": "16.5.0",
38
+ "hash-runner": "2.0.1",
39
+ "@types/node": "22.15.17",
40
+ "serialize-error": "12.0.0",
41
+ "tsx": "4.20.3",
42
+ "tsup": "8.5.0",
43
+ "typescript": "5.8.3",
44
+ "vitest": "3.2.4",
45
+ "loglayer": "6.4.3",
46
+ "@internal/tsconfig": "2.1.0"
47
+ },
48
+ "bugs": "https://github.com/loglayer/loglayer/issues",
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "files": [
53
+ "dist"
54
+ ],
55
+ "homepage": "https://loglayer.dev",
56
+ "scripts": {
57
+ "build": "tsup src/index.ts",
58
+ "test": "vitest --run",
59
+ "build:dev": "hash-runner",
60
+ "clean": "rm -rf .turbo node_modules dist",
61
+ "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
62
+ "verify-types": "tsc --noEmit",
63
+ "livetest": "tsx src/__tests__/livetest.ts"
64
+ }
65
+ }