@logtide/sdk-node 0.1.1 → 0.2.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 CHANGED
@@ -1,21 +1,36 @@
1
- # LogTide Node.js SDK
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/logtide-dev/logtide/main/docs/images/logo.png" alt="LogTide Logo" width="400">
3
+ </p>
2
4
 
3
- Official Node.js SDK for LogTide with advanced features: retry logic, circuit breaker, query API, live streaming, and middleware support.
5
+ <h1 align="center">LogTide Node.js SDK</h1>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@logtide/sdk-node"><img src="https://img.shields.io/npm/v/@logtide/sdk-node?color=blue" alt="npm"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
10
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-18+-green.svg" alt="Node.js"></a>
11
+ <a href="https://github.com/logtide-dev/logtide-sdk-node/releases"><img src="https://img.shields.io/github/v/release/logtide-dev/logtide-sdk-node" alt="Release"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ Official Node.js SDK for <a href="https://logtide.dev">LogTide</a> with automatic batching, retry logic, circuit breaker, query API, live streaming, and middleware support.
16
+ </p>
17
+
18
+ ---
4
19
 
5
20
  ## Features
6
21
 
7
- - **Automatic batching** with configurable size and interval
8
- - **Retry logic** with exponential backoff
9
- - **Circuit breaker** pattern for fault tolerance
10
- - **Max buffer size** with drop policy to prevent memory leaks
11
- - **Query API** for searching and filtering logs
12
- - **Live tail** with Server-Sent Events (SSE)
13
- - **Trace ID context** for distributed tracing
14
- - **Global metadata** added to all logs
15
- - **Structured error serialization**
16
- - **Internal metrics** (logs sent, errors, latency, etc.)
17
- - **Express & Fastify middleware** for auto-logging HTTP requests
18
- - **Full TypeScript support** with strict types
22
+ - **Automatic batching** with configurable size and interval
23
+ - **Retry logic** with exponential backoff
24
+ - **Circuit breaker** pattern for fault tolerance
25
+ - **Max buffer size** with drop policy to prevent memory leaks
26
+ - **Query API** for searching and filtering logs
27
+ - **Live tail** with Server-Sent Events (SSE)
28
+ - **Trace ID context** for distributed tracing
29
+ - **Global metadata** added to all logs
30
+ - **Structured error serialization**
31
+ - **Internal metrics** (logs sent, errors, latency, etc.)
32
+ - **Express & Fastify middleware** for auto-logging HTTP requests
33
+ - **Full TypeScript support** with strict types
19
34
 
20
35
  ## Installation
21
36
 
