From 1b2ace3946299b00b82e16e4f1858942e449fa84 Mon Sep 17 00:00:00 2001 From: Rajkumar Rangaraj Date: Thu, 3 Sep 2020 17:33:07 -0700 Subject: [PATCH] Ingestion service data delivery status (#1887) * Added eventhandler to transmission * Updated public API * Modified changelog.md * Added more tests * Fix API * Added coverage for timeout * Update to API * Modified comment in transmission. * PR Comments * PR feedback * Fix test * Added TransmissionStatusEvent to InMemoryChannel. * Remove Inmemory change Co-authored-by: Cijo Thomas Co-authored-by: Timothy Mothra --- .../net452/PublicAPI.Unshipped.txt | 2 + .../netstandard2.0/PublicAPI.Unshipped.txt | 2 + .../net452/PublicAPI.Unshipped.txt | 7 +- .../net46/PublicAPI.Unshipped.txt | 7 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 7 +- .../Channel/TransmissionTest.cs | 306 +++++++++++++++++- .../Implementation/TelemetrySerializerTest.cs | 29 ++ .../Channel/Transmission.cs | 23 +- .../Channel/TransmissionStatusEventArgs.cs | 25 ++ .../Implementation/Tracing/CoreEventSource.cs | 13 + .../Implementation/TelemetrySerializer.cs | 8 +- .../ServerTelemetryChannel.cs | 16 + CHANGELOG.md | 1 + 13 files changed, 436 insertions(+), 10 deletions(-) create mode 100644 BASE/src/Microsoft.ApplicationInsights/Channel/TransmissionStatusEventArgs.cs diff --git a/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/net452/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/net452/PublicAPI.Unshipped.txt index e69de29bb2..c7a05e0ea5 100644 --- a/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/net452/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/net452/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.ServerTelemetryChannel.TransmissionStatusEvent.get -> System.EventHandler +Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.ServerTelemetryChannel.TransmissionStatusEvent.set -> void \ No newline at end of file diff --git a/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/netstandard2.0/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2..c7a05e0ea5 100644 --- a/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/netstandard2.0/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.AI.ServerTelemetryChannel.dll/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.ServerTelemetryChannel.TransmissionStatusEvent.get -> System.EventHandler +Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel.ServerTelemetryChannel.TransmissionStatusEvent.set -> void \ No newline at end of file diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt index 5daa71104f..6fb298a3f3 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ -Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration \ No newline at end of file +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.TransmissionStatusEventArgs(Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper response) -> void +Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.get -> System.EventHandler +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.set -> void +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.Response.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper \ No newline at end of file diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt index 5daa71104f..6fb298a3f3 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ -Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration \ No newline at end of file +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.TransmissionStatusEventArgs(Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper response) -> void +Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.get -> System.EventHandler +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.set -> void +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.Response.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper \ No newline at end of file diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt index 5daa71104f..6fb298a3f3 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ -Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration \ No newline at end of file +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.TransmissionStatusEventArgs(Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper response) -> void +Microsoft.ApplicationInsights.TelemetryClient.TelemetryConfiguration.get -> Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.get -> System.EventHandler +Microsoft.ApplicationInsights.Channel.Transmission.TransmissionStatusEvent.set -> void +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs +Microsoft.ApplicationInsights.Channel.TransmissionStatusEventArgs.Response.get -> Microsoft.ApplicationInsights.Extensibility.Implementation.HttpWebResponseWrapper \ No newline at end of file diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Channel/TransmissionTest.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Channel/TransmissionTest.cs index 1b1374e51d..d1b4c922bb 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Channel/TransmissionTest.cs +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Channel/TransmissionTest.cs @@ -17,6 +17,9 @@ using System.CodeDom; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; using System.Diagnostics.Tracing; + using System.Runtime.Serialization; + using System.Text; + using System.Runtime.Serialization.Json; public class TransmissionTest { @@ -403,13 +406,13 @@ public async Task SendAsyncLogsIngestionReponseTimeEventCounter() var payload = (IDictionary)traces[0].Payload[0]; Assert.AreEqual("IngestionEndpoint-ResponseTimeMsec", payload["Name"].ToString()); Assert.IsTrue((int)payload["Count"] >= 5); - // Mean should be more than 30 ms, as we introduced a delay of 30ms in SendAsync. + // Max should be more than 30 ms, as we introduced a delay of 30ms in SendAsync. #if NETCOREAPP2_1 - Assert.IsTrue((float)payload["Mean"] >= 30); + Assert.IsTrue((float)payload["Max"] >= 30); #endif #if NETCOREAPP3_1 - Assert.IsTrue((double)payload["Mean"] >= 30); + Assert.IsTrue((double)payload["Max"] >= 30); #endif } } @@ -501,6 +504,303 @@ public async Task SendAsyncLogsIngestionReponseTimeAndStatusCode() } } } + + [TestMethod] + public async Task TestTransmissionStatusEventHandlerWithSuccessTransmission() + { + // ARRANGE + var handler = new HandlerForFakeHttpClient + { + InnerHandler = new HttpClientHandler(), + OnSendAsync = (req, cancellationToken) => + { + return Task.FromResult(new HttpResponseMessage()); + } + }; + + using (var fakeHttpClient = new HttpClient(handler)) + { + // Instantiate Transmission with the mock HttpClient + Transmission transmission = new Transmission(testUri, new byte[] { 1, 2, 3, 4, 5 }, fakeHttpClient, string.Empty, string.Empty); + + // VALIDATE + transmission.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) + { + Assert.IsTrue(sender is Transmission); + Assert.AreEqual((int)HttpStatusCode.OK, args.Response.StatusCode); + }; + + // ACT + HttpWebResponseWrapper result = await transmission.SendAsync(); + } + } + + [TestMethod] + public async Task TestTransmissionStatusEventHandlerWithKnownFailureTransmission() + { + // ARRANGE + var handler = new HandlerForFakeHttpClient + { + InnerHandler = new HttpClientHandler(), + OnSendAsync = (req, cancellationToken) => + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); + } + }; + + using (var fakeHttpClient = new HttpClient(handler)) + { + // Instantiate Transmission with the mock HttpClient + Transmission transmission = new Transmission(testUri, new byte[] { 1, 2, 3, 4, 5 }, fakeHttpClient, string.Empty, string.Empty); + transmission.Timeout = TimeSpan.Zero; + + // VALIDATE + transmission.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) + { + Assert.AreEqual((int)HttpStatusCode.RequestTimeout, args.Response.StatusCode); + }; + + // ACT + HttpWebResponseWrapper result = await transmission.SendAsync(); + } + } + + [TestMethod] + public async Task TestTransmissionStatusEventHandlerWithUnKnownFailureTransmission() + { + // ARRANGE + var handler = new HandlerForFakeHttpClient + { + InnerHandler = new HttpClientHandler(), + OnSendAsync = (req, cancellationToken) => + { + throw new Exception("test"); + } + }; + + using (var fakeHttpClient = new HttpClient(handler)) + { + // Instantiate Transmission with the mock HttpClient + Transmission transmission = new Transmission(testUri, new byte[] { 1, 2, 3, 4, 5 }, fakeHttpClient, string.Empty, string.Empty); + transmission.Timeout = TimeSpan.Zero; + + // VALIDATE + transmission.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) + { + Assert.AreEqual(999, args.Response.StatusCode); + }; + + // ACT + try + { + HttpWebResponseWrapper result = await transmission.SendAsync(); + } + catch (Exception ex) + { + Assert.AreEqual("test", ex.Message); + } + } + } + + [TestMethod] + public async Task TestTransmissionStatusEventHandlerFails() + { + // ARRANGE + var handler = new HandlerForFakeHttpClient + { + InnerHandler = new HttpClientHandler(), + OnSendAsync = (req, cancellationToken) => + { + return Task.FromResult(new HttpResponseMessage()); + } + }; + + using (var listener = new TestEventListener()) + { + listener.EnableEvents(CoreEventSource.Log, EventLevel.LogAlways, + (EventKeywords)AllKeywords); + + using (var fakeHttpClient = new HttpClient(handler)) + { + // Instantiate Transmission with the mock HttpClient + Transmission transmission = new Transmission(testUri, new byte[] { 1, 2, 3, 4, 5 }, fakeHttpClient, string.Empty, string.Empty); + + // VALIDATE + transmission.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) + { + throw new Exception("test"); + }; + + // ACT + HttpWebResponseWrapper result = await transmission.SendAsync(); + } + + // Assert: + var allTraces = listener.Messages.ToList(); + var traces = allTraces.Where(item => item.EventId == 71).ToList(); + Assert.AreEqual(1, traces.Count); + } + } + + [TestMethod] + public async Task TestTransmissionStatusEventWithEventsFromMultipleIKey() + { + // ARRANGE + // Raw response from backend for partial response + var ingestionResponse = "{" + + "\r\n \"itemsReceived\": 5,\r\n \"itemsAccepted\": 2,\r\n " + + "\"errors\": [\r\n {\r\n " + + "\"index\": 0,\r\n \"statusCode\": 400,\r\n \"message\": \"Error 1\"\r\n },\r\n {\r\n " + + "\"index\": 2,\r\n \"statusCode\": 503,\r\n \"message\": \"Error 2\"\r\n },\r\n {\r\n " + + "\"index\": 3,\r\n \"statusCode\": 500,\r\n \"message\": \"Error 3\"\r\n }\r\n ]\r\n}"; + + // Fake HttpClient will respond back with partial content + var handler = new HandlerForFakeHttpClient + { + InnerHandler = new HttpClientHandler(), + OnSendAsync = (req, cancellationToken) => + { + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.PartialContent, Content = new StringContent(ingestionResponse) }); + } + }; + + using (var fakeHttpClient = new HttpClient(handler)) + { + // Create a list of telemetry which could send information to different instrumentation keys + var telemetryItems = new List(); + + EventTelemetry eventTelemetry1 = new EventTelemetry("Event1"); + eventTelemetry1.Context.InstrumentationKey = "IKEY_1"; + telemetryItems.Add(eventTelemetry1); + + EventTelemetry eventTelemetry2 = new EventTelemetry("Event2"); + eventTelemetry2.Context.InstrumentationKey = "IKEY_2"; + telemetryItems.Add(eventTelemetry2); + + EventTelemetry eventTelemetry3 = new EventTelemetry("Event3"); + eventTelemetry3.Context.InstrumentationKey = "IKEY_3"; + telemetryItems.Add(eventTelemetry3); + + EventTelemetry eventTelemetry4 = new EventTelemetry("Event3"); + eventTelemetry4.Context.InstrumentationKey = "IKEY_2"; + telemetryItems.Add(eventTelemetry4); + + EventTelemetry eventTelemetry5 = new EventTelemetry("Event5"); + eventTelemetry5.Context.InstrumentationKey = "IKEY_1"; + telemetryItems.Add(eventTelemetry5); + + // Serialize the telemetry items before passing to transmission + var serializedData = JsonSerializer.Serialize(telemetryItems, true); + + // Instantiate Transmission with the mock HttpClient + Transmission transmission = new Transmission(testUri, serializedData, fakeHttpClient, string.Empty, string.Empty); + + // VALIDATE + transmission.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) + { + var sendertransmission = sender as Transmission; + // convert raw JSON response to Backendresponse object + BackendResponse backendResponse = GetBackendResponse(args.Response.Content); + + // Deserialize telemetry items to identify which items has failed + string[] items = JsonSerializer + .Deserialize(sendertransmission.Content) + .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + + string[] failedItems = new string[3]; + int i = 0; + + // Create a list of failed items + foreach (var error in backendResponse.Errors) + { + failedItems[i++] = items[error.Index]; + } + + Assert.AreEqual((int)HttpStatusCode.PartialContent, args.Response.StatusCode); + Assert.AreEqual(5, backendResponse.ItemsReceived); + Assert.AreEqual(2, backendResponse.ItemsAccepted); + + //IKEY_1 + int totalItemsForIkey = items.Where(x => x.Contains("IKEY_1")).Count(); + int failedItemsForIkey = failedItems.Where(x => x.Contains("IKEY_1")).Count(); + Assert.AreEqual(2, totalItemsForIkey); + Assert.AreEqual(1, failedItemsForIkey); + + //IKEY_2 + totalItemsForIkey = items.Where(x => x.Contains("IKEY_2")).Count(); + failedItemsForIkey = failedItems.Where(x => x.Contains("IKEY_2")).Count(); + Assert.AreEqual(2, totalItemsForIkey); + Assert.AreEqual(1, failedItemsForIkey); + + //IKEY_3 + totalItemsForIkey = items.Where(x => x.Contains("IKEY_3")).Count(); + failedItemsForIkey = failedItems.Where(x => x.Contains("IKEY_3")).Count(); + Assert.AreEqual(1, totalItemsForIkey); + Assert.AreEqual(1, failedItemsForIkey); + }; + + // ACT + HttpWebResponseWrapper result = await transmission.SendAsync(); + } + } + + /// + /// Serializes response from ingestion service to BackendResponse object. + /// + /// Response from ingestion service. + /// + private BackendResponse GetBackendResponse(string responseContent) + { + BackendResponse backendResponse = null; + DataContractJsonSerializer Serializer = new DataContractJsonSerializer(typeof(BackendResponse)); + + try + { + if (!string.IsNullOrEmpty(responseContent)) + { + using (MemoryStream ms = new MemoryStream(Encoding.Unicode.GetBytes(responseContent))) + { + backendResponse = Serializer.ReadObject(ms) as BackendResponse; + } + } + } + catch + { + backendResponse = null; + } + + return backendResponse; + } + } + } + + /// + /// DataContract class to hold response from ingestion service. + /// + [DataContract] + internal class BackendResponse + { + [DataMember(Name = "itemsReceived", IsRequired = true)] + public int ItemsReceived { get; set; } + + [DataMember(Name = "itemsAccepted", IsRequired = true)] + public int ItemsAccepted { get; set; } + + [DataMember(Name = "errors")] + public Error[] Errors { get; set; } + + [DataContract] + internal class Error + { + [DataMember(Name = "index")] + public int Index { get; set; } + + [DataMember(Name = "statusCode")] + public int StatusCode { get; set; } + + [DataMember(Name = "message")] + public string Message { get; set; } } } diff --git a/BASE/Test/ServerTelemetryChannel.Test/TelemetryChannel.Tests/Implementation/TelemetrySerializerTest.cs b/BASE/Test/ServerTelemetryChannel.Test/TelemetryChannel.Tests/Implementation/TelemetrySerializerTest.cs index 3064b93ca2..1b45e6acb9 100644 --- a/BASE/Test/ServerTelemetryChannel.Test/TelemetryChannel.Tests/Implementation/TelemetrySerializerTest.cs +++ b/BASE/Test/ServerTelemetryChannel.Test/TelemetryChannel.Tests/Implementation/TelemetrySerializerTest.cs @@ -165,6 +165,35 @@ public void DoesNotContinueAsyncOperationsOnCapturedSynchronizationContextToImpr Assert.IsFalse(postedBack); } + [TestMethod] + public void EnqueuesTransmissionWithSetTransmissionStatusEvent() + { + Transmission transmission = null; + var transmitter = new StubTransmitter(); + transmitter.OnEnqueue = t => + { + transmission = t; + }; + + var serializer = new TelemetrySerializer(transmitter) { EndpointAddress = new Uri("http://expected.uri") }; + // Set TransmissionStatusEvent does not change the behavior, only wires up event to transmission. + serializer.TransmissionStatusEvent += delegate (object sender, TransmissionStatusEventArgs args) { }; + serializer.Serialize(new[] { new StubSerializableTelemetry() }); + + Assert.AreEqual(serializer.EndpointAddress, transmission.EndpointAddress); + Assert.AreEqual("application/x-json-stream", transmission.ContentType); + Assert.AreEqual("gzip", transmission.ContentEncoding); + + var expectedContent = "{" + + "\"name\":\"StubTelemetryName\"," + + "\"time\":\"0001-01-01T00:00:00.0000000Z\"," + + "\"data\":{\"baseType\":\"StubTelemetryBaseType\"," + + "\"baseData\":{}" + + "}" + + "}"; + Assert.AreEqual(expectedContent, Unzip(transmission.Content)); + } + private static string Unzip(byte[] content) { var memoryStream = new MemoryStream(content); diff --git a/BASE/src/Microsoft.ApplicationInsights/Channel/Transmission.cs b/BASE/src/Microsoft.ApplicationInsights/Channel/Transmission.cs index 24ed335d0d..56d8d3dcbb 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Channel/Transmission.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Channel/Transmission.cs @@ -21,7 +21,7 @@ public class Transmission private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(100); private static HttpClient client = new HttpClient() { Timeout = System.Threading.Timeout.InfiniteTimeSpan }; - + private int isSending; /// @@ -40,7 +40,7 @@ public class Transmission /// /// Initializes a new instance of the class. - /// + /// public Transmission(Uri address, ICollection telemetryItems, TimeSpan timeout = default(TimeSpan)) : this(address, JsonSerializer.Serialize(telemetryItems, true), JsonSerializer.ContentType, JsonSerializer.CompressionType, timeout) { @@ -71,6 +71,11 @@ protected internal Transmission() { } + /// + /// Gets or Sets an event notification to track ingestion endpoint response. + /// + public EventHandler TransmissionStatusEvent { get; set; } + /// /// Gets the Address of the endpoint to which transmission will be sent. /// @@ -209,12 +214,24 @@ public virtual async Task SendAsync() } } catch (OperationCanceledException) - { + { wrapper = new HttpWebResponseWrapper { StatusCode = (int)HttpStatusCode.RequestTimeout, }; } + finally + { + try + { + // Initiates event notification to subscriber with Transmission and TransmissionStatusEventArgs. + this.TransmissionStatusEvent?.Invoke(this, new TransmissionStatusEventArgs(wrapper ?? new HttpWebResponseWrapper() { StatusCode = 999 })); + } + catch (Exception ex) + { + CoreEventSource.Log.TransmissionStatusEventFailed(ex); + } + } return wrapper; } diff --git a/BASE/src/Microsoft.ApplicationInsights/Channel/TransmissionStatusEventArgs.cs b/BASE/src/Microsoft.ApplicationInsights/Channel/TransmissionStatusEventArgs.cs new file mode 100644 index 0000000000..e1c914eddd --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Channel/TransmissionStatusEventArgs.cs @@ -0,0 +1,25 @@ +namespace Microsoft.ApplicationInsights.Channel +{ + using System; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + /// + /// Event argument to track response from ingestion endpoint. + /// + public class TransmissionStatusEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// Response from ingestion endpoint. + public TransmissionStatusEventArgs(HttpWebResponseWrapper response) + { + this.Response = response; + } + + /// + /// Gets the response from ingestion endpoint. + /// + public HttpWebResponseWrapper Response { get; } + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs index 2dc3feadf3..3a8e0da409 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs @@ -1,5 +1,6 @@ namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing { + using System; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; @@ -652,6 +653,18 @@ public void IngestionResponseTimeEventCounter(float responseDurationInMs) [Event(70, Message = "Updating Exception has failed. Error: {0}", Level = EventLevel.Error)] public void UpdateDataFailed(string error, string appDomainName = "Incorrect") => this.WriteEvent(70, error, this.nameProvider.Name); + [Event(71, Keywords = Keywords.UserActionable, Message = "TransmissionStatusEvent has failed. Error: {0}. Monitoring will continue.", Level = EventLevel.Error)] + public void TransmissionStatusEventError(string error, string appDomainName = "Incorrect") => this.WriteEvent(71, error, this.nameProvider.Name); + + [NonEvent] + public void TransmissionStatusEventFailed(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1))) + { + this.TransmissionStatusEventError(ex.ToInvariantString()); + } + } + /// /// Keywords for the PlatformEventSource. /// diff --git a/BASE/src/ServerTelemetryChannel/Implementation/TelemetrySerializer.cs b/BASE/src/ServerTelemetryChannel/Implementation/TelemetrySerializer.cs index e25f373da1..6848f8e1ed 100644 --- a/BASE/src/ServerTelemetryChannel/Implementation/TelemetrySerializer.cs +++ b/BASE/src/ServerTelemetryChannel/Implementation/TelemetrySerializer.cs @@ -26,6 +26,11 @@ public Uri EndpointAddress set { this.endpoindAddress = value ?? throw new ArgumentNullException(nameof(this.EndpointAddress)); } } + /// + /// Gets or Sets the subscriber to an event with Transmission and HttpWebResponseWrapper. + /// + public EventHandler TransmissionStatusEvent { get; set; } + public virtual void Serialize(ICollection items) { if (items == null) @@ -43,7 +48,8 @@ public virtual void Serialize(ICollection items) throw new Exception("TelemetrySerializer.EndpointAddress was not set."); } - var transmission = new Transmission(this.EndpointAddress, items); + var transmission = new Transmission(this.EndpointAddress, items) + { TransmissionStatusEvent = this.TransmissionStatusEvent }; this.Transmitter.Enqueue(transmission); } } diff --git a/BASE/src/ServerTelemetryChannel/ServerTelemetryChannel.cs b/BASE/src/ServerTelemetryChannel/ServerTelemetryChannel.cs index fca04c6dc9..b58398c91a 100644 --- a/BASE/src/ServerTelemetryChannel/ServerTelemetryChannel.cs +++ b/BASE/src/ServerTelemetryChannel/ServerTelemetryChannel.cs @@ -121,6 +121,22 @@ public string EndpointAddress set { this.TelemetrySerializer.EndpointAddress = new Uri(value); } } + /// + /// Gets or Sets the subscriber to an event with Transmission and HttpWebResponseWrapper. + /// + public EventHandler TransmissionStatusEvent + { + get + { + return this.TelemetrySerializer.TransmissionStatusEvent; + } + + set + { + this.TelemetrySerializer.TransmissionStatusEvent = value; + } + } + /// /// Gets or sets the maximum telemetry batching interval. Once the interval expires, /// serializes the accumulated telemetry items for transmission. diff --git a/CHANGELOG.md b/CHANGELOG.md index eecfd09217..4cec1b588d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [ServerTelemetryChannel does not fall back to any default directory if user explicitly configures StorageFolder, and have trouble read/write to it](https://github.com/microsoft/ApplicationInsights-dotnet/pull/2002) - [Fixed a bug which caused ApplicationInsights.config file being read for populating TelemetryConfiguration in .NET Core projects](https://github.com/microsoft/ApplicationInsights-dotnet/issues/1795) - [Remove System.RunTime EventCounters by default](https://github.com/microsoft/ApplicationInsights-dotnet/issues/2009) +- [Ingestion service data delivery status](https://github.com/microsoft/ApplicationInsights-dotnet/pull/1887) ## Version 2.15.0-beta2 - [Read all properties of ApplicationInsightsServiceOptions from IConfiguration](https://github.com/microsoft/ApplicationInsights-dotnet/issues/1882)