-
-
Save samsp-msft/37a5aba3ef7abb5bbadf01ed48477886 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using LogEnumerator; | |
using Microsoft.Extensions.DependencyInjection.Extensions; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
using System.Collections.Concurrent; | |
using System.Runtime.Versioning; | |
using Microsoft.Extensions.Logging.Configuration; | |
using System.Reflection.Metadata.Ecma335; | |
using System.Text; | |
namespace LogEnumerator | |
{ | |
public sealed class LogEnumeratorLoggerConfiguration | |
{ | |
public bool IsEnabled = true; | |
} | |
public sealed class LogEnumeratorLogger : ILogger | |
{ | |
private LogsPerCategory _myLogs; | |
private string _category; | |
public LogEnumeratorLogger(string category, LogsPerCategory myLogs, Func<LogEnumeratorLoggerConfiguration> _getCurrentConfig) | |
{ | |
_category = category; | |
_myLogs = myLogs; | |
} | |
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!; | |
public bool IsEnabled(LogLevel logLevel) => true; | |
public void Log<TState>( | |
LogLevel logLevel, | |
EventId eventId, | |
TState state, | |
Exception? exception, | |
Func<TState, Exception?, string> formatter) | |
{ | |
string? message = null; | |
if (state is IEnumerable<KeyValuePair<string, object>> props) | |
{ | |
foreach (var kvp in props) | |
{ | |
if (kvp.Key == "{OriginalFormat}") | |
{ | |
message = kvp.Value as string; | |
break; | |
} | |
} | |
} | |
Func<LogEntry> logEntryFactory = () => new LogEntry() | |
{ | |
Category = _category, | |
Level = logLevel, | |
EventID = eventId.Id, | |
MessageFormat = message, | |
Count = 0 | |
}; | |
if (eventId.Id == 0) | |
{ | |
if (message != null) | |
{ | |
var entry = _myLogs._OtherEntries.GetOrAdd(message, (name) => logEntryFactory()); | |
Interlocked.Increment(ref entry.Count); | |
} | |
} | |
else | |
{ | |
var entry = _myLogs._IDEntries.GetOrAdd(eventId.Id, (id) => logEntryFactory()); | |
Interlocked.Increment(ref entry.Count); | |
} | |
} | |
} | |
public class LogsPerCategory | |
{ | |
public ConcurrentDictionary<int, LogEntry> _IDEntries = new(); | |
public ConcurrentDictionary<string, LogEntry> _OtherEntries = new(); | |
} | |
public class LogEntry | |
{ | |
public string Category; | |
public LogLevel Level; | |
public int? EventID; | |
public string MessageFormat; | |
public int Count; | |
} | |
[UnsupportedOSPlatform("browser")] | |
[ProviderAlias("LogEnumerator")] | |
public sealed class LogEnumeratorLoggerProvider : ILoggerProvider | |
{ | |
private readonly IDisposable? _onChangeToken; | |
private LogEnumeratorLoggerConfiguration _currentConfig; | |
private static ConcurrentDictionary<string, LogsPerCategory> _logs = new(StringComparer.OrdinalIgnoreCase); | |
private readonly ConcurrentDictionary<string, LogEnumeratorLogger> _loggers = | |
new(StringComparer.OrdinalIgnoreCase); | |
public LogEnumeratorLoggerProvider( | |
IOptionsMonitor<LogEnumeratorLoggerConfiguration> config) | |
{ | |
_currentConfig = config.CurrentValue; | |
_onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig); | |
} | |
public ILogger CreateLogger(string categoryName) | |
{ | |
var logData = _logs.GetOrAdd(categoryName, name => new LogsPerCategory()); | |
var logger = _loggers.GetOrAdd(categoryName, name => new LogEnumeratorLogger(name, logData, GetCurrentConfig)); | |
return logger; | |
} | |
private LogEnumeratorLoggerConfiguration GetCurrentConfig() => _currentConfig; | |
public void Dispose() | |
{ | |
_loggers.Clear(); | |
_onChangeToken?.Dispose(); | |
} | |
public static string dumpLogInfo() | |
{ | |
StringBuilder stringBuilder = new StringBuilder(); | |
stringBuilder.AppendLine($"Category\tLevel\tID\tMessage Format\tCount"); | |
foreach (var provider in _logs) | |
{ | |
foreach (var entry in provider.Value._IDEntries.Values) | |
{ | |
stringBuilder.AppendLine($"{entry.Category}\t{entry.Level}\t{entry.EventID,2}\t{entry.MessageFormat}\t{entry.Count}"); | |
} | |
foreach (var entry in provider.Value._OtherEntries.Values) | |
{ | |
stringBuilder.AppendLine($"{entry.Category}\t{entry.Level}\t--\t{entry.MessageFormat}\t{entry.Count}"); | |
} | |
} | |
return stringBuilder.ToString(); | |
} | |
} | |
} | |
public static class LogEnumeratorLoggerExtensions | |
{ | |
public static ILoggingBuilder AddLogEnumeratorLogger( | |
this ILoggingBuilder builder) | |
{ | |
builder.AddConfiguration(); | |
builder.Services.TryAddEnumerable( | |
ServiceDescriptor.Singleton<ILoggerProvider, LogEnumeratorLoggerProvider>()); | |
LoggerProviderOptions.RegisterProviderOptions | |
<LogEnumeratorLoggerConfiguration, LogEnumeratorLoggerProvider>(builder.Services); | |
return builder; | |
} | |
public static ILoggingBuilder AddLogEnumeratorLogger( | |
this ILoggingBuilder builder, | |
Action<LogEnumeratorLoggerConfiguration> configure) | |
{ | |
builder.AddLogEnumeratorLogger(); | |
builder.Services.Configure(configure); | |
return builder; | |
} | |
} |
Example output for the markdown format
Category | Level | ID | Message Format | Count |
---|---|---|---|---|
LoadTest | Information | -- | Loading service #{index} for {duration} with {threads} | 1 |
LoadTest | Trace | -- | Thread {threadID} making request {count} | 85 |
LoadTest | Information | -- | using service url {url} | 1 |
Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware | Debug | -- | Wildcard detected, all requests with hosts will be allowed. | 1 |
Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware | Trace | 2 | All hosts are allowed. | 7 |
Microsoft.AspNetCore.Hosting.Diagnostics | Information | 1 | 7 | |
Microsoft.AspNetCore.Hosting.Diagnostics | Information | 2 | 6 | |
Microsoft.AspNetCore.Hosting.Diagnostics | Debug | 13 | Loaded hosting startup assembly {assemblyName} | 3 |
Microsoft.AspNetCore.Routing.EndpointMiddleware | Information | -- | Executing endpoint '{EndpointName}' | 7 |
Microsoft.AspNetCore.Routing.EndpointMiddleware | Information | 1 | Executed endpoint '{EndpointName}' | 6 |
Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware | Debug | 1 | Request matched endpoint '{EndpointName}' | 7 |
Microsoft.AspNetCore.Routing.Matching.DfaMatcher | Debug | 1001 | {CandidateCount} candidate(s) found for the request path '{Path}' | 7 |
Microsoft.AspNetCore.Routing.Matching.DfaMatcher | Debug | 1005 | Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is valid for the request path '{Path}' | 1 |
Microsoft.AspNetCore.Server.Kestrel.Connections | Debug | 1 | Connection id "{ConnectionId}" started. | 6 |
Microsoft.AspNetCore.Server.Kestrel.Connections | Debug | 2 | Connection id "{ConnectionId}" stopped. | 4 |
Microsoft.AspNetCore.Server.Kestrel.Connections | Debug | 9 | Connection id "{ConnectionId}" completed keep alive response. | 3 |
Microsoft.AspNetCore.Server.Kestrel.Connections | Debug | 10 | Connection id "{ConnectionId}" disconnecting. | 3 |
Microsoft.AspNetCore.Server.Kestrel.Connections | Debug | 39 | Connection id "{ConnectionId}" accepted. | 6 |
Microsoft.AspNetCore.Server.Kestrel.Http2 | Trace | 37 | Connection id "{ConnectionId}" received {type} frame for stream ID {id} with length {length} and flags {flags}. | 8 |
Microsoft.AspNetCore.Server.Kestrel.Http2 | Trace | 49 | Connection id "{ConnectionId}" sending {type} frame for stream ID {id} with length {length} and flags {flags}. | 69 |
Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionMiddleware | Debug | 1 | Failed to authenticate HTTPS connection. | 1 |
Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionMiddleware | Debug | 3 | Connection {connectionId} established using the following protocol: {protocol} | 5 |
Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets | Debug | 6 | Connection id "{ConnectionId}" received FIN. | 4 |
Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets | Debug | 7 | Connection id "{ConnectionId}" sending FIN because: "{Reason}" | 4 |
Microsoft.Extensions.Hosting.Internal.Host | Debug | 1 | Hosting starting | 1 |
Microsoft.Extensions.Hosting.Internal.Host | Debug | 2 | Hosting started | 1 |
Microsoft.Extensions.Http.DefaultHttpClientFactory | Debug | 100 | Starting HttpMessageHandler cleanup cycle with {InitialCount} items | 32 |
Microsoft.Extensions.Http.DefaultHttpClientFactory | Debug | 101 | Ending HttpMessageHandler cleanup cycle after {ElapsedMilliseconds}ms - processed: {DisposedCount} items - remaining: {RemainingItems} items | 32 |
Microsoft.Extensions.Http.DefaultHttpClientFactory | Debug | 103 | HttpMessageHandler expired after {HandlerLifetime}ms for client '{ClientName}' | 1 |
Microsoft.Hosting.Lifetime | Information | -- | Hosting environment: {EnvName} | 1 |
Microsoft.Hosting.Lifetime | Information | -- | Application started. Press Ctrl+C to shut down. | 1 |
Microsoft.Hosting.Lifetime | Information | -- | Content root path: {ContentRoot} | 1 |
Microsoft.Hosting.Lifetime | Information | 14 | Now listening on: {address} | 2 |
System.Net.Http.HttpClient.Default.ClientHandler | Information | 100 | Sending HTTP request {HttpMethod} {Uri} | 85 |
System.Net.Http.HttpClient.Default.ClientHandler | Information | 101 | Received HTTP response headers after {ElapsedMilliseconds}ms - {StatusCode} | 84 |
System.Net.Http.HttpClient.Default.ClientHandler | Trace | 102 | 85 | |
System.Net.Http.HttpClient.Default.ClientHandler | Trace | 103 | 84 | |
System.Net.Http.HttpClient.Default.LogicalHandler | Information | 100 | Start processing HTTP request {HttpMethod} {Uri} | 85 |
System.Net.Http.HttpClient.Default.LogicalHandler | Information | 101 | End processing HTTP request after {ElapsedMilliseconds}ms - {StatusCode} | 84 |
System.Net.Http.HttpClient.Default.LogicalHandler | Trace | 102 | 85 | |
System.Net.Http.HttpClient.Default.LogicalHandler | Trace | 103 | 84 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To Use:
Add to the app initialization
Provide a configuration with the max level of information you want to collect:
And then to get the log messages created use:
Which will dump a table of results dependent on the requested format. eg curl -k https://localhost:7088/logs?format=markdown