Skip to content
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

fix: Route file writing through FileSystem #3614

Merged
merged 19 commits into from
Sep 26, 2024
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
Sentry will reject all metrics sent after October 7, 2024.
Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics ([#3619](https://github.com/getsentry/sentry-dotnet/pull/3619))

### Features

- Added a flag to options `DisableFileWrite` to allow users to opt-out of all file writing operations. Note that toggling this will affect features such as offline caching and auto-session tracking and release health as these rely on some file persistency ([#3614](https://github.com/getsentry/sentry-dotnet/pull/3614))

### Dependencies

- Bump Native SDK from v0.7.9 to v0.7.10 ([#3623](https://github.com/getsentry/sentry-dotnet/pull/3623))
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal partial class BindableSentryOptions
public string? CacheDirectoryPath { get; set; }
public bool? CaptureFailedRequests { get; set; }
public List<string>? FailedRequestTargets { get; set; }
public bool? DisableFileWrite { get; set; }
public TimeSpan? InitCacheFlushTimeout { get; set; }
public Dictionary<string, string>? DefaultTags { get; set; }
public bool? EnableTracing { get; set; }
Expand Down Expand Up @@ -81,6 +82,7 @@ public void ApplyTo(SentryOptions options)
options.CacheDirectoryPath = CacheDirectoryPath ?? options.CacheDirectoryPath;
options.CaptureFailedRequests = CaptureFailedRequests ?? options.CaptureFailedRequests;
options.FailedRequestTargets = FailedRequestTargets?.Select(s => new SubstringOrRegexPattern(s)).ToList() ?? options.FailedRequestTargets;
options.DisableFileWrite = DisableFileWrite ?? options.DisableFileWrite;
options.InitCacheFlushTimeout = InitCacheFlushTimeout ?? options.InitCacheFlushTimeout;
options.DefaultTags = DefaultTags ?? options.DefaultTags;
#pragma warning disable CS0618 // Type or member is obsolete
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry/FileAttachmentContent.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Sentry.Internal;

namespace Sentry;

/// <summary>
Expand Down
25 changes: 19 additions & 6 deletions src/Sentry/GlobalSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public GlobalSessionManager(
_options = options;
_clock = clock ?? SystemClock.Clock;
_persistedSessionProvider = persistedSessionProvider
?? (filePath => Json.Load(filePath, PersistedSessionUpdate.FromJson));
?? (filePath => Json.Load(_options.FileSystem, filePath, PersistedSessionUpdate.FromJson));

// TODO: session file should really be process-isolated, but we
// don't have a proper mechanism for that right now.
Expand All @@ -53,14 +53,27 @@ private void PersistSession(SessionUpdate update, DateTimeOffset? pauseTimestamp

try
{
Directory.CreateDirectory(_persistenceDirectoryPath);
_options.LogDebug("Creating persistence directory for session file at '{0}'.", _persistenceDirectoryPath);

_options.LogDebug("Created persistence directory for session file '{0}'.", _persistenceDirectoryPath);
var result = _options.FileSystem.CreateDirectory(_persistenceDirectoryPath);
if (result is not FileOperationResult.Success)
{
if (result is FileOperationResult.Disabled)
{
_options.LogInfo("Persistent directory for session file has not been created. File-write has been disabled via the options.");
}
else
{
_options.LogError("Failed to create persistent directory for session file.");
}

return;
}

var filePath = Path.Combine(_persistenceDirectoryPath, PersistedSessionFileName);

var persistedSessionUpdate = new PersistedSessionUpdate(update, pauseTimestamp);
persistedSessionUpdate.WriteToFile(filePath, _options.DiagnosticLogger);
persistedSessionUpdate.WriteToFile(_options.FileSystem, filePath, _options.DiagnosticLogger);

_options.LogDebug("Persisted session to a file '{0}'.", filePath);
}
Expand All @@ -86,7 +99,7 @@ private void DeletePersistedSession()
{
try
{
var contents = File.ReadAllText(filePath);
var contents = _options.FileSystem.ReadAllTextFromFile(filePath);
_options.LogDebug("Deleting persisted session file with contents: {0}", contents);
}
catch (Exception ex)
Expand All @@ -95,7 +108,7 @@ private void DeletePersistedSession()
}
}

File.Delete(filePath);
_options.FileSystem.DeleteFile(filePath);

_options.LogInfo("Deleted persisted session file '{0}'.", filePath);
}
Expand Down
29 changes: 25 additions & 4 deletions src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,9 +386,19 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope)
var destination = Path.Combine(destinationDirectory, "envelope_too_large",
(eventId ?? SentryId.Create()).ToString());

Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!);
if (createDirectoryResult is not FileOperationResult.Success)
{
_options.DiagnosticLogger.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult);
return;
}

var envelopeFile = File.Create(destination);
var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile);
if (result is not FileOperationResult.Success)
{
_options.DiagnosticLogger.LogError("Failed to create envelope file: {0}", result);
return;
}

using (envelopeFile)
{
Expand Down Expand Up @@ -442,9 +452,20 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env
var destination = Path.Combine(destinationDirectory, "envelope_too_large",
(eventId ?? SentryId.Create()).ToString());

Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!);
if (createDirectoryResult is not FileOperationResult.Success)
{
_options.DiagnosticLogger.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult);
return;
}

var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile);
if (result is not FileOperationResult.Success)
{
_options.DiagnosticLogger.LogError("Failed to create envelope file: {0}", result);
return;
}

