Skip to content

Instantly share code, notes, and snippets.

@samsp-msft
Last active May 26, 2023 22:17
Show Gist options
  • Save samsp-msft/37a5aba3ef7abb5bbadf01ed48477886 to your computer and use it in GitHub Desktop.
Save samsp-msft/37a5aba3ef7abb5bbadf01ed48477886 to your computer and use it in GitHub Desktop.
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;
}
}
@samsp-msft
Copy link
Author

samsp-msft commented May 26, 2023

To Use:
Add to the app initialization

builder.Logging.AddLogEnumeratorLogger();

Provide a configuration with the max level of information you want to collect:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "System.Net.Http": "Warning"
    },
    "Console": {
      "IncludeScopes": true,
      "LogLevel": {
        "Microsoft": "Information",
        "Default": "Information"
      }
    },
    "LogEnumerator": {
      "LogLevel": {
        "Default": "Trace"
      }
    }
  }
}

And then to get the log messages created use:

app.MapGet("/Logs", LogEnumeratorLoggerProvider.DumpLogInfo);

Which will dump a table of results dependent on the requested format. eg curl -k https://localhost:7088/logs?format=markdown

@samsp-msft
Copy link
Author

samsp-msft commented May 26, 2023

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