Skip to content

Conversation

@snakefoot
Copy link
Contributor

Resolves #5997

@snakefoot snakefoot added bug Bug report / Bug fix file-target labels Sep 11, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Walkthrough

Extracted DirectoryNotFoundException handling from CreateFileStreamWithRetry into a private helper CreateFileStreamWithDirectory, preserving retry-and-create-directory behavior; updated a unit test to write into a GUID-nested subdirectory, exercise directory creation, and assert created directories.

Changes

Cohort / File(s) Summary
FileTarget stream open refactor
src/NLog/Targets/FileTarget.cs
Moved DirectoryNotFoundException handling and directory-creation retry logic into a new private CreateFileStreamWithDirectory(string filePath, int bufferSize, bool initialFileOpen). CreateFileStreamWithRetry now delegates to the helper. No public API changes; caller behavior preserved. Debug message adjusted to reference "for file".
Unit test adjustment for directory creation
tests/NLog.UnitTests/Targets/FileTargetTests.cs
Modified ReplaceFileContentsOnEachWrite_CreateDirs to use tempDir/${guid}/log.log, added an extra logger.Info("b") write, asserted two subdirectories exist under tempDir when CreateDirs is true, and removed explicit single-file deletion in favor of cleaning tempDir.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant FT as FileTarget
  participant FS as OpenNewFileStream
  participant IO as Directory

  Caller->>FT: CreateFileStreamWithRetry(filePath, bufferSize, initialFileOpen)
  FT->>FT: CreateFileStreamWithDirectory(filePath, bufferSize, initialFileOpen)
  FT->>FS: OpenNewFileStream(filePath, bufferSize, initialFileOpen)
  alt File opened
    FS-->>FT: FileStream
    FT-->>Caller: FileStream
  else DirectoryNotFoundException
    FT->>FT: if CreateDirs==false -> rethrow
    alt CreateDirs==true
      FT->>IO: CreateDirectory(directoryName)
      IO-->>FT: success / throws
      alt CreateDirectory succeeded
        FT->>FS: OpenNewFileStream(filePath, bufferSize, initialFileOpen) (retry)
        FS-->>FT: FileStream
        FT-->>Caller: FileStream
      else CreateDirectory failed
        FT-->>Caller: throw NLogRuntimeException(IO exception)
      end
    end
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Pre-merge checks (5 passed)

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title "FileTarget - Rolling to next directory should not fail" is concise and directly related to the main change (preventing failures when FileTarget must create/roll into a new directory) and communicates the primary intent to reviewers at a glance.
Linked Issues Check ✅ Passed The changes centralize DirectoryNotFoundException handling into a private CreateFileStreamWithDirectory helper that creates the missing directory (when CreateDirs is true) and retries opening the file stream, and the tests were updated to exercise directory creation with nested GUID subdirectories; this directly addresses the linked issue's objective that the first FileTarget message should not be lost when the directory must be created [#5997]. The patch does not change public APIs and preserves the retry path, so the implementation appears to meet the coding requirement from the issue, although the updated test asserts directory creation and may not explicitly assert that the very first log message was successfully written.
Out of Scope Changes Check ✅ Passed All code changes are confined to src/NLog/Targets/FileTarget.cs and the related unit test file; there are no public API signature changes and the modifications are directly related to directory-creation and retry behavior described in the linked issue, so no out-of-scope changes were detected.
Description Check ✅ Passed The description "Resolves #5997" is directly related to the changeset and therefore passes this lenient check, though it is minimal and does not describe the implemented code changes or test updates.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

I hopped through paths the logs would write,
A folder sprang where none took light;
The first small nibble kept its place,
A stream retried, returned apace.
Two burrows now — hooray, good night! 🐇


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
tests/NLog.UnitTests/Targets/FileTargetTests.cs (3)

1019-1019: Assert files and contents to catch “first message lost”.

To ensure no event is dropped when creating directories, verify both files exist and contain “a” and “b”.

                 var logger = LogManager.GetLogger("A");
                 logger.Info("a");
                 logger.Info("b");
 
                 Assert.Equal(createDirs, Directory.Exists(tempDir));
                 if (createDirs)
-                    Assert.Equal(2, Directory.GetDirectories(tempDir).Length);
+                {
+                    Assert.Equal(2, Directory.GetDirectories(tempDir).Length);
+                    var files = Directory.GetFiles(tempDir, "log.log", SearchOption.AllDirectories);
+                    Assert.Equal(2, files.Length);
+                    var contents = files.Select(f => File.ReadAllText(f).Trim()).OrderBy(s => s).ToArray();
+                    Assert.Equal(new[] { "a", "b" }, contents);
+                }
+                else
+                {
+                    Assert.Empty(Directory.GetDirectories(tempDir));
+                    Assert.Empty(Directory.GetFiles(tempDir, "log.log", SearchOption.AllDirectories));
+                }

