Skip to content

Commit 46b1a61

Browse files
committed
Add keyed ILogger which forwards to ScriptHost when possible
Add mechanism to forward logs from WebHost to ScriptHost
1 parent 47bc720 commit 46b1a61

File tree

14 files changed

+577
-10
lines changed

14 files changed

+577
-10
lines changed
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>();
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 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 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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 Microsoft.Azure.WebJobs.Script;
7+
8+
#nullable enable
9+
10+
namespace Microsoft.Extensions.Logging
11+
{
12+
/// <summary>
13+
/// A logger factory that creates loggers which track the current active ScriptHost (if any), falling
14+
/// back to the WebHost logger if no ScriptHost is active.
15+
/// </summary>
16+
[DebuggerDisplay(@"InnerFactory = \{ {_inner} \}, ScriptHostState = {_manager.State}")]
17+
public sealed class ForwardingLoggerFactory : ILoggerFactory
18+
{
19+
private readonly ILoggerFactory _inner;
20+
private readonly IScriptHostManager _manager;
21+
22+
public ForwardingLoggerFactory(ILoggerFactory inner, IScriptHostManager manager)
23+
{
24+
ArgumentNullException.ThrowIfNull(inner);
25+
ArgumentNullException.ThrowIfNull(manager);
26+
_inner = inner;
27+
_manager = manager;
28+
}
29+
30+
/// <inheritdoc />
31+
public void AddProvider(ILoggerProvider provider)
32+
=> throw new NotSupportedException(
33+
$"{nameof(ILoggerProvider)} can not be added to the {nameof(ForwardingLoggerFactory)}.");
34+
35+
/// <inheritdoc />
36+
public ILogger CreateLogger(string categoryName)
37+
=> new ForwardingLogger(categoryName, _inner.CreateLogger(categoryName), _manager);
38+
39+
/// <inheritdoc />
40+
public void Dispose()
41+
{
42+
// no op.
43+
}
44+
}
45+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public partial class TelemetryHealthCheckPublisher : IHealthCheckPublisher
2121
public TelemetryHealthCheckPublisher(
2222
HealthCheckMetrics metrics,
2323
TelemetryHealthCheckPublisherOptions options,
24-
ILogger<TelemetryHealthCheckPublisher> logger)
24+
[ForwardingLogger] ILogger<TelemetryHealthCheckPublisher> logger)
2525
{
2626
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
2727
_options = options ?? throw new ArgumentNullException(nameof(options));

src/WebJobs.Script/Extensions/ScriptLoggingBuilderExtensions.cs

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

4+
using System;
45
using System.Collections.Concurrent;
56
using System.Linq;
67
using Microsoft.AspNetCore.Hosting;
78
using Microsoft.Azure.WebJobs.Script;
89
using Microsoft.Azure.WebJobs.Script.Configuration;
910
using Microsoft.Azure.WebJobs.Script.Workers;
1011
using Microsoft.Extensions.Configuration;
12+
using Microsoft.Extensions.DependencyInjection;
1113
using Microsoft.Extensions.Hosting;
1214

1315
namespace Microsoft.Extensions.Logging
1416
{
1517
public static class ScriptLoggingBuilderExtensions
1618
{
17-
private static ConcurrentDictionary<string, bool> _filteredCategoryCache = new ConcurrentDictionary<string, bool>();
19+
private static readonly ConcurrentDictionary<string, bool> _filteredCategoryCache = new();
20+
21+
public static ILoggingBuilder AddForwardingLogger(this ILoggingBuilder builder)
22+
{
23+
ArgumentNullException.ThrowIfNull(builder);
24+
builder.Services.AddKeyedSingleton<ILoggerFactory, ForwardingLoggerFactory>(
25+
ForwardingLogger.ServiceKey);
26+
builder.Services.AddKeyedSingleton(
27+
typeof(ILogger<>), ForwardingLogger.ServiceKey, typeof(ForwardingLogger<>));
28+
return builder;
29+
}
1830

1931
public static ILoggingBuilder AddDefaultWebJobsFilters(this ILoggingBuilder builder)
2032
{
@@ -23,7 +35,8 @@ public static ILoggingBuilder AddDefaultWebJobsFilters(this ILoggingBuilder buil
2335
return builder;
2436
}
2537

26-
public static ILoggingBuilder AddDefaultWebJobsFilters<T>(this ILoggingBuilder builder, LogLevel level) where T : ILoggerProvider
38+
public static ILoggingBuilder AddDefaultWebJobsFilters<T>(this ILoggingBuilder builder, LogLevel level)
39+
where T : ILoggerProvider
2740
{
2841
builder.AddFilter<T>(null, LogLevel.None);
2942
builder.AddFilter<T>((c, l) => Filter(c, l, level));
@@ -37,20 +50,24 @@ internal static bool Filter(string category, LogLevel actualLevel, LogLevel minL
3750

3851
private static bool IsFiltered(string category)
3952
{
40-
return _filteredCategoryCache.GetOrAdd(category, static cat => ScriptConstants.SystemLogCategoryPrefixes.Any(p => cat.StartsWith(p)));
53+
return _filteredCategoryCache.GetOrAdd(
54+
category,
55+
static cat => ScriptConstants.SystemLogCategoryPrefixes.Any(p => cat.StartsWith(p)));
4156
}
4257

4358
public static void AddConsoleIfEnabled(this ILoggingBuilder builder, HostBuilderContext context)
4459
{
4560
builder.AddConsoleIfEnabled(context.HostingEnvironment.IsDevelopment(), context.Configuration);
4661
}
4762

48-
private static void AddConsoleIfEnabled(this ILoggingBuilder builder, bool isDevelopment, IConfiguration configuration)
63+
private static void AddConsoleIfEnabled(
64+
this ILoggingBuilder builder, bool isDevelopment, IConfiguration configuration)
4965
{
5066
// console logging defaults to false, except for self host
5167
bool enableConsole = isDevelopment;
5268

53-
string consolePath = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, "Logging", "Console", "IsEnabled");
69+
string consolePath = ConfigurationPath.Combine(
70+
ConfigurationSectionNames.JobHost, "Logging", "Console", "IsEnabled");
5471
IConfigurationSection configSection = configuration.GetSection(consolePath);
5572

5673
if (configSection.Exists())

src/WebJobs.Script/Host/IScriptHostManager.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Threading;
66
using System.Threading.Tasks;
77

8+
#nullable enable
9+
810
namespace Microsoft.Azure.WebJobs.Script
911
{
1012
public interface IScriptHostManager
@@ -24,7 +26,12 @@ public interface IScriptHostManager
2426
/// <summary>
2527
/// Gets the last host <see cref="Exception"/> that has occurred.
2628
/// </summary>
27-
Exception LastError { get; }
29+
Exception? LastError { get; }
30+
31+
/// <summary>
32+
/// Gets the current <see cref="IServiceProvider"/> for the active Script Host.
33+
/// </summary>
34+
IServiceProvider? Services { get; }
2835

2936
/// <summary>
3037
/// Restarts the current Script Job Host.

test/WebJobs.Script.Tests.Shared/TestHelpers.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,8 @@ event EventHandler IScriptHostManager.HostInitializing
654654

655655
Exception IScriptHostManager.LastError => throw new NotImplementedException();
656656

657+
IServiceProvider IScriptHostManager.Services => this;
658+
657659
public void OnActiveHostChanged()
658660
{
659661
ActiveHostChanged?.Invoke(this, new ActiveHostChangedEventArgs(null, null));
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 AwesomeAssertions;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using Xunit;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics
10+
{
11+
public class ForwardingLoggerAttributeTests
12+
{
13+
[Fact]
14+
public void Key_IsCorrect()
15+
{
16+
ForwardingLoggerAttribute attribute = new();
17+
18+
attribute.Key.Should().BeOfType<string>()
19+
.Which.Should().NotBeNullOrWhiteSpace()
20+
.And.Be(ForwardingLogger.ServiceKey);
21+
}
22+
23+
[Fact]
24+
public void Import_GetsService()
25+
{
26+
object nonKeyed = new();
27+
object keyed = new();
28+
29+
ServiceCollection services = new();
30+
services.AddSingleton(nonKeyed);
31+
services.AddKeyedSingleton(ForwardingLogger.ServiceKey, keyed);
32+
services.AddSingleton<TestClass>();
33+
34+
TestClass test = services.BuildServiceProvider().GetRequiredService<TestClass>();
35+
36+
test.Instance.Should().BeSameAs(keyed);
37+
}
38+
39+
private class TestClass([ForwardingLogger] object instance)
40+
{
41+
public object Instance => instance;
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)