Skip to content

Commit 2a1684b

Browse files
authored
Add keyed ILogger which forwards to ScriptHost when possible (#11398)
* Add keyed ILogger which forwards to ScriptHost when possible Add mechanism to forward logs from WebHost to ScriptHost * Suppress warning in test * Add caching to ForwardingLoggerFactory * Skip copying ILoggerFactory * Update release_notes.md * Rearrange some serivce setup. Fix tests * Move dependency setup back to AddTelemetryPublisher * Address PR feedback * Fix merge/build issues * Fix warning
1 parent bacde52 commit 2a1684b

18 files changed

+615
-15
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Emit diagnostic warning for deprecated Azure Functions Proxies usage (#11405)
77
- Update Python Worker Version to [4.40.2](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.40.2)
88
- Add JitTrace Files for v4.1044
9+
- Send `TelemetryHealthCheckPublisher` logs to ScriptHost `ILogger` (#11398)
910
- Implementing a resolver that resolves worker configurations from specified probing paths (#11258)
1011
- Remove duplicate function names from sync triggers payload(#11371)
1112
- Avoid emitting empty tag values for health check metrics (#11393)

src/WebJobs.Script.WebHost/DependencyInjection/ServiceProviderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public static class ServiceProviderExtensions
2727
typeof(IManagedHostedService), // These shouldn't be instantiated twice
2828
typeof(IHostedService), // These shouldn't be instantiated twice
2929
typeof(ILoggerProvider), // These shouldn't be instantiated twice
30+
typeof(ILoggerFactory), // WebHost has a keyed implementation which will fail propagation. ScriptHost registers its own anyways.
31+
typeof(ILogger<>), // Same reason as ILoggerFactory.
3032
typeof(HealthCheckService), // Child container should instantiate its own.
3133
typeof(IConfigureOptions<HealthCheckServiceOptions>), // Child container should instantiate its own.
3234
typeof(IPostConfigureOptions<HealthCheckServiceOptions>), // Child container should instantiate its own.
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
55
using Microsoft.Extensions.DependencyInjection;
66

7+
#nullable enable
8+
79
namespace Microsoft.Extensions.Logging
810
{
911
public static class ILoggingBuilderExtensions
1012
{
11-
public static void AddWebJobsSystem<T>(this ILoggingBuilder builder) where T : SystemLoggerProvider
13+
public static ILoggingBuilder AddWebJobsSystem<T>(this ILoggingBuilder builder)
14+
where T : SystemLoggerProvider
1215
{
1316
builder.Services.AddSingleton<ILoggerProvider, T>();
1417

1518
// Log all logs to SystemLogger
1619
builder.AddDefaultWebJobsFilters<T>(LogLevel.Trace);
20+
return builder;
1721
}
1822
}
1923
}

src/WebJobs.Script.WebHost/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
@@ -88,6 +88,7 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args = null)
8888

8989
loggingBuilder.AddDefaultWebJobsFilters();
9090
loggingBuilder.AddWebJobsSystem<WebHostSystemLoggerProvider>();
91+
loggingBuilder.AddForwardingLogger();
9192
loggingBuilder.Services.AddSingleton<DeferredLoggerProvider>();
9293
loggingBuilder.Services.AddSingleton<ILoggerProvider>(s => s.GetRequiredService<DeferredLoggerProvider>());
9394
loggingBuilder.Services.AddSingleton<ISystemLoggerFactory, SystemLoggerFactory>();

src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi
251251
services.AddAzureStorageProviders();
252252

253253
// Add health checks
254+
services.AddMetrics();
254255
services.AddHealthChecks().AddWebJobsScriptHealthChecks();
255256
}
256257

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
using Microsoft.Azure.WebJobs.Script;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
#nullable enable
11+
12+
namespace Microsoft.Extensions.Logging
13+
{
14+
internal sealed class ForwardingLogger : ILogger
15+
{
16+
// The service key to use for dependency injection to get forwarding loggers.
17+
public const string ServiceKey = "Forwarding";
18+
19+
private readonly string _categoryName;
20+
private readonly ILogger _fallback;
21+
private readonly IScriptHostManager _manager;
22+
23+
// We use weak references so as to not keep a ScriptHost alive after it shuts down.
24+
private readonly WeakReference<ILogger> _current = new(null!);
25+
private readonly WeakReference<IServiceProvider> _services = new(null!);
26+
27+
public ForwardingLogger(string categoryName, ILogger inner, IScriptHostManager manager)
28+
{
29+
ArgumentNullException.ThrowIfNull(inner);
30+
ArgumentNullException.ThrowIfNull(manager);
31+
_categoryName = categoryName;
32+
_fallback = inner;
33+
_manager = manager;
34+
}
35+
36+
private ILogger Current
37+
{
38+
get
39+
{
40+
if (TryGetCurrentLogger(out ILogger? logger))
41+
{
42+
return logger;
43+
}
44+
45+
// No current ScriptHost logger, or the ScriptHost is gone. Use the fallback WebHost logger.
46+
return _fallback;
47+
}
48+
}
49+
50+
public IDisposable? BeginScope<TState>(TState state)
51+
where TState : notnull
52+
=> Current.BeginScope(state);
53+
54+
public bool IsEnabled(LogLevel logLevel) => Current.IsEnabled(logLevel);
55+
56+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
57+
=> Current.Log(logLevel, eventId, state, exception, formatter);
58+
59+
private bool TryGetCurrentLogger([NotNullWhen(true)] out ILogger? logger)
60+
{
61+
if (IsLoggerCurrent(out logger))
62+
{
63+
return true;
64+
}
65+
else if (_manager.Services is { } services)
66+
{
67+
logger = services.GetRequiredService<ILoggerFactory>().CreateLogger(_categoryName);
68+
_services.SetTarget(services);
69+
_current.SetTarget(logger);
70+
return true;
71+
}
72+
73+
logger = null;
74+
return false;
75+
}
76+
77+
private bool IsLoggerCurrent([NotNullWhen(true)] out ILogger? logger)
78+
{
79+
// First check if the last IServiceProvider we used is still active.
80+
if (_services.TryGetTarget(out IServiceProvider? services)
81+
&& ReferenceEquals(services, _manager.Services))
82+
{
83+
// Service provider is still correct, so our logger is current.
84+
return _current.TryGetTarget(out logger);
85+
}
86+
87+
logger = null;
88+
return false;
89+
}
90+
}
91+
92+
[DebuggerDisplay("{_logger}")]
93+
internal sealed class ForwardingLogger<T> : ILogger<T>
94+
{
95+
private readonly ILogger _logger;
96+
97+
public ForwardingLogger([ForwardingLogger] ILoggerFactory factory)
98+
{
99+
ArgumentNullException.ThrowIfNull(factory);
100+
_logger = factory.CreateLogger<T>();
101+
}
102+
103+
IDisposable? ILogger.BeginScope<TState>(TState state) => _logger.BeginScope(state);
104+
105+
bool ILogger.IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel);
106+
107+
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) =>
108+
_logger.Log(logLevel, eventId, state, exception, formatter);
109+
}
110+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 Microsoft.Extensions.DependencyInjection;
6+
7+
namespace Microsoft.Extensions.Logging
8+
{
9+
[AttributeUsage(AttributeTargets.Parameter)]
10+
internal class ForwardingLoggerAttribute()
11+
: FromKeyedServicesAttribute(ForwardingLogger.ServiceKey)
12+
{
13+
}
14+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.Concurrent;
6+
using System.Diagnostics;
7+
using Microsoft.Azure.WebJobs.Script;
8+
9+
#nullable enable
10+
11+
namespace Microsoft.Extensions.Logging
12+
{
13+
/// <summary>
14+
/// A logger factory that creates loggers which track the current active ScriptHost (if any), falling
15+
/// back to the WebHost logger if no ScriptHost is active.
16+
/// </summary>
17+
[DebuggerDisplay(@"InnerFactory = {_inner}, ScriptHostState = {_manager.State}")]
18+
public sealed class ForwardingLoggerFactory : ILoggerFactory
19+
{
20+
private readonly ConcurrentDictionary<string, ForwardingLogger> _loggers = new(StringComparer.Ordinal);
21+
private readonly ILoggerFactory _inner;
22+
private readonly IScriptHostManager _manager;
23+
24+
private bool _disposed;
25+
26+
public ForwardingLoggerFactory(ILoggerFactory inner, IScriptHostManager manager)
27+
{
28+
ArgumentNullException.ThrowIfNull(inner);
29+
ArgumentNullException.ThrowIfNull(manager);
30+
_inner = inner;
31+
_manager = manager;
32+
}
33+
34+
/// <inheritdoc />
35+
public void AddProvider(ILoggerProvider provider)
36+
=> throw new NotSupportedException(
37+
$"{nameof(ILoggerProvider)} can not be added to the {nameof(ForwardingLoggerFactory)}.");
38+
39+
/// <inheritdoc />
40+
public ILogger CreateLogger(string categoryName)
41+
{
42+
ObjectDisposedException.ThrowIf(_disposed, this);
43+
44+
ForwardingLogger CreateLoggerImpl(string categoryName)
45+
{
46+
ILogger innerLogger = _inner.CreateLogger(categoryName);
47+
return new ForwardingLogger(categoryName, innerLogger, _manager);
48+
}
49+
50+
return _loggers.GetOrAdd(categoryName, CreateLoggerImpl);
51+
}
52+
53+
/// <inheritdoc />
54+
public void Dispose()
55+
{
56+
// this is just to block further logger creation.
57+
_disposed = true;
58+
}
59+
}
60+
}

src/WebJobs.Script/Diagnostics/HealthChecks/DynamicHealthCheckService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private HealthReport MergeReports(HealthReport left, HealthReport? right)
9292
Func<HealthCheckRegistration, bool>? predicate,
9393
CancellationToken cancellationToken = default)
9494
{
95-
HealthCheckService? scriptHostCheck = _manager.Services.GetService<HealthCheckService>();
95+
HealthCheckService? scriptHostCheck = _manager.Services?.GetService<HealthCheckService>();
9696
if (scriptHostCheck is null)
9797
{
9898
Log.ScriptHostNoHealthCheckService(_logger);

src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using Microsoft.Extensions.DependencyInjection;
88
using Microsoft.Extensions.Diagnostics.HealthChecks;
9+
using Microsoft.Extensions.Logging;
910

1011
namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
1112
{
@@ -82,8 +83,8 @@ static void RegisterPublisher(IServiceCollection services, string tag)
8283
});
8384
}
8485

85-
builder.Services.AddLogging();
8686
builder.Services.AddMetrics();
87+
builder.Services.AddLogging(b => b.AddForwardingLogger());
8788
builder.Services.AddSingleton<HealthCheckMetrics>();
8889
RegisterPublisher(builder.Services, null); // always register the default publisher
8990

0 commit comments

Comments
 (0)