Skip to content

Commit

Permalink
adding FunctionActivitySource to Worker; refactoring AppInsights supp…
Browse files Browse the repository at this point in the history
…ort (#1307)
  • Loading branch information
brettsam committed Mar 15, 2023
1 parent 584cab0 commit b963a28
Show file tree
Hide file tree
Showing 23 changed files with 399 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
<MajorProductVersion>1</MajorProductVersion>
<MinorProductVersion>0</MinorProductVersion>
<PatchProductVersion>0</PatchProductVersion>
<VersionSuffix>-preview3</VersionSuffix>
<VersionSuffix>-preview4</VersionSuffix>
</PropertyGroup>

<Import Project="..\..\build\Common.props" />

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
35 changes: 0 additions & 35 deletions src/DotNetWorker.ApplicationInsights/FunctionActivitySource.cs

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Linq;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.WorkerService;
using Microsoft.Azure.Functions.Worker.ApplicationInsights;
using Microsoft.Azure.Functions.Worker.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
Expand All @@ -25,6 +23,7 @@ public static class FunctionsApplicationInsightsExtensions
public static IFunctionsWorkerApplicationBuilder AddApplicationInsights(this IFunctionsWorkerApplicationBuilder builder, Action<ApplicationInsightsServiceOptions>? configureOptions = null)
{
builder.AddCommonServices();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITelemetryModule, FunctionsTelemetryModule>());

builder.Services.AddApplicationInsightsTelemetryWorkerService(options =>
{
Expand All @@ -44,11 +43,13 @@ public static IFunctionsWorkerApplicationBuilder AddApplicationInsightsLogger(th
{
builder.AddCommonServices();

// Lets the host know that the worker is sending logs to App Insights. The host will now ignore these.
builder.Services.Configure<WorkerOptions>(workerOptions => workerOptions.Capabilities["WorkerApplicationInsightsLoggingEnabled"] = bool.TrueString);

builder.Services.AddLogging(logging =>
{
logging.AddApplicationInsights(options =>
{
options.IncludeScopes = false;
configureOptions?.Invoke(options);
});
});
Expand All @@ -58,26 +59,7 @@ public static IFunctionsWorkerApplicationBuilder AddApplicationInsightsLogger(th

private static IFunctionsWorkerApplicationBuilder AddCommonServices(this IFunctionsWorkerApplicationBuilder builder)
{
builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(ITelemetryInitializer), typeof(FunctionsTelemetryInitializer), ServiceLifetime.Singleton));
builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(ITelemetryModule), typeof(FunctionsTelemetryModule), ServiceLifetime.Singleton));

// User logs will be written directly to Application Insights; this prevents duplicate logging.
builder.Services.AddSingleton<IUserLogWriter>(_ => NullUserLogWriter.Instance);

// This middleware is temporary for the preview. Eventually this behavior will move into the
// core worker assembly.
if (!builder.Services.Any(p => p.ImplementationType == typeof(FunctionActivitySourceMiddleware)))
{
builder.Services.AddSingleton<FunctionActivitySourceMiddleware>();
builder.Use(next =>
{
return async context =>
{
var middleware = context.InstanceServices.GetRequiredService<FunctionActivitySourceMiddleware>();
await middleware.Invoke(context, next);
};
});
}
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITelemetryInitializer, FunctionsTelemetryInitializer>());

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +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 System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.Implementation;

namespace Microsoft.Azure.Functions.Worker.ApplicationInsights
{
internal class FunctionsTelemetryInitializer : ITelemetryInitializer
{
private const string NameKey = "Name";
private readonly string? _sdkVersion;

private readonly string _sdkVersion;
private readonly string _roleInstanceName;

internal FunctionsTelemetryInitializer(string sdkVersion, string roleInstanceName)
internal FunctionsTelemetryInitializer(string? sdkVersion)
{
_sdkVersion = sdkVersion;
_roleInstanceName = roleInstanceName;
}

public FunctionsTelemetryInitializer() :
this(GetSdkVersion(), GetRoleInstanceName())
{
}

private static string GetSdkVersion()
this(GetSdkVersion())
{
return "azurefunctions-netiso: " + typeof(FunctionsTelemetryInitializer).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()!.Version;
}

private static string GetRoleInstanceName()
private static string? GetSdkVersion()
{
const string ComputerNameKey = "COMPUTERNAME";
const string WebSiteInstanceIdKey = "WEBSITE_INSTANCE_ID";
const string ContainerNameKey = "CONTAINER_NAME";

string? instanceName = Environment.GetEnvironmentVariable(WebSiteInstanceIdKey);
if (string.IsNullOrEmpty(instanceName))
{
instanceName = Environment.GetEnvironmentVariable(ComputerNameKey);
if (string.IsNullOrEmpty(instanceName))
{
instanceName = Environment.GetEnvironmentVariable(ContainerNameKey);
}
}
string? version = typeof(FunctionsTelemetryInitializer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;

return instanceName ?? Environment.MachineName;
return version == null ? null : $"azurefunctions-netiso: {version}";
}

public void Initialize(ITelemetry telemetry)
{
if (telemetry == null)
if (telemetry is null || _sdkVersion is null)
{
return;
}

telemetry.Context.Cloud.RoleInstance = _roleInstanceName;
telemetry.Context.GetInternalContext().SdkVersion = _sdkVersion;

telemetry.Context.Location.Ip ??= "0.0.0.0";

if (Activity.Current is not null)
{
foreach (var tag in Activity.Current.Tags)
{
switch (tag.Key)
{
case NameKey:
telemetry.Context.Operation.Name = tag.Value;
continue;
default:
break;
}

if (telemetry is ISupportProperties properties && !tag.Key.StartsWith("ai_"))
{
properties.Properties[tag.Key] = tag.Value;
}
}

}
}
}
}
96 changes: 86 additions & 10 deletions src/DotNetWorker.ApplicationInsights/FunctionsTelemetryModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace Microsoft.Azure.Functions.Worker.ApplicationInsights
{
internal class FunctionsTelemetryModule : ITelemetryModule, IDisposable
internal sealed class FunctionsTelemetryModule : ITelemetryModule, IAsyncDisposable
{
private TelemetryClient _telemetryClient = default!;
private const string DependencyTelemetryKey = "_tel";
private const string DependencyTypeInProc = "InProc";

private TelemetryClient? _telemetryClient;
private ActivityListener? _listener;

public void Initialize(TelemetryConfiguration configuration)
Expand All @@ -20,18 +27,37 @@ public void Initialize(TelemetryConfiguration configuration)

_listener = new ActivityListener
{
ShouldListenTo = source => source.Name.StartsWith("Microsoft.Azure.Functions.Worker"),
ShouldListenTo = source => source.Name.StartsWith(TraceConstants.FunctionsActivitySource),
ActivityStarted = activity =>
{
var dependency = _telemetryClient.StartOperation<DependencyTelemetry>(activity);
dependency.Telemetry.Type = "Azure.Functions";
activity.SetCustomProperty("_depTel", dependency);
dependency.Telemetry.Type = DependencyTypeInProc; // Required for proper rendering in App Insights.
activity.SetCustomProperty(DependencyTelemetryKey, dependency);
},
ActivityStopped = activity =>
{
var dependency = activity.GetCustomProperty("_depTel") as IOperationHolder<DependencyTelemetry>;
_telemetryClient.StopOperation(dependency);
dependency?.Dispose();
// Check for Exceptions events
foreach (ActivityEvent activityEvent in activity.Events)
{
TrackExceptionTelemetryFromActivityEvent(activityEvent, _telemetryClient);
}
if (activity.GetCustomProperty(DependencyTelemetryKey) is IOperationHolder<DependencyTelemetry> dependencyHolder)
{
var dependency = dependencyHolder.Telemetry;
foreach (var item in activity.Tags)
{
if (!dependency.Properties.ContainsKey(item.Key))
{
dependency.Properties[item.Key] = item.Value;
}
}
dependency.Success = activity.Status != ActivityStatusCode.Error;
_telemetryClient.StopOperation(dependencyHolder);
dependencyHolder.Dispose();
}
},
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
SampleUsingParentId = (ref ActivityCreationOptions<string> _) => ActivitySamplingResult.AllData
Expand All @@ -40,10 +66,60 @@ public void Initialize(TelemetryConfiguration configuration)
ActivitySource.AddActivityListener(_listener);
}

public void Dispose()
private static void TrackExceptionTelemetryFromActivityEvent(ActivityEvent activityEvent, TelemetryClient telemetryClient)
{
if (activityEvent.Name == TraceConstants.AttributeExceptionEventName)
{
string? exceptionType = null;
string? exceptionStackTrace = null;
string? exceptionMessage = null;

foreach (var tag in activityEvent.Tags)
{
if (tag.Key == TraceConstants.AttributeExceptionType)
{
exceptionType = tag.Value?.ToString();
continue;
}
if (tag.Key == TraceConstants.AttributeExceptionMessage)
{
exceptionMessage = tag.Value?.ToString();
continue;
}
if (tag.Key == TraceConstants.AttributeExceptionStacktrace)
{
exceptionStackTrace = tag.Value?.ToString();
continue;
}
}

ExceptionDetailsInfo edi = new(1, -1, exceptionType, exceptionMessage, exceptionStackTrace != null,
exceptionStackTrace, Enumerable.Empty<Microsoft.ApplicationInsights.DataContracts.StackFrame>());

ExceptionTelemetry et = new(new[] { edi }, SeverityLevel.Error, null, new Dictionary<string, string>() { }, new Dictionary<string, double>() { });

telemetryClient.TrackException(et);
}
}

public async ValueTask DisposeAsync()
{
_telemetryClient?.Flush();
_listener?.Dispose();

if (_telemetryClient is not null)
{
using CancellationTokenSource cts = new(millisecondsDelay: 5000);
try
{
await _telemetryClient.FlushAsync(cts.Token);
}
catch
{
// Ignore for now; potentially log this in the future.
}
}

GC.SuppressFinalize(this);
}
}
}
19 changes: 0 additions & 19 deletions src/DotNetWorker.ApplicationInsights/NullUserLogWriter.cs

This file was deleted.

12 changes: 12 additions & 0 deletions src/DotNetWorker.ApplicationInsights/TraceConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Microsoft.Azure.Functions.Worker.ApplicationInsights
{
internal class TraceConstants
{
public const string FunctionsActivitySource = "Microsoft.Azure.Functions.Worker";

public const string AttributeExceptionEventName = "exception";
public const string AttributeExceptionType = "exception.type";
public const string AttributeExceptionMessage = "exception.message";
public const string AttributeExceptionStacktrace = "exception.stacktrace";
}
}
Loading

0 comments on commit b963a28

Please sign in to comment.