-
Notifications
You must be signed in to change notification settings - Fork 751
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce new extended logging model.
- Replace the OpenTelemetry-specific logger design with a LoggerFactory-based approach independent of OpenTelemetry. This delivers enrichment and redaction to all ILogger users. The basic idea is that the code generator will no longer be responsible for doing redaction. Instead, the generated code will accumulate normal properties in one collection and will accumulate classified properties in a different collection. Within the Logger type, the classified properties are run through redaction and added to the normal property list. This list is then used to enrich into. And then the final thing is given to the set of currently registered ILogger instances in the system. So all these loggers get redacted and enriched state. NOTES: - The idea is that the customer should call AddExtendedLogging instead of AddLogging. By virtue of using this call, they'll also be using the newer code generator which will take advantage of the new features. - This doesn't currently include updated tests, that's coming next after we validate the general approach. - This doesn't currently include an updated logging code generator that will take advantage of this new functionality. - Right now, if a user calls AddLogging after they had already called AddExtendedLogging, this will undo the extended logging stuff and leave the system in classic mode. We need to find a strategy to deal with this.
- Loading branch information
Martin Taillefer
committed
Jun 23, 2023
1 parent
1f7da6f
commit c9ebcd5
Showing
9 changed files
with
503 additions
and
351 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
288 changes: 288 additions & 0 deletions
288
src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LoggerMessageState.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.ComponentModel; | ||
using System.Diagnostics.CodeAnalysis; | ||
using Microsoft.Extensions.Compliance.Classification; | ||
#if NET6_0_OR_GREATER | ||
using Microsoft.Extensions.Logging; | ||
#endif | ||
using Microsoft.Extensions.ObjectPool; | ||
using Microsoft.Extensions.Telemetry.Enrichment; | ||
using Microsoft.Shared.Diagnostics; | ||
using Microsoft.Shared.Pools; | ||
|
||
namespace Microsoft.Extensions.Telemetry.Logging; | ||
|
||
/// <summary> | ||
/// Utility type to support generated logging methods. | ||
/// </summary> | ||
/// <remarks> | ||
/// This type is not intended to be directly invoked by application code, | ||
/// it is intended to be invoked by generated logging method code. | ||
/// </remarks> | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
[Experimental] | ||
public sealed class LoggerMessageState : ILogPropertyCollector, IEnrichmentPropertyBag, IResettable | ||
{ | ||
private const string Separator = "_"; | ||
|
||
private static readonly ObjectPool<LoggerMessageState> _states = PoolFactory.CreateResettingPool<LoggerMessageState>(); | ||
|
||
/// <inheritdoc/> | ||
public void Add(string propertyName, object? propertyValue) | ||
{ | ||
_ = Throw.IfNull(propertyName); | ||
|
||
string fullName = ParameterName.Length > 0 ? ParameterName + Separator + propertyName : propertyName; | ||
Properties.Add(new KeyValuePair<string, object?>(fullName, propertyValue)); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public void Add(string propertyName, object? propertyValue, DataClassification classification) | ||
{ | ||
_ = Throw.IfNull(propertyName); | ||
|
||
string fullName = ParameterName.Length > 0 ? ParameterName + Separator + propertyName : propertyName; | ||
Properties.Add(new KeyValuePair<string, object?>(fullName, propertyValue)); | ||
ClassifiedProperties.Add(new(propertyName, propertyValue, classification)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds many properties. | ||
/// </summary> | ||
public void AddRange(IEnumerable<KeyValuePair<string, object?>> properties) | ||
{ | ||
_ = Throw.IfNull(properties); | ||
Properties.AddRange(properties); | ||
} | ||
|
||
/// <summary> | ||
/// Resets state of this container as described in <see cref="IResettable.TryReset"/>. | ||
/// </summary> | ||
/// <returns> | ||
/// <see langword="true" /> if the object successfully reset and can be reused. | ||
/// </returns> | ||
public bool TryReset() | ||
{ | ||
Properties.Clear(); | ||
ClassifiedProperties.Clear(); | ||
ParameterName = string.Empty; | ||
return true; | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the name of the logging method parameter for which to collect properties. | ||
/// </summary> | ||
public string ParameterName { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// Enumerates an enumerable into a string. | ||
/// </summary> | ||
/// <param name="enumerable">The enumerable object.</param> | ||
/// <returns> | ||
/// A string representation of the enumerable. | ||
/// </returns> | ||
public static string Stringify(IEnumerable? enumerable) | ||
{ | ||
if (enumerable == null) | ||
{ | ||
return "null"; | ||
} | ||
|
||
var sb = PoolFactory.SharedStringBuilderPool.Get(); | ||
_ = sb.Append('['); | ||
|
||
bool first = true; | ||
foreach (object? e in enumerable) | ||
{ | ||
if (!first) | ||
{ | ||
_ = sb.Append(','); | ||
} | ||
|
||
if (e == null) | ||
{ | ||
_ = sb.Append("null"); | ||
} | ||
else | ||
{ | ||
_ = sb.Append(FormattableString.Invariant($"\"{e}\"")); | ||
} | ||
|
||
first = false; | ||
} | ||
|
||
_ = sb.Append(']'); | ||
var result = sb.ToString(); | ||
PoolFactory.SharedStringBuilderPool.Return(sb); | ||
return result; | ||
} | ||
|
||
/// <summary> | ||
/// Enumerates an enumerable of key/value pairs into a string. | ||
/// </summary> | ||
/// <typeparam name="TKey">Type of keys.</typeparam> | ||
/// <typeparam name="TValue">Type of values.</typeparam> | ||
/// <param name="enumerable">The enumerable object.</param> | ||
/// <returns> | ||
/// A string representation of the enumerable. | ||
/// </returns> | ||
public static string Stringify<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>>? enumerable) | ||
{ | ||
if (enumerable == null) | ||
{ | ||
return "null"; | ||
} | ||
|
||
var sb = PoolFactory.SharedStringBuilderPool.Get(); | ||
_ = sb.Append('{'); | ||
|
||
bool first = true; | ||
foreach (var kvp in enumerable) | ||
{ | ||
if (!first) | ||
{ | ||
_ = sb.Append(','); | ||
} | ||
|
||
if (typeof(TKey).IsValueType || kvp.Key is not null) | ||
{ | ||
_ = sb.Append(FormattableString.Invariant($"\"{kvp.Key}\"=")); | ||
} | ||
else | ||
{ | ||
_ = sb.Append("null="); | ||
} | ||
|
||
if (typeof(TValue).IsValueType || kvp.Value is not null) | ||
{ | ||
_ = sb.Append(FormattableString.Invariant($"\"{kvp.Value}\"")); | ||
} | ||
else | ||
{ | ||
_ = sb.Append("null"); | ||
} | ||
|
||
first = false; | ||
} | ||
|
||
_ = sb.Append('}'); | ||
var result = sb.ToString(); | ||
PoolFactory.SharedStringBuilderPool.Return(sb); | ||
return result; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the list of properties added to this instance. | ||
/// </summary> | ||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Not intended for application use")] | ||
public List<KeyValuePair<string, object?>> Properties { get; } = new(); | ||
|
||
/// <summary> | ||
/// Gets an additional list of properties which must receive redaction before being used. | ||
/// </summary> | ||
[SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Not intended for application use")] | ||
public List<ClassifiedProperty> ClassifiedProperties { get; } = new(); | ||
|
||
/// <summary> | ||
/// Gets or sets an optional formatting delegate to be used by a logging call. | ||
/// </summary> | ||
public object? Formatter { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets optional logging state to be used by a logging call. | ||
/// </summary> | ||
public object? State { get; set; } | ||
|
||
/// <summary> | ||
/// Gets an instance of this class from the global pool. | ||
/// </summary> | ||
/// <returns>A usable instance.</returns> | ||
public static LoggerMessageState Rent() => _states.Get(); | ||
|
||
/// <summary> | ||
/// Returns an instance of this class to the global pool. | ||
/// </summary> | ||
/// <param name="state">The state instance.</param> | ||
public static void Return(LoggerMessageState state) => _states.Return(state); | ||
|
||
/// <inheritdoc/> | ||
void IEnrichmentPropertyBag.Add(string key, object value) | ||
{ | ||
_ = Throw.IfNullOrEmpty(key); | ||
Properties.Add(new KeyValuePair<string, object?>(key, value)); | ||
} | ||
|
||
/// <inheritdoc/> | ||
void IEnrichmentPropertyBag.Add(string key, string value) | ||
{ | ||
_ = Throw.IfNullOrEmpty(key); | ||
Properties.Add(new KeyValuePair<string, object?>(key, value)); | ||
} | ||
|
||
/// <inheritdoc/> | ||
void IEnrichmentPropertyBag.Add(ReadOnlySpan<KeyValuePair<string, object>> properties) | ||
{ | ||
foreach (var p in properties) | ||
{ | ||
// we're going from KVP<string, object> to KVP<string, object?> which is strictly correct, so ignore the complaint | ||
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. | ||
Properties.Add(p); | ||
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. | ||
} | ||
} | ||
|
||
/// <inheritdoc/> | ||
void IEnrichmentPropertyBag.Add(ReadOnlySpan<KeyValuePair<string, string>> properties) | ||
{ | ||
foreach (var p in properties) | ||
{ | ||
Properties.Add(new KeyValuePair<string, object?>(p.Key, p.Value)); | ||
} | ||
} | ||
|
||
#if NET6_0_OR_GREATER | ||
/// <summary> | ||
/// Gets log define options configured to skip the log level enablement check. | ||
/// </summary> | ||
public static LogDefineOptions SkipEnabledCheckOptions { get; } = new() { SkipEnabledCheck = true }; | ||
#endif | ||
|
||
/// <summary> | ||
/// Represents a captured property that needs redaction. | ||
/// </summary> | ||
[SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Not for customer use and hidden from docs")] | ||
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not needed")] | ||
[EditorBrowsable(EditorBrowsableState.Never)] | ||
public readonly struct ClassifiedProperty | ||
{ | ||
/// <summary> | ||
/// Gets the name of the property. | ||
/// </summary> | ||
public readonly string Name { get; } | ||
|
||
/// <summary> | ||
/// Gets the property's value. | ||
/// </summary> | ||
public readonly object? Value { get; } | ||
|
||
/// <summary> | ||
/// Gets the property's data classification. | ||
/// </summary> | ||
public readonly DataClassification Classification { get; } | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ClassifiedProperty"/> struct. | ||
/// </summary> | ||
public ClassifiedProperty(string name, object? value, DataClassification classification) | ||
{ | ||
Name = name; | ||
Value = value; | ||
Classification = classification; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/Libraries/Microsoft.Extensions.Telemetry/Logging/ExtendedLoggerFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.Compliance.Redaction; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Extensions.Telemetry.Enrichment; | ||
|
||
namespace Microsoft.Extensions.Telemetry.Logging; | ||
|
||
internal sealed class ExtendedLoggerFactory : ILoggerFactory | ||
{ | ||
private readonly LoggerFactory _loggerFactory; | ||
private readonly ConcurrentDictionary<string, ILogger> _cache = new(); | ||
private readonly ILogEnricher[] _enrichers; | ||
private readonly LoggingOptions _loggingOptions; | ||
private readonly IRedactorProvider? _redactorProvider; | ||
|
||
public ExtendedLoggerFactory( | ||
IEnumerable<ILoggerProvider> providers, | ||
IOptionsMonitor<LoggerFilterOptions> filterOption, | ||
IOptions<LoggingOptions> loggingOptions, | ||
IEnumerable<ILogEnricher> enrichers, | ||
IRedactorProvider? redactorProvider = null, | ||
IOptions<LoggerFactoryOptions>? options = null, | ||
IExternalScopeProvider? scopeProvider = null) | ||
{ | ||
_loggerFactory = new LoggerFactory(providers, filterOption, options, scopeProvider); | ||
_enrichers = enrichers.ToArray(); | ||
_loggingOptions = loggingOptions.Value; | ||
_redactorProvider = redactorProvider; | ||
} | ||
|
||
public ILogger CreateLogger(string categoryName) | ||
{ | ||
return _cache.GetOrAdd(categoryName, (name, lfp) | ||
=> new Logger( | ||
lfp._loggerFactory.CreateLogger(name), | ||
_enrichers, | ||
_loggingOptions.IncludeStackTrace, | ||
_loggingOptions.MaxStackTraceLength, | ||
_redactorProvider), this); | ||
} | ||
|
||
public void AddProvider(ILoggerProvider provider) => _loggerFactory.AddProvider(provider); | ||
public void Dispose() => _loggerFactory.Dispose(); | ||
} |
Oops, something went wrong.