@loglayer/transport-new-relic 2.1.6 → 2.1.8

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,239 +1,237 @@
1
- // src/NewRelicTransport.ts
2
1
  import { LoggerlessTransport } from "@loglayer/transport";
3
- var MAX_PAYLOAD_SIZE = 1e6;
4
- var MAX_ATTRIBUTES = 255;
5
- var MAX_ATTRIBUTE_NAME_LENGTH = 255;
6
- var MAX_ATTRIBUTE_VALUE_LENGTH = 4094;
2
+
3
+ //#region src/NewRelicTransport.ts
4
+ const MAX_PAYLOAD_SIZE = 1e6;
5
+ const MAX_ATTRIBUTES = 255;
6
+ const MAX_ATTRIBUTE_NAME_LENGTH = 255;
7
+ const MAX_ATTRIBUTE_VALUE_LENGTH = 4094;
8
+ /**
9
+ * Error thrown when log entry validation fails.
10
+ * This includes payload size, attribute count, and attribute name length validations.
11
+ */
7
12
  var ValidationError = class extends Error {
8
- constructor(message) {
9
- super(message);
10
- this.name = "ValidationError";
11
- }
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "ValidationError";
16
+ }
12
17
  };
18
+ /**
19
+ * Error thrown when New Relic's API rate limit is exceeded.
20
+ * Contains the retry-after duration specified by the API.
21
+ */
13
22
  var RateLimitError = class extends Error {
14
- constructor(message, retryAfter) {
15
- super(message);
16
- this.retryAfter = retryAfter;
17
- this.name = "RateLimitError";
18
- }
23
+ constructor(message, retryAfter) {
24
+ super(message);
25
+ this.retryAfter = retryAfter;
26
+ this.name = "RateLimitError";
27
+ }
19
28
  };
29
+ /**
30
+ * Validates a log entry against New Relic's constraints.
31
+ * - Checks number of attributes (max 255)
32
+ * - Validates attribute name length (max 255 characters)
33
+ * - Truncates attribute values longer than 4094 characters
34
+ *
35
+ * @param logEntry - The log entry to validate
36
+ * @returns The validated (and potentially modified) log entry
37
+ * @throws {ValidationError} If validation fails
38
+ */
20
39
  function validateLogEntry(logEntry) {
21
- if (logEntry.attributes) {
22
- const attributeCount = Object.keys(logEntry.attributes).length;
23
- if (attributeCount > MAX_ATTRIBUTES) {
24
- throw new ValidationError(
25
- `Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`
26
- );
27
- }
28
- for (const [key, value] of Object.entries(logEntry.attributes)) {
29
- if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) {
30
- throw new ValidationError(
31
- `Attribute name '${key}' exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Length: ${key.length}`
32
- );
33
- }
34
- if (typeof value === "string" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) {
35
- logEntry.attributes[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);
36
- }
37
- }
38
- }
39
- return logEntry;
40
+ if (logEntry.attributes) {
41
+ const attributeCount = Object.keys(logEntry.attributes).length;
42
+ if (attributeCount > MAX_ATTRIBUTES) throw new ValidationError(`Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`);
43
+ for (const [key, value] of Object.entries(logEntry.attributes)) {
44
+ if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) throw new ValidationError(`Attribute name '${key}' exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Length: ${key.length}`);
45
+ if (typeof value === "string" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) logEntry.attributes[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);
46
+ }
47
+ }
48
+ return logEntry;
40
49
  }
