Self-diagnostics - support formatted message for better human readability (#6411)

Co-authored-by: Rajkumar Rangaraj <rajrang@microsoft.com>
This commit is contained in:
Cijo Thomas 2025-08-05 10:25:11 -07:00 committed by GitHub
parent 27234c2a14
commit d2f8e54bb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 161 additions and 17 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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,6 +231,15 @@ internal sealed class SelfDiagnosticsEventListener : EventListener
var pos = DateTimeGetBytes(DateTime.UtcNow, buffer, 0);
buffer[pos++] = (byte)':';
if (this.formatMessage && eventMessage != null && payload != null && payload.Count > 0)
{
// 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)
{
@ -246,6 +257,7 @@ internal sealed class SelfDiagnosticsEventListener : EventListener
}
}
}
}
buffer[pos++] = (byte)'\n';
int byteCount = pos - 0;

View File

@ -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.

View File

@ -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);
}
}