Skip to content

Commit 2d36bbe

Browse files
Deferred Log Dispatcher (#10530)
* Deferred Log Dispatcher
1 parent 3510835 commit 2d36bbe

File tree

10 files changed

+390
-6
lines changed

10 files changed

+390
-6
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Improving console log handling during specialization (#10345)
99
- Update Node.js Worker Version to [3.10.1](https://github.com/Azure/azure-functions-nodejs-worker/releases/tag/v3.10.1)
1010
- Remove packages `Microsoft.Azure.Cosmos.Table` and `Microsoft.Azure.DocumentDB.Core` (#10503)
11+
- Buffering startup logs and forwarding to ApplicationInsights/OpenTelemetry after logger providers are added to the logging system (#10530)
1112
- Implement host configuration property to allow configuration of the metadata provider timeout period (#10526)
1213
- The value can be set via `metadataProviderTimeout` in host.json and defaults to "00:00:30" (30 seconds).
1314
- For logic apps, unless configured via the host.json, the timeout is disabled by default.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics
9+
{
10+
public struct DeferredLogEntry
11+
{
12+
public LogLevel LogLevel { get; set; }
13+
14+
public string Category { get; set; }
15+
16+
public string Message { get; set; }
17+
18+
public Exception Exception { get; set; }
19+
20+
public EventId EventId { get; set; }
21+
22+
public List<object> ScopeStorage { get; set; }
23+
}
24+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Channels;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics
10+
{
11+
/// <summary>
12+
/// A logger that defers log entries to a channel.
13+
/// </summary>
14+
public class DeferredLogger : ILogger
15+
{
16+
private readonly Channel<DeferredLogEntry> _channel;
17+
private readonly string _categoryName;
18+
private readonly IExternalScopeProvider _scopeProvider;
19+
private readonly IEnvironment _environment;
20+
21+
// Cached placeholder mode flag
22+
private bool _isPlaceholderModeDisabled = false;
23+
24+
public DeferredLogger(Channel<DeferredLogEntry> channel, string categoryName, IExternalScopeProvider scopeProvider, IEnvironment environment)
25+
{
26+
_channel = channel;
27+
_categoryName = categoryName;
28+
_scopeProvider = scopeProvider;
29+
_environment = environment;
30+
}
31+
32+
public IDisposable BeginScope<TState>(TState state) => _scopeProvider.Push(state);
33+
34+
// Restrict logging to errors only for now, as we are seeing a lot of unnecessary logs.
35+
// https://github.com/Azure/azure-functions-host/issues/10556
36+
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Error;
37+
38+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
39+
{
40+
// Skip logging if it's not enabled or placeholder mode is enabled
41+
if (!IsEnabled(logLevel))
42+
{
43+
return;
44+
}
45+
46+
// Only check IsPlaceholderModeEnabled if it hasn't been disabled
47+
if (!_isPlaceholderModeDisabled && _environment.IsPlaceholderModeEnabled())
48+
{
49+
return;
50+
}
51+
52+
// Cache the fact that placeholder mode is disabled
53+
_isPlaceholderModeDisabled = true;
54+
55+
string formattedMessage = formatter?.Invoke(state, exception);
56+
if (string.IsNullOrEmpty(formattedMessage))
57+
{
58+
return;
59+
}
60+
61+
var log = new DeferredLogEntry
62+
{
63+
LogLevel = logLevel,
64+
Category = _categoryName,
65+
Message = formattedMessage,
66+
Exception = exception,
67+
EventId = eventId
68+
};
69+
70+
// Persist the scope state so it can be reapplied in the original order when forwarding logs to the logging provider.
71+
_scopeProvider.ForEachScope((scope, state) =>
72+
{
73+
state.ScopeStorage ??= new List<object>();
74+
state.ScopeStorage.Add(scope);
75+
}, log);
76+
77+
_channel.Writer.TryWrite(log);
78+
}
79+
}
80+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Channels;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Logging.Abstractions;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics
12+
{
13+
public sealed class DeferredLoggerProvider : ILoggerProvider, ISupportExternalScope
14+
{
15+
private readonly Channel<DeferredLogEntry> _channel = Channel.CreateBounded<DeferredLogEntry>(new BoundedChannelOptions(150)
16+
{
17+
FullMode = BoundedChannelFullMode.DropOldest,
18+
// Avoids locks and interlocked operations when reading from the channel.
19+
SingleReader = true,
20+
SingleWriter = false
21+
});
22+
23+
private readonly TimeSpan _deferredLogDelay = TimeSpan.FromSeconds(10);
24+
private readonly IEnvironment _environment;
25+
private IExternalScopeProvider _scopeProvider;
26+
private bool _isEnabled = true;
27+
28+
public DeferredLoggerProvider(IEnvironment environment)
29+
{
30+
_environment = environment;
31+
}
32+
33+
public int Count => _channel.Reader.Count;
34+
35+
public ILogger CreateLogger(string categoryName)
36+
{
37+
return _isEnabled ? new DeferredLogger(_channel, categoryName, _scopeProvider, _environment) : NullLogger.Instance;
38+
}
39+
40+
public void ProcessBufferedLogs(IReadOnlyCollection<ILoggerProvider> forwardingProviders, bool runImmediately = false)
41+
{
42+
// Forward all buffered logs to the new provider
43+
Task.Run(async () =>
44+
{
45+
if (!runImmediately)
46+
{
47+
// Wait for 10 seconds, this will increase the probability of these logs appearing in live metrics.
48+
await Task.Delay(_deferredLogDelay);
49+
}
50+
51+
// Disable the channel and let the consumer know that there won't be any more logs.
52+
_isEnabled = false;
53+
_channel.Writer.TryComplete();
54+
55+
try
56+
{
57+
if (forwardingProviders is null || forwardingProviders.Count == 0)
58+
{
59+
// No providers, just drain the messages without logging
60+
while (_channel.Reader.TryRead(out _))
61+
{
62+
// Drain the channel
63+
}
64+
}
65+
66+
await foreach (var log in _channel.Reader.ReadAllAsync())
67+
{
68+
foreach (var forwardingProvider in forwardingProviders)
69+
{
70+
var logger = forwardingProvider.CreateLogger(log.Category);
71+
List<IDisposable> scopes = null;
72+
73+
try
74+
{
75+
// Create a scope for each object in ScopeObject
76+
if (log.ScopeStorage?.Count > 0)
77+
{
78+
scopes ??= new List<IDisposable>();
79+
foreach (var scope in log.ScopeStorage)
80+
{
81+
// Create and store each scope
82+
scopes.Add(logger.BeginScope(scope));
83+
}
84+
}
85+
86+
// Log the message
87+
logger.Log(log.LogLevel, log.EventId, log.Exception, log.Message);
88+
}
89+
finally
90+
{
91+
if (scopes is not null)
92+
{
93+
// Dispose all scopes in reverse order to properly unwind them
94+
for (int i = scopes.Count - 1; i >= 0; i--)
95+
{
96+
scopes[i].Dispose();
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
103+
catch (Exception ex)
104+
{
105+
if (ex.IsFatal())
106+
{
107+
throw;
108+
}
109+
}
110+
});
111+
}
112+
113+
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
114+
{
115+
_scopeProvider = scopeProvider;
116+
}
117+
118+
public void Dispose()
119+
{
120+
_isEnabled = false;
121+
_channel.Writer.TryComplete();
122+
}
123+
}
124+
}

src/WebJobs.Script.WebHost/Diagnostics/SystemLogger.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using Microsoft.AspNetCore.Http;
76
using Microsoft.Azure.WebJobs.Host;
87
using Microsoft.Azure.WebJobs.Host.Executors.Internal;
98
using Microsoft.Azure.WebJobs.Host.Indexers;
10-
using Microsoft.Azure.WebJobs.Host.Listeners;
119
using Microsoft.Azure.WebJobs.Logging;
1210
using Microsoft.Azure.WebJobs.Script.Configuration;
1311
using Microsoft.Azure.WebJobs.Script.Eventing;
14-
using Microsoft.Azure.WebJobs.Script.Extensions;
1512
using Microsoft.Extensions.Logging;
1613
using Microsoft.Extensions.Options;
1714

@@ -31,7 +28,7 @@ public class SystemLogger : ILogger
3128
private readonly IExternalScopeProvider _scopeProvider;
3229
private AppServiceOptions _appServiceOptions;
3330

34-
public SystemLogger(string hostInstanceId, string categoryName, IEventGenerator eventGenerator, IEnvironment environment, IDebugStateProvider debugStateProvider,
31+
public SystemLogger(string hostInstanceId, string categoryName, IEventGenerator eventGenerator, IEnvironment environment, IDebugStateProvider debugStateProvider,
3532
IScriptEventManager eventManager, IExternalScopeProvider scopeProvider, IOptionsMonitor<AppServiceOptions> appServiceOptionsMonitor)
3633
{
3734
_environment = environment;
@@ -181,7 +178,6 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
181178
formattedMessage = Sanitizer.Sanitize(formattedMessage);
182179
innerExceptionMessage = innerExceptionMessage ?? string.Empty;
183180
}
184-
185181
_eventGenerator.LogFunctionTraceEvent(logLevel, subscriptionId, appName, functionName, eventName, source, details, summary, innerExceptionType, innerExceptionMessage, invocationId, _hostInstanceId, activityId, runtimeSiteName, slotName, DateTime.UtcNow);
186182
}
187183
}

src/WebJobs.Script.WebHost/Diagnostics/SystemLoggerProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ public void SetScopeProvider(IExternalScopeProvider scopeProvider)
6161
_scopeProvider = scopeProvider;
6262
}
6363
}
64-
}
64+
}

src/WebJobs.Script.WebHost/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args = null)
8888

8989
loggingBuilder.AddDefaultWebJobsFilters();
9090
loggingBuilder.AddWebJobsSystem<WebHostSystemLoggerProvider>();
91+
loggingBuilder.Services.AddSingleton<DeferredLoggerProvider>();
92+
loggingBuilder.Services.AddSingleton<ILoggerProvider>(s => s.GetRequiredService<DeferredLoggerProvider>());
9193
})
9294
.UseStartup<Startup>();
9395
}

src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Collections.ObjectModel;
67
using System.IO;
78
using System.Linq;
@@ -14,12 +15,14 @@
1415
using Microsoft.ApplicationInsights.Extensibility.Implementation.ApplicationId;
1516
using Microsoft.Azure.WebJobs.Host;
1617
using Microsoft.Azure.WebJobs.Logging;
18+
using Microsoft.Azure.WebJobs.Logging.ApplicationInsights;
1719
using Microsoft.Azure.WebJobs.Script.Config;
1820
using Microsoft.Azure.WebJobs.Script.Configuration;
1921
using Microsoft.Azure.WebJobs.Script.Diagnostics;
2022
using Microsoft.Azure.WebJobs.Script.Eventing;
2123
using Microsoft.Azure.WebJobs.Script.Metrics;
2224
using Microsoft.Azure.WebJobs.Script.Scale;
25+
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
2326
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions;
2427
using Microsoft.Azure.WebJobs.Script.Workers;
2528
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
@@ -348,6 +351,16 @@ private async Task UnsynchronizedStartHostAsync(ScriptHostStartupOperation activ
348351

349352
ActiveHost = localHost;
350353

354+
// Forward initial startup logs to AppInsights/OpenTelemetry.
355+
// These are not tracked by the AppInsights and OpenTelemetry logger provider as these are added in the script host.
356+
var loggerProviders = ActiveHost.Services.GetServices<ILoggerProvider>();
357+
var deferredLogProvider = ActiveHost.Services.GetService<DeferredLoggerProvider>();
358+
if (deferredLogProvider is not null)
359+
{
360+
var selectedProviders = loggerProviders.Where(provider => provider is ApplicationInsightsLoggerProvider or OpenTelemetryLoggerProvider).ToArray();
361+
deferredLogProvider.ProcessBufferedLogs(selectedProviders);
362+
}
363+
351364
var scriptHost = (ScriptHost)ActiveHost.Services.GetService<ScriptHost>();
352365
if (scriptHost != null)
353366
{

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
9494
<PackageReference Include="System.Text.Json" Version="8.0.5" />
9595
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
96+
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
9697
</ItemGroup>
9798

9899
<ItemGroup>

0 commit comments

Comments
 (0)