50
+ /**
51
+ * NewRelicTransport is responsible for sending logs to New Relic's Log API.
52
+ * It handles validation, compression, retries, and rate limiting according to New Relic's specifications.
53
+ *
54
+ * Features:
55
+ * - Validates payload size (max 1MB)
56
+ * - Validates number of attributes (max 255)
57
+ * - Validates attribute name length (max 255 characters)
58
+ * - Truncates attribute values longer than 4094 characters
59
+ * - Supports gzip compression
60
+ * - Handles rate limiting with configurable behavior
61
+ * - Implements retry logic with exponential backoff
62
+ */
41
63
  var NewRelicTransport = class extends LoggerlessTransport {
42
- apiKey;
43
- endpoint;
44
- onError;
45
- onDebug;
46
- useCompression;
47
- maxRetries;
48
- retryDelay;
49
- respectRateLimit;
50
- /**
51
- * Creates a new instance of NewRelicTransport.
52
- *
53
- * @param config - Configuration options for the transport
54
- * @param config.apiKey - New Relic API key for authentication
55
- * @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
56
- * @param config.onError - Optional error callback for handling errors
57
- * @param config.onDebug - Optional callback for debugging log entries before they are sent
58
- * @param config.useCompression - Whether to use gzip compression (defaults to true)
59
- * @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
60
- * @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
61
- * @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
62
- */
63
- constructor(config) {
64
- super(config);
65
- this.apiKey = config.apiKey;
66
- this.endpoint = config.endpoint ?? "https://log-api.newrelic.com/log/v1";
67
- this.onError = config.onError;
68
- this.onDebug = config.onDebug;
69
- this.useCompression = config.useCompression ?? true;
70
- this.maxRetries = config.maxRetries ?? 3;
71
- this.retryDelay = config.retryDelay ?? 1e3;
72
- this.respectRateLimit = config.respectRateLimit ?? true;
73
- }
74
- /**
75
- * Processes and ships log entries to New Relic.
76
- *
77
- * This method:
78
- * 1. Validates the message size
79
- * 2. Creates and validates the log entry
80
- * 3. Validates the final payload size
81
- * 4. Asynchronously sends the log entry to New Relic
82
- *
83
- * The actual sending is done asynchronously in a fire-and-forget manner to maintain
84
- * compatibility with the base transport class while still providing retry and error handling.
85
- *
86
- * @param params - Log parameters including level, messages, and metadata
87
- * @param params.logLevel - The severity level of the log
88
- * @param params.messages - Array of message strings to be joined
89
- * @param params.data - Optional metadata to include with the log
90
- * @param params.hasData - Whether metadata is present
91
- * @returns The original messages array
92
- * @throws {ValidationError} If the payload exceeds size limits or validation fails
93
- */
94
- shipToLogger({ logLevel, messages, data, hasData }) {
95
- try {
96
- const message = messages.join(" ");
97
- const messageBytes = new TextEncoder().encode(message).length;
98
- if (messageBytes > MAX_PAYLOAD_SIZE) {
99
- throw new ValidationError(
100
- `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes`
101
- );
102
- }
103
- const logEntry = {
104
- timestamp: Date.now(),
105
- level: logLevel,
106
- log: message
107
- };
108
- if (data && hasData) {
109
- Object.assign(logEntry, {
110
- attributes: data
111
- });
112
- }
113
- const validatedEntry = validateLogEntry(logEntry);
114
- if (this.onDebug) {
115
- this.onDebug(validatedEntry);
116
- }
117
- const payload = JSON.stringify([validatedEntry]);
118
- const payloadBytes = new TextEncoder().encode(payload).length;
119
- if (payloadBytes > MAX_PAYLOAD_SIZE) {
120
- throw new ValidationError(
121
- `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`
122
- );
123
- }
124
- (async () => {
125
- try {
126
- await sendWithRetry(
127
- this.endpoint,
128
- this.apiKey,
129
- payload,
130
- this.useCompression,
131
- this.maxRetries,
132
- this.retryDelay,
133
- this.respectRateLimit
134
- );
135
- } catch (error) {
136
- if (this.onError) {
137
- this.onError(error instanceof Error ? error : new Error(String(error)));
138
- }
139
- if (error instanceof ValidationError) {
140
- throw error;
141
- }
142
- }
143
- })();
144
- } catch (error) {
145
- if (this.onError) {
146
- this.onError(error instanceof Error ? error : new Error(String(error)));
147
- }
148
- }
149
- return messages;
150
- }
64
+ apiKey;
65
+ endpoint;
66
+ onError;
67
+ onDebug;
68
+ useCompression;
69
+ maxRetries;
70
+ retryDelay;
71
+ respectRateLimit;
72
+ /**
73
+ * Creates a new instance of NewRelicTransport.
74
+ *
75
+ * @param config - Configuration options for the transport
76
+ * @param config.apiKey - New Relic API key for authentication
77
+ * @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
78
+ * @param config.onError - Optional error callback for handling errors
79
+ * @param config.onDebug - Optional callback for debugging log entries before they are sent
80
+ * @param config.useCompression - Whether to use gzip compression (defaults to true)
81
+ * @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
82
+ * @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
83
+ * @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
84
+ */
85
+ constructor(config) {
86
+ super(config);
87
+ this.apiKey = config.apiKey;
88
+ this.endpoint = config.endpoint ?? "https://log-api.newrelic.com/log/v1";
89
+ this.onError = config.onError;
90
+ this.onDebug = config.onDebug;
91
+ this.useCompression = config.useCompression ?? true;
92
+ this.maxRetries = config.maxRetries ?? 3;
93
+ this.retryDelay = config.retryDelay ?? 1e3;
94
+ this.respectRateLimit = config.respectRateLimit ?? true;
95
+ }
96
+ /**
97
+ * Processes and ships log entries to New Relic.
98
+ *
99
+ * This method:
100
+ * 1. Validates the message size
101
+ * 2. Creates and validates the log entry
102
+ * 3. Validates the final payload size
103
+ * 4. Asynchronously sends the log entry to New Relic
104
+ *
105
+ * The actual sending is done asynchronously in a fire-and-forget manner to maintain
106
+ * compatibility with the base transport class while still providing retry and error handling.
107
+ *
108
+ * @param params - Log parameters including level, messages, and metadata
109
+ * @param params.logLevel - The severity level of the log
110
+ * @param params.messages - Array of message strings to be joined
111
+ * @param params.data - Optional metadata to include with the log
112
+ * @param params.hasData - Whether metadata is present
113
+ * @returns The original messages array
114
+ * @throws {ValidationError} If the payload exceeds size limits or validation fails
115
+ */
116
+ shipToLogger({ logLevel, messages, data, hasData }) {
117
+ try {
118
+ const message = messages.join(" ");
119
+ const messageBytes = new TextEncoder().encode(message).length;
120
+ if (messageBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes`);
121
+ const logEntry = {
122
+ timestamp: Date.now(),
123
+ level: logLevel,
124
+ log: message
125
+ };
126
+ if (data && hasData) Object.assign(logEntry, { attributes: data });
127
+ const validatedEntry = validateLogEntry(logEntry);
128
+ if (this.onDebug) this.onDebug(validatedEntry);
129
+ const payload = JSON.stringify([validatedEntry]);
130
+ const payloadBytes = new TextEncoder().encode(payload).length;
131
+ if (payloadBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);
132
+ (async () => {
133
+ try {
134
+ await sendWithRetry(this.endpoint, this.apiKey, payload, this.useCompression, this.maxRetries, this.retryDelay, this.respectRateLimit);
135
+ } catch (error) {
136
+ if (this.onError) this.onError(error instanceof Error ? error : new Error(String(error)));
137
+ if (error instanceof ValidationError) throw error;
138
+ }
139
+ })();
140
+ } catch (error) {
141
+ if (this.onError) this.onError(error instanceof Error ? error : new Error(String(error)));
142
+ }
143
+ return messages;
144
+ }
151
145
  };
146
+ /**
147
+ * Compresses data using gzip compression.
148
+ *
149
+ * @param data - The string data to compress
150
+ * @returns A promise that resolves to the compressed data as a Uint8Array
151
+ */
152
152
  async function compressData(data) {
153
- const stream = new CompressionStream("gzip");
154
- const writer = stream.writable.getWriter();
155
- const encoder = new TextEncoder();
156
- const chunks = [];
157
- await writer.write(encoder.encode(data));
158
- await writer.close();
159
- const reader = stream.readable.getReader();
160
- while (true) {
161
- const { value, done } = await reader.read();
162
- if (done) break;
163
- chunks.push(value);
164
- }
165
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
166
- const result = new Uint8Array(totalLength);
167
- let offset = 0;
168
- for (const chunk of chunks) {
169
- result.set(chunk, offset);
170
- offset += chunk.length;
171
- }
172
- return result;
153
+ const stream = new CompressionStream("gzip");
154
+ const writer = stream.writable.getWriter();
155
+ const encoder = new TextEncoder();
156
+ const chunks = [];
157
+ await writer.write(encoder.encode(data));
158
+ await writer.close();
159
+ const reader = stream.readable.getReader();
160
+ while (true) {
161
+ const { value, done } = await reader.read();
162
+ if (done) break;
163
+ chunks.push(value);
164
+ }
165
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
166
+ const result = new Uint8Array(totalLength);
167
+ let offset = 0;
168
+ for (const chunk of chunks) {
169
+ result.set(chunk, offset);
170
+ offset += chunk.length;
171
+ }
172
+ return result;
173
173
  }
174
+ /**
175
+ * Sends a log entry to New Relic with retry logic.
176
+ * Handles rate limiting, compression, and error cases.
177
+ *
178
+ * @param endpoint - The New Relic API endpoint
179
+ * @param apiKey - The New Relic API key
180
+ * @param payload - The JSON payload to send
181
+ * @param useCompression - Whether to use gzip compression
182
+ * @param maxRetries - Maximum number of retry attempts
183
+ * @param retryDelay - Base delay between retries in milliseconds
184
+ * @param respectRateLimit - Whether to honor rate limiting headers
185
+ * @returns A promise that resolves to the API response
186
+ * @throws {ValidationError} If payload validation fails
187
+ * @throws {RateLimitError} If rate limited and not respecting rate limits
188
+ * @throws {Error} If the request fails after all retries
189
+ */
174
190
  async function sendWithRetry(endpoint, apiKey, payload, useCompression, maxRetries, retryDelay, respectRateLimit = true) {
175
- const payloadBytes = new TextEncoder().encode(payload).length;
176
- if (payloadBytes > MAX_PAYLOAD_SIZE) {
177
- throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);
178
- }
179
- let lastError;
180
- let compressedPayload;
181
- if (useCompression) {
182
- compressedPayload = await compressData(payload);
183
- if (compressedPayload.length > MAX_PAYLOAD_SIZE) {
184
- throw new ValidationError(
185
- `Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes`
186
- );
187
- }
188
- }
189
- const headers = {
190
- "Content-Type": "application/json",
191
- "Api-Key": apiKey
192
- };
193
- if (useCompression) {
194
- headers["Content-Encoding"] = "gzip";
195
- }
196
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
197
- try {
198
- const response = await fetch(endpoint, {
199
- method: "POST",
200
- headers,
201
- body: useCompression ? compressedPayload : payload
202
- });
203
- if (response.status === 429) {
204
- const retryAfter = Number.parseInt(response.headers.get("Retry-After") || "60", 10);
205
- if (respectRateLimit) {
206
- await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
207
- attempt--;
208
- continue;
209
- }
210
- throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter);
211
- }
212
- if (!response.ok) {
213
- throw new Error(`Failed to send logs to New Relic: ${response.statusText}`);
214
- }
215
- return response;
216
- } catch (error) {
217
- lastError = error instanceof Error ? error : new Error(String(error));
218
- if (error instanceof ValidationError) {
219
- throw error;
220
- }
221
- if (!respectRateLimit && error instanceof RateLimitError) {
222
- throw error;
223
- }
224
- if (attempt === maxRetries) {
225
- throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`);
226
- }
227
- if (!(error instanceof RateLimitError)) {
228
- const jitter = Math.random() * 200;
229
- const delay = retryDelay * 2 ** attempt + jitter;
230
- await new Promise((resolve) => setTimeout(resolve, delay));
231
- }
232
- }
233
- }
234
- throw lastError;
191
+ const payloadBytes = new TextEncoder().encode(payload).length;
192
+ if (payloadBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);
193
+ let lastError;
194
+ let compressedPayload;
195
+ if (useCompression) {
196
+ compressedPayload = await compressData(payload);
197
+ if (compressedPayload.length > MAX_PAYLOAD_SIZE) throw new ValidationError(`Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes`);
198
+ }
199
+ const headers = {
200
+ "Content-Type": "application/json",
201
+ "Api-Key": apiKey
202
+ };
203
+ if (useCompression) headers["Content-Encoding"] = "gzip";
204
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
205
+ const response = await fetch(endpoint, {
206
+ method: "POST",
207
+ headers,
208
+ body: useCompression ? compressedPayload : payload
209
+ });
210
+ if (response.status === 429) {
211
+ const retryAfter = Number.parseInt(response.headers.get("Retry-After") || "60", 10);
212
+ if (respectRateLimit) {
213
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
214
+ attempt--;
215
+ continue;
216
+ }
217
+ throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter);
218
+ }
219
+ if (!response.ok) throw new Error(`Failed to send logs to New Relic: ${response.statusText}`);
220
+ return response;
221
+ } catch (error) {
222
+ lastError = error instanceof Error ? error : new Error(String(error));
223
+ if (error instanceof ValidationError) throw error;
224
+ if (!respectRateLimit && error instanceof RateLimitError) throw error;
225
+ if (attempt === maxRetries) throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`);
226
+ if (!(error instanceof RateLimitError)) {
227
+ const jitter = Math.random() * 200;
228
+ const delay = retryDelay * 2 ** attempt + jitter;
229
+ await new Promise((resolve) => setTimeout(resolve, delay));
230
+ }
231
+ }
232
+ throw lastError;
235
233
  }
236
- export {
237
- NewRelicTransport
238
- };
234
+
235
+ //#endregion
236
+ export { NewRelicTransport };
239
237
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/NewRelicTransport.ts"],"sourcesContent":["import type { LoggerlessTransportConfig, LogLayerTransportParams } from \"@loglayer/transport\";\nimport { LoggerlessTransport } from \"@loglayer/transport\";\n\n// Constants defining New Relic's API limits\nconst MAX_PAYLOAD_SIZE = 1_000_000; // 1MB in bytes\nconst MAX_ATTRIBUTES = 255;\nconst MAX_ATTRIBUTE_NAME_LENGTH = 255;\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 4094;\n\n/**\n * Error thrown when log entry validation fails.\n * This includes payload size, attribute count, and attribute name length validations.\n */\nclass ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * Error thrown when New Relic's API rate limit is exceeded.\n * Contains the retry-after duration specified by the API.\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 * Configuration options for the New Relic transport.\n */\nexport interface NewRelicTransportConfig extends LoggerlessTransportConfig {\n /**\n * The New Relic API key\n */\n apiKey: string;\n /**\n * The New Relic Log API endpoint\n * @default https://log-api.newrelic.com/log/v1\n */\n endpoint?: 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 * Whether to use gzip compression\n * @default true\n */\n useCompression?: 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\n/**\n * Validates a log entry against New Relic's constraints.\n * - Checks number of attributes (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n *\n * @param logEntry - The log entry to validate\n * @returns The validated (and potentially modified) log entry\n * @throws {ValidationError} If validation fails\n */\nfunction validateLogEntry(logEntry: Record<string, any>) {\n if (logEntry.attributes) {\n // Check number of attributes\n const attributeCount = Object.keys(logEntry.attributes).length;\n if (attributeCount > MAX_ATTRIBUTES) {\n throw new ValidationError(\n `Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`,\n );\n }\n\n // Check attribute names and values\n for (const [key, value] of Object.entries(logEntry.attributes)) {\n // Check attribute name length\n if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) {\n throw new ValidationError(\n `Attribute name '${key}' exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Length: ${key.length}`,\n );\n }\n\n // Check string value length\n if (typeof value === \"string\" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) {\n // Truncate the string value to the maximum length\n logEntry.attributes[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);\n }\n }\n }\n\n return logEntry;\n}\n\n/**\n * NewRelicTransport is responsible for sending logs to New Relic's Log API.\n * It handles validation, compression, retries, and rate limiting according to New Relic's specifications.\n *\n * Features:\n * - Validates payload size (max 1MB)\n * - Validates number of attributes (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n * - Supports gzip compression\n * - Handles rate limiting with configurable behavior\n * - Implements retry logic with exponential backoff\n */\nexport class NewRelicTransport extends LoggerlessTransport {\n private apiKey: string;\n private endpoint: string;\n private onError?: (err: Error) => void;\n private onDebug?: (entry: Record<string, any>) => void;\n private useCompression: boolean;\n private maxRetries: number;\n private retryDelay: number;\n private respectRateLimit: boolean;\n\n /**\n * Creates a new instance of NewRelicTransport.\n *\n * @param config - Configuration options for the transport\n * @param config.apiKey - New Relic API key for authentication\n * @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)\n * @param config.onError - Optional error callback for handling errors\n * @param config.onDebug - Optional callback for debugging log entries before they are sent\n * @param config.useCompression - Whether to use gzip compression (defaults to true)\n * @param config.maxRetries - Maximum number of retry attempts (defaults to 3)\n * @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)\n * @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)\n */\n constructor(config: NewRelicTransportConfig) {\n super(config);\n\n this.apiKey = config.apiKey;\n this.endpoint = config.endpoint ?? \"https://log-api.newrelic.com/log/v1\";\n this.onError = config.onError;\n this.onDebug = config.onDebug;\n this.useCompression = config.useCompression ?? true;\n this.maxRetries = config.maxRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.respectRateLimit = config.respectRateLimit ?? true;\n }\n\n /**\n * Processes and ships log entries to New Relic.\n *\n * This method:\n * 1. Validates the message size\n * 2. Creates and validates the log entry\n * 3. Validates the final payload size\n * 4. Asynchronously sends the log entry to New Relic\n *\n * The actual sending is done asynchronously in a fire-and-forget manner to maintain\n * compatibility with the base transport class while still providing retry and error handling.\n *\n * @param params - Log parameters including level, messages, and metadata\n * @param params.logLevel - The severity level of the log\n * @param params.messages - Array of message strings to be joined\n * @param params.data - Optional metadata to include with the log\n * @param params.hasData - Whether metadata is present\n * @returns The original messages array\n * @throws {ValidationError} If the payload exceeds size limits or validation fails\n */\n shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[] {\n try {\n // Check message size first\n const message = messages.join(\" \");\n const messageBytes = new TextEncoder().encode(message).length;\n if (messageBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes`,\n );\n }\n\n const logEntry: Record<string, any> = {\n timestamp: Date.now(),\n level: logLevel,\n log: message,\n };\n\n if (data && hasData) {\n Object.assign(logEntry, {\n attributes: data,\n });\n }\n\n const validatedEntry = validateLogEntry(logEntry);\n\n // Call onDebug callback if defined\n if (this.onDebug) {\n this.onDebug(validatedEntry);\n }\n\n // Check final payload size\n const payload = JSON.stringify([validatedEntry]);\n const payloadBytes = new TextEncoder().encode(payload).length;\n if (payloadBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`,\n );\n }\n\n // Fire and forget the async processing\n (async () => {\n try {\n await sendWithRetry(\n this.endpoint,\n this.apiKey,\n payload,\n this.useCompression,\n this.maxRetries,\n this.retryDelay,\n this.respectRateLimit,\n );\n } catch (error) {\n if (this.onError) {\n this.onError(error instanceof Error ? error : new Error(String(error)));\n }\n // Re-throw validation errors to prevent further processing\n if (error instanceof ValidationError) {\n throw error;\n }\n }\n })();\n } catch (error) {\n if (this.onError) {\n this.onError(error instanceof Error ? error : new Error(String(error)));\n }\n }\n\n return messages;\n }\n}\n\n/**\n * Compresses data using gzip compression.\n *\n * @param data - The string data to compress\n * @returns A promise that resolves to the compressed data as a Uint8Array\n */\nasync function compressData(data: string): Promise<Uint8Array> {\n const stream = new CompressionStream(\"gzip\");\n const writer = stream.writable.getWriter();\n const encoder = new TextEncoder();\n const chunks: Uint8Array[] = [];\n\n await writer.write(encoder.encode(data));\n await writer.close();\n\n const reader = stream.readable.getReader();\n\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n // Combine all chunks into a single Uint8Array\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/**\n * Sends a log entry to New Relic with retry logic.\n * Handles rate limiting, compression, and error cases.\n *\n * @param endpoint - The New Relic API endpoint\n * @param apiKey - The New Relic API key\n * @param payload - The JSON payload to send\n * @param useCompression - Whether to use gzip compression\n * @param maxRetries - Maximum number of retry attempts\n * @param retryDelay - Base delay between retries in milliseconds\n * @param respectRateLimit - Whether to honor rate limiting headers\n * @returns A promise that resolves to the API response\n * @throws {ValidationError} If payload validation fails\n * @throws {RateLimitError} If rate limited and not respecting rate limits\n * @throws {Error} If the request fails after all retries\n */\nasync function sendWithRetry(\n endpoint: string,\n apiKey: string,\n payload: string,\n useCompression: boolean,\n maxRetries: number,\n retryDelay: number,\n respectRateLimit = true,\n): Promise<Response> {\n // Check payload size before compression\n const payloadBytes = new TextEncoder().encode(payload).length;\n if (payloadBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);\n }\n\n let lastError: Error;\n let compressedPayload: Uint8Array | undefined;\n\n if (useCompression) {\n compressedPayload = await compressData(payload);\n // Check compressed payload size\n if (compressedPayload.length > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes`,\n );\n }\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"Api-Key\": apiKey,\n };\n\n if (useCompression) {\n headers[\"Content-Encoding\"] = \"gzip\";\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: useCompression ? compressedPayload : payload,\n });\n\n if (response.status === 429) {\n const retryAfter = Number.parseInt(response.headers.get(\"Retry-After\") || \"60\", 10);\n if (respectRateLimit) {\n // Wait for the specified time before retrying\n await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));\n // Don't count rate limit retries against maxRetries\n attempt--;\n continue;\n }\n\n throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter);\n }\n\n if (!response.ok) {\n throw new Error(`Failed to send logs to New Relic: ${response.statusText}`);\n }\n\n return response;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry validation errors\n if (error instanceof ValidationError) {\n throw error;\n }\n\n // If we're not respecting rate limits, don't retry rate limit errors\n if (!respectRateLimit && error instanceof RateLimitError) {\n throw error;\n }\n\n if (attempt === maxRetries) {\n throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`);\n }\n\n // For non-rate-limit errors, use exponential backoff with jitter\n if (!(error instanceof RateLimitError)) {\n const jitter = Math.random() * 200;\n const delay = retryDelay * 2 ** attempt + jitter;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError!;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"index.js","names":["retryAfter: number","logEntry: Record<string, any>","chunks: Uint8Array[]","lastError: Error","compressedPayload: Uint8Array | undefined","headers: Record<string, string>"],"sources":["../src/NewRelicTransport.ts"],"sourcesContent":["import type { LoggerlessTransportConfig, LogLayerTransportParams } from \"@loglayer/transport\";\nimport { LoggerlessTransport } from \"@loglayer/transport\";\n\n// Constants defining New Relic's API limits\nconst MAX_PAYLOAD_SIZE = 1_000_000; // 1MB in bytes\nconst MAX_ATTRIBUTES = 255;\nconst MAX_ATTRIBUTE_NAME_LENGTH = 255;\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 4094;\n\n/**\n * Error thrown when log entry validation fails.\n * This includes payload size, attribute count, and attribute name length validations.\n */\nclass ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * Error thrown when New Relic's API rate limit is exceeded.\n * Contains the retry-after duration specified by the API.\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 * Configuration options for the New Relic transport.\n */\nexport interface NewRelicTransportConfig extends LoggerlessTransportConfig {\n /**\n * The New Relic API key\n */\n apiKey: string;\n /**\n * The New Relic Log API endpoint\n * @default https://log-api.newrelic.com/log/v1\n */\n endpoint?: 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 * Whether to use gzip compression\n * @default true\n */\n useCompression?: 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\n/**\n * Validates a log entry against New Relic's constraints.\n * - Checks number of attributes (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n *\n * @param logEntry - The log entry to validate\n * @returns The validated (and potentially modified) log entry\n * @throws {ValidationError} If validation fails\n */\nfunction validateLogEntry(logEntry: Record<string, any>) {\n if (logEntry.attributes) {\n // Check number of attributes\n const attributeCount = Object.keys(logEntry.attributes).length;\n if (attributeCount > MAX_ATTRIBUTES) {\n throw new ValidationError(\n `Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`,\n );\n }\n\n // Check attribute names and values\n for (const [key, value] of Object.entries(logEntry.attributes)) {\n // Check attribute name length\n if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) {\n throw new ValidationError(\n `Attribute name '${key}' exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Length: ${key.length}`,\n );\n }\n\n // Check string value length\n if (typeof value === \"string\" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) {\n // Truncate the string value to the maximum length\n logEntry.attributes[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);\n }\n }\n }\n\n return logEntry;\n}\n\n/**\n * NewRelicTransport is responsible for sending logs to New Relic's Log API.\n * It handles validation, compression, retries, and rate limiting according to New Relic's specifications.\n *\n * Features:\n * - Validates payload size (max 1MB)\n * - Validates number of attributes (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n * - Supports gzip compression\n * - Handles rate limiting with configurable behavior\n * - Implements retry logic with exponential backoff\n */\nexport class NewRelicTransport extends LoggerlessTransport {\n private apiKey: string;\n private endpoint: string;\n private onError?: (err: Error) => void;\n private onDebug?: (entry: Record<string, any>) => void;\n private useCompression: boolean;\n private maxRetries: number;\n private retryDelay: number;\n private respectRateLimit: boolean;\n\n /**\n * Creates a new instance of NewRelicTransport.\n *\n * @param config - Configuration options for the transport\n * @param config.apiKey - New Relic API key for authentication\n * @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)\n * @param config.onError - Optional error callback for handling errors\n * @param config.onDebug - Optional callback for debugging log entries before they are sent\n * @param config.useCompression - Whether to use gzip compression (defaults to true)\n * @param config.maxRetries - Maximum number of retry attempts (defaults to 3)\n * @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)\n * @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)\n */\n constructor(config: NewRelicTransportConfig) {\n super(config);\n\n this.apiKey = config.apiKey;\n this.endpoint = config.endpoint ?? \"https://log-api.newrelic.com/log/v1\";\n this.onError = config.onError;\n this.onDebug = config.onDebug;\n this.useCompression = config.useCompression ?? true;\n this.maxRetries = config.maxRetries ?? 3;\n this.retryDelay = config.retryDelay ?? 1000;\n this.respectRateLimit = config.respectRateLimit ?? true;\n }\n\n /**\n * Processes and ships log entries to New Relic.\n *\n * This method:\n * 1. Validates the message size\n * 2. Creates and validates the log entry\n * 3. Validates the final payload size\n * 4. Asynchronously sends the log entry to New Relic\n *\n * The actual sending is done asynchronously in a fire-and-forget manner to maintain\n * compatibility with the base transport class while still providing retry and error handling.\n *\n * @param params - Log parameters including level, messages, and metadata\n * @param params.logLevel - The severity level of the log\n * @param params.messages - Array of message strings to be joined\n * @param params.data - Optional metadata to include with the log\n * @param params.hasData - Whether metadata is present\n * @returns The original messages array\n * @throws {ValidationError} If the payload exceeds size limits or validation fails\n */\n shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[] {\n try {\n // Check message size first\n const message = messages.join(\" \");\n const messageBytes = new TextEncoder().encode(message).length;\n if (messageBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes`,\n );\n }\n\n const logEntry: Record<string, any> = {\n timestamp: Date.now(),\n level: logLevel,\n log: message,\n };\n\n if (data && hasData) {\n Object.assign(logEntry, {\n attributes: data,\n });\n }\n\n const validatedEntry = validateLogEntry(logEntry);\n\n // Call onDebug callback if defined\n if (this.onDebug) {\n this.onDebug(validatedEntry);\n }\n\n // Check final payload size\n const payload = JSON.stringify([validatedEntry]);\n const payloadBytes = new TextEncoder().encode(payload).length;\n if (payloadBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`,\n );\n }\n\n // Fire and forget the async processing\n (async () => {\n try {\n await sendWithRetry(\n this.endpoint,\n this.apiKey,\n payload,\n this.useCompression,\n this.maxRetries,\n this.retryDelay,\n this.respectRateLimit,\n );\n } catch (error) {\n if (this.onError) {\n this.onError(error instanceof Error ? error : new Error(String(error)));\n }\n // Re-throw validation errors to prevent further processing\n if (error instanceof ValidationError) {\n throw error;\n }\n }\n })();\n } catch (error) {\n if (this.onError) {\n this.onError(error instanceof Error ? error : new Error(String(error)));\n }\n }\n\n return messages;\n }\n}\n\n/**\n * Compresses data using gzip compression.\n *\n * @param data - The string data to compress\n * @returns A promise that resolves to the compressed data as a Uint8Array\n */\nasync function compressData(data: string): Promise<Uint8Array> {\n const stream = new CompressionStream(\"gzip\");\n const writer = stream.writable.getWriter();\n const encoder = new TextEncoder();\n const chunks: Uint8Array[] = [];\n\n await writer.write(encoder.encode(data));\n await writer.close();\n\n const reader = stream.readable.getReader();\n\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n // Combine all chunks into a single Uint8Array\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/**\n * Sends a log entry to New Relic with retry logic.\n * Handles rate limiting, compression, and error cases.\n *\n * @param endpoint - The New Relic API endpoint\n * @param apiKey - The New Relic API key\n * @param payload - The JSON payload to send\n * @param useCompression - Whether to use gzip compression\n * @param maxRetries - Maximum number of retry attempts\n * @param retryDelay - Base delay between retries in milliseconds\n * @param respectRateLimit - Whether to honor rate limiting headers\n * @returns A promise that resolves to the API response\n * @throws {ValidationError} If payload validation fails\n * @throws {RateLimitError} If rate limited and not respecting rate limits\n * @throws {Error} If the request fails after all retries\n */\nasync function sendWithRetry(\n endpoint: string,\n apiKey: string,\n payload: string,\n useCompression: boolean,\n maxRetries: number,\n retryDelay: number,\n respectRateLimit = true,\n): Promise<Response> {\n // Check payload size before compression\n const payloadBytes = new TextEncoder().encode(payload).length;\n if (payloadBytes > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);\n }\n\n let lastError: Error;\n let compressedPayload: Uint8Array | undefined;\n\n if (useCompression) {\n compressedPayload = await compressData(payload);\n // Check compressed payload size\n if (compressedPayload.length > MAX_PAYLOAD_SIZE) {\n throw new ValidationError(\n `Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes`,\n );\n }\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"Api-Key\": apiKey,\n };\n\n if (useCompression) {\n headers[\"Content-Encoding\"] = \"gzip\";\n }\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: useCompression ? compressedPayload : payload,\n });\n\n if (response.status === 429) {\n const retryAfter = Number.parseInt(response.headers.get(\"Retry-After\") || \"60\", 10);\n if (respectRateLimit) {\n // Wait for the specified time before retrying\n await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));\n // Don't count rate limit retries against maxRetries\n attempt--;\n continue;\n }\n\n throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter);\n }\n\n if (!response.ok) {\n throw new Error(`Failed to send logs to New Relic: ${response.statusText}`);\n }\n\n return response;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry validation errors\n if (error instanceof ValidationError) {\n throw error;\n }\n\n // If we're not respecting rate limits, don't retry rate limit errors\n if (!respectRateLimit && error instanceof RateLimitError) {\n throw error;\n }\n\n if (attempt === maxRetries) {\n throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`);\n }\n\n // For non-rate-limit errors, use exponential backoff with jitter\n if (!(error instanceof RateLimitError)) {\n const jitter = Math.random() * 200;\n const delay = retryDelay * 2 ** attempt + jitter;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw lastError!;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loglayer/transport-new-relic",
3
3
  "description": "New Relic transport for the LogLayer logging library.",
