Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Improvements - Logging #2946

Merged
merged 19 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,7 +35,7 @@ internal class ApplicationInsightsLogger : ILogger
internal const string MetricMinKey = "min";
internal const string MetricMaxKey = "max";
internal const string MetricStandardDeviationKey = "standarddeviation";

RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
private static readonly HashSet<string> SystemScopeKeys = new HashSet<string>
{
LogConstants.CategoryNameKey,
Expand Down Expand Up @@ -73,50 +75,41 @@ public void Log<TState>(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<string, object>();

// Add some well-known properties to the scope dictionary so the TelemetryInitializer can add them
// for all telemetry.
using (BeginScope(new Dictionary<string, object>
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<KeyValuePair<string, object>> values)
private void LogMetric(IEnumerable<KeyValuePair<string, object>> 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)
{
Expand All @@ -130,7 +123,14 @@ private void LogMetric(IEnumerable<KeyValuePair<string, object>> values)
switch (entry.Key)
{
case LogConstants.NameKey:
telemetry.Name = entry.Value.ToString();
if (entry.Value is string name)
{
telemetry.Name = name;
}
else
{
telemetry.Name = entry.Value.ToString();
}
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
continue;
case LogConstants.MetricValueKey:
telemetry.Sum = (double)entry.Value;
Expand Down Expand Up @@ -165,17 +165,29 @@ private void LogMetric(IEnumerable<KeyValuePair<string, object>> values)
}

// Applies scope properties; filters most system properties, which are used internally
private static void ApplyScopeProperties(IDictionary<string, string> properties)
private static void ApplyScopeProperties(ITelemetry telemetry)
{
var scopeProperties = DictionaryLoggerScope.GetMergedStateDictionaryOrNull();
var scopeProperties = DictionaryLoggerScope.GetMergedStateDictionaryOrNull();
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
if (scopeProperties != null)
{
var customScopeProperties = scopeProperties.Where(p => !SystemScopeKeys.Contains(p.Key, StringComparer.Ordinal));
ApplyProperties(properties, customScopeProperties, true);
foreach (var scopeProperty in scopeProperties)
{
if (!SystemScopeKeys.Contains(scopeProperty.Key, StringComparer.Ordinal))
{
ApplyProperty(telemetry.Context.Properties, scopeProperty.Key, scopeProperty.Value, true);
}
}

string invocationId = scopeProperties.GetValueOrDefault<string>(ScopeKeys.FunctionInvocationId);
if (invocationId != null)
{
telemetry.Context.Properties[LogConstants.InvocationIdKey] = invocationId;
}
}
telemetry.Context.Operation.Name = scopeProperties.GetValueOrDefault<string>(ScopeKeys.FunctionName);
}

private void LogException(LogLevel logLevel, IEnumerable<KeyValuePair<string, object>> values, Exception exception, string formattedMessage)
private void LogException(LogLevel logLevel, IEnumerable<KeyValuePair<string, object>> values, Exception exception, string formattedMessage, EventId eventId)
{
ExceptionTelemetry telemetry = new ExceptionTelemetry(exception)
{
Expand All @@ -189,29 +201,33 @@ private void LogException(LogLevel logLevel, IEnumerable<KeyValuePair<string, ob

// Also log a trace if there's a formattedMessage. This ensures that the error is visible
// in both App Insights analytics tables.
LogTrace(logLevel, values, formattedMessage);
LogTrace(logLevel, values, formattedMessage, eventId);
}

ApplyScopeAndStateProperties(telemetry.Properties, values);
ApplyScopeAndStateProperties(telemetry.Properties, values, telemetry);
ApplyKnownProperties(telemetry.Properties, logLevel, eventId);

_telemetryClient.TrackException(telemetry);
}

private void LogTrace(LogLevel logLevel, IEnumerable<KeyValuePair<string, object>> values, string formattedMessage)
private void LogTrace(LogLevel logLevel, IEnumerable<KeyValuePair<string, object>> values, string formattedMessage, EventId eventId)
{
var properties = new Dictionary<string, string>();
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)
Expand All @@ -236,35 +252,54 @@ private void LogTrace(LogLevel logLevel, IEnumerable<KeyValuePair<string, object
}

// Makes sure these are done in the correct order. If there are duplicate keys, the last State property wins.
private static void ApplyScopeAndStateProperties(IDictionary<string, string> properties, IEnumerable<KeyValuePair<string, object>> state)
private static void ApplyScopeAndStateProperties(IDictionary<string, string> properties, IEnumerable<KeyValuePair<string, object>> state, ITelemetry telemetry)
{
ApplyScopeProperties(properties);
ApplyScopeProperties(telemetry);
ApplyProperties(properties, state, true);
}

internal static void ApplyProperty(IDictionary<string, string> properties, string key, object value, bool applyPrefix = false)
internal void ApplyKnownProperties(IDictionary<string, string> properties, LogLevel logLevel, EventId eventId)
{
properties[LogConstants.CategoryNameKey] = _categoryName;
properties[LogConstants.LogLevelKey] = LogLevelEnumHelper.ToStringOptimized(logLevel);
if (eventId != null)
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
{
if (eventId.Id != 0)
{
properties[LogConstants.EventIdKey] = Convert.ToString(eventId.Id);
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
}
if (!string.IsNullOrEmpty(eventId.Name))
{
properties[LogConstants.EventNameKey] = eventId.Name;
}
}
}
internal static void ApplyProperty(IDictionary<string, string> properties, string key, object objectValue, bool applyPrefix = false)
{
// do not apply null values
if (value == null)
if (objectValue == null)
{
return;
}

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))
if (objectValue is DateTime date)
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
{
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 =>
Expand All @@ -288,10 +323,10 @@ private static void ApplyProperties(IDictionary<string, string> properties, IEnu
}
}

private void LogFunctionResultAggregate(IEnumerable<KeyValuePair<string, object>> values)
private void LogFunctionResultAggregate(IEnumerable<KeyValuePair<string, object>> values, LogLevel logLevel, EventId eventId)
{
// Metric names will be created like "{FunctionName} {MetricName}"
IDictionary<string, double> metrics = new Dictionary<string, double>();
IDictionary<string, double> metrics = new Dictionary<string, double>(7);
string functionName = LoggingConstants.Unknown;

// build up the collection of metrics to send
Expand All @@ -308,39 +343,45 @@ private void LogFunctionResultAggregate(IEnumerable<KeyValuePair<string, object>
// 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<string, string> properties = new Dictionary<string, string>(2);
ApplyKnownProperties(properties, logLevel, eventId);

foreach (KeyValuePair<string, double> metric in metrics)
{
_telemetryClient.TrackMetric($"{functionName} {metric.Key}", metric.Value);
_telemetryClient.TrackMetric($"{functionName} {metric.Key}", metric.Value, properties);
RohitRanjanMS marked this conversation as resolved.
Show resolved Hide resolved
}
}

private void LogFunctionResult(IEnumerable<KeyValuePair<string, object>> state, LogLevel logLevel, Exception exception)
private void LogFunctionResult(IEnumerable<KeyValuePair<string, object>> state, LogLevel logLevel, Exception exception, EventId eventId)
{
IDictionary<string, object> scopeProps = DictionaryLoggerScope.GetMergedStateDictionaryOrNull();
KeyValuePair<string, object>[] stateProps = state as KeyValuePair<string, object>[] ?? 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<RequestTelemetry> requestOperation = scopeProps?.GetValueOrDefault<IOperationHolder<RequestTelemetry>>(OperationContext);
if (requestOperation != null)
Expand All @@ -362,7 +403,7 @@ private void LogFunctionResult(IEnumerable<KeyValuePair<string, object>> state,
/// </summary>
/// <param name="state"></param>
/// <param name="scope"></param>
private void ApplyFunctionResultActivityTags(IEnumerable<KeyValuePair<string, object>> state, IDictionary<string, object> scope)
private void ApplyFunctionResultActivityTags(IEnumerable<KeyValuePair<string, object>> state, IDictionary<string, object> scope, LogLevel logLevel)
{
// Activity carries tracing context. It is managed by instrumented library (e.g. ServiceBus or Asp.Net Core)
// and consumed by ApplicationInsights.
Expand Down Expand Up @@ -412,19 +453,12 @@ private void ApplyFunctionResultActivityTags(IEnumerable<KeyValuePair<string, ob
break;
}
}


currentActivity.AddTag(LogConstants.CategoryNameKey, _categoryName);
currentActivity.AddTag(LogConstants.LogLevelKey, LogLevelEnumHelper.ToStringOptimized(logLevel));

if (scope != null)
{
if (scope.TryGetValue(LogConstants.CategoryNameKey, out object category))
{
currentActivity.AddTag(LogConstants.CategoryNameKey, category.ToString());
}

if (scope.TryGetValue(LogConstants.LogLevelKey, out object logLevel))
{
currentActivity.AddTag(LogConstants.LogLevelKey, logLevel.ToString());
}

if (!_loggerOptions.HttpAutoCollectionOptions.EnableHttpTriggerExtendedInfoCollection &&
scope.TryGetValue(ApplicationInsightsScopeKeys.HttpRequest, out var request) &&
request is HttpRequest httpRequest)
Expand Down
Loading