Skip to content

Commit 223249f

Browse files
geeknoidMartin Tailleferstephentoub
authored
Introduce IBufferedLogger (#103138)
* Introduce IBufferedLogger * Update following review * Apply suggestions from code review Co-authored-by: Stephen Toub <[email protected]> * Next batch of review changes * Another fix * More fixes --------- Co-authored-by: Martin Taillefer <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent c19c03e commit 223249f

File tree

12 files changed

+294
-41
lines changed

12 files changed

+294
-41
lines changed

src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,21 @@ public NullLogger() { }
202202
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; }
203203
public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func<TState, System.Exception?, string> formatter) { }
204204
}
205+
public abstract class BufferedLogRecord
206+
{
207+
public abstract System.DateTimeOffset Timestamp { get; }
208+
public abstract Microsoft.Extensions.Logging.LogLevel LogLevel { get; }
209+
public abstract Microsoft.Extensions.Logging.EventId EventId { get; }
210+
public virtual string? Exception { get; }
211+
public virtual System.Diagnostics.ActivitySpanId? ActivitySpanId { get; }
212+
public virtual System.Diagnostics.ActivityTraceId? ActivityTraceId { get; }
213+
public virtual int? ManagedThreadId { get; }
214+
public virtual string? FormattedMessage { get; }
215+
public virtual string? MessageTemplate { get; }
216+
public virtual System.Collections.Generic.IReadOnlyList<System.Collections.Generic.KeyValuePair<string, object?>> Attributes { get; }
217+
}
218+
public interface IBufferedLogger
219+
{
220+
void LogRecords(System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.Abstractions.BufferedLogRecord> records);
221+
}
205222
}

src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99

