@loglayer/transport-new-relic 3.0.2 → 4.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 +1 -1
- package/README.md +11 -12
- package/dist/index.cjs +71 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -79
- package/dist/index.d.mts +28 -79
- package/dist/index.mjs +53 -195
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Theo Gravity / [Disaresta](https://disaresta.com)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
The New Relic transport for the [LogLayer](https://loglayer.dev) logging library.
|
|
8
8
|
|
|
9
|
-
Ships logs to New Relic using their [Log API](https://docs.newrelic.com/docs/logs/log-api/introduction-log-api/). Features include:
|
|
9
|
+
Ships logs to New Relic using their [Log API](https://docs.newrelic.com/docs/logs/log-api/introduction-log-api/). Compatible with Node.js, Bun, and Deno. Features include:
|
|
10
10
|
- Automatic gzip compression (configurable)
|
|
11
11
|
- Retry mechanism with exponential backoff
|
|
12
|
-
- Rate limiting support
|
|
12
|
+
- Rate limiting support
|
|
13
|
+
- Batch sending with configurable size and timeout
|
|
13
14
|
- Validation of New Relic's API constraints
|
|
14
|
-
- Error handling
|
|
15
|
-
- Configurable endpoints for different regions
|
|
15
|
+
- Error handling and debug callbacks
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -32,12 +32,12 @@ const log = new LogLayer({
|
|
|
32
32
|
transport: new NewRelicTransport({
|
|
33
33
|
apiKey: "YOUR_NEW_RELIC_API_KEY",
|
|
34
34
|
endpoint: "https://log-api.newrelic.com/log/v1", // optional, this is the default
|
|
35
|
-
|
|
35
|
+
compression: true, // optional, defaults to true
|
|
36
36
|
maxRetries: 3, // optional, defaults to 3
|
|
37
37
|
retryDelay: 1000, // optional, base delay in ms, defaults to 1000
|
|
38
38
|
respectRateLimit: true, // optional, defaults to true
|
|
39
39
|
onError: (err) => {
|
|
40
|
-
console.error('Failed to send logs
|
|
40
|
+
console.error('Failed to send logs:', err);
|
|
41
41
|
},
|
|
42
42
|
onDebug: (entry) => {
|
|
43
43
|
console.log('Log entry being sent:', entry);
|
|
@@ -64,7 +64,7 @@ interface NewRelicTransportConfig {
|
|
|
64
64
|
apiKey: string;
|
|
65
65
|
/**
|
|
66
66
|
* The New Relic Log API endpoint
|
|
67
|
-
* @default https://log-api.newrelic.com/log/v1
|
|
67
|
+
* @default "https://log-api.newrelic.com/log/v1"
|
|
68
68
|
*/
|
|
69
69
|
endpoint?: string;
|
|
70
70
|
/**
|
|
@@ -73,22 +73,21 @@ interface NewRelicTransportConfig {
|
|
|
73
73
|
onError?: (err: Error) => void;
|
|
74
74
|
/**
|
|
75
75
|
* Optional callback for debugging log entries
|
|
76
|
-
* Called with the
|
|
76
|
+
* Called with the entry before it is sent
|
|
77
77
|
*/
|
|
78
78
|
onDebug?: (entry: Record<string, any>) => void;
|
|
79
79
|
/**
|
|
80
80
|
* Whether to use gzip compression
|
|
81
81
|
* @default true
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
compression?: boolean;
|
|
84
84
|
/**
|
|
85
85
|
* Number of retry attempts before giving up
|
|
86
86
|
* @default 3
|
|
87
87
|
*/
|
|
88
88
|
maxRetries?: number;
|
|
89
89
|
/**
|
|
90
|
-
* Base delay between retries in milliseconds
|
|
91
|
-
* The actual delay will use exponential backoff with jitter.
|
|
90
|
+
* Base delay between retries in milliseconds
|
|
92
91
|
* @default 1000
|
|
93
92
|
*/
|
|
94
93
|
retryDelay?: number;
|
|
@@ -102,4 +101,4 @@ interface NewRelicTransportConfig {
|
|
|
102
101
|
|
|
103
102
|
## Documentation
|
|
104
103
|
|
|
105
|
-
For more details, visit [https://loglayer.dev/transports/new-relic](https://loglayer.dev/transports/new-relic)
|
|
104
|
+
For more details, visit [https://loglayer.dev/transports/new-relic](https://loglayer.dev/transports/new-relic)
|
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
let
|
|
2
|
+
let _loglayer_transport_http = require("@loglayer/transport-http");
|
|
3
3
|
|
|
4
4
|
//#region src/NewRelicTransport.ts
|
|
5
|
-
const MAX_PAYLOAD_SIZE = 1e6;
|
|
6
5
|
const MAX_ATTRIBUTES = 255;
|
|
7
6
|
const MAX_ATTRIBUTE_NAME_LENGTH = 255;
|
|
8
7
|
const MAX_ATTRIBUTE_VALUE_LENGTH = 4094;
|
|
9
8
|
/**
|
|
10
9
|
* Error thrown when log entry validation fails.
|
|
11
|
-
* This includes
|
|
10
|
+
* This includes attribute count, attribute name length, and other New Relic API validations.
|
|
12
11
|
*/
|
|
13
12
|
var ValidationError = class extends Error {
|
|
14
13
|
constructor(message) {
|
|
@@ -17,222 +16,100 @@ var ValidationError = class extends Error {
|
|
|
17
16
|
}
|
|
18
17
|
};
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
* Contains the retry-after duration specified by the API.
|
|
22
|
-
*/
|
|
23
|
-
var RateLimitError = class extends Error {
|
|
24
|
-
constructor(message, retryAfter) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.retryAfter = retryAfter;
|
|
27
|
-
this.name = "RateLimitError";
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Validates a log entry against New Relic's constraints.
|
|
19
|
+
* Validates attributes against New Relic's constraints.
|
|
32
20
|
* - Checks number of attributes (max 255)
|
|
33
21
|
* - Validates attribute name length (max 255 characters)
|
|
34
22
|
* - Truncates attribute values longer than 4094 characters
|
|
35
23
|
*
|
|
36
|
-
* @param
|
|
37
|
-
* @returns The validated (and potentially modified)
|
|
24
|
+
* @param attributes - The attributes to validate
|
|
25
|
+
* @returns The validated (and potentially modified) attributes
|
|
38
26
|
* @throws {ValidationError} If validation fails
|
|
39
27
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
function validateAttributes(attributes) {
|
|
29
|
+
const validated = {};
|
|
30
|
+
const attributeCount = Object.keys(attributes).length;
|
|
31
|
+
if (attributeCount > MAX_ATTRIBUTES) throw new ValidationError(`Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`);
|
|
32
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
33
|
+
if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) throw new ValidationError(`Attribute name exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Found: ${key.length}`);
|
|
34
|
+
if (typeof value === "string" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) validated[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);
|
|
35
|
+
else validated[key] = value;
|
|
48
36
|
}
|
|
49
|
-
return
|
|
37
|
+
return validated;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Default payload template that formats log data for New Relic's Log API.
|
|
41
|
+
* Each entry includes timestamp, level, the log message, and optional attributes.
|
|
42
|
+
*/
|
|
43
|
+
function defaultNewRelicPayload(params) {
|
|
44
|
+
const logEntry = {
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
level: params.logLevel,
|
|
47
|
+
log: params.message
|
|
48
|
+
};
|
|
49
|
+
if (params.data) logEntry.attributes = validateAttributes(params.data);
|
|
50
|
+
return JSON.stringify(logEntry);
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
53
|
-
* It
|
|
54
|
+
* It extends HttpTransport with New Relic-specific configuration, validation,
|
|
55
|
+
* and formatting.
|
|
54
56
|
*
|
|
55
57
|
* Features:
|
|
56
|
-
* - Validates
|
|
57
|
-
* - Validates number of attributes (max 255)
|
|
58
|
+
* - Validates attribute count (max 255)
|
|
58
59
|
* - Validates attribute name length (max 255 characters)
|
|
59
60
|
* - Truncates attribute values longer than 4094 characters
|
|
60
|
-
* -
|
|
61
|
-
* -
|
|
62
|
-
* -
|
|
61
|
+
* - Gzip compression by default (via HttpTransport)
|
|
62
|
+
* - Retry logic with exponential backoff (via HttpTransport)
|
|
63
|
+
* - Rate limiting support (via HttpTransport)
|
|
64
|
+
* - Batch sending support (via HttpTransport)
|
|
65
|
+
* - Error and debug callbacks
|
|
63
66
|
*/
|
|
64
|
-
var NewRelicTransport = class extends
|
|
65
|
-
apiKey;
|
|
66
|
-
endpoint;
|
|
67
|
-
onError;
|
|
68
|
-
onDebug;
|
|
69
|
-
useCompression;
|
|
70
|
-
maxRetries;
|
|
71
|
-
retryDelay;
|
|
72
|
-
respectRateLimit;
|
|
67
|
+
var NewRelicTransport = class extends _loglayer_transport_http.HttpTransport {
|
|
73
68
|
/**
|
|
74
69
|
* Creates a new instance of NewRelicTransport.
|
|
75
70
|
*
|
|
76
71
|
* @param config - Configuration options for the transport
|
|
77
|
-
* @param config.apiKey - New Relic API key for authentication
|
|
78
|
-
* @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
|
|
79
|
-
* @param config.onError - Optional error callback for handling errors
|
|
80
|
-
* @param config.onDebug - Optional callback for debugging log entries before they are sent
|
|
81
|
-
* @param config.useCompression - Whether to use gzip compression (defaults to true)
|
|
82
|
-
* @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
|
|
83
|
-
* @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
|
|
84
|
-
* @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
|
|
85
72
|
*/
|
|
86
73
|
constructor(config) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.onDebug = config.onDebug;
|
|
92
|
-
this.useCompression = config.useCompression ?? true;
|
|
93
|
-
this.maxRetries = config.maxRetries ?? 3;
|
|
94
|
-
this.retryDelay = config.retryDelay ?? 1e3;
|
|
95
|
-
this.respectRateLimit = config.respectRateLimit ?? true;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Processes and ships log entries to New Relic.
|
|
99
|
-
*
|
|
100
|
-
* This method:
|
|
101
|
-
* 1. Validates the message size
|
|
102
|
-
* 2. Creates and validates the log entry
|
|
103
|
-
* 3. Validates the final payload size
|
|
104
|
-
* 4. Asynchronously sends the log entry to New Relic
|
|
105
|
-
*
|
|
106
|
-
* The actual sending is done asynchronously in a fire-and-forget manner to maintain
|
|
107
|
-
* compatibility with the base transport class while still providing retry and error handling.
|
|
108
|
-
*
|
|
109
|
-
* @param params - Log parameters including level, messages, and metadata
|
|
110
|
-
* @param params.logLevel - The severity level of the log
|
|
111
|
-
* @param params.messages - Array of message strings to be joined
|
|
112
|
-
* @param params.data - Optional metadata to include with the log
|
|
113
|
-
* @param params.hasData - Whether metadata is present
|
|
114
|
-
* @returns The original messages array
|
|
115
|
-
* @throws {ValidationError} If the payload exceeds size limits or validation fails
|
|
116
|
-
*/
|
|
117
|
-
shipToLogger({ logLevel, messages, data, hasData }) {
|
|
118
|
-
try {
|
|
119
|
-
const message = messages.join(" ");
|
|
120
|
-
const messageBytes = new TextEncoder().encode(message).length;
|
|
121
|
-
if (messageBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes`);
|
|
122
|
-
const logEntry = {
|
|
123
|
-
timestamp: Date.now(),
|
|
124
|
-
level: logLevel,
|
|
125
|
-
log: message
|
|
126
|
-
};
|
|
127
|
-
if (data && hasData) Object.assign(logEntry, { attributes: data });
|
|
128
|
-
const validatedEntry = validateLogEntry(logEntry);
|
|
129
|
-
if (this.onDebug) this.onDebug(validatedEntry);
|
|
130
|
-
const payload = JSON.stringify([validatedEntry]);
|
|
131
|
-
const payloadBytes = new TextEncoder().encode(payload).length;
|
|
132
|
-
if (payloadBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);
|
|
133
|
-
(async () => {
|
|
134
|
-
try {
|
|
135
|
-
await sendWithRetry(this.endpoint, this.apiKey, payload, this.useCompression, this.maxRetries, this.retryDelay, this.respectRateLimit);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (this.onError) this.onError(error instanceof Error ? error : new Error(String(error)));
|
|
138
|
-
if (error instanceof ValidationError) throw error;
|
|
139
|
-
}
|
|
140
|
-
})();
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (this.onError) this.onError(error instanceof Error ? error : new Error(String(error)));
|
|
143
|
-
}
|
|
144
|
-
return messages;
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* Compresses data using gzip compression.
|
|
149
|
-
*
|
|
150
|
-
* @param data - The string data to compress
|
|
151
|
-
* @returns A promise that resolves to the compressed data as a Uint8Array
|
|
152
|
-
*/
|
|
153
|
-
async function compressData(data) {
|
|
154
|
-
const stream = new CompressionStream("gzip");
|
|
155
|
-
const writer = stream.writable.getWriter();
|
|
156
|
-
const encoder = new TextEncoder();
|
|
157
|
-
const chunks = [];
|
|
158
|
-
await writer.write(encoder.encode(data));
|
|
159
|
-
await writer.close();
|
|
160
|
-
const reader = stream.readable.getReader();
|
|
161
|
-
while (true) {
|
|
162
|
-
const { value, done } = await reader.read();
|
|
163
|
-
if (done) break;
|
|
164
|
-
chunks.push(value);
|
|
165
|
-
}
|
|
166
|
-
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
167
|
-
const result = new Uint8Array(totalLength);
|
|
168
|
-
let offset = 0;
|
|
169
|
-
for (const chunk of chunks) {
|
|
170
|
-
result.set(chunk, offset);
|
|
171
|
-
offset += chunk.length;
|
|
172
|
-
}
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Sends a log entry to New Relic with retry logic.
|
|
177
|
-
* Handles rate limiting, compression, and error cases.
|
|
178
|
-
*
|
|
179
|
-
* @param endpoint - The New Relic API endpoint
|
|
180
|
-
* @param apiKey - The New Relic API key
|
|
181
|
-
* @param payload - The JSON payload to send
|
|
182
|
-
* @param useCompression - Whether to use gzip compression
|
|
183
|
-
* @param maxRetries - Maximum number of retry attempts
|
|
184
|
-
* @param retryDelay - Base delay between retries in milliseconds
|
|
185
|
-
* @param respectRateLimit - Whether to honor rate limiting headers
|
|
186
|
-
* @returns A promise that resolves to the API response
|
|
187
|
-
* @throws {ValidationError} If payload validation fails
|
|
188
|
-
* @throws {RateLimitError} If rate limited and not respecting rate limits
|
|
189
|
-
* @throws {Error} If the request fails after all retries
|
|
190
|
-
*/
|
|
191
|
-
async function sendWithRetry(endpoint, apiKey, payload, useCompression, maxRetries, retryDelay, respectRateLimit = true) {
|
|
192
|
-
const payloadBytes = new TextEncoder().encode(payload).length;
|
|
193
|
-
if (payloadBytes > MAX_PAYLOAD_SIZE) throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`);
|
|
194
|
-
let lastError;
|
|
195
|
-
let compressedPayload;
|
|
196
|
-
if (useCompression) {
|
|
197
|
-
compressedPayload = await compressData(payload);
|
|
198
|
-
if (compressedPayload.length > MAX_PAYLOAD_SIZE) throw new ValidationError(`Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes`);
|
|
199
|
-
}
|
|
200
|
-
const headers = {
|
|
201
|
-
"Content-Type": "application/json",
|
|
202
|
-
"Api-Key": apiKey
|
|
203
|
-
};
|
|
204
|
-
if (useCompression) headers["Content-Encoding"] = "gzip";
|
|
205
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
206
|
-
const response = await fetch(endpoint, {
|
|
74
|
+
const { apiKey, endpoint, payloadTemplate, ...httpConfig } = config;
|
|
75
|
+
super({
|
|
76
|
+
...httpConfig,
|
|
77
|
+
url: endpoint ?? "https://log-api.newrelic.com/log/v1",
|
|
207
78
|
method: "POST",
|
|
208
|
-
headers
|
|
209
|
-
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Api-Key": apiKey
|
|
82
|
+
},
|
|
83
|
+
payloadTemplate: payloadTemplate ?? defaultNewRelicPayload,
|
|
84
|
+
compression: httpConfig.compression ?? true,
|
|
85
|
+
maxRetries: httpConfig.maxRetries ?? 3,
|
|
86
|
+
retryDelay: httpConfig.retryDelay ?? 1e3,
|
|
87
|
+
respectRateLimit: httpConfig.respectRateLimit ?? true,
|
|
88
|
+
maxLogSize: httpConfig.maxLogSize ?? 1048576,
|
|
89
|
+
maxPayloadSize: httpConfig.maxPayloadSize ?? 1048576
|
|
210
90
|
});
|
|
211
|
-
if (response.status === 429) {
|
|
212
|
-
const retryAfter = Number.parseInt(response.headers.get("Retry-After") || "60", 10);
|
|
213
|
-
if (respectRateLimit) {
|
|
214
|
-
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
|
|
215
|
-
attempt--;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter);
|
|
219
|
-
}
|
|
220
|
-
if (!response.ok) throw new Error(`Failed to send logs to New Relic: ${response.statusText}`);
|
|
221
|
-
return response;
|
|
222
|
-
} catch (error) {
|
|
223
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
224
|
-
if (error instanceof ValidationError) throw error;
|
|
225
|
-
if (!respectRateLimit && error instanceof RateLimitError) throw error;
|
|
226
|
-
if (attempt === maxRetries) throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`);
|
|
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
91
|
}
|
|
233
|
-
|
|
234
|
-
}
|
|
92
|
+
};
|
|
235
93
|
|
|
236
94
|
//#endregion
|
|
95
|
+
Object.defineProperty(exports, 'HttpTransportError', {
|
|
96
|
+
enumerable: true,
|
|
97
|
+
get: function () {
|
|
98
|
+
return _loglayer_transport_http.HttpTransportError;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
Object.defineProperty(exports, 'LogSizeError', {
|
|
102
|
+
enumerable: true,
|
|
103
|
+
get: function () {
|
|
104
|
+
return _loglayer_transport_http.LogSizeError;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
237
107
|
exports.NewRelicTransport = NewRelicTransport;
|
|
108
|
+
Object.defineProperty(exports, 'RateLimitError', {
|
|
109
|
+
enumerable: true,
|
|
110
|
+
get: function () {
|
|
111
|
+
return _loglayer_transport_http.RateLimitError;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
exports.ValidationError = ValidationError;
|
|
238
115
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["LoggerlessTransport"],"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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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["HttpTransport"],"sources":["../src/NewRelicTransport.ts"],"sourcesContent":["import type { HttpTransportConfig } from \"@loglayer/transport-http\";\nimport { HttpTransport } from \"@loglayer/transport-http\";\n\n// Constants defining New Relic's API limits\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 attribute count, attribute name length, and other New Relic API validations.\n */\nexport class ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * Configuration options for the New Relic transport.\n * Extends HttpTransportConfig with New Relic-specific options.\n */\nexport interface NewRelicTransportConfig extends Omit<HttpTransportConfig, \"url\" | \"headers\" | \"payloadTemplate\"> {\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 * Custom payload template function (optional, defaults to New Relic format).\n * Receives log level, message, and optional data (metadata).\n */\n payloadTemplate?: (params: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n}\n\n/**\n * Validates attributes 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 attributes - The attributes to validate\n * @returns The validated (and potentially modified) attributes\n * @throws {ValidationError} If validation fails\n */\nfunction validateAttributes(attributes: Record<string, any>): Record<string, any> {\n const validated: Record<string, any> = {};\n\n // Check number of attributes\n const attributeCount = Object.keys(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(attributes)) {\n // Check attribute name length\n if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) {\n throw new ValidationError(\n `Attribute name exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Found: ${key.length}`,\n );\n }\n\n // Truncate string values that are too long\n if (typeof value === \"string\" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) {\n validated[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);\n } else {\n validated[key] = value;\n }\n }\n\n return validated;\n}\n\n/**\n * Default payload template that formats log data for New Relic's Log API.\n * Each entry includes timestamp, level, the log message, and optional attributes.\n */\nfunction defaultNewRelicPayload(params: { logLevel: string; message: string; data?: Record<string, any> }): string {\n const logEntry: Record<string, any> = {\n timestamp: Date.now(),\n level: params.logLevel,\n log: params.message,\n };\n\n if (params.data) {\n logEntry.attributes = validateAttributes(params.data);\n }\n\n return JSON.stringify(logEntry);\n}\n\n/**\n * NewRelicTransport is responsible for sending logs to New Relic's Log API.\n * It extends HttpTransport with New Relic-specific configuration, validation,\n * and formatting.\n *\n * Features:\n * - Validates attribute count (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n * - Gzip compression by default (via HttpTransport)\n * - Retry logic with exponential backoff (via HttpTransport)\n * - Rate limiting support (via HttpTransport)\n * - Batch sending support (via HttpTransport)\n * - Error and debug callbacks\n */\nexport class NewRelicTransport extends HttpTransport {\n /**\n * Creates a new instance of NewRelicTransport.\n *\n * @param config - Configuration options for the transport\n */\n constructor(config: NewRelicTransportConfig) {\n const { apiKey, endpoint, payloadTemplate, ...httpConfig } = config;\n\n super({\n ...httpConfig,\n url: endpoint ?? \"https://log-api.newrelic.com/log/v1\",\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Api-Key\": apiKey,\n },\n payloadTemplate: payloadTemplate ?? defaultNewRelicPayload,\n // Defaults (only apply if not provided)\n compression: httpConfig.compression ?? true,\n maxRetries: httpConfig.maxRetries ?? 3,\n retryDelay: httpConfig.retryDelay ?? 1000,\n respectRateLimit: httpConfig.respectRateLimit ?? true,\n maxLogSize: httpConfig.maxLogSize ?? 1_048_576,\n maxPayloadSize: httpConfig.maxPayloadSize ?? 1_048_576,\n } as HttpTransportConfig);\n }\n}\n"],"mappings":";;;;AAIA,MAAM,iBAAiB;AACvB,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;;;;;AAMnC,IAAa,kBAAb,cAAqC,MAAM;CACzC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;;;;;;;AAmChB,SAAS,mBAAmB,YAAsD;CAChF,MAAM,YAAiC,EAAE;CAGzC,MAAM,iBAAiB,OAAO,KAAK,WAAW,CAAC;AAC/C,KAAI,iBAAiB,eACnB,OAAM,IAAI,gBACR,mDAAmD,eAAe,YAAY,iBAC/E;AAIH,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;AAErD,MAAI,IAAI,SAAS,0BACf,OAAM,IAAI,gBACR,0CAA0C,0BAA0B,YAAY,IAAI,SACrF;AAIH,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,2BAC9C,WAAU,OAAO,MAAM,MAAM,GAAG,2BAA2B;MAE3D,WAAU,OAAO;;AAIrB,QAAO;;;;;;AAOT,SAAS,uBAAuB,QAAmF;CACjH,MAAM,WAAgC;EACpC,WAAW,KAAK,KAAK;EACrB,OAAO,OAAO;EACd,KAAK,OAAO;EACb;AAED,KAAI,OAAO,KACT,UAAS,aAAa,mBAAmB,OAAO,KAAK;AAGvD,QAAO,KAAK,UAAU,SAAS;;;;;;;;;;;;;;;;;AAkBjC,IAAa,oBAAb,cAAuCA,uCAAc;;;;;;CAMnD,YAAY,QAAiC;EAC3C,MAAM,EAAE,QAAQ,UAAU,iBAAiB,GAAG,eAAe;AAE7D,QAAM;GACJ,GAAG;GACH,KAAK,YAAY;GACjB,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,WAAW;IACZ;GACD,iBAAiB,mBAAmB;GAEpC,aAAa,WAAW,eAAe;GACvC,YAAY,WAAW,cAAc;GACrC,YAAY,WAAW,cAAc;GACrC,kBAAkB,WAAW,oBAAoB;GACjD,YAAY,WAAW,cAAc;GACrC,gBAAgB,WAAW,kBAAkB;GAC9C,CAAwB"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,111 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpTransport, HttpTransportConfig, HttpTransportError, LogSizeError, RateLimitError } from "@loglayer/transport-http";
|
|
2
2
|
|
|
3
3
|
//#region src/NewRelicTransport.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when log entry validation fails.
|
|
6
|
+
* This includes attribute count, attribute name length, and other New Relic API validations.
|
|
7
|
+
*/
|
|
8
|
+
declare class ValidationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
4
11
|
/**
|
|
5
12
|
* Configuration options for the New Relic transport.
|
|
13
|
+
* Extends HttpTransportConfig with New Relic-specific options.
|
|
6
14
|
*/
|
|
7
|
-
interface NewRelicTransportConfig extends
|
|
15
|
+
interface NewRelicTransportConfig extends Omit<HttpTransportConfig, "url" | "headers" | "payloadTemplate"> {
|
|
8
16
|
/**
|
|
9
17
|
* The New Relic API key
|
|
10
18
|
*/
|
|
11
19
|
apiKey: string;
|
|
12
20
|
/**
|
|
13
21
|
* The New Relic Log API endpoint
|
|
14
|
-
* @default https://log-api.newrelic.com/log/v1
|
|
22
|
+
* @default "https://log-api.newrelic.com/log/v1"
|
|
15
23
|
*/
|
|
16
24
|
endpoint?: string;
|
|
17
25
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
onError?: (err: Error) => void;
|
|
21
|
-
/**
|
|
22
|
-
* Optional callback for debugging log entries before they are sent
|
|
23
|
-
*/
|
|
24
|
-
onDebug?: (entry: Record<string, any>) => void;
|
|
25
|
-
/**
|
|
26
|
-
* Whether to use gzip compression
|
|
27
|
-
* @default true
|
|
28
|
-
*/
|
|
29
|
-
useCompression?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Number of retry attempts before giving up
|
|
32
|
-
* @default 3
|
|
26
|
+
* Custom payload template function (optional, defaults to New Relic format).
|
|
27
|
+
* Receives log level, message, and optional data (metadata).
|
|
33
28
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
retryDelay?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
42
|
-
* @default true
|
|
43
|
-
*/
|
|
44
|
-
respectRateLimit?: boolean;
|
|
29
|
+
payloadTemplate?: (params: {
|
|
30
|
+
logLevel: string;
|
|
31
|
+
message: string;
|
|
32
|
+
data?: Record<string, any>;
|
|
33
|
+
}) => string;
|
|
45
34
|
}
|
|
46
35
|
/**
|
|
47
36
|
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
48
|
-
* It
|
|
37
|
+
* It extends HttpTransport with New Relic-specific configuration, validation,
|
|
38
|
+
* and formatting.
|
|
49
39
|
*
|
|
50
40
|
* Features:
|
|
51
|
-
* - Validates
|
|
52
|
-
* - Validates number of attributes (max 255)
|
|
41
|
+
* - Validates attribute count (max 255)
|
|
53
42
|
* - Validates attribute name length (max 255 characters)
|
|
54
43
|
* - Truncates attribute values longer than 4094 characters
|
|
55
|
-
* -
|
|
56
|
-
* -
|
|
57
|
-
* -
|
|
44
|
+
* - Gzip compression by default (via HttpTransport)
|
|
45
|
+
* - Retry logic with exponential backoff (via HttpTransport)
|
|
46
|
+
* - Rate limiting support (via HttpTransport)
|
|
47
|
+
* - Batch sending support (via HttpTransport)
|
|
48
|
+
* - Error and debug callbacks
|
|
58
49
|
*/
|
|
59
|
-
declare class NewRelicTransport extends
|
|
60
|
-
private apiKey;
|
|
61
|
-
private endpoint;
|
|
62
|
-
private onError?;
|
|
63
|
-
private onDebug?;
|
|
64
|
-
private useCompression;
|
|
65
|
-
private maxRetries;
|
|
66
|
-
private retryDelay;
|
|
67
|
-
private respectRateLimit;
|
|
50
|
+
declare class NewRelicTransport extends HttpTransport {
|
|
68
51
|
/**
|
|
69
52
|
* Creates a new instance of NewRelicTransport.
|
|
70
53
|
*
|
|
71
54
|
* @param config - Configuration options for the transport
|
|
72
|
-
* @param config.apiKey - New Relic API key for authentication
|
|
73
|
-
* @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
|
|
74
|
-
* @param config.onError - Optional error callback for handling errors
|
|
75
|
-
* @param config.onDebug - Optional callback for debugging log entries before they are sent
|
|
76
|
-
* @param config.useCompression - Whether to use gzip compression (defaults to true)
|
|
77
|
-
* @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
|
|
78
|
-
* @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
|
|
79
|
-
* @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
|
|
80
55
|
*/
|
|
81
56
|
constructor(config: NewRelicTransportConfig);
|
|
82
|
-
/**
|
|
83
|
-
* Processes and ships log entries to New Relic.
|
|
84
|
-
*
|
|
85
|
-
* This method:
|
|
86
|
-
* 1. Validates the message size
|
|
87
|
-
* 2. Creates and validates the log entry
|
|
88
|
-
* 3. Validates the final payload size
|
|
89
|
-
* 4. Asynchronously sends the log entry to New Relic
|
|
90
|
-
*
|
|
91
|
-
* The actual sending is done asynchronously in a fire-and-forget manner to maintain
|
|
92
|
-
* compatibility with the base transport class while still providing retry and error handling.
|
|
93
|
-
*
|
|
94
|
-
* @param params - Log parameters including level, messages, and metadata
|
|
95
|
-
* @param params.logLevel - The severity level of the log
|
|
96
|
-
* @param params.messages - Array of message strings to be joined
|
|
97
|
-
* @param params.data - Optional metadata to include with the log
|
|
98
|
-
* @param params.hasData - Whether metadata is present
|
|
99
|
-
* @returns The original messages array
|
|
100
|
-
* @throws {ValidationError} If the payload exceeds size limits or validation fails
|
|
101
|
-
*/
|
|
102
|
-
shipToLogger({
|
|
103
|
-
logLevel,
|
|
104
|
-
messages,
|
|
105
|
-
data,
|
|
106
|
-
hasData
|
|
107
|
-
}: LogLayerTransportParams): any[];
|
|
108
57
|
}
|
|
109
58
|
//#endregion
|
|
110
|
-
export { NewRelicTransport, NewRelicTransportConfig };
|
|
59
|
+
export { HttpTransportError, LogSizeError, NewRelicTransport, type NewRelicTransportConfig, RateLimitError, ValidationError };
|
|
111
60
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -1,111 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpTransport, HttpTransportConfig, HttpTransportError, LogSizeError, RateLimitError } from "@loglayer/transport-http";
|
|
2
2
|
|
|
3
3
|
//#region src/NewRelicTransport.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when log entry validation fails.
|
|
6
|
+
* This includes attribute count, attribute name length, and other New Relic API validations.
|
|
7
|
+
*/
|
|
8
|
+
declare class ValidationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
4
11
|
/**
|
|
5
12
|
* Configuration options for the New Relic transport.
|
|
13
|
+
* Extends HttpTransportConfig with New Relic-specific options.
|
|
6
14
|
*/
|
|
7
|
-
interface NewRelicTransportConfig extends
|
|
15
|
+
interface NewRelicTransportConfig extends Omit<HttpTransportConfig, "url" | "headers" | "payloadTemplate"> {
|
|
8
16
|
/**
|
|
9
17
|
* The New Relic API key
|
|
10
18
|
*/
|
|
11
19
|
apiKey: string;
|
|
12
20
|
/**
|
|
13
21
|
* The New Relic Log API endpoint
|
|
14
|
-
* @default https://log-api.newrelic.com/log/v1
|
|
22
|
+
* @default "https://log-api.newrelic.com/log/v1"
|
|
15
23
|
*/
|
|
16
24
|
endpoint?: string;
|
|
17
25
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
onError?: (err: Error) => void;
|
|
21
|
-
/**
|
|
22
|
-
* Optional callback for debugging log entries before they are sent
|
|
23
|
-
*/
|
|
24
|
-
onDebug?: (entry: Record<string, any>) => void;
|
|
25
|
-
/**
|
|
26
|
-
* Whether to use gzip compression
|
|
27
|
-
* @default true
|
|
28
|
-
*/
|
|
29
|
-
useCompression?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Number of retry attempts before giving up
|
|
32
|
-
* @default 3
|
|
26
|
+
* Custom payload template function (optional, defaults to New Relic format).
|
|
27
|
+
* Receives log level, message, and optional data (metadata).
|
|
33
28
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
retryDelay?: number;
|
|
40
|
-
/**
|
|
41
|
-
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
42
|
-
* @default true
|
|
43
|
-
*/
|
|
44
|
-
respectRateLimit?: boolean;
|
|
29
|
+
payloadTemplate?: (params: {
|
|
30
|
+
logLevel: string;
|
|
31
|
+
message: string;
|
|
32
|
+
data?: Record<string, any>;
|
|
33
|
+
}) => string;
|
|
45
34
|
}
|
|
46
35
|
/**
|
|
47
36
|
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
48
|
-
* It
|
|
37
|
+
* It extends HttpTransport with New Relic-specific configuration, validation,
|
|
38
|
+
* and formatting.
|
|
49
39
|
*
|
|
50
40
|
* Features:
|
|
51
|
-
* - Validates
|
|
52
|
-
* - Validates number of attributes (max 255)
|
|
41
|
+
* - Validates attribute count (max 255)
|
|
53
42
|
* - Validates attribute name length (max 255 characters)
|
|
54
43
|
* - Truncates attribute values longer than 4094 characters
|
|
55
|
-
* -
|
|
56
|
-
* -
|
|
57
|
-
* -
|
|
44
|
+
* - Gzip compression by default (via HttpTransport)
|
|
45
|
+
* - Retry logic with exponential backoff (via HttpTransport)
|
|
46
|
+
* - Rate limiting support (via HttpTransport)
|
|
47
|
+
* - Batch sending support (via HttpTransport)
|
|
48
|
+
* - Error and debug callbacks
|
|
58
49
|
*/
|
|
59
|
-
declare class NewRelicTransport extends
|
|
60
|
-
private apiKey;
|
|
61
|
-
private endpoint;
|
|
62
|
-
private onError?;
|
|
63
|
-
private onDebug?;
|
|
64
|
-
private useCompression;
|
|
65
|
-
private maxRetries;
|
|
66
|
-
private retryDelay;
|
|
67
|
-
private respectRateLimit;
|
|
50
|
+
declare class NewRelicTransport extends HttpTransport {
|
|
68
51
|
/**
|
|
69
52
|
* Creates a new instance of NewRelicTransport.
|
|
70
53
|
*
|
|
71
54
|
* @param config - Configuration options for the transport
|
|
72
|
-
* @param config.apiKey - New Relic API key for authentication
|
|
73
|
-
* @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
|
|
74
|
-
* @param config.onError - Optional error callback for handling errors
|
|
75
|
-
* @param config.onDebug - Optional callback for debugging log entries before they are sent
|
|
76
|
-
* @param config.useCompression - Whether to use gzip compression (defaults to true)
|
|
77
|
-
* @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
|
|
78
|
-
* @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
|
|
79
|
-
* @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
|
|
80
55
|
*/
|
|
81
56
|
constructor(config: NewRelicTransportConfig);
|
|
82
|
-
/**
|
|
83
|
-
* Processes and ships log entries to New Relic.
|
|
84
|
-
*
|
|
85
|
-
* This method:
|
|
86
|
-
* 1. Validates the message size
|
|
87
|
-
* 2. Creates and validates the log entry
|
|
88
|
-
* 3. Validates the final payload size
|
|
89
|
-
* 4. Asynchronously sends the log entry to New Relic
|
|
90
|
-
*
|
|
91
|
-
* The actual sending is done asynchronously in a fire-and-forget manner to maintain
|
|
92
|
-
* compatibility with the base transport class while still providing retry and error handling.
|
|
93
|
-
*
|
|
94
|
-
* @param params - Log parameters including level, messages, and metadata
|
|
95
|
-
* @param params.logLevel - The severity level of the log
|
|
96
|
-
* @param params.messages - Array of message strings to be joined
|
|
97
|
-
* @param params.data - Optional metadata to include with the log
|
|
98
|
-
* @param params.hasData - Whether metadata is present
|
|
99
|
-
* @returns The original messages array
|
|
100
|
-
* @throws {ValidationError} If the payload exceeds size limits or validation fails
|
|
101
|
-
*/
|
|
102
|
-
shipToLogger({
|
|
103
|
-
logLevel,
|
|
104
|
-
messages,
|
|
105
|
-
data,
|
|
106
|
-
hasData
|
|
107
|
-
}: LogLayerTransportParams): any[];
|
|
108
57
|
}
|
|
109
58
|
//#endregion
|
|
110
|
-
export { NewRelicTransport, NewRelicTransportConfig };
|
|
59
|
+
export { HttpTransportError, LogSizeError, NewRelicTransport, type NewRelicTransportConfig, RateLimitError, ValidationError };
|
|
111
60
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HttpTransport, HttpTransportError, LogSizeError, RateLimitError } from "@loglayer/transport-http";
|
|
2
2
|
|
|
3
3
|
//#region src/NewRelicTransport.ts
|
|
4
|
-
const MAX_PAYLOAD_SIZE = 1e6;
|
|
5
4
|
const MAX_ATTRIBUTES = 255;
|
|
6
5
|
const MAX_ATTRIBUTE_NAME_LENGTH = 255;
|
|
7
6
|
const MAX_ATTRIBUTE_VALUE_LENGTH = 4094;
|
|
8
7
|
/**
|
|
9
8
|
* Error thrown when log entry validation fails.
|
|
10
|
-
* This includes
|
|
9
|
+
* This includes attribute count, attribute name length, and other New Relic API validations.
|
|
11
10
|
*/
|
|
12
11
|
var ValidationError = class extends Error {
|
|
13
12
|
constructor(message) {
|
|
@@ -16,222 +15,81 @@ var ValidationError = class extends Error {
|
|
|
16
15
|
}
|
|
17
16
|
};
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Contains the retry-after duration specified by the API.
|
|
21
|
-
*/
|
|
22
|
-
var RateLimitError = class extends Error {
|
|
23
|
-
constructor(message, retryAfter) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.retryAfter = retryAfter;
|
|
26
|
-
this.name = "RateLimitError";
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Validates a log entry against New Relic's constraints.
|
|
18
|
+
* Validates attributes against New Relic's constraints.
|
|
31
19
|
* - Checks number of attributes (max 255)
|
|
32
20
|
* - Validates attribute name length (max 255 characters)
|
|
33
21
|
* - Truncates attribute values longer than 4094 characters
|
|
34
22
|
*
|
|
35
|
-
* @param
|
|
36
|
-
* @returns The validated (and potentially modified)
|
|
23
|
+
* @param attributes - The attributes to validate
|
|
24
|
+
* @returns The validated (and potentially modified) attributes
|
|
37
25
|
* @throws {ValidationError} If validation fails
|
|
38
26
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
function validateAttributes(attributes) {
|
|
28
|
+
const validated = {};
|
|
29
|
+
const attributeCount = Object.keys(attributes).length;
|
|
30
|
+
if (attributeCount > MAX_ATTRIBUTES) throw new ValidationError(`Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}`);
|
|
31
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
32
|
+
if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) throw new ValidationError(`Attribute name exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Found: ${key.length}`);
|
|
33
|
+
if (typeof value === "string" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) validated[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);
|
|
34
|
+
else validated[key] = value;
|
|
47
35
|
}
|
|
48
|
-
return
|
|
36
|
+
return validated;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default payload template that formats log data for New Relic's Log API.
|
|
40
|
+
* Each entry includes timestamp, level, the log message, and optional attributes.
|
|
41
|
+
*/
|
|
42
|
+
function defaultNewRelicPayload(params) {
|
|
43
|
+
const logEntry = {
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
level: params.logLevel,
|
|
46
|
+
log: params.message
|
|
47
|
+
};
|
|
48
|
+
if (params.data) logEntry.attributes = validateAttributes(params.data);
|
|
49
|
+
return JSON.stringify(logEntry);
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
52
|
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
52
|
-
* It
|
|
53
|
+
* It extends HttpTransport with New Relic-specific configuration, validation,
|
|
54
|
+
* and formatting.
|
|
53
55
|
*
|
|
54
56
|
* Features:
|
|
55
|
-
* - Validates
|
|
56
|
-
* - Validates number of attributes (max 255)
|
|
57
|
+
* - Validates attribute count (max 255)
|
|
57
58
|
* - Validates attribute name length (max 255 characters)
|
|
58
59
|
* - Truncates attribute values longer than 4094 characters
|
|
59
|
-
* -
|
|
60
|
-
* -
|
|
61
|
-
* -
|
|
60
|
+
* - Gzip compression by default (via HttpTransport)
|
|
61
|
+
* - Retry logic with exponential backoff (via HttpTransport)
|
|
62
|
+
* - Rate limiting support (via HttpTransport)
|
|
63
|
+
* - Batch sending support (via HttpTransport)
|
|
64
|
+
* - Error and debug callbacks
|
|
62
65
|
*/
|
|
63
|
-
var NewRelicTransport = class extends
|
|
64
|
-
apiKey;
|
|
65
|
-
endpoint;
|
|
66
|
-
onError;
|
|
67
|
-
onDebug;
|
|
68
|
-
useCompression;
|
|
69
|
-
maxRetries;
|
|
70
|
-
retryDelay;
|
|
71
|
-
respectRateLimit;
|
|
66
|
+
var NewRelicTransport = class extends HttpTransport {
|
|
72
67
|
/**
|
|
73
68
|
* Creates a new instance of NewRelicTransport.
|
|
74
69
|
*
|
|
75
70
|
* @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
71
|
*/
|
|
85
72
|
constructor(config) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
}
|
|
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
|
-
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;
|
|
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
|
-
*/
|
|
190
|
-
async function sendWithRetry(endpoint, apiKey, payload, useCompression, maxRetries, retryDelay, respectRateLimit = true) {
|
|
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, {
|
|
73
|
+
const { apiKey, endpoint, payloadTemplate, ...httpConfig } = config;
|
|
74
|
+
super({
|
|
75
|
+
...httpConfig,
|
|
76
|
+
url: endpoint ?? "https://log-api.newrelic.com/log/v1",
|
|
206
77
|
method: "POST",
|
|
207
|
-
headers
|
|
208
|
-
|
|
78
|
+
headers: {
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"Api-Key": apiKey
|
|
81
|
+
},
|
|
82
|
+
payloadTemplate: payloadTemplate ?? defaultNewRelicPayload,
|
|
83
|
+
compression: httpConfig.compression ?? true,
|
|
84
|
+
maxRetries: httpConfig.maxRetries ?? 3,
|
|
85
|
+
retryDelay: httpConfig.retryDelay ?? 1e3,
|
|
86
|
+
respectRateLimit: httpConfig.respectRateLimit ?? true,
|
|
87
|
+
maxLogSize: httpConfig.maxLogSize ?? 1048576,
|
|
88
|
+
maxPayloadSize: httpConfig.maxPayloadSize ?? 1048576
|
|
209
89
|
});
|
|
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
90
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
91
|
+
};
|
|
234
92
|
|
|
235
93
|
//#endregion
|
|
236
|
-
export { NewRelicTransport };
|
|
94
|
+
export { HttpTransportError, LogSizeError, NewRelicTransport, RateLimitError, ValidationError };
|
|
237
95
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"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":";;;AAIA,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;AACvB,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;;;;;AAMnC,IAAM,kBAAN,cAA8B,MAAM;CAClC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;AAQhB,IAAM,iBAAN,cAA6B,MAAM;CACjC,YACE,SACA,AAAO,YACP;AACA,QAAM,QAAQ;EAFP;AAGP,OAAK,OAAO;;;;;;;;;;;;;AAyDhB,SAAS,iBAAiB,UAA+B;AACvD,KAAI,SAAS,YAAY;EAEvB,MAAM,iBAAiB,OAAO,KAAK,SAAS,WAAW,CAAC;AACxD,MAAI,iBAAiB,eACnB,OAAM,IAAI,gBACR,mDAAmD,eAAe,YAAY,iBAC/E;AAIH,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,WAAW,EAAE;AAE9D,OAAI,IAAI,SAAS,0BACf,OAAM,IAAI,gBACR,mBAAmB,IAAI,4BAA4B,0BAA0B,aAAa,IAAI,SAC/F;AAIH,OAAI,OAAO,UAAU,YAAY,MAAM,SAAS,2BAE9C,UAAS,WAAW,OAAO,MAAM,MAAM,GAAG,2BAA2B;;;AAK3E,QAAO;;;;;;;;;;;;;;;AAgBT,IAAa,oBAAb,cAAuC,oBAAoB;CACzD,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;;;;;;;;;;;;;;CAeR,YAAY,QAAiC;AAC3C,QAAM,OAAO;AAEb,OAAK,SAAS,OAAO;AACrB,OAAK,WAAW,OAAO,YAAY;AACnC,OAAK,UAAU,OAAO;AACtB,OAAK,UAAU,OAAO;AACtB,OAAK,iBAAiB,OAAO,kBAAkB;AAC/C,OAAK,aAAa,OAAO,cAAc;AACvC,OAAK,aAAa,OAAO,cAAc;AACvC,OAAK,mBAAmB,OAAO,oBAAoB;;;;;;;;;;;;;;;;;;;;;;CAuBrD,aAAa,EAAE,UAAU,UAAU,MAAM,WAA2C;AAClF,MAAI;GAEF,MAAM,UAAU,SAAS,KAAK,IAAI;GAClC,MAAM,eAAe,IAAI,aAAa,CAAC,OAAO,QAAQ,CAAC;AACvD,OAAI,eAAe,iBACjB,OAAM,IAAI,gBACR,mCAAmC,iBAAiB,gBAAgB,aAAa,QAClF;GAGH,MAAM,WAAgC;IACpC,WAAW,KAAK,KAAK;IACrB,OAAO;IACP,KAAK;IACN;AAED,OAAI,QAAQ,QACV,QAAO,OAAO,UAAU,EACtB,YAAY,MACb,CAAC;GAGJ,MAAM,iBAAiB,iBAAiB,SAAS;AAGjD,OAAI,KAAK,QACP,MAAK,QAAQ,eAAe;GAI9B,MAAM,UAAU,KAAK,UAAU,CAAC,eAAe,CAAC;GAChD,MAAM,eAAe,IAAI,aAAa,CAAC,OAAO,QAAQ,CAAC;AACvD,OAAI,eAAe,iBACjB,OAAM,IAAI,gBACR,mCAAmC,iBAAiB,gBAAgB,aAAa,QAClF;AAIH,IAAC,YAAY;AACX,QAAI;AACF,WAAM,cACJ,KAAK,UACL,KAAK,QACL,SACA,KAAK,gBACL,KAAK,YACL,KAAK,YACL,KAAK,iBACN;aACM,OAAO;AACd,SAAI,KAAK,QACP,MAAK,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;AAGzE,SAAI,iBAAiB,gBACnB,OAAM;;OAGR;WACG,OAAO;AACd,OAAI,KAAK,QACP,MAAK,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;;AAI3E,SAAO;;;;;;;;;AAUX,eAAe,aAAa,MAAmC;CAC7D,MAAM,SAAS,IAAI,kBAAkB,OAAO;CAC5C,MAAM,SAAS,OAAO,SAAS,WAAW;CAC1C,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,SAAuB,EAAE;AAE/B,OAAM,OAAO,MAAM,QAAQ,OAAO,KAAK,CAAC;AACxC,OAAM,OAAO,OAAO;CAEpB,MAAM,SAAS,OAAO,SAAS,WAAW;AAE1C,QAAO,MAAM;EACX,MAAM,EAAE,OAAO,SAAS,MAAM,OAAO,MAAM;AAC3C,MAAI,KAAM;AACV,SAAO,KAAK,MAAM;;CAIpB,MAAM,cAAc,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAM,QAAQ,EAAE;CACxE,MAAM,SAAS,IAAI,WAAW,YAAY;CAC1C,IAAI,SAAS;AACb,MAAK,MAAM,SAAS,QAAQ;AAC1B,SAAO,IAAI,OAAO,OAAO;AACzB,YAAU,MAAM;;AAGlB,QAAO;;;;;;;;;;;;;;;;;;AAmBT,eAAe,cACb,UACA,QACA,SACA,gBACA,YACA,YACA,mBAAmB,MACA;CAEnB,MAAM,eAAe,IAAI,aAAa,CAAC,OAAO,QAAQ,CAAC;AACvD,KAAI,eAAe,iBACjB,OAAM,IAAI,gBAAgB,mCAAmC,iBAAiB,gBAAgB,aAAa,QAAQ;CAGrH,IAAI;CACJ,IAAI;AAEJ,KAAI,gBAAgB;AAClB,sBAAoB,MAAM,aAAa,QAAQ;AAE/C,MAAI,kBAAkB,SAAS,iBAC7B,OAAM,IAAI,gBACR,8CAA8C,iBAAiB,gBAAgB,kBAAkB,OAAO,QACzG;;CAIL,MAAM,UAAkC;EACtC,gBAAgB;EAChB,WAAW;EACZ;AAED,KAAI,eACF,SAAQ,sBAAsB;AAGhC,MAAK,IAAI,UAAU,GAAG,WAAW,YAAY,UAC3C,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,UAAU;GACrC,QAAQ;GACR;GACA,MAAM,iBAAiB,oBAAoB;GAC5C,CAAC;AAEF,MAAI,SAAS,WAAW,KAAK;GAC3B,MAAM,aAAa,OAAO,SAAS,SAAS,QAAQ,IAAI,cAAc,IAAI,MAAM,GAAG;AACnF,OAAI,kBAAkB;AAEpB,UAAM,IAAI,SAAS,YAAY,WAAW,SAAS,aAAa,IAAK,CAAC;AAEtE;AACA;;AAGF,SAAM,IAAI,eAAe,oCAAoC,WAAW,WAAW,WAAW;;AAGhG,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,qCAAqC,SAAS,aAAa;AAG7E,SAAO;UACA,OAAO;AACd,cAAY,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AAGrE,MAAI,iBAAiB,gBACnB,OAAM;AAIR,MAAI,CAAC,oBAAoB,iBAAiB,eACxC,OAAM;AAGR,MAAI,YAAY,WACd,OAAM,IAAI,MAAM,6BAA6B,WAAW,YAAY,UAAU,UAAU;AAI1F,MAAI,EAAE,iBAAiB,iBAAiB;GACtC,MAAM,SAAS,KAAK,QAAQ,GAAG;GAC/B,MAAM,QAAQ,aAAa,KAAK,UAAU;AAC1C,SAAM,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,CAAC;;;AAKhE,OAAM"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/NewRelicTransport.ts"],"sourcesContent":["import type { HttpTransportConfig } from \"@loglayer/transport-http\";\nimport { HttpTransport } from \"@loglayer/transport-http\";\n\n// Constants defining New Relic's API limits\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 attribute count, attribute name length, and other New Relic API validations.\n */\nexport class ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * Configuration options for the New Relic transport.\n * Extends HttpTransportConfig with New Relic-specific options.\n */\nexport interface NewRelicTransportConfig extends Omit<HttpTransportConfig, \"url\" | \"headers\" | \"payloadTemplate\"> {\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 * Custom payload template function (optional, defaults to New Relic format).\n * Receives log level, message, and optional data (metadata).\n */\n payloadTemplate?: (params: { logLevel: string; message: string; data?: Record<string, any> }) => string;\n}\n\n/**\n * Validates attributes 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 attributes - The attributes to validate\n * @returns The validated (and potentially modified) attributes\n * @throws {ValidationError} If validation fails\n */\nfunction validateAttributes(attributes: Record<string, any>): Record<string, any> {\n const validated: Record<string, any> = {};\n\n // Check number of attributes\n const attributeCount = Object.keys(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(attributes)) {\n // Check attribute name length\n if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) {\n throw new ValidationError(\n `Attribute name exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Found: ${key.length}`,\n );\n }\n\n // Truncate string values that are too long\n if (typeof value === \"string\" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) {\n validated[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH);\n } else {\n validated[key] = value;\n }\n }\n\n return validated;\n}\n\n/**\n * Default payload template that formats log data for New Relic's Log API.\n * Each entry includes timestamp, level, the log message, and optional attributes.\n */\nfunction defaultNewRelicPayload(params: { logLevel: string; message: string; data?: Record<string, any> }): string {\n const logEntry: Record<string, any> = {\n timestamp: Date.now(),\n level: params.logLevel,\n log: params.message,\n };\n\n if (params.data) {\n logEntry.attributes = validateAttributes(params.data);\n }\n\n return JSON.stringify(logEntry);\n}\n\n/**\n * NewRelicTransport is responsible for sending logs to New Relic's Log API.\n * It extends HttpTransport with New Relic-specific configuration, validation,\n * and formatting.\n *\n * Features:\n * - Validates attribute count (max 255)\n * - Validates attribute name length (max 255 characters)\n * - Truncates attribute values longer than 4094 characters\n * - Gzip compression by default (via HttpTransport)\n * - Retry logic with exponential backoff (via HttpTransport)\n * - Rate limiting support (via HttpTransport)\n * - Batch sending support (via HttpTransport)\n * - Error and debug callbacks\n */\nexport class NewRelicTransport extends HttpTransport {\n /**\n * Creates a new instance of NewRelicTransport.\n *\n * @param config - Configuration options for the transport\n */\n constructor(config: NewRelicTransportConfig) {\n const { apiKey, endpoint, payloadTemplate, ...httpConfig } = config;\n\n super({\n ...httpConfig,\n url: endpoint ?? \"https://log-api.newrelic.com/log/v1\",\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Api-Key\": apiKey,\n },\n payloadTemplate: payloadTemplate ?? defaultNewRelicPayload,\n // Defaults (only apply if not provided)\n compression: httpConfig.compression ?? true,\n maxRetries: httpConfig.maxRetries ?? 3,\n retryDelay: httpConfig.retryDelay ?? 1000,\n respectRateLimit: httpConfig.respectRateLimit ?? true,\n maxLogSize: httpConfig.maxLogSize ?? 1_048_576,\n maxPayloadSize: httpConfig.maxPayloadSize ?? 1_048_576,\n } as HttpTransportConfig);\n }\n}\n"],"mappings":";;;AAIA,MAAM,iBAAiB;AACvB,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;;;;;AAMnC,IAAa,kBAAb,cAAqC,MAAM;CACzC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;;;;;;;AAmChB,SAAS,mBAAmB,YAAsD;CAChF,MAAM,YAAiC,EAAE;CAGzC,MAAM,iBAAiB,OAAO,KAAK,WAAW,CAAC;AAC/C,KAAI,iBAAiB,eACnB,OAAM,IAAI,gBACR,mDAAmD,eAAe,YAAY,iBAC/E;AAIH,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;AAErD,MAAI,IAAI,SAAS,0BACf,OAAM,IAAI,gBACR,0CAA0C,0BAA0B,YAAY,IAAI,SACrF;AAIH,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,2BAC9C,WAAU,OAAO,MAAM,MAAM,GAAG,2BAA2B;MAE3D,WAAU,OAAO;;AAIrB,QAAO;;;;;;AAOT,SAAS,uBAAuB,QAAmF;CACjH,MAAM,WAAgC;EACpC,WAAW,KAAK,KAAK;EACrB,OAAO,OAAO;EACd,KAAK,OAAO;EACb;AAED,KAAI,OAAO,KACT,UAAS,aAAa,mBAAmB,OAAO,KAAK;AAGvD,QAAO,KAAK,UAAU,SAAS;;;;;;;;;;;;;;;;;AAkBjC,IAAa,oBAAb,cAAuC,cAAc;;;;;;CAMnD,YAAY,QAAiC;EAC3C,MAAM,EAAE,QAAQ,UAAU,iBAAiB,GAAG,eAAe;AAE7D,QAAM;GACJ,GAAG;GACH,KAAK,YAAY;GACjB,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,WAAW;IACZ;GACD,iBAAiB,mBAAmB;GAEpC,aAAa,WAAW,eAAe;GACvC,YAAY,WAAW,cAAc;GACrC,YAAY,WAAW,cAAc;GACrC,kBAAkB,WAAW,oBAAoB;GACjD,YAAY,WAAW,cAAc;GACrC,gBAAgB,WAAW,kBAAkB;GAC9C,CAAwB"}
|
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": "
|
|
4
|
+
"version": "4.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"transport"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@loglayer/transport": "
|
|
37
|
+
"@loglayer/transport-http": "2.1.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "25.2.3",
|
|
@@ -44,12 +44,15 @@
|
|
|
44
44
|
"tsx": "4.21.0",
|
|
45
45
|
"typescript": "5.9.3",
|
|
46
46
|
"vitest": "4.0.18",
|
|
47
|
-
"loglayer": "
|
|
48
|
-
"@internal/tsconfig": "2.1.0"
|
|
47
|
+
"@loglayer/transport": "3.0.3",
|
|
48
|
+
"@internal/tsconfig": "2.1.0",
|
|
49
|
+
"loglayer": "9.1.1"
|
|
49
50
|
},
|
|
50
51
|
"bugs": "https://github.com/loglayer/loglayer/issues",
|
|
51
52
|
"engines": {
|
|
52
|
-
"node": ">=18"
|
|
53
|
+
"node": ">=18",
|
|
54
|
+
"bun": ">=1.0.0",
|
|
55
|
+
"deno": ">=1.40"
|
|
53
56
|
},
|
|
54
57
|
"files": [
|
|
55
58
|
"dist"
|