diff --git a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/ApplicationInsightsLogger.cs b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/ApplicationInsightsLogger.cs index ff01d7bad..df68d4566 100644 --- a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/ApplicationInsightsLogger.cs +++ b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/ApplicationInsightsLogger.cs @@ -9,9 +9,11 @@ using System.Linq; using System.Text; using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Logging.ApplicationInsights.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -73,50 +75,41 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, // Initialize stateValues so the rest of the methods don't have to worry about null values. stateValues = stateValues ?? new Dictionary(); - // Add some well-known properties to the scope dictionary so the TelemetryInitializer can add them - // for all telemetry. - using (BeginScope(new Dictionary + if (_isUserFunction && eventId.Id == LogConstants.MetricEventId) { - [LogConstants.CategoryNameKey] = _categoryName, - [LogConstants.LogLevelKey] = (LogLevel?)logLevel, - [LogConstants.EventIdKey] = eventId.Id, - [LogConstants.EventNameKey] = eventId.Name, - })) + // Log a metric from user logs only + LogMetric(stateValues, logLevel, eventId); + } + else if (_categoryName == LogCategories.Results) { - if (_isUserFunction && eventId.Id == LogConstants.MetricEventId) - { - // Log a metric from user logs only - LogMetric(stateValues); - } - else if (_categoryName == LogCategories.Results) - { - // Log a function result - LogFunctionResult(stateValues, logLevel, exception); - } - else if (_categoryName == LogCategories.Aggregator) - { - // Log an aggregate record - LogFunctionResultAggregate(stateValues); - } - else if (exception != null) - { - // Log an exception - LogException(logLevel, stateValues, exception, formattedMessage); - } - else - { - // Otherwise, log a trace - LogTrace(logLevel, stateValues, formattedMessage); - } + // Log a function result + LogFunctionResult(stateValues, logLevel, exception, eventId); + } + else if (_categoryName == LogCategories.Aggregator) + { + // Log an aggregate record + LogFunctionResultAggregate(stateValues, logLevel, eventId); + } + else if (exception != null) + { + // Log an exception + LogException(logLevel, stateValues, exception, formattedMessage, eventId); + } + else + { + // Otherwise, log a trace + LogTrace(logLevel, stateValues, formattedMessage, eventId); } } - private void LogMetric(IEnumerable> values) + private void LogMetric(IEnumerable> values, LogLevel logLevel, EventId eventId) { MetricTelemetry telemetry = new MetricTelemetry(); - // Always apply scope first to allow state to override. - ApplyScopeProperties(telemetry.Properties); + ApplyScopeProperties(telemetry); + + // Add known properties like category, logLevel and event + ApplyKnownProperties(telemetry.Properties, logLevel, eventId); foreach (var entry in values) { @@ -129,11 +122,11 @@ private void LogMetric(IEnumerable> values) // next entry if found. switch (entry.Key) { - case LogConstants.NameKey: - telemetry.Name = entry.Value.ToString(); + case LogConstants.NameKey when entry.Value is string name: + telemetry.Name = name; continue; - case LogConstants.MetricValueKey: - telemetry.Sum = (double)entry.Value; + case LogConstants.MetricValueKey when entry.Value is double sum: + telemetry.Sum = sum; continue; default: break; @@ -142,8 +135,8 @@ private void LogMetric(IEnumerable> values) // Now check for case-insensitive matches switch (entry.Key.ToLowerInvariant()) { - case MetricCountKey: - telemetry.Count = Convert.ToInt32(entry.Value); + case MetricCountKey when entry.Value is int count: + telemetry.Count = count; break; case MetricMinKey: telemetry.Min = Convert.ToDouble(entry.Value); @@ -165,17 +158,28 @@ private void LogMetric(IEnumerable> values) } // Applies scope properties; filters most system properties, which are used internally - private static void ApplyScopeProperties(IDictionary properties) + private static void ApplyScopeProperties(ITelemetry telemetry) { var scopeProperties = DictionaryLoggerScope.GetMergedStateDictionaryOrNull(); if (scopeProperties != null) { - var customScopeProperties = scopeProperties.Where(p => !SystemScopeKeys.Contains(p.Key, StringComparer.Ordinal)); - ApplyProperties(properties, customScopeProperties, true); - } + foreach (var scopeProperty in scopeProperties) + { + if (scopeProperty.Value != null && !SystemScopeKeys.Contains(scopeProperty.Key, StringComparer.Ordinal)) + { + ApplyProperty(telemetry.Context.Properties, scopeProperty.Key, scopeProperty.Value, true); + } + } + + if (scopeProperties.GetValueOrDefault(ScopeKeys.FunctionInvocationId) is string invocationId) + { + telemetry.Context.Properties[LogConstants.InvocationIdKey] = invocationId; + } + } + telemetry.Context.Operation.Name = scopeProperties.GetValueOrDefault(ScopeKeys.FunctionName); } - private void LogException(LogLevel logLevel, IEnumerable> values, Exception exception, string formattedMessage) + private void LogException(LogLevel logLevel, IEnumerable> values, Exception exception, string formattedMessage, EventId eventId) { ExceptionTelemetry telemetry = new ExceptionTelemetry(exception) { @@ -189,29 +193,33 @@ private void LogException(LogLevel logLevel, IEnumerable> values, string formattedMessage) + private void LogTrace(LogLevel logLevel, IEnumerable> values, string formattedMessage, EventId eventId) { - var properties = new Dictionary(); - ApplyScopeAndStateProperties(properties, values); + TraceTelemetry telemetry = new TraceTelemetry(formattedMessage); + + ApplyScopeAndStateProperties(telemetry.Properties, values, telemetry); + ApplyKnownProperties(telemetry.Properties, logLevel, eventId); + var severityLevel = GetSeverityLevel(logLevel); + + if (severityLevel.HasValue) { - _telemetryClient.TrackTrace(formattedMessage, severityLevel.Value, properties); - } - else - { - // LogLevel.None maps to null, so we have to handle that specially - _telemetryClient.TrackTrace(formattedMessage, properties); + telemetry.SeverityLevel = severityLevel; } + + // LogLevel.None maps to null, so we have to handle that specially + _telemetryClient.TrackTrace(telemetry); } private static SeverityLevel? GetSeverityLevel(LogLevel logLevel) @@ -236,16 +244,30 @@ private void LogTrace(LogLevel logLevel, IEnumerable properties, IEnumerable> state) + private static void ApplyScopeAndStateProperties(IDictionary properties, IEnumerable> state, ITelemetry telemetry) { - ApplyScopeProperties(properties); + ApplyScopeProperties(telemetry); ApplyProperties(properties, state, true); } - internal static void ApplyProperty(IDictionary properties, string key, object value, bool applyPrefix = false) + internal void ApplyKnownProperties(IDictionary properties, LogLevel logLevel, EventId eventId) + { + properties[LogConstants.CategoryNameKey] = _categoryName; + properties[LogConstants.LogLevelKey] = logLevel.ToStringOptimized(); + + if (eventId.Id != 0) + { + properties[LogConstants.EventIdKey] = Convert.ToString(eventId.Id); + } + if (!string.IsNullOrEmpty(eventId.Name)) + { + properties[LogConstants.EventNameKey] = eventId.Name; + } + } + internal static void ApplyProperty(IDictionary properties, string key, object objectValue, bool applyPrefix = false) { // do not apply null values - if (value == null) + if (objectValue == null) { return; } @@ -253,18 +275,21 @@ internal static void ApplyProperty(IDictionary properties, strin string stringValue = null; // Format dates - Type propertyType = value.GetType(); - if (propertyType == typeof(DateTime)) + if (objectValue is string value) { - stringValue = ((DateTime)value).ToUniversalTime().ToString(DateTimeFormatString); + stringValue = value; } - else if (propertyType == typeof(DateTimeOffset)) + else if (objectValue is DateTime date) { - stringValue = ((DateTimeOffset)value).UtcDateTime.ToString(DateTimeFormatString); + stringValue = date.ToUniversalTime().ToString(DateTimeFormatString); + } + else if (objectValue is DateTimeOffset dateOffset) + { + stringValue = dateOffset.UtcDateTime.ToString(DateTimeFormatString); } else { - stringValue = value.ToString(); + stringValue = objectValue.ToString(); } string prefixedKey = applyPrefix ? _prefixedProperyNames.GetOrAdd(key, k => @@ -288,10 +313,10 @@ private static void ApplyProperties(IDictionary properties, IEnu } } - private void LogFunctionResultAggregate(IEnumerable> values) + private void LogFunctionResultAggregate(IEnumerable> values, LogLevel logLevel, EventId eventId) { // Metric names will be created like "{FunctionName} {MetricName}" - IDictionary metrics = new Dictionary(); + IDictionary metrics = new Dictionary(7); string functionName = LoggingConstants.Unknown; // build up the collection of metrics to send @@ -299,8 +324,8 @@ private void LogFunctionResultAggregate(IEnumerable { switch (value.Key) { - case LogConstants.NameKey: - functionName = value.Value.ToString(); + case LogConstants.NameKey when value.Value is string name: + functionName = name; break; case LogConstants.TimestampKey: case LogConstants.OriginalFormatKey: @@ -308,39 +333,45 @@ private void LogFunctionResultAggregate(IEnumerable // We won't use the format string here break; default: - if (value.Value is TimeSpan) + if (value.Value is int intValue) { - // if it's a TimeSpan, log the milliseconds - metrics.Add(value.Key, ((TimeSpan)value.Value).TotalMilliseconds); + metrics.Add(value.Key, Convert.ToDouble(intValue)); } - else if (value.Value is double || value.Value is int) + else if (value.Value is double doubleValue) { - metrics.Add(value.Key, Convert.ToDouble(value.Value)); + metrics.Add(value.Key, doubleValue); + } + else if (value.Value is TimeSpan timeSpan) + { + // if it's a TimeSpan, log the milliseconds + metrics.Add(value.Key, timeSpan.TotalMilliseconds); } - // do nothing otherwise break; } } + IDictionary properties = new Dictionary(2); + ApplyKnownProperties(properties, logLevel, eventId); + foreach (KeyValuePair metric in metrics) { - _telemetryClient.TrackMetric($"{functionName} {metric.Key}", metric.Value); + _telemetryClient.TrackMetric($"{functionName} {metric.Key}", metric.Value, properties); } } - private void LogFunctionResult(IEnumerable> state, LogLevel logLevel, Exception exception) + private void LogFunctionResult(IEnumerable> state, LogLevel logLevel, Exception exception, EventId eventId) { - IDictionary scopeProps = DictionaryLoggerScope.GetMergedStateDictionaryOrNull(); + IReadOnlyDictionary scopeProps = DictionaryLoggerScope.GetMergedStateDictionaryOrNull(); KeyValuePair[] stateProps = state as KeyValuePair[] ?? state.ToArray(); // log associated exception details if (exception != null) { - LogException(logLevel, stateProps, exception, null); + LogException(logLevel, stateProps, exception, null, eventId); } - ApplyFunctionResultActivityTags(stateProps, scopeProps); + ApplyFunctionResultActivityTags(stateProps, scopeProps, logLevel); IOperationHolder requestOperation = scopeProps?.GetValueOrDefault>(OperationContext); if (requestOperation != null) @@ -362,7 +393,7 @@ private void LogFunctionResult(IEnumerable> state, /// /// /// - private void ApplyFunctionResultActivityTags(IEnumerable> state, IDictionary scope) + private void ApplyFunctionResultActivityTags(IEnumerable> state, IReadOnlyDictionary scope, LogLevel logLevel) { // Activity carries tracing context. It is managed by instrumented library (e.g. ServiceBus or Asp.Net Core) // and consumed by ApplicationInsights. @@ -412,19 +443,12 @@ private void ApplyFunctionResultActivityTags(IEnumerable(TState state) } StartTelemetryIfFunctionInvocation(state as IDictionary); - return DictionaryLoggerScope.Push(state); } diff --git a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/DictionaryLoggerScope.cs b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/DictionaryLoggerScope.cs index 00c2ee3bb..c305f9e0c 100644 --- a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/DictionaryLoggerScope.cs +++ b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/DictionaryLoggerScope.cs @@ -12,16 +12,17 @@ internal class DictionaryLoggerScope { private static AsyncLocal _value = new AsyncLocal(); - private DictionaryLoggerScope(IReadOnlyDictionary state, DictionaryLoggerScope parent) + // Cache merged dictionary. + internal IReadOnlyDictionary CurrentScope { get; private set; } + + internal DictionaryLoggerScope Parent { get; private set; } + + private DictionaryLoggerScope(IReadOnlyDictionary currentScope, DictionaryLoggerScope parent) { - State = state; + CurrentScope = currentScope; Parent = parent; } - internal IReadOnlyDictionary State { get; private set; } - - internal DictionaryLoggerScope Parent { get; private set; } - public static DictionaryLoggerScope Current { get @@ -37,54 +38,63 @@ public static DictionaryLoggerScope Current public static IDisposable Push(object state) { - IDictionary stateValues; - - if (state is IEnumerable> stateEnum) + if (state is IDictionary currentState) + { + BuildCurrentScope(currentState); + } + else if (state is IEnumerable> stateEnum) { - // Convert this to a dictionary as we have scenarios where we cannot have duplicates. In this - // case, if there are dupes, the later entry wins. + IDictionary stateValues; + // Convert this to a dictionary as we have scenarios where we cannot have duplicates. + // In this case, if there are dupes, the later entry wins. stateValues = new Dictionary(); foreach (var entry in stateEnum) { stateValues[entry.Key] = entry.Value; } + BuildCurrentScope(stateValues); } else { // There's nothing we can do with other states. return null; } - - Current = new DictionaryLoggerScope(new ReadOnlyDictionary(stateValues), Current); return new DisposableScope(); } - // Builds a state dictionary of all scopes. If an inner scope - // contains the same key as an outer scope, it overwrites the value. - public static IDictionary GetMergedStateDictionaryOrNull() + private static void BuildCurrentScope(IDictionary state) { - IDictionary scopeInfo = null; + IDictionary scopeInfo; - var current = Current; - while (current != null) + // Copy the current scope to the new scope dictionary + if (Current != null && Current.CurrentScope != null) { - if (scopeInfo == null) + scopeInfo = new Dictionary(state.Count + Current.CurrentScope.Count, StringComparer.Ordinal); + + foreach (var entry in Current.CurrentScope) { - scopeInfo = new Dictionary(); + scopeInfo.Add(entry); } - - foreach (var entry in current.State) + // If the state contains the same key as current scope, it overwrites the value. + foreach (var entry in state) { - // inner scopes win - if (!scopeInfo.Keys.Contains(entry.Key)) - { - scopeInfo.Add(entry); - } + scopeInfo[entry.Key] = entry.Value; } - current = current.Parent; } - - return scopeInfo; + else + { + scopeInfo = new Dictionary(state, StringComparer.Ordinal); + } + Current = new DictionaryLoggerScope(new ReadOnlyDictionary(scopeInfo), Current); + } + + public static IReadOnlyDictionary GetMergedStateDictionaryOrNull() + { + if (Current == null) + { + return null; + } + return Current.CurrentScope; } private class DisposableScope : IDisposable diff --git a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Extensions/LogLevelExtension.cs b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Extensions/LogLevelExtension.cs new file mode 100644 index 000000000..0194f1458 --- /dev/null +++ b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Extensions/LogLevelExtension.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System; +using System.Globalization; + +namespace Microsoft.Azure.WebJobs.Logging.ApplicationInsights.Extensions +{ + internal static class LogLevelExtension + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string ToStringOptimized(this LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return "Trace"; + case LogLevel.Debug: + return "Debug"; + case LogLevel.Information: + return "Information"; + case LogLevel.Warning: + return "Warning"; + case LogLevel.Error: + return "Error"; + case LogLevel.Critical: + return "Critical"; + case LogLevel.None: + return "None"; + default: + return logLevel.ToString(CultureInfo.InvariantCulture); + } + } + } +} diff --git a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Initializers/WebJobsTelemetryInitializer.cs b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Initializers/WebJobsTelemetryInitializer.cs index de89d9db8..a1becacd4 100644 --- a/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Initializers/WebJobsTelemetryInitializer.cs +++ b/src/Microsoft.Azure.WebJobs.Logging.ApplicationInsights/Initializers/WebJobsTelemetryInitializer.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using System.Globalization; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; +using Microsoft.Azure.WebJobs.Logging.ApplicationInsights.Extensions; using Microsoft.Extensions.Logging; namespace Microsoft.Azure.WebJobs.Logging.ApplicationInsights @@ -48,54 +49,61 @@ public void Initialize(ITelemetry telemetry) { telemetryContext.Location.Ip = LoggingConstants.ZeroIpAddress; } - - IDictionary telemetryProps = telemetryContext.Properties; - telemetryProps[LogConstants.ProcessIdKey] = _currentProcessId; - + telemetryContext.Properties[LogConstants.ProcessIdKey] = _currentProcessId; + // Apply our special scope properties - IDictionary scopeProps = DictionaryLoggerScope.GetMergedStateDictionaryOrNull(); - - string invocationId = scopeProps?.GetValueOrDefault(ScopeKeys.FunctionInvocationId); - if (invocationId != null) - { - telemetryProps[LogConstants.InvocationIdKey] = invocationId; - } + IReadOnlyDictionary scopeProps = DictionaryLoggerScope.GetMergedStateDictionaryOrNull(); // this could be telemetry tracked in scope of function call - then we should apply the logger scope // or RequestTelemetry tracked by the WebJobs SDK or AppInsight SDK - then we should apply Activity.Tags - if (scopeProps != null && scopeProps.Count > 0) + if (scopeProps?.Count > 0) { + if (!telemetryContext.Properties.ContainsKey(LogConstants.InvocationIdKey)) + { + if (scopeProps?.GetValueOrDefault(ScopeKeys.FunctionInvocationId) is string invocationId) + { + telemetryContext.Properties[LogConstants.InvocationIdKey] = invocationId; + } + } + telemetryContext.Operation.Name = scopeProps.GetValueOrDefault(ScopeKeys.FunctionName); - // Apply Category and LogLevel to all telemetry - string category = scopeProps.GetValueOrDefault(LogConstants.CategoryNameKey); - if (category != null) + // Apply Category, LogLevel event details to all telemetry + if (!telemetryContext.Properties.ContainsKey(LogConstants.CategoryNameKey)) { - telemetryProps[LogConstants.CategoryNameKey] = category; + if (scopeProps.GetValueOrDefault(LogConstants.CategoryNameKey) is string category) + { + telemetryContext.Properties[LogConstants.CategoryNameKey] = category; + } } - LogLevel? logLevel = scopeProps.GetValueOrDefault(LogConstants.LogLevelKey); - if (logLevel != null) + if (!telemetryContext.Properties.ContainsKey(LogConstants.LogLevelKey)) { - telemetryProps[LogConstants.LogLevelKey] = logLevel.Value.ToString(); + if (scopeProps.GetValueOrDefault(LogConstants.LogLevelKey) is LogLevel logLevel) + { + telemetryContext.Properties[LogConstants.LogLevelKey] = logLevel.ToStringOptimized(); + } } - int? eventId = scopeProps.GetValueOrDefault(LogConstants.EventIdKey); - if (eventId != null && eventId.HasValue && eventId.Value != 0) + if (!telemetryContext.Properties.ContainsKey(LogConstants.EventIdKey)) { - telemetryProps[LogConstants.EventIdKey] = eventId.Value.ToString(); + if (scopeProps.GetValueOrDefault(LogConstants.EventIdKey) is int eventId && eventId != 0) + { + telemetryContext.Properties[LogConstants.EventIdKey] = eventId.ToString(CultureInfo.InvariantCulture); + } } - string eventName = scopeProps.GetValueOrDefault(LogConstants.EventNameKey); - if (eventName != null) + if (!telemetryContext.Properties.ContainsKey(LogConstants.EventNameKey)) { - telemetryProps[LogConstants.EventNameKey] = eventName; + if (scopeProps.GetValueOrDefault(LogConstants.EventNameKey) is string eventName) + { + telemetryContext.Properties[LogConstants.EventNameKey] = eventName; + } } } // we may track traces/dependencies after function scope ends - we don't want to update those - RequestTelemetry request = telemetry as RequestTelemetry; - if (request != null) + if (telemetry is RequestTelemetry request) { UpdateRequestProperties(request); diff --git a/src/Microsoft.Azure.WebJobs.Shared/DictionaryExtensions.cs b/src/Microsoft.Azure.WebJobs.Shared/DictionaryExtensions.cs index d091d16ae..26abab701 100644 --- a/src/Microsoft.Azure.WebJobs.Shared/DictionaryExtensions.cs +++ b/src/Microsoft.Azure.WebJobs.Shared/DictionaryExtensions.cs @@ -15,4 +15,17 @@ public static T GetValueOrDefault(this IDictionary dictionary return default(T); } } + + internal static class ReadOnlyDictionaryExtensions + { + public static T GetValueOrDefault(this IReadOnlyDictionary dictionary, string key) + { + object value; + if (dictionary != null && dictionary.TryGetValue(key, out value)) + { + return (T)value; + } + return default(T); + } + } } diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Loggers/ApplicationInsightsLoggerTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Loggers/ApplicationInsightsLoggerTests.cs index f72b7753e..3a90ba899 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Loggers/ApplicationInsightsLoggerTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Loggers/ApplicationInsightsLoggerTests.cs @@ -933,7 +933,8 @@ private async Task Level1(Guid asyncLocalSetting) var level1 = new Dictionary { ["AsyncLocal"] = asyncLocalSetting, - ["1"] = 1 + ["1"] = 1, + ["shared"] = "Level1" }; ILogger logger = CreateLogger(_functionCategoryName); @@ -953,14 +954,16 @@ private async Task Level2(Guid asyncLocalSetting) var level2 = new Dictionary { - ["2"] = 2 + ["2"] = 2, + ["shared"] = "Level2" }; var expectedLevel2 = new Dictionary { ["1"] = 1, ["2"] = 2, - ["AsyncLocal"] = asyncLocalSetting + ["AsyncLocal"] = asyncLocalSetting, + ["shared"] = "Level2" }; ILogger logger2 = CreateLogger(_functionCategoryName); @@ -982,7 +985,8 @@ private async Task Level3(Guid asyncLocalSetting) var level3 = new Dictionary { ["1"] = 11, - ["3"] = 3 + ["3"] = 3, + ["shared"] = "Level3" }; var expectedLevel3 = new Dictionary @@ -990,7 +994,8 @@ private async Task Level3(Guid asyncLocalSetting) ["1"] = 11, ["2"] = 2, ["3"] = 3, - ["AsyncLocal"] = asyncLocalSetting + ["AsyncLocal"] = asyncLocalSetting, + ["shared"] = "Level3" }; ILogger logger3 = CreateLogger(_functionCategoryName);