-
Notifications
You must be signed in to change notification settings - Fork 844
Add logging buffering #5635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
evgenyfedorov2
merged 55 commits into
dotnet:main
from
evgenyfedorov2:evgenyfedorov2/log_buffering
Apr 9, 2025
Merged
Add logging buffering #5635
Changes from 1 commit
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
988c709
Buffering
evgenyfedorov2 2f1a335
Major update
evgenyfedorov2 2d2412e
Remove Json exception converter
evgenyfedorov2 f7eaab1
Fix namespaces
evgenyfedorov2 1f464df
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 a371d9c
Fix build
evgenyfedorov2 d7661a6
Slight design changes with interfaces as per PR comments
evgenyfedorov2 9d13ab0
Drop json serialization
evgenyfedorov2 fe00658
Add log record size estimation and limit buffer size in bytes
evgenyfedorov2 5fc421c
Add filtering by attributes
evgenyfedorov2 70cfc7c
Use attributes directly instead of Func delegate
evgenyfedorov2 e96277f
Add http buffer holder
evgenyfedorov2 a79fcbf
Make ILoggingBuffer and DeserializedLogRecord types internal
evgenyfedorov2 8a91c15
Move shared files to Shared project and add more tests
evgenyfedorov2 4f524eb
Add custom equality comparer
evgenyfedorov2 b2b6e56
Address API Review feedback
evgenyfedorov2 f3a6b85
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 1370225
merge
evgenyfedorov2 393ce26
API review feedback
evgenyfedorov2 20b5a4c
Remove extra lines
evgenyfedorov2 f5ce71e
Make tests culture agnostic
ce2f7fb
Global log buffering - options validation, rule selector optimization
6ffbacd
Rename to PerIncomingRequest
evgenyfedorov2 1370ccb
Minor updates
56257ca
More renames
73a8678
Rename shared folder LoggingBuffering to LogBuffering
8a42385
Remove per request options refresh because buffer are scoped anyway
7ff16fc
Remove unnecessary casting
35223f3
Add DebuggerDisplay to SerializedLogRecord.cs
bf0b59d
Added pooling of log record attributes lists
715ce25
Moved validation for max one asterisk in log category to options vali…
ccdfaeb
Add size of SerializedLogRecord struct to ballpark size estimation
4e1566b
Added a remark
5171413
Added remarks on buffer filter rules
de83448
Enable log buffering for .NET 8
b137e88
Revert "Enable log buffering for .NET 8"
821ff0b
Fix warnings
406df25
Revert unnecessary changes
evgenyfedorov2 4dc02f4
update
evgenyfedorov2 053d6c1
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 6a26ce8
PR feedback
c93a183
.
542bdd2
Improve string size calculation
6c85897
Refactor TryEnqueue
4248980
Add a double-buffer pattern to avoid race conditions
c824379
Flush in batches and pool lists for emitted records
1bc60c0
Fix racing conditions for buffers
2e2a6a4
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 5e49d69
Use two buffers
f6a2dec
Add more list pools
6e92aec
More tests
1fef67b
Update
51d67d4
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 621578a
PR Comments
ec2ef64
Merge branch 'main' into evgenyfedorov2/log_buffering
evgenyfedorov2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
97 changes: 97 additions & 0 deletions
97
src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBuffer.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.Diagnostics; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Abstractions; | ||
| using Microsoft.Extensions.Options; | ||
|
|
||
| namespace Microsoft.AspNetCore.Diagnostics.Logging; | ||
|
|
||
| internal sealed class HttpRequestBuffer : ILoggingBuffer | ||
| { | ||
| private readonly IOptionsMonitor<HttpRequestBufferOptions> _options; | ||
| private readonly ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>> _buffers; | ||
| private readonly TimeProvider _timeProvider = TimeProvider.System; | ||
| private DateTimeOffset _lastFlushTimestamp; | ||
|
|
||
| public HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options) | ||
| { | ||
| _options = options; | ||
| _buffers = new ConcurrentDictionary<IBufferedLogger, ConcurrentQueue<HttpRequestBufferedLogRecord>>(); | ||
| _lastFlushTimestamp = _timeProvider.GetUtcNow(); | ||
| } | ||
|
|
||
| internal HttpRequestBuffer(IOptionsMonitor<HttpRequestBufferOptions> options, TimeProvider timeProvider) | ||
| : this(options) | ||
| { | ||
| _timeProvider = timeProvider; | ||
| _lastFlushTimestamp = _timeProvider.GetUtcNow(); | ||
| } | ||
|
|
||
| public bool TryEnqueue( | ||
| IBufferedLogger logger, | ||
| LogLevel logLevel, | ||
| string category, | ||
| EventId eventId, | ||
| IReadOnlyList<KeyValuePair<string, object?>> joiner, | ||
| Exception? exception, | ||
| string formatter) | ||
| { | ||
| if (!IsEnabled(category, logLevel, eventId)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| var record = new HttpRequestBufferedLogRecord(logLevel, eventId, joiner, exception, formatter); | ||
| var queue = _buffers.GetOrAdd(logger, _ => new ConcurrentQueue<HttpRequestBufferedLogRecord>()); | ||
|
|
||
| // probably don't need to limit buffer capacity? | ||
| // because buffer is disposed when the respective HttpContext is disposed | ||
| // don't expect it to grow so much to cause a problem? | ||
| if (queue.Count >= _options.CurrentValue.PerRequestCapacity) | ||
| { | ||
| _ = queue.TryDequeue(out HttpRequestBufferedLogRecord? _); | ||
| } | ||
|
|
||
| queue.Enqueue(record); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| public void Flush() | ||
| { | ||
| foreach (var (logger, queue) in _buffers) | ||
| { | ||
| var result = new List<BufferedLogRecord>(); | ||
| while (!queue.IsEmpty) | ||
| { | ||
| if (queue.TryDequeue(out HttpRequestBufferedLogRecord? item)) | ||
| { | ||
| result.Add(item); | ||
| } | ||
| } | ||
|
|
||
| logger.LogRecords(result); | ||
| } | ||
|
|
||
| _lastFlushTimestamp = _timeProvider.GetUtcNow(); | ||
| } | ||
|
|
||
| public bool IsEnabled(string category, LogLevel logLevel, EventId eventId) | ||
| { | ||
| if (_timeProvider.GetUtcNow() < _lastFlushTimestamp + _options.CurrentValue.SuspendAfterFlushDuration) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| LoggerFilterRuleSelector.Select<BufferFilterRule>(_options.CurrentValue.Rules, category, logLevel, eventId, out BufferFilterRule? rule); | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return rule is not null; | ||
| } | ||
| } | ||
| #endif | ||
43 changes: 43 additions & 0 deletions
43
...icrosoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferConfigureOptions.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.Options; | ||
|
|
||
| namespace Microsoft.AspNetCore.Diagnostics.Logging; | ||
|
|
||
| internal sealed class HttpRequestBufferConfigureOptions : IConfigureOptions<HttpRequestBufferOptions> | ||
| { | ||
| private const string BufferingKey = "Buffering"; | ||
| private readonly IConfiguration _configuration; | ||
|
|
||
| public HttpRequestBufferConfigureOptions(IConfiguration configuration) | ||
| { | ||
| _configuration = configuration; | ||
| } | ||
|
|
||
| public void Configure(HttpRequestBufferOptions options) | ||
| { | ||
| if (_configuration == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var section = _configuration.GetSection(BufferingKey); | ||
| if (!section.Exists()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var parsedOptions = section.Get<HttpRequestBufferOptions>(); | ||
| if (parsedOptions is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| options.Rules.AddRange(parsedOptions.Rules); | ||
| } | ||
| } | ||
| #endif |
97 changes: 97 additions & 0 deletions
97
...t.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferLoggerBuilderExtensions.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using Microsoft.AspNetCore.Diagnostics.Logging; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Extensions.Logging; | ||
|
|
||
| /// <summary> | ||
| /// Lets you register log buffers in a dependency injection container. | ||
| /// </summary> | ||
| [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public static class HttpRequestBufferLoggerBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in | ||
| /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
| /// <param name="configuration">The <see cref="IConfiguration" /> to add.</param> | ||
| /// <returns>The value of <paramref name="builder"/>.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
| public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, IConfiguration configuration) | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| _ = Throw.IfNull(builder); | ||
| _ = Throw.IfNull(configuration); | ||
|
|
||
| return builder | ||
| .AddHttpRequestBufferConfiguration(configuration) | ||
| .AddHttpRequestBufferProvider(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds HTTP request-aware buffer to the logging infrastructure. Matched logs will be buffered in | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// a buffer specific to each HTTP request and can optionally be flushed and emitted during the request lifetime./>. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
| /// <param name="level">The log level (and below) to apply the buffer to.</param> | ||
| /// <param name="configure">The buffer configuration options.</param> | ||
| /// <returns>The value of <paramref name="builder"/>.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
| public static ILoggingBuilder AddHttpRequestBuffer(this ILoggingBuilder builder, LogLevel? level = null, Action<HttpRequestBufferOptions>? configure = null) | ||
| { | ||
| _ = Throw.IfNull(builder); | ||
|
|
||
| _ = builder.Services | ||
| .Configure<HttpRequestBufferOptions>(options => options.Rules.Add(new BufferFilterRule(null, level, null))) | ||
| .Configure(configure ?? new Action<HttpRequestBufferOptions>(_ => { })); | ||
|
|
||
| return builder.AddHttpRequestBufferProvider(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds HTTP request buffer provider to the logging infrastructure. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
| /// <returns>The <see cref="ILoggingBuilder"/> so that additional calls can be chained.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
| public static ILoggingBuilder AddHttpRequestBufferProvider(this ILoggingBuilder builder) | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| _ = Throw.IfNull(builder); | ||
|
|
||
| builder.Services.TryAddScoped<HttpRequestBuffer>(); | ||
| builder.Services.TryAddScoped<ILoggingBuffer>(sp => sp.GetRequiredService<HttpRequestBuffer>()); | ||
| builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | ||
| builder.Services.TryAddActivatedSingleton<ILoggingBufferProvider, HttpRequestBufferProvider>(); | ||
|
|
||
| return builder.AddGlobalBufferProvider(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures <see cref="HttpRequestBufferOptions" /> from an instance of <see cref="IConfiguration" />. | ||
| /// </summary> | ||
| /// <param name="builder">The <see cref="ILoggingBuilder" />.</param> | ||
| /// <param name="configuration">The <see cref="IConfiguration" /> to add.</param> | ||
| /// <returns>The value of <paramref name="builder"/>.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception> | ||
| internal static ILoggingBuilder AddHttpRequestBufferConfiguration(this ILoggingBuilder builder, IConfiguration configuration) | ||
| { | ||
| _ = Throw.IfNull(builder); | ||
|
|
||
| _ = builder.Services.AddSingleton<IConfigureOptions<HttpRequestBufferOptions>>(new HttpRequestBufferConfigureOptions(configuration)); | ||
|
|
||
| return builder; | ||
| } | ||
| } | ||
| #endif | ||
47 changes: 47 additions & 0 deletions
47
...braries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferOptions.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
|
|
||
| namespace Microsoft.AspNetCore.Diagnostics.Logging; | ||
|
|
||
| /// <summary> | ||
| /// The options for LoggerBuffer. | ||
| /// </summary> | ||
| [Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)] | ||
| public class HttpRequestBufferOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the time to suspend the buffer after flushing. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Use this to temporarily suspend buffering after a flush, e.g. in case of an incident you may want all logs to be emitted immediately, | ||
| /// so the buffering will be suspended for the <see paramref="SuspendAfterFlushDuration"/> time. | ||
| /// </remarks> | ||
| public TimeSpan SuspendAfterFlushDuration { get; set; } = TimeSpan.FromSeconds(30); | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the size of the buffer for a request. | ||
| /// </summary> | ||
| public int PerRequestCapacity { get; set; } = 1_000; | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// <summary> | ||
| /// Gets or sets the size of the global buffer which applies to non-request logs only. | ||
| /// </summary> | ||
| public int GlobalCapacity { get; set; } = 1_000_000; | ||
|
|
||
| #pragma warning disable CA1002 // Do not expose generic lists - List is necessary to be able to call .AddRange() | ||
| #pragma warning disable CA2227 // Collection properties should be read only - setter is necessary for options pattern | ||
| /// <summary> | ||
| /// Gets or sets the collection of <see cref="BufferFilterRule"/> used for filtering log messages for the purpose of further buffering. | ||
| /// </summary> | ||
| public List<BufferFilterRule> Rules { get; set; } = []; | ||
| #pragma warning restore CA2227 // Collection properties should be read only | ||
| #pragma warning restore CA1002 // Do not expose generic lists | ||
| } | ||
| #endif | ||
30 changes: 30 additions & 0 deletions
30
...raries/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferProvider.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System.Collections.Concurrent; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Microsoft.AspNetCore.Diagnostics.Logging; | ||
|
|
||
| internal sealed class HttpRequestBufferProvider : ILoggingBufferProvider | ||
| { | ||
| private readonly GlobalBufferProvider _globalBufferProvider; | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| private readonly IHttpContextAccessor _accessor; | ||
| private readonly ConcurrentDictionary<string, HttpRequestBuffer> _requestBuffers = new(); | ||
|
|
||
| public HttpRequestBufferProvider(GlobalBufferProvider globalBufferProvider, IHttpContextAccessor accessor) | ||
| { | ||
| _globalBufferProvider = globalBufferProvider; | ||
| _accessor = accessor; | ||
| } | ||
|
|
||
| public ILoggingBuffer CurrentBuffer => _accessor.HttpContext is null | ||
| ? _globalBufferProvider.CurrentBuffer | ||
| : _requestBuffers.GetOrAdd(_accessor.HttpContext.TraceIdentifier, _accessor.HttpContext.RequestServices.GetRequiredService<HttpRequestBuffer>()); | ||
|
|
||
| // TO DO: Dispose request buffer when the respective HttpContext is disposed | ||
evgenyfedorov2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| #endif | ||
38 changes: 38 additions & 0 deletions
38
...ies/Microsoft.AspNetCore.Diagnostics.Middleware/Buffering/HttpRequestBufferedLogRecord.cs
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| #if NET9_0_OR_GREATER | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Abstractions; | ||
|
|
||
| namespace Microsoft.AspNetCore.Diagnostics.Logging; | ||
|
|
||
| internal sealed class HttpRequestBufferedLogRecord : BufferedLogRecord | ||
| { | ||
| public HttpRequestBufferedLogRecord( | ||
| LogLevel logLevel, | ||
| EventId eventId, | ||
| IReadOnlyList<KeyValuePair<string, object?>> state, | ||
| Exception? exception, | ||
| string? formatter) | ||
| { | ||
| LogLevel = logLevel; | ||
| EventId = eventId; | ||
| Attributes = state; | ||
| Exception = exception?.ToString(); // wtf?? | ||
| FormattedMessage = formatter; | ||
| } | ||
|
|
||
| public override IReadOnlyList<KeyValuePair<string, object?>> Attributes { get; } | ||
| public override string? FormattedMessage { get; } | ||
| public override string? Exception { get; } | ||
|
|
||
| public override DateTimeOffset Timestamp { get; } | ||
|
|
||
| public override LogLevel LogLevel { get; } | ||
|
|
||
| public override EventId EventId { get; } | ||
| } | ||
| #endif |
This file contains hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.