var envelopeFile = File.Create(destination);
#if NETFRAMEWORK || NETSTANDARD2_0
using (envelopeFile)
#else
Expand Down
11 changes: 9 additions & 2 deletions src/Sentry/ISentryJsonSerializable.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Sentry.Extensibility;
using Sentry.Internal;

namespace Sentry;

Expand All @@ -19,12 +20,18 @@ public interface ISentryJsonSerializable

internal static class JsonSerializableExtensions
{
public static void WriteToFile(this ISentryJsonSerializable serializable, string filePath, IDiagnosticLogger? logger)
public static void WriteToFile(this ISentryJsonSerializable serializable, IFileSystem fileSystem, string filePath, IDiagnosticLogger? logger)
{
using var file = File.Create(filePath);
var result = fileSystem.CreateFileForWriting(filePath, out var file);
if (result is not FileOperationResult.Success)
{
return;
}

using var writer = new Utf8JsonWriter(file);

serializable.WriteTo(writer, logger);
writer.Flush();
file.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

this couldn’t have using anymore? I’m concerned an exception will keep dangling filesa

}
}
3 changes: 2 additions & 1 deletion src/Sentry/Internal/DebugStackTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame)
{
return reader.Invoke(assemblyName);
}
var assembly = File.OpenRead(assemblyName);

var assembly = options.FileSystem.OpenFileForReading(assemblyName);
return new PEReader(assembly);
}
catch (Exception)
Expand Down
53 changes: 0 additions & 53 deletions src/Sentry/Internal/FileSystem.cs

This file was deleted.

29 changes: 29 additions & 0 deletions src/Sentry/Internal/FileSystemBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Sentry.Internal;

internal abstract class FileSystemBase : IFileSystem
{
public IEnumerable<string> EnumerateFiles(string path) => Directory.EnumerateFiles(path);

public IEnumerable<string> EnumerateFiles(string path, string searchPattern) =>
Directory.EnumerateFiles(path, searchPattern);

public IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption) =>
Directory.EnumerateFiles(path, searchPattern, searchOption);

public bool DirectoryExists(string path) => Directory.Exists(path);

public bool FileExists(string path) => File.Exists(path);

public DateTimeOffset GetFileCreationTime(string path) => new FileInfo(path).CreationTimeUtc;

public string ReadAllTextFromFile(string path) => File.ReadAllText(path);

public Stream OpenFileForReading(string path) => File.OpenRead(path);

public abstract FileOperationResult CreateDirectory(string path);
public abstract FileOperationResult DeleteDirectory(string path, bool recursive = false);
public abstract FileOperationResult CreateFileForWriting(string path, out Stream fileStream);
public abstract FileOperationResult WriteAllTextToFile(string path, string contents);
public abstract FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
public abstract FileOperationResult DeleteFile(string path);
}
8 changes: 7 additions & 1 deletion src/Sentry/Internal/Http/CachingTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,13 @@ private async Task StoreToCacheAsync(

EnsureFreeSpaceInCache();

var stream = _fileSystem.CreateFileForWriting(envelopeFilePath);
var result = _options.FileSystem.CreateFileForWriting(envelopeFilePath, out var stream);
if (result is not FileOperationResult.Success)
{
_options.LogDebug("Failed to store to cache.");
return;
}

#if NETFRAMEWORK || NETSTANDARD2_0
using(stream)
#else
Expand Down
21 changes: 15 additions & 6 deletions src/Sentry/Internal/IFileSystem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
namespace Sentry.Internal;

internal enum FileOperationResult
{
Success,
Failure,
Disabled
}

internal interface IFileSystem
{
// Note: This is not comprehensive. If you need other filesystem methods, add to this interface,
Expand All @@ -8,14 +15,16 @@ internal interface IFileSystem
IEnumerable<string> EnumerateFiles(string path);
IEnumerable<string> EnumerateFiles(string path, string searchPattern);
IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption);
void CreateDirectory(string path);
void DeleteDirectory(string path, bool recursive = false);
bool DirectoryExists(string path);
bool FileExists(string path);
void MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
void DeleteFile(string path);
DateTimeOffset GetFileCreationTime(string path);
string ReadAllTextFromFile(string file);
string? ReadAllTextFromFile(string file);
Stream OpenFileForReading(string path);
Stream CreateFileForWriting(string path);

FileOperationResult CreateDirectory(string path);
FileOperationResult DeleteDirectory(string path, bool recursive = false);
FileOperationResult CreateFileForWriting(string path, out Stream fileStream);
FileOperationResult WriteAllTextToFile(string path, string contents);
FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
FileOperationResult DeleteFile(string path);
}
23 changes: 19 additions & 4 deletions src/Sentry/Internal/InstallationIdHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,43 @@ internal class InstallationIdHelper(SentryOptions options)

private string? TryGetPersistentInstallationId()
{
if (options.DisableFileWrite)
{
options.LogDebug("File write has been disabled via the options. Skipping trying to get persistent installation ID.");
return null;
}

try
{
var rootPath = options.CacheDirectoryPath ??
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var directoryPath = Path.Combine(rootPath, "Sentry", options.Dsn!.GetHashString());
var fileSystem = options.FileSystem;

Directory.CreateDirectory(directoryPath);
if (fileSystem.CreateDirectory(directoryPath) is not FileOperationResult.Success)
{
options.LogDebug("Failed to create a directory for installation ID file ({0}).", directoryPath);
return null;
}

options.LogDebug("Created directory for installation ID file ({0}).", directoryPath);

var filePath = Path.Combine(directoryPath, ".installation");

// Read installation ID stored in a file
if (File.Exists(filePath))
if (fileSystem.FileExists(filePath))
{
return File.ReadAllText(filePath);
return fileSystem.ReadAllTextFromFile(filePath);
}
options.LogDebug("File containing installation ID does not exist ({0}).", filePath);

// Generate new installation ID and store it in a file
var id = Guid.NewGuid().ToString();
File.WriteAllText(filePath, id);
if (fileSystem.WriteAllTextToFile(filePath, id) is not FileOperationResult.Success)
{
options.LogDebug("Failed to write Installation ID to file ({0}).", filePath);
return null;
}

options.LogDebug("Saved installation ID '{0}' to file '{1}'.", id, filePath);
return id;
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/Internal/Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public static T Parse<T>(string json, Func<JsonElement, T> factory)
return factory.Invoke(jsonDocument.RootElement);
}

public static T Load<T>(string filePath, Func<JsonElement, T> factory)
public static T Load<T>(IFileSystem fileSystem, string filePath, Func<JsonElement, T> factory)
{
using var file = File.OpenRead(filePath);
using var file = fileSystem.OpenFileForReading(filePath);
using var jsonDocument = JsonDocument.Parse(file);
return factory.Invoke(jsonDocument.RootElement);
}
Expand Down
21 changes: 21 additions & 0 deletions src/Sentry/Internal/ReadOnlyFilesystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Sentry.Internal;

internal class ReadOnlyFileSystem : FileSystemBase
{
public override FileOperationResult CreateDirectory(string path) => FileOperationResult.Disabled;

public override FileOperationResult DeleteDirectory(string path, bool recursive = false) => FileOperationResult.Disabled;

public override FileOperationResult CreateFileForWriting(string path, out Stream fileStream)
{
fileStream = Stream.Null;
return FileOperationResult.Disabled;
}

public override FileOperationResult WriteAllTextToFile(string path, string contents) => FileOperationResult.Disabled;

public override FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false) =>
FileOperationResult.Disabled;

public override FileOperationResult DeleteFile(string path) => FileOperationResult.Disabled;
}
Loading
Loading