Skip to content

Commit

Permalink
Adding support for Application Insights Diagnostics event logging (#3045
Browse files Browse the repository at this point in the history
)

* Adding support for ApplicationInsights Diagnostics event logging
---------
Co-authored-by: Jacob Viau <[email protected]>
  • Loading branch information
RohitRanjanMS authored Nov 21, 2023
1 parent 59d3c4a commit 9fad0f7
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Diagnostics.Tracing;
using Microsoft.ApplicationInsights.SnapshotCollector;
using Microsoft.ApplicationInsights.WindowsServer.Channel.Implementation;
using Microsoft.Azure.WebJobs.Hosting;
Expand Down Expand Up @@ -136,6 +137,12 @@ public TimeSpan QuickPulseInitializationDelay
/// </summary>
public bool EnableQueryStringTracing { get; set; } = false;

/// <summary>
/// Gets or sets the event level that enables diagnostic logging by event listener.
/// Disabled by default.
/// </summary>
public EventLevel? DiagnosticsEventListenerLogLevel { get; set; }

public string Format()
{
JObject sampling = null;
Expand Down Expand Up @@ -223,7 +230,8 @@ public string Format()
{ nameof(EnableQueryStringTracing), EnableQueryStringTracing },
{ nameof(EnableDependencyTracking), EnableDependencyTracking },
{ nameof(DependencyTrackingOptions), dependencyTrackingOptions },
{ nameof(TokenCredentialOptions), tokenCredentialOptions }
{ nameof(TokenCredentialOptions), tokenCredentialOptions },
{ nameof(DiagnosticsEventListenerLogLevel), DiagnosticsEventListenerLogLevel?.ToString() },
};

return options.ToString(Formatting.Indented);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Linq;
using Azure.Core;
using Microsoft.ApplicationInsights;
Expand Down Expand Up @@ -108,6 +109,17 @@ internal static IServiceCollection AddApplicationInsights(this IServiceCollectio
return NullTelemetryModule.Instance;
});

services.AddSingleton<ITelemetryModule>(provider =>
{
ApplicationInsightsLoggerOptions options = provider.GetService<IOptions<ApplicationInsightsLoggerOptions>>().Value;
if (options.DiagnosticsEventListenerLogLevel != null)
{
return new SelfDiagnosticsTelemetryModule((EventLevel)options.DiagnosticsEventListenerLogLevel);
}

return NullTelemetryModule.Instance;
});

services.AddSingleton<IApplicationIdProvider, ApplicationInsightsApplicationIdProvider>();

services.AddSingleton<ITelemetryModule>(provider =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Timers;

namespace Microsoft.Azure.WebJobs.Logging.ApplicationInsights
{
/// <summary>
/// Implementation of <see cref="EventListener"/> that listens to events produced by ApplicationInsights SDK.
/// Logs data either every 10 seconds or when the batch becomes full, whichever occurs first.
/// Logs in batch to reduce the volume of logs in kusto
/// </summary>
internal class ApplicationInsightsEventListener : EventListener
{
private static readonly DiagnosticListener _source = new DiagnosticListener(string.Concat(ApplicationInsightsDiagnosticConstants.ApplicationInsightsDiagnosticSourcePrefix, nameof(ApplicationInsightsEventListener)));
private readonly EventLevel _eventLevel;

private const int LogFlushIntervalMs = 10 * 1000;
private const string EventSourceNamePrefix = "Microsoft-ApplicationInsights-";
private const string EventName = nameof(ApplicationInsightsEventListener);
private const int MaxLogLinesPerFlushInterval = 30;

private Timer _flushTimer;
private ConcurrentQueue<string> _logBuffer = new ConcurrentQueue<string>();
private ConcurrentQueue<EventSource> _eventSource = new ConcurrentQueue<EventSource>();
private static object _syncLock = new object();
private bool _disposed = false;

public ApplicationInsightsEventListener(EventLevel eventLevel)
{
this._eventLevel = eventLevel;
_flushTimer = new Timer
{
AutoReset = true,
Interval = LogFlushIntervalMs
};
_flushTimer.Elapsed += (sender, e) => Flush();
_flushTimer.Start();
}

protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name.StartsWith(EventSourceNamePrefix))
{
EnableEvents(eventSource, _eventLevel, EventKeywords.All);
_eventSource.Enqueue(eventSource);
}
base.OnEventSourceCreated(eventSource);
}

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (!string.IsNullOrWhiteSpace(eventData.Message))
{
_logBuffer.Enqueue(string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload.ToArray()));
if (_logBuffer.Count >= MaxLogLinesPerFlushInterval)
{
Flush();
}
}
}