@@ -89,7 +104,7 @@ const client = new LogTideClient({
89
104
  // Buffer management
90
105
  maxBufferSize: 10000,
91
106
 
92
- // Retry with exponential backoff (1s 2s 4s)
107
+ // Retry with exponential backoff (1s -> 2s -> 4s)
93
108
  maxRetries: 3,
94
109
  retryDelayMs: 1000,
95
110
 
@@ -158,9 +173,9 @@ client.log({
158
173
  service: 'custom-service',
159
174
  level: 'info',
160
175
  message: 'Custom log',
161
- time: new Date().toISOString(), // Optional
176
+ time: new Date().toISOString(),
162
177
  metadata: { key: 'value' },
163
- trace_id: 'custom-trace-id', // Optional
178
+ trace_id: 'custom-trace-id',
164
179
  });
165
180
  ```
166
181
 
@@ -201,16 +216,6 @@ client.withNewTraceId(() => {
201
216
  });
202
217
  ```
203
218
 
204
- ### Auto Trace ID Mode
205
-
206
- ```typescript
207
- const client = new LogTideClient({
208
- apiUrl: 'http://localhost:8080',
209
- apiKey: 'lp_your_api_key_here',
210
- autoTraceId: true, // Every log gets a unique trace ID
211
- });
212
- ```
213
-
214
219
  ---
215
220
 
216
221
  ## Query API
@@ -315,7 +320,9 @@ client.resetMetrics();
315
320
 
316
321
  ---
317
322
 
318
- ## Middleware
323
+ ## Middleware Integration
324
+
325
+ LogTide provides ready-to-use middleware for popular frameworks.
319
326
 
320
327
  ### Express Middleware
321
328
 
@@ -385,6 +392,17 @@ await fastify.listen({ port: 3000 });
385
392
 
386
393
  ---
387
394
 
395
+ ## Examples
396
+
397
+ See the [examples/](./examples) directory for complete working examples:
398
+
399
+ - **[basic.ts](./examples/basic.ts)** - Simple usage
400
+ - **[advanced.ts](./examples/advanced.ts)** - All advanced features
401
+ - **[express-middleware.ts](./examples/express-middleware.ts)** - Express integration
402
+ - **[fastify-plugin.ts](./examples/fastify-plugin.ts)** - Fastify integration
403
+
404
+ ---
405
+
388
406
  ## Best Practices
389
407
 
390
408
  ### 1. Always Close on Shutdown
@@ -442,72 +460,6 @@ setInterval(() => {
442
460
  }, 60000);
443
461
  ```
444
462
 
445
- ### 5. Use Trace IDs for Request Correlation
446
-
447
- ```typescript
448
- app.use((req, res, next) => {
449
- const traceId = req.headers['x-trace-id'] || randomUUID();
450
- req.traceId = traceId;
451
-
452
- client.withTraceId(traceId, () => {
453
- next();
454
- });
455
- });
456
- ```
457
-
458
- ---
459
-
460
- ## API Reference
461
-
462
- ### LogTideClient
463
-
464
- #### Constructor
465
- ```typescript
466
- new LogTideClient(options: LogTideClientOptions)
467
- ```
468
-
469
- #### Logging Methods
470
- - `log(entry: LogEntry): void`
471
- - `debug(service: string, message: string, metadata?: object): void`
472
- - `info(service: string, message: string, metadata?: object): void`
473
- - `warn(service: string, message: string, metadata?: object): void`
474
- - `error(service: string, message: string, metadataOrError?: object | Error): void`
475
- - `critical(service: string, message: string, metadataOrError?: object | Error): void`
476
-
477
- #### Context Methods
478
- - `setTraceId(traceId: string | null): void`
479
- - `getTraceId(): string | null`
480
- - `withTraceId<T>(traceId: string, fn: () => T): T`
481
- - `withNewTraceId<T>(fn: () => T): T`
482
-
483
- #### Query Methods
484
- - `query(options: QueryOptions): Promise<LogsResponse>`
485
- - `getByTraceId(traceId: string): Promise<InternalLogEntry[]>`
486
- - `getAggregatedStats(options: AggregatedStatsOptions): Promise<AggregatedStatsResponse>`
487
-
488
- #### Streaming
489
- - `stream(options: StreamOptions): () => void` (returns cleanup function)
490
-
491
- #### Metrics
492
- - `getMetrics(): ClientMetrics`
493
- - `resetMetrics(): void`
494
- - `getCircuitBreakerState(): string`
495
-
496
- #### Lifecycle
497
- - `flush(): Promise<void>`
498
- - `close(): Promise<void>`
499
-
500
- ---
501
-
502
- ## Examples
503
-
504
- See the [examples/](./examples) directory for complete working examples:
505
-
506
- - **[basic.ts](./examples/basic.ts)** - Simple usage
507
- - **[advanced.ts](./examples/advanced.ts)** - All advanced features
508
- - **[express-middleware.ts](./examples/express-middleware.ts)** - Express integration
509
- - **[fastify-plugin.ts](./examples/fastify-plugin.ts)** - Fastify integration
510
-
511
463
  ---
512
464
 
513
465
  ## TypeScript Support
@@ -526,36 +478,16 @@ import type {
526
478
 
527
479
  ---
528
480
 
529
- ## Testing
530
-
531
- Run the test suite:
532
-
533
- ```bash
534
- # Run tests
535
- pnpm test
536
-
537
- # Watch mode
538
- pnpm test:watch
539
-
540
- # Coverage
541
- pnpm test:coverage
542
- ```
481
+ ## Contributing
543
482
 
544
- ---
483
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
545
484
 
546
485
  ## License
547
486
 
548
- MIT
549
-
550
- ---
551
-
552
- ## Contributing
553
-
554
- Contributions are welcome! Please open an issue or PR on [GitHub](https://github.com/logtide-dev/logtide-sdk-node).
555
-
556
- ---
487
+ MIT License - see [LICENSE](LICENSE) for details.
557
488
 
558
- ## Support
489
+ ## Links
559
490
 
560
- - **Documentation**: [https://logtide.dev/docs](https://logtide.dev/docs)
561
- - **Issues**: [GitHub Issues](https://github.com/logtide-dev/logtide-sdk-node/issues)
491
+ - [LogTide Website](https://logtide.dev)
492
+ - [Documentation](https://logtide.dev/docs/sdks/node/)
493
+ - [GitHub Issues](https://github.com/logtide-dev/logtide-sdk-node/issues)
package/dist/index.cjs CHANGED
@@ -42,25 +42,6 @@ var CircuitBreaker = class {
42
42
  return this.state;
43
43
  }
44
44
  };
45
- function isValidUUID(str) {
46
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
47
- return uuidRegex.test(str);
48
- }
49
- function normalizeTraceId(traceId, debug) {
50
- if (!traceId) {
51
- return void 0;
52
- }
53
- if (isValidUUID(traceId)) {
54
- return traceId;
55
- }
56
- const newTraceId = crypto.randomUUID();
57
- if (debug) {
58
- console.warn(
59
- `[LogTide] Invalid trace_id "${traceId}" (must be UUID v4). Generated new UUID: ${newTraceId}`
60
- );
61
- }
62
- return newTraceId;
63
- }
64
45
  function serializeError(error) {
65
46
  if (error instanceof Error) {
66
47
  const result = {
@@ -108,6 +89,11 @@ var LogTideClient = class {
108
89
  latencies = [];
109
90
  // Context tracking
110
91
  currentTraceId = null;
92
+ // Console interception
93
+ consoleInterceptOptions = null;
94
+ originalConsole = null;
95
+ // Payload limits
96
+ payloadLimits;
111
97
  constructor(options) {
112
98
  this.apiUrl = options.apiUrl.replace(/\/$/, "");
113
99
  this.apiKey = options.apiKey;
@@ -124,15 +110,25 @@ var LogTideClient = class {
124
110
  options.circuitBreakerThreshold || 5,
125
111
  options.circuitBreakerResetMs || 3e4
126
112
  );
113
+ this.payloadLimits = {
114
+ maxFieldSize: options.payloadLimits?.maxFieldSize ?? 10 * 1024,
115
+ // 10KB
116
+ maxLogSize: options.payloadLimits?.maxLogSize ?? 100 * 1024,
117
+ // 100KB
118
+ excludeFields: options.payloadLimits?.excludeFields ?? [],
119
+ truncationMarker: options.payloadLimits?.truncationMarker ?? "...[TRUNCATED]"
120
+ };
127
121
  this.startFlushTimer();
122
+ if (options.interceptConsole?.enabled) {
123
+ this.startConsoleInterception(options.interceptConsole);
124
+ }
128
125
  }
129
126
  // ==================== Context Helpers ====================
130
127
  /**
131
128
  * Set trace ID for subsequent logs
132
- * Automatically validates and normalizes to UUID v4
133
129
  */
134
130
  setTraceId(traceId) {
135
- this.currentTraceId = normalizeTraceId(traceId, this.debugMode) || null;
131
+ this.currentTraceId = traceId;
136
132
  }
137
133
  /**
138
134
  * Get current trace ID
@@ -158,6 +154,207 @@ var LogTideClient = class {
158
154
  withNewTraceId(fn) {
159
155
  return this.withTraceId(crypto.randomUUID(), fn);
160
156
  }
157
+ // ==================== Console Interception ====================
158
+ /**
159
+ * Start intercepting console methods and forward them to LogTide
160
+ */
161
+ startConsoleInterception(options) {
162
+ if (this.originalConsole) {
163
+ return;
164
+ }
165
+ const config = {
166
+ enabled: true,
167
+ service: options?.service ?? "console",
168
+ preserveOriginal: options?.preserveOriginal ?? true,
169
+ includeStackTrace: options?.includeStackTrace ?? false,
170
+ levels: {
171
+ log: options?.levels?.log ?? true,
172
+ info: options?.levels?.info ?? true,
173
+ warn: options?.levels?.warn ?? true,
174
+ error: options?.levels?.error ?? true,
175
+ debug: options?.levels?.debug ?? true
176
+ }
177
+ };
178
+ this.consoleInterceptOptions = config;
179
+ this.originalConsole = {
180
+ log: console.log.bind(console),
181
+ info: console.info.bind(console),
182
+ warn: console.warn.bind(console),
183
+ error: console.error.bind(console),
184
+ debug: console.debug.bind(console)
185
+ };
186
+ const createInterceptor = (method, level) => {
187
+ const original = this.originalConsole[method];
188
+ return (...args) => {
189
+ if (config.preserveOriginal) {
190
+ original(...args);
191
+ }
192
+ if (!config.levels?.[method]) {
193
+ return;
194
+ }
195
+ const message = args.map((arg) => {
196
+ if (typeof arg === "string") return arg;
197
+ if (arg instanceof Error) return arg.message;
198
+ try {
199
+ return JSON.stringify(arg);
200
+ } catch {
201
+ return String(arg);
202
+ }
203
+ }).join(" ");
204
+ if (message.startsWith("[LogTide]")) {
205
+ return;
206
+ }
207
+ const metadata = {
208
+ source: "console",
209
+ originalMethod: method
210
+ };
211
+ if (config.includeStackTrace) {
212
+ const stack = new Error().stack;
213
+ if (stack) {
214
+ const stackLines = stack.split("\n").slice(2);
215
+ metadata.stackTrace = stackLines.join("\n");
216
+ const callerLine = stackLines[0];
217
+ if (callerLine) {
218
+ const match = callerLine.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/);
219
+ if (match) {
220
+ metadata.caller = {
221
+ function: match[1],
222
+ file: match[2],
223
+ line: parseInt(match[3], 10),
224
+ column: parseInt(match[4], 10)
225
+ };
226
+ } else {
227
+ const altMatch = callerLine.match(/at\s+(.+):(\d+):(\d+)/);
228
+ if (altMatch) {
229
+ metadata.caller = {
230
+ file: altMatch[1],
231
+ line: parseInt(altMatch[2], 10),
232
+ column: parseInt(altMatch[3], 10)
233
+ };
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ if (args.length > 1 || args.length === 1 && typeof args[0] !== "string") {
240
+ metadata.args = args.map((arg) => {
241
+ if (arg instanceof Error) {
242
+ return serializeError(arg);
243
+ }
244
+ return arg;
245
+ });
246
+ }
247
+ this.log({
248
+ service: config.service,
249
+ level,
250
+ message,
251
+ metadata
252
+ });
253
+ };
254
+ };
255
+ console.log = createInterceptor("log", "info");
256
+ console.info = createInterceptor("info", "info");
257
+ console.warn = createInterceptor("warn", "warn");
258
+ console.error = createInterceptor("error", "error");
259
+ console.debug = createInterceptor("debug", "debug");
260
+ if (this.debugMode) {
261
+ this.originalConsole.log("[LogTide] Console interception started");
262
+ }
263
+ }
264
+ /**
265
+ * Stop intercepting console methods and restore originals
266
+ */
267
+ stopConsoleInterception() {
268
+ if (!this.originalConsole) {
269
+ return;
270
+ }
271
+ console.log = this.originalConsole.log;
272
+ console.info = this.originalConsole.info;
273
+ console.warn = this.originalConsole.warn;
274
+ console.error = this.originalConsole.error;
275
+ console.debug = this.originalConsole.debug;
276
+ if (this.debugMode) {
277
+ console.log("[LogTide] Console interception stopped");
278
+ }
279
+ this.originalConsole = null;
280
+ this.consoleInterceptOptions = null;
281
+ }
282
+ /**
283
+ * Check if console interception is active
284
+ */
285
+ isConsoleInterceptionActive() {
286
+ return this.originalConsole !== null;
287
+ }
288
+ // ==================== Payload Processing ====================
289
+ /**
290
+ * Check if a string looks like base64 encoded data
291
+ */
292
+ looksLikeBase64(str) {
293
+ if (typeof str !== "string" || str.length < 100) return false;
294
+ if (str.startsWith("data:")) return true;
295
+ const base64Regex = /^[A-Za-z0-9+/=]{100,}$/;
296
+ return base64Regex.test(str.replace(/\s/g, ""));
297
+ }
298
+ /**
299
+ * Process a value for payload limits (truncation, base64 removal, etc.)
300
+ */
301
+ processValue(value, fieldPath) {
302
+ const fieldName = fieldPath.split(".").pop() || fieldPath;
303
+ if (this.payloadLimits.excludeFields.includes(fieldName)) {
304
+ return "[EXCLUDED]";
305
+ }
306
+ if (value === null || value === void 0) {
307
+ return value;
308
+ }
309
+ if (typeof value === "string") {
310
+ if (this.looksLikeBase64(value)) {
311
+ return "[BASE64 DATA REMOVED]";
312
+ }
313
+ if (value.length > this.payloadLimits.maxFieldSize) {
314
+ return value.substring(0, this.payloadLimits.maxFieldSize) + this.payloadLimits.truncationMarker;
315
+ }
316
+ return value;
317
+ }
318
+ if (Array.isArray(value)) {
319
+ return value.map((item, index) => this.processValue(item, `${fieldPath}[${index}]`));
320
+ }
321
+ if (typeof value === "object") {
322
+ const processed = {};
323
+ for (const [key, val] of Object.entries(value)) {
324
+ processed[key] = this.processValue(val, `${fieldPath}.${key}`);
325
+ }
326
+ return processed;
327
+ }
328
+ return value;
329
+ }
330
+ /**
331
+ * Process metadata to apply payload limits
332
+ */
333
+ processMetadata(metadata) {
334
+ if (!metadata) return metadata;
335
+ return this.processValue(metadata, "metadata");
336
+ }
337
+ /**
338
+ * Ensure log entry doesn't exceed max size
339
+ */
340
+ enforceMaxLogSize(entry) {
341
+ const serialized = JSON.stringify(entry);
342
+ if (serialized.length <= this.payloadLimits.maxLogSize) {
343
+ return entry;
344
+ }
345
+ if (this.debugMode) {
346
+ console.warn(`[LogTide] Log entry too large (${serialized.length} bytes), truncating metadata`);
347
+ }
348
+ const truncated = {
349
+ ...entry,
350
+ metadata: {
351
+ _truncated: true,
352
+ _originalSize: serialized.length,
353
+ message: entry.message
354
+ }
355
+ };
356
+ return truncated;
357
+ }
161
358
  // ==================== Logging Methods ====================
162
359
  startFlushTimer() {
163
360
  this.timer = setInterval(() => {
@@ -172,16 +369,19 @@ var LogTideClient = class {
172
369
  }
173
370
  return;
174
371
  }
175
- const normalizedTraceId = normalizeTraceId(entry.trace_id, this.debugMode) || normalizeTraceId(this.currentTraceId, this.debugMode) || (this.autoTraceId ? crypto.randomUUID() : void 0);
176
- const internalEntry = {
372
+ const traceId = entry.trace_id || this.currentTraceId || (this.autoTraceId ? crypto.randomUUID() : void 0);
373
+ const mergedMetadata = {
374
+ ...this.globalMetadata,
375
+ ...entry.metadata
376
+ };
377
+ const processedMetadata = this.processMetadata(mergedMetadata);
378
+ let internalEntry = {
177
379
  ...entry,
178
380
  time: entry.time || (/* @__PURE__ */ new Date()).toISOString(),
179
- metadata: {
180
- ...this.globalMetadata,
181
- ...entry.metadata
182
- },
183
- trace_id: normalizedTraceId
381
+ metadata: processedMetadata,
382
+ trace_id: traceId
184
383
  };
384
+ internalEntry = this.enforceMaxLogSize(internalEntry);
185
385
  this.buffer.push(internalEntry);
186
386
  if (this.buffer.length >= this.batchSize) {
187
387
  this.flush();
@@ -396,6 +596,7 @@ var LogTideClient = class {
396
596
  clearInterval(this.timer);
397
597
  this.timer = null;
398
598
  }
599
+ this.stopConsoleInterception();
399
600
  await this.flush();
400
601
  }
401
602
  };