Skip to content

Commit 9c91a5f

Browse files
authored
adding diagnostic logs for startup and function loading (#236)
1 parent 6556367 commit 9c91a5f

File tree

12 files changed

+252
-17
lines changed

12 files changed

+252
-17
lines changed

sdk/FunctionMetadataLoaderExtension/Startup.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public void Configure(WebJobsBuilderContext context, IWebJobsConfigurationBuilde
3939
{ _dotnetIsolatedWorkerConfigPath, appRootPath },
4040
{ _dotnetIsolatedWorkerExePath, newWorkerDescription.DefaultExecutablePath! }
4141
});
42+
43+
Environment.SetEnvironmentVariable("DOTNET_NOLOGO", "true");
44+
Environment.SetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true");
45+
Environment.SetEnvironmentVariable("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "true");
4246
}
4347

4448
public void Configure(WebJobsBuilderContext context, IWebJobsBuilder builder)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Azure.Functions.Worker.Diagnostics
5+
{
6+
/// <summary>
7+
/// Represents an interface for sending logs directly to the Fucctions host.
8+
/// </summary>
9+
internal interface IWorkerDiagnostics
10+
{
11+
void OnApplicationCreated(WorkerInformation workerInfo);
12+
13+
void OnFunctionLoaded(FunctionDefinition definition);
14+
}
15+
}

src/DotNetWorker.Core/FunctionsApplication.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ internal partial class FunctionsApplication : IFunctionsApplication
1616
private readonly ConcurrentDictionary<string, FunctionDefinition> _functionMap = new ConcurrentDictionary<string, FunctionDefinition>();
1717
private readonly FunctionExecutionDelegate _functionExecutionDelegate;
1818
private readonly IFunctionContextFactory _functionContextFactory;
19-
private readonly ILogger<FunctionsApplication> _logger;
2019
private readonly IOptions<WorkerOptions> _workerOptions;
20+
private readonly ILogger<FunctionsApplication> _logger;
21+
private readonly IWorkerDiagnostics _diagnostics;
2122

2223
public FunctionsApplication(FunctionExecutionDelegate functionExecutionDelegate, IFunctionContextFactory functionContextFactory,
23-
IOptions<WorkerOptions> workerOptions, ILogger<FunctionsApplication> logger)
24+
IOptions<WorkerOptions> workerOptions, ILogger<FunctionsApplication> logger, IWorkerDiagnostics diagnostics)
2425
{
2526
_functionExecutionDelegate = functionExecutionDelegate ?? throw new ArgumentNullException(nameof(functionExecutionDelegate));
2627
_functionContextFactory = functionContextFactory ?? throw new ArgumentNullException(nameof(functionContextFactory));
2728
_workerOptions = workerOptions ?? throw new ArgumentNullException(nameof(workerOptions));
2829
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
30+
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
2931
}
3032

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

53-
Log.FunctionDefinitionCreated(_logger, definition);
55+
_diagnostics.OnFunctionLoaded(definition);
5456
}
5557

5658
public Task InvokeFunctionAsync(FunctionContext context)

src/DotNetWorker.Core/WorkerHostedService.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@
44
using System;
55
using System.Threading;
66
using System.Threading.Tasks;
7+
using Microsoft.Azure.Functions.Worker.Diagnostics;
78
using Microsoft.Extensions.Hosting;
89

