Skip to content

Commit a25f03b

Browse files
author
Martin Taillefer
committed
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. This separates static vs. dynamic log enrichment. The idea is to reduce overhead for stuff that never changes. Thus, we have the new IStaticLogEnricher type. We now support structured logging as a first class concept using the new StructuredLog extension methods around ILogger. 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 PR doesn't currently include updated tests, that's coming next after we validate the general approach. - This PR 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.
1 parent 1f7da6f commit a25f03b

35 files changed

+1553
-462
lines changed

src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Enrichment/EnricherExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Shared.Diagnostics;
78

@@ -42,6 +43,38 @@ public static IServiceCollection AddLogEnricher(this IServiceCollection services
4243
return services.AddSingleton(enricher);
4344
}
4445

46+
/// <summary>
47+
/// Registers a static log enricher type.
48+
/// </summary>
49+
/// <param name="services">The dependency injection container to add the enricher type to.</param>
50+
/// <typeparam name="T">Enricher type.</typeparam>
51+
/// <returns>The value of <paramref name="services"/>.</returns>
52+
/// <exception cref="ArgumentNullException"><paramref name="services"/> is <see langword="null"/>.</exception>
53+
[Experimental]
54+
public static IServiceCollection AddStaticLogEnricher<T>(this IServiceCollection services)
55+
where T : class, IStaticLogEnricher
56+
{
57+
_ = Throw.IfNull(services);
58+
59+
return services.AddSingleton<IStaticLogEnricher, T>();
60+
}
61+
62+
/// <summary>
63+
/// Registers a static log enricher instance.
64+
/// </summary>
65+
/// <param name="services">The dependency injection container to add the enricher instance to.</param>
66+
/// <param name="enricher">The enricher instance to add.</param>
67+
/// <returns>The value of <paramref name="services"/>.</returns>
68+
/// <exception cref="ArgumentNullException"><paramref name="services"/> or <paramref name="enricher"/> are <see langword="null"/>.</exception>
69+
[Experimental]
70+
public static IServiceCollection AddStaticLogEnricher(this IServiceCollection services, IStaticLogEnricher enricher)
71+
{
72+
_ = Throw.IfNull(services);
73+
_ = Throw.IfNull(enricher);
74+
75+
return services.AddSingleton(enricher);
76+
}
77+
4578
/// <summary>
4679
/// Registers a metric enricher type.
4780
/// </summary>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Microsoft.Extensions.Telemetry.Enrichment;
7+
8+
/// <summary>
9+
/// A component that augments log records with additional properties which are unchanging over the life of the object.
10+
/// </summary>
11+
[Experimental]
12+
public interface IStaticLogEnricher
13+
{
14+
/// <summary>
15+
/// Called to generate properties for a log record.
16+
/// </summary>
17+
/// <param name="bag">Where the enricher puts the properties it is producing.</param>
18+
void Enrich(IEnrichmentPropertyBag bag);
19+
}

src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/ILogPropertyCollector.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.Extensions.Compliance.Classification;
57

68
namespace Microsoft.Extensions.Telemetry.Logging;
79

@@ -23,4 +25,17 @@ public interface ILogPropertyCollector
2325
/// or when a property of the same name has already been added.
2426
/// </exception>
2527
void Add(string propertyName, object? propertyValue);
28+
29+
/// <summary>
30+
/// Adds a property to the current log record.
31+
/// </summary>
32+
/// <param name="propertyName">The name of the property to add.</param>
33+
/// <param name="propertyValue">The value of the property to add.</param>
34+
/// <param name="classification">The data classification of the property value.</param>
35+
/// <exception cref="ArgumentNullException"><paramref name="propertyName"/> is <see langword="null"/>.</exception>
36+
/// <exception cref="ArgumentException"><paramref name="propertyName" /> is empty or contains exclusively whitespace,
37+
/// or when a property of the same name has already been added.
38+
/// </exception>
39+
[Experimental]
40+
void Add(string propertyName, object? propertyValue, DataClassification classification);
2641
}

src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Logging/LogMethodHelper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.ComponentModel;
88
using System.Diagnostics.CodeAnalysis;
9+
using Microsoft.Extensions.Compliance.Classification;
910
#if NET6_0_OR_GREATER
1011
using Microsoft.Extensions.Logging;
1112
#endif
@@ -37,6 +38,10 @@ public void Add(string propertyName, object? propertyValue)
3738
Add(new KeyValuePair<string, object?>(fullName, propertyValue));
3839
}
3940

