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 + + + +