1010
<ItemGroup>
1111
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.DependencyInjection.Abstractions\ref\Microsoft.Extensions.DependencyInjection.Abstractions.csproj" />
12+
<ProjectReference Include="$(LibrariesProjectRoot)System.Diagnostics.DiagnosticSource\ref\System.Diagnostics.DiagnosticSource.csproj" />
1213
</ItemGroup>
1314
</Project>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
8+
namespace Microsoft.Extensions.Logging.Abstractions
9+
{
10+
/// <summary>
11+
/// Represents a buffered log record to be written in batch to an <see cref="IBufferedLogger" />.
12+
/// </summary>
13+
/// <remarks>
14+
/// Instances of this type may be pooled and reused. Implementations of <see cref="IBufferedLogger" /> must
15+
/// not hold onto instance of <see cref="BufferedLogRecord" /> passed to its <see cref="IBufferedLogger.LogRecords" /> method
16+
/// beyond the invocation of that method.
17+
/// </remarks>
18+
public abstract class BufferedLogRecord
19+
{
20+
/// <summary>
21+
/// Gets the time when the log record was first created.
22+
/// </summary>
23+
public abstract DateTimeOffset Timestamp { get; }
24+
25+
/// <summary>
26+
/// Gets the record's logging severity level.
27+
/// </summary>
28+
public abstract LogLevel LogLevel { get; }
29+
30+
/// <summary>
31+
/// Gets the record's event id.
32+
/// </summary>
33+
public abstract EventId EventId { get; }
34+
35+
/// <summary>
36+
/// Gets an exception string for this record.
37+
/// </summary>
38+
public virtual string? Exception => null;
39+
40+
/// <summary>
41+
/// Gets an activity span ID for this record, representing the state of the thread that created the record.
42+
/// </summary>
43+
public virtual ActivitySpanId? ActivitySpanId => null;
44+
45+
/// <summary>
46+
/// Gets an activity trace ID for this record, representing the state of the thread that created the record.
47+
/// </summary>
48+
public virtual ActivityTraceId? ActivityTraceId => null;
49+
50+
/// <summary>
51+
/// Gets the ID of the thread that created the log record.
52+
/// </summary>
53+
public virtual int? ManagedThreadId => null;
54+
55+
/// <summary>
56+
/// Gets the formatted log message.
57+
/// </summary>
58+
public virtual string? FormattedMessage => null;
59+
60+
/// <summary>
61+
/// Gets the original log message template.
62+
/// </summary>
63+
public virtual string? MessageTemplate => null;
64+
65+
/// <summary>
66+
/// Gets the variable set of name/value pairs associated with the record.
67+
/// </summary>
68+
public virtual IReadOnlyList<KeyValuePair<string, object?>> Attributes => [];
69+
}
70+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Extensions.Logging.Abstractions
7+
{
8+
/// <summary>
9+
/// Represents the ability of a logging provider to support buffered logging.
10+
/// </summary>
11+
/// <remarks>
12+
/// A logging provider implements the <see cref="ILogger" /> interface that gets invoked by the
13+
/// logging infrastructure whenever it’s time to log a piece of state.
14+
///
15+
/// A logging provider may also optionally implement the <see cref="IBufferedLogger" /> interface.
16+
/// The logging infrastructure may type-test the <see cref="ILogger" /> object to determine if
17+
/// it supports the <see cref="IBufferedLogger" /> interface. If it does, that indicates to the
18+
/// logging infrastructure that the logging provider supports buffering. Whenever log
19+
/// buffering is enabled, buffered log records may be delivered to the logging provider
20+
/// in a batch via <see cref="IBufferedLogger.LogRecords" />.
21+
///
22+
/// If a logging provider does not support log buffering, then it will always be given
23+
/// unbuffered log records. If a logging provider does support log buffering, whether its
24+
/// <see cref="ILogger" /> or <see cref="IBufferedLogger" /> implementation is used is
25+
/// determined by the log producer.
26+
/// </remarks>
27+
public interface IBufferedLogger
28+
{
29+
/// <summary>
30+
/// Delivers a batch of buffered log records to a logging provider.
31+
/// </summary>
32+
/// <param name="records">The buffered log records to log.</param>
33+
/// <remarks>
34+
/// Once this function returns, the implementation should no longer access the records
35+
/// or state referenced by these records since the instances may be reused to represent other logs.
36+
/// </remarks>
37+
void LogRecords(IEnumerable<BufferedLogRecord> records);
38+
}
39+
}

src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Microsoft.Extensions.Logging.Abstractions.NullLogger</PackageDescription>
5353
<ProjectReference Include="..\gen\Microsoft.Extensions.Logging.Generators.Roslyn4.4.csproj"
5454
ReferenceOutputAssembly="false"
5555
PackAsAnalyzer="true" />
56+
<ProjectReference Include="$(LibrariesProjectRoot)System.Diagnostics.DiagnosticSource\src\System.Diagnostics.DiagnosticSource.csproj" />
5657
</ItemGroup>
5758

5859
</Project>

src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected ConsoleFormatter(string name)
3232
/// Writes the log message to the specified TextWriter.
3333
/// </summary>
3434
/// <remarks>
35-
/// if the formatter wants to write colors to the console, it can do so by embedding ANSI color codes into the string
35+
/// If the formatter wants to write colors to the console, it can do so by embedding ANSI color codes into the string.
3636
/// </remarks>
3737
/// <param name="logEntry">The log entry.</param>
3838
/// <param name="scopeProvider">The provider of scope data.</param>

src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Diagnostics.CodeAnalysis;
67
using System.IO;
78
using System.Runtime.Versioning;
@@ -13,7 +14,7 @@ namespace Microsoft.Extensions.Logging.Console
1314
/// A logger that writes messages in the console.
1415
/// </summary>
1516
[UnsupportedOSPlatform("browser")]
16-
internal sealed class ConsoleLogger : ILogger
17+
internal sealed class ConsoleLogger : ILogger, IBufferedLogger
1718
{
1819
private readonly string _name;
1920
private readonly ConsoleLoggerProcessor _queueProcessor;
@@ -69,6 +70,35 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
6970
_queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: logLevel >= Options.LogToStandardErrorThreshold));
7071
}
7172

