Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/FunctionMetadataLoaderExtension/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public void Configure(WebJobsBuilderContext context, IWebJobsConfigurationBuilde
{ _dotnetIsolatedWorkerConfigPath, appRootPath },
{ _dotnetIsolatedWorkerExePath, newWorkerDescription.DefaultExecutablePath! }
});

Environment.SetEnvironmentVariable("DOTNET_NOLOGO", "true");
Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if this will help any at startup? I'm trying it out...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the telemetry option wouldn't impact much, as it won't likely ever be running for the first time. Nologo might.

Environment.SetEnvironmentVariable("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "true");
}

public void Configure(WebJobsBuilderContext context, IWebJobsBuilder builder)
Expand Down
15 changes: 15 additions & 0 deletions src/DotNetWorker.Core/Diagnostics/IWorkerDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.Azure.Functions.Worker.Diagnostics
{
/// <summary>
/// Represents an interface for sending logs directly to the Fucctions host.
/// </summary>
internal interface IWorkerDiagnostics
{
void OnApplicationCreated(WorkerInformation workerInfo);

void OnFunctionLoaded(FunctionDefinition definition);
}
}
8 changes: 5 additions & 3 deletions src/DotNetWorker.Core/FunctionsApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ internal partial class FunctionsApplication : IFunctionsApplication
private readonly ConcurrentDictionary<string, FunctionDefinition> _functionMap = new ConcurrentDictionary<string, FunctionDefinition>();
private readonly FunctionExecutionDelegate _functionExecutionDelegate;
private readonly IFunctionContextFactory _functionContextFactory;
private readonly ILogger<FunctionsApplication> _logger;
private readonly IOptions<WorkerOptions> _workerOptions;
private readonly ILogger<FunctionsApplication> _logger;
private readonly IWorkerDiagnostics _diagnostics;

public FunctionsApplication(FunctionExecutionDelegate functionExecutionDelegate, IFunctionContextFactory functionContextFactory,
IOptions<WorkerOptions> workerOptions, ILogger<FunctionsApplication> logger)
IOptions<WorkerOptions> workerOptions, ILogger<FunctionsApplication> logger, IWorkerDiagnostics diagnostics)
{
_functionExecutionDelegate = functionExecutionDelegate ?? throw new ArgumentNullException(nameof(functionExecutionDelegate));
_functionContextFactory = functionContextFactory ?? throw new ArgumentNullException(nameof(functionContextFactory));
_workerOptions = workerOptions ?? throw new ArgumentNullException(nameof(workerOptions));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
}

public FunctionContext CreateContext(IInvocationFeatures features)
Expand All @@ -50,7 +52,7 @@ public void LoadFunction(FunctionDefinition definition)
throw new InvalidOperationException($"Unable to load Function '{definition.Name}'. A function with the id '{definition.Id}' name already exists.");
}

Log.FunctionDefinitionCreated(_logger, definition);
_diagnostics.OnFunctionLoaded(definition);
}