1001-1001: Restore ThrowExceptions to its prior value.

Avoid leaking global state to other tests.

-                LogManager.ThrowExceptions = false;
+                var prevThrow = LogManager.ThrowExceptions;
+                LogManager.ThrowExceptions = false;

And in finally:

-                LogManager.ThrowExceptions = true;
+                LogManager.ThrowExceptions = prevThrow;

1029-1030: Remove ineffective file cleanup.

logfile contains ${guid} and won’t match actual files. Directory cleanup below already removes them.

-                if (File.Exists(logfile))
-                    File.Delete(logfile);
src/NLog/Targets/FileTarget.cs (1)

1231-1259: Guard against null/empty directory names before CreateDirectory.

Path.GetDirectoryName(filePath) can be null/empty in edge cases; be defensive to avoid ArgumentNullException.

-                var directoryName = Path.GetDirectoryName(filePath);
-
-                try
-                {
-                    Directory.CreateDirectory(directoryName);
-                }
+                var directoryName = Path.GetDirectoryName(filePath);
+                try
+                {
+                    if (string.IsNullOrEmpty(directoryName))
+                        throw; // No directory part to create; rethrow original DirectoryNotFoundException
+                    Directory.CreateDirectory(directoryName);
+                }
                 catch (Exception ex)
                 {
                     // If creating a directory failed, don't retry for this message
                     throw new NLogRuntimeException($"Could not create directory {directoryName}", ex);
                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f63723 and 5c2ebc1.

📒 Files selected for processing (2)
  • src/NLog/Targets/FileTarget.cs (2 hunks)
  • tests/NLog.UnitTests/Targets/FileTargetTests.cs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileTarget.cs (3)
src/NLog.Targets.GZipFile/GZipFileTarget.cs (1)
  • Stream (88-103)
src/NLog/Common/InternalLogger.cs (1)
  • InternalLogger (51-511)
src/NLog/NLogRuntimeException.cs (4)
  • NLogRuntimeException (51-53)
  • NLogRuntimeException (59-62)
  • NLogRuntimeException (83-86)
  • NLogRuntimeException (100-103)
🔇 Additional comments (2)
tests/NLog.UnitTests/Targets/FileTargetTests.cs (1)

997-997: Good: exercising directory creation with dynamic ${guid}.

This reliably forces different directories per write and covers the reported scenario.

src/NLog/Targets/FileTarget.cs (1)

1212-1212: LGTM: refactor improves readability and reusability.

Delegating to a helper keeps retry logic focused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Targets/FileTarget.cs (1)

1231-1258: Harden directory creation: guard null dir, preserve retry on transient IO, fix log text.

Two small robustness tweaks:

  • Handle Path.GetDirectoryName returning null/empty (rare but possible) to avoid wrapping an ArgumentNullException and to let the outer retry handle it.
  • If Directory.CreateDirectory throws an IOException (transient/lock/share issues), rethrow it so CreateFileStreamWithRetry’s retry-loop can kick in. Keep NLogRuntimeException for non-IO errors.
  • Nit: log says “FileName”; it’s a path.

Apply:

-                InternalLogger.Debug("{0}: DirectoryNotFoundException - Attempting to create directory for FileName: {1}", this, filePath);
+                InternalLogger.Debug("{0}: DirectoryNotFoundException - Attempting to create directory for FilePath: {1}", this, filePath);

-                var directoryName = Path.GetDirectoryName(filePath);
+                var directoryName = Path.GetDirectoryName(filePath);
+                if (string.IsNullOrEmpty(directoryName))
+                {
+                    // Nothing to create; let outer retry-logic decide.
+                    throw;
+                }

                 try
                 {
-                    Directory.CreateDirectory(directoryName);
+                    Directory.CreateDirectory(directoryName);
                 }
-                catch (Exception ex)
+                catch (IOException)
                 {
-                    // If creating a directory failed, don't retry for this message
-                    throw new NLogRuntimeException($"Could not create directory {directoryName}", ex);
+                    // Allow the outer retry-loop in CreateFileStreamWithRetry to handle transient IO failures
+                    throw;
+                }
+                catch (Exception ex)
+                {
+                    // Non-IO errors won't benefit from retry; keep current behavior
+                    throw new NLogRuntimeException($"Could not create directory '{directoryName}' for '{filePath}'", ex);
                 }

Would you like me to add/adjust a unit test that asserts the very first write succeeds when the directory does not exist (with minimal race via multiple concurrent loggers)?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c2ebc1 and 79375b8.

📒 Files selected for processing (2)
  • src/NLog/Targets/FileTarget.cs (2 hunks)
  • tests/NLog.UnitTests/Targets/FileTargetTests.cs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Targets/FileTargetTests.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileTarget.cs (3)
src/NLog.Targets.GZipFile/GZipFileTarget.cs (1)
  • Stream (88-103)
src/NLog/Common/InternalLogger.cs (1)
  • InternalLogger (51-511)
src/NLog/NLogRuntimeException.cs (4)
  • NLogRuntimeException (51-53)
  • NLogRuntimeException (59-62)
  • NLogRuntimeException (83-86)
  • NLogRuntimeException (100-103)
🔇 Additional comments (1)
src/NLog/Targets/FileTarget.cs (1)

1212-1212: Good extraction and delegation.

Refactoring the DirectoryNotFoundException path into a helper keeps CreateFileStreamWithRetry lean and preserves the retry semantics.

@snakefoot snakefoot closed this Sep 11, 2025
@snakefoot snakefoot reopened this Sep 11, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Targets/FileTarget.cs (1)

1231-1258: Guard null/empty directory names and preserve original exception; improve debug context

If Path.GetDirectoryName(filePath) returns null/empty, Directory.CreateDirectory will throw ArgumentNullException, masking the original DirectoryNotFoundException. Also, logging the target directory aids diagnostics.

Apply:

-        private Stream CreateFileStreamWithDirectory(string filePath, int bufferSize, bool initialFileOpen)
+        private Stream CreateFileStreamWithDirectory(string filePath, int bufferSize, bool initialFileOpen)
         {
             try
             {
                 return OpenNewFileStream(filePath, bufferSize, initialFileOpen);
             }
-            catch (DirectoryNotFoundException)
+            catch (DirectoryNotFoundException dnfe)
             {
                 if (!CreateDirs)
                     throw;
 
-                InternalLogger.Debug("{0}: DirectoryNotFoundException - Attempting to create directory for file: {1}", this, filePath);
-
-                var directoryName = Path.GetDirectoryName(filePath);
+                var directoryName = Path.GetDirectoryName(filePath);
+                if (string.IsNullOrEmpty(directoryName))
+                {
+                    // No directory component to create; keep original stack/exception.
+                    throw;
+                }
+                InternalLogger.Debug("{0}: DirectoryNotFoundException - Attempting to create directory: {1} for file: {2}", this, directoryName, filePath);
 
                 try
                 {
                     Directory.CreateDirectory(directoryName);
                 }
                 catch (Exception ex)
                 {
-                    // If creating a directory failed, don't retry for this message
-                    throw new NLogRuntimeException($"Could not create directory {directoryName}", ex);
+                    // If creating a directory failed, don't retry for this message
+                    throw new NLogRuntimeException($"Could not create directory '{directoryName}' for file '{filePath}'", ex);
                 }
 
                 return OpenNewFileStream(filePath, bufferSize, initialFileOpen);
             }
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79375b8 and f79fa52.

📒 Files selected for processing (2)
  • src/NLog/Targets/FileTarget.cs (2 hunks)
  • tests/NLog.UnitTests/Targets/FileTargetTests.cs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Targets/FileTargetTests.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Targets/FileTarget.cs (3)
src/NLog.Targets.GZipFile/GZipFileTarget.cs (1)
  • Stream (88-103)
src/NLog/Common/InternalLogger.cs (1)
  • InternalLogger (51-511)
src/NLog/NLogRuntimeException.cs (4)
  • NLogRuntimeException (51-53)
  • NLogRuntimeException (59-62)
  • NLogRuntimeException (83-86)
  • NLogRuntimeException (100-103)
🔇 Additional comments (1)
src/NLog/Targets/FileTarget.cs (1)

1212-1212: Nice extraction: delegate to a directory-aware stream opener

Moving DirectoryNotFoundException handling into a helper simplifies the retry loop and keeps behavior intact.

@sonarqubecloud
Copy link

@snakefoot snakefoot enabled auto-merge (squash) September 11, 2025 18:20
@snakefoot snakefoot merged commit 3128290 into NLog:dev Sep 11, 2025
6 checks passed
@snakefoot snakefoot added this to the 6.0.5 milestone Sep 30, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug report / Bug fix file-target size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NLog lost first FileTarget log message when first time it need to create directory which not exists

1 participant