@loglayer/transport-new-relic 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Theo Gravity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# New Relic Transport for LogLayer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@loglayer/transport-new-relic)
|
|
4
|
+
[](https://www.npmjs.com/package/@loglayer/transport-new-relic)
|
|
5
|
+
[](http://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
Ships logs to New Relic using their [Log API](https://docs.newrelic.com/docs/logs/log-api/introduction-log-api/). Features include:
|
|
8
|
+
- Automatic gzip compression (configurable)
|
|
9
|
+
- Retry mechanism with exponential backoff
|
|
10
|
+
- Rate limiting support with configurable behavior
|
|
11
|
+
- Validation of New Relic's API constraints
|
|
12
|
+
- Error handling callback
|
|
13
|
+
- Configurable endpoints for different regions
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install loglayer @loglayer/transport-new-relic serialize-error
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { LogLayer } from 'loglayer'
|
|
25
|
+
import { NewRelicTransport } from "@loglayer/transport-new-relic"
|
|
26
|
+
import { serializeError } from "serialize-error";
|
|
27
|
+
|
|
28
|
+
const log = new LogLayer({
|
|
29
|
+
errorSerializer: serializeError,
|
|
30
|
+
transport: new NewRelicTransport({
|
|
31
|
+
apiKey: "YOUR_NEW_RELIC_API_KEY",
|
|
32
|
+
endpoint: "https://log-api.newrelic.com/log/v1", // optional, this is the default
|
|
33
|
+
useCompression: true, // optional, defaults to true
|
|
34
|
+
maxRetries: 3, // optional, defaults to 3
|
|
35
|
+
retryDelay: 1000, // optional, base delay in ms, defaults to 1000
|
|
36
|
+
respectRateLimit: true, // optional, defaults to true
|
|
37
|
+
onError: (err) => {
|
|
38
|
+
console.error('Failed to send logs to New Relic:', err);
|
|
39
|
+
},
|
|
40
|
+
onDebug: (entry) => {
|
|
41
|
+
console.log('Log entry being sent:', entry);
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Use the logger
|
|
47
|
+
log.info("This is a test message");
|
|
48
|
+
log.withMetadata({ userId: "123" }).error("User not found");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface NewRelicTransportConfig {
|
|
55
|
+
/**
|
|
56
|
+
* Whether the transport is enabled. Default is true.
|
|
57
|
+
*/
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The New Relic API key
|
|
61
|
+
*/
|
|
62
|
+
apiKey: string;
|
|
63
|
+
/**
|
|
64
|
+
* The New Relic Log API endpoint
|
|
65
|
+
* @default https://log-api.newrelic.com/log/v1
|
|
66
|
+
*/
|
|
67
|
+
endpoint?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Optional callback for error handling
|
|
70
|
+
*/
|
|
71
|
+
onError?: (err: Error) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Optional callback for debugging log entries
|
|
74
|
+
* Called with the validated entry before it is sent
|
|
75
|
+
*/
|
|
76
|
+
onDebug?: (entry: Record<string, any>) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Whether to use gzip compression
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
81
|
+
useCompression?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Number of retry attempts before giving up
|
|
84
|
+
* @default 3
|
|
85
|
+
*/
|
|
86
|
+
maxRetries?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Base delay between retries in milliseconds.
|
|
89
|
+
* The actual delay will use exponential backoff with jitter.
|
|
90
|
+
* @default 1000
|
|
91
|
+
*/
|
|
92
|
+
retryDelay?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
95
|
+
* @default true
|
|
96
|
+
*/
|
|
97
|
+
respectRateLimit?: boolean;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Documentation
|
|
102
|
+
|
|
103
|
+
For more details, visit [https://loglayer.dev/transports/new-relic](https://loglayer.dev/transports/new-relic)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/NewRelicTransport.ts
|
|
2
|
+
var _transport = require('@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;
|
|
7
|
+
var ValidationError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ValidationError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var RateLimitError = class extends Error {
|
|
14
|
+
constructor(message, retryAfter) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.retryAfter = retryAfter;
|
|
17
|
+
this.name = "RateLimitError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
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
|
+
}
|
|
41
|
+
var NewRelicTransport = class extends _transport.LoggerlessTransport {
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
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 = _nullishCoalesce(config.endpoint, () => ( "https://log-api.newrelic.com/log/v1"));
|
|
67
|
+
this.onError = config.onError;
|
|
68
|
+
this.onDebug = config.onDebug;
|
|
69
|
+
this.useCompression = _nullishCoalesce(config.useCompression, () => ( true));
|
|
70
|
+
this.maxRetries = _nullishCoalesce(config.maxRetries, () => ( 3));
|
|
71
|
+
this.retryDelay = _nullishCoalesce(config.retryDelay, () => ( 1e3));
|
|
72
|
+
this.respectRateLimit = _nullishCoalesce(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: (/* @__PURE__ */ new Date()).getTime(),
|
|
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
|
+
}
|
|
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
|
+
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;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
exports.NewRelicTransport = NewRelicTransport;
|
|
239
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/loglayer/loglayer/packages/transports/new-relic/dist/index.cjs","../src/NewRelicTransport.ts"],"names":[],"mappings":"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","file":"/home/runner/work/loglayer/loglayer/packages/transports/new-relic/dist/index.cjs","sourcesContent":[null,"import type { LogLayerTransportParams } from \"@loglayer/transport\";\nimport type { LoggerlessTransportConfig } 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: new Date().getTime(),\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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LoggerlessTransportConfig, LoggerlessTransport, LogLayerTransportParams } from '@loglayer/transport';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the New Relic transport.
|
|
5
|
+
*/
|
|
6
|
+
interface NewRelicTransportConfig extends LoggerlessTransportConfig {
|
|
7
|
+
/**
|
|
8
|
+
* The New Relic API key
|
|
9
|
+
*/
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/**
|
|
12
|
+
* The New Relic Log API endpoint
|
|
13
|
+
* @default https://log-api.newrelic.com/log/v1
|
|
14
|
+
*/
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional callback for error handling
|
|
18
|
+
*/
|
|
19
|
+
onError?: (err: Error) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback for debugging log entries before they are sent
|
|
22
|
+
*/
|
|
23
|
+
onDebug?: (entry: Record<string, any>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to use gzip compression
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
useCompression?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Number of retry attempts before giving up
|
|
31
|
+
* @default 3
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Base delay between retries in milliseconds
|
|
36
|
+
* @default 1000
|
|
37
|
+
*/
|
|
38
|
+
retryDelay?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
respectRateLimit?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
47
|
+
* It handles validation, compression, retries, and rate limiting according to New Relic's specifications.
|
|
48
|
+
*
|
|
49
|
+
* Features:
|
|
50
|
+
* - Validates payload size (max 1MB)
|
|
51
|
+
* - Validates number of attributes (max 255)
|
|
52
|
+
* - Validates attribute name length (max 255 characters)
|
|
53
|
+
* - Truncates attribute values longer than 4094 characters
|
|
54
|
+
* - Supports gzip compression
|
|
55
|
+
* - Handles rate limiting with configurable behavior
|
|
56
|
+
* - Implements retry logic with exponential backoff
|
|
57
|
+
*/
|
|
58
|
+
declare class NewRelicTransport extends LoggerlessTransport {
|
|
59
|
+
private apiKey;
|
|
60
|
+
private endpoint;
|
|
61
|
+
private onError?;
|
|
62
|
+
private onDebug?;
|
|
63
|
+
private useCompression;
|
|
64
|
+
private maxRetries;
|
|
65
|
+
private retryDelay;
|
|
66
|
+
private respectRateLimit;
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new instance of NewRelicTransport.
|
|
69
|
+
*
|
|
70
|
+
* @param config - Configuration options for the transport
|
|
71
|
+
* @param config.apiKey - New Relic API key for authentication
|
|
72
|
+
* @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
|
|
73
|
+
* @param config.onError - Optional error callback for handling errors
|
|
74
|
+
* @param config.onDebug - Optional callback for debugging log entries before they are sent
|
|
75
|
+
* @param config.useCompression - Whether to use gzip compression (defaults to true)
|
|
76
|
+
* @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
|
|
77
|
+
* @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
|
|
78
|
+
* @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
|
|
79
|
+
*/
|
|
80
|
+
constructor(config: NewRelicTransportConfig);
|
|
81
|
+
/**
|
|
82
|
+
* Processes and ships log entries to New Relic.
|
|
83
|
+
*
|
|
84
|
+
* This method:
|
|
85
|
+
* 1. Validates the message size
|
|
86
|
+
* 2. Creates and validates the log entry
|
|
87
|
+
* 3. Validates the final payload size
|
|
88
|
+
* 4. Asynchronously sends the log entry to New Relic
|
|
89
|
+
*
|
|
90
|
+
* The actual sending is done asynchronously in a fire-and-forget manner to maintain
|
|
91
|
+
* compatibility with the base transport class while still providing retry and error handling.
|
|
92
|
+
*
|
|
93
|
+
* @param params - Log parameters including level, messages, and metadata
|
|
94
|
+
* @param params.logLevel - The severity level of the log
|
|
95
|
+
* @param params.messages - Array of message strings to be joined
|
|
96
|
+
* @param params.data - Optional metadata to include with the log
|
|
97
|
+
* @param params.hasData - Whether metadata is present
|
|
98
|
+
* @returns The original messages array
|
|
99
|
+
* @throws {ValidationError} If the payload exceeds size limits or validation fails
|
|
100
|
+
*/
|
|
101
|
+
shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { NewRelicTransport, type NewRelicTransportConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LoggerlessTransportConfig, LoggerlessTransport, LogLayerTransportParams } from '@loglayer/transport';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for the New Relic transport.
|
|
5
|
+
*/
|
|
6
|
+
interface NewRelicTransportConfig extends LoggerlessTransportConfig {
|
|
7
|
+
/**
|
|
8
|
+
* The New Relic API key
|
|
9
|
+
*/
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/**
|
|
12
|
+
* The New Relic Log API endpoint
|
|
13
|
+
* @default https://log-api.newrelic.com/log/v1
|
|
14
|
+
*/
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional callback for error handling
|
|
18
|
+
*/
|
|
19
|
+
onError?: (err: Error) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback for debugging log entries before they are sent
|
|
22
|
+
*/
|
|
23
|
+
onDebug?: (entry: Record<string, any>) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Whether to use gzip compression
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
useCompression?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Number of retry attempts before giving up
|
|
31
|
+
* @default 3
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Base delay between retries in milliseconds
|
|
36
|
+
* @default 1000
|
|
37
|
+
*/
|
|
38
|
+
retryDelay?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to respect rate limiting by waiting when a 429 response is received
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
respectRateLimit?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* NewRelicTransport is responsible for sending logs to New Relic's Log API.
|
|
47
|
+
* It handles validation, compression, retries, and rate limiting according to New Relic's specifications.
|
|
48
|
+
*
|
|
49
|
+
* Features:
|
|
50
|
+
* - Validates payload size (max 1MB)
|
|
51
|
+
* - Validates number of attributes (max 255)
|
|
52
|
+
* - Validates attribute name length (max 255 characters)
|
|
53
|
+
* - Truncates attribute values longer than 4094 characters
|
|
54
|
+
* - Supports gzip compression
|
|
55
|
+
* - Handles rate limiting with configurable behavior
|
|
56
|
+
* - Implements retry logic with exponential backoff
|
|
57
|
+
*/
|
|
58
|
+
declare class NewRelicTransport extends LoggerlessTransport {
|
|
59
|
+
private apiKey;
|
|
60
|
+
private endpoint;
|
|
61
|
+
private onError?;
|
|
62
|
+
private onDebug?;
|
|
63
|
+
private useCompression;
|
|
64
|
+
private maxRetries;
|
|
65
|
+
private retryDelay;
|
|
66
|
+
private respectRateLimit;
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new instance of NewRelicTransport.
|
|
69
|
+
*
|
|
70
|
+
* @param config - Configuration options for the transport
|
|
71
|
+
* @param config.apiKey - New Relic API key for authentication
|
|
72
|
+
* @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint)
|
|
73
|
+
* @param config.onError - Optional error callback for handling errors
|
|
74
|
+
* @param config.onDebug - Optional callback for debugging log entries before they are sent
|
|
75
|
+
* @param config.useCompression - Whether to use gzip compression (defaults to true)
|
|
76
|
+
* @param config.maxRetries - Maximum number of retry attempts (defaults to 3)
|
|
77
|
+
* @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000)
|
|
78
|
+
* @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true)
|
|
79
|
+
*/
|
|
80
|
+
constructor(config: NewRelicTransportConfig);
|
|
81
|
+
/**
|
|
82
|
+
* Processes and ships log entries to New Relic.
|
|
83
|
+
*
|
|
84
|
+
* This method:
|
|
85
|
+
* 1. Validates the message size
|
|
86
|
+
* 2. Creates and validates the log entry
|
|
87
|
+
* 3. Validates the final payload size
|
|
88
|
+
* 4. Asynchronously sends the log entry to New Relic
|
|
89
|
+
*
|
|
90
|
+
* The actual sending is done asynchronously in a fire-and-forget manner to maintain
|
|
91
|
+
* compatibility with the base transport class while still providing retry and error handling.
|
|
92
|
+
*
|
|
93
|
+
* @param params - Log parameters including level, messages, and metadata
|
|
94
|
+
* @param params.logLevel - The severity level of the log
|
|
95
|
+
* @param params.messages - Array of message strings to be joined
|
|
96
|
+
* @param params.data - Optional metadata to include with the log
|
|
97
|
+
* @param params.hasData - Whether metadata is present
|
|
98
|
+
* @returns The original messages array
|
|
99
|
+
* @throws {ValidationError} If the payload exceeds size limits or validation fails
|
|
100
|
+
*/
|
|
101
|
+
shipToLogger({ logLevel, messages, data, hasData }: LogLayerTransportParams): any[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { NewRelicTransport, type NewRelicTransportConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// src/NewRelicTransport.ts
|
|
2
|
+
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;
|
|
7
|
+
var ValidationError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ValidationError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var RateLimitError = class extends Error {
|
|
14
|
+
constructor(message, retryAfter) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.retryAfter = retryAfter;
|
|
17
|
+
this.name = "RateLimitError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
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
|
+
}
|
|
41
|
+
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: (/* @__PURE__ */ new Date()).getTime(),
|
|
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
|
+
}
|
|
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
|
+
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;
|
|
235
|
+
}
|
|
236
|
+
export {
|
|
237
|
+
NewRelicTransport
|
|
238
|
+
};
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NewRelicTransport.ts"],"sourcesContent":["import type { LogLayerTransportParams } from \"@loglayer/transport\";\nimport type { LoggerlessTransportConfig } 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: new Date().getTime(),\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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loglayer/transport-new-relic",
|
|
3
|
+
"description": "New Relic transport for loglayer.",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.cts",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": "loglayer/loglayer.git",
|
|
21
|
+
"author": "Theo Gravity <theo@suteki.nu>",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"logging",
|
|
24
|
+
"log",
|
|
25
|
+
"loglayer",
|
|
26
|
+
"new-relic",
|
|
27
|
+
"newrelic",
|
|
28
|
+
"relic",
|
|
29
|
+
"transport"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@loglayer/transport": "1.1.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"dotenv": "16.4.7",
|
|
36
|
+
"hash-runner": "2.0.1",
|
|
37
|
+
"@types/node": "22.10.4",
|
|
38
|
+
"serialize-error": "11.0.3",
|
|
39
|
+
"tsx": "4.19.2",
|
|
40
|
+
"tsup": "8.3.5",
|
|
41
|
+
"typescript": "5.7.2",
|
|
42
|
+
"vitest": "2.1.8",
|
|
43
|
+
"loglayer": "5.0.7",
|
|
44
|
+
"@internal/tsconfig": "1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"bugs": "https://github.com/loglayer/loglayer/issues",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist"
|
|
52
|
+
],
|
|
53
|
+
"homepage": "https://loglayer.dev",
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts",
|
|
56
|
+
"test": "vitest --run",
|
|
57
|
+
"build:dev": "hash-runner",
|
|
58
|
+
"clean": "rm -rf .turbo node_modules dist",
|
|
59
|
+
"lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
|
|
60
|
+
"verify-types": "tsc --noEmit",
|
|
61
|
+
"livetest": "tsx src/__tests__/livetest.ts"
|
|
62
|
+
}
|
|
63
|
+
}
|