41+
/// <inheritdoc/>
42+
[Experimental]
43+
public void Add(string propertyName, object? propertyValue, DataClassification classification) => Add(propertyName, propertyValue);
44+
4045
/// <summary>
4146
/// Resets state of this container as described in <see cref="IResettable.TryReset"/>.
4247
/// </summary>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Shared.Diagnostics;
8+
9+
namespace Microsoft.Extensions.Telemetry.Logging;
10+
11+
/// <summary>
12+
/// Extensions for <see cref="ILogger"/>.
13+
/// </summary>
14+
[Experimental]
15+
public static class LoggerExtensions
16+
{
17+
/// <summary>
18+
/// Emits a structured log entry.
19+
/// </summary>
20+
/// <param name="logger">The logger to emit the log entry to.</param>
21+
/// <param name="logLevel">Entry will be written on this level.</param>
22+
/// <param name="eventId">Id of the event.</param>
23+
/// <param name="properties">The set of name/value pairs to log with this entry.</param>
24+
public static void StructuredLog(this ILogger logger, LogLevel logLevel, EventId eventId, LoggerMessageProperties properties)
25+
{
26+
Throw.IfNull(logger).Log<LoggerMessageProperties>(logLevel, eventId, properties, null, (_, _) => string.Empty);
27+
}
28+
29+
/// <summary>
30+
/// Emits a structured log entry.
31+
/// </summary>
32+
/// <param name="logger">The logger to emit the log entry to.</param>
33+
/// <param name="logLevel">Entry will be written on this level.</param>
34+
/// <param name="eventId">Id of the event.</param>
35+
/// <param name="properties">The set of name/value pairs to log with this entry.</param>
36+
/// <param name="exception">The exception related to this entry.</param>
37+
public static void StructuredLog(this ILogger logger, LogLevel logLevel, EventId eventId, LoggerMessageProperties properties, Exception? exception)
38+
{
39+
Throw.IfNull(logger).Log<LoggerMessageProperties>(logLevel, eventId, properties, exception, (_, _) => string.Empty);
40+
}
41+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.ComponentModel;
8+
using System.Diagnostics.CodeAnalysis;
9+
using Microsoft.Extensions.ObjectPool;
10+
using Microsoft.Shared.Pools;
11+
12+
namespace Microsoft.Extensions.Telemetry.Logging;
13+
14+
/// <summary>
15+
/// Utility type to support generated logging methods.
16+
/// </summary>
17+
[EditorBrowsable(EditorBrowsableState.Never)]
18+
[Experimental]
19+
public static class LoggerMessageHelper
20+
{
21+
[ThreadStatic]
22+
private static LoggerMessageProperties? _properties;
23+
24+
/// <summary>
25+
/// Gets a thread-local instance of this type.
26+
/// </summary>
27+
public static LoggerMessageProperties ThreadStaticLoggerMessageProperties
28+
{
29+
get
30+
{
31+
_properties ??= new();
32+
_ = _properties.TryReset();
33+
return _properties;
34+
}
35+
}
36+
37+
/// <summary>
38+
/// Enumerates an enumerable into a string.
39+
/// </summary>
40+
/// <param name="enumerable">The enumerable object.</param>
41+
/// <returns>
42+
/// A string representation of the enumerable.
43+
/// </returns>
44+
public static string Stringify(IEnumerable? enumerable)
45+
{
46+
if (enumerable == null)
47+
{
48+
return "null";
49+
}
50+
51+
var sb = PoolFactory.SharedStringBuilderPool.Get();
52+
_ = sb.Append('[');
53+
54+
bool first = true;
55+
foreach (object? e in enumerable)
56+
{
57+
if (!first)
58+
{
59+
_ = sb.Append(',');
60+
}
61+
62+
if (e == null)
63+
{
64+
_ = sb.Append("null");
65+
}
66+
else
67+
{
68+
_ = sb.Append(FormattableString.Invariant($"\"{e}\""));
69+
}
70+
71+
first = false;
72+
}
73+
74+
_ = sb.Append(']');
75+
var result = sb.ToString();
76+
PoolFactory.SharedStringBuilderPool.Return(sb);
77+
return result;
78+
}
79+
80+
/// <summary>
81+
/// Enumerates an enumerable of key/value pairs into a string.
82+
/// </summary>
83+
/// <typeparam name="TKey">Type of keys.</typeparam>
84+
/// <typeparam name="TValue">Type of values.</typeparam>
85+
/// <param name="enumerable">The enumerable object.</param>
86+
/// <returns>
87+
/// A string representation of the enumerable.
88+
/// </returns>
89+
public static string Stringify<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>>? enumerable)
90+
{
91+
if (enumerable == null)
92+
{
93+
return "null";
94+
}
95+
96+
var sb = PoolFactory.SharedStringBuilderPool.Get();
97+
_ = sb.Append('{');
98+
99+
bool first = true;
100+
foreach (var kvp in enumerable)
101+
{
102+
if (!first)
103+
{
104+
_ = sb.Append(',');
105+
}
106+
107+
if (typeof(TKey).IsValueType || kvp.Key is not null)
108+
{
109+
_ = sb.Append(FormattableString.Invariant($"\"{kvp.Key}\"="));
110+
}
111+
else
112+
{
113+
_ = sb.Append("null=");
114+
}
115+
116+
if (typeof(TValue).IsValueType || kvp.Value is not null)
117+
{
118+
_ = sb.Append(FormattableString.Invariant($"\"{kvp.Value}\""));
119+
}
120+
else
121+
{
122+
_ = sb.Append("null");
123+
}
124+
125+
first = false;
126+
}
127+
128+
_ = sb.Append('}');
129+
var result = sb.ToString();
130+
PoolFactory.SharedStringBuilderPool.Return(sb);
131+
return result;
132+
}
133+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using System.Diagnostics.CodeAnalysis;
6+
using Microsoft.Extensions.Compliance.Classification;
7+
8+
namespace Microsoft.Extensions.Telemetry.Logging;
9+
10+
public partial class LoggerMessageProperties
11+
{
12+
/// <summary>
13+
/// Represents a captured property that needs redaction.
14+
/// </summary>
15+
[SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Not for customer use and hidden from docs")]
16+
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Not needed")]
17+
[EditorBrowsable(EditorBrowsableState.Never)]
18+
public readonly struct ClassifiedProperty
19+
{
20+
/// <summary>
21+
/// Gets the name of the property.
22+
/// </summary>
23+
public readonly string Name { get; }
24+
25+
/// <summary>
26+
/// Gets the property's value.
27+
/// </summary>
28+
public readonly object? Value { get; }
29+
30+
/// <summary>
31+
/// Gets the property's data classification.
32+
/// </summary>
33+
public readonly DataClassification Classification { get; }
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="ClassifiedProperty"/> struct.
37+
/// </summary>
38+
public ClassifiedProperty(string name, object? value, DataClassification classification)
39+
{
40+
Name = name;
41+
Value = value;
42+
Classification = classification;
43+
}
44+
}
45+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Telemetry.Enrichment;
7+
8+
namespace Microsoft.Extensions.Telemetry.Logging;
9+
10+
public partial class LoggerMessageProperties
11+
{
12+
private sealed class PropertyBag : IEnrichmentPropertyBag
13+
{
14+
private readonly List<KeyValuePair<string, object?>> _properties;
15+
16+
public PropertyBag(List<KeyValuePair<string, object?>> properties)
17+
{
18+
_properties = properties;
19+
}
20+
21+
void IEnrichmentPropertyBag.Add(string key, object value)
22+
{
23+
_properties.Add(new KeyValuePair<string, object?>(key, value));
24+
}
25+
26+
/// <inheritdoc/>
27+
void IEnrichmentPropertyBag.Add(string key, string value)
28+
{
29+
_properties.Add(new KeyValuePair<string, object?>(key, value));
30+
}
31+
32+
/// <inheritdoc/>
33+
void IEnrichmentPropertyBag.Add(ReadOnlySpan<KeyValuePair<string, object>> properties)
34+
{
35+
foreach (var p in properties)
36+
{
37+
// we're going from KVP<string, object> to KVP<string, object?> which is strictly correct, so ignore the complaint
38+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
39+
_properties.Add(p);
40+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
41+
}
42+
}
43+
44+
/// <inheritdoc/>
45+
void IEnrichmentPropertyBag.Add(ReadOnlySpan<KeyValuePair<string, string>> properties)
46+
{
47+
foreach (var p in properties)
48+
{
49+
_properties.Add(new KeyValuePair<string, object?>(p.Key, p.Value));
50+
}
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)