diff --git a/DotNetWorker.sln b/DotNetWorker.sln
index aa804036f..7865ddbac 100644
--- a/DotNetWorker.sln
+++ b/DotNetWorker.sln
@@ -100,6 +100,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Storage.Blobs", "extensions\Worker.Extensions.Storage.Blobs\src\Worker.Extensions.Storage.Blobs.csproj", "{FC352905-BD72-4049-8D32-3CBB9304FDC8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Storage.Tables", "extensions\Worker.Extensions.Storage.Tables\src\Worker.Extensions.Storage.Tables.csproj", "{2B2B47E9-2973-4269-AC5D-E5C32BDD5346}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sdk.Generators", "sdk\Sdk.Generators\Sdk.Generators.csproj", "{F77CCCE6-2DC3-48AA-8FE8-1B135B76B90E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sdk.Generator.Tests", "test\Sdk.Generator.Tests\Sdk.Generator.Tests.csproj", "{18A09B24-8646-40A6-BD85-2773AF567453}"
@@ -112,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker.Extensions.Sample-In
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetFxWorker", "samples\NetFxWorker\NetFxWorker.csproj", "{B37E6BAC-F16B-4366-94FB-8B94B52A08C9}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetWorker.ApplicationInsights", "src\DotNetWorker.ApplicationInsights\DotNetWorker.ApplicationInsights.csproj", "{65DE66B6-568F-46AC-8F0D-C79A02F48214}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -274,6 +277,10 @@ Global
{B37E6BAC-F16B-4366-94FB-8B94B52A08C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B37E6BAC-F16B-4366-94FB-8B94B52A08C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B37E6BAC-F16B-4366-94FB-8B94B52A08C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {65DE66B6-568F-46AC-8F0D-C79A02F48214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65DE66B6-568F-46AC-8F0D-C79A02F48214}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65DE66B6-568F-46AC-8F0D-C79A02F48214}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65DE66B6-568F-46AC-8F0D-C79A02F48214}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -322,6 +329,7 @@ Global
{922A387F-8595-4C74-ABF1-AEFF9530950C} = {B5821230-6E0A-4535-88A9-ED31B6F07596}
{22FCE0DF-65FE-4650-8202-765832C40E6D} = {922A387F-8595-4C74-ABF1-AEFF9530950C}
{B37E6BAC-F16B-4366-94FB-8B94B52A08C9} = {9D6603BD-7EA2-4D11-A69C-0D9E01317FD6}
+ {65DE66B6-568F-46AC-8F0D-C79A02F48214} = {083592CA-7DAB-44CE-8979-44FAFA46AEC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {497D2ED4-A13E-4BCA-8D29-F30CA7D0EA4A}
diff --git a/build/DotNetWorker.Core.slnf b/build/DotNetWorker.Core.slnf
index 2858c25c8..cba0b6f4a 100644
--- a/build/DotNetWorker.Core.slnf
+++ b/build/DotNetWorker.Core.slnf
@@ -5,6 +5,7 @@
"sdk\\Sdk.Analyzers\\Sdk.Analyzers.csproj",
"sdk\\Sdk.Generators\\Sdk.Generators.csproj",
"sdk\\Sdk\\Sdk.csproj",
+ "src\\DotNetWorker.ApplicationInsights\\DotNetWorker.ApplicationInsights.csproj",
"src\\DotNetWorker.Core\\DotNetWorker.Core.csproj",
"src\\DotNetWorker.Grpc\\DotNetWorker.Grpc.csproj",
"src\\DotNetWorker\\DotNetWorker.csproj"
diff --git a/samples/FunctionApp/FunctionApp.csproj b/samples/FunctionApp/FunctionApp.csproj
index 6dbfe0048..e6d181e66 100644
--- a/samples/FunctionApp/FunctionApp.csproj
+++ b/samples/FunctionApp/FunctionApp.csproj
@@ -22,6 +22,7 @@
+
diff --git a/samples/FunctionApp/Program.cs b/samples/FunctionApp/Program.cs
index 8e5feccf6..59635d038 100644
--- a/samples/FunctionApp/Program.cs
+++ b/samples/FunctionApp/Program.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
using System.Threading.Tasks;
+using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -17,7 +18,12 @@ static async Task Main(string[] args)
//
var host = new HostBuilder()
//
- .ConfigureFunctionsWorkerDefaults()
+ .ConfigureFunctionsWorkerDefaults(builder =>
+ {
+ builder
+ .AddApplicationInsights()
+ .AddApplicationInsightsLogger();
+ })
//
//
.ConfigureServices(s =>
diff --git a/samples/FunctionApp/local.settings.json b/samples/FunctionApp/local.settings.json
index 2dfe159d8..401ae0c34 100644
--- a/samples/FunctionApp/local.settings.json
+++ b/samples/FunctionApp/local.settings.json
@@ -1,7 +1,7 @@
{
- "IsEncrypted": false,
- "Values": {
- "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
- "AzureWebJobsStorage": "UseDevelopmentStorage=true"
- }
+ "IsEncrypted": false,
+ "Values": {
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true"
}
+}
\ No newline at end of file
diff --git a/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj b/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj
new file mode 100644
index 000000000..f26b7ed95
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/DotNetWorker.ApplicationInsights.csproj
@@ -0,0 +1,24 @@
+
+
+
+ netstandard2.0
+ Microsoft.Azure.Functions.Worker.ApplicationInsights
+ Microsoft.Azure.Functions.Worker.ApplicationInsights
+ Microsoft.Azure.Functions.Worker.ApplicationInsights
+ 1
+ 0
+ 0
+ -preview1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DotNetWorker.ApplicationInsights/FunctionActivitySource.cs b/src/DotNetWorker.ApplicationInsights/FunctionActivitySource.cs
new file mode 100644
index 000000000..b70880fb7
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/FunctionActivitySource.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Diagnostics;
+
+namespace Microsoft.Azure.Functions.Worker.Core.Diagnostics
+{
+ /// Note: This class will eventually move in to the core worker assembly. Including it in the
+ /// ApplicationInsights package so we can utilize it during preview.
+ internal static class FunctionActivitySource
+ {
+ private const string InvocationIdKey = "InvocationId";
+ private const string NameKey = "Name";
+ private const string ProcessIdKey = "ProcessId";
+
+ private static readonly ActivitySource _activitySource = new("Microsoft.Azure.Functions.Worker");
+ private static readonly string _processId = Process.GetCurrentProcess().Id.ToString();
+
+ public static Activity? StartInvoke(FunctionContext context)
+ {
+ var activity = _activitySource.StartActivity("Invoke", ActivityKind.Internal, context.TraceContext.TraceParent);
+
+ if (activity is not null)
+ {
+ activity.AddTag(InvocationIdKey, context.InvocationId);
+ activity.AddTag(NameKey, context.FunctionDefinition.Name);
+ activity.AddTag(ProcessIdKey, _processId);
+ }
+
+ return activity;
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/FunctionActivitySourceMiddleware.cs b/src/DotNetWorker.ApplicationInsights/FunctionActivitySourceMiddleware.cs
new file mode 100644
index 000000000..08c80807b
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/FunctionActivitySourceMiddleware.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.Azure.Functions.Worker.Core.Diagnostics;
+using Microsoft.Azure.Functions.Worker.Middleware;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights;
+
+internal class FunctionActivitySourceMiddleware : IFunctionsWorkerMiddleware
+{
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ using (FunctionActivitySource.StartInvoke(context))
+ {
+ await next.Invoke(context);
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs
new file mode 100644
index 000000000..27c5fb7da
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/FunctionsApplicationInsightsExtensions.cs
@@ -0,0 +1,85 @@
+// 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.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;
+using Microsoft.Extensions.Logging.ApplicationInsights;
+
+namespace Microsoft.Azure.Functions.Worker
+{
+ public static class FunctionsApplicationInsightsExtensions
+ {
+ ///
+ /// Adds Application Insights support by internally calling .
+ ///
+ /// The
+ /// Action to configure ApplicationInsights services.
+ /// The
+ public static IFunctionsWorkerApplicationBuilder AddApplicationInsights(this IFunctionsWorkerApplicationBuilder builder, Action? configureOptions = null)
+ {
+ builder.AddCommonServices();
+
+ builder.Services.AddApplicationInsightsTelemetryWorkerService(options =>
+ {
+ configureOptions?.Invoke(options);
+ });
+
+ return builder;
+ }
+
+ ///
+ /// Adds the and disables the Functions host passthrough logger.
+ ///
+ /// The
+ /// Action to configure ApplicationInsights logger.
+ /// The
+ public static IFunctionsWorkerApplicationBuilder AddApplicationInsightsLogger(this IFunctionsWorkerApplicationBuilder builder, Action? configureOptions = null)
+ {
+ builder.AddCommonServices();
+
+ builder.Services.AddLogging(logging =>
+ {
+ logging.AddApplicationInsights(options =>
+ {
+ options.IncludeScopes = false;
+ configureOptions?.Invoke(options);
+ });
+ });
+
+ return builder;
+ }
+
+ 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(_ => 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();
+ builder.Use(next =>
+ {
+ return async context =>
+ {
+ var middleware = context.InstanceServices.GetRequiredService();
+ await middleware.Invoke(context, next);
+ };
+ });
+ }
+
+ return builder;
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryInitializer.cs b/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryInitializer.cs
new file mode 100644
index 000000000..cc4dae12a
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryInitializer.cs
@@ -0,0 +1,90 @@
+// 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 _roleInstanceName;
+
+ internal FunctionsTelemetryInitializer(string sdkVersion, string roleInstanceName)
+ {
+ _sdkVersion = sdkVersion;
+ _roleInstanceName = roleInstanceName;
+ }
+
+ public FunctionsTelemetryInitializer() :
+ this(GetSdkVersion(), GetRoleInstanceName())
+ {
+ }
+
+ private static string GetSdkVersion()
+ {
+ return "azurefunctions-netiso: " + typeof(FunctionsTelemetryInitializer).Assembly.GetCustomAttribute()!.Version;
+ }
+
+ private static string GetRoleInstanceName()
+ {
+ 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);
+ }
+ }
+
+ return instanceName ?? Environment.MachineName;
+ }
+
+ public void Initialize(ITelemetry telemetry)
+ {
+ if (telemetry == 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;
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryModule.cs b/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryModule.cs
new file mode 100644
index 000000000..6d4fa6012
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/FunctionsTelemetryModule.cs
@@ -0,0 +1,49 @@
+// 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 Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.ApplicationInsights.Extensibility;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights
+{
+ internal class FunctionsTelemetryModule : ITelemetryModule, IDisposable
+ {
+ private TelemetryClient _telemetryClient = default!;
+ private ActivityListener? _listener;
+
+ public void Initialize(TelemetryConfiguration configuration)
+ {
+ _telemetryClient = new TelemetryClient(configuration);
+
+ _listener = new ActivityListener
+ {
+ ShouldListenTo = source => source.Name.StartsWith("Microsoft.Azure.Functions.Worker"),
+ ActivityStarted = activity =>
+ {
+ var dependency = new DependencyTelemetry("Azure.Functions", activity.OperationName, activity.OperationName, null);
+ activity.SetCustomProperty("_depTel", dependency);
+ dependency.Start();
+ },
+ ActivityStopped = activity =>
+ {
+ var dependency = activity.GetCustomProperty("_depTel") as DependencyTelemetry;
+ dependency.Stop();
+ _telemetryClient.TrackDependency(dependency);
+ },
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ SampleUsingParentId = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData
+ };
+
+ ActivitySource.AddActivityListener(_listener);
+ }
+
+ public void Dispose()
+ {
+ _telemetryClient?.Flush();
+ _listener?.Dispose();
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/NullUserLogWriter.cs b/src/DotNetWorker.ApplicationInsights/NullUserLogWriter.cs
new file mode 100644
index 000000000..a7c842ec8
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/NullUserLogWriter.cs
@@ -0,0 +1,19 @@
+using System;
+using Microsoft.Azure.Functions.Worker.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights
+{
+ internal class NullUserLogWriter : IUserLogWriter
+ {
+ private NullUserLogWriter()
+ {
+ }
+
+ public static NullUserLogWriter Instance = new NullUserLogWriter();
+
+ public void WriteUserLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ }
+ }
+}
diff --git a/src/DotNetWorker.ApplicationInsights/Properties/AssemblyInfo.cs b/src/DotNetWorker.ApplicationInsights/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..f25d9cff5
--- /dev/null
+++ b/src/DotNetWorker.ApplicationInsights/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Azure.Functions.Worker.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001005148be37ac1d9f58bd40a2e472c9d380d635b6048278f7d47480b08c928858f0f7fe17a6e4ce98da0e7a7f0b8c308aecd9e9b02d7e9680a5b5b75ac7773cec096fbbc64aebd429e77cb5f89a569a79b28e9c76426783f624b6b70327eb37341eb498a2c3918af97c4860db6cdca4732787150841e395a29cfacb959c1fd971c1")]
diff --git a/src/DotNetWorker.Core/Diagnostics/IWorkerDiagnostics.cs b/src/DotNetWorker.Core/Diagnostics/IWorkerDiagnostics.cs
index 6c405eef5..5d83b6959 100644
--- a/src/DotNetWorker.Core/Diagnostics/IWorkerDiagnostics.cs
+++ b/src/DotNetWorker.Core/Diagnostics/IWorkerDiagnostics.cs
@@ -4,7 +4,7 @@
namespace Microsoft.Azure.Functions.Worker.Diagnostics
{
///
- /// Represents an interface for sending logs directly to the Fucctions host.
+ /// Represents an interface for sending logs directly to the Functions host.
///
internal interface IWorkerDiagnostics
{
diff --git a/src/DotNetWorker.Core/DotNetWorker.Core.csproj b/src/DotNetWorker.Core/DotNetWorker.Core.csproj
index 439a771e1..602fe25cf 100644
--- a/src/DotNetWorker.Core/DotNetWorker.Core.csproj
+++ b/src/DotNetWorker.Core/DotNetWorker.Core.csproj
@@ -1,33 +1,34 @@
-
- Library
- net5.0;netstandard2.0
- Microsoft.Azure.Functions.Worker.Core
- This library provides the core functionality to build an Azure Functions .NET Worker, adding support for the isolated, out-of-process execution model.
- Microsoft.Azure.Functions.Worker.Core
- Microsoft.Azure.Functions.Worker.Core
- true
- 6
-
+
+ Library
+ net5.0;netstandard2.0
+ Microsoft.Azure.Functions.Worker.Core
+ This library provides the core functionality to build an Azure Functions .NET Worker, adding support for the isolated, out-of-process execution model.
+ Microsoft.Azure.Functions.Worker.Core
+ Microsoft.Azure.Functions.Worker.Core
+ true
+ 7
+ -preview1
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
diff --git a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs
index d8b32999c..34c7b9fc0 100644
--- a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs
+++ b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs
@@ -11,10 +11,12 @@
using Microsoft.Azure.Functions.Worker.Converters;
using Microsoft.Azure.Functions.Worker.Core;
using Microsoft.Azure.Functions.Worker.Invocation;
+using Microsoft.Azure.Functions.Worker.Logging;
using Microsoft.Azure.Functions.Worker.OutputBindings;
using Microsoft.Azure.Functions.Worker.Pipeline;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
@@ -78,6 +80,12 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe
}
});
+ services.AddSingleton();
+ services.AddSingleton(NullLogWriter.Instance);
+ services.AddSingleton(s => s.GetRequiredService());
+ services.AddSingleton(s => s.GetRequiredService());
+ services.AddSingleton(s => s.GetRequiredService());
+
if (configure != null)
{
services.Configure(configure);
diff --git a/src/DotNetWorker.Core/Logging/ISystemLogWriter.cs b/src/DotNetWorker.Core/Logging/ISystemLogWriter.cs
new file mode 100644
index 000000000..77f200ae6
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/ISystemLogWriter.cs
@@ -0,0 +1,27 @@
+// 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 Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ ///
+ /// An abstraction for writing system logs.
+ ///
+ public interface ISystemLogWriter
+ {
+ ///
+ /// Writes a system log entry.
+ ///
+ /// The type of the object to be written.
+ /// The provider of scope data.
+ /// The category name for messages produced by the logger.
+ /// Entry will be written on this level.
+ /// Id of the event.
+ /// The entry to be written. Can be also an object.
+ /// The exception related to this entry.
+ /// Function to create a message of the state and exception.
+ void WriteSystemLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter);
+ }
+}
diff --git a/src/DotNetWorker.Core/Logging/IUserLogWriter.cs b/src/DotNetWorker.Core/Logging/IUserLogWriter.cs
new file mode 100644
index 000000000..17f16eb4d
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/IUserLogWriter.cs
@@ -0,0 +1,27 @@
+// 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 Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ ///
+ /// An abstraction for writing user logs.
+ ///
+ public interface IUserLogWriter
+ {
+ ///
+ /// Writes a user log entry.
+ ///
+ /// The type of the object to be written.
+ /// The provider of scope data.
+ /// The category name for messages produced by the logger.
+ /// Entry will be written on this level.
+ /// Id of the event.
+ /// The entry to be written. Can be also an object.
+ /// The exception related to this entry.
+ /// Function to create a message of the state and exception.
+ void WriteUserLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter);
+ }
+}
diff --git a/src/DotNetWorker.Core/Logging/IUserMetricWriter.cs b/src/DotNetWorker.Core/Logging/IUserMetricWriter.cs
new file mode 100644
index 000000000..a5578dedd
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/IUserMetricWriter.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ ///
+ /// An abstraction for writing user metrics.
+ ///
+ internal interface IUserMetricWriter
+ {
+ ///
+ /// Writes user metrics.
+ ///
+ /// The provider of scope data.
+ /// Additional properties.
+ void WriteUserMetric(IExternalScopeProvider scopeProvider, IDictionary state);
+ }
+}
diff --git a/src/DotNetWorker.Core/Logging/NullLogWriter.cs b/src/DotNetWorker.Core/Logging/NullLogWriter.cs
new file mode 100644
index 000000000..bb08e2bb6
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/NullLogWriter.cs
@@ -0,0 +1,44 @@
+// 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.Generic;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ ///
+ /// Minimalistic LogWriter that does nothing.
+ ///
+ internal class NullLogWriter : IUserLogWriter, ISystemLogWriter, IUserMetricWriter
+ {
+ private NullLogWriter()
+ {
+ }
+
+ ///
+ /// Returns the shared instance of .
+ ///
+ public static NullLogWriter Instance = new NullLogWriter();
+
+ ///
+ public void WriteSystemLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ }
+
+ ///
+ public void WriteUserLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ }
+
+ ///
+ public void WriteUserMetric(IExternalScopeProvider scopeProvider, string metricName, string metricValue, IDictionary properties)
+ {
+ }
+
+ ///
+ public void WriteUserMetric(IExternalScopeProvider scopeProvider, IDictionary state)
+ {
+ }
+ }
+}
diff --git a/src/DotNetWorker.Core/Logging/WorkerLogger.cs b/src/DotNetWorker.Core/Logging/WorkerLogger.cs
new file mode 100644
index 000000000..324e58a86
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/WorkerLogger.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Azure.Functions.Worker.Logging.ApplicationInsights;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ internal class WorkerLogger : ILogger
+ {
+ private readonly string _category;
+ private readonly ISystemLogWriter _systemLogWriter;
+ private readonly IUserLogWriter _userLogWriter;
+ private readonly IUserMetricWriter _userMetricWriter;
+ private readonly IExternalScopeProvider _scopeProvider;
+
+ public WorkerLogger(string category, ISystemLogWriter systemLogWriter, IUserLogWriter userLogWriter, IUserMetricWriter userMetricWriter, IExternalScopeProvider scopeProvider)
+ {
+ _category = category;
+ _systemLogWriter = systemLogWriter;
+ _userLogWriter = userLogWriter;
+ _userMetricWriter = userMetricWriter;
+ _scopeProvider = scopeProvider;
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ // The built-in DI wire-up guarantees that scope provider will be set.
+ return _scopeProvider.Push(state);
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logLevel != LogLevel.None;
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (WorkerMessage.IsSystemLog)
+ {
+ _systemLogWriter.WriteSystemLog(_scopeProvider, _category, logLevel, eventId, state, exception, formatter);
+ }
+ else
+ {
+ if (eventId.Name == LogConstants.MetricEventId.Name)
+ {
+ _userMetricWriter.WriteUserMetric(_scopeProvider, (state as IDictionary) ?? new Dictionary());
+ return;
+ }
+
+ _userLogWriter.WriteUserLog(_scopeProvider, _category, logLevel, eventId, state, exception, formatter);
+ }
+ }
+ }
+}
diff --git a/src/DotNetWorker.Core/Logging/WorkerLoggerProvider.cs b/src/DotNetWorker.Core/Logging/WorkerLoggerProvider.cs
new file mode 100644
index 000000000..d2682dbaa
--- /dev/null
+++ b/src/DotNetWorker.Core/Logging/WorkerLoggerProvider.cs
@@ -0,0 +1,33 @@
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ internal class WorkerLoggerProvider : ILoggerProvider, ISupportExternalScope
+ {
+ private readonly ISystemLogWriter _systemLogWriter;
+ private readonly IUserLogWriter _userLogWriter;
+ private readonly IUserMetricWriter _userMetricWriter;
+ private IExternalScopeProvider? _scopeProvider;
+
+ public WorkerLoggerProvider(ISystemLogWriter systemLogWriter, IUserLogWriter userLogWriter, IUserMetricWriter userMetricWriter)
+ {
+ _systemLogWriter = systemLogWriter ?? throw new ArgumentNullException(nameof(systemLogWriter));
+ _userLogWriter = userLogWriter ?? throw new ArgumentNullException(nameof(userLogWriter));
+ _userMetricWriter = userMetricWriter ?? throw new ArgumentNullException(nameof(userMetricWriter));
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new WorkerLogger(categoryName, _systemLogWriter, _userLogWriter, _userMetricWriter, _scopeProvider!);
+ }
+
+ public void SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs
new file mode 100644
index 000000000..9178a9af2
--- /dev/null
+++ b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs
@@ -0,0 +1,125 @@
+// 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.Generic;
+using System.Threading.Channels;
+using Azure.Core.Serialization;
+using Microsoft.Azure.Functions.Worker.Diagnostics;
+using Microsoft.Azure.Functions.Worker.Grpc.Messages;
+using Microsoft.Azure.Functions.Worker.Rpc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using static Microsoft.Azure.Functions.Worker.Grpc.Messages.RpcLog.Types;
+
+namespace Microsoft.Azure.Functions.Worker.Logging
+{
+ ///
+ /// A logger that sends logs back to the Functions host.
+ ///
+ internal class GrpcFunctionsHostLogWriter : ISystemLogWriter, IUserLogWriter, IUserMetricWriter
+ {
+ private readonly ChannelWriter _channelWriter;
+ private readonly ObjectSerializer _serializer;
+
+ public GrpcFunctionsHostLogWriter(GrpcHostChannel channel, IOptions workerOptions)
+ {
+ _channelWriter = channel?.Channel?.Writer ?? throw new ArgumentNullException(nameof(channel));
+ _serializer = workerOptions.Value.Serializer ?? throw new ArgumentNullException(nameof(workerOptions.Value.Serializer), "Serializer on WorkerOptions cannot be null");
+ }
+
+ public void WriteUserLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Log(RpcLogCategory.User, scopeProvider, categoryName, logLevel, eventId, state, exception, formatter);
+ }
+
+ public void WriteUserMetric(IExternalScopeProvider scopeProvider, IDictionary properties)
+ {
+ var response = new StreamingMessage();
+ var rpcMetric = new RpcLog
+ {
+ LogCategory = RpcLogCategory.CustomMetric,
+ };
+
+ foreach (var kvp in properties)
+ {
+ rpcMetric.PropertiesMap.Add(kvp.Key, kvp.Value.ToRpc(_serializer));
+ }
+
+ // Grab the invocation id from the current scope, if present.
+ rpcMetric = AppendInvocationIdToLog(rpcMetric, scopeProvider);
+
+ response.RpcLog = rpcMetric;
+
+ _channelWriter.TryWrite(response);
+ }
+
+ public void WriteSystemLog(IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Log(RpcLogCategory.System, scopeProvider, categoryName, logLevel, eventId, state, exception, formatter);
+ }
+
+ public void Log(RpcLogCategory category, IExternalScopeProvider scopeProvider, string categoryName, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ var response = new StreamingMessage();
+ var rpcLog = new RpcLog
+ {
+ EventId = eventId.ToString(),
+ Exception = exception.ToRpcException(),
+ Category = categoryName,
+ LogCategory = category,
+ Level = ToRpcLogLevel(logLevel),
+ Message = formatter(state, exception)
+ };
+
+ // Grab the invocation id from the current scope, if present.
+ rpcLog = AppendInvocationIdToLog(rpcLog, scopeProvider);
+
+ response.RpcLog = rpcLog;
+
+ _channelWriter.TryWrite(response);
+ }
+
+ private RpcLog AppendInvocationIdToLog(RpcLog rpcLog, IExternalScopeProvider scopeProvider)
+ {
+ scopeProvider.ForEachScope((scope, log) =>
+ {
+ if (scope is IEnumerable> properties)
+ {
+ foreach (var pair in properties)
+ {
+ if (pair.Key == FunctionInvocationScope.FunctionInvocationIdKey)
+ {
+ log.InvocationId = pair.Value?.ToString();
+ break;
+ }
+ }
+ }
+ },
+ rpcLog);
+
+ return rpcLog;
+ }
+
+ private static Level ToRpcLogLevel(LogLevel logLevel) =>
+ logLevel switch
+ {
+ LogLevel.Trace => Level.Trace,
+ LogLevel.Debug => Level.Debug,
+ LogLevel.Information => Level.Information,
+ LogLevel.Warning => Level.Warning,
+ LogLevel.Error => Level.Error,
+ LogLevel.Critical => Level.Critical,
+ _ => Level.None,
+ };
+
+ private class EmptyDisposable : IDisposable
+ {
+ public static IDisposable Instance = new EmptyDisposable();
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogger.cs b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogger.cs
deleted file mode 100644
index fca0bd913..000000000
--- a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogger.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-// 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.Generic;
-using System.Threading.Channels;
-using Azure.Core.Serialization;
-using Microsoft.Azure.Functions.Worker.Diagnostics;
-using Microsoft.Azure.Functions.Worker.Grpc.Messages;
-using Microsoft.Azure.Functions.Worker.Logging.ApplicationInsights;
-using Microsoft.Azure.Functions.Worker.Rpc;
-using Microsoft.Extensions.Logging;
-using static Microsoft.Azure.Functions.Worker.Grpc.Messages.RpcLog.Types;
-
-namespace Microsoft.Azure.Functions.Worker.Logging
-{
- ///
- /// A logger that sends logs back to the Functions host.
- ///
- internal class GrpcFunctionsHostLogger : ILogger
- {
- private readonly string _category;
- private readonly ChannelWriter _channelWriter;
- private readonly IExternalScopeProvider _scopeProvider;
- private readonly ObjectSerializer _serializer;
-
- public GrpcFunctionsHostLogger(string category, ChannelWriter channelWriter, IExternalScopeProvider scopeProvider, ObjectSerializer serializer)
- {
- _category = category ?? throw new ArgumentNullException(nameof(category));
- _channelWriter = channelWriter ?? throw new ArgumentNullException(nameof(channelWriter));
- _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
- _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
- }
-
- public IDisposable BeginScope(TState state)
- {
- // The built-in DI wire-up guarantees that scope provider will be set.
- return _scopeProvider!.Push(state);
- }
-
- public bool IsEnabled(LogLevel logLevel)
- {
- return logLevel != LogLevel.None;
- }
-
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
- {
- if (eventId.Name == LogConstants.MetricEventId.Name)
- {
- LogMetric((IDictionary)state!);
- }
- else
- {
- var response = new StreamingMessage();
- string message = formatter(state, exception);
- var rpcLog = new RpcLog
- {
- EventId = eventId.ToString(),
- Exception = exception.ToRpcException(),
- Category = _category,
- LogCategory = WorkerMessage.IsSystemLog ? RpcLogCategory.System : RpcLogCategory.User,
- Level = ToRpcLogLevel(logLevel),
- Message = message
- };
-
- // Grab the invocation id from the current scope, if present.
- rpcLog = AppendInvocationIdToLog(rpcLog);
-
- response.RpcLog = rpcLog;
-
- _channelWriter.TryWrite(response);
- }
- }
-
- private void LogMetric(IDictionary state)
- {
- if (state == null)
- {
- return;
- }
-
- var response = new StreamingMessage();
- var rpcMetric = new RpcLog
- {
- LogCategory = RpcLogCategory.CustomMetric,
- };
-
- foreach (var kvp in state)
- {
- rpcMetric.PropertiesMap.Add(kvp.Key, kvp.Value.ToRpc(_serializer));
- }
-
- // Grab the invocation id from the current scope, if present.
- rpcMetric = AppendInvocationIdToLog(rpcMetric);
-
- response.RpcLog = rpcMetric;
-
- _channelWriter.TryWrite(response);
- }
-
- private RpcLog AppendInvocationIdToLog(RpcLog rpcLog)
- {
- _scopeProvider?.ForEachScope((scope, log) =>
- {
- if (scope is IEnumerable> properties)
- {
- foreach (var pair in properties)
- {
- if (pair.Key == FunctionInvocationScope.FunctionInvocationIdKey)
- {
- log.InvocationId = pair.Value?.ToString();
- break;
- }
- }
- }
- },
- rpcLog);
-
- return rpcLog;
- }
-
- private static Level ToRpcLogLevel(LogLevel logLevel) =>
- logLevel switch
- {
- LogLevel.Trace => Level.Trace,
- LogLevel.Debug => Level.Debug,
- LogLevel.Information => Level.Information,
- LogLevel.Warning => Level.Warning,
- LogLevel.Error => Level.Error,
- LogLevel.Critical => Level.Critical,
- _ => Level.None,
- };
-
- private class EmptyDisposable : IDisposable
- {
- public static IDisposable Instance = new EmptyDisposable();
-
- public void Dispose()
- {
- }
- }
- }
-}
diff --git a/src/DotNetWorker.Grpc/GrpcFunctionsHostLoggerProvider.cs b/src/DotNetWorker.Grpc/GrpcFunctionsHostLoggerProvider.cs
deleted file mode 100644
index a8745120a..000000000
--- a/src/DotNetWorker.Grpc/GrpcFunctionsHostLoggerProvider.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-
-using System.Threading.Channels;
-using Microsoft.Azure.Functions.Worker.Logging;
-using Microsoft.Azure.Functions.Worker.Grpc.Messages;
-using Microsoft.Extensions.Logging;
-using Azure.Core.Serialization;
-using Microsoft.Extensions.Options;
-using System;
-
-namespace Microsoft.Azure.Functions.Worker.Diagnostics
-{
- internal class GrpcFunctionsHostLoggerProvider : ILoggerProvider, ISupportExternalScope
- {
- private readonly ChannelWriter _channelWriter;
- private readonly ObjectSerializer _serializer;
- private IExternalScopeProvider? _scopeProvider;
-
- public GrpcFunctionsHostLoggerProvider(GrpcHostChannel outputChannel, IOptions workerOptions)
- {
- _channelWriter = outputChannel.Channel.Writer;
- _serializer = workerOptions?.Value?.Serializer ?? throw new ArgumentNullException(nameof(workerOptions.Value.Serializer), "Serializer on WorkerOptions cannot be null");
- }
-
- public ILogger CreateLogger(string categoryName) => new GrpcFunctionsHostLogger(categoryName, _channelWriter, _scopeProvider!, _serializer);
-
- public void Dispose()
- {
- }
-
- public void SetScopeProvider(IExternalScopeProvider scopeProvider)
- {
- _scopeProvider = scopeProvider;
- }
- }
-}
diff --git a/src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs b/src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs
index 78abd87d0..40e7e0664 100644
--- a/src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs
+++ b/src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs
@@ -2,18 +2,17 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
-using System.Collections.Generic;
using System.Threading.Channels;
using Grpc.Core;
using Microsoft.Azure.Functions.Worker;
-using Microsoft.Azure.Functions.Worker.Diagnostics;
-using Microsoft.Azure.Functions.Worker.Grpc;
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static Microsoft.Azure.Functions.Worker.Grpc.Messages.FunctionRpc;
+using Microsoft.Azure.Functions.Worker.Logging;
+using Microsoft.Azure.Functions.Worker.Grpc;
+using Microsoft.Azure.Functions.Worker.Diagnostics;
#if NET5_0_OR_GREATER
using Grpc.Net.Client;
@@ -43,12 +42,12 @@ public static IServiceCollection AddGrpc(this IServiceCollection services)
// Channels
services.RegisterOutputChannel();
- // Internal logging
- services.AddLogging(logging =>
- {
- logging.Services.AddSingleton();
- logging.Services.AddSingleton();
- });
+ // Internal logging
+ services.AddSingleton();
+ services.AddSingleton(p => p.GetRequiredService());
+ services.AddSingleton(p => p.GetRequiredService());
+ services.AddSingleton(p => p.GetRequiredService());
+ services.AddSingleton();
// FunctionMetadataProvider for worker driven function-indexing
services.AddSingleton();
diff --git a/src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs
index 4702e77dd..271641d4a 100644
--- a/src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs
+++ b/src/DotNetWorker/Hosting/ServiceCollectionExtensions.cs
@@ -1,47 +1,47 @@
-// 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.Text.Json;
-using Microsoft.Azure.Functions.Worker;
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
-namespace Microsoft.Extensions.DependencyInjection
-{
- ///
- /// Azure Functions extensions for .
- ///
- public static class ServiceCollectionExtensions
- {
- ///
- /// Adds the core set of services for the Azure Functions worker.
- /// This call also adds the default set of binding converters and gRPC support.
- /// This call also adds a default ObjectSerializer that treats property names as case insensitive.
- ///
- /// The .
- /// The action used to configure .
- /// The same for chaining.
- public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerDefaults(this IServiceCollection services, Action? configure = null)
- {
- if (services == null)
- {
- throw new ArgumentNullException(nameof(services));
- }
-
- services.AddDefaultInputConvertersToWorkerOptions();
-
- // Default Json serialization should ignore casing on property names
- services.Configure(options =>
- {
- options.PropertyNameCaseInsensitive = true;
- });
-
- // Core services registration
- var builder = services.AddFunctionsWorkerCore(configure);
-
- // gRPC support
- services.AddGrpc();
-
- return builder;
- }
- }
-}
+using System;
+using System.Text.Json;
+using Microsoft.Azure.Functions.Worker;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Azure Functions extensions for .
+ ///
+ public static class ServiceCollectionExtensions
+ {
+ ///
+ /// Adds the core set of services for the Azure Functions worker.
+ /// This call also adds the default set of binding converters and gRPC support.
+ /// This call also adds a default ObjectSerializer that treats property names as case insensitive.
+ ///
+ /// The .
+ /// The action used to configure .
+ /// The same for chaining.
+ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerDefaults(this IServiceCollection services, Action? configure = null)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddDefaultInputConvertersToWorkerOptions();
+
+ // Default Json serialization should ignore casing on property names
+ services.Configure(options =>
+ {
+ options.PropertyNameCaseInsensitive = true;
+ });
+
+ // Core services registration
+ var builder = services.AddFunctionsWorkerCore(configure);
+
+ // gRPC support
+ services.AddGrpc();
+
+ return builder;
+ }
+ }
+}
diff --git a/test/DotNetWorkerTests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs b/test/DotNetWorkerTests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs
new file mode 100644
index 000000000..c9e4f79d9
--- /dev/null
+++ b/test/DotNetWorkerTests/ApplicationInsights/ApplicationInsightsConfigurationTests.cs
@@ -0,0 +1,208 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ApplicationInsights.DependencyCollector;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;
+using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing;
+using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector;
+using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse;
+using Microsoft.ApplicationInsights.WindowsServer;
+using Microsoft.ApplicationInsights.WorkerService;
+using Microsoft.ApplicationInsights.WorkerService.TelemetryInitializers;
+using Microsoft.Azure.Functions.Worker.ApplicationInsights;
+using Microsoft.Azure.Functions.Worker.Logging;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.ApplicationInsights;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.Tests.ApplicationInsights;
+
+public class ApplicationInsightsConfigurationTests
+{
+ [Fact]
+ public void AddApplicationInsights_AddsDefaults()
+ {
+ var builder = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults(worker =>
+ {
+ worker
+ .AddApplicationInsights();
+ });
+
+ IEnumerable initializers = null;
+ IEnumerable modules = null;
+
+ builder.ConfigureServices(services =>
+ {
+ initializers = services.Where(s => s.ServiceType == typeof(ITelemetryInitializer));
+ modules = services.Where(s => s.ServiceType == typeof(ITelemetryModule));
+ });
+
+ var provider = builder.Build().Services;
+
+ Assert.Collection(initializers,
+ t => Assert.Equal(typeof(FunctionsTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureWebAppRoleEnvironmentTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(Microsoft.ApplicationInsights.WorkerService.TelemetryInitializers.DomainNameRoleInstanceTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(HttpDependenciesParsingTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(ComponentVersionTelemetryInitializer), t.ImplementationType));
+
+ Assert.Collection(modules,
+ t => Assert.Equal(typeof(FunctionsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DiagnosticsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AppServicesHeartbeatTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureInstanceMetadataTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(PerformanceCollectorModule), t.ImplementationType),
+ t => Assert.Equal(typeof(QuickPulseTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DependencyTrackingTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(EventCounterCollectionModule), t.ImplementationType));
+
+ var middleware = provider.GetRequiredService();
+ Assert.NotNull(middleware);
+ }
+
+ [Fact]
+ public void AddApplicationInsights_CallsConfigure()
+ {
+ bool called = false;
+ var builder = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults(worker =>
+ {
+ worker.AddApplicationInsights(o =>
+ {
+ Assert.NotNull(o);
+ called = true;
+ });
+ });
+
+ Assert.False(called);
+
+ var provider = builder.Build().Services;
+ var options = provider.GetRequiredService>();
+ Assert.NotNull(options.Value);
+
+ var middleware = provider.GetRequiredService();
+ Assert.NotNull(middleware);
+
+ Assert.True(called);
+ }
+
+ [Fact]
+ public void AddApplicationInsightsLogger_AddsDefaults()
+ {
+ var builder = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults(worker =>
+ {
+ worker.AddApplicationInsightsLogger();
+ });
+
+ bool called = false;
+
+ builder.ConfigureServices(services =>
+ {
+ var loggerProviders = services.Where(s => s.ServiceType == typeof(ILoggerProvider));
+ Assert.Collection(loggerProviders,
+ t => Assert.Equal(typeof(WorkerLoggerProvider), t.ImplementationType),
+ t => Assert.Equal(typeof(ApplicationInsightsLoggerProvider), t.ImplementationType));
+
+ var initializers = services.Where(s => s.ServiceType == typeof(ITelemetryInitializer));
+ Assert.Collection(initializers,
+ t => Assert.Equal(typeof(FunctionsTelemetryInitializer), t.ImplementationType));
+
+ var modules = services.Where(s => s.ServiceType == typeof(ITelemetryModule));
+ Assert.Collection(modules,
+ t => Assert.Equal(typeof(FunctionsTelemetryModule), t.ImplementationType));
+
+ called = true;
+ });
+
+ var serviceProvider = builder.Build().Services;
+
+ var appInsightsOptions = serviceProvider.GetRequiredService>();
+ Assert.False(appInsightsOptions.Value.IncludeScopes);
+
+ var userWriter = serviceProvider.GetRequiredService();
+ Assert.IsType(userWriter);
+
+ var systemWriter = serviceProvider.GetRequiredService();
+ Assert.IsNotType(systemWriter);
+
+ var middleware = serviceProvider.GetRequiredService();
+ Assert.NotNull(middleware);
+
+ Assert.True(called);
+ }
+
+ [Fact]
+ public void AddApplicationInsightsLogger_CallsConfigure()
+ {
+ bool called = false;
+ var builder = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults(worker =>
+ {
+ worker.AddApplicationInsightsLogger(o =>
+ {
+ Assert.NotNull(o);
+ called = true;
+ });
+ });
+
+ Assert.False(called);
+
+ var provider = builder.Build().Services;
+ var options = provider.GetRequiredService>();
+ Assert.NotNull(options.Value);
+
+ var middleware = provider.GetRequiredService();
+ Assert.NotNull(middleware);
+
+ Assert.True(called);
+ }
+
+ [Fact]
+ public void AddingServiceAndLogger_OnlyAddsServicesOnce()
+ {
+ var builder = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults(worker =>
+ {
+ worker
+ .AddApplicationInsights()
+ .AddApplicationInsightsLogger();
+ });
+
+ IEnumerable initializers = null;
+ IEnumerable modules = null;
+
+ builder.ConfigureServices(services =>
+ {
+ initializers = services.Where(s => s.ServiceType == typeof(ITelemetryInitializer));
+ modules = services.Where(s => s.ServiceType == typeof(ITelemetryModule));
+ });
+
+ var provider = builder.Build().Services;
+
+ // Ensure that our Initializer and Module are added alongside the defaults
+ Assert.Collection(initializers,
+ t => Assert.Equal(typeof(FunctionsTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureWebAppRoleEnvironmentTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(Microsoft.ApplicationInsights.WorkerService.TelemetryInitializers.DomainNameRoleInstanceTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(HttpDependenciesParsingTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(ComponentVersionTelemetryInitializer), t.ImplementationType));
+
+ Assert.Collection(modules,
+ t => Assert.Equal(typeof(FunctionsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DiagnosticsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AppServicesHeartbeatTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureInstanceMetadataTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(PerformanceCollectorModule), t.ImplementationType),
+ t => Assert.Equal(typeof(QuickPulseTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DependencyTrackingTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(EventCounterCollectionModule), t.ImplementationType));
+
+ var middleware = provider.GetRequiredService();
+ Assert.NotNull(middleware);
+ }
+}
diff --git a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs
new file mode 100644
index 000000000..0b63c16aa
--- /dev/null
+++ b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs
@@ -0,0 +1,142 @@
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ApplicationInsights.Channel;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.Azure.Functions.Worker.Context.Features;
+using Microsoft.Azure.Functions.Worker.Diagnostics;
+using Microsoft.Azure.Functions.Worker.Tests.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.Tests.ApplicationInsights;
+
+public class EndToEndTests
+{
+ private readonly TestTelemetryChannel _channel;
+ private readonly IHost _host;
+ private readonly IFunctionsApplication _application;
+ private readonly IInvocationFeaturesFactory _invocationFeatures;
+
+ public EndToEndTests()
+ {
+ _channel = new TestTelemetryChannel();
+
+ _host = new HostBuilder()
+ .ConfigureServices(services =>
+ {
+ var functionsBuilder = services.AddFunctionsWorkerCore();
+ functionsBuilder
+ .AddApplicationInsights(appInsightsOptions => appInsightsOptions.InstrumentationKey = "abc")
+ .AddApplicationInsightsLogger();
+
+ functionsBuilder.UseDefaultWorkerMiddleware();
+ services.AddDefaultInputConvertersToWorkerOptions();
+
+ // Register our own in-memory channel
+ services.AddSingleton(_channel);
+ services.AddSingleton(_ => new Mock().Object);
+ })
+ .Build();
+
+ _application = _host.Services.GetService();
+ _invocationFeatures = _host.Services.GetService();
+ }
+
+ [Fact]
+ public async Task Logger_SendsTraceAndDependencyTelemetry()
+ {
+ var def = new AppInsightsFunctionDefinition();
+ _application.LoadFunction(def);
+ var invocation = new TestFunctionInvocation(functionId: def.Id);
+
+ var features = _invocationFeatures.Create();
+ features.Set(invocation);
+ var inputConversionProvider = _host.Services.GetRequiredService();
+ inputConversionProvider.TryCreate(typeof(DefaultInputConversionFeature), out var inputConversion);
+ features.Set(new TestFunctionBindingsFeature());
+ features.Set(inputConversion);
+
+ var context = _application.CreateContext(features);
+
+ await _application.InvokeFunctionAsync(context);
+
+ void ValidateProperties(ISupportProperties props)
+ {
+ Assert.Equal(invocation.Id, props.Properties["InvocationId"]);
+ Assert.Contains("ProcessId", props.Properties.Keys);
+ }
+
+ var activity = AppInsightsFunctionDefinition.LastActivity;
+
+ // App Insights can potentially log this, which causes tests to be flaky. Explicitly ignore.
+ var aiTelemetry = _channel.Telemetries.Where(p => p is TraceTelemetry t && t.Message.Contains("AI: TelemetryChannel found a telemetry item"));
+ var telemetries = _channel.Telemetries.Except(aiTelemetry);
+
+ // Log written in test function should go to App Insights directly
+ Assert.Collection(telemetries,
+ t =>
+ {
+ var dependency = (DependencyTelemetry)t;
+
+ Assert.Equal("TestName", dependency.Context.Operation.Name);
+ Assert.Equal(activity.SpanId.ToString(), dependency.Context.Operation.ParentId);
+
+ ValidateProperties(dependency);
+ },
+ t =>
+ {
+ var trace = (TraceTelemetry)t;
+ Assert.Equal("Test", trace.Message);
+ Assert.Equal(SeverityLevel.Warning, trace.SeverityLevel);
+
+ // This ensures we've disabled scopes by default
+ Assert.DoesNotContain("AzureFunctions_InvocationId", trace.Properties.Keys);
+
+ Assert.Equal("TestName", trace.Context.Operation.Name);
+ Assert.Equal(activity.SpanId.ToString(), trace.Context.Operation.ParentId);
+
+ ValidateProperties(trace);
+ });
+ }
+
+ internal class AppInsightsFunctionDefinition : FunctionDefinition
+ {
+ public static readonly string DefaultPathToAssembly = typeof(AppInsightsFunctionDefinition).Assembly.Location;
+ public static readonly string DefaultEntryPoint = $"{typeof(AppInsightsFunctionDefinition).FullName}.{nameof(TestFunction)}";
+ public static readonly string DefaultId = "TestId";
+ public static readonly string DefaultName = "TestName";
+
+ public AppInsightsFunctionDefinition()
+ {
+ Parameters = (new[] { new FunctionParameter("context", typeof(FunctionContext)) }).ToImmutableArray();
+ }
+
+ public override ImmutableArray Parameters { get; }
+
+ public override string PathToAssembly { get; } = DefaultPathToAssembly;
+
+ public override string EntryPoint { get; } = DefaultEntryPoint;
+
+ public override string Id { get; } = DefaultId;
+
+ public override string Name { get; } = DefaultName;
+
+ public override IImmutableDictionary InputBindings { get; } = ImmutableDictionary.Empty;
+
+ public override IImmutableDictionary OutputBindings { get; } = ImmutableDictionary.Empty;
+
+ public static Activity LastActivity;
+
+ public void TestFunction(FunctionContext context)
+ {
+ LastActivity = Activity.Current;
+ var logger = context.GetLogger("TestFunction");
+ logger.LogWarning("Test");
+ }
+ }
+}
diff --git a/test/DotNetWorkerTests/ApplicationInsights/FunctionsTelemetryInitializerTests.cs b/test/DotNetWorkerTests/ApplicationInsights/FunctionsTelemetryInitializerTests.cs
new file mode 100644
index 000000000..f7d3b4dee
--- /dev/null
+++ b/test/DotNetWorkerTests/ApplicationInsights/FunctionsTelemetryInitializerTests.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.ApplicationInsights.Extensibility.Implementation;
+using Microsoft.Azure.Functions.Worker.ApplicationInsights;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.Tests.ApplicationInsights;
+
+public class FunctionsTelemetryInitializerTests
+{
+ [Fact]
+ public void Initialize_SetsContextProperties()
+ {
+ var initializer = new FunctionsTelemetryInitializer("testversion", "testrolename");
+ var telemetry = new TraceTelemetry();
+ initializer.Initialize(telemetry);
+
+ Assert.Equal("testversion", telemetry.Context.GetInternalContext().SdkVersion);
+ Assert.Equal("testrolename", telemetry.Context.Cloud.RoleInstance);
+ }
+
+ [Fact]
+ public void Initialize_SetsProperties_WithActivityTags()
+ {
+ var activity = new Activity("operation");
+ var telemetry = new TraceTelemetry();
+
+ try
+ {
+ activity.Start();
+ activity.AddTag("Name", "MyFunction");
+ activity.AddTag("CustomKey", "CustomValue");
+
+ var initializer = new FunctionsTelemetryInitializer("testversion", "testrolename");
+ initializer.Initialize(telemetry);
+ }
+ finally
+ {
+ activity.Stop();
+ }
+
+ Assert.Equal("MyFunction", telemetry.Context.Operation.Name);
+ Assert.Equal("CustomValue", telemetry.Properties["CustomKey"]);
+ }
+}
diff --git a/test/DotNetWorkerTests/ApplicationInsights/TestTelemetryChannel.cs b/test/DotNetWorkerTests/ApplicationInsights/TestTelemetryChannel.cs
new file mode 100644
index 000000000..798d5aa44
--- /dev/null
+++ b/test/DotNetWorkerTests/ApplicationInsights/TestTelemetryChannel.cs
@@ -0,0 +1,29 @@
+//// Copyright (c) .NET Foundation. All rights reserved.
+//// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Collections.Concurrent;
+using Microsoft.ApplicationInsights.Channel;
+
+namespace Microsoft.Azure.Functions.Worker.Tests.ApplicationInsights;
+
+internal class TestTelemetryChannel : ITelemetryChannel
+{
+ public ConcurrentBag Telemetries { get; private set; } = new ConcurrentBag();
+
+ public bool? DeveloperMode { get; set; }
+
+ public string EndpointAddress { get; set; }
+
+ public void Dispose()
+ {
+ }
+
+ public void Flush()
+ {
+ }
+
+ public void Send(ITelemetry item)
+ {
+ Telemetries.Add(item);
+ }
+}
diff --git a/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs b/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs
index 3053ef121..73d457c08 100644
--- a/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs
+++ b/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs
@@ -9,6 +9,7 @@
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker.Diagnostics;
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
+using Microsoft.Azure.Functions.Worker.Logging;
using Microsoft.Azure.Functions.Worker.Logging.ApplicationInsights;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -17,26 +18,24 @@
namespace Microsoft.Azure.Functions.Worker.Tests.Diagnostics
{
- public class GrpcHostLoggerTests
+ public class GrpcHostLogWriterTests
{
- private readonly GrpcFunctionsHostLoggerProvider _provider;
- private readonly Channel _channel;
+ private readonly Channel _channel = Channel.CreateUnbounded();
+ private readonly IExternalScopeProvider _scopeProvider = new LoggerExternalScopeProvider();
+ private readonly GrpcFunctionsHostLogWriter _logWriter;
+ private readonly Func _formatter = (s, e) => s;
- public GrpcHostLoggerTests()
+ public GrpcHostLogWriterTests()
{
- _channel = Channel.CreateUnbounded();
var outputChannel = new GrpcHostChannel(_channel);
var workerOptions = Options.Create(new WorkerOptions { Serializer = new JsonObjectSerializer() });
- _provider = new GrpcFunctionsHostLoggerProvider(outputChannel, workerOptions);
- _provider.SetScopeProvider(new LoggerExternalScopeProvider());
+ _logWriter = new GrpcFunctionsHostLogWriter(outputChannel, workerOptions);
}
[Fact]
public async Task UserLog()
{
- var logger = _provider.CreateLogger("TestLogger");
-
- logger.LogInformation("user");
+ _logWriter.WriteUserLog(_scopeProvider, "TestLogger", LogLevel.Information, default(EventId), "user", null, _formatter);
_channel.Writer.Complete();
@@ -58,10 +57,10 @@ public async Task UserLog()
[Fact]
public async Task CustomMetric()
{
- var logger = _provider.CreateLogger("TestLogger");
-
- logger.LogMetric("testMetric", 1d, new Dictionary
+ _logWriter.WriteUserMetric(_scopeProvider, new Dictionary
{
+ {"Name", "testMetric" },
+ {"Value", 1d },
{"foo", "bar" }
});
@@ -94,10 +93,9 @@ public async Task CustomMetric()
[Fact]
public async Task SystemLog_WithException_AndScope()
{
- var logger = _provider.CreateLogger("TestLogger");
Exception thrownException = null;
- using (logger.BeginScope(new FunctionInvocationScope("MyFunction", "MyInvocationId")))
+ using (_scopeProvider.Push(new FunctionInvocationScope("MyFunction", "MyInvocationId")))
{
try
{
@@ -105,14 +103,9 @@ public async Task SystemLog_WithException_AndScope()
}
catch (Exception ex)
{
- // The only way to log a system log.
- var log = WorkerMessage.Define(LogLevel.Trace, new EventId(1, "One"), "system log with {param}");
- log(logger, "this", ex);
+ _logWriter.WriteSystemLog(_scopeProvider, "TestLogger", LogLevel.Trace, new EventId(1, "One"), "system log", ex, _formatter);
thrownException = ex;
}
-
- // make sure this is now user
- logger.LogInformation("user");
}
_channel.Writer.Complete();
@@ -124,27 +117,20 @@ public async Task SystemLog_WithException_AndScope()
msgs.Add(msg);
}
- Assert.Collection(msgs,
- p =>
- {
- Assert.Equal("TestLogger", p.RpcLog.Category);
- Assert.Equal(RpcLogCategory.System, p.RpcLog.LogCategory);
- Assert.Equal("system log with this", p.RpcLog.Message);
- Assert.Equal("One", p.RpcLog.EventId);
- Assert.Equal("MyInvocationId", p.RpcLog.InvocationId);
- Assert.Equal(thrownException.ToString(), p.RpcLog.Exception.Message);
- Assert.Equal("Microsoft.Azure.Functions.Worker.Tests", p.RpcLog.Exception.Source);
- Assert.Contains(nameof(SystemLog_WithException_AndScope), p.RpcLog.Exception.StackTrace);
- },
- p =>
- {
- Assert.Equal("TestLogger", p.RpcLog.Category);
- Assert.Equal(RpcLogCategory.User, p.RpcLog.LogCategory);
- Assert.Equal("user", p.RpcLog.Message);
- Assert.Equal("0", p.RpcLog.EventId);
- Assert.Equal("MyInvocationId", p.RpcLog.InvocationId);
- Assert.Null(p.RpcLog.Exception);
- });
+ List> expected = new();
+ expected.Add(p =>
+ {
+ Assert.Equal("TestLogger", p.RpcLog.Category);
+ Assert.Equal(RpcLogCategory.System, p.RpcLog.LogCategory);
+ Assert.Equal("system log", p.RpcLog.Message);
+ Assert.Equal("One", p.RpcLog.EventId);
+ Assert.Equal("MyInvocationId", p.RpcLog.InvocationId);
+ Assert.Equal(thrownException.ToString(), p.RpcLog.Exception.Message);
+ Assert.Equal("Microsoft.Azure.Functions.Worker.Tests", p.RpcLog.Exception.Source);
+ Assert.Contains(nameof(SystemLog_WithException_AndScope), p.RpcLog.Exception.StackTrace);
+ });
+
+ Assert.Collection(msgs, expected.ToArray());
}
}
}
diff --git a/test/DotNetWorkerTests/DotNetWorkerTests.csproj b/test/DotNetWorkerTests/DotNetWorkerTests.csproj
index 7f8c8488b..a14b748a1 100644
--- a/test/DotNetWorkerTests/DotNetWorkerTests.csproj
+++ b/test/DotNetWorkerTests/DotNetWorkerTests.csproj
@@ -6,6 +6,7 @@
Microsoft.Azure.Functions.Worker.Tests
Microsoft.Azure.Functions.Worker.Tests
true
+ preview
..\..\key.snk
disable
@@ -13,7 +14,7 @@
-
+
all
@@ -24,11 +25,8 @@
+
-
-
-
-
diff --git a/test/Worker.ApplicationInsights.Tests/ApplicationInsightsConfigurationTests.cs b/test/Worker.ApplicationInsights.Tests/ApplicationInsightsConfigurationTests.cs
new file mode 100644
index 000000000..87c6ba62f
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/ApplicationInsightsConfigurationTests.cs
@@ -0,0 +1,99 @@
+using System.Linq;
+using Microsoft.ApplicationInsights.DependencyCollector;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.ApplicationInsights.Extensibility.EventCounterCollector;
+using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing;
+using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector;
+using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse;
+using Microsoft.ApplicationInsights.WindowsServer;
+using Microsoft.ApplicationInsights.WorkerService;
+using Microsoft.ApplicationInsights.WorkerService.TelemetryInitializers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.ApplicationInsights;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests;
+
+public class ApplicationInsightsConfigurationTests
+{
+ [Fact]
+ public void AddApplicationInsights_AddsDefaults()
+ {
+ var builder = new TestAppBuilder().AddApplicationInsights();
+
+ // Ensure that our Initializer and Module are added alongside the defaults
+ var initializers = builder.Services.Where(s => s.ServiceType == typeof(ITelemetryInitializer));
+ Assert.Collection(initializers,
+ t => Assert.Equal(typeof(FunctionsTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureWebAppRoleEnvironmentTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(Microsoft.ApplicationInsights.WorkerService.TelemetryInitializers.DomainNameRoleInstanceTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(HttpDependenciesParsingTelemetryInitializer), t.ImplementationType),
+ t => Assert.Equal(typeof(ComponentVersionTelemetryInitializer), t.ImplementationType));
+
+ var modules = builder.Services.Where(s => s.ServiceType == typeof(ITelemetryModule));
+ Assert.Collection(modules,
+ t => Assert.Equal(typeof(FunctionsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DiagnosticsTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AppServicesHeartbeatTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(AzureInstanceMetadataTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(PerformanceCollectorModule), t.ImplementationType),
+ t => Assert.Equal(typeof(QuickPulseTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(DependencyTrackingTelemetryModule), t.ImplementationType),
+ t => Assert.Equal(typeof(EventCounterCollectionModule), t.ImplementationType));
+ }
+
+ [Fact]
+ public void AddApplicationInsights_CallsConfigure()
+ {
+ bool called = false;
+ var builder = new TestAppBuilder().AddApplicationInsights(o =>
+ {
+ Assert.NotNull(o);
+ called = true;
+ });
+
+ Assert.False(called);
+
+ var provider = builder.Services.BuildServiceProvider();
+ var options = provider.GetRequiredService>();
+ Assert.NotNull(options.Value);
+
+ Assert.True(called);
+ }
+
+ [Fact]
+ public void AddApplicationInsightsLogger_AddsDefaults()
+ {
+ var builder = new TestAppBuilder().AddApplicationInsightsLogger();
+
+ var loggerProviders = builder.Services.Where(s => s.ServiceType == typeof(ILoggerProvider));
+ Assert.Collection(loggerProviders,
+ t => Assert.Equal(typeof(ApplicationInsightsLoggerProvider), t.ImplementationType));
+
+ var serviceProvider = builder.Services.BuildServiceProvider();
+ var workerOptions = serviceProvider.GetRequiredService>();
+ Assert.True(workerOptions.Value.DisableHostLogger);
+ }
+
+ [Fact]
+ public void AddApplicationInsightsLogger_CallsConfigure()
+ {
+ bool called = false;
+ var builder = new TestAppBuilder().AddApplicationInsightsLogger(o =>
+ {
+ Assert.NotNull(o);
+ called = true;
+ });
+
+ Assert.False(called);
+
+ var provider = builder.Services.BuildServiceProvider();
+ var options = provider.GetRequiredService>();
+ Assert.NotNull(options.Value);
+
+ Assert.True(called);
+
+ }
+}
diff --git a/test/Worker.ApplicationInsights.Tests/EndToEndTests.cs b/test/Worker.ApplicationInsights.Tests/EndToEndTests.cs
new file mode 100644
index 000000000..916ff0b4a
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/EndToEndTests.cs
@@ -0,0 +1,102 @@
+using System.Collections.Immutable;
+using System.Threading.Tasks;
+using Microsoft.ApplicationInsights.Channel;
+using Microsoft.Azure.Functions.Worker.Context.Features;
+using Microsoft.Azure.Functions.Worker.Diagnostics;
+using Microsoft.Azure.Functions.Worker.Tests;
+using Microsoft.Azure.Functions.Worker.Tests.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests;
+
+public class EndToEndTests
+{
+ private readonly TestTelemetryChannel _channel;
+ private readonly IHost _host;
+ private readonly IFunctionsApplication _application;
+ private readonly IInvocationFeaturesFactory _invocationFeatures;
+
+ public EndToEndTests()
+ {
+ _channel = new TestTelemetryChannel();
+
+ _host = new HostBuilder()
+ .ConfigureServices(services =>
+ {
+ var functionsBuilder = services.AddFunctionsWorkerCore();
+ functionsBuilder
+ .AddApplicationInsights(appInsightsOptions => appInsightsOptions.InstrumentationKey = "abc")
+ .AddApplicationInsightsLogger();
+
+ functionsBuilder.UseDefaultWorkerMiddleware();
+ services.AddDefaultInputConvertersToWorkerOptions();
+
+ // Register our own in-memory channel
+ services.AddSingleton(_channel);
+ services.AddSingleton(_ => new Mock().Object);
+ })
+ .Build();
+
+ _application = _host.Services.GetService();
+ _invocationFeatures = _host.Services.GetService();
+ }
+
+ [Fact]
+ public async Task DoIt()
+ {
+ var def = new AppInsightsFunctionDefinition();
+ _application.LoadFunction(def);
+
+ var invocation = new TestFunctionInvocation(functionId: def.Id);
+
+ var features = _invocationFeatures.Create();
+ features.Set(invocation);
+
+ var inputConversionProvider = _host.Services.GetRequiredService();
+ inputConversionProvider.TryCreate(typeof(DefaultInputConversionFeature), out var inputConversion);
+
+ features.Set(new TestFunctionBindingsFeature());
+ features.Set(inputConversion);
+
+ var context = _application.CreateContext(features);
+
+ await _application.InvokeFunctionAsync(context);
+ }
+
+ internal class AppInsightsFunctionDefinition : FunctionDefinition
+ {
+ public static readonly string DefaultPathToAssembly = typeof(AppInsightsFunctionDefinition).Assembly.Location;
+ public static readonly string DefaultEntryPoint = $"{typeof(AppInsightsFunctionDefinition).FullName}.{nameof(TestFunction)}";
+ public static readonly string DefaultId = "TestId";
+ public static readonly string DefaultName = "TestName";
+
+ public AppInsightsFunctionDefinition()
+ {
+ Parameters = (new[] { new FunctionParameter("context", typeof(FunctionContext)) }).ToImmutableArray();
+ }
+
+ public override ImmutableArray Parameters { get; }
+
+ public override string PathToAssembly { get; } = DefaultPathToAssembly;
+
+ public override string EntryPoint { get; } = DefaultEntryPoint;
+
+ public override string Id { get; } = DefaultId;
+
+ public override string Name { get; } = DefaultName;
+
+ public override IImmutableDictionary InputBindings { get; } = ImmutableDictionary.Empty;
+
+ public override IImmutableDictionary OutputBindings { get; } = ImmutableDictionary.Empty;
+
+ public void TestFunction(FunctionContext context)
+ {
+ var logger = context.GetLogger("TestFunction");
+ logger.LogWarning("Test");
+ }
+ }
+}
diff --git a/test/Worker.ApplicationInsights.Tests/FunctionsTelemetryInitializerTests.cs b/test/Worker.ApplicationInsights.Tests/FunctionsTelemetryInitializerTests.cs
new file mode 100644
index 000000000..9054bb2b8
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/FunctionsTelemetryInitializerTests.cs
@@ -0,0 +1,44 @@
+using System.Diagnostics;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.ApplicationInsights.Extensibility.Implementation;
+using Xunit;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests;
+
+public class FunctionsTelemetryInitializerTests
+{
+ [Fact]
+ public void Initialize_SetsContextProperties()
+ {
+ var initializer = new FunctionsTelemetryInitializer("testversion", "testrolename");
+ var telemetry = new TraceTelemetry();
+ initializer.Initialize(telemetry);
+
+ Assert.Equal("testversion", telemetry.Context.GetInternalContext().SdkVersion);
+ Assert.Equal("testrolename", telemetry.Context.Cloud.RoleInstance);
+ }
+
+ [Fact]
+ public void Initialize_SetsProperties_WithActivityTags()
+ {
+ var activity = new Activity("operation");
+ var telemetry = new TraceTelemetry();
+
+ try
+ {
+ activity.Start();
+ activity.AddTag("Name", "MyFunction");
+ activity.AddTag("CustomKey", "CustomValue");
+
+ var initializer = new FunctionsTelemetryInitializer("testversion", "testrolename");
+ initializer.Initialize(telemetry);
+ }
+ finally
+ {
+ activity.Stop();
+ }
+
+ Assert.Equal("MyFunction", telemetry.Context.Operation.Name);
+ Assert.Equal("CustomValue", telemetry.Properties["CustomKey"]);
+ }
+}
diff --git a/test/Worker.ApplicationInsights.Tests/TestAppBuilder.cs b/test/Worker.ApplicationInsights.Tests/TestAppBuilder.cs
new file mode 100644
index 000000000..1930942bc
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/TestAppBuilder.cs
@@ -0,0 +1,15 @@
+using System;
+using Microsoft.Azure.Functions.Worker.Middleware;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests;
+
+internal class TestAppBuilder : IFunctionsWorkerApplicationBuilder
+{
+ public IServiceCollection Services { get; } = new ServiceCollection();
+
+ public IFunctionsWorkerApplicationBuilder Use(Func middleware)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/test/Worker.ApplicationInsights.Tests/TestTelemetryChannel.cs b/test/Worker.ApplicationInsights.Tests/TestTelemetryChannel.cs
new file mode 100644
index 000000000..8e762f880
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/TestTelemetryChannel.cs
@@ -0,0 +1,29 @@
+//// Copyright (c) .NET Foundation. All rights reserved.
+//// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Collections.Concurrent;
+using Microsoft.ApplicationInsights.Channel;
+
+namespace Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests;
+
+internal class TestTelemetryChannel : ITelemetryChannel
+{
+ public ConcurrentBag Telemetries { get; private set; } = new ConcurrentBag();
+
+ public bool? DeveloperMode { get; set; }
+
+ public string EndpointAddress { get; set; }
+
+ public void Dispose()
+ {
+ }
+
+ public void Flush()
+ {
+ }
+
+ public void Send(ITelemetry item)
+ {
+ Telemetries.Add(item);
+ }
+}
diff --git a/test/Worker.ApplicationInsights.Tests/Worker.ApplicationInsights.Tests.csproj b/test/Worker.ApplicationInsights.Tests/Worker.ApplicationInsights.Tests.csproj
new file mode 100644
index 000000000..436cff015
--- /dev/null
+++ b/test/Worker.ApplicationInsights.Tests/Worker.ApplicationInsights.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0
+ false
+ Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests
+ Microsoft.Azure.Functions.Worker.ApplicationInsights.Tests
+ true
+ ..\..\key.snk
+ disable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+