4
- "version": "2.1.6",
4
+ "version": "2.1.8",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -33,18 +33,18 @@
33
33
  "transport"
34
34
  ],
35
35
  "dependencies": {
36
- "@loglayer/transport": "2.3.3"
36
+ "@loglayer/transport": "2.3.5"
37
37
  },
38
38
  "devDependencies": {
39
+ "@types/node": "24.9.2",
39
40
  "dotenv": "17.2.3",
40
- "@types/node": "24.6.1",
41
41
  "serialize-error": "12.0.0",
42
+ "tsdown": "0.15.12",
42
43
  "tsx": "4.20.6",
43
- "tsup": "8.5.0",
44
44
  "typescript": "5.9.3",
45
- "vitest": "3.2.4",
46
- "loglayer": "6.8.2",
47
- "@internal/tsconfig": "2.1.0"
45
+ "vitest": "4.0.6",
46
+ "@internal/tsconfig": "2.1.0",
47
+ "loglayer": "7.1.0"
48
48
  },
49
49
  "bugs": "https://github.com/loglayer/loglayer/issues",
50
50
  "engines": {
@@ -55,7 +55,7 @@
55
55
  ],
56
56
  "homepage": "https://loglayer.dev",
57
57
  "scripts": {
58
- "build": "tsup src/index.ts",
58
+ "build": "tsdown src/index.ts",
59
59
  "test": "vitest --run",
60
60
  "clean": "rm -rf .turbo node_modules dist",
61
61
  "lint": "biome check --no-errors-on-unmatched --write --unsafe src",