agent-telemetry 0.1.0 → 0.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.
package/src/writer.ts CHANGED
@@ -10,23 +10,32 @@
10
10
  */
11
11
 
12
12
  export interface WriterConfig {
13
- logDir: string
14
- filename: string
15
- maxSize: number
16
- maxBackups: number
17
- prefix: string
13
+ logDir: string;
14
+ filename: string;
15
+ maxSize: number;
16
+ maxBackups: number;
17
+ prefix: string;
18
18
  }
19
19
 
20
20
  export interface Writer {
21
- write: (line: string) => void
21
+ write: (line: string) => void;
22
22
  }
23
23
 
24
24
  const DEFAULTS: WriterConfig = {
25
- logDir: 'logs',
26
- filename: 'telemetry.jsonl',
25
+ logDir: "logs",
26
+ filename: "telemetry.jsonl",
27
27
  maxSize: 5_000_000,
28
28
  maxBackups: 3,
29
- prefix: '[TEL]',
29
+ prefix: "[TEL]",
30
+ };
31
+
32
+ function hasErrnoCode(err: unknown, code: string): boolean {
33
+ return (
34
+ typeof err === "object" &&
35
+ err !== null &&
36
+ "code" in err &&
37
+ (err as { code?: unknown }).code === code
38
+ );
30
39
  }
31
40
 
