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:
Родитель
dd497a9b7a
Коммит
1b8e58f950
|
@ -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
|
Загрузка…
Ссылка в новой задаче