diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index bbe48c3f04a9e..2d56ef86f70f9 100755 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -31,7 +31,6 @@ - @@ -81,7 +80,6 @@ - @@ -95,7 +93,6 @@ - @@ -103,4 +100,12 @@ + + + + + + + + \ No newline at end of file diff --git a/sdk/appconfiguration/Azure.ApplicationModel.Configuration/src/ConfigurationClient.cs b/sdk/appconfiguration/Azure.ApplicationModel.Configuration/src/ConfigurationClient.cs index c67b0c793eeda..2b101ca868623 100644 --- a/sdk/appconfiguration/Azure.ApplicationModel.Configuration/src/ConfigurationClient.cs +++ b/sdk/appconfiguration/Azure.ApplicationModel.Configuration/src/ConfigurationClient.cs @@ -10,7 +10,6 @@ using Azure.Core; using Azure.Core.Http; using Azure.Core.Pipeline; -using Azure.Core.Pipeline.Policies; namespace Azure.ApplicationModel.Configuration { @@ -67,7 +66,8 @@ public ConfigurationClient(string connectionString, ConfigurationClientOptions o /// A controlling the request lifetime. public virtual async Task> AddAsync(string key, string value, string label = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(key)) throw new ArgumentNullException($"{nameof(key)}"); + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException($"{nameof(key)}"); return await AddAsync(new ConfigurationSetting(key, value, label), cancellationToken).ConfigureAwait(false); } @@ -80,7 +80,8 @@ public virtual async Task> AddAsync(string key, s /// A controlling the request lifetime. public virtual Response Add(string key, string value, string label = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(key)) throw new ArgumentNullException($"{nameof(key)}"); + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException($"{nameof(key)}"); return Add(new ConfigurationSetting(key, value, label), cancellationToken); } @@ -91,8 +92,12 @@ public virtual Response Add(string key, string value, stri /// A controlling the request lifetime. public virtual async Task> AddAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateAddRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Add"); + scope.Start(); + + try { + using Request request = CreateAddRequest(setting); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -104,6 +109,11 @@ public virtual async Task> AddAsync(Configuration throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -113,8 +123,13 @@ public virtual async Task> AddAsync(Configuration /// A controlling the request lifetime. public virtual Response Add(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateAddRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Add"); + scope.AddAttribute("key", setting?.Key); + scope.Start(); + + try { + using Request request = CreateAddRequest(setting); Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) @@ -126,6 +141,11 @@ public virtual Response Add(ConfigurationSetting setting, throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateAddRequest(ConfigurationSetting setting) @@ -160,7 +180,8 @@ private Request CreateAddRequest(ConfigurationSetting setting) /// A controlling the request lifetime. public virtual async Task> SetAsync(string key, string value, string label = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(key)) throw new ArgumentNullException($"{nameof(key)}"); + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException($"{nameof(key)}"); return await SetAsync(new ConfigurationSetting(key, value, label), cancellationToken).ConfigureAwait(false); } @@ -173,7 +194,8 @@ public virtual async Task> SetAsync(string key, s /// A controlling the request lifetime. public virtual Response Set(string key, string value, string label = default, CancellationToken cancellationToken = default) { - if (string.IsNullOrEmpty(key)) throw new ArgumentNullException($"{nameof(key)}"); + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException($"{nameof(key)}"); return Set(new ConfigurationSetting(key, value, label), cancellationToken); } @@ -184,8 +206,13 @@ public virtual Response Set(string key, string value, stri /// A controlling the request lifetime. public virtual async Task> SetAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateSetRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Set"); + scope.AddAttribute("key", setting?.Key); + scope.Start(); + + try { + using Request request = CreateSetRequest(setting); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -198,6 +225,11 @@ public virtual async Task> SetAsync(Configuration throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -207,9 +239,15 @@ public virtual async Task> SetAsync(Configuration /// A controlling the request lifetime. public virtual Response Set(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateSetRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Set"); + scope.AddAttribute("key", setting?.Key); + scope.Start(); + + try { - var response = _pipeline.SendRequest(request, cancellationToken); + using Request request = CreateSetRequest(setting); + + Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) { @@ -221,6 +259,11 @@ public virtual Response Set(ConfigurationSetting setting, throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateSetRequest(ConfigurationSetting setting) @@ -283,8 +326,13 @@ public virtual Response Update(string key, string value, s /// A controlling the request lifetime. public virtual async Task> UpdateAsync(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateUpdateRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Update"); + scope.AddAttribute("key", setting?.Key); + scope.Start(); + + try { + using Request request = CreateUpdateRequest(setting); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -295,6 +343,11 @@ public virtual async Task> UpdateAsync(Configurat throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -304,8 +357,13 @@ public virtual async Task> UpdateAsync(Configurat /// A controlling the request lifetime. public virtual Response Update(ConfigurationSetting setting, CancellationToken cancellationToken = default) { - using (Request request = CreateUpdateRequest(setting)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Update"); + scope.AddAttribute("key", setting?.Key); + scope.Start(); + + try { + using Request request = CreateUpdateRequest(setting); Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) @@ -316,6 +374,11 @@ public virtual Response Update(ConfigurationSetting settin throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateUpdateRequest(ConfigurationSetting setting) @@ -357,8 +420,13 @@ private Request CreateUpdateRequest(ConfigurationSetting setting) /// A controlling the request lifetime. public virtual async Task DeleteAsync(string key, string label = default, ETag etag = default, CancellationToken cancellationToken = default) { - using (Request request = CreateDeleteRequest(key, label, etag)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Delete"); + scope.AddAttribute("key", key); + scope.Start(); + + try { + using Request request = CreateDeleteRequest(key, label, etag); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -370,6 +438,11 @@ public virtual async Task DeleteAsync(string key, string label = defau throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -383,8 +456,13 @@ public virtual async Task DeleteAsync(string key, string label = defau /// A controlling the request lifetime. public virtual Response Delete(string key, string label = default, ETag etag = default, CancellationToken cancellationToken = default) { - using (Request request = CreateDeleteRequest(key, label, etag)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Set"); + scope.AddAttribute("key", key); + scope.Start(); + + try { + using Request request = CreateDeleteRequest(key, label, etag); Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) @@ -396,6 +474,11 @@ public virtual Response Delete(string key, string label = default, ETag etag = d throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateDeleteRequest(string key, string label, ETag etag) @@ -424,8 +507,13 @@ private Request CreateDeleteRequest(string key, string label, ETag etag) /// A controlling the request lifetime. public virtual async Task> GetAsync(string key, string label = default, DateTimeOffset acceptDateTime = default, CancellationToken cancellationToken = default) { - using (Request request = CreateGetRequest(key, label, acceptDateTime)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Get"); + scope.AddAttribute("key", key); + scope.Start(); + + try { + using Request request = CreateGetRequest(key, label, acceptDateTime); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -436,6 +524,11 @@ public virtual async Task> GetAsync(string key, s throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -447,6 +540,10 @@ public virtual async Task> GetAsync(string key, s /// A controlling the request lifetime. public virtual Response Get(string key, string label = default, DateTimeOffset acceptDateTime = default, CancellationToken cancellationToken = default) { + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Get"); + scope.AddAttribute(nameof(key), key); + scope.Start(); + using (Request request = CreateGetRequest(key, label, acceptDateTime)) { Response response = _pipeline.SendRequest(request, cancellationToken); @@ -529,8 +626,12 @@ private Request CreateGetRequest(string key, string label, DateTimeOffset accept /// A controlling the request lifetime. private async Task> GetSettingsPageAsync(SettingSelector selector, string pageLink, CancellationToken cancellationToken = default) { - using (Request request = CreateBatchRequest(selector, pageLink)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.GetSettingsPage"); + scope.Start(); + + try { + using Request request = CreateBatchRequest(selector, pageLink); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) @@ -543,6 +644,11 @@ private async Task> GetSettingsPageAsync(Sett throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -553,8 +659,12 @@ private async Task> GetSettingsPageAsync(Sett /// A controlling the request lifetime. private PageResponse GetSettingsPage(SettingSelector selector, string pageLink, CancellationToken cancellationToken = default) { - using (Request request = CreateBatchRequest(selector, pageLink)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.GetSettingsPage"); + scope.Start(); + + try { + using Request request = CreateBatchRequest(selector, pageLink); Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) @@ -567,6 +677,11 @@ private PageResponse GetSettingsPage(SettingSelector selec throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateBatchRequest(SettingSelector selector, string pageLink) @@ -593,8 +708,12 @@ private Request CreateBatchRequest(SettingSelector selector, string pageLink) /// A controlling the request lifetime. private async Task> GetRevisionsPageAsync(SettingSelector selector, string pageLink, CancellationToken cancellationToken = default) { - using (Request request = CreateGetRevisionsRequest(selector, pageLink)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Set"); + scope.Start(); + + try { + using Request request = CreateGetRevisionsRequest(selector, pageLink); Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); switch (response.Status) { @@ -606,6 +725,11 @@ private async Task> GetRevisionsPageAsync(Set throw await response.CreateRequestFailedExceptionAsync().ConfigureAwait(false); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } /// @@ -617,8 +741,12 @@ private async Task> GetRevisionsPageAsync(Set /// A controlling the request lifetime. private PageResponse GetRevisionsPage(SettingSelector selector, string pageLink, CancellationToken cancellationToken = default) { - using (Request request = CreateGetRevisionsRequest(selector, pageLink)) + using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("ConfigurationClient.Set"); + scope.Start(); + + try { + using Request request = CreateGetRevisionsRequest(selector, pageLink); Response response = _pipeline.SendRequest(request, cancellationToken); switch (response.Status) { @@ -630,6 +758,11 @@ private PageResponse GetRevisionsPage(SettingSelector sele throw response.CreateRequestFailedException(); } } + catch (Exception e) + { + scope.Failed(e); + throw; + } } private Request CreateGetRevisionsRequest(SettingSelector selector, string pageLink) diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj index e2f7afc740e2d..395497e9db038 100644 --- a/sdk/core/Azure.Core/src/Azure.Core.csproj +++ b/sdk/core/Azure.Core/src/Azure.Core.csproj @@ -20,6 +20,7 @@ + diff --git a/sdk/core/Azure.Core/src/Pipeline/AzureOperationScope.cs b/sdk/core/Azure.Core/src/Pipeline/AzureOperationScope.cs new file mode 100644 index 0000000000000..bf6c61b3d1b4f --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/AzureOperationScope.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +namespace Azure.Core.Pipeline +{ + public struct DiagnosticScope: IDisposable + { + private Activity _activity; + + private readonly string _name; + + private readonly DiagnosticListener _source; + + internal DiagnosticScope(string name, DiagnosticListener source) + { + _name = name; + _source = source; + _activity = _source.IsEnabled() ? new Activity(_name) : null; + } + + public bool IsEnabled => _activity != null; + + public void AddAttribute(string name, string value) + { + _activity?.AddTag(name, value); + } + + public void AddAttribute(string name, T value) + { + if (_activity != null) + { + AddAttribute(name, value.ToString()); + } + } + + public void AddAttribute(string name, T value, Func format) + { + if (_activity != null) + { + AddAttribute(name, format(value)); + } + } + + public void Start() + { + if (_activity != null && _source.IsEnabled(_name)) + { + _source.StartActivity(_activity, null); + } + } + + public void Dispose() + { + if (_activity == null) + { + return; + } + + if (_source != null) + { + _source.StopActivity(_activity, null); + } + else + { + _activity?.Stop(); + } + } + + public void Failed(Exception e) + { + if (_activity == null) + { + return; + } + + _source?.Write(_activity.OperationName + ".Exception", e); + _activity?.Stop(); + + _activity = null; + + } + } +} diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index 6e93a3fed3dd5..5e3433aadcbfc 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -15,11 +15,13 @@ public class HttpPipeline private readonly ResponseClassifier _responseClassifier; private readonly ReadOnlyMemory _pipeline; - public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[] policies = null, ResponseClassifier responseClassifier = null) + public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[] policies = null, ResponseClassifier responseClassifier = null, ClientDiagnostics clientDiagnostics = null) { _transport = transport ?? throw new ArgumentNullException(nameof(transport)); _responseClassifier = responseClassifier ?? new ResponseClassifier(); + Diagnostics = clientDiagnostics ?? new ClientDiagnostics(true); + policies = policies ?? Array.Empty(); var all = new HttpPipelinePolicy[policies.Length + 1]; @@ -32,6 +34,8 @@ public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[] polici public Request CreateRequest() => _transport.CreateRequest(); + public ClientDiagnostics Diagnostics { get; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task SendRequestAsync(Request request, CancellationToken cancellationToken) { diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs index e4f134f31f5f7..54e13b95d0cfd 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs @@ -40,9 +40,11 @@ public static HttpPipeline Build(ClientOptions options, bool bufferResponse = tr policies.Add(BufferResponsePolicy.Shared); } + policies.Add(new RequestActivityPolicy()); + policies.RemoveAll(policy => policy == null); - return new HttpPipeline(options.Transport, policies.ToArray(), options.ResponseClassifier); + return new HttpPipeline(options.Transport, policies.ToArray(), options.ResponseClassifier, new ClientDiagnostics(options.Diagnostics.IsLoggingEnabled)); } // internal for testing diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineDiagnostics.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineDiagnostics.cs new file mode 100644 index 0000000000000..a3370a3eba238 --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineDiagnostics.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace Azure.Core.Pipeline +{ + public sealed class ClientDiagnostics + { + private readonly bool _isActivityEnabled; + + public ClientDiagnostics(bool isActivityEnabled) + { + _isActivityEnabled = isActivityEnabled; + } + + private static readonly DiagnosticListener s_source = new DiagnosticListener("Azure.Clients"); + + public DiagnosticScope CreateScope(string name) + { + if (!_isActivityEnabled) + { + return default; + } + return new DiagnosticScope(name, s_source); + } + } +} diff --git a/sdk/core/Azure.Core/src/Pipeline/Policies/RequestActivityPolicy.cs b/sdk/core/Azure.Core/src/Pipeline/Policies/RequestActivityPolicy.cs new file mode 100644 index 0000000000000..958483f2e9067 --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/Policies/RequestActivityPolicy.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Threading.Tasks; + +namespace Azure.Core.Pipeline.Policies +{ + internal class RequestActivityPolicy : HttpPipelinePolicy + { + private const string TraceParentHeaderName = "traceparent"; + private const string TraceStateHeaderName = "tracestate"; + private const string RequestIdHeaderName = "Request-Id"; + + public static RequestActivityPolicy Shared { get; } = new RequestActivityPolicy(); + + private static readonly DiagnosticListener s_diagnosticSource = new DiagnosticListener("Azure.Pipeline"); + + public override Task ProcessAsync(HttpPipelineMessage message, ReadOnlyMemory pipeline) + { + return ProcessAsync(message, pipeline, true); + } + + public override void Process(HttpPipelineMessage message, ReadOnlyMemory pipeline) + { + ProcessAsync(message, pipeline, false).EnsureCompleted(); + } + + private static async Task ProcessAsync(HttpPipelineMessage message, ReadOnlyMemory pipeline, bool isAsync) + { + if (!s_diagnosticSource.IsEnabled()) + { + await ProcessNextAsync(message, pipeline, isAsync).ConfigureAwait(false); + + return; + } + + var activity = new Activity("Azure.Core.Http.Request"); + activity.AddTag("http.method", message.Request.Method.Method); + activity.AddTag("http.url", message.Request.UriBuilder.ToString()); + if (message.Request.Headers.TryGetValue("User-Agent", out string userAgent)) + { + activity.AddTag("http.user_agent", userAgent); + } + + var diagnosticSourceActivityEnabled = s_diagnosticSource.IsEnabled(activity.OperationName, message); + + if (diagnosticSourceActivityEnabled) + { + s_diagnosticSource.StartActivity(activity, message); + } + else + { + activity.Start(); + } + + + if (isAsync) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } + + activity.AddTag("http.status_code", message.Response.Status.ToString(CultureInfo.InvariantCulture)); + + if (diagnosticSourceActivityEnabled) + { + s_diagnosticSource.StopActivity(activity, message); + } + else + { + activity.Stop(); + } + } + + private static async Task ProcessNextAsync(HttpPipelineMessage message, ReadOnlyMemory pipeline, bool isAsync) + { + var currentActivity = Activity.Current; + + if (currentActivity != null) + { + if (currentActivity.IdFormat == ActivityIdFormat.W3C) + { + if (!message.Request.Headers.Contains(TraceParentHeaderName)) + { + message.Request.Headers.Add(TraceParentHeaderName, currentActivity.Id); + if (currentActivity.TraceStateString != null) + { + message.Request.Headers.Add(TraceStateHeaderName, currentActivity.TraceStateString); + } + } + } + else + { + if (!message.Request.Headers.Contains(RequestIdHeaderName)) + { + message.Request.Headers.Add(RequestIdHeaderName, currentActivity.Id); + } + } + } + + if (isAsync) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/ClientDiagnosticsTests.cs b/sdk/core/Azure.Core/tests/ClientDiagnosticsTests.cs new file mode 100644 index 0000000000000..2bfeb7a8828b8 --- /dev/null +++ b/sdk/core/Azure.Core/tests/ClientDiagnosticsTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Azure.Core.Pipeline; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class ClientDiagnosticsTests + { + [Test] + public void CreatesActivityWithNameAndTags() + { + + using var testListener = new TestDiagnosticListener("Azure.Clients"); + ClientDiagnostics clientDiagnostics = new ClientDiagnostics(true); + + DiagnosticScope scope = clientDiagnostics.CreateScope("ActivityName"); + + scope.AddAttribute("Attribute1", "Value1"); + scope.AddAttribute("Attribute2", 2, i => i.ToString()); + scope.AddAttribute("Attribute3", 3); + + scope.Start(); + + KeyValuePair startEvent = testListener.Events.Dequeue(); + + Activity activity = Activity.Current; + + scope.Dispose(); + + KeyValuePair stopEvent = testListener.Events.Dequeue(); + + Assert.Null(Activity.Current); + Assert.AreEqual("ActivityName.Start", startEvent.Key); + Assert.AreEqual("ActivityName.Stop", stopEvent.Key); + + CollectionAssert.Contains(activity.Tags, new KeyValuePair("Attribute1", "Value1")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("Attribute2", "2")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("Attribute3", "3")); + } + + [Test] + public void FailedStopsActivityAndWritesExceptionEvent() + { + using var testListener = new TestDiagnosticListener("Azure.Clients"); + ClientDiagnostics clientDiagnostics = new ClientDiagnostics(true); + + DiagnosticScope scope = clientDiagnostics.CreateScope("ActivityName"); + + scope.AddAttribute("Attribute1", "Value1"); + scope.AddAttribute("Attribute2", 2, i => i.ToString()); + + scope.Start(); + + KeyValuePair startEvent = testListener.Events.Dequeue(); + + Activity activity = Activity.Current; + + var exception = new Exception(); + scope.Failed(exception); + scope.Dispose(); + + KeyValuePair stopEvent = testListener.Events.Dequeue(); + + Assert.Null(Activity.Current); + Assert.AreEqual("ActivityName.Start", startEvent.Key); + Assert.AreEqual("ActivityName.Exception", stopEvent.Key); + Assert.AreEqual(exception, stopEvent.Value); + Assert.AreEqual(0, testListener.Events.Count); + + CollectionAssert.Contains(activity.Tags, new KeyValuePair("Attribute1", "Value1")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("Attribute2", "2")); + } + + [Test] + public void NoopsWhenDisabled() + { + ClientDiagnostics clientDiagnostics = new ClientDiagnostics(false); + DiagnosticScope scope = clientDiagnostics.CreateScope(""); + + scope.AddAttribute("Attribute1", "Value1"); + scope.AddAttribute("Attribute2", 2, i => i.ToString()); + scope.Failed(new Exception()); + scope.Dispose(); + } + } +} diff --git a/sdk/core/Azure.Core/tests/RequestActivityPolicyTests.cs b/sdk/core/Azure.Core/tests/RequestActivityPolicyTests.cs new file mode 100644 index 0000000000000..7de97844d65d3 --- /dev/null +++ b/sdk/core/Azure.Core/tests/RequestActivityPolicyTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Azure.Core.Http; +using Azure.Core.Pipeline; +using Azure.Core.Pipeline.Policies; +using Azure.Core.Testing; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class RequestActivityPolicyTests : SyncAsyncPolicyTestBase + { + public RequestActivityPolicyTests(bool isAsync) : base(isAsync) + { + } + + [Test] + [NonParallelizable] + public async Task ActivityIsCreatedForRequest() + { + Activity activity = null; + KeyValuePair startEvent = default; + using var testListener = new TestDiagnosticListener("Azure.Pipeline"); + + MockTransport mockTransport = CreateMockTransport(_ => + { + activity = Activity.Current; + startEvent = testListener.Events.Dequeue(); + return new MockResponse(201); + }); + + using Request request = mockTransport.CreateRequest(); + request.Method = RequestMethod.Get; + request.UriBuilder.Uri = new Uri("http://example.com"); + request.Headers.Add("User-Agent", "agent"); + + Task requestTask = SendRequestAsync(mockTransport, request, RequestActivityPolicy.Shared); + + await requestTask; + + KeyValuePair stopEvent = testListener.Events.Dequeue(); + + Assert.AreEqual("Azure.Core.Http.Request.Start", startEvent.Key); + Assert.AreEqual("Azure.Core.Http.Request.Stop", stopEvent.Key); + + Assert.AreEqual("Azure.Core.Http.Request", activity.OperationName); + + CollectionAssert.Contains(activity.Tags, new KeyValuePair("http.status_code", "201")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("http.url", "http://example.com/")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("http.method", "GET")); + CollectionAssert.Contains(activity.Tags, new KeyValuePair("http.user_agent", "agent")); + } + + [Test] + [NonParallelizable] + public async Task CurrentActivityIsInjectedIntoRequest() + { + var transport = new MockTransport(new MockResponse(200)); + + var activity = new Activity("Dummy"); + + activity.Start(); + + await SendGetRequest(transport, RequestActivityPolicy.Shared); + + activity.Stop(); + + Assert.True(transport.SingleRequest.TryGetHeader("Request-Id", out string requestId)); + Assert.AreEqual(activity.Id, requestId); + } + + [Test] + [NonParallelizable] + public async Task CurrentActivityIsInjectedIntoRequestW3C() + { + var previousFormat = Activity.DefaultIdFormat; + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + try + { + var transport = new MockTransport(new MockResponse(200)); + + var activity = new Activity("Dummy"); + + activity.Start(); + activity.TraceStateString = "trace"; + + await SendGetRequest(transport, RequestActivityPolicy.Shared); + + activity.Stop(); + + Assert.True(transport.SingleRequest.TryGetHeader("traceparent", out string requestId)); + Assert.AreEqual(activity.Id, requestId); + + Assert.True(transport.SingleRequest.TryGetHeader("tracestate", out string traceState)); + Assert.AreEqual("trace", traceState); + } + finally + { + Activity.DefaultIdFormat = previousFormat; + } + } + + [Test] + [NonParallelizable] + public async Task PassesMessageIntoIsEnabledStartAndStopEvents() + { + using var testListener = new TestDiagnosticListener("Azure.Pipeline"); + + var transport = new MockTransport(new MockResponse(200)); + + await SendGetRequest(transport, RequestActivityPolicy.Shared); + + KeyValuePair startEvent = testListener.Events.Dequeue(); + KeyValuePair stopEvent = testListener.Events.Dequeue(); + var isEnabledCall = testListener.IsEnabledCalls.Dequeue(); + + Assert.AreEqual("Azure.Core.Http.Request.Start", startEvent.Key); + Assert.IsInstanceOf(startEvent.Value); + + Assert.AreEqual("Azure.Core.Http.Request.Stop", stopEvent.Key); + Assert.IsInstanceOf(stopEvent.Value); + + Assert.AreEqual("Azure.Core.Http.Request", isEnabledCall.Item1); + Assert.IsInstanceOf(isEnabledCall.Item2); + } + + } +} diff --git a/sdk/core/Azure.Core/tests/TestDiagnosticListener.cs b/sdk/core/Azure.Core/tests/TestDiagnosticListener.cs new file mode 100644 index 0000000000000..1c42a40913db4 --- /dev/null +++ b/sdk/core/Azure.Core/tests/TestDiagnosticListener.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Azure.Core.Tests +{ + public class TestDiagnosticListener : IObserver, IObserver>, IDisposable + { + private readonly string _diagnosticSourceName; + + private readonly List _subscriptions = new List(); + + public Queue> Events { get; } = new Queue>(); + + public Queue<(string, object, object)> IsEnabledCalls { get; } = new Queue<(string, object, object)>(); + + public TestDiagnosticListener(string diagnosticSourceName) + { + _diagnosticSourceName = diagnosticSourceName; + DiagnosticListener.AllListeners.Subscribe(this); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } + + public void OnNext(KeyValuePair value) + { + Events.Enqueue(value); + } + + public void OnNext(DiagnosticListener value) + { + if (value.Name == _diagnosticSourceName) + { + _subscriptions.Add(value.Subscribe(this, IsEnabled)); + } + } + + private bool IsEnabled(string arg1, object arg2, object arg3) + { + IsEnabledCalls.Enqueue((arg1, arg2, arg3)); + return true; + } + + public void Dispose() + { + foreach (IDisposable subscription in _subscriptions) + { + subscription.Dispose(); + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncPolicyTestBase.cs b/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncPolicyTestBase.cs index ef74ff10fbd20..eb88f7f029170 100644 --- a/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncPolicyTestBase.cs +++ b/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncPolicyTestBase.cs @@ -23,17 +23,20 @@ protected Task SendRequestAsync(HttpPipeline pipeline, Request request return IsAsync ? pipeline.SendRequestAsync(request, cancellationToken) : Task.FromResult(pipeline.SendRequest(request, cancellationToken)); } - protected async Task SendGetRequest(HttpPipelineTransport transport, HttpPipelinePolicy policy, ResponseClassifier responseClassifier = null) + protected async Task SendRequestAsync(HttpPipelineTransport transport, Request request, HttpPipelinePolicy policy, ResponseClassifier responseClassifier = null) { await Task.Yield(); - using (Request request = transport.CreateRequest()) - { - request.Method = RequestMethod.Get; - request.UriBuilder.Uri = new Uri("http://example.com"); - var pipeline = new HttpPipeline(transport, new [] { policy }, responseClassifier); - return await SendRequestAsync(pipeline, request, CancellationToken.None); - } + var pipeline = new HttpPipeline(transport, new [] { policy }, responseClassifier); + return await SendRequestAsync(pipeline, request, CancellationToken.None); + } + + protected async Task SendGetRequest(HttpPipelineTransport transport, HttpPipelinePolicy policy, ResponseClassifier responseClassifier = null) + { + using Request request = transport.CreateRequest(); + request.Method = RequestMethod.Get; + request.UriBuilder.Uri = new Uri("http://example.com"); + return await SendRequestAsync(transport, request, policy, responseClassifier); } } } diff --git a/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncTestBase.cs b/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncTestBase.cs index 0ac57e84ed013..b52a8db6e8c64 100644 --- a/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncTestBase.cs +++ b/sdk/core/Azure.Core/tests/TestFramework/SyncAsyncTestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.IO; namespace Azure.Core.Testing @@ -22,6 +23,14 @@ protected MockTransport CreateMockTransport() }; } + protected MockTransport CreateMockTransport(Func responseFactory) + { + return new MockTransport(responseFactory) + { + ExpectSyncPipeline = !IsAsync + }; + } + protected MockTransport CreateMockTransport(params MockResponse[] responses) { return new MockTransport(responses) diff --git a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs index 1bfca87e6cf68..247d523fc842d 100644 --- a/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs +++ b/sdk/storage/Azure.Storage.Blobs/src/Generated/BlobRestClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for // license information.