public Task InvokeFunctionAsync(FunctionContext context)
Expand Down
11 changes: 8 additions & 3 deletions src/DotNetWorker.Core/WorkerHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Diagnostics;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Azure.Functions.Worker
{
internal class WorkerHostedService : IHostedService
{
private readonly IWorker _worker;
private readonly IWorkerDiagnostics _diagnostics;

public WorkerHostedService(IWorker worker)
public WorkerHostedService(IWorker worker, IWorkerDiagnostics diagnostics)
{
_worker = worker ?? throw new ArgumentNullException(nameof(worker));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
}

public Task StartAsync(CancellationToken cancellationToken)
public async Task StartAsync(CancellationToken cancellationToken)
{
return _worker.StartAsync(cancellationToken);
await _worker.StartAsync(cancellationToken);

_diagnostics.OnApplicationCreated(WorkerInformation.Instance);
}

public Task StopAsync(CancellationToken cancellationToken)
Expand Down
38 changes: 38 additions & 0 deletions src/DotNetWorker.Core/WorkerInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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 System.Runtime.InteropServices;

namespace Microsoft.Azure.Functions.Worker
{
internal class WorkerInformation
{
private static Assembly _thisAssembly = typeof(WorkerInformation).Assembly;
private static FileVersionInfo _fileVersionInfo = FileVersionInfo.GetVersionInfo(_thisAssembly.Location);

private WorkerInformation()
{
}

public static WorkerInformation Instance = new WorkerInformation();

public int ProcessId { get; } = Process.GetCurrentProcess().Id;

public string WorkerVersion { get; } = _thisAssembly.GetName().Version?.ToString()!;

public string? ProductVersion { get; } = _fileVersionInfo.ProductVersion;

public string FrameworkDescription => RuntimeInformation.FrameworkDescription;

public string OSDescription => RuntimeInformation.OSDescription;

public Architecture OSArchitecture => RuntimeInformation.OSArchitecture;

public string RuntimeIdentifier => RuntimeInformation.RuntimeIdentifier;

public string CommandLine => Environment.CommandLine;
}
}
2 changes: 2 additions & 0 deletions src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Grpc.Net.Client;
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;
Expand Down Expand Up @@ -42,6 +43,7 @@ public static IServiceCollection AddGrpc(this IServiceCollection services)
services.AddLogging(logging =>
{
logging.Services.AddSingleton<ILoggerProvider, GrpcFunctionsHostLoggerProvider>();
logging.Services.AddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();
});

// gRPC Core services
Expand Down
2 changes: 1 addition & 1 deletion src/DotNetWorker.Grpc/GrpcWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest re
var response = new WorkerInitResponse
{
Result = new StatusResult { Status = StatusResult.Types.Status.Success },
WorkerVersion = typeof(IWorker).Assembly.GetName().Version?.ToString()
WorkerVersion = WorkerInformation.Instance.WorkerVersion
};

response.Capabilities.Add("RpcHttpBodyOnly", bool.TrueString);
Expand Down
87 changes: 87 additions & 0 deletions src/DotNetWorker.Grpc/Http/GrpcWorkerDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using Microsoft.Azure.Functions.Worker.Diagnostics;
using Microsoft.Azure.Functions.Worker.Grpc.Messages;

namespace Microsoft.Azure.Functions.Worker.Grpc
{

internal class GrpcWorkerDiagnostics : IWorkerDiagnostics
{
private ChannelWriter<StreamingMessage> _outputChannel;

internal static readonly JsonSerializerOptions SerializerOptions;

static GrpcWorkerDiagnostics()
{
SerializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
};

SerializerOptions.Converters.Add(new JsonStringEnumConverter());
SerializerOptions.Converters.Add(new TypeNameConverter());
}

public GrpcWorkerDiagnostics(GrpcHostChannel hostChannel)
{
if (hostChannel == null)
{
throw new ArgumentNullException(nameof(hostChannel));
}

if (hostChannel.Channel == null)
{
throw new InvalidOperationException($"{nameof(Channel)} cannot be null.");
}

_outputChannel = hostChannel.Channel.Writer ?? throw new InvalidOperationException($"Writer cannot be null.");
}

public void OnApplicationCreated(WorkerInformation workerInfo)
{
var message = new StreamingMessage
{
RpcLog = new RpcLog
{
EventId = nameof(OnApplicationCreated),
Level = RpcLog.Types.Level.Debug,
LogCategory = RpcLog.Types.RpcLogCategory.System,
Message = JsonSerializer.Serialize(workerInfo, SerializerOptions)
}
};

_outputChannel.TryWrite(message);
}

public void OnFunctionLoaded(FunctionDefinition definition)
{
var message = new StreamingMessage
{
RpcLog = new RpcLog
{
EventId = nameof(OnFunctionLoaded),
Level = RpcLog.Types.Level.Debug,
LogCategory = RpcLog.Types.RpcLogCategory.System,
Message = JsonSerializer.Serialize(definition, SerializerOptions)
}
};
}

private class TypeNameConverter : JsonConverter<Type>
{
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// We'll never deserialize.
throw new NotSupportedException();
}

public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.FullName);
}
}
}
}
8 changes: 4 additions & 4 deletions test/DotNetWorkerTests/Helpers/TestLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ namespace Microsoft.Azure.Functions.Worker.Tests
public class TestLoggerProvider : ILoggerProvider, ISupportExternalScope
{
private IExternalScopeProvider _scopeProvider;
private Lazy<ILoggerFactory> _lazyFactory;
private static Lazy<ILoggerFactory> _lazyFactory;

public TestLoggerProvider()
static TestLoggerProvider()
{
_lazyFactory = new Lazy<ILoggerFactory>(() =>
{
return new ServiceCollection()
.AddLogging(b =>
{
b.AddProvider(this);
b.AddProvider(new TestLoggerProvider());
})
.BuildServiceProvider()
.GetService<ILoggerFactory>();
Expand All @@ -31,7 +31,7 @@ public TestLoggerProvider()

private ConcurrentDictionary<string, TestLogger> LoggerCache { get; } = new ConcurrentDictionary<string, TestLogger>();

public ILoggerFactory Factory => _lazyFactory.Value;
public static ILoggerFactory Factory => _lazyFactory.Value;

public IEnumerable<TestLogger> CreatedLoggers => LoggerCache.Values;

Expand Down
27 changes: 27 additions & 0 deletions test/DotNetWorkerTests/Helpers/TestLoggerT.cs
Original file line number Diff line number Diff line change
@@ -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.Linq;
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.Functions.Worker.Tests
{
public class TestLogger<T> : TestLogger, ILogger<T>
{
private TestLogger(string category)
: base(category)
{
}

public static TestLogger<T> Create()
{
// We want to use the logic for category naming which is internal to LoggerFactory.
// So we'll create a TestLogger via the LoggerFactory and grab it's category.
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
LoggerFactory _testLoggerFactory = new LoggerFactory(new[] { testLoggerProvider });
_testLoggerFactory.CreateLogger<T>();
TestLogger testLogger = testLoggerProvider.CreatedLoggers.Single();
return new TestLogger<T>(testLogger.Category);
}
}
}
59 changes: 54 additions & 5 deletions test/DotNetWorkerTests/TestFunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,78 @@ namespace Microsoft.Azure.Functions.Worker.Tests
{
public class TestFunctionDefinition : FunctionDefinition
{
public TestFunctionDefinition(string functionId = null, IDictionary<string, BindingMetadata> outputBindings = null, IEnumerable<FunctionParameter> parameters = null)
public static readonly string DefaultPathToAssembly = typeof(TestFunctionDefinition).Assembly.Location;
public static readonly string DefaultEntrypPoint = $"{nameof(TestFunctionDefinition)}.{nameof(DefaultEntrypPoint)}";
public static readonly string DefaultId = "TestId";
public static readonly string DefaultName = "TestName";

public TestFunctionDefinition(string functionId = null, IDictionary<string, BindingMetadata> inputBindings = null, IDictionary<string, BindingMetadata> outputBindings = null, IEnumerable<FunctionParameter> parameters = null)
{
if (functionId is not null)
{
Id = functionId;
}

Parameters = parameters == null ? ImmutableArray<FunctionParameter>.Empty : parameters.ToImmutableArray();
InputBindings = inputBindings == null ? ImmutableDictionary<string, BindingMetadata>.Empty : inputBindings.ToImmutableDictionary();
OutputBindings = outputBindings == null ? ImmutableDictionary<string, BindingMetadata>.Empty : outputBindings.ToImmutableDictionary();
}

public override ImmutableArray<FunctionParameter> Parameters { get; }

public override string PathToAssembly { get; }
public override string PathToAssembly { get; } = DefaultPathToAssembly;

public override string EntryPoint { get; }
public override string EntryPoint { get; } = DefaultEntrypPoint;

public override string Id { get; } = Guid.NewGuid().ToString();
public override string Id { get; } = DefaultId;

public override string Name { get; }
public override string Name { get; } = DefaultName;

public override IImmutableDictionary<string, BindingMetadata> InputBindings { get; }

public override IImmutableDictionary<string, BindingMetadata> OutputBindings { get; }

/// <summary>
/// Generates a pre-made <see cref="FunctionDefinition"/> for testing. Always includes a single trigger named "TestTrigger".
/// </summary>
/// <param name="inputBindingCount">The number of input bindings to generate. Names will be of the format "TestInput0", "TestInput1", etc.</param>
/// <param name="outputBindingCount">The number of output bindings to generate. Names will be of the format "TestOutput0", "TestOutput1", etc.</param>
/// <param name="paramTypes">A list of types that will be used to generate the <see cref="Parameters"/>. Names will be of the format "Parameter0", "Parameter1", etc.</param>
/// <returns>The generated <see cref="FunctionDefinition"/>.</returns>
public static FunctionDefinition Generate(int inputBindingCount = 0, int outputBindingCount = 0, params Type[] paramTypes)
{
var inputs = new Dictionary<string, BindingMetadata>();
var outputs = new Dictionary<string, BindingMetadata>();
var parameters = new List<FunctionParameter>();

// Always provide a trigger
inputs.Add($"triggerName", new TestBindingMetadata("TestTrigger", BindingDirection.In));

for (int i = 0; i < inputBindingCount; i++)
{
inputs.Add($"inputName{i}", new TestBindingMetadata($"TestInput{i}", BindingDirection.In));
}

for (int i = 0; i < outputBindingCount; i++)
{
outputs.Add($"outputName{i}", new TestBindingMetadata($"TestOutput{i}", BindingDirection.Out));
}

for (int i = 0; i < paramTypes.Length; i++)
{
var properties = new Dictionary<string, object>
{
{"TestPropertyKey", "TestPropertyValue" }
};

parameters.Add(new FunctionParameter($"Parameter{i}", paramTypes[i], properties.ToImmutableDictionary()));
}

return new TestFunctionDefinition(inputBindings: inputs, outputBindings: outputs, parameters: parameters);
}

private void DefaultEntryPoint()
{
}
}
}
Loading