Skip to content

Commit 0fdee54

Browse files
Extending deferred logger to forward logs to AppInsights from webhost. (#10771)
* Extending deferred logger to forward logs to AppInsights from webhost. * Added a feature flag to control the behavior.
1 parent 11ce426 commit 0fdee54

File tree

6 files changed

+86
-37
lines changed

6 files changed

+86
-37
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Fix modification of ISO strings during the parsing of function metadata bindings.
1515
- This fixes the listener errors related to the CosmosDB trigger's `StartFromTime` parameter. (#10735)
1616
- Updated `WebJobs.Script` to target .NET 8 (instead of .NET Standard 2.1)
17+
- Extending Log Forwarding to AppInsights and OTel for Enhanced Troubleshooting
1718
- Allow for binding names to use snake case (#10764). Examples include `_`, `binding_name`, and `_binding`.
1819
- Add support for the release channel setting `WEBSITE_PlatformReleaseChannel` and use this value in extension bundles resolution.
1920
- Update PowerShell 7.4 worker to [4.0.4134](https://github.com/Azure/azure-functions-powershell-worker/releases/tag/v4.0.4134)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
6868
};
6969

7070
// 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) =>
71+
_scopeProvider?.ForEachScope((scope, state) =>
7272
{
7373
state.ScopeStorage ??= new List<object>();
7474
state.ScopeStorage.Add(scope);

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

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,54 +48,61 @@ public void ProcessBufferedLogs(IReadOnlyCollection<ILoggerProvider> forwardingP
4848
await Task.Delay(_deferredLogDelay);
4949
}
5050

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-
5551
try
5652
{
5753
if (forwardingProviders is null || forwardingProviders.Count == 0)
5854
{
59-
// No providers, just drain the messages without logging
55+
// No providers, just drain the messages without logging and disable the channel.
56+
_isEnabled = false;
57+
_channel.Writer.TryComplete();
6058
while (_channel.Reader.TryRead(out _))
6159
{
6260
// Drain the channel
6361
}
62+
return;
6463
}
6564

66-
await foreach (var log in _channel.Reader.ReadAllAsync())
65+
while (await _channel.Reader.WaitToReadAsync())
6766
{
68-
foreach (var forwardingProvider in forwardingProviders)
67+
while (_channel.Reader.TryRead(out var log))
6968
{
70-
var logger = forwardingProvider.CreateLogger(log.Category);
71-
List<IDisposable> scopes = null;
72-
73-
try
69+
foreach (var forwardingProvider in forwardingProviders)
7470
{
75-
// Create a scope for each object in ScopeObject
71+
var logger = forwardingProvider.CreateLogger(log.Category);
72+
List<IDisposable> scopes = null;
73+
7674
if (log.ScopeStorage?.Count > 0)
7775
{
78-
scopes ??= new List<IDisposable>();
79-
foreach (var scope in log.ScopeStorage)
76+
try
8077
{
81-
// Create and store each scope
82-
scopes.Add(logger.BeginScope(scope));
83-
}
84-
}
78+
// Create a scope for each object in ScopeObject
79+
scopes ??= new List<IDisposable>();
80+
foreach (var scope in log.ScopeStorage)
81+
{
82+
// Create and store each scope
83+
scopes.Add(logger.BeginScope(scope));
84+
}
8585

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--)
86+
// Log the message
87+
logger.Log(log.LogLevel, log.EventId, log.Exception, log.Message);
88+
}
89+
finally
9590
{
96-
scopes[i].Dispose();
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+
}
9799
}
98100
}
101+
else
102+
{
103+
// No scopes, avoid try-finally block
104+
logger.Log(log.LogLevel, log.EventId, log.Exception, log.Message);
105+
}
99106
}
100107
}
101108
}

src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -351,14 +351,17 @@ private async Task UnsynchronizedStartHostAsync(ScriptHostStartupOperation activ
351351

352352
ActiveHost = localHost;
353353

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);
354+
if (!FeatureFlags.IsEnabled(ScriptConstants.FeatureFlagDisableWebHostLogForwarding, _environment))
355+
{
356+
// Forward logs to AppInsights/OpenTelemetry.
357+
// These are not tracked by the AppInsights and OpenTelemetry logger provider as these are added in the script host.
358+
var loggerProviders = ActiveHost.Services.GetServices<ILoggerProvider>();
359+
var deferredLogProvider = ActiveHost.Services.GetService<DeferredLoggerProvider>();
360+
if (deferredLogProvider is not null)
361+
{
362+
var selectedProviders = loggerProviders.Where(provider => provider is ApplicationInsightsLoggerProvider or OpenTelemetryLoggerProvider).ToArray();
363+
deferredLogProvider.ProcessBufferedLogs(selectedProviders);
364+
}
362365
}
363366

364367
var scriptHost = (ScriptHost)ActiveHost.Services.GetService<ScriptHost>();

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public static class ScriptConstants
130130
public const string FeatureFlagRelaxedAssemblyUnification = "RelaxedAssemblyUnification";
131131
public const string FeatureFlagEnableDiagnosticEventLogging = "EnableDiagnosticEventLogging";
132132
public const string FeatureFlagDisableDiagnosticEventLogging = "DisableDiagnosticEventLogging";
133+
public const string FeatureFlagDisableWebHostLogForwarding = "DisableWebHostLogForwarding";
133134
public const string FeatureFlagDisableMergedWebHostScriptHostConfiguration = "DisableMergedConfiguration";
134135
public const string FeatureFlagEnableWorkerIndexing = "EnableWorkerIndexing";
135136
public const string FeatureFlagEnableDebugTracing = "EnableDebugTracing";

test/WebJobs.Script.Tests/Eventing/DeferredLoggerProviderTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
77
using Microsoft.Extensions.Logging;
88
using Microsoft.Extensions.Logging.Abstractions;
9+
using Microsoft.Extensions.Options;
10+
using Microsoft.WebJobs.Script.Tests;
911
using Moq;
12+
using OpenTelemetry.Logs;
1013
using Xunit;
1114

1215
namespace Microsoft.Azure.WebJobs.Script.Tests.Eventing
@@ -139,5 +142,39 @@ public void ProcessBufferedLogs_ThrowsNoExceptionsWhenChannelIsEmpty()
139142
// Act & Assert (no exceptions should be thrown)
140143
provider.ProcessBufferedLogs(new[] { mockLoggerProvider.Object }, true);
141144
}
145+
146+
[Fact]
147+
public async Task Dispose_DoNotDisablesProviderAndCompletesChannel()
148+
{
149+
var testEnvironment = new TestEnvironment();
150+
testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "0");
151+
152+
// Arrange
153+
var provider = new DeferredLoggerProvider(testEnvironment);
154+
var logger = provider.CreateLogger("TestCategory");
155+
logger.LogError("Error Log");
156+
157+
// Create an instance of IOptionsMonitor<OpenTelemetryLoggerOptions>
158+
var optionsMonitor = Mock.Of<IOptionsMonitor<OpenTelemetryLoggerOptions>>();
159+
160+
// Pass the optionsMonitor to the OpenTelemetryLoggerProvider constructor
161+
//OpenTelemetryLoggerProvider openTelemetryLoggerProvider = new(optionsMonitor);
162+
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
163+
testLoggerProvider.SetScopeProvider(new LoggerExternalScopeProvider());
164+
165+
// Act
166+
provider.ProcessBufferedLogs(new List<ILoggerProvider>() { testLoggerProvider }, true); // Process immediately
167+
168+
// Wait a short period to ensure the channel is completed
169+
await Task.Delay(100);
170+
Assert.Equal(0, provider.Count); // Ensure channel is drained
171+
172+
// Assert that the log was forwarded to the testLoggerProvider
173+
Assert.Equal(1, testLoggerProvider.GetAllLogMessages().Count);
174+
Assert.Equal(Assert.Single(testLoggerProvider.GetAllLogMessages()).FormattedMessage, "Error Log");
175+
176+
// Ensure that the LoggerProvider is not disabled and still returns DeferredLogger
177+
Assert.True(provider.CreateLogger("TestCategory") is DeferredLogger);
178+
}
142179
}
143180
}

0 commit comments

Comments
 (0)