910
namespace Microsoft.Azure.Functions.Worker
1011
{
1112
internal class WorkerHostedService : IHostedService
1213
{
1314
private readonly IWorker _worker;
15+
private readonly IWorkerDiagnostics _diagnostics;
1416

15-
public WorkerHostedService(IWorker worker)
17+
public WorkerHostedService(IWorker worker, IWorkerDiagnostics diagnostics)
1618
{
1719
_worker = worker ?? throw new ArgumentNullException(nameof(worker));
20+
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
1821
}
1922

20-
public Task StartAsync(CancellationToken cancellationToken)
23+
public async Task StartAsync(CancellationToken cancellationToken)
2124
{
22-
return _worker.StartAsync(cancellationToken);
25+
await _worker.StartAsync(cancellationToken);
26+
27+
_diagnostics.OnApplicationCreated(WorkerInformation.Instance);
2328
}
2429

2530
public Task StopAsync(CancellationToken cancellationToken)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Reflection;
7+
using System.Runtime.InteropServices;
8+
9+
namespace Microsoft.Azure.Functions.Worker
10+
{
11+
internal class WorkerInformation
12+
{
13+
private static Assembly _thisAssembly = typeof(WorkerInformation).Assembly;
14+
private static FileVersionInfo _fileVersionInfo = FileVersionInfo.GetVersionInfo(_thisAssembly.Location);
15+
16+
private WorkerInformation()
17+
{
18+
}
19+
20+
public static WorkerInformation Instance = new WorkerInformation();
21+
22+
public int ProcessId { get; } = Process.GetCurrentProcess().Id;
23+
24+
public string WorkerVersion { get; } = _thisAssembly.GetName().Version?.ToString()!;
25+
26+
public string? ProductVersion { get; } = _fileVersionInfo.ProductVersion;
27+
28+
public string FrameworkDescription => RuntimeInformation.FrameworkDescription;
29+
30+
public string OSDescription => RuntimeInformation.OSDescription;
31+
32+
public Architecture OSArchitecture => RuntimeInformation.OSArchitecture;
33+
34+
public string RuntimeIdentifier => RuntimeInformation.RuntimeIdentifier;
35+
36+
public string CommandLine => Environment.CommandLine;
37+
}
38+
}

src/DotNetWorker.Grpc/GrpcServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Grpc.Net.Client;
88
using Microsoft.Azure.Functions.Worker;
99
using Microsoft.Azure.Functions.Worker.Diagnostics;
10+
using Microsoft.Azure.Functions.Worker.Grpc;
1011
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
1112
using Microsoft.Extensions.Configuration;
1213
using Microsoft.Extensions.DependencyInjection;
@@ -42,6 +43,7 @@ public static IServiceCollection AddGrpc(this IServiceCollection services)
4243
services.AddLogging(logging =>
4344
{
4445
logging.Services.AddSingleton<ILoggerProvider, GrpcFunctionsHostLoggerProvider>();
46+
logging.Services.AddSingleton<IWorkerDiagnostics, GrpcWorkerDiagnostics>();
4547
});
4648

4749
// gRPC Core services

src/DotNetWorker.Grpc/GrpcWorker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ internal static WorkerInitResponse WorkerInitRequestHandler(WorkerInitRequest re
218218
var response = new WorkerInitResponse
219219
{
220220
Result = new StatusResult { Status = StatusResult.Types.Status.Success },
221-
WorkerVersion = typeof(IWorker).Assembly.GetName().Version?.ToString()
221+
WorkerVersion = WorkerInformation.Instance.WorkerVersion
222222
};
223223

224224
response.Capabilities.Add("RpcHttpBodyOnly", bool.TrueString);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using System.Threading.Channels;
5+
using Microsoft.Azure.Functions.Worker.Diagnostics;
6+
using Microsoft.Azure.Functions.Worker.Grpc.Messages;
7+
8+
namespace Microsoft.Azure.Functions.Worker.Grpc
9+
{
10+
11+
internal class GrpcWorkerDiagnostics : IWorkerDiagnostics
12+
{
13+
private ChannelWriter<StreamingMessage> _outputChannel;
14+
15+
internal static readonly JsonSerializerOptions SerializerOptions;
16+
17+
static GrpcWorkerDiagnostics()
18+
{
19+
SerializerOptions = new JsonSerializerOptions
20+
{
21+
WriteIndented = true,
22+
};
23+
24+
SerializerOptions.Converters.Add(new JsonStringEnumConverter());
25+
SerializerOptions.Converters.Add(new TypeNameConverter());
26+
}
27+
28+
public GrpcWorkerDiagnostics(GrpcHostChannel hostChannel)
29+
{
30+
if (hostChannel == null)
31+
{
32+
throw new ArgumentNullException(nameof(hostChannel));
33+
}
34+
35+
if (hostChannel.Channel == null)
36+
{
37+
throw new InvalidOperationException($"{nameof(Channel)} cannot be null.");
38+
}
39+
40+
_outputChannel = hostChannel.Channel.Writer ?? throw new InvalidOperationException($"Writer cannot be null.");
41+
}
42+
43+
public void OnApplicationCreated(WorkerInformation workerInfo)
44+
{
45+
var message = new StreamingMessage
46+
{
47+
RpcLog = new RpcLog
48+
{
49+
EventId = nameof(OnApplicationCreated),
50+
Level = RpcLog.Types.Level.Debug,
51+
LogCategory = RpcLog.Types.RpcLogCategory.System,
52+
Message = JsonSerializer.Serialize(workerInfo, SerializerOptions)
53+
}
54+
};
55+
56+
_outputChannel.TryWrite(message);
57+
}
58+
59+
public void OnFunctionLoaded(FunctionDefinition definition)
60+
{
61+
var message = new StreamingMessage
62+
{
63+
RpcLog = new RpcLog
64+
{
65+
EventId = nameof(OnFunctionLoaded),
66+
Level = RpcLog.Types.Level.Debug,
67+
LogCategory = RpcLog.Types.RpcLogCategory.System,
68+
Message = JsonSerializer.Serialize(definition, SerializerOptions)
69+
}
70+
};
71+
}
72+
73+
private class TypeNameConverter : JsonConverter<Type>
74+
{
75+
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
76+
{
77+
// We'll never deserialize.
78+
throw new NotSupportedException();
79+
}
80+
81+
public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
82+
{
83+
writer.WriteStringValue(value.FullName);
84+
}
85+
}
86+
}
87+
}

test/DotNetWorkerTests/Helpers/TestLoggerProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ namespace Microsoft.Azure.Functions.Worker.Tests
1313
public class TestLoggerProvider : ILoggerProvider, ISupportExternalScope
1414
{
1515
private IExternalScopeProvider _scopeProvider;
16-
private Lazy<ILoggerFactory> _lazyFactory;
16+
private static Lazy<ILoggerFactory> _lazyFactory;
1717

18-
public TestLoggerProvider()
18+
static TestLoggerProvider()
1919
{
2020
_lazyFactory = new Lazy<ILoggerFactory>(() =>
2121
{
2222
return new ServiceCollection()
2323
.AddLogging(b =>
2424
{
25-
b.AddProvider(this);
25+
b.AddProvider(new TestLoggerProvider());
2626
})
2727
.BuildServiceProvider()
2828
.GetService<ILoggerFactory>();
@@ -31,7 +31,7 @@ public TestLoggerProvider()
3131

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

34-
public ILoggerFactory Factory => _lazyFactory.Value;
34+
public static ILoggerFactory Factory => _lazyFactory.Value;
3535

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.Azure.Functions.Worker.Tests
8+
{
9+
public class TestLogger<T> : TestLogger, ILogger<T>
10+
{
11+
private TestLogger(string category)
12+
: base(category)
13+
{
14+
}
15+
16+
public static TestLogger<T> Create()
17+
{
18+
// We want to use the logic for category naming which is internal to LoggerFactory.
19+
// So we'll create a TestLogger via the LoggerFactory and grab it's category.
20+
TestLoggerProvider testLoggerProvider = new TestLoggerProvider();
21+
LoggerFactory _testLoggerFactory = new LoggerFactory(new[] { testLoggerProvider });
22+
_testLoggerFactory.CreateLogger<T>();
23+
TestLogger testLogger = testLoggerProvider.CreatedLoggers.Single();
24+
return new TestLogger<T>(testLogger.Category);
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)