Update Readme and changelog for SelfDiagnostics (#2267)

* Add Readme and changelog for Self Diagnostics

* Fix failed test cases

* Add changelog entry

* Update troubleshooting/ETW/Readme.md with Self Diagnostics section

* Remove unused code

* Add "as of version 2.18.0"
This commit is contained in:
xiang17 2021-05-18 13:22:08 -07:00 коммит произвёл GitHub
Родитель dd497a9b7a
Коммит 1b8e58f950
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 302 добавлений и 9 удалений

Просмотреть файл

@ -0,0 +1,118 @@
namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.SelfDiagnostics
{
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class SelfDiagnosticsConfigRefresherTest
{
private static readonly string ConfigFilePath = SelfDiagnosticsConfigParser.ConfigFileName;
private static readonly byte[] MessageOnNewFile = MemoryMappedFileHandler.MessageOnNewFile;
private static readonly string MessageOnNewFileString = Encoding.UTF8.GetString(MessageOnNewFile);
private static readonly Regex TimeStringRegex = new Regex(
@"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{7}Z:", RegexOptions.IgnoreCase | RegexOptions.Compiled);
[TestMethod]
public void SelfDiagnosticsConfigRefresher_OmitAsConfigured()
{
try
{
CreateConfigFile();
using (var configRefresher = new SelfDiagnosticsConfigRefresher())
{
// Emitting event of EventLevel.Warning
CoreEventSource.Log.OperationIsNullWarning();
int bufferSize = 512;
byte[] actualBytes = ReadFile(bufferSize);
string logText = Encoding.UTF8.GetString(actualBytes);
Assert.IsTrue(logText.StartsWith(MessageOnNewFileString));
// The event was omitted
Assert.AreEqual('\0', (char)actualBytes[MessageOnNewFile.Length]);
}}
finally
{
CleanupConfigFile();
}
}
[TestMethod]
public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured()
{
try
{
CreateConfigFile();
using (var configRefresher = new SelfDiagnosticsConfigRefresher())
{
// Emitting event of EventLevel.Error
CoreEventSource.Log.InvalidOperationToStopError();
int bufferSize = 512;
byte[] actualBytes = ReadFile(bufferSize);
string logText = Encoding.UTF8.GetString(actualBytes);
Assert.IsTrue(logText.StartsWith(MessageOnNewFileString));
// The event was captured
string logLine = logText.Substring(MessageOnNewFileString.Length);
string logMessage = ParseLogMessage(logLine);
string expectedMessage = "Operation to stop does not match the current operation. Telemetry is not tracked.";
Assert.IsTrue(logMessage.StartsWith(expectedMessage));
}
}
finally
{
CleanupConfigFile();
}
}
private static string ParseLogMessage(string logLine)
{
int timestampPrefixLength = "2020-08-14T20:33:24.4788109Z:".Length;
Assert.IsTrue(TimeStringRegex.IsMatch(logLine.Substring(0, timestampPrefixLength)));
return logLine.Substring(timestampPrefixLength);
}
private static byte[] ReadFile(int byteCount)
{
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[byteCount];
file.Read(actualBytes, 0, byteCount);
return actualBytes;
}
}
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);
}
}
private static void CleanupConfigFile()
{
try
{
File.Delete(ConfigFilePath);
}
catch
{
}
}
}
}

Просмотреть файл

