using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;
+using System.Text;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Extensions.Logging;
{
public class LoggingSourceConfiguration : MonitoringSourceConfiguration
{
- private const string UseAppFilters = "UseAppFilters";
-
- private readonly LogLevel _level;
- private readonly bool _useAppFilters;
+ private readonly string _filterSpecs;
+ private readonly long _keywords;
+ private readonly EventLevel _level;
/// <summary>
/// Creates a new logging source configuration.
/// </summary>
- /// <param name="level">The logging level. Log messages at or above the log level will be included.</param>
- /// <param name="useAppFilters">Use the UseAppFilters filterspec. This supersedes the log level and generates
- /// log messages with the same levels per category as specified by the application configuration.</param>
- public LoggingSourceConfiguration(LogLevel level = LogLevel.Debug, bool useAppFilters = false)
+ public LoggingSourceConfiguration(LogLevel level, LogMessageType messageType, IDictionary<string, LogLevel?> filterSpecs, bool useAppFilters)
{
- _level = level;
- _useAppFilters = useAppFilters;
+ _filterSpecs = ToFilterSpecsString(filterSpecs, useAppFilters);
+ _keywords = (long)ToKeywords(messageType);
+ _level = ToEventLevel(level);
}
public override IList<EventPipeProvider> GetProviders()
{
- string filterSpec = _useAppFilters ? UseAppFilters : FormattableString.Invariant($"*:{_level:G}");
-
- var providers = new List<EventPipeProvider>()
+ return new List<EventPipeProvider>()
{
-
- // Logging
new EventPipeProvider(
MicrosoftExtensionsLoggingProviderName,
- EventLevel.LogAlways,
- (long)(LoggingEventSource.Keywords.JsonMessage | LoggingEventSource.Keywords.FormattedMessage),
+ _level,
+ _keywords,
arguments: new Dictionary<string,string>
{
-
- { "FilterSpecs", filterSpec }
+ { "FilterSpecs", _filterSpecs }
}
)
};
+ }
+
+ private static string ToFilterSpecsString(IDictionary<string, LogLevel?> filterSpecs, bool useAppFilters)
+ {
+ if (!useAppFilters && (filterSpecs?.Count).GetValueOrDefault(0) == 0)
+ {
+ return String.Empty;
+ }
+
+ StringBuilder filterSpecsBuilder = new StringBuilder();
+
+ if (useAppFilters)
+ {
+ filterSpecsBuilder.Append("UseAppFilters");
+ }
+
+ if (null != filterSpecs)
+ {
+ foreach (KeyValuePair<string, LogLevel?> filterSpec in filterSpecs)
+ {
+ if (!string.IsNullOrEmpty(filterSpec.Key))
+ {
+ if (filterSpecsBuilder.Length > 0)
+ {
+ filterSpecsBuilder.Append(";");
+ }
+ filterSpecsBuilder.Append(filterSpec.Key);
+ if (filterSpec.Value.HasValue)
+ {
+ filterSpecsBuilder.Append(":");
+ filterSpecsBuilder.Append(filterSpec.Value.Value.ToString("G"));
+ }
+ }
+ }
+ }
+
+ return filterSpecsBuilder.ToString();
+ }
- return providers;
+ private static EventLevel ToEventLevel(LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.None:
+ throw new NotSupportedException($"{nameof(LogLevel)} {nameof(LogLevel.None)} is not supported as the default log level.");
+ case LogLevel.Trace:
+ return EventLevel.LogAlways;
+ case LogLevel.Debug:
+ return EventLevel.Verbose;
+ case LogLevel.Information:
+ return EventLevel.Informational;
+ case LogLevel.Warning:
+ return EventLevel.Warning;
+ case LogLevel.Error:
+ return EventLevel.Error;
+ case LogLevel.Critical:
+ return EventLevel.Critical;
+ }
+ throw new InvalidOperationException($"Unable to convert {logLevel:G} to EventLevel.");
+ }
+
+ private static EventKeywords ToKeywords(LogMessageType messageType)
+ {
+ EventKeywords keywords = 0;
+ if (messageType.HasFlag(LogMessageType.FormattedMessage))
+ {
+ keywords |= LoggingEventSource.Keywords.FormattedMessage;
+ }
+ if (messageType.HasFlag(LogMessageType.JsonMessage))
+ {
+ keywords |= LoggingEventSource.Keywords.JsonMessage;
+ }
+ if (messageType.HasFlag(LogMessageType.Message))
+ {
+ keywords |= LoggingEventSource.Keywords.Message;
+ }
+ return keywords;
}
private sealed class LoggingEventSource
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using Microsoft.Diagnostics.NETCore.Client;
+using Microsoft.Diagnostics.NETCore.Client.UnitTests;
+using Microsoft.Extensions.Logging;
using System;
-using System.Collections;
using System.Collections.Generic;
using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Diagnostics.Monitoring;
-using Microsoft.Diagnostics.Monitoring.EventPipe;
-using Microsoft.Diagnostics.NETCore.Client;
-using Microsoft.Diagnostics.NETCore.Client.UnitTests;
-using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
-using Xunit.Extensions;
namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests
{
public class EventLogsPipelineUnitTests
{
+ private const string WildcardCagetoryName = "*";
+ private const string AppLoggerCategoryName = "AppLoggerCategory";
+ private const string LoggerRemoteTestName = "LoggerRemoteTest";
+
private readonly ITestOutputHelper _output;
public EventLogsPipelineUnitTests(ITestOutputHelper output)
_output = output;
}
+ /// <summary>
+ /// Test that all log events are collected if no filters are specified.
+ /// </summary>
+ [Fact]
+ public async Task TestLogsAllCategoriesAllLevels()
+ {
+ using Stream outputStream = await GetLogsAsync(settings =>
+ {
+ settings.UseAppFilters = false;
+ });
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateLoggerRemoteCategoryInformationMessage(reader);
+ ValidateLoggerRemoteCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryInformationMessage(reader);
+ ValidateAppLoggerCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ /// <summary>
+ /// Test that log events at or above the default level are collected.
+ /// </summary>
+ [Fact]
+ public async Task TestLogsAllCategoriesDefaultLevel()
+ {
+ using Stream outputStream = await GetLogsAsync(settings =>
+ {
+ settings.UseAppFilters = false;
+ settings.LogLevel = LogLevel.Warning;
+ });
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateLoggerRemoteCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ /// <summary>
+ /// Test that log events at the default level are collected for categories without a specified level.
+ /// </summary>
+ [Fact]
+ public async Task TestLogsAllCategoriesDefaultLevelFallback()
+ {
+ using Stream outputStream = await GetLogsAsync(settings =>
+ {
+ settings.UseAppFilters = false;
+ settings.LogLevel = LogLevel.Error;
+ settings.FilterSpecs = new Dictionary<string, LogLevel?>()
+ {
+ { AppLoggerCategoryName, null },
+ { LoggerRemoteTestName, LogLevel.Trace }
+ };
+ });
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateLoggerRemoteCategoryInformationMessage(reader);
+ ValidateLoggerRemoteCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ /// <summary>
+ /// Test that LogLevel.None is not supported as the default log level.
+ /// </summary>
+ [Fact]
+ public Task TestLogsAllCategoriesDefaultLevelNoneNotSupported()
+ {
+ return Assert.ThrowsAsync<NotSupportedException>(() => GetLogsAsync(settings =>
+ {
+ settings.UseAppFilters = false;
+ settings.LogLevel = LogLevel.None;
+ }));
+ }
+
+ /// <summary>
+ /// Test that log events are collected for the categories and levels specified by the application.
+ /// </summary>
+ [Fact]
+ public async Task TestLogsUseAppFilters()
+ {
+ using Stream outputStream = await GetLogsAsync();
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateAppLoggerCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ /// <summary>
+ /// Test that log events are collected for the categories and levels specified by the application
+ /// and for the categories and levels specified in the filter specs.
+ /// </summary>
[Fact]
- public async Task TestLogs()
+ public async Task TestLogsUseAppFiltersAndFilterSpecs()
+ {
+ using Stream outputStream = await GetLogsAsync(settings =>
+ {
+ settings.FilterSpecs = new Dictionary<string, LogLevel?>()
+ {
+ { LoggerRemoteTestName, LogLevel.Warning }
+ };
+ });
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateLoggerRemoteCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ /// <summary>
+ /// Test that log events are collected for wildcard categories.
+ /// </summary>
+ [Fact]
+ public async Task TestLogsWildcardCategory()
+ {
+ using Stream outputStream = await GetLogsAsync(settings =>
+ {
+ settings.UseAppFilters = false;
+ settings.LogLevel = LogLevel.Critical;
+ settings.FilterSpecs = new Dictionary<string, LogLevel?>()
+ {
+ { WildcardCagetoryName, LogLevel.Warning },
+ { LoggerRemoteTestName, LogLevel.Error },
+ };
+ });
+
+ Assert.True(outputStream.Length > 0, "No data written by logging process.");
+
+ using var reader = new StreamReader(outputStream);
+
+ ValidateAppLoggerCategoryWarningMessage(reader);
+ ValidateAppLoggerCategoryErrorMessage(reader);
+
+ Assert.True(reader.EndOfStream, "Expected to have read all entries from stream.");
+ }
+
+ private async Task<Stream> GetLogsAsync(Action<EventLogsPipelineSettings> settingsCallback = null)
{
var outputStream = new MemoryStream();
- await using (var testExecution = StartTraceeProcess("LoggerRemoteTest"))
+ await using (var testExecution = StartTraceeProcess(LoggerRemoteTestName))
{
//TestRunner should account for start delay to make sure that the diagnostic pipe is available.
using var loggerFactory = new LoggerFactory(new[] { new TestStreamingLoggerProvider(outputStream) });
var client = new DiagnosticsClient(testExecution.TestRunner.Pid);
- var logSettings = new EventLogsPipelineSettings { Duration = Timeout.InfiniteTimeSpan};
+ var logSettings = new EventLogsPipelineSettings { Duration = Timeout.InfiniteTimeSpan };
+ if (null != settingsCallback)
+ {
+ settingsCallback(logSettings);
+ }
await using var pipeline = new EventLogsPipeline(client, logSettings, loggerFactory);
await PipelineTestUtilities.ExecutePipelineWithDebugee(pipeline, testExecution);
outputStream.Position = 0L;
- Assert.True(outputStream.Length > 0, "No data written by logging process.");
-
- using var reader = new StreamReader(outputStream);
+ return outputStream;
+ }
- string firstMessage = reader.ReadLine();
- Assert.NotNull(firstMessage);
+ private static void ValidateLoggerRemoteCategoryInformationMessage(StreamReader reader)
+ {
+ string message = reader.ReadLine();
+ Assert.NotNull(message);
- LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(firstMessage);
+ LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(message);
Assert.Equal("Some warning message with 6", result.Message);
- Assert.Equal("LoggerRemoteTest", result.Category);
- Assert.Equal("Warning", result.LogLevel);
+ Assert.Equal(LoggerRemoteTestName, result.Category);
+ Assert.Equal("Information", result.LogLevel);
Assert.Equal("0", result.EventId);
Validate(result.Scopes, ("BoolValue", "true"), ("StringValue", "test"), ("IntValue", "5"));
Validate(result.Arguments, ("arg", "6"));
+ }
- string secondMessage = reader.ReadLine();
- Assert.NotNull(secondMessage);
+ private static void ValidateLoggerRemoteCategoryWarningMessage(StreamReader reader)
+ {
+ string message = reader.ReadLine();
+ Assert.NotNull(message);
- result = JsonSerializer.Deserialize<LoggerTestResult>(secondMessage);
+ LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(message);
Assert.Equal("Another message", result.Message);
- Assert.Equal("LoggerRemoteTest", result.Category);
+ Assert.Equal(LoggerRemoteTestName, result.Category);
Assert.Equal("Warning", result.LogLevel);
Assert.Equal("0", result.EventId);
Assert.Equal(0, result.Scopes.Count);
Assert.Equal(1, result.Arguments.Count);
}
+ private static void ValidateAppLoggerCategoryInformationMessage(StreamReader reader)
+ {
+ string message = reader.ReadLine();
+ Assert.NotNull(message);
+
+ LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(message);
+ Assert.Equal("Information message.", result.Message);
+ Assert.Equal(AppLoggerCategoryName, result.Category);
+ Assert.Equal("Information", result.LogLevel);
+ Assert.Equal("0", result.EventId);
+ Assert.Equal(0, result.Scopes.Count);
+ //We are expecting only the original format
+ Assert.Equal(1, result.Arguments.Count);
+ }
+
+ private static void ValidateAppLoggerCategoryWarningMessage(StreamReader reader)
+ {
+ string message = reader.ReadLine();
+ Assert.NotNull(message);
+
+ LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(message);
+ Assert.Equal("Warning message.", result.Message);
+ Assert.Equal(AppLoggerCategoryName, result.Category);
+ Assert.Equal("Warning", result.LogLevel);
+ Assert.Equal("0", result.EventId);
+ Assert.Equal(0, result.Scopes.Count);
+ //We are expecting only the original format
+ Assert.Equal(1, result.Arguments.Count);
+ }
+
+ private static void ValidateAppLoggerCategoryErrorMessage(StreamReader reader)
+ {
+ string message = reader.ReadLine();
+ Assert.NotNull(message);
+
+ LoggerTestResult result = JsonSerializer.Deserialize<LoggerTestResult>(message);
+ Assert.Equal("Error message.", result.Message);
+ Assert.Equal(AppLoggerCategoryName, result.Category);
+ Assert.Equal("Error", result.LogLevel);
+ Assert.Equal("0", result.EventId);
+ Assert.Equal(0, result.Scopes.Count);
+ //We are expecting only the original format
+ Assert.Equal(1, result.Arguments.Count);
+ }
+
private static void Validate(IDictionary<string, JsonElement> values, params (string key, object value)[] expectedValues)
{
Assert.NotNull(values);