73+
/// <inheritdoc />
74+
public void LogRecords(IEnumerable<BufferedLogRecord> records)
75+
{
76+
ThrowHelper.ThrowIfNull(records);
77+
78+
StringWriter writer = t_stringWriter ??= new StringWriter();
79+
80+
var sb = writer.GetStringBuilder();
81+
foreach (var rec in records)
82+
{
83+
var logEntry = new LogEntry<BufferedLogRecord>(rec.LogLevel, _name, rec.EventId, rec, null, static (s, _) => s.FormattedMessage ?? string.Empty);
84+
Formatter.Write(in logEntry, null, writer);
85+
86+
if (sb.Length == 0)
87+
{
88+
continue;
89+
}
90+
91+
string computedAnsiString = sb.ToString();
92+
sb.Clear();
93+
_queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: rec.LogLevel >= Options.LogToStandardErrorThreshold));
94+
}
95+
96+
if (sb.Capacity > 1024)
97+
{
98+
sb.Capacity = 1024;
99+
}
100+
}
101+
72102
/// <inheritdoc />
73103
public bool IsEnabled(LogLevel logLevel)
74104
{

src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,34 @@ public JsonConsoleFormatter(IOptionsMonitor<JsonConsoleFormatterOptions> options
2828

2929
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
3030
{
31-
string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
32-
if (logEntry.Exception == null && message == null)
31+
if (logEntry.State is BufferedLogRecord bufferedRecord)
3332
{
34-
return;
33+
string message = bufferedRecord.FormattedMessage ?? string.Empty;
34+
WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, logEntry.Category, bufferedRecord.EventId.Id, bufferedRecord.Exception,
35+
bufferedRecord.Attributes.Count > 0, null, bufferedRecord.Attributes, bufferedRecord.Timestamp);
3536
}
37+
else
38+
{
39+
string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
40+
if (logEntry.Exception == null && message == null)
41+
{
42+
return;
43+
}
3644

37-
// We extract most of the work into a non-generic method to save code size. If this was left in the generic
38-
// method, we'd get generic specialization for all TState parameters, but that's unnecessary.
39-
WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception,
40-
logEntry.State != null, logEntry.State?.ToString(), logEntry.State as IReadOnlyCollection<KeyValuePair<string, object>>);
45+
DateTimeOffset stamp = FormatterOptions.TimestampFormat != null
46+
? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now)
47+
: DateTimeOffset.MinValue;
48+
49+
// We extract most of the work into a non-generic method to save code size. If this was left in the generic
50+
// method, we'd get generic specialization for all TState parameters, but that's unnecessary.
51+
WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception?.ToString(),
52+
logEntry.State != null, logEntry.State?.ToString(), logEntry.State as IReadOnlyList<KeyValuePair<string, object?>>, stamp);
53+
}
4154
}
4255

