vedatrace 0.2.1 → 0.3.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/README.md +53 -7
- package/dist/index-BcBZY8as.mjs +231 -0
- package/dist/index-osD28Hs0.cjs +239 -0
- package/dist/index.cjs +122 -119
- package/dist/index.d.cts +80 -35
- package/dist/index.d.mts +80 -35
- package/dist/index.mjs +122 -120
- package/dist/integrations/express.cjs +3 -4
- package/dist/integrations/express.d.cts +1 -1
- package/dist/integrations/express.d.mts +1 -1
- package/dist/integrations/express.mjs +3 -4
- package/dist/integrations/nextjs.cjs +31 -5
- package/dist/integrations/nextjs.d.cts +7 -15
- package/dist/integrations/nextjs.d.mts +7 -15
- package/dist/integrations/nextjs.mjs +31 -6
- package/dist/integrations/react.cjs +1 -1
- package/dist/integrations/react.d.cts +1 -1
- package/dist/integrations/react.d.mts +1 -1
- package/dist/integrations/react.mjs +1 -1
- package/dist/transports/index.cjs +5 -153
- package/dist/transports/index.d.cts +16 -6
- package/dist/transports/index.d.mts +16 -6
- package/dist/transports/index.mjs +1 -153
- package/dist/{types-BU0UESs9.d.cts → types-CSBzeGyK.d.cts} +8 -0
- package/dist/{types-BU0UESs9.d.mts → types-CSBzeGyK.d.mts} +8 -0
- package/package.json +150 -141
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ bun add vedatrace
|
|
|
32
32
|
import { vedatrace } from 'vedatrace'
|
|
33
33
|
|
|
34
34
|
const logger = vedatrace({
|
|
35
|
-
apiKey: 'your-api-key',
|
|
35
|
+
apiKey: 'your-api-key', // or set VEDATRACE_API_KEY in the environment
|
|
36
36
|
service: 'my-service'
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -66,12 +66,6 @@ const logger = vedatrace({
|
|
|
66
66
|
maxRetries: 3,
|
|
67
67
|
retryDelay: 1000,
|
|
68
68
|
|
|
69
|
-
// Redaction
|
|
70
|
-
redaction: {
|
|
71
|
-
paths: ['password', 'token'],
|
|
72
|
-
mask: '[REDACTED]'
|
|
73
|
-
},
|
|
74
|
-
|
|
75
69
|
// Advanced
|
|
76
70
|
immediateFlush: false, // Flush on each log (dev mode, edge runtimes)
|
|
77
71
|
runtime: 'auto', // Force runtime: 'node' | 'browser' | 'cloudflare' | 'deno' | 'bun'
|
|
@@ -250,6 +244,8 @@ The SDK timer uses `unref()` so the process will exit automatically when there's
|
|
|
250
244
|
### Error Handling
|
|
251
245
|
|
|
252
246
|
```typescript
|
|
247
|
+
import { vedatrace, VedaTraceTransportError } from 'vedatrace'
|
|
248
|
+
|
|
253
249
|
const logger = vedatrace({
|
|
254
250
|
apiKey: '...',
|
|
255
251
|
onError: (err) => {
|
|
@@ -262,6 +258,50 @@ const logger = vedatrace({
|
|
|
262
258
|
logger.error('Critical error', { stack: err.stack })
|
|
263
259
|
```
|
|
264
260
|
|
|
261
|
+
Transport failures arrive as `VedaTraceTransportError`, carrying `status`,
|
|
262
|
+
`retryable`, `fatal` and `retryAfterMs` so you can tell a bad key from a blip:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
onError: (err) => {
|
|
266
|
+
if (err instanceof VedaTraceTransportError && err.fatal) {
|
|
267
|
+
// 401 / 403 - the key or its allowed origins are wrong
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Retry behaviour
|
|
273
|
+
|
|
274
|
+
Failed batches retry with backoff on `408`, `425`, `429` and `5xx`, and on
|
|
275
|
+
network errors. A `Retry-After` header takes precedence over the local backoff.
|
|
276
|
+
Other 4xx responses are reported once and not retried, since a retry cannot
|
|
277
|
+
change the outcome.
|
|
278
|
+
|
|
279
|
+
A `401` or `403` **halts the logger**: the credentials will fail identically on
|
|
280
|
+
every future batch, so the SDK reports once through `onError` and stops instead
|
|
281
|
+
of spending a doomed request per flush. Fix the key (or the key's allowed
|
|
282
|
+
origins in your project settings) and call `logger.start()` to resume.
|
|
283
|
+
|
|
284
|
+
## Environment variables
|
|
285
|
+
|
|
286
|
+
| Variable | Purpose |
|
|
287
|
+
| -------------------- | ---------------------------------------------------- |
|
|
288
|
+
| `VEDATRACE_API_KEY` | Used when `apiKey` is not passed to `vedatrace()`. |
|
|
289
|
+
|
|
290
|
+
Reading it is guarded, so it is safe in browsers and in Workers without
|
|
291
|
+
`nodejs_compat` - those simply see no key and the logger stays inert.
|
|
292
|
+
|
|
293
|
+
## Browser usage
|
|
294
|
+
|
|
295
|
+
In the browser the SDK batches and flushes on `visibilitychange` and `pagehide`,
|
|
296
|
+
using `keepalive` so the final request outlives the page. It deliberately does
|
|
297
|
+
**not** register `beforeunload` or `unload`, either of which would make the host
|
|
298
|
+
page ineligible for the back/forward cache.
|
|
299
|
+
|
|
300
|
+
> **Your API key ships in the bundle.** Anyone can read it out of your JavaScript.
|
|
301
|
+
> Restrict browser keys to your own origins under **Project settings -> API keys ->
|
|
302
|
+
> Allowed origins**, and use a separate, unrestricted key for your backend. A key
|
|
303
|
+
> with no origin list accepts logs from anywhere.
|
|
304
|
+
|
|
265
305
|
## Log Schema
|
|
266
306
|
|
|
267
307
|
Logs sent to VedaTrace follow this schema:
|
|
@@ -328,6 +368,12 @@ Get the detected runtime environment. Returns: `'node' | 'browser' | 'cloudflare
|
|
|
328
368
|
console.log(logger.runtime) // 'cloudflare' when running in Workers
|
|
329
369
|
```
|
|
330
370
|
|
|
371
|
+
## Contributing
|
|
372
|
+
|
|
373
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Note that commit messages drive the
|
|
374
|
+
published version — `feat:` is a minor, `fix:` a patch, `refactor:` releases
|
|
375
|
+
nothing.
|
|
376
|
+
|
|
331
377
|
## License
|
|
332
378
|
|
|
333
379
|
MIT
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
2
|
+
const FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
3
|
+
class VedaTraceTransportError extends Error {
|
|
4
|
+
name = "VedaTraceTransportError";
|
|
5
|
+
status;
|
|
6
|
+
retryable;
|
|
7
|
+
fatal;
|
|
8
|
+
/** Server-requested backoff from a `Retry-After` header, in milliseconds. */
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
constructor(message, options = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = options.status;
|
|
13
|
+
this.fatal = options.fatal ?? false;
|
|
14
|
+
this.retryable = options.retryable ?? true;
|
|
15
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
16
|
+
}
|
|
17
|
+
/** Build an error from an HTTP response status. */
|
|
18
|
+
static fromStatus(status, body, retryAfterMs2) {
|
|
19
|
+
const fatal = FATAL_STATUSES.has(status);
|
|
20
|
+
const retryable = status >= 500 || RETRYABLE_STATUSES.has(status);
|
|
21
|
+
return new VedaTraceTransportError(`HTTP ${status}: ${body}`, {
|
|
22
|
+
status,
|
|
23
|
+
retryable,
|
|
24
|
+
fatal,
|
|
25
|
+
retryAfterMs: retryAfterMs2
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isRetryable(error) {
|
|
30
|
+
if (error instanceof VedaTraceTransportError) return error.retryable;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function isFatal(error) {
|
|
34
|
+
return error instanceof VedaTraceTransportError && error.fatal;
|
|
35
|
+
}
|
|
36
|
+
function retryAfterMs(errors) {
|
|
37
|
+
let longest;
|
|
38
|
+
for (const error of errors) {
|
|
39
|
+
if (error instanceof VedaTraceTransportError && error.retryAfterMs) {
|
|
40
|
+
longest = Math.max(longest ?? 0, error.retryAfterMs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return longest;
|
|
44
|
+
}
|
|
45
|
+
function parseRetryAfter(header) {
|
|
46
|
+
if (!header) return void 0;
|
|
47
|
+
const seconds = Number(header);
|
|
48
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
49
|
+
const date = Date.parse(header);
|
|
50
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LEVEL_COLORS = {
|
|
55
|
+
debug: "\x1B[36m",
|
|
56
|
+
// cyan
|
|
57
|
+
info: "\x1B[32m",
|
|
58
|
+
// green
|
|
59
|
+
warn: "\x1B[33m",
|
|
60
|
+
// yellow
|
|
61
|
+
error: "\x1B[31m",
|
|
62
|
+
// red
|
|
63
|
+
fatal: "\x1B[35m"
|
|
64
|
+
// magenta
|
|
65
|
+
};
|
|
66
|
+
const RESET_COLOR = "\x1B[0m";
|
|
67
|
+
const LEVEL_PRIORITY = {
|
|
68
|
+
debug: 0,
|
|
69
|
+
info: 1,
|
|
70
|
+
warn: 2,
|
|
71
|
+
error: 3,
|
|
72
|
+
fatal: 4
|
|
73
|
+
};
|
|
74
|
+
class VedaTraceConsoleTransport {
|
|
75
|
+
name = "console";
|
|
76
|
+
format;
|
|
77
|
+
colors;
|
|
78
|
+
minLevel;
|
|
79
|
+
constructor(config = {}) {
|
|
80
|
+
this.format = config.format ?? "pretty";
|
|
81
|
+
this.colors = config.colors ?? true;
|
|
82
|
+
this.minLevel = config.minLevel ?? "debug";
|
|
83
|
+
}
|
|
84
|
+
/** Send logs to console */
|
|
85
|
+
send(logs) {
|
|
86
|
+
for (const log of logs) {
|
|
87
|
+
if (LEVEL_PRIORITY[log.level] < LEVEL_PRIORITY[this.minLevel]) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
switch (this.format) {
|
|
91
|
+
case "json":
|
|
92
|
+
this.logJson(log);
|
|
93
|
+
break;
|
|
94
|
+
case "simple":
|
|
95
|
+
this.logSimple(log);
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
this.logPretty(log);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Format as JSON */
|
|
104
|
+
logJson(log) {
|
|
105
|
+
console.log(JSON.stringify(log));
|
|
106
|
+
}
|
|
107
|
+
/** Format as simple text */
|
|
108
|
+
logSimple(log) {
|
|
109
|
+
const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
|
|
110
|
+
const service = log.service ? `[${log.service}] ` : "";
|
|
111
|
+
console.log(
|
|
112
|
+
`${timestamp} ${log.level.toUpperCase()} ${service}${log.message}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
/** Format as pretty colored output */
|
|
116
|
+
logPretty(log) {
|
|
117
|
+
const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
|
|
118
|
+
const color = this.colors ? LEVEL_COLORS[log.level] : "";
|
|
119
|
+
const reset = this.colors ? RESET_COLOR : "";
|
|
120
|
+
const service = log.service ? ` ${color}[${log.service}]${reset}` : "";
|
|
121
|
+
let output = `${timestamp} ${color}${log.level.toUpperCase()}${reset}${service} ${log.message}`;
|
|
122
|
+
if (log.metadata && Object.keys(log.metadata).length > 0) {
|
|
123
|
+
output += `
|
|
124
|
+
${color}metadata:${reset} ${JSON.stringify(log.metadata, null, 2).replace(/\n/g, "\n ")}`;
|
|
125
|
+
}
|
|
126
|
+
console.log(output);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const KEEPALIVE_MAX_BYTES = 56 * 1024;
|
|
131
|
+
const encoder = new TextEncoder();
|
|
132
|
+
class VedaTraceHttpTransport {
|
|
133
|
+
name = "http";
|
|
134
|
+
endpoint;
|
|
135
|
+
apiKey;
|
|
136
|
+
timeout;
|
|
137
|
+
headers;
|
|
138
|
+
keepalive;
|
|
139
|
+
constructor(config) {
|
|
140
|
+
this.apiKey = config.apiKey;
|
|
141
|
+
this.endpoint = config.endpoint ?? "https://ingest.vedatrace.dev/v1/logs";
|
|
142
|
+
this.timeout = config.timeout ?? 3e4;
|
|
143
|
+
this.headers = config.headers ?? {};
|
|
144
|
+
this.keepalive = config.keepalive ?? false;
|
|
145
|
+
}
|
|
146
|
+
/** Send logs via HTTP POST */
|
|
147
|
+
async send(logs) {
|
|
148
|
+
if (logs.length === 0) return;
|
|
149
|
+
const body = JSON.stringify(logs.map(toWirePayload));
|
|
150
|
+
if (this.keepalive && encoder.encode(body).length > KEEPALIVE_MAX_BYTES) {
|
|
151
|
+
if (logs.length > 1) {
|
|
152
|
+
const mid = Math.ceil(logs.length / 2);
|
|
153
|
+
await this.send(logs.slice(0, mid));
|
|
154
|
+
await this.send(logs.slice(mid));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return this.post(body, false);
|
|
158
|
+
}
|
|
159
|
+
return this.post(body, this.keepalive);
|
|
160
|
+
}
|
|
161
|
+
async post(body, keepalive) {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(this.endpoint, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"X-API-Key": this.apiKey,
|
|
170
|
+
...this.headers
|
|
171
|
+
},
|
|
172
|
+
body,
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
keepalive
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const text = await response.text().catch(() => "");
|
|
178
|
+
throw VedaTraceTransportError.fromStatus(
|
|
179
|
+
response.status,
|
|
180
|
+
text,
|
|
181
|
+
parseRetryAfter(response.headers.get("Retry-After"))
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof VedaTraceTransportError) throw error;
|
|
186
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
187
|
+
throw new VedaTraceTransportError(
|
|
188
|
+
`Request timeout after ${this.timeout}ms`,
|
|
189
|
+
{ retryable: true }
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
throw new VedaTraceTransportError(
|
|
193
|
+
error instanceof Error ? error.message : String(error),
|
|
194
|
+
{ retryable: true }
|
|
195
|
+
);
|
|
196
|
+
} finally {
|
|
197
|
+
clearTimeout(timeoutId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* No-op: this transport holds no buffer of its own. The batcher owns the
|
|
202
|
+
* queue and calls send() directly, including on the browser's final flush.
|
|
203
|
+
*/
|
|
204
|
+
async flush() {
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
}
|
|
207
|
+
/** Check if keepalive is enabled */
|
|
208
|
+
isKeepaliveEnabled() {
|
|
209
|
+
return this.keepalive;
|
|
210
|
+
}
|
|
211
|
+
/** Enable/disable keepalive */
|
|
212
|
+
setKeepalive(enabled) {
|
|
213
|
+
this.keepalive = enabled;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function toWirePayload(log) {
|
|
217
|
+
return {
|
|
218
|
+
level: log.level,
|
|
219
|
+
message: log.message,
|
|
220
|
+
service: log.service,
|
|
221
|
+
timestamp: log.timestamp ? new Date(log.timestamp).toISOString() : void 0,
|
|
222
|
+
metadata: log.metadata
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
class VedaTraceHttpTransportBrowser extends VedaTraceHttpTransport {
|
|
226
|
+
constructor(config) {
|
|
227
|
+
super({ ...config, keepalive: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { VedaTraceHttpTransportBrowser as V, isRetryable as a, VedaTraceHttpTransport as b, VedaTraceConsoleTransport as c, VedaTraceTransportError as d, isFatal as i, retryAfterMs as r };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
4
|
+
const FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
5
|
+
class VedaTraceTransportError extends Error {
|
|
6
|
+
name = "VedaTraceTransportError";
|
|
7
|
+
status;
|
|
8
|
+
retryable;
|
|
9
|
+
fatal;
|
|
10
|
+
/** Server-requested backoff from a `Retry-After` header, in milliseconds. */
|
|
11
|
+
retryAfterMs;
|
|
12
|
+
constructor(message, options = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = options.status;
|
|
15
|
+
this.fatal = options.fatal ?? false;
|
|
16
|
+
this.retryable = options.retryable ?? true;
|
|
17
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
18
|
+
}
|
|
19
|
+
/** Build an error from an HTTP response status. */
|
|
20
|
+
static fromStatus(status, body, retryAfterMs2) {
|
|
21
|
+
const fatal = FATAL_STATUSES.has(status);
|
|
22
|
+
const retryable = status >= 500 || RETRYABLE_STATUSES.has(status);
|
|
23
|
+
return new VedaTraceTransportError(`HTTP ${status}: ${body}`, {
|
|
24
|
+
status,
|
|
25
|
+
retryable,
|
|
26
|
+
fatal,
|
|
27
|
+
retryAfterMs: retryAfterMs2
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isRetryable(error) {
|
|
32
|
+
if (error instanceof VedaTraceTransportError) return error.retryable;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
function isFatal(error) {
|
|
36
|
+
return error instanceof VedaTraceTransportError && error.fatal;
|
|
37
|
+
}
|
|
38
|
+
function retryAfterMs(errors) {
|
|
39
|
+
let longest;
|
|
40
|
+
for (const error of errors) {
|
|
41
|
+
if (error instanceof VedaTraceTransportError && error.retryAfterMs) {
|
|
42
|
+
longest = Math.max(longest ?? 0, error.retryAfterMs);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return longest;
|
|
46
|
+
}
|
|
47
|
+
function parseRetryAfter(header) {
|
|
48
|
+
if (!header) return void 0;
|
|
49
|
+
const seconds = Number(header);
|
|
50
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
51
|
+
const date = Date.parse(header);
|
|
52
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LEVEL_COLORS = {
|
|
57
|
+
debug: "\x1B[36m",
|
|
58
|
+
// cyan
|
|
59
|
+
info: "\x1B[32m",
|
|
60
|
+
// green
|
|
61
|
+
warn: "\x1B[33m",
|
|
62
|
+
// yellow
|
|
63
|
+
error: "\x1B[31m",
|
|
64
|
+
// red
|
|
65
|
+
fatal: "\x1B[35m"
|
|
66
|
+
// magenta
|
|
67
|
+
};
|
|
68
|
+
const RESET_COLOR = "\x1B[0m";
|
|
69
|
+
const LEVEL_PRIORITY = {
|
|
70
|
+
debug: 0,
|
|
71
|
+
info: 1,
|
|
72
|
+
warn: 2,
|
|
73
|
+
error: 3,
|
|
74
|
+
fatal: 4
|
|
75
|
+
};
|
|
76
|
+
class VedaTraceConsoleTransport {
|
|
77
|
+
name = "console";
|
|
78
|
+
format;
|
|
79
|
+
colors;
|
|
80
|
+
minLevel;
|
|
81
|
+
constructor(config = {}) {
|
|
82
|
+
this.format = config.format ?? "pretty";
|
|
83
|
+
this.colors = config.colors ?? true;
|
|
84
|
+
this.minLevel = config.minLevel ?? "debug";
|
|
85
|
+
}
|
|
86
|
+
/** Send logs to console */
|
|
87
|
+
send(logs) {
|
|
88
|
+
for (const log of logs) {
|
|
89
|
+
if (LEVEL_PRIORITY[log.level] < LEVEL_PRIORITY[this.minLevel]) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
switch (this.format) {
|
|
93
|
+
case "json":
|
|
94
|
+
this.logJson(log);
|
|
95
|
+
break;
|
|
96
|
+
case "simple":
|
|
97
|
+
this.logSimple(log);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
this.logPretty(log);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Format as JSON */
|
|
106
|
+
logJson(log) {
|
|
107
|
+
console.log(JSON.stringify(log));
|
|
108
|
+
}
|
|
109
|
+
/** Format as simple text */
|
|
110
|
+
logSimple(log) {
|
|
111
|
+
const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
|
|
112
|
+
const service = log.service ? `[${log.service}] ` : "";
|
|
113
|
+
console.log(
|
|
114
|
+
`${timestamp} ${log.level.toUpperCase()} ${service}${log.message}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
/** Format as pretty colored output */
|
|
118
|
+
logPretty(log) {
|
|
119
|
+
const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
|
|
120
|
+
const color = this.colors ? LEVEL_COLORS[log.level] : "";
|
|
121
|
+
const reset = this.colors ? RESET_COLOR : "";
|
|
122
|
+
const service = log.service ? ` ${color}[${log.service}]${reset}` : "";
|
|
123
|
+
let output = `${timestamp} ${color}${log.level.toUpperCase()}${reset}${service} ${log.message}`;
|
|
124
|
+
if (log.metadata && Object.keys(log.metadata).length > 0) {
|
|
125
|
+
output += `
|
|
126
|
+
${color}metadata:${reset} ${JSON.stringify(log.metadata, null, 2).replace(/\n/g, "\n ")}`;
|
|
127
|
+
}
|
|
128
|
+
console.log(output);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const KEEPALIVE_MAX_BYTES = 56 * 1024;
|
|
133
|
+
const encoder = new TextEncoder();
|
|
134
|
+
class VedaTraceHttpTransport {
|
|
135
|
+
name = "http";
|
|
136
|
+
endpoint;
|
|
137
|
+
apiKey;
|
|
138
|
+
timeout;
|
|
139
|
+
headers;
|
|
140
|
+
keepalive;
|
|
141
|
+
constructor(config) {
|
|
142
|
+
this.apiKey = config.apiKey;
|
|
143
|
+
this.endpoint = config.endpoint ?? "https://ingest.vedatrace.dev/v1/logs";
|
|
144
|
+
this.timeout = config.timeout ?? 3e4;
|
|
145
|
+
this.headers = config.headers ?? {};
|
|
146
|
+
this.keepalive = config.keepalive ?? false;
|
|
147
|
+
}
|
|
148
|
+
/** Send logs via HTTP POST */
|
|
149
|
+
async send(logs) {
|
|
150
|
+
if (logs.length === 0) return;
|
|
151
|
+
const body = JSON.stringify(logs.map(toWirePayload));
|
|
152
|
+
if (this.keepalive && encoder.encode(body).length > KEEPALIVE_MAX_BYTES) {
|
|
153
|
+
if (logs.length > 1) {
|
|
154
|
+
const mid = Math.ceil(logs.length / 2);
|
|
155
|
+
await this.send(logs.slice(0, mid));
|
|
156
|
+
await this.send(logs.slice(mid));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
return this.post(body, false);
|
|
160
|
+
}
|
|
161
|
+
return this.post(body, this.keepalive);
|
|
162
|
+
}
|
|
163
|
+
async post(body, keepalive) {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(this.endpoint, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"X-API-Key": this.apiKey,
|
|
172
|
+
...this.headers
|
|
173
|
+
},
|
|
174
|
+
body,
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
keepalive
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const text = await response.text().catch(() => "");
|
|
180
|
+
throw VedaTraceTransportError.fromStatus(
|
|
181
|
+
response.status,
|
|
182
|
+
text,
|
|
183
|
+
parseRetryAfter(response.headers.get("Retry-After"))
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof VedaTraceTransportError) throw error;
|
|
188
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
189
|
+
throw new VedaTraceTransportError(
|
|
190
|
+
`Request timeout after ${this.timeout}ms`,
|
|
191
|
+
{ retryable: true }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
throw new VedaTraceTransportError(
|
|
195
|
+
error instanceof Error ? error.message : String(error),
|
|
196
|
+
{ retryable: true }
|
|
197
|
+
);
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timeoutId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* No-op: this transport holds no buffer of its own. The batcher owns the
|
|
204
|
+
* queue and calls send() directly, including on the browser's final flush.
|
|
205
|
+
*/
|
|
206
|
+
async flush() {
|
|
207
|
+
return Promise.resolve();
|
|
208
|
+
}
|
|
209
|
+
/** Check if keepalive is enabled */
|
|
210
|
+
isKeepaliveEnabled() {
|
|
211
|
+
return this.keepalive;
|
|
212
|
+
}
|
|
213
|
+
/** Enable/disable keepalive */
|
|
214
|
+
setKeepalive(enabled) {
|
|
215
|
+
this.keepalive = enabled;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function toWirePayload(log) {
|
|
219
|
+
return {
|
|
220
|
+
level: log.level,
|
|
221
|
+
message: log.message,
|
|
222
|
+
service: log.service,
|
|
223
|
+
timestamp: log.timestamp ? new Date(log.timestamp).toISOString() : void 0,
|
|
224
|
+
metadata: log.metadata
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
class VedaTraceHttpTransportBrowser extends VedaTraceHttpTransport {
|
|
228
|
+
constructor(config) {
|
|
229
|
+
super({ ...config, keepalive: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
exports.VedaTraceConsoleTransport = VedaTraceConsoleTransport;
|
|
234
|
+
exports.VedaTraceHttpTransport = VedaTraceHttpTransport;
|
|
235
|
+
exports.VedaTraceHttpTransportBrowser = VedaTraceHttpTransportBrowser;
|
|
236
|
+
exports.VedaTraceTransportError = VedaTraceTransportError;
|
|
237
|
+
exports.isFatal = isFatal;
|
|
238
|
+
exports.isRetryable = isRetryable;
|
|
239
|
+
exports.retryAfterMs = retryAfterMs;
|