Self-diagnostics - support formatted message for better human readability (#6411)
Co-authored-by: Rajkumar Rangaraj <rajrang@microsoft.com>
This commit is contained in:
parent
27234c2a14
commit
d2f8e54bb9
|
|
@ -6,6 +6,21 @@ Notes](../../RELEASENOTES.md).
|
|||
|
||||
## Unreleased
|
||||
|
||||
* Added `FormatMessage` configuration option to self-diagnostics feature. When
|
||||
set to `true` (default is false), log messages will be formatted by replacing
|
||||
placeholders with actual parameter values for improved readability.
|
||||
|
||||
Example `OTEL_DIAGNOSTICS.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"LogDirectory": ".",
|
||||
"FileSize": 32768,
|
||||
"LogLevel": "Warning",
|
||||
"FormatMessage": true
|
||||
}
|
||||
```
|
||||
|
||||
## 1.12.0
|
||||
|
||||
Released 2025-Apr-29
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ internal sealed class SelfDiagnosticsConfigParser
|
|||
private static readonly Regex LogLevelRegex = new(
|
||||
@"""LogLevel""\s*:\s*""(?<LogLevel>.*?)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex FormatMessageRegex = new(
|
||||
@"""FormatMessage""\s*:\s*(?:""(?<FormatMessage>.*?)""|(?<FormatMessage>true|false))", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
// This class is called in SelfDiagnosticsConfigRefresher.UpdateMemoryMappedFileFromConfiguration
|
||||
// in both main thread and the worker thread.
|
||||
// In theory the variable won't be access at the same time because worker thread first Task.Delay for a few seconds.
|
||||
|
|
@ -36,11 +39,13 @@ internal sealed class SelfDiagnosticsConfigParser
|
|||
public bool TryGetConfiguration(
|
||||
[NotNullWhen(true)] out string? logDirectory,
|
||||
out int fileSizeInKB,
|
||||
out EventLevel logLevel)
|
||||
out EventLevel logLevel,
|
||||
out bool formatMessage)
|
||||
{
|
||||
logDirectory = null;
|
||||
fileSizeInKB = 0;
|
||||
logLevel = EventLevel.LogAlways;
|
||||
formatMessage = false;
|
||||
try
|
||||
{
|
||||
var configFilePath = ConfigFileName;
|
||||
|
|
@ -107,6 +112,9 @@ internal sealed class SelfDiagnosticsConfigParser
|
|||
return false;
|
||||
}
|
||||
|
||||
// FormatMessage is optional, defaults to false
|
||||
_ = TryParseFormatMessage(configJson, out formatMessage);
|
||||
|
||||
return Enum.TryParse(logLevelString, out logLevel);
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
@ -141,4 +149,17 @@ internal sealed class SelfDiagnosticsConfigParser
|
|||
logLevel = logLevelResult.Groups["LogLevel"].Value;
|
||||
return logLevelResult.Success && !string.IsNullOrWhiteSpace(logLevel);
|
||||
}
|
||||
|
||||
internal static bool TryParseFormatMessage(string configJson, out bool formatMessage)
|
||||
{
|
||||
formatMessage = false;
|
||||
var formatMessageResult = FormatMessageRegex.Match(configJson);
|
||||
if (formatMessageResult.Success)
|
||||
{
|
||||
var formatMessageValue = formatMessageResult.Groups["FormatMessage"].Value;
|
||||
return bool.TryParse(formatMessageValue, out formatMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ internal class SelfDiagnosticsConfigRefresher : IDisposable
|
|||
private int logFileSize; // Log file size in bytes
|
||||
private long logFilePosition; // The logger will write into the byte at this position
|
||||
private EventLevel logEventLevel = (EventLevel)(-1);
|
||||
private bool formatMessage;
|
||||
|
||||
public SelfDiagnosticsConfigRefresher()
|
||||
{
|
||||
|
|
@ -138,7 +139,7 @@ internal class SelfDiagnosticsConfigRefresher : IDisposable
|
|||
|
||||
private void UpdateMemoryMappedFileFromConfiguration()
|
||||
{
|
||||
if (this.configParser.TryGetConfiguration(out string? newLogDirectory, out int fileSizeInKB, out EventLevel newEventLevel))
|
||||
if (this.configParser.TryGetConfiguration(out string? newLogDirectory, out int fileSizeInKB, out EventLevel newEventLevel, out bool formatMessage))
|
||||
{
|
||||
int newFileSize = fileSizeInKB * 1024;
|
||||
if (!newLogDirectory.Equals(this.logDirectory, StringComparison.Ordinal) || this.logFileSize != newFileSize)
|
||||
|
|
@ -147,16 +148,18 @@ internal class SelfDiagnosticsConfigRefresher : IDisposable
|
|||
this.OpenLogFile(newLogDirectory, newFileSize);
|
||||
}
|
||||
|
||||
if (!newEventLevel.Equals(this.logEventLevel))
|
||||
if (!newEventLevel.Equals(this.logEventLevel) || this.formatMessage != formatMessage)
|
||||
{
|
||||
if (this.eventListener != null)
|
||||
{
|
||||
this.eventListener.Dispose();
|
||||
}
|
||||
|
||||
this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this);
|
||||
this.eventListener = new SelfDiagnosticsEventListener(newEventLevel, this, formatMessage);
|
||||
this.logEventLevel = newEventLevel;
|
||||
}
|
||||
|
||||
this.formatMessage = formatMessage;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,17 +19,19 @@ internal sealed class SelfDiagnosticsEventListener : EventListener
|
|||
private readonly Lock lockObj = new();
|
||||
private readonly EventLevel logLevel;
|
||||
private readonly SelfDiagnosticsConfigRefresher configRefresher;
|
||||
private readonly bool formatMessage;
|
||||
private readonly ThreadLocal<byte[]?> writeBuffer = new(() => null);
|
||||
private readonly List<EventSource>? eventSourcesBeforeConstructor = [];
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher)
|
||||
public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher, bool formatMessage = false)
|
||||
{
|
||||
Guard.ThrowIfNull(configRefresher);
|
||||
|
||||
this.logLevel = logLevel;
|
||||
this.configRefresher = configRefresher;
|
||||
this.formatMessage = formatMessage;
|
||||
|
||||
List<EventSource> eventSources;
|
||||
lock (this.lockObj)
|
||||
|
|
@ -229,20 +231,30 @@ internal sealed class SelfDiagnosticsEventListener : EventListener
|
|||
|
||||
var pos = DateTimeGetBytes(DateTime.UtcNow, buffer, 0);
|
||||
buffer[pos++] = (byte)':';
|
||||
pos = EncodeInBuffer(eventMessage, false, buffer, pos);
|
||||
if (payload != null)
|
||||
|
||||
if (this.formatMessage && eventMessage != null && payload != null && payload.Count > 0)
|
||||
{
|
||||
// Not using foreach because it can cause allocations
|
||||
for (int i = 0; i < payload.Count; ++i)
|
||||
// Use string.Format to format the message with parameters
|
||||
string messageToWrite = string.Format(System.Globalization.CultureInfo.InvariantCulture, eventMessage, payload.ToArray());
|
||||
pos = EncodeInBuffer(messageToWrite, false, buffer, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = EncodeInBuffer(eventMessage, false, buffer, pos);
|
||||
if (payload != null)
|
||||
{
|
||||
object? obj = payload[i];
|
||||
if (obj != null)
|
||||
// Not using foreach because it can cause allocations
|
||||
for (int i = 0; i < payload.Count; ++i)
|
||||
{
|
||||
pos = EncodeInBuffer(obj.ToString() ?? "null", true, buffer, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = EncodeInBuffer("null", true, buffer, pos);
|
||||
object? obj = payload[i];
|
||||
if (obj != null)
|
||||
{
|
||||
pos = EncodeInBuffer(obj.ToString() ?? "null", true, buffer, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = EncodeInBuffer("null", true, buffer, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ the following content:
|
|||
{
|
||||
"LogDirectory": ".",
|
||||
"FileSize": 32768,
|
||||
"LogLevel": "Warning"
|
||||
"LogLevel": "Warning",
|
||||
"FormatMessage": "true"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -117,6 +118,25 @@ You can also find the exact directory by calling these methods from your code.
|
|||
higher severity levels. For example, `Warning` includes the `Error` and
|
||||
`Critical` levels.
|
||||
|
||||
4. `FormatMessage` is a boolean value that controls whether log messages should
|
||||
be formatted by replacing placeholders (`{0}`, `{1}`, etc.) with their actual
|
||||
parameter values. When set to `false` (default), messages are logged with
|
||||
unformatted placeholders followed by raw parameter values. When set to
|
||||
`true`, placeholders are replaced with formatted parameter values for
|
||||
improved readability.
|
||||
|
||||
**Example with `FormatMessage: false` (default):**
|
||||
|
||||
```txt
|
||||
2025-07-24T01:45:04.1020880Z:Measurements from Instrument '{0}', Meter '{1}' will be ignored. Reason: '{2}'. Suggested action: '{3}'{dotnet.gc.collections}{System.Runtime}{Instrument belongs to a Meter not subscribed by the provider.}{Use AddMeter to add the Meter to the provider.}
|
||||
```
|
||||
|
||||
**Example with `FormatMessage: true`:**
|
||||
|
||||
```txt
|
||||
2025-07-24T01:44:44.7059260Z:Measurements from Instrument 'dotnet.gc.collections', Meter 'System.Runtime' will be ignored. Reason: 'Instrument belongs to a Meter not subscribed by the provider.'. Suggested action: 'Use AddMeter to add the Meter to the provider.'
|
||||
```
|
||||
|
||||
#### Remarks
|
||||
|
||||
A `FileSize`-KiB log file named as `ExecutableName.ProcessId.log` (e.g.
|
||||
|
|
|
|||
|
|
@ -72,4 +72,77 @@ public class SelfDiagnosticsConfigParserTests
|
|||
Assert.True(SelfDiagnosticsConfigParser.TryParseLogLevel(configJson, out string? logLevelString));
|
||||
Assert.Equal("Error", logLevelString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDiagnosticsConfigParser_TryParseFormatMessage_Success()
|
||||
{
|
||||
string configJson = """
|
||||
{
|
||||
"LogDirectory": "Diagnostics",
|
||||
"FileSize": 1024,
|
||||
"LogLevel": "Error",
|
||||
"FormatMessage": "true"
|
||||
}
|
||||
""";
|
||||
Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage));
|
||||
Assert.True(formatMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDiagnosticsConfigParser_TryParseFormatMessage_CaseInsensitive()
|
||||
{
|
||||
string configJson = """
|
||||
{
|
||||
"LogDirectory": "Diagnostics",
|
||||
"fileSize": 1024,
|
||||
"formatMessage": "FALSE"
|
||||
}
|
||||
""";
|
||||
Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage));
|
||||
Assert.False(formatMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDiagnosticsConfigParser_TryParseFormatMessage_MissingField()
|
||||
{
|
||||
string configJson = """
|
||||
{
|
||||
"LogDirectory": "Diagnostics",
|
||||
"FileSize": 1024,
|
||||
"LogLevel": "Error"
|
||||
}
|
||||
""";
|
||||
Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage));
|
||||
Assert.False(formatMessage); // Should default to false
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDiagnosticsConfigParser_TryParseFormatMessage_InvalidValue()
|
||||
{
|
||||
string configJson = """
|
||||
{
|
||||
"LogDirectory": "Diagnostics",
|
||||
"FileSize": 1024,
|
||||
"LogLevel": "Error",
|
||||
"FormatMessage": "invalid"
|
||||
}
|
||||
""";
|
||||
Assert.False(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage));
|
||||
Assert.False(formatMessage); // Should default to false
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfDiagnosticsConfigParser_TryParseFormatMessage_UnquotedBoolean()
|
||||
{
|
||||
string configJson = """
|
||||
{
|
||||
"LogDirectory": "Diagnostics",
|
||||
"FileSize": 1024,
|
||||
"LogLevel": "Error",
|
||||
"FormatMessage": true
|
||||
}
|
||||
""";
|
||||
Assert.True(SelfDiagnosticsConfigParser.TryParseFormatMessage(configJson, out bool formatMessage));
|
||||
Assert.True(formatMessage);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue