-
Notifications
You must be signed in to change notification settings - Fork 57
fix(fs): disable the drain when its directory is not writable #509
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "evlog": patch | ||
| --- | ||
|
|
||
| The file system drain disables itself when its directory is not writable. | ||
|
|
||
| `createFsDrain()` guarded neither its `mkdir` nor its `appendFile`, so attaching it on a serverless host — where everything outside the temp directory is read-only — threw once per batch for the lifetime of the deployment, and the events went nowhere regardless. Callers had to guess at the environment to avoid it: | ||
|
|
||
| ```ts | ||
| // no longer needed | ||
| const drain = process.env.VERCEL !== '1' ? createFsDrain() : undefined | ||
| ``` | ||
|
|
||
| The drain now probes the directory once, and on `EROFS`, `EACCES` or `EPERM` warns a single time and stops, the same way it already bows out of the Edge runtime. Attach it unconditionally. Any other failure — a full disk, a genuine bug — still propagates. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ const FS_FIELDS: ConfigField<FsConfig>[] = [ | |
| ] | ||
|
|
||
| const gitignoreWritten = new Set<string>() | ||
| const writableDirs = new Map<string, boolean>() | ||
| let warnedFsEdgeRuntime = false | ||
|
|
||
| function isEdgeRuntime(): boolean { | ||
|
|
@@ -38,6 +39,36 @@ function warnFsEdgeRuntimeOnce(): void { | |
| console.warn('[evlog/fs] File system drain is not available on the Edge runtime. Use evlog/memory or a HTTP adapter instead.') | ||
| } | ||
|
|
||
| /** Read-only or permission-denied, as opposed to a full disk or a real bug. */ | ||
| function isUnwritableError(error: unknown): boolean { | ||
| const code = (error as NodeJS.ErrnoException | null)?.code | ||
| return code === 'EROFS' || code === 'EACCES' || code === 'EPERM' | ||
| } | ||
|
|
||
| /** | ||
| * Whether `dir` can be created and written to, probed once per directory. | ||
| * | ||
| * Serverless hosts mount everything outside the temp directory read-only, so a | ||
| * drain attached there would otherwise throw on every batch for the lifetime of | ||
| * the deployment. Anything other than a permission failure still propagates. | ||
| */ | ||
| async function isDirWritable(dir: string): Promise<boolean> { | ||
| const cached = writableDirs.get(dir) | ||
| if (cached !== undefined) return cached | ||
|
|
||
| try { | ||
| await mkdir(dir, { recursive: true }) | ||
|
Comment on lines
+55
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
curl -fsSL 'https://nodejs.org/download/release/v18.20.8/docs/api/fs.html' |
rg -n 'Calling .*fsPromises\.mkdir.*recursive.*false'Repository: HugoRCD/evlog Length of output: 151 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Tracked fs adapter candidates:\n'
fd -a 'fs\.ts$' . | sed 's#^\./##' | head -50
printf '\nInspect target file size and relevant sections:\n'
wc -l packages/evlog/src/adapters/fs.ts
sed -n '1,140p' packages/evlog/src/adapters/fs.ts
printf '\nSearch writableDirs and isDirWritable usages:\n'
rg -n "writableDirs|isDirWritable|mkdir\(dir|recursive" packages/evlog/src/adapters/fs.ts packages -g '*.ts' -g '*.tsx' | head -120
printf '\nFind writableDirs usages across repo:\n'
rg -n "writableDirs" . -g '*.ts' -g '*.tsx' -g '*.test.ts' -g '*.test.tsx' -g '*.spec.ts' -g '*.spec.tsx' | head -200Repository: HugoRCD/evlog Length of output: 11451 🌐 Web query:
💡 Result: In Node.js 18 (and other versions), calling fsPromises.mkdir with the { recursive: true } option will not reject if the target directory already exists [1][2]. The promise will resolve successfully because the recursive option is specifically designed to prevent errors when the directory path already exists [1]. If the directory does not already exist, the promise resolves with the first directory path that was created [1]. If the directory already exists, it simply fulfills without creating any new directories [1]. Rejection only occurs when { recursive: false } is used and the target directory already exists [1]. Citations:
Probe actual file write access instead of directory existence.
🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| if (!isUnwritableError(error)) throw error | ||
| writableDirs.set(dir, false) | ||
| console.warn(`[evlog/fs] "${dir}" is not writable, so the file system drain is disabled. This is expected on a serverless host, where only the temp directory is writable and does not outlive the instance — send events to a HTTP adapter instead, or set the drain's \`dir\` to a writable path.`) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use factual, plain wording in the new user-facing text.
As per coding guidelines, keep prose factual and plain, including log messages and changeset descriptions, and omit filler. 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| return false | ||
| } | ||
|
|
||
| writableDirs.set(dir, true) | ||
| return true | ||
| } | ||
|
|
||
| async function ensureGitignore(dir: string): Promise<void> { | ||
| const normalized = dir.replace(/[\\/]/g, sep) | ||
| const segments = normalized.split(sep) | ||
|
|
@@ -155,8 +186,10 @@ export function createFsDrain(overrides?: Partial<FsConfig>) { | |
| return null | ||
| } | ||
| const resolved = await resolveAdapterConfig<FsConfig>('fs', FS_FIELDS, overrides) | ||
| const dir = resolved.dir ?? '.evlog/logs' | ||
| if (!await isDirWritable(dir)) return null | ||
| return { | ||
| dir: resolved.dir ?? '.evlog/logs', | ||
| dir, | ||
| pretty: resolved.pretty ?? false, | ||
| maxFiles: resolved.maxFiles, | ||
| maxSizePerFile: resolved.maxSizePerFile, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 151
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 12864
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 241
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 28811
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 8301
🏁 Script executed:
Repository: HugoRCD/evlog
Length of output: 16479
Coalesce concurrent writability probes.
writableDirsstores only settled booleans. Concurrent dispatches can all miss the cache, run separateisDirWritable()probes, and callconsole.warn(...)on the same non-permission probe failure. Cache the in-flightPromise<boolean>per directory and clean it up if a non-permission error rejects. Add a regression test with concurrent calls to the same drain, andPromise.all-wait for the multiple resolved drains if the hook permits concurrent resolution.🤖 Prompt for AI Agents