-
Notifications
You must be signed in to change notification settings - Fork 5.1k
[System.ClientModel] Add Logging #45472
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
85 commits
Select commit
Hold shift + click to select a range
8d9dd93
initial draft
m-redding 74c77c4
WIP test stubs pt 1
m-redding 4192f6c
WIP test stubs 2
m-redding 7f5c7cf
tweaks
m-redding edccce7
changes
m-redding b08f2c5
p1 of adding Event Source
m-redding 291365d
tests p1
m-redding 107abf0
test updates
m-redding 207b215
Tests2
m-redding 3747094
updates
m-redding c986aed
API
m-redding d802bc3
try slimming methods and adding retry policy log
m-redding fc17694
test
m-redding 7253073
conflicts
m-redding 205ef71
refactory + add abstraction
m-redding 1c2f4dc
add tests and update api
m-redding cb53622
more tests
m-redding 8f98aec
fix functional tests
m-redding 06126f2
static -> instance
m-redding 6f51ac6
updates
m-redding 69ced27
tweaks
m-redding e445a1b
tweaks
m-redding dec45fd
fix
m-redding 524edc9
Merge branch 'Azure:main' into loggingpolicy2
m-redding 469aeb7
little refactorings
m-redding ecf0a26
tweaks
m-redding 373fb99
tweaks
m-redding 441ee3e
WIP feedback
m-redding 49a3bcd
reformat
m-redding da1b7f8
WIP more feedback
m-redding 86dc327
WIP
m-redding 672390a
Merge branch 'Azure:main' into loggingpolicy2
m-redding ef183ef
Merge branch 'loggingpolicy2' of https://github.com/m-redding/azure-s…
m-redding 9f93eea
Merge branch 'Azure:main' into loggingpolicy2
m-redding 281aa37
Merge branch 'loggingpolicy2' of https://github.com/m-redding/azure-s…
m-redding b5d97f0
Merge branch 'Azure:main' into loggingpolicy2
m-redding 4a97e78
WIP - updates
m-redding 2726470
Export API
m-redding e93cbd9
API
m-redding 9e5f1d3
API tweaks
m-redding 87bafa7
comment
m-redding 009792c
Merge branch 'Azure:main' into loggingpolicy2
m-redding 1fb84da
changes 1
m-redding d5a9360
changes 2
m-redding 985844f
changes
m-redding 3afbc2e
fixes
m-redding b5ba8ac
more updates
m-redding 382f0d7
some tests
m-redding eaebe02
WIP - experiment
m-redding 72c2062
Merge branch 'Azure:main' into loggingpolicy2
m-redding 21bea5e
wip
m-redding fca37df
Merge branch 'Azure:main' into loggingpolicy2
m-redding 7818432
Merge branch 'loggingpolicy2' of https://github.com/m-redding/azure-s…
m-redding 909cdb8
wip fixes
m-redding e21486a
Merge branch 'Azure:main' into loggingpolicy2
m-redding f474c2c
event source / logger tests green
m-redding 8cd0d58
API
m-redding 75c24dc
fb
m-redding bc0c547
implement request ID
m-redding 80ca39a
adding tests, some fb
m-redding f35144c
more FB
m-redding 2e92d6f
more fb + more tests
m-redding 7cdeb49
WIP
m-redding 41e3abd
tweaks to logging stream
m-redding eadd1bb
tests green
m-redding 37975b6
test updates
m-redding 9b2f9a0
more test updates
m-redding 8d307b8
Update sdk/core/System.ClientModel/src/Options/ClientLoggingOptions.cs
m-redding 0ebff9d
fb 1
m-redding 778c70b
fb 2
m-redding dccb471
fixes
m-redding 57b9166
Merge branch 'Azure:main' into loggingpolicy2
m-redding bdf8068
initial samples
m-redding 91e2a03
api fix and logging.md
m-redding 18d6ae0
tests
m-redding fbcbf57
Merge branch 'Azure:main' into loggingpolicy2
m-redding 9f75d15
Merge branch 'Azure:main' into loggingpolicy2
m-redding d0123df
Merge branch 'Azure:main' into loggingpolicy2
m-redding 2ade2a6
Apply suggestions from code review
m-redding 96a36c8
feedback 1
m-redding 433a018
more fb
m-redding 7dbc599
more feedback
m-redding 9f5b4e6
test fix
m-redding ccd5775
prefer activity.current for request id
m-redding 0ce5416
last clean up tweaks
m-redding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
initial draft
- Loading branch information
commit 8d9dd9375a51e3bca63a23441e3d9d054a0ecc62
There are no files selected for viewing
This file contains hidden or 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
82 changes: 82 additions & 0 deletions
82
sdk/core/System.ClientModel/src/Internal/ClientModelLogMessages.cs
This file contains hidden or 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,82 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Net.NetworkInformation; | ||
| using System.Text; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace System.ClientModel.Internal; | ||
|
|
||
| internal static partial class ClientModelLogMessages | ||
| { | ||
| private const int RequestEvent = 1; | ||
| private const int RequestContentEvent = 2; | ||
| private const int ResponseEvent = 5; | ||
| private const int ResponseContentEvent = 6; | ||
| private const int ResponseDelayEvent = 7; | ||
| private const int ErrorResponseEvent = 8; | ||
| private const int ErrorResponseContentEvent = 9; | ||
| private const int RequestRetryingEvent = 10; | ||
| private const int ResponseContentBlockEvent = 11; | ||
| private const int ErrorResponseContentBlockEvent = 12; | ||
| private const int ResponseContentTextEvent = 13; | ||
| private const int ErrorResponseContentTextEvent = 14; | ||
| private const int ResponseContentTextBlockEvent = 15; | ||
| private const int ErrorResponseContentTextBlockEvent = 16; | ||
| private const int RequestContentTextEvent = 17; | ||
| private const int ExceptionResponseEvent = 18; | ||
| private const int PipelineTransportOptionsNotAppliedEvent = 23; // TODO | ||
|
|
||
| [LoggerMessage(RequestEvent, LogLevel.Information, "Request [{requestId}] {method} {uri}\r\n{headers}client assembly: {assemblyName}", EventName = "Request")] | ||
| public static partial void Request(ILogger logger, string requestId, string method, string uri, string headers, string? assemblyName); | ||
|
|
||
| // TODO - these are verbose logs in EventSource, should they be trace logs here? | ||
| [LoggerMessage(RequestContentEvent, LogLevel.Information, "Request [{requestId}] content: {content}", EventName = "RequestContent")] | ||
| public static partial void RequestContent(ILogger logger, string requestId, byte[] content); | ||
|
|
||
| // same as above | ||
| [LoggerMessage(RequestContentTextEvent, LogLevel.Information, "Request [{requestId}] content: {content}", EventName = "RequestContentText")] | ||
| public static partial void RequestContentText(ILogger logger, string requestID, string content); | ||
|
|
||
| [LoggerMessage(ResponseEvent, LogLevel.Information, "Response [{requestId}] {status} {reasonPhrase} ({seconds:00.0}s)\r\n{headers}", EventName = "Response")] | ||
| public static partial void Response(ILogger logger, string requestID, int status, string reasonPhrase, double seconds, string headers); | ||
|
|
||
| // same | ||
| [LoggerMessage(ResponseContentEvent, LogLevel.Information, "Response [{requestId}] content: {content}", EventName = "ResponseContent")] | ||
| public static partial void ResponseContent(ILogger logger, string requestId, byte[] content); | ||
|
|
||
| [LoggerMessage(ResponseContentTextEvent, LogLevel.Information, "Response [{requestId}] content: {content}", EventName = "ResponseContentText")] | ||
| public static partial void ResponseContentText(ILogger logger, string requestId, string content); | ||
|
|
||
| [LoggerMessage(ResponseContentBlockEvent, LogLevel.Information, "Response [{requestId}] content block {blockNumber}: {content}", EventName = "ResponseContentBlock")] | ||
| public static partial void ResponseContentBlock(ILogger logger, string requestId, int blockNumber, byte[] content); | ||
|
|
||
| [LoggerMessage(ResponseContentTextBlockEvent, LogLevel.Information, "Response [{requestId}] content block {blockNumber}: {content}", EventName = "ResponseContentTextBlock")] | ||
| public static partial void ResponseContentTextBlock(ILogger logger, string requestId, int blockNumber, string content); | ||
|
|
||
| [LoggerMessage(ErrorResponseEvent, LogLevel.Warning, "Error response [{requestId}] {status} {reasonPhrase} ({seconds:00.0}s)\r\n{headers}", EventName = "ErrorResponse")] | ||
| public static partial void ErrorResponse(ILogger logger, string requestId, int status, string reasonPhrase, double seconds, string headers); | ||
|
|
||
| [LoggerMessage(ErrorResponseContentEvent, LogLevel.Warning, "Error response [{requestId}] content: {content}", EventName = "ErrorResponseContent")] | ||
| public static partial void ErrorResponseContent(ILogger logger, string requestId, byte[] content); | ||
|
|
||
| [LoggerMessage(ErrorResponseContentTextEvent, LogLevel.Warning, "Error response [{requestId}] content: {content}", EventName = "ErrorResponseContentText")] | ||
| public static partial void ErrorResponseContentText(ILogger logger, string requestId, string content); | ||
|
|
||
| [LoggerMessage(ErrorResponseContentBlockEvent, LogLevel.Warning, "Error response [{requestId}] content block {blockNumber}: {content}", EventName = "ErrorResponseContentBlock")] | ||
| public static partial void ErrorResponseContentBlock(ILogger logger, string requestId, int blockNumber, byte[] content); | ||
|
|
||
| [LoggerMessage(ErrorResponseContentTextBlockEvent, LogLevel.Warning, "Error response [{requestId}] content block {blockNumber}: {content}", EventName = "ErrorResponseContentTextBlock")] | ||
| public static partial void ErrorResponseContentTextBlock(ILogger logger, string requestId, int blockNumber, string content); | ||
|
|
||
| [LoggerMessage(RequestRetryingEvent, LogLevel.Information, "Request [{requestId}] attempt number {retryCount} took {seconds:00.0}s", EventName = "RequestRetrying")] | ||
| public static partial void RequestRetrying(ILogger logger, string requestId, int retryCount, double seconds); | ||
|
|
||
| [LoggerMessage(ResponseDelayEvent, LogLevel.Warning, "Response [{requestId}] took {seconds:00.0}s", EventName = "ResponseDelay")] | ||
| public static partial void ResponseDelay(ILogger logger, string requestId, double seconds); | ||
|
|
||
| [LoggerMessage(ExceptionResponseEvent, LogLevel.Information, "Request [{requestId}] exception {exception}", EventName = "ExceptionResponse")] | ||
| public static partial void ExceptionResponse(ILogger logger, string requestId, string exception); | ||
| } |
57 changes: 57 additions & 0 deletions
57
sdk/core/System.ClientModel/src/Internal/ContentTypeUtilities.cs
This file contains hidden or 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,57 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Text; | ||
|
|
||
| namespace System.ClientModel.Internal; | ||
|
|
||
| /// <summary> | ||
| /// TODO. | ||
| /// </summary> | ||
| internal class ContentTypeUtilities | ||
| { | ||
| public static bool TryGetTextEncoding(string contentType, out Encoding? encoding) | ||
| { | ||
| const string charsetMarker = "; charset="; | ||
| const string utf8Charset = "utf-8"; | ||
| const string textContentTypePrefix = "text/"; | ||
| const string jsonSuffix = "json"; | ||
| const string appJsonPrefix = "application/json"; | ||
| const string xmlSuffix = "xml"; | ||
| const string urlEncodedSuffix = "-urlencoded"; | ||
|
|
||
| // Default is technically US-ASCII, but will default to UTF-8 which is a superset. | ||
| const string appFormUrlEncoded = "application/x-www-form-urlencoded"; | ||
|
|
||
| if (contentType == null) | ||
| { | ||
| encoding = null; | ||
| return false; | ||
| } | ||
|
|
||
| var charsetIndex = contentType.IndexOf(charsetMarker, StringComparison.OrdinalIgnoreCase); | ||
m-redding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (charsetIndex != -1) | ||
| { | ||
| ReadOnlySpan<char> charset = contentType.AsSpan().Slice(charsetIndex + charsetMarker.Length); | ||
| if (charset.StartsWith(utf8Charset.AsSpan(), StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| encoding = Encoding.UTF8; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| if (contentType.StartsWith(textContentTypePrefix, StringComparison.OrdinalIgnoreCase) || | ||
| contentType.EndsWith(jsonSuffix, StringComparison.OrdinalIgnoreCase) || | ||
| contentType.EndsWith(xmlSuffix, StringComparison.OrdinalIgnoreCase) || | ||
| contentType.EndsWith(urlEncodedSuffix, StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith(appJsonPrefix, StringComparison.OrdinalIgnoreCase) || | ||
| contentType.StartsWith(appFormUrlEncoded, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| encoding = Encoding.UTF8; | ||
| return true; | ||
| } | ||
|
|
||
| encoding = null; | ||
| return false; | ||
| } | ||
| } | ||
215 changes: 215 additions & 0 deletions
215
sdk/core/System.ClientModel/src/Internal/PipelineMessageSanitizer.cs
This file contains hidden or 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,215 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text; | ||
|
|
||
| namespace System.ClientModel.Internal; | ||
|
|
||
| /// <summary> | ||
| /// TODO. | ||
| /// </summary> | ||
| internal class PipelineMessageSanitizer | ||
| { | ||
| private const string LogAllValue = "*"; | ||
| private readonly bool _logAllHeaders; | ||
| private readonly bool _logFullQueries; | ||
| private readonly string[] _allowedQueryParameters; | ||
| private readonly string _redactedPlaceholder; | ||
| private readonly HashSet<string> _allowedHeaders; | ||
|
|
||
| [ThreadStatic] | ||
| private static StringBuilder? s_cachedStringBuilder; | ||
| private const int MaxCachedStringBuilderCapacity = 1024; | ||
|
|
||
| /// <summary> | ||
| /// TODO. | ||
| /// </summary> | ||
| /// <param name="allowedQueryParameters"></param> | ||
| /// <param name="allowedHeaders"></param> | ||
| /// <param name="redactedPlaceholder"></param> | ||
| public PipelineMessageSanitizer(string[] allowedQueryParameters, string[] allowedHeaders, string redactedPlaceholder = "REDACTED") | ||
| { | ||
| _logAllHeaders = allowedHeaders.Contains(LogAllValue); | ||
| _logFullQueries = allowedQueryParameters.Contains(LogAllValue); | ||
|
|
||
| _allowedQueryParameters = allowedQueryParameters; | ||
| _redactedPlaceholder = redactedPlaceholder; | ||
| _allowedHeaders = new HashSet<string>(allowedHeaders, StringComparer.InvariantCultureIgnoreCase); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// TODO. | ||
| /// </summary> | ||
| /// <param name="name"></param> | ||
| /// <param name="value"></param> | ||
| /// <returns></returns> | ||
| public string SanitizeHeader(string name, string value) | ||
| { | ||
| if (_logAllHeaders || _allowedHeaders.Contains(name)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| return _redactedPlaceholder; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// TODO. | ||
| /// </summary> | ||
| /// <param name="url"></param> | ||
| /// <returns></returns> | ||
| public string SanitizeUrl(string url) | ||
| { | ||
| if (_logFullQueries) | ||
| { | ||
| return url; | ||
| } | ||
|
|
||
| #if NET5_0_OR_GREATER | ||
| int indexOfQuerySeparator = url.IndexOf('?', StringComparison.Ordinal); | ||
| #else | ||
| int indexOfQuerySeparator = url.IndexOf('?'); | ||
| #endif | ||
|
|
||
| if (indexOfQuerySeparator == -1) | ||
| { | ||
| return url; | ||
| } | ||
|
|
||
| // PERF: Avoid allocations in this heavily-used method: | ||
| // 1. Use ReadOnlySpan<char> to avoid creating substrings. | ||
| // 2. Defer creating a StringBuilder until absolutely necessary. | ||
| // 3. Use a rented StringBuilder to avoid allocating a new one | ||
| // each time. | ||
|
|
||
| // Create the StringBuilder only when necessary (when we encounter | ||
| // a query parameter that needs to be redacted) | ||
| StringBuilder? stringBuilder = null; | ||
|
|
||
| // Keeps track of the number of characters we've processed so far | ||
| // so that, if we need to create a StringBuilder, we know how many | ||
| // characters to copy over from the original URL. | ||
| int lengthSoFar = indexOfQuerySeparator + 1; | ||
|
|
||
| ReadOnlySpan<char> query = url.AsSpan(indexOfQuerySeparator + 1); // +1 to skip the '?' | ||
|
|
||
| while (query.Length > 0) | ||
| { | ||
| int endOfParameterValue = query.IndexOf('&'); | ||
| int endOfParameterName = query.IndexOf('='); | ||
| bool noValue = false; | ||
|
|
||
| // Check if we have parameter without value | ||
| if ((endOfParameterValue == -1 && endOfParameterName == -1) || | ||
| (endOfParameterValue != -1 && (endOfParameterName == -1 || endOfParameterName > endOfParameterValue))) | ||
| { | ||
| endOfParameterName = endOfParameterValue; | ||
| noValue = true; | ||
| } | ||
|
|
||
| if (endOfParameterName == -1) | ||
| { | ||
| endOfParameterName = query.Length; | ||
| } | ||
|
|
||
| if (endOfParameterValue == -1) | ||
| { | ||
| endOfParameterValue = query.Length; | ||
| } | ||
| else | ||
| { | ||
| // include the separator | ||
| endOfParameterValue++; | ||
| } | ||
|
|
||
| ReadOnlySpan<char> parameterName = query.Slice(0, endOfParameterName); | ||
|
|
||
| bool isAllowed = false; | ||
| foreach (string name in _allowedQueryParameters) | ||
| { | ||
| if (parameterName.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| isAllowed = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| int valueLength = endOfParameterValue; | ||
| int nameLength = endOfParameterName; | ||
|
|
||
| if (isAllowed || noValue) | ||
| { | ||
| if (stringBuilder is null) | ||
| { | ||
| lengthSoFar += valueLength; | ||
| } | ||
| else | ||
| { | ||
| AppendReadOnlySpan(stringBuilder, query.Slice(0, valueLength)); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // Encountered a query value that needs to be redacted. | ||
| // Create the StringBuilder if we haven't already. | ||
| stringBuilder ??= RentStringBuilder(url.Length).Append(url, 0, lengthSoFar); | ||
|
|
||
| AppendReadOnlySpan(stringBuilder, query.Slice(0, nameLength)) | ||
| .Append('=') | ||
| .Append(_redactedPlaceholder); | ||
|
|
||
| if (query[endOfParameterValue - 1] == '&') | ||
| { | ||
| stringBuilder.Append('&'); | ||
| } | ||
| } | ||
|
|
||
| query = query.Slice(valueLength); | ||
| } | ||
|
|
||
| return stringBuilder is null ? url : ToStringAndReturnStringBuilder(stringBuilder); | ||
|
|
||
| static StringBuilder AppendReadOnlySpan(StringBuilder builder, ReadOnlySpan<char> span) | ||
| { | ||
| #if NET6_0_OR_GREATER | ||
| return builder.Append(span); | ||
| #else | ||
| foreach (char c in span) | ||
| { | ||
| builder.Append(c); | ||
| } | ||
|
|
||
| return builder; | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| private static StringBuilder RentStringBuilder(int capacity) | ||
| { | ||
| if (capacity <= MaxCachedStringBuilderCapacity) | ||
| { | ||
| StringBuilder? builder = s_cachedStringBuilder; | ||
| if (builder is not null && builder.Capacity >= capacity) | ||
| { | ||
| s_cachedStringBuilder = null; | ||
| return builder; | ||
| } | ||
| } | ||
|
|
||
| return new StringBuilder(capacity); | ||
| } | ||
|
|
||
| private static string ToStringAndReturnStringBuilder(StringBuilder builder) | ||
| { | ||
| string result = builder.ToString(); | ||
| if (builder.Capacity <= MaxCachedStringBuilderCapacity) | ||
| { | ||
| s_cachedStringBuilder = builder.Clear(); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.