Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Notes](../../RELEASENOTES.md).
all subsequent telemetry in that execution context to be suppressed.
([#7966](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7966))

* Self-diagnostics log files, whether configured through `OTEL_DIAGNOSTICS.json`
or `SelfDiagnosticsOptions`, are now always created as new files.
([#7968](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7968))

## 1.20.0

Released 2026-Oct-15
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal class SelfDiagnosticsConfigRefresher : IDisposable
private bool formatMessage;

public SelfDiagnosticsConfigRefresher()
: this(static path => new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 1, FileOptions.None))
: this(static path => new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read, 1, FileOptions.None))
{
}

Expand Down Expand Up @@ -148,6 +148,13 @@ private void OpenLogFile(string newLogDirectory, int newFileSize)
#endif
var filePath = Path.Combine(newLogDirectory, fileName);

// The file name is predictable (executable name + process id), so an entry that already
// exists under it may be a symbolic link planted by another user of the log directory and
// must never be opened for writing. Deleting it removes a stale log of an earlier process
// with the same id, or the link itself but not its target, and FileMode.CreateNew then
// fails instead of following a link that was (re)created in between.
File.Delete(filePath);

// Ordinary file I/O reports a full disk as an exception. Writing to an
// unbacked memory-mapped page can instead terminate the process.
// Disable buffering so write errors are caught when the event is logged.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,12 @@ private bool TryOpenWriter()
var fileName = $"otel-dotnet-{ProcessIdentity}-{this.fileIndex}.log";
var filePath = Path.Combine(this.logDirectory, fileName);

// The index is always past any file already in the series, so the name is expected to
// be free; an existing entry is most likely a symbolic link planted by another user of
// the log directory. FileMode.CreateNew fails instead of following it (and writing into
// its target), and the next attempt moves on to the next index.
// FileShare.Read lets external tools tail the log while it's open.
var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read);
var stream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
var newWriter = new StreamWriter(stream, Utf8NoBom) { AutoFlush = false };

this.CurrentFilePath = filePath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ public void RollOver_WritesFreshPreambleToEachFile()
}
}

#if NET
[Fact]
public void RollOver_DoesNotFollowLink()
{
Assert.SkipWhen(OperatingSystem.IsWindows(), "Creating symbolic links requires elevated privileges on Windows.");

var linkTarget = Path.Combine(this.tempDirectory.Path, "target.txt");
File.WriteAllText(linkTarget, "target content");

using (var sink = this.CreateSink(fileSizeLimitKilobytes: 1))
{
var firstPath = sink.CurrentFilePath;
Assert.NotNull(firstPath);
Assert.EndsWith("-1.log", firstPath, StringComparison.Ordinal);

File.CreateSymbolicLink(firstPath[..^"-1.log".Length] + "-2.log", linkTarget);

WriteLine(sink, new string('x', 1_100));
Assert.False(sink.IsActive, "expected the rollover to fail on the name taken by the link");

// The retry interval is zero, so the next write moves on to the next index.
WriteLine(sink, "after the rollover");
sink.Flush();

Assert.True(sink.IsActive, "expected sink to recover by skipping the name taken by the link");
Assert.EndsWith("-3.log", sink.CurrentFilePath, StringComparison.Ordinal);
}

Assert.Equal("target content", File.ReadAllText(linkTarget));
}
#endif

[Fact]
public void FirstOpen_WritesStartupMessageToStderr()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,44 @@ public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured()
Assert.StartsWith(expectedMessage, logMessage, StringComparison.Ordinal);
}

[Fact]
public void SelfDiagnosticsConfigRefresher_ReplacesExistingLogFile()
{
var logDirectory = Utils.GetCurrentMethodName();
var logFilePath = GetLogFilePath(logDirectory);

Directory.CreateDirectory(logDirectory);
File.WriteAllText(logFilePath, "stale content");

using var configRefresher = CreateRefresher(logDirectory);

Assert.StartsWith(MessageOnNewFileString, ReadLogFile(logDirectory), StringComparison.Ordinal);
}

#if NET
[Fact]
public void SelfDiagnosticsConfigRefresher_DoesNotFollowLink()
{
Assert.SkipWhen(OperatingSystem.IsWindows(), "Creating symbolic links requires elevated privileges on Windows.");

var logDirectory = Utils.GetCurrentMethodName();
var logFilePath = GetLogFilePath(logDirectory);
var linkTarget = Path.GetFullPath(Path.Combine(logDirectory, "target.txt"));

Directory.CreateDirectory(logDirectory);
File.Delete(logFilePath);
File.WriteAllText(linkTarget, "target content");
File.CreateSymbolicLink(logFilePath, linkTarget);

using var configRefresher = CreateRefresher(logDirectory);

// The link was replaced by the log file and its target was left alone.
Assert.Equal("target content", File.ReadAllText(linkTarget));
Assert.Null(new FileInfo(logFilePath).LinkTarget);
Assert.StartsWith(MessageOnNewFileString, ReadLogFile(logDirectory), StringComparison.Ordinal);
}
#endif

[Fact]
public void SelfDiagnosticsConfigRefresher_CaptureAfterLogFileTruncated()
{
Expand Down
Loading