43-
private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel,
44-
string category, int eventId, Exception? exception, bool hasState, string? stateMessage, IReadOnlyCollection<KeyValuePair<string, object>>? stateProperties)
56+
private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string? message, LogLevel logLevel,
57+
string category, int eventId, string? exception, bool hasState, string? stateMessage, IReadOnlyList<KeyValuePair<string, object?>>? stateProperties,
58+
DateTimeOffset stamp)
4559
{
4660
const int DefaultBufferSize = 1024;
4761
using (var output = new PooledByteBufferWriter(DefaultBufferSize))
@@ -52,8 +66,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex
5266
var timestampFormat = FormatterOptions.TimestampFormat;
5367
if (timestampFormat != null)
5468
{
55-
DateTimeOffset dateTimeOffset = FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
56-
writer.WriteString("Timestamp", dateTimeOffset.ToString(timestampFormat));
69+
writer.WriteString("Timestamp", stamp.ToString(timestampFormat));
5770
}
5871
writer.WriteNumber(nameof(LogEntry<object>.EventId), eventId);
5972
writer.WriteString(nameof(LogEntry<object>.LogLevel), GetLogLevelString(logLevel));
@@ -62,7 +75,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex
6275

6376
if (exception != null)
6477
{
65-
writer.WriteString(nameof(Exception), exception.ToString());
78+
writer.WriteString(nameof(Exception), exception);
6679
}
6780

6881
if (hasState)
@@ -71,7 +84,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex
7184
writer.WriteString("Message", stateMessage);
7285
if (stateProperties != null)
7386
{
74-
foreach (KeyValuePair<string, object> item in stateProperties)
87+
foreach (KeyValuePair<string, object?> item in stateProperties)
7588
{
7689
WriteItem(writer, item);
7790
}
@@ -131,11 +144,11 @@ private void WriteScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider
131144
writer.WriteStartArray("Scopes");
132145
scopeProvider.ForEachScope((scope, state) =>
133146
{
134-
if (scope is IEnumerable<KeyValuePair<string, object>> scopeItems)
147+
if (scope is IEnumerable<KeyValuePair<string, object?>> scopeItems)
135148
{
136149
state.WriteStartObject();
137150
state.WriteString("Message", scope.ToString());
138-
foreach (KeyValuePair<string, object> item in scopeItems)
151+
foreach (KeyValuePair<string, object?> item in scopeItems)
139152
{
140153
WriteItem(state, item);
141154
}
@@ -150,7 +163,7 @@ private void WriteScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider
150163
}
151164
}
152165

153-
private static void WriteItem(Utf8JsonWriter writer, KeyValuePair<string, object> item)
166+
private static void WriteItem(Utf8JsonWriter writer, KeyValuePair<string, object?> item)
154167
{
155168
var key = item.Key;
156169
switch (item.Value)

src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,27 @@ public void Dispose()
4646

4747
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
4848
{
49-
string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
50-
if (logEntry.Exception == null && message == null)
49+
if (logEntry.State is BufferedLogRecord bufferedRecord)
5150
{
52-
return;
51+
string message = bufferedRecord.FormattedMessage ?? string.Empty;
52+
WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, bufferedRecord.EventId.Id, bufferedRecord.Exception, logEntry.Category, bufferedRecord.Timestamp);
5353
}
54+
else
55+
{
56+
string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
57+
if (logEntry.Exception == null && message == null)
58+
{
59+
return;
60+
}
5461

55-
// We extract most of the work into a non-generic method to save code size. If this was left in the generic
56-
// method, we'd get generic specialization for all TState parameters, but that's unnecessary.
57-
WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.EventId.Id, logEntry.Exception, logEntry.Category);
62+
// We extract most of the work into a non-generic method to save code size. If this was left in the generic
63+
// method, we'd get generic specialization for all TState parameters, but that's unnecessary.
64+
WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.EventId.Id, logEntry.Exception?.ToString(), logEntry.Category, GetCurrentDateTime());
65+
}
5866
}
5967

6068
private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel,
61-
int eventId, Exception? exception, string category)
69+
int eventId, string? exception, string category, DateTimeOffset stamp)
6270
{
6371
ConsoleColors logLevelColors = GetLogLevelConsoleColors(logLevel);
6472
string logLevelString = GetLogLevelString(logLevel);
@@ -67,8 +75,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex
6775
string? timestampFormat = FormatterOptions.TimestampFormat;
6876
if (timestampFormat != null)
6977
{
70-
DateTimeOffset dateTimeOffset = GetCurrentDateTime();
71-
timestamp = dateTimeOffset.ToString(timestampFormat);
78+
timestamp = stamp.ToString(timestampFormat);
7279
}
7380
if (timestamp != null)
7481
{
@@ -114,7 +121,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex
114121
if (exception != null)
115122
{
116123
// exception message
117-
WriteMessage(textWriter, exception.ToString(), singleLine);
124+
WriteMessage(textWriter, exception, singleLine);
118125
}
119126
if (singleLine)
120127
{
@@ -148,7 +155,9 @@ static void WriteReplacing(TextWriter writer, string oldValue, string newValue,
148155

149156
private DateTimeOffset GetCurrentDateTime()
150157
{
151-
return FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
158+
return FormatterOptions.TimestampFormat != null
159+
? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now)
160+
: DateTimeOffset.MinValue;
152161
}
153162

154163
private static string GetLogLevelString(LogLevel logLevel)

0 commit comments

Comments
 (0)