32
41
  /**
@@ -40,92 +49,158 @@ export async function createWriter(config?: Partial<WriterConfig>): Promise<Writ
40
49
  maxSize: config?.maxSize ?? DEFAULTS.maxSize,
41
50
  maxBackups: config?.maxBackups ?? DEFAULTS.maxBackups,
42
51
  prefix: config?.prefix ?? DEFAULTS.prefix,
43
- }
52
+ };
44
53
  const writeToConsole = (line: string): void => {
45
54
  // biome-ignore lint/suspicious/noConsole: intentional fallback for runtimes without filesystem
46
- console.log(`${cfg.prefix} ${line}`)
47
- }
55
+ console.log(`${cfg.prefix} ${line}`);
56
+ };
48
57
 
49
58
  try {
50
- const fs = await import('node:fs')
51
- const path = await import('node:path')
59
+ const fs = await import("node:fs");
60
+ const fsPromises = await import("node:fs/promises");
61
+ const path = await import("node:path");
52
62
 
53
- const logDir = path.resolve(cfg.logDir)
54
- const logFile = path.join(logDir, cfg.filename)
63
+ const logDir = path.resolve(cfg.logDir);
64
+ const logFile = path.join(logDir, cfg.filename);
55
65
 
56
66
  // Probe: verify filesystem actually works
57
67
  // (Cloudflare's nodejs_compat stubs succeed silently)
58
- fs.mkdirSync(logDir, { recursive: true })
68
+ await fsPromises.mkdir(logDir, { recursive: true });
59
69
  if (!fs.existsSync(logDir)) {
60
- throw new Error('Filesystem probe failed')
70
+ throw new Error("Filesystem probe failed");
61
71
  }
62
72
 
63
- let useConsoleFallback = false
73
+ let useConsoleFallback = false;
74
+ let flushScheduled = false;
75
+ let flushInProgress = false;
76
+ let sizeCache: number | undefined;
77
+ let pending: string[] = [];
78
+
79
+ const unlinkIfExists = async (filePath: string): Promise<void> => {
80
+ try {
81
+ await fsPromises.unlink(filePath);
82
+ } catch (err) {
83
+ if (!hasErrnoCode(err, "ENOENT")) throw err;
84
+ }
85
+ };
86
+
87
+ const fileExists = async (filePath: string): Promise<boolean> => {
88
+ try {
89
+ await fsPromises.access(filePath);
90
+ return true;
91
+ } catch (err) {
92
+ if (hasErrnoCode(err, "ENOENT")) return false;
93
+ throw err;
94
+ }
95
+ };
96
+
97
+ const getCurrentSize = async (): Promise<number> => {
98
+ if (sizeCache !== undefined) return sizeCache;
99
+ try {
100
+ sizeCache = (await fsPromises.stat(logFile)).size;
101
+ } catch (err) {
102
+ if (!hasErrnoCode(err, "ENOENT")) throw err;
103
+ sizeCache = 0;
104
+ }
105
+ return sizeCache;
106
+ };
64
107
 
65
- const rotate = (): void => {
66
- if (!fs.existsSync(logFile)) return
108
+ const rotate = async (): Promise<void> => {
109
+ if (!(await fileExists(logFile))) {
110
+ sizeCache = 0;
111
+ return;
112
+ }
67
113
 
68
114
  if (cfg.maxBackups <= 0) {
69
- fs.unlinkSync(logFile)
70
- return
115
+ await unlinkIfExists(logFile);
116
+ sizeCache = 0;
117
+ return;
71
118
  }
72
119
 
73
- const oldestBackup = `${logFile}.${cfg.maxBackups}`
74
- if (fs.existsSync(oldestBackup)) {
75
- fs.unlinkSync(oldestBackup)
76
- }
120
+ const oldestBackup = `${logFile}.${cfg.maxBackups}`;
121
+ await unlinkIfExists(oldestBackup);
77
122
 
78
123
  for (let i = cfg.maxBackups - 1; i >= 1; i--) {
79
- const from = `${logFile}.${i}`
80
- const to = `${logFile}.${i + 1}`
81
- if (fs.existsSync(from)) {
82
- fs.renameSync(from, to)
124
+ const from = `${logFile}.${i}`;
125
+ const to = `${logFile}.${i + 1}`;
126
+ if (await fileExists(from)) {
127
+ await fsPromises.rename(from, to);
83
128
  }
84
129
  }
85
130
 
86
- fs.renameSync(logFile, `${logFile}.1`)
87
- }
131
+ await fsPromises.rename(logFile, `${logFile}.1`);
132
+ sizeCache = 0;
133
+ };
88
134
 
89
- return {
90
- write(line: string) {
91
- if (useConsoleFallback) {
92
- writeToConsole(line)
93
- return
94
- }
135
+ const scheduleFlush = (): void => {
136
+ if (flushScheduled || flushInProgress || useConsoleFallback) return;
137
+ flushScheduled = true;
138
+ queueMicrotask(() => {
139
+ flushScheduled = false;
140
+ void flushPending();
141
+ });
142
+ };
95
143
 
96
- try {
97
- const lineWithNewline = `${line}\n`
98
- const incomingSize = Buffer.byteLength(lineWithNewline)
99
- let currentSize = 0
144
+ const flushPending = async (): Promise<void> => {
145
+ if (flushInProgress || useConsoleFallback) return;
146
+ flushInProgress = true;
147
+
148
+ try {
149
+ while (pending.length > 0 && !useConsoleFallback) {
150
+ const batch = pending;
151
+ pending = [];
152
+
153
+ const chunk = batch.map((line) => `${line}\n`).join("");
154
+ const incomingSize = Buffer.byteLength(chunk);
100
155
 
101
156
  try {
102
- currentSize = fs.statSync(logFile).size
103
- } catch (err) {
104
- const isEnoent =
105
- typeof err === 'object' &&
106
- err !== null &&
107
- 'code' in err &&
108
- (err as { code?: unknown }).code === 'ENOENT'
109
- if (!isEnoent) {
110
- throw err
157
+ let currentSize = await getCurrentSize();
158
+ if (cfg.maxSize > 0 && currentSize + incomingSize > cfg.maxSize) {
159
+ await rotate();
160
+ currentSize = 0;
161
+ }
162
+
163
+ await fsPromises.appendFile(logFile, chunk);
164
+ sizeCache = currentSize + incomingSize;
165
+ } catch {
166
+ useConsoleFallback = true;
167
+ for (const line of batch) {
168
+ writeToConsole(line);
111
169
  }
170
+ for (const line of pending) {
171
+ writeToConsole(line);
172
+ }
173
+ pending = [];
174
+ sizeCache = undefined;
112
175
  }
176
+ }
177
+ } finally {
178
+ flushInProgress = false;
179
+ if (pending.length > 0 && !useConsoleFallback) {
180
+ scheduleFlush();
181
+ }
182
+ }
183
+ };
113
184
 
114
- if (cfg.maxSize > 0 && currentSize + incomingSize > cfg.maxSize) {
115
- rotate()
185
+ return {
186
+ write(line: string) {
187
+ try {
188
+ if (useConsoleFallback) {
189
+ writeToConsole(line);
190
+ return;
116
191
  }
117
192
 
118
- fs.appendFileSync(logFile, lineWithNewline)
193
+ pending.push(line);
194
+ scheduleFlush();
119
195
  } catch {
120
- useConsoleFallback = true
121
- writeToConsole(line)
196
+ writeToConsole(line);
122
197
  }
123
198
  },
124
- }
199
+ };
125
200
  } catch {
126
201
  // Import failed or filesystem probe failed — console fallback
127
202
  return {
128
203
  write: writeToConsole,
129
- }
204
+ };
130
205
  }
131
206
  }