public void Flush()
{
if (_logBuffer.Count == 0)
{
return;
}

ConcurrentQueue<string> currentBuffer = null;
lock (_syncLock)
{
if (_logBuffer.Count == 0)
{
return;
}
currentBuffer = _logBuffer;
_logBuffer = new ConcurrentQueue<string>();
}

// batch up to 30 events in one log
StringBuilder sb = new StringBuilder();
// start with a new line
sb.AppendLine(string.Empty);
while (currentBuffer.TryDequeue(out string line))
{
sb.AppendLine(line);
}
_source.Write(EventName, sb.ToString());
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
while (_eventSource.TryDequeue(out EventSource source))
{
if (source != null)
{
DisableEvents(source);
source.Dispose();
}
}

if (_flushTimer != null)
{
_flushTimer.Dispose();
}
base.Dispose();
// ensure any remaining logs are flushed
Flush();
}
_disposed = true;
}
}

public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.ApplicationInsights.Extensibility;
using System;
using System.Diagnostics.Tracing;

namespace Microsoft.Azure.WebJobs.Logging.ApplicationInsights
{
/// <summary>
/// Initializes <see cref="ApplicationInsightsEventListener"/> that listens to events produced by ApplicationInsights SDK.
/// </summary>
internal class SelfDiagnosticsTelemetryModule : ITelemetryModule, IDisposable
{
private ApplicationInsightsEventListener _eventListener;
private EventLevel _eventLevel;

internal SelfDiagnosticsTelemetryModule(EventLevel eventLevel)
{
_eventLevel = eventLevel;
}

public void Initialize(TelemetryConfiguration configuration)
{
_eventListener = new ApplicationInsightsEventListener(_eventLevel);
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (disposing)
{
_eventListener?.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<Version>3.0.38$(VersionSuffix)</Version>
<Version>3.0.39$(VersionSuffix)</Version>
<InformationalVersion>$(Version) Commit hash: $(CommitHash)</InformationalVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Microsoft.Azure.WebJobs.Logging.ApplicationInsights</PackageId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Reflection;
using Azure.Identity;
Expand Down Expand Up @@ -84,13 +85,15 @@ public void DependencyInjectionConfiguration_Configures_With_InstrumentationKey(
var modules = host.Services.GetServices<ITelemetryModule>().ToList();

// Verify Modules
Assert.Equal(5, modules.Count);
Assert.Equal(6, modules.Count);
Assert.Single(modules.OfType<DependencyTrackingTelemetryModule>());

Assert.Single(modules.OfType<QuickPulseTelemetryModule>());
Assert.Single(modules.OfType<PerformanceCollectorModule>());
Assert.Single(modules.OfType<AppServicesHeartbeatTelemetryModule>());
Assert.Single(modules.OfType<RequestTrackingTelemetryModule>());
// SelfDiagnosticsTelemetryModule is dabled by default and instead NullTelemetryModule is added
Assert.Single(modules.OfType<NullTelemetryModule>());

var dependencyModule = modules.OfType<DependencyTrackingTelemetryModule>().Single();

Expand Down Expand Up @@ -128,7 +131,11 @@ public void DependencyInjectionConfiguration_Configures_With_ConnectionString()
var builder = new HostBuilder()
.ConfigureLogging(b =>
{
b.AddApplicationInsightsWebJobs(o => o.ConnectionString = "InstrumentationKey=somekey;EndpointSuffix=applicationinsights.us");
b.AddApplicationInsightsWebJobs(o =>
{
o.ConnectionString = "InstrumentationKey=somekey;EndpointSuffix=applicationinsights.us";
o.DiagnosticsEventListenerLogLevel = EventLevel.Verbose;
});
});

using (var host = builder.Build())
Expand Down Expand Up @@ -161,13 +168,14 @@ public void DependencyInjectionConfiguration_Configures_With_ConnectionString()
var modules = host.Services.GetServices<ITelemetryModule>().ToList();

// Verify Modules
Assert.Equal(5, modules.Count);
Assert.Equal(6, modules.Count);
Assert.Single(modules.OfType<DependencyTrackingTelemetryModule>());

Assert.Single(modules.OfType<QuickPulseTelemetryModule>());
Assert.Single(modules.OfType<PerformanceCollectorModule>());
Assert.Single(modules.OfType<AppServicesHeartbeatTelemetryModule>());
Assert.Single(modules.OfType<RequestTrackingTelemetryModule>());
Assert.Single(modules.OfType<SelfDiagnosticsTelemetryModule>());

var dependencyModule = modules.OfType<DependencyTrackingTelemetryModule>().Single();

Expand Down
Loading

0 comments on commit 9fad0f7

Please sign in to comment.