| ... | @@ -74,184 +74,180 @@ function resolveStreamLabel(stream: EventNdjsonStream): string { | ... | @@ -74,184 +74,180 @@ function resolveStreamLabel(stream: EventNdjsonStream): string { |
| 74 | } | 74 | } |
| 75 | } | 75 | } |
| 76 | | 76 | |
| 77 | function toLogMessage(event: unknown): Effect.Effect<string | undefined> { | 77 | const toLogMessage = Effect.fn("toLogMessage")(function* ( |
| 78 | return Effect.gen(function* () { | 78 | event: unknown, |
| 79 | const serialized = yield* Effect.sync(() => { | 79 | ): Effect.fn.Return<string | undefined> { |
| 80 | try { | 80 | const serialized = yield* Effect.sync(() => { |
| 81 | return { ok: true as const, value: JSON.stringify(event) }; | 81 | try { |
| 82 | } catch (error) { | 82 | return { ok: true as const, value: JSON.stringify(event) }; |
| 83 | return { ok: false as const, error }; | 83 | } catch (error) { |
| 84 | } | 84 | return { ok: false as const, error }; |
| 85 | }); | | |
| 86 | | | |
| 87 | if (!serialized.ok) { | | |
| 88 | yield* logWarning("failed to serialize provider event log record", { | | |
| 89 | error: serialized.error, | | |
| 90 | }); | | |
| 91 | return undefined; | | |
| 92 | } | 85 | } |
| | 86 | }); |
| 93 | | 87 | |
| 94 | if (typeof serialized.value !== "string") { | 88 | if (!serialized.ok) { |
| 95 | return undefined; | 89 | yield* logWarning("failed to serialize provider event log record", { |
| 96 | } | 90 | error: serialized.error, |
| | 91 | }); |
| | 92 | return undefined; |
| | 93 | } |
| 97 | | 94 | |
| 98 | return serialized.value; | 95 | if (typeof serialized.value !== "string") { |
| 99 | }); | 96 | return undefined; |
| 100 | } | 97 | } |
| | 98 | |
| | 99 | return serialized.value; |
| | 100 | }); |
| 101 | | 101 | |
| 102 | function makeThreadWriter(input: { | 102 | const makeThreadWriter = Effect.fn("makeThreadWriter")(function* (input: { |
| 103 | readonly filePath: string; | 103 | readonly filePath: string; |
| 104 | readonly maxBytes: number; | 104 | readonly maxBytes: number; |
| 105 | readonly maxFiles: number; | 105 | readonly maxFiles: number; |
| 106 | readonly batchWindowMs: number; | 106 | readonly batchWindowMs: number; |
| 107 | readonly streamLabel: string; | 107 | readonly streamLabel: string; |
| 108 | }): Effect.Effect<ThreadWriter | undefined> { | 108 | }): Effect.fn.Return<ThreadWriter | undefined> { |
| 109 | return Effect.gen(function* () { | 109 | const sinkResult = yield* Effect.sync(() => { |
| 110 | const sinkResult = yield* Effect.sync(() => { | 110 | try { |
| 111 | try { | 111 | return { |
| 112 | return { | 112 | ok: true as const, |
| 113 | ok: true as const, | 113 | sink: new RotatingFileSink({ |
| 114 | sink: new RotatingFileSink({ | 114 | filePath: input.filePath, |
| 115 | filePath: input.filePath, | 115 | maxBytes: input.maxBytes, |
| 116 | maxBytes: input.maxBytes, | 116 | maxFiles: input.maxFiles, |
| 117 | maxFiles: input.maxFiles, | 117 | throwOnError: true, |
| 118 | throwOnError: true, | 118 | }), |
| 119 | }), | 119 | }; |
| 120 | }; | 120 | } catch (error) { |
| 121 | } catch (error) { | 121 | return { ok: false as const, error }; |
| 122 | return { ok: false as const, error }; | | |
| 123 | } | | |
| 124 | }); | | |
| 125 | | | |
| 126 | if (!sinkResult.ok) { | | |
| 127 | yield* logWarning("failed to initialize provider thread log file", { | | |
| 128 | filePath: input.filePath, | | |
| 129 | error: sinkResult.error, | | |
| 130 | }); | | |
| 131 | return undefined; | | |
| 132 | } | 122 | } |
| | 123 | }); |
| | 124 | |
| | 125 | if (!sinkResult.ok) { |
| | 126 | yield* logWarning("failed to initialize provider thread log file", { |
| | 127 | filePath: input.filePath, |
| | 128 | error: sinkResult.error, |
| | 129 | }); |
| | 130 | return undefined; |
| | 131 | } |
| 133 | | 132 | |
| 134 | const sink = sinkResult.sink; | 133 | const sink = sinkResult.sink; |
| 135 | const scope = yield* Scope.make(); | 134 | const scope = yield* Scope.make(); |
| 136 | const lineLogger = makeLineLogger(input.streamLabel); | 135 | const lineLogger = makeLineLogger(input.streamLabel); |
| 137 | const batchedLogger = yield* Logger.batched(lineLogger, { | 136 | const batchedLogger = yield* Logger.batched(lineLogger, { |
| 138 | window: input.batchWindowMs, | 137 | window: input.batchWindowMs, |
| 139 | flush: (messages) => | 138 | flush: Effect.fn("makeThreadWriter.flush")(function* (messages) { |
| 140 | Effect.gen(function* () { | 139 | const flushResult = yield* Effect.sync(() => { |
| 141 | const flushResult = yield* Effect.sync(() => { | 140 | try { |
| 142 | try { | 141 | for (const message of messages) { |
| 143 | for (const message of messages) { | 142 | sink.write(message); |
| 144 | sink.write(message); | | |
| 145 | } | | |
| 146 | return { ok: true as const }; | | |
| 147 | } catch (error) { | | |
| 148 | return { ok: false as const, error }; | | |
| 149 | } | | |
| 150 | }); | | |
| 151 | | | |
| 152 | if (!flushResult.ok) { | | |
| 153 | yield* logWarning("provider event log batch flush failed", { | | |
| 154 | filePath: input.filePath, | | |
| 155 | error: flushResult.error, | | |
| 156 | }); | | |
| 157 | } | 143 | } |
| 158 | }), | 144 | return { ok: true as const }; |
| 159 | }).pipe(Effect.provideService(Scope.Scope, scope)); | 145 | } catch (error) { |
| 160 | | 146 | return { ok: false as const, error }; |
| 161 | const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false }); | 147 | } |
| 162 | | 148 | }); |
| 163 | return { | | |
| 164 | writeMessage(message: string) { | | |
| 165 | return Effect.log(message).pipe(Effect.provide(loggerLayer)); | | |
| 166 | }, | | |
| 167 | close() { | | |
| 168 | return Scope.close(scope, Exit.void); | | |
| 169 | }, | | |
| 170 | } satisfies ThreadWriter; | | |
| 171 | }); | | |
| 172 | } | | |
| 173 | | 149 | |
| 174 | export function makeEventNdjsonLogger( | 150 | if (!flushResult.ok) { |
| | 151 | yield* logWarning("provider event log batch flush failed", { |
| | 152 | filePath: input.filePath, |
| | 153 | error: flushResult.error, |
| | 154 | }); |
| | 155 | } |
| | 156 | }), |
| | 157 | }).pipe(Effect.provideService(Scope.Scope, scope)); |
| | 158 | |
| | 159 | const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false }); |
| | 160 | |
| | 161 | return { |
| | 162 | writeMessage(message: string) { |
| | 163 | return Effect.log(message).pipe(Effect.provide(loggerLayer)); |
| | 164 | }, |
| | 165 | close() { |
| | 166 | return Scope.close(scope, Exit.void); |
| | 167 | }, |
| | 168 | } satisfies ThreadWriter; |
| | 169 | }); |
| | 170 | |
| | 171 | export const makeEventNdjsonLogger = Effect.fn("makeEventNdjsonLogger")(function* ( |
| 175 | filePath: string, | 172 | filePath: string, |
| 176 | options: EventNdjsonLoggerOptions, | 173 | options: EventNdjsonLoggerOptions, |
| 177 | ): Effect.Effect<EventNdjsonLogger | undefined> { | 174 | ): Effect.fn.Return<EventNdjsonLogger | undefined> { |
| 178 | return Effect.gen(function* () { | 175 | const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; |
| 179 | const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; | 176 | const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; |
| 180 | const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; | 177 | const batchWindowMs = options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS; |
| 181 | const batchWindowMs = options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS; | 178 | const streamLabel = resolveStreamLabel(options.stream); |
| 182 | const streamLabel = resolveStreamLabel(options.stream); | 179 | |
| 183 | | 180 | const directoryReady = yield* Effect.sync(() => { |
| 184 | const directoryReady = yield* Effect.sync(() => { | 181 | try { |
| 185 | try { | 182 | fs.mkdirSync(path.dirname(filePath), { recursive: true }); |
| 186 | fs.mkdirSync(path.dirname(filePath), { recursive: true }); | 183 | return true; |
| 187 | return true; | 184 | } catch (error) { |
| 188 | } catch (error) { | 185 | return { ok: false as const, error }; |
| 189 | return { ok: false as const, error }; | 186 | } |
| 190 | } | 187 | }); |
| | 188 | if (directoryReady !== true) { |
| | 189 | yield* logWarning("failed to create provider event log directory", { |
| | 190 | filePath, |
| | 191 | error: directoryReady.error, |
| 191 | }); | 192 | }); |
| 192 | if (directoryReady !== true) { | 193 | return undefined; |
| 193 | yield* logWarning("failed to create provider event log directory", { | 194 | } |
| 194 | filePath, | 195 | |
| 195 | error: directoryReady.error, | 196 | const threadWriters = new Map<string, ThreadWriter>(); |
| 196 | }); | 197 | const failedSegments = new Set<string>(); |
| | 198 | |
| | 199 | const resolveThreadWriter = Effect.fn("resolveThreadWriter")(function* ( |
| | 200 | threadSegment: string, |
| | 201 | ): Effect.fn.Return<ThreadWriter | undefined> { |
| | 202 | if (failedSegments.has(threadSegment)) { |
| 197 | return undefined; | 203 | return undefined; |
| 198 | } | 204 | } |
| | 205 | const existing = threadWriters.get(threadSegment); |
| | 206 | if (existing) { |
| | 207 | return existing; |
| | 208 | } |
| 199 | | 209 | |
| 200 | const threadWriters = new Map<string, ThreadWriter>(); | 210 | const writer = yield* makeThreadWriter({ |
| 201 | const failedSegments = new Set<string>(); | 211 | filePath: path.join(path.dirname(filePath), `${threadSegment}.log`), |
| 202 | | 212 | maxBytes, |
| 203 | const resolveThreadWriter = (threadSegment: string): Effect.Effect<ThreadWriter | undefined> => | 213 | maxFiles, |
| 204 | Effect.gen(function* () { | 214 | batchWindowMs, |
| 205 | if (failedSegments.has(threadSegment)) { | 215 | streamLabel, |
| 206 | return undefined; | 216 | }); |
| 207 | } | 217 | if (!writer) { |
| 208 | const existing = threadWriters.get(threadSegment); | 218 | failedSegments.add(threadSegment); |
| 209 | if (existing) { | 219 | return undefined; |
| 210 | return existing; | 220 | } |
| 211 | } | | |
| 212 | | 221 | |
| 213 | const writer = yield* makeThreadWriter({ | 222 | threadWriters.set(threadSegment, writer); |
| 214 | filePath: path.join(path.dirname(filePath), `${threadSegment}.log`), | 223 | return writer; |
| 215 | maxBytes, | 224 | }); |
| 216 | maxFiles, | | |
| 217 | batchWindowMs, | | |
| 218 | streamLabel, | | |
| 219 | }); | | |
| 220 | if (!writer) { | | |
| 221 | failedSegments.add(threadSegment); | | |
| 222 | return undefined; | | |
| 223 | } | | |
| 224 | | 225 | |
| 225 | threadWriters.set(threadSegment, writer); | 226 | const write = Effect.fn("write")(function* (event: unknown, threadId: ThreadId | null) { |
| 226 | return writer; | 227 | const threadSegment = resolveThreadSegment(threadId); |
| 227 | }); | 228 | const message = yield* toLogMessage(event); |
| | 229 | if (!message) { |
| | 230 | return; |
| | 231 | } |
| 228 | | 232 | |
| 229 | return { | 233 | const writer = yield* resolveThreadWriter(threadSegment); |
| 230 | filePath, | 234 | if (!writer) { |
| 231 | write(event: unknown, threadId: ThreadId | null) { | 235 | return; |
| 232 | return Effect.gen(function* () { | 236 | } |
| 233 | const threadSegment = resolveThreadSegment(threadId); | | |
| 234 | const message = yield* toLogMessage(event); | | |
| 235 | if (!message) { | | |
| 236 | return; | | |
| 237 | } | | |
| 238 | | 237 | |
| 239 | const writer = yield* resolveThreadWriter(threadSegment); | 238 | yield* writer.writeMessage(message); |
| 240 | if (!writer) { | 239 | }); |
| 241 | return; | | |
| 242 | } | | |
| 243 | | 240 | |
| 244 | yield* writer.writeMessage(message); | 241 | const close = Effect.fn("close")(function* () { |
| 245 | }); | 242 | for (const writer of threadWriters.values()) { |
| 246 | }, | 243 | yield* writer.close(); |
| 247 | close() { | 244 | } |
| 248 | return Effect.gen(function* () { | 245 | threadWriters.clear(); |
| 249 | for (const writer of threadWriters.values()) { | | |
| 250 | yield* writer.close(); | | |
| 251 | } | | |
| 252 | threadWriters.clear(); | | |
| 253 | }); | | |
| 254 | }, | | |
| 255 | } satisfies EventNdjsonLogger; | | |
| 256 | }); | 246 | }); |
| 257 | } | 247 | |
| | 248 | return { |
| | 249 | filePath, |
| | 250 | write, |
| | 251 | close, |
| | 252 | } satisfies EventNdjsonLogger; |
| | 253 | }); |