@statly/observe 1.0.0 → 1.2.0

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.
@@ -0,0 +1,1422 @@
1
+ // src/logger/types.ts
2
+ var LOG_LEVELS = {
3
+ trace: 0,
4
+ debug: 1,
5
+ info: 2,
6
+ warn: 3,
7
+ error: 4,
8
+ fatal: 5,
9
+ audit: 6
10
+ // Special: always logged, never sampled
11
+ };
12
+ var DEFAULT_LEVELS = ["debug", "info", "warn", "error", "fatal"];
13
+ var EXTENDED_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "audit"];
14
+
15
+ // src/logger/scrubbing/patterns.ts
16
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
17
+ "password",
18
+ "passwd",
19
+ "pwd",
20
+ "secret",
21
+ "api_key",
22
+ "apikey",
23
+ "api-key",
24
+ "token",
25
+ "access_token",
26
+ "accesstoken",
27
+ "refresh_token",
28
+ "auth",
29
+ "authorization",
30
+ "bearer",
31
+ "credential",
32
+ "credentials",
33
+ "private_key",
34
+ "privatekey",
35
+ "private-key",
36
+ "secret_key",
37
+ "secretkey",
38
+ "secret-key",
39
+ "session_id",
40
+ "sessionid",
41
+ "session-id",
42
+ "session",
43
+ "cookie",
44
+ "x-api-key",
45
+ "x-auth-token",
46
+ "x-access-token"
47
+ ]);
48
+ var SCRUB_PATTERNS = {
49
+ apiKey: {
50
+ regex: /(?:api[_-]?key|apikey)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?/gi,
51
+ description: "API keys in various formats"
52
+ },
53
+ password: {
54
+ regex: /(?:password|passwd|pwd|secret)\s*[=:]\s*["']?([^"'\s]{3,})["']?/gi,
55
+ description: "Passwords and secrets"
56
+ },
57
+ token: {
58
+ regex: /(?:bearer\s+|token\s*[=:]\s*["']?)([a-zA-Z0-9_\-\.]{20,})["']?/gi,
59
+ description: "Bearer tokens and auth tokens"
60
+ },
61
+ creditCard: {
62
+ // Visa, Mastercard, Amex, Discover, etc.
63
+ regex: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b/g,
64
+ description: "Credit card numbers"
65
+ },
66
+ ssn: {
67
+ regex: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
68
+ description: "US Social Security Numbers"
69
+ },
70
+ email: {
71
+ regex: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
72
+ description: "Email addresses"
73
+ },
74
+ ipAddress: {
75
+ regex: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
76
+ description: "IPv4 addresses"
77
+ },
78
+ awsKey: {
79
+ regex: /(?:AKIA|ABIA|ACCA)[A-Z0-9]{16}/g,
80
+ description: "AWS Access Key IDs"
81
+ },
82
+ privateKey: {
83
+ regex: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
84
+ description: "Private keys in PEM format"
85
+ },
86
+ jwt: {
87
+ regex: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
88
+ description: "JSON Web Tokens"
89
+ }
90
+ };
91
+ var REDACTED = "[REDACTED]";
92
+ function isSensitiveKey(key) {
93
+ const lowerKey = key.toLowerCase();
94
+ return SENSITIVE_KEYS.has(lowerKey);
95
+ }
96
+
97
+ // src/logger/scrubbing/scrubber.ts
98
+ var Scrubber = class {
99
+ constructor(config = {}) {
100
+ this.enabled = config.enabled !== false;
101
+ this.patterns = /* @__PURE__ */ new Map();
102
+ this.customPatterns = config.customPatterns || [];
103
+ this.allowlist = new Set((config.allowlist || []).map((k) => k.toLowerCase()));
104
+ this.customScrubber = config.customScrubber;
105
+ const patternNames = config.patterns || [
106
+ "apiKey",
107
+ "password",
108
+ "token",
109
+ "creditCard",
110
+ "ssn",
111
+ "awsKey",
112
+ "privateKey",
113
+ "jwt"
114
+ ];
115
+ for (const name of patternNames) {
116
+ const pattern = SCRUB_PATTERNS[name];
117
+ if (pattern) {
118
+ this.patterns.set(name, new RegExp(pattern.regex.source, pattern.regex.flags));
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Scrub sensitive data from a value
124
+ */
125
+ scrub(value) {
126
+ if (!this.enabled) {
127
+ return value;
128
+ }
129
+ return this.scrubValue(value, "");
130
+ }
131
+ /**
132
+ * Scrub a log message string
133
+ */
134
+ scrubMessage(message) {
135
+ if (!this.enabled) {
136
+ return message;
137
+ }
138
+ let result = message;
139
+ for (const [, regex] of this.patterns) {
140
+ result = result.replace(regex, REDACTED);
141
+ }
142
+ for (const regex of this.customPatterns) {
143
+ result = result.replace(regex, REDACTED);
144
+ }
145
+ return result;
146
+ }
147
+ /**
148
+ * Recursively scrub sensitive data
149
+ */
150
+ scrubValue(value, key) {
151
+ if (key && this.allowlist.has(key.toLowerCase())) {
152
+ return value;
153
+ }
154
+ if (this.customScrubber && key) {
155
+ const result = this.customScrubber(key, value);
156
+ if (result !== value) {
157
+ return result;
158
+ }
159
+ }
160
+ if (key && isSensitiveKey(key)) {
161
+ return REDACTED;
162
+ }
163
+ if (value === null || value === void 0) {
164
+ return value;
165
+ }
166
+ if (typeof value === "string") {
167
+ return this.scrubString(value);
168
+ }
169
+ if (Array.isArray(value)) {
170
+ return value.map((item, index) => this.scrubValue(item, String(index)));
171
+ }
172
+ if (typeof value === "object") {
173
+ return this.scrubObject(value);
174
+ }
175
+ return value;
176
+ }
177
+ /**
178
+ * Scrub sensitive patterns from a string
179
+ */
180
+ scrubString(value) {
181
+ let result = value;
182
+ for (const [, regex] of this.patterns) {
183
+ regex.lastIndex = 0;
184
+ result = result.replace(regex, REDACTED);
185
+ }
186
+ for (const regex of this.customPatterns) {
187
+ const newRegex = new RegExp(regex.source, regex.flags);
188
+ result = result.replace(newRegex, REDACTED);
189
+ }
190
+ return result;
191
+ }
192
+ /**
193
+ * Scrub sensitive data from an object
194
+ */
195
+ scrubObject(obj) {
196
+ const result = {};
197
+ for (const [key, value] of Object.entries(obj)) {
198
+ result[key] = this.scrubValue(value, key);
199
+ }
200
+ return result;
201
+ }
202
+ /**
203
+ * Add a custom pattern at runtime
204
+ */
205
+ addPattern(pattern) {
206
+ this.customPatterns.push(pattern);
207
+ }
208
+ /**
209
+ * Add a key to the allowlist
210
+ */
211
+ addToAllowlist(key) {
212
+ this.allowlist.add(key.toLowerCase());
213
+ }
214
+ /**
215
+ * Check if scrubbing is enabled
216
+ */
217
+ isEnabled() {
218
+ return this.enabled;
219
+ }
220
+ /**
221
+ * Enable or disable scrubbing
222
+ */
223
+ setEnabled(enabled) {
224
+ this.enabled = enabled;
225
+ }
226
+ };
227
+
228
+ // src/logger/formatters/console.ts
229
+ var COLORS = {
230
+ reset: "\x1B[0m",
231
+ bold: "\x1B[1m",
232
+ dim: "\x1B[2m",
233
+ // Foreground colors
234
+ black: "\x1B[30m",
235
+ red: "\x1B[31m",
236
+ green: "\x1B[32m",
237
+ yellow: "\x1B[33m",
238
+ blue: "\x1B[34m",
239
+ magenta: "\x1B[35m",
240
+ cyan: "\x1B[36m",
241
+ white: "\x1B[37m",
242
+ gray: "\x1B[90m",
243
+ // Background colors
244
+ bgRed: "\x1B[41m",
245
+ bgYellow: "\x1B[43m"
246
+ };
247
+ var LEVEL_COLORS = {
248
+ trace: COLORS.gray,
249
+ debug: COLORS.cyan,
250
+ info: COLORS.green,
251
+ warn: COLORS.yellow,
252
+ error: COLORS.red,
253
+ fatal: `${COLORS.bgRed}${COLORS.white}`,
254
+ audit: COLORS.magenta
255
+ };
256
+ var LEVEL_LABELS = {
257
+ trace: "TRACE",
258
+ debug: "DEBUG",
259
+ info: "INFO ",
260
+ warn: "WARN ",
261
+ error: "ERROR",
262
+ fatal: "FATAL",
263
+ audit: "AUDIT"
264
+ };
265
+ function formatPretty(entry, options = {}) {
266
+ const {
267
+ colors = true,
268
+ timestamps = true,
269
+ showLevel = true,
270
+ showLogger = true,
271
+ showContext = true,
272
+ showSource = false
273
+ } = options;
274
+ const parts = [];
275
+ if (timestamps) {
276
+ const date = new Date(entry.timestamp);
277
+ const time = date.toISOString().replace("T", " ").replace("Z", "");
278
+ parts.push(colors ? `${COLORS.dim}${time}${COLORS.reset}` : time);
279
+ }
280
+ if (showLevel) {
281
+ const levelColor = colors ? LEVEL_COLORS[entry.level] : "";
282
+ const levelLabel = LEVEL_LABELS[entry.level];
283
+ parts.push(colors ? `${levelColor}${levelLabel}${COLORS.reset}` : levelLabel);
284
+ }
285
+ if (showLogger && entry.loggerName) {
286
+ parts.push(colors ? `${COLORS.blue}[${entry.loggerName}]${COLORS.reset}` : `[${entry.loggerName}]`);
287
+ }
288
+ parts.push(entry.message);
289
+ if (showSource && entry.source) {
290
+ const { file, line, function: fn } = entry.source;
291
+ const loc = [file, line, fn].filter(Boolean).join(":");
292
+ if (loc) {
293
+ parts.push(colors ? `${COLORS.dim}(${loc})${COLORS.reset}` : `(${loc})`);
294
+ }
295
+ }
296
+ let result = parts.join(" ");
297
+ if (showContext && entry.context && Object.keys(entry.context).length > 0) {
298
+ const contextStr = JSON.stringify(entry.context, null, 2);
299
+ result += "\n" + (colors ? `${COLORS.dim}${contextStr}${COLORS.reset}` : contextStr);
300
+ }
301
+ return result;
302
+ }
303
+ function formatJson(entry) {
304
+ return JSON.stringify(entry);
305
+ }
306
+ function formatJsonPretty(entry) {
307
+ return JSON.stringify(entry, null, 2);
308
+ }
309
+ function getConsoleMethod(level) {
310
+ switch (level) {
311
+ case "trace":
312
+ return "trace";
313
+ case "debug":
314
+ return "debug";
315
+ case "info":
316
+ return "info";
317
+ case "warn":
318
+ return "warn";
319
+ case "error":
320
+ case "fatal":
321
+ return "error";
322
+ case "audit":
323
+ return "info";
324
+ default:
325
+ return "log";
326
+ }
327
+ }
328
+
329
+ // src/logger/destinations/console.ts
330
+ var ConsoleDestination = class {
331
+ constructor(config = {}) {
332
+ this.name = "console";
333
+ this.config = {
334
+ enabled: config.enabled !== false,
335
+ colors: config.colors !== false,
336
+ format: config.format || "pretty",
337
+ timestamps: config.timestamps !== false,
338
+ levels: config.levels || ["trace", "debug", "info", "warn", "error", "fatal", "audit"]
339
+ };
340
+ this.minLevel = 0;
341
+ }
342
+ /**
343
+ * Write a log entry to the console
344
+ */
345
+ write(entry) {
346
+ if (!this.config.enabled) {
347
+ return;
348
+ }
349
+ if (!this.config.levels.includes(entry.level)) {
350
+ return;
351
+ }
352
+ if (LOG_LEVELS[entry.level] < this.minLevel) {
353
+ return;
354
+ }
355
+ let output;
356
+ if (this.config.format === "json") {
357
+ output = formatJson(entry);
358
+ } else {
359
+ output = formatPretty(entry, {
360
+ colors: this.config.colors && this.supportsColors(),
361
+ timestamps: this.config.timestamps
362
+ });
363
+ }
364
+ const method = getConsoleMethod(entry.level);
365
+ console[method](output);
366
+ }
367
+ /**
368
+ * Check if the environment supports colors
369
+ */
370
+ supportsColors() {
371
+ if (typeof window !== "undefined") {
372
+ return true;
373
+ }
374
+ if (typeof process !== "undefined") {
375
+ if (process.stdout && "isTTY" in process.stdout) {
376
+ return Boolean(process.stdout.isTTY);
377
+ }
378
+ const env = process.env;
379
+ if (env.FORCE_COLOR !== void 0) {
380
+ return env.FORCE_COLOR !== "0";
381
+ }
382
+ if (env.NO_COLOR !== void 0) {
383
+ return false;
384
+ }
385
+ if (env.TERM === "dumb") {
386
+ return false;
387
+ }
388
+ return true;
389
+ }
390
+ return false;
391
+ }
392
+ /**
393
+ * Set minimum log level
394
+ */
395
+ setMinLevel(level) {
396
+ this.minLevel = LOG_LEVELS[level];
397
+ }
398
+ /**
399
+ * Enable or disable the destination
400
+ */
401
+ setEnabled(enabled) {
402
+ this.config.enabled = enabled;
403
+ }
404
+ /**
405
+ * Set color mode
406
+ */
407
+ setColors(enabled) {
408
+ this.config.colors = enabled;
409
+ }
410
+ /**
411
+ * Set output format
412
+ */
413
+ setFormat(format) {
414
+ this.config.format = format;
415
+ }
416
+ };
417
+
418
+ // src/logger/destinations/observe.ts
419
+ var DEFAULT_BATCH_SIZE = 50;
420
+ var DEFAULT_FLUSH_INTERVAL = 5e3;
421
+ var DEFAULT_SAMPLING = {
422
+ trace: 0.01,
423
+ // 1%
424
+ debug: 0.1,
425
+ // 10%
426
+ info: 0.5,
427
+ // 50%
428
+ warn: 1,
429
+ // 100%
430
+ error: 1,
431
+ // 100%
432
+ fatal: 1,
433
+ // 100%
434
+ audit: 1
435
+ // 100% - never sampled
436
+ };
437
+ var ObserveDestination = class {
438
+ constructor(dsn, config = {}) {
439
+ this.name = "observe";
440
+ this.queue = [];
441
+ this.isFlushing = false;
442
+ this.minLevel = 0;
443
+ this.dsn = dsn;
444
+ this.endpoint = this.parseEndpoint(dsn);
445
+ this.config = {
446
+ enabled: config.enabled !== false,
447
+ batchSize: config.batchSize || DEFAULT_BATCH_SIZE,
448
+ flushInterval: config.flushInterval || DEFAULT_FLUSH_INTERVAL,
449
+ sampling: { ...DEFAULT_SAMPLING, ...config.sampling },
450
+ levels: config.levels || ["trace", "debug", "info", "warn", "error", "fatal", "audit"]
451
+ };
452
+ this.startFlushTimer();
453
+ }
454
+ /**
455
+ * Parse DSN to construct endpoint
456
+ */
457
+ parseEndpoint(dsn) {
458
+ try {
459
+ const url = new URL(dsn);
460
+ return `${url.protocol}//${url.host}/api/v1/logs/ingest`;
461
+ } catch {
462
+ return "https://statly.live/api/v1/logs/ingest";
463
+ }
464
+ }
465
+ /**
466
+ * Start the flush timer
467
+ */
468
+ startFlushTimer() {
469
+ if (this.flushTimer) {
470
+ clearInterval(this.flushTimer);
471
+ }
472
+ if (typeof setInterval !== "undefined") {
473
+ this.flushTimer = setInterval(() => {
474
+ this.flush();
475
+ }, this.config.flushInterval);
476
+ }
477
+ }
478
+ /**
479
+ * Write a log entry (queues for batching)
480
+ */
481
+ write(entry) {
482
+ if (!this.config.enabled) {
483
+ return;
484
+ }
485
+ if (!this.config.levels.includes(entry.level)) {
486
+ return;
487
+ }
488
+ if (LOG_LEVELS[entry.level] < this.minLevel) {
489
+ return;
490
+ }
491
+ if (entry.level !== "audit") {
492
+ const sampleRate = this.config.sampling[entry.level] ?? 1;
493
+ if (Math.random() > sampleRate) {
494
+ return;
495
+ }
496
+ }
497
+ this.queue.push(entry);
498
+ if (this.queue.length >= this.config.batchSize) {
499
+ this.flush();
500
+ }
501
+ }
502
+ /**
503
+ * Flush all queued entries to the server
504
+ */
505
+ async flush() {
506
+ if (this.isFlushing || this.queue.length === 0) {
507
+ return;
508
+ }
509
+ this.isFlushing = true;
510
+ const entries = [...this.queue];
511
+ this.queue = [];
512
+ try {
513
+ await this.sendBatch(entries);
514
+ } catch (error2) {
515
+ const maxQueue = this.config.batchSize * 3;
516
+ this.queue = [...entries, ...this.queue].slice(0, maxQueue);
517
+ console.error("[Statly Logger] Failed to send logs:", error2);
518
+ } finally {
519
+ this.isFlushing = false;
520
+ }
521
+ }
522
+ /**
523
+ * Send a batch of entries to the server
524
+ */
525
+ async sendBatch(entries) {
526
+ if (entries.length === 0) {
527
+ return;
528
+ }
529
+ const response = await fetch(this.endpoint, {
530
+ method: "POST",
531
+ headers: {
532
+ "Content-Type": "application/json",
533
+ "X-Statly-DSN": this.dsn
534
+ },
535
+ body: JSON.stringify({ logs: entries }),
536
+ keepalive: true
537
+ });
538
+ if (!response.ok) {
539
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
540
+ }
541
+ }
542
+ /**
543
+ * Close the destination
544
+ */
545
+ async close() {
546
+ if (this.flushTimer) {
547
+ clearInterval(this.flushTimer);
548
+ }
549
+ await this.flush();
550
+ }
551
+ /**
552
+ * Set minimum log level
553
+ */
554
+ setMinLevel(level) {
555
+ this.minLevel = LOG_LEVELS[level];
556
+ }
557
+ /**
558
+ * Set sampling rate for a level
559
+ */
560
+ setSamplingRate(level, rate) {
561
+ this.config.sampling[level] = Math.max(0, Math.min(1, rate));
562
+ }
563
+ /**
564
+ * Enable or disable the destination
565
+ */
566
+ setEnabled(enabled) {
567
+ this.config.enabled = enabled;
568
+ }
569
+ /**
570
+ * Get the current queue size
571
+ */
572
+ getQueueSize() {
573
+ return this.queue.length;
574
+ }
575
+ };
576
+
577
+ // src/logger/destinations/file.ts
578
+ function parseSize(size) {
579
+ const match = size.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB|B)?$/i);
580
+ if (!match) return 10 * 1024 * 1024;
581
+ const value = parseFloat(match[1]);
582
+ const unit = (match[2] || "B").toUpperCase();
583
+ switch (unit) {
584
+ case "KB":
585
+ return value * 1024;
586
+ case "MB":
587
+ return value * 1024 * 1024;
588
+ case "GB":
589
+ return value * 1024 * 1024 * 1024;
590
+ default:
591
+ return value;
592
+ }
593
+ }
594
+ function formatDate(date) {
595
+ return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
596
+ }
597
+ var FileDestination = class {
598
+ constructor(config) {
599
+ this.name = "file";
600
+ this.minLevel = 0;
601
+ this.buffer = [];
602
+ this.currentSize = 0;
603
+ this.writePromise = Promise.resolve();
604
+ // File system operations (injected for Node.js compatibility)
605
+ this.fs = null;
606
+ this.config = {
607
+ enabled: config.enabled !== false,
608
+ path: config.path,
609
+ format: config.format || "json",
610
+ rotation: config.rotation || { type: "size", maxSize: "10MB", maxFiles: 5 },
611
+ levels: config.levels || ["trace", "debug", "info", "warn", "error", "fatal", "audit"]
612
+ };
613
+ this.maxSize = parseSize(this.config.rotation.maxSize || "10MB");
614
+ this.lastRotation = /* @__PURE__ */ new Date();
615
+ this.rotationInterval = this.getRotationInterval();
616
+ this.initFileSystem();
617
+ }
618
+ /**
619
+ * Initialize file system operations
620
+ */
621
+ async initFileSystem() {
622
+ if (typeof process !== "undefined" && process.versions?.node) {
623
+ try {
624
+ const fs = await import("fs/promises");
625
+ const path = await import("path");
626
+ const fsOps = {
627
+ appendFile: fs.appendFile,
628
+ rename: fs.rename,
629
+ stat: fs.stat,
630
+ mkdir: (p, opts) => fs.mkdir(p, opts),
631
+ readdir: fs.readdir,
632
+ unlink: fs.unlink
633
+ };
634
+ this.fs = fsOps;
635
+ const dir = path.dirname(this.config.path);
636
+ await fsOps.mkdir(dir, { recursive: true });
637
+ } catch {
638
+ console.warn("[Statly Logger] File destination not available (not Node.js)");
639
+ this.config.enabled = false;
640
+ }
641
+ } else {
642
+ this.config.enabled = false;
643
+ }
644
+ }
645
+ /**
646
+ * Get rotation interval in milliseconds
647
+ */
648
+ getRotationInterval() {
649
+ const { interval } = this.config.rotation;
650
+ switch (interval) {
651
+ case "hourly":
652
+ return 60 * 60 * 1e3;
653
+ case "daily":
654
+ return 24 * 60 * 60 * 1e3;
655
+ case "weekly":
656
+ return 7 * 24 * 60 * 60 * 1e3;
657
+ default:
658
+ return Infinity;
659
+ }
660
+ }
661
+ /**
662
+ * Write a log entry
663
+ */
664
+ write(entry) {
665
+ if (!this.config.enabled || !this.fs) {
666
+ return;
667
+ }
668
+ if (!this.config.levels.includes(entry.level)) {
669
+ return;
670
+ }
671
+ if (LOG_LEVELS[entry.level] < this.minLevel) {
672
+ return;
673
+ }
674
+ let line;
675
+ if (this.config.format === "json") {
676
+ line = formatJson(entry);
677
+ } else {
678
+ const date = new Date(entry.timestamp).toISOString();
679
+ line = `${date} [${entry.level.toUpperCase()}] ${entry.loggerName ? `[${entry.loggerName}] ` : ""}${entry.message}`;
680
+ if (entry.context && Object.keys(entry.context).length > 0) {
681
+ line += ` ${JSON.stringify(entry.context)}`;
682
+ }
683
+ }
684
+ this.buffer.push(line + "\n");
685
+ this.currentSize += line.length + 1;
686
+ if (this.buffer.length >= 100 || this.currentSize >= 64 * 1024) {
687
+ this.scheduleWrite();
688
+ }
689
+ }
690
+ /**
691
+ * Schedule a buffered write
692
+ */
693
+ scheduleWrite() {
694
+ this.writePromise = this.writePromise.then(() => this.writeBuffer());
695
+ }
696
+ /**
697
+ * Write buffer to file
698
+ */
699
+ async writeBuffer() {
700
+ if (!this.fs || this.buffer.length === 0) {
701
+ return;
702
+ }
703
+ await this.checkRotation();
704
+ const data = this.buffer.join("");
705
+ this.buffer = [];
706
+ this.currentSize = 0;
707
+ try {
708
+ await this.fs.appendFile(this.config.path, data);
709
+ } catch (error2) {
710
+ console.error("[Statly Logger] Failed to write to file:", error2);
711
+ }
712
+ }
713
+ /**
714
+ * Check if rotation is needed
715
+ */
716
+ async checkRotation() {
717
+ if (!this.fs) return;
718
+ const { type } = this.config.rotation;
719
+ let shouldRotate = false;
720
+ if (type === "size") {
721
+ try {
722
+ const stats = await this.fs.stat(this.config.path);
723
+ shouldRotate = stats.size >= this.maxSize;
724
+ } catch {
725
+ }
726
+ } else if (type === "time") {
727
+ const now = /* @__PURE__ */ new Date();
728
+ shouldRotate = now.getTime() - this.lastRotation.getTime() >= this.rotationInterval;
729
+ }
730
+ if (shouldRotate) {
731
+ await this.rotate();
732
+ }
733
+ }
734
+ /**
735
+ * Rotate the log file
736
+ */
737
+ async rotate() {
738
+ if (!this.fs) return;
739
+ try {
740
+ const rotatedPath = `${this.config.path}.${formatDate(/* @__PURE__ */ new Date())}`;
741
+ await this.fs.rename(this.config.path, rotatedPath);
742
+ this.lastRotation = /* @__PURE__ */ new Date();
743
+ await this.cleanupOldFiles();
744
+ } catch (error2) {
745
+ console.error("[Statly Logger] Failed to rotate file:", error2);
746
+ }
747
+ }
748
+ /**
749
+ * Clean up old rotated files
750
+ */
751
+ async cleanupOldFiles() {
752
+ if (!this.fs) return;
753
+ const { maxFiles, retentionDays } = this.config.rotation;
754
+ try {
755
+ const path = await import("path");
756
+ const dir = path.dirname(this.config.path);
757
+ const basename = path.basename(this.config.path);
758
+ const files = await this.fs.readdir(dir);
759
+ const rotatedFiles = files.filter((f) => f.startsWith(basename + ".")).map((f) => ({ name: f, path: path.join(dir, f) })).sort((a, b) => b.name.localeCompare(a.name));
760
+ if (maxFiles) {
761
+ for (const file of rotatedFiles.slice(maxFiles)) {
762
+ await this.fs.unlink(file.path);
763
+ }
764
+ }
765
+ if (retentionDays) {
766
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
767
+ for (const file of rotatedFiles) {
768
+ const match = file.name.match(/\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})$/);
769
+ if (match) {
770
+ const fileDate = new Date(match[1].replace("_", "T").replace(/-/g, ":"));
771
+ if (fileDate.getTime() < cutoff) {
772
+ await this.fs.unlink(file.path);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ } catch (error2) {
778
+ console.error("[Statly Logger] Failed to cleanup old files:", error2);
779
+ }
780
+ }
781
+ /**
782
+ * Flush buffered writes
783
+ */
784
+ async flush() {
785
+ this.scheduleWrite();
786
+ await this.writePromise;
787
+ }
788
+ /**
789
+ * Close the destination
790
+ */
791
+ async close() {
792
+ await this.flush();
793
+ }
794
+ /**
795
+ * Set minimum log level
796
+ */
797
+ setMinLevel(level) {
798
+ this.minLevel = LOG_LEVELS[level];
799
+ }
800
+ /**
801
+ * Enable or disable the destination
802
+ */
803
+ setEnabled(enabled) {
804
+ this.config.enabled = enabled;
805
+ }
806
+ };
807
+
808
+ // src/logger/ai/index.ts
809
+ var AIFeatures = class {
810
+ constructor(dsn, config = {}) {
811
+ this.dsn = dsn;
812
+ this.config = {
813
+ enabled: config.enabled ?? true,
814
+ apiKey: config.apiKey || "",
815
+ model: config.model || "claude-3-haiku-20240307",
816
+ endpoint: config.endpoint || this.parseEndpoint(dsn)
817
+ };
818
+ }
819
+ /**
820
+ * Parse DSN to construct AI endpoint
821
+ */
822
+ parseEndpoint(dsn) {
823
+ try {
824
+ const url = new URL(dsn);
825
+ return `${url.protocol}//${url.host}/api/v1/logs/ai`;
826
+ } catch {
827
+ return "https://statly.live/api/v1/logs/ai";
828
+ }
829
+ }
830
+ /**
831
+ * Explain an error using AI
832
+ */
833
+ async explainError(error2) {
834
+ if (!this.config.enabled) {
835
+ return {
836
+ summary: "AI features are disabled",
837
+ possibleCauses: []
838
+ };
839
+ }
840
+ const errorData = this.normalizeError(error2);
841
+ try {
842
+ const response = await fetch(`${this.config.endpoint}/explain`, {
843
+ method: "POST",
844
+ headers: {
845
+ "Content-Type": "application/json",
846
+ "X-Statly-DSN": this.dsn,
847
+ ...this.config.apiKey && { "X-AI-API-Key": this.config.apiKey }
848
+ },
849
+ body: JSON.stringify({
850
+ error: errorData,
851
+ model: this.config.model
852
+ })
853
+ });
854
+ if (!response.ok) {
855
+ throw new Error(`HTTP ${response.status}`);
856
+ }
857
+ return await response.json();
858
+ } catch (err) {
859
+ console.error("[Statly Logger AI] Failed to explain error:", err);
860
+ return {
861
+ summary: "Failed to get AI explanation",
862
+ possibleCauses: []
863
+ };
864
+ }
865
+ }
866
+ /**
867
+ * Suggest fixes for an error using AI
868
+ */
869
+ async suggestFix(error2, context) {
870
+ if (!this.config.enabled) {
871
+ return {
872
+ summary: "AI features are disabled",
873
+ suggestedFixes: []
874
+ };
875
+ }
876
+ const errorData = this.normalizeError(error2);
877
+ try {
878
+ const response = await fetch(`${this.config.endpoint}/suggest-fix`, {
879
+ method: "POST",
880
+ headers: {
881
+ "Content-Type": "application/json",
882
+ "X-Statly-DSN": this.dsn,
883
+ ...this.config.apiKey && { "X-AI-API-Key": this.config.apiKey }
884
+ },
885
+ body: JSON.stringify({
886
+ error: errorData,
887
+ context,
888
+ model: this.config.model
889
+ })
890
+ });
891
+ if (!response.ok) {
892
+ throw new Error(`HTTP ${response.status}`);
893
+ }
894
+ return await response.json();
895
+ } catch (err) {
896
+ console.error("[Statly Logger AI] Failed to suggest fix:", err);
897
+ return {
898
+ summary: "Failed to get AI fix suggestion",
899
+ suggestedFixes: []
900
+ };
901
+ }
902
+ }
903
+ /**
904
+ * Analyze a batch of logs for patterns
905
+ */
906
+ async analyzePatterns(logs) {
907
+ if (!this.config.enabled) {
908
+ return {
909
+ patterns: [],
910
+ summary: "AI features are disabled",
911
+ recommendations: []
912
+ };
913
+ }
914
+ try {
915
+ const response = await fetch(`${this.config.endpoint}/analyze-patterns`, {
916
+ method: "POST",
917
+ headers: {
918
+ "Content-Type": "application/json",
919
+ "X-Statly-DSN": this.dsn,
920
+ ...this.config.apiKey && { "X-AI-API-Key": this.config.apiKey }
921
+ },
922
+ body: JSON.stringify({
923
+ logs: logs.slice(0, 1e3),
924
+ // Limit to 1000 logs
925
+ model: this.config.model
926
+ })
927
+ });
928
+ if (!response.ok) {
929
+ throw new Error(`HTTP ${response.status}`);
930
+ }
931
+ return await response.json();
932
+ } catch (err) {
933
+ console.error("[Statly Logger AI] Failed to analyze patterns:", err);
934
+ return {
935
+ patterns: [],
936
+ summary: "Failed to analyze patterns",
937
+ recommendations: []
938
+ };
939
+ }
940
+ }
941
+ /**
942
+ * Normalize error input to a standard format
943
+ */
944
+ normalizeError(error2) {
945
+ if (typeof error2 === "string") {
946
+ return { message: error2 };
947
+ }
948
+ if (error2 instanceof Error) {
949
+ return {
950
+ message: error2.message,
951
+ stack: error2.stack,
952
+ type: error2.name
953
+ };
954
+ }
955
+ return {
956
+ message: error2.message,
957
+ type: error2.level,
958
+ context: error2.context
959
+ };
960
+ }
961
+ /**
962
+ * Set API key for AI features
963
+ */
964
+ setApiKey(apiKey) {
965
+ this.config.apiKey = apiKey;
966
+ }
967
+ /**
968
+ * Enable or disable AI features
969
+ */
970
+ setEnabled(enabled) {
971
+ this.config.enabled = enabled;
972
+ }
973
+ /**
974
+ * Check if AI features are enabled
975
+ */
976
+ isEnabled() {
977
+ return this.config.enabled;
978
+ }
979
+ };
980
+
981
+ // src/logger/logger.ts
982
+ var SDK_NAME = "@statly/observe";
983
+ var SDK_VERSION = "1.1.0";
984
+ var Logger = class _Logger {
985
+ constructor(config = {}) {
986
+ this.destinations = [];
987
+ this.ai = null;
988
+ this.context = {};
989
+ this.tags = {};
990
+ this.name = config.loggerName || "default";
991
+ this.config = config;
992
+ this.minLevel = LOG_LEVELS[config.level || "debug"];
993
+ this.enabledLevels = this.parseLevelSet(config.levels || "default");
994
+ this.scrubber = new Scrubber(config.scrubbing);
995
+ this.context = config.context || {};
996
+ this.tags = config.tags || {};
997
+ this.sessionId = this.generateId();
998
+ this.initDestinations();
999
+ if (config.dsn) {
1000
+ this.ai = new AIFeatures(config.dsn);
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Parse level set configuration
1005
+ */
1006
+ parseLevelSet(levels) {
1007
+ if (levels === "default") {
1008
+ return new Set(DEFAULT_LEVELS);
1009
+ }
1010
+ if (levels === "extended") {
1011
+ return new Set(EXTENDED_LEVELS);
1012
+ }
1013
+ return new Set(levels);
1014
+ }
1015
+ /**
1016
+ * Initialize destinations from config
1017
+ */
1018
+ initDestinations() {
1019
+ const { destinations } = this.config;
1020
+ if (!destinations || destinations.console?.enabled !== false) {
1021
+ this.destinations.push(new ConsoleDestination(destinations?.console));
1022
+ }
1023
+ if (destinations?.file?.enabled && destinations.file.path) {
1024
+ this.destinations.push(new FileDestination(destinations.file));
1025
+ }
1026
+ if (this.config.dsn && destinations?.observe?.enabled !== false) {
1027
+ this.destinations.push(new ObserveDestination(this.config.dsn, destinations?.observe));
1028
+ }
1029
+ }
1030
+ /**
1031
+ * Generate a unique ID
1032
+ */
1033
+ generateId() {
1034
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1035
+ return crypto.randomUUID();
1036
+ }
1037
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1038
+ const r = Math.random() * 16 | 0;
1039
+ const v = c === "x" ? r : r & 3 | 8;
1040
+ return v.toString(16);
1041
+ });
1042
+ }
1043
+ /**
1044
+ * Check if a level should be logged
1045
+ */
1046
+ shouldLog(level) {
1047
+ if (level === "audit") {
1048
+ return true;
1049
+ }
1050
+ if (LOG_LEVELS[level] < this.minLevel) {
1051
+ return false;
1052
+ }
1053
+ return this.enabledLevels.has(level);
1054
+ }
1055
+ /**
1056
+ * Get source location (if available)
1057
+ */
1058
+ getSource() {
1059
+ try {
1060
+ const err = new Error();
1061
+ const stack = err.stack?.split("\n");
1062
+ if (!stack || stack.length < 5) return void 0;
1063
+ for (let i = 3; i < stack.length; i++) {
1064
+ const frame = stack[i];
1065
+ if (!frame.includes("logger.ts") && !frame.includes("Logger.")) {
1066
+ const match = frame.match(/at\s+(?:(.+?)\s+\()?(.+?):(\d+)(?::\d+)?\)?/);
1067
+ if (match) {
1068
+ return {
1069
+ function: match[1] || void 0,
1070
+ file: match[2],
1071
+ line: parseInt(match[3], 10)
1072
+ };
1073
+ }
1074
+ }
1075
+ }
1076
+ } catch {
1077
+ }
1078
+ return void 0;
1079
+ }
1080
+ /**
1081
+ * Create a log entry
1082
+ */
1083
+ createEntry(level, message, context) {
1084
+ return {
1085
+ level,
1086
+ message: this.scrubber.scrubMessage(message),
1087
+ timestamp: Date.now(),
1088
+ loggerName: this.name,
1089
+ context: context ? this.scrubber.scrub({ ...this.context, ...context }) : this.scrubber.scrub(this.context),
1090
+ tags: this.tags,
1091
+ source: this.getSource(),
1092
+ traceId: this.traceId,
1093
+ spanId: this.spanId,
1094
+ sessionId: this.sessionId,
1095
+ environment: this.config.environment,
1096
+ release: this.config.release,
1097
+ sdkName: SDK_NAME,
1098
+ sdkVersion: SDK_VERSION
1099
+ };
1100
+ }
1101
+ /**
1102
+ * Write to all destinations
1103
+ */
1104
+ write(entry) {
1105
+ for (const dest of this.destinations) {
1106
+ try {
1107
+ dest.write(entry);
1108
+ } catch (error2) {
1109
+ console.error(`[Statly Logger] Failed to write to ${dest.name}:`, error2);
1110
+ }
1111
+ }
1112
+ }
1113
+ // ==================== Public Logging Methods ====================
1114
+ /**
1115
+ * Log a trace message
1116
+ */
1117
+ trace(message, context) {
1118
+ if (!this.shouldLog("trace")) return;
1119
+ this.write(this.createEntry("trace", message, context));
1120
+ }
1121
+ /**
1122
+ * Log a debug message
1123
+ */
1124
+ debug(message, context) {
1125
+ if (!this.shouldLog("debug")) return;
1126
+ this.write(this.createEntry("debug", message, context));
1127
+ }
1128
+ /**
1129
+ * Log an info message
1130
+ */
1131
+ info(message, context) {
1132
+ if (!this.shouldLog("info")) return;
1133
+ this.write(this.createEntry("info", message, context));
1134
+ }
1135
+ /**
1136
+ * Log a warning message
1137
+ */
1138
+ warn(message, context) {
1139
+ if (!this.shouldLog("warn")) return;
1140
+ this.write(this.createEntry("warn", message, context));
1141
+ }
1142
+ error(messageOrError, context) {
1143
+ if (!this.shouldLog("error")) return;
1144
+ if (messageOrError instanceof Error) {
1145
+ const entry = this.createEntry("error", messageOrError.message, {
1146
+ ...context,
1147
+ stack: messageOrError.stack,
1148
+ errorType: messageOrError.name
1149
+ });
1150
+ this.write(entry);
1151
+ } else {
1152
+ this.write(this.createEntry("error", messageOrError, context));
1153
+ }
1154
+ }
1155
+ fatal(messageOrError, context) {
1156
+ if (!this.shouldLog("fatal")) return;
1157
+ if (messageOrError instanceof Error) {
1158
+ const entry = this.createEntry("fatal", messageOrError.message, {
1159
+ ...context,
1160
+ stack: messageOrError.stack,
1161
+ errorType: messageOrError.name
1162
+ });
1163
+ this.write(entry);
1164
+ } else {
1165
+ this.write(this.createEntry("fatal", messageOrError, context));
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Log an audit message (always logged, never sampled)
1170
+ */
1171
+ audit(message, context) {
1172
+ this.write(this.createEntry("audit", message, context));
1173
+ }
1174
+ /**
1175
+ * Log at a specific level
1176
+ */
1177
+ log(level, message, context) {
1178
+ if (!this.shouldLog(level)) return;
1179
+ this.write(this.createEntry(level, message, context));
1180
+ }
1181
+ // ==================== Context & Tags ====================
1182
+ /**
1183
+ * Set persistent context
1184
+ */
1185
+ setContext(context) {
1186
+ this.context = { ...this.context, ...context };
1187
+ }
1188
+ /**
1189
+ * Clear context
1190
+ */
1191
+ clearContext() {
1192
+ this.context = {};
1193
+ }
1194
+ /**
1195
+ * Set a tag
1196
+ */
1197
+ setTag(key, value) {
1198
+ this.tags[key] = value;
1199
+ }
1200
+ /**
1201
+ * Set multiple tags
1202
+ */
1203
+ setTags(tags) {
1204
+ this.tags = { ...this.tags, ...tags };
1205
+ }
1206
+ /**
1207
+ * Clear tags
1208
+ */
1209
+ clearTags() {
1210
+ this.tags = {};
1211
+ }
1212
+ // ==================== Tracing ====================
1213
+ /**
1214
+ * Set trace ID for distributed tracing
1215
+ */
1216
+ setTraceId(traceId) {
1217
+ this.traceId = traceId;
1218
+ }
1219
+ /**
1220
+ * Set span ID
1221
+ */
1222
+ setSpanId(spanId) {
1223
+ this.spanId = spanId;
1224
+ }
1225
+ /**
1226
+ * Clear tracing context
1227
+ */
1228
+ clearTracing() {
1229
+ this.traceId = void 0;
1230
+ this.spanId = void 0;
1231
+ }
1232
+ // ==================== Child Loggers ====================
1233
+ /**
1234
+ * Create a child logger with additional context
1235
+ */
1236
+ child(options = {}) {
1237
+ const childConfig = {
1238
+ ...this.config,
1239
+ loggerName: options.name || `${this.name}.child`,
1240
+ context: { ...this.context, ...options.context },
1241
+ tags: { ...this.tags, ...options.tags }
1242
+ };
1243
+ const child = new _Logger(childConfig);
1244
+ child.traceId = this.traceId;
1245
+ child.spanId = this.spanId;
1246
+ child.sessionId = this.sessionId;
1247
+ return child;
1248
+ }
1249
+ // ==================== AI Features ====================
1250
+ /**
1251
+ * Explain an error using AI
1252
+ */
1253
+ async explainError(error2) {
1254
+ if (!this.ai) {
1255
+ return {
1256
+ summary: "AI features not available (no DSN configured)",
1257
+ possibleCauses: []
1258
+ };
1259
+ }
1260
+ return this.ai.explainError(error2);
1261
+ }
1262
+ /**
1263
+ * Suggest fixes for an error using AI
1264
+ */
1265
+ async suggestFix(error2, context) {
1266
+ if (!this.ai) {
1267
+ return {
1268
+ summary: "AI features not available (no DSN configured)",
1269
+ suggestedFixes: []
1270
+ };
1271
+ }
1272
+ return this.ai.suggestFix(error2, context);
1273
+ }
1274
+ /**
1275
+ * Configure AI features
1276
+ */
1277
+ configureAI(config) {
1278
+ if (this.ai) {
1279
+ if (config.apiKey) this.ai.setApiKey(config.apiKey);
1280
+ if (config.enabled !== void 0) this.ai.setEnabled(config.enabled);
1281
+ }
1282
+ }
1283
+ // ==================== Destination Management ====================
1284
+ /**
1285
+ * Add a custom destination
1286
+ */
1287
+ addDestination(destination) {
1288
+ this.destinations.push(destination);
1289
+ }
1290
+ /**
1291
+ * Remove a destination by name
1292
+ */
1293
+ removeDestination(name) {
1294
+ this.destinations = this.destinations.filter((d) => d.name !== name);
1295
+ }
1296
+ /**
1297
+ * Get all destinations
1298
+ */
1299
+ getDestinations() {
1300
+ return [...this.destinations];
1301
+ }
1302
+ // ==================== Level Configuration ====================
1303
+ /**
1304
+ * Set minimum log level
1305
+ */
1306
+ setLevel(level) {
1307
+ this.minLevel = LOG_LEVELS[level];
1308
+ }
1309
+ /**
1310
+ * Get current minimum level
1311
+ */
1312
+ getLevel() {
1313
+ const entries = Object.entries(LOG_LEVELS);
1314
+ const entry = entries.find(([, value]) => value === this.minLevel);
1315
+ return entry ? entry[0] : "debug";
1316
+ }
1317
+ /**
1318
+ * Check if a level is enabled
1319
+ */
1320
+ isLevelEnabled(level) {
1321
+ return this.shouldLog(level);
1322
+ }
1323
+ // ==================== Lifecycle ====================
1324
+ /**
1325
+ * Flush all destinations
1326
+ */
1327
+ async flush() {
1328
+ await Promise.all(
1329
+ this.destinations.filter((d) => d.flush).map((d) => d.flush())
1330
+ );
1331
+ }
1332
+ /**
1333
+ * Close the logger and all destinations
1334
+ */
1335
+ async close() {
1336
+ await Promise.all(
1337
+ this.destinations.filter((d) => d.close).map((d) => d.close())
1338
+ );
1339
+ }
1340
+ /**
1341
+ * Get logger name
1342
+ */
1343
+ getName() {
1344
+ return this.name;
1345
+ }
1346
+ /**
1347
+ * Get session ID
1348
+ */
1349
+ getSessionId() {
1350
+ return this.sessionId;
1351
+ }
1352
+ };
1353
+
1354
+ // src/logger/index.ts
1355
+ var defaultLogger = null;
1356
+ function getDefaultLogger() {
1357
+ if (!defaultLogger) {
1358
+ defaultLogger = new Logger();
1359
+ }
1360
+ return defaultLogger;
1361
+ }
1362
+ function setDefaultLogger(logger) {
1363
+ defaultLogger = logger;
1364
+ }
1365
+ function trace(message, context) {
1366
+ getDefaultLogger().trace(message, context);
1367
+ }
1368
+ function debug(message, context) {
1369
+ getDefaultLogger().debug(message, context);
1370
+ }
1371
+ function info(message, context) {
1372
+ getDefaultLogger().info(message, context);
1373
+ }
1374
+ function warn(message, context) {
1375
+ getDefaultLogger().warn(message, context);
1376
+ }
1377
+ function error(messageOrError, context) {
1378
+ if (messageOrError instanceof Error) {
1379
+ getDefaultLogger().error(messageOrError, context);
1380
+ } else {
1381
+ getDefaultLogger().error(messageOrError, context);
1382
+ }
1383
+ }
1384
+ function fatal(messageOrError, context) {
1385
+ if (messageOrError instanceof Error) {
1386
+ getDefaultLogger().fatal(messageOrError, context);
1387
+ } else {
1388
+ getDefaultLogger().fatal(messageOrError, context);
1389
+ }
1390
+ }
1391
+ function audit(message, context) {
1392
+ getDefaultLogger().audit(message, context);
1393
+ }
1394
+
1395
+ export {
1396
+ LOG_LEVELS,
1397
+ DEFAULT_LEVELS,
1398
+ EXTENDED_LEVELS,
1399
+ SENSITIVE_KEYS,
1400
+ SCRUB_PATTERNS,
1401
+ REDACTED,
1402
+ isSensitiveKey,
1403
+ Scrubber,
1404
+ formatPretty,
1405
+ formatJson,
1406
+ formatJsonPretty,
1407
+ getConsoleMethod,
1408
+ ConsoleDestination,
1409
+ ObserveDestination,
1410
+ FileDestination,
1411
+ AIFeatures,
1412
+ Logger,
1413
+ getDefaultLogger,
1414
+ setDefaultLogger,
1415
+ trace,
1416
+ debug,
1417
+ info,
1418
+ warn,
1419
+ error,
1420
+ fatal,
1421
+ audit
1422
+ };