@ -10,10 +10,13 @@
using Moq;
[TestClass]
class SelfDiagnosticsEventListenerTest
public class SelfDiagnosticsEventListenerTest
{
private const string Ellipses = "...\n";
private const string EllipsesWithBrackets = "{...}\n";
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
[ExpectedException(typeof(ArgumentNullException))]
public void SelfDiagnosticsEventListener_constructor_Invalid_Input()
{
// no configRefresher object
@ -26,7 +29,7 @@
var fileHandlerMock = new Mock<MemoryMappedFileHandler>();
var listener = new SelfDiagnosticsEventListener(EventLevel.Error, fileHandlerMock.Object);
// Emitting a Verbose event. Or any EventSource event with lower severity than Error.
// Emitting a Warning event. Or any EventSource event with lower severity than Error.
CoreEventSource.Log.OperationIsNullWarning();
fileHandlerMock.Verify(fileHandler => fileHandler.Write(It.IsAny<byte[]>(), It.IsAny<int>()), Times.Never());
}
@ -78,7 +81,110 @@
pos += len;
}
Assert.AreEqual(expected, results);
CollectionAssert.AreEqual(expected, results);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_Empty()
{
byte[] buffer = new byte[20];
int startPos = 0;
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer(string.Empty, false, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes(string.Empty);
AssertBufferOutput(expected, buffer, startPos, endPos);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_EnoughSpace()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - Ellipses.Length - 6; // Just enough space for "abc" even if "...\n" needs to be added.
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos);
// '\n' will be appended to the original string "abc" after EncodeInBuffer is called.
// The byte where '\n' will be placed should not be touched within EncodeInBuffer, so it stays as '\0'.
byte[] expected = Encoding.UTF8.GetBytes("abc\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEnoughSpaceForFullString()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - Ellipses.Length - 5; // Just not space for "abc" if "...\n" needs to be added.
// It's a quick estimate by assumption that most Unicode characters takes up to 2 16-bit UTF-16 chars,
// which can be up to 4 bytes when encoded in UTF-8.
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes("ab...\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncatedString()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - Ellipses.Length; // Just enough space for "...\n".
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes("...\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_NotEvenSpaceForTruncationEllipses()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - Ellipses.Length + 1; // Not enough space for "...\n".
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", false, buffer, startPos);
Assert.AreEqual(startPos, endPos);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_EnoughSpace()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - EllipsesWithBrackets.Length - 6; // Just enough space for "abc" even if "...\n" need to be added.
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes("{abc}\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEnoughSpaceForFullString()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - EllipsesWithBrackets.Length - 5; // Just not space for "...\n".
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes("{ab...}\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncatedString()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - EllipsesWithBrackets.Length; // Just enough space for "{...}\n".
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos);
byte[] expected = Encoding.UTF8.GetBytes("{...}\0");
AssertBufferOutput(expected, buffer, startPos, endPos + 1);
}
[TestMethod]
public void SelfDiagnosticsEventListener_EncodeInBuffer_IsParameter_NotEvenSpaceForTruncationEllipses()
{
byte[] buffer = new byte[20];
int startPos = buffer.Length - EllipsesWithBrackets.Length + 1; // Not enough space for "{...}\n".
int endPos = SelfDiagnosticsEventListener.EncodeInBuffer("abc", true, buffer, startPos);
Assert.AreEqual(startPos, endPos);
}
private static void AssertBufferOutput(byte[] expected, byte[] buffer, int startPos, int endPos)
{
Assert.AreEqual(expected.Length, endPos - startPos);
for (int i = 0, j = startPos; j < endPos; ++i, ++j)
{
Assert.AreEqual(expected[i], buffer[j]);
}
}
}
}

Просмотреть файл

@ -137,7 +137,7 @@ namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.Sel
/// </summary>
/// <param name="buffer">The buffer which contains the data to be written.</param>
/// <param name="byteCount">The count of bytes to be written.</param>
public void Write(byte[] buffer, int byteCount)
public virtual void Write(byte[] buffer, int byteCount)
{
try
{

Просмотреть файл

@ -1,10 +1,7 @@
namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.SelfDiagnostics
{
using System;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

Просмотреть файл

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Tracing;
using System.IO;
using System.Text;
using System.Threading;

Просмотреть файл

@ -2,6 +2,7 @@
## VNext
- Reduce technical debt: Use pattern matching
- [Improve Self Diagnostics and support setting configuration in file](https://github.com/microsoft/ApplicationInsights-dotnet/issues/2238)
## Version 2.18.0-beta1

Просмотреть файл

@ -116,6 +116,78 @@ StatusMonitor uses TraceEventSession to record ETW logs.
- https://github.com/dotnet/roslyn/wiki/Recording-performance-traces-with-PerfView
- https://github.com/microsoft/perfview/blob/master/src/TraceEvent/TraceEventSession.cs
### Self Diagnostics
As of version 2.18.0, this SDK also ships a "self-diagnostics feature", which
helps troubleshooting.
When enabled, internal events generated by ApplicationInsights SDK will be
written to a log file.
The self-diagnostics feature can be enabled/disabled while the process
is running.
To enable self-diagnostics, go to the
[current working directory](https://en.wikipedia.org/wiki/Working_directory) of
your process and create a configuration file named `ApplicationInsightsDiagnostics.json`
with the following content:
```json
{
"LogDirectory": ".",
"FileSize": 1024,
"LogLevel": "Error"
}
```
To disable self-diagnostics, delete the above file.
Tip on finding the "current working directory":
In most cases, you could just drop the file along your application.
On Windows, you can use [Process Explorer](https://docs.microsoft.com/sysinternals/downloads/process-explorer),
double click on the process to pop up Properties dialog and find "Current
directory" in "Image" tab.
Internally, it looks for the configuration file located in
[GetCurrentDirectory](https://docs.microsoft.com/dotnet/api/system.io.directory.getcurrentdirectory),
and then [AppContext.BaseDirectory](https://docs.microsoft.com/dotnet/api/system.appcontext.basedirectory).
You can also find the exact directory by calling these methods from your code.
#### Configuration Parameters
1. `LogDirectory` is the directory where the output log file will be stored. It
can be an absolute path or a relative path to the current directory.
2. `FileSize` is a positive integer, which specifies the log file size in
[KiB](https://en.wikipedia.org/wiki/Kibibyte).
3. `LogLevel` is the lowest level of the events to be captured. It has to be one
of the
[values](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventlevel#fields)
of the [`EventLevel`
enum](https://docs.microsoft.com/dotnet/api/system.diagnostics.tracing.eventlevel).
The level signifies the severity of an event. Lower severity levels encompass
higher severity levels. For example, `Warning` includes the `Error` and
`Critical` levels.
#### Remarks
A `FileSize`-KiB log file named as `ExecutableName.ProcessId.log` (e.g.
`foobar.exe.12345.log`) will be generated at the specified directory
`LogDirectory`, into which logs are written to.
The SDK will attempt to open the configuration file in non-exclusive read-only
mode, read the file and parse it as the configuration file every 10 seconds. If
the SDK fails to parse the `LogDirectory`, `FileSize` or `LogLevel` fields as
the specified format, the configuration file will be treated as invalid and no
log file would be generated. Otherwise, it will create or overwrite the log file
as described above.
Note that the `FileSize` has to be between 1 MiB and 128 MiB (inclusive), or it
will be rounded to the closest upper or lower limit. When the `LogDirectory` or
`FileSize` is found to be changed, the SDK will create or overwrite a file with
new logs according to the new configuration. The configuration file has to be no
more than 4 KiB. In case the file is larger than 4 KiB, only the first 4 KiB of
content will be read.
## References
This document is referenced by: https://docs.microsoft.com/azure/azure-monitor/app/asp-net-troubleshoot-no-data#PerfView