Allow viewing the log file for SelfDiagnostics for .NET Framework (#1693)

* Fix different FileShare behavior in MemoryMappedFile between .NET Framework and .NET Core for SelfDiagnostics

* Create Stress Test to test self diagnostics log file's file share status

(cherry picked from commit 9fe60757c4254b67c1d166fc5f42eb795d424bf9)

* Revert "Create Stress Test to test self diagnostics log file's file share status"

This reverts commit 1007ffef42.

* Dispose underlyingFileStreamForMemoryMappedFile after memoryMappedFile is disposed.

* Add unit test for validating that the log file can be opened as read-only by others while being used by SelfDiagnosticsModule

* Remove en-us in links. Clean up config file. Add change log.

* minor: improve wording

* Fix error SA1501: Statement should not be on a single line
This commit is contained in:
xiang17 2021-01-28 13:03:38 -08:00 committed by GitHub
parent 4f39a58048
commit 4b5388bdc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 4 deletions

View File

@ -33,6 +33,9 @@
* `BatchExportProcessor` will now flush any remaining spans left in a `Batch`
after the export operation has completed.
([#1726](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1726))
* Fixed a bug to allow the Self Diagnostics log file to be opened simutaneously
by another process in read-only mode for .NET Framework.
([#1693](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1693))
## 1.0.0-rc1.1

View File

@ -24,7 +24,7 @@ namespace OpenTelemetry.Internal
{
internal class SelfDiagnosticsConfigParser
{
private const string ConfigFileName = "OTEL_DIAGNOSTICS.json";
public const string ConfigFileName = "OTEL_DIAGNOSTICS.json";
private const int FileSizeLowerLimit = 1024; // Lower limit for log file size in KB: 1MB
private const int FileSizeUpperLimit = 128 * 1024; // Upper limit for log file size in KB: 128MB

View File

@ -34,9 +34,9 @@ namespace OpenTelemetry.Internal
/// </summary>
internal class SelfDiagnosticsConfigRefresher : IDisposable
{
private const int ConfigurationUpdatePeriodMilliSeconds = 10000;
public static readonly byte[] MessageOnNewFile = Encoding.UTF8.GetBytes("Successfully opened file.\n");
private static readonly byte[] MessageOnNewFile = Encoding.UTF8.GetBytes("Successfully opened file.\n");
private const int ConfigurationUpdatePeriodMilliSeconds = 10000;
private readonly CancellationTokenSource cancellationTokenSource;
private readonly Task worker;
@ -53,6 +53,7 @@ namespace OpenTelemetry.Internal
// Once the configuration file is valid, an eventListener object will be created.
// Commented out for now to avoid the "field was never used" compiler error.
private SelfDiagnosticsEventListener eventListener;
private volatile FileStream underlyingFileStreamForMemoryMappedFile;
private volatile MemoryMappedFile memoryMappedFile;
private string logDirectory; // Log directory for log files
private int logFileSize; // Log file size in bytes
@ -193,6 +194,12 @@ namespace OpenTelemetry.Internal
mmf.Dispose();
}
FileStream fs = Interlocked.CompareExchange(
ref this.underlyingFileStreamForMemoryMappedFile,
null,
this.underlyingFileStreamForMemoryMappedFile);
fs?.Dispose();
}
private void OpenLogFile(string newLogDirectory, int newFileSize)
@ -203,7 +210,40 @@ namespace OpenTelemetry.Internal
var fileName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName) + "."
+ Process.GetCurrentProcess().Id + ".log";
var filePath = Path.Combine(newLogDirectory, fileName);
this.memoryMappedFile = MemoryMappedFile.CreateFromFile(filePath, FileMode.Create, null, newFileSize);
// Because the API [MemoryMappedFile.CreateFromFile][1](the string version) behaves differently on
// .NET Framework and .NET Core, here I am using the [FileStream version][2] of it.
// Taking the last four prameter values from [.NET Framework]
// (https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,148)
// and [.NET Core]
// (https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L152)
// The parameter for FileAccess is different in type but the same in rules, both are Read and Write.
// The parameter for FileShare is different in values and in behavior.
// .NET Framework doesn't allow sharing but .NET Core allows reading by other programs.
// The last two parameters are the same values for both frameworks.
// [1]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_String_System_IO_FileMode_System_String_System_Int64_
// [2]: https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=net-5.0#System_IO_MemoryMappedFiles_MemoryMappedFile_CreateFromFile_System_IO_FileStream_System_String_System_Int64_System_IO_MemoryMappedFiles_MemoryMappedFileAccess_System_IO_HandleInheritability_System_Boolean_
this.underlyingFileStreamForMemoryMappedFile =
new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 0x1000, FileOptions.None);
// The parameter values for MemoryMappedFileSecurity, HandleInheritability and leaveOpen are the same
// values for .NET Framework and .NET Core:
// https://referencesource.microsoft.com/#system.core/System/IO/MemoryMappedFiles/MemoryMappedFile.cs,172
// https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.cs#L168-L179
this.memoryMappedFile = MemoryMappedFile.CreateFromFile(
this.underlyingFileStreamForMemoryMappedFile,
null,
newFileSize,
MemoryMappedFileAccess.ReadWrite,
#if NET452
// Only .NET Framework 4.5.2 among all .NET Framework versions is lacking a method omitting this
// default value for MemoryMappedFileSecurity.
// https://docs.microsoft.com/dotnet/api/system.io.memorymappedfiles.memorymappedfile.createfromfile?view=netframework-4.5.2
// .NET Core simply doesn't support this parameter.
null,
#endif
HandleInheritability.None,
false);
this.logDirectory = newLogDirectory;
this.logFileSize = newFileSize;
this.logFilePosition = MessageOnNewFile.Length;

View File

@ -0,0 +1,70 @@
// <copyright file="SelfDiagnosticsConfigRefresherTest.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Diagnostics;
using System.IO;
using System.Text;
using Xunit;
namespace OpenTelemetry.Internal.Tests
{
public class SelfDiagnosticsConfigRefresherTest
{
private static readonly string ConfigFilePath = SelfDiagnosticsConfigParser.ConfigFileName;
private static readonly byte[] MessageOnNewFile = SelfDiagnosticsConfigRefresher.MessageOnNewFile;
[Fact]
[Trait("Platform", "Any")]
public void SelfDiagnosticsConfigRefresher_FileShare()
{
try
{
CreateConfigFile();
using var configRefresher = new SelfDiagnosticsConfigRefresher();
var outputFileName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName) + "."
+ Process.GetCurrentProcess().Id + ".log";
var outputFilePath = Path.Combine(".", outputFileName);
using var file = File.Open(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
byte[] actualBytes = new byte[MessageOnNewFile.Length];
file.Read(actualBytes, 0, actualBytes.Length);
Assert.Equal(MessageOnNewFile, actualBytes);
}
finally
{
try
{
File.Delete(ConfigFilePath);
}
catch
{
}
}
}
private static void CreateConfigFile()
{
string configJson = @"{
""LogDirectory"": ""."",
""FileSize"": 1024,
""LogLevel"": ""Error""
}";
using FileStream file = File.Open(ConfigFilePath, FileMode.Create, FileAccess.Write);
byte[] configBytes = Encoding.UTF8.GetBytes(configJson);
file.Write(configBytes, 0, configBytes.Length);
}
}
}