Skip to content

Commit 03bd635

Browse files
Surface exception type and details from out-of-proc user code to App Insights (#8314)
* Surface exception type and details from out-of-proc user code to App Insights * test * var rename and clarifying comment * reflect protobuf additions * nits * switch setting feature flag from app setting to setting from capability * to do - flesh out design details for feature flag * option setting from worker working * Update RpcWorkerConstants.cs accidental addition from dev merge * reference versions * package ref's * version
1 parent efbb61c commit 03bd635

File tree

10 files changed

+192
-9
lines changed

10 files changed

+192
-9
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- My change description (#PR)
44
-->
55

6+
- Surface exception type and details from out-of-proc user code to App Insights (#8314)
67
- Updated Java Worker Version to [2.3.1](https://github.com/Azure/azure-functions-java-worker/releases/tag/2.3.1)
78

89
**Release sprint:** Sprint 123

src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,9 +655,11 @@ private IList<string> GetOutputMaps(IList<ParameterBinding> bindings)
655655
internal async Task InvokeResponse(InvocationResponse invokeResponse)
656656
{
657657
_workerChannelLogger.LogDebug("InvocationResponse received for invocation id: '{invocationId}'", invokeResponse.InvocationId);
658+
// Check if the worker supports logging user-code-thrown exceptions to app insights
659+
bool capabilityEnabled = !string.IsNullOrEmpty(_workerCapabilities.GetCapabilityState(RpcWorkerConstants.EnableUserCodeException));
658660

659661
if (_executingInvocations.TryRemove(invokeResponse.InvocationId, out ScriptInvocationContext context)
660-
&& invokeResponse.Result.IsSuccess(context.ResultSource))
662+
&& invokeResponse.Result.IsInvocationSuccess(context.ResultSource, capabilityEnabled))
661663
{
662664
try
663665
{

src/WebJobs.Script.Grpc/MessageExtensions/StatusResultExtensions.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Grpc.Core;
7+
using Microsoft.Azure.WebJobs.Script.Config;
68
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
79

810
namespace Microsoft.Azure.WebJobs.Script.Grpc
@@ -27,12 +29,17 @@ public static bool IsFailure(this StatusResult statusResult, out Exception excep
2729
}
2830
}
2931

30-
public static bool IsSuccess<T>(this StatusResult status, TaskCompletionSource<T> tcs)
32+
/// <summary>
33+
/// This method is only hit on the invocation code path. enableUserCodeExceptionCapability = feature flag,
34+
/// exposed as a capability that is set by the worker.
35+
/// </summary>
36+
public static bool IsInvocationSuccess<T>(this StatusResult status, TaskCompletionSource<T> tcs, bool enableUserCodeExceptionCapability = false)
3137
{
3238
switch (status.Status)
3339
{
3440
case StatusResult.Types.Status.Failure:
35-
tcs.SetException(GetRpcException(status));
41+
var rpcException = GetRpcException(status, enableUserCodeExceptionCapability);
42+
tcs.SetException(rpcException);
3643
return false;
3744

3845
case StatusResult.Types.Status.Cancelled:
@@ -44,12 +51,20 @@ public static bool IsSuccess<T>(this StatusResult status, TaskCompletionSource<T
4451
}
4552
}
4653

47-
public static Workers.Rpc.RpcException GetRpcException(StatusResult statusResult)
54+
/// <summary>
55+
/// If the capability is enabled, surface additional exception properties
56+
/// so that they can be surfaced to app insights by the ScriptTelemetryProcessor.
57+
/// </summary>
58+
public static Workers.Rpc.RpcException GetRpcException(StatusResult statusResult, bool enableUserCodeExceptionCapability = false)
4859
{
4960
var ex = statusResult?.Exception;
5061
var status = statusResult?.Status.ToString();
5162
if (ex != null)
5263
{
64+
if (enableUserCodeExceptionCapability)
65+
{
66+
return new Workers.Rpc.RpcException(status, ex.Message, ex.StackTrace, ex.Type, ex.IsUserException);
67+
}
5368
return new Workers.Rpc.RpcException(status, ex.Message, ex.StackTrace);
5469
}
5570
return new Workers.Rpc.RpcException(status, string.Empty, string.Empty);

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
<PackageReference Include="Microsoft.Azure.AppService.Middleware.Functions" Version="1.4.17" />
7575
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
7676
<PackageReference Include="Microsoft.Azure.Storage.File" Version="11.1.7" />
77+
7778
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.34-11938" />
7879
<PackageReference Include="Microsoft.Azure.WebJobs.Host.Storage" Version="5.0.0-beta.2-11938" />
7980
<PackageReference Include="Microsoft.Azure.WebSites.DataProtection" Version="2.1.91-alpha" />
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.Collections.Generic;
6+
using Microsoft.ApplicationInsights.Channel;
7+
using Microsoft.ApplicationInsights.DataContracts;
8+
using Microsoft.ApplicationInsights.Extensibility;
9+
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.Config
12+
{
13+
internal class ScriptTelemetryProcessor : ITelemetryProcessor
14+
{
15+
public ScriptTelemetryProcessor(ITelemetryProcessor next)
16+
{
17+
this.Next = next;
18+
}
19+
20+
private ITelemetryProcessor Next { get; set; }
21+
22+
public void Process(ITelemetry item)
23+
{
24+
// Only process if exception is thrown by user code (if IsUserException is true).
25+
if (item is ExceptionTelemetry exceptionTelemetry
26+
&& exceptionTelemetry?.Exception?.InnerException is RpcException rpcException
27+
&& (rpcException?.IsUserException).GetValueOrDefault())
28+
{
29+
item = ToUserException(rpcException, item);
30+
}
31+
this.Next.Process(item);
32+
}
33+
34+
private ITelemetry ToUserException(RpcException rpcException, ITelemetry originalItem)
35+
{
36+
string typeName = string.IsNullOrEmpty(rpcException.RemoteTypeName) ? rpcException.GetType().ToString() : rpcException.RemoteTypeName;
37+
38+
var userExceptionDetails = new ExceptionDetailsInfo(1, -1, typeName, rpcException.RemoteMessage, true, rpcException.RemoteStackTrace, new StackFrame[] { });
39+
40+
ExceptionTelemetry newET = new ExceptionTelemetry(new[] { userExceptionDetails },
41+
SeverityLevel.Error, "ProblemId",
42+
new Dictionary<string, string>() { },
43+
new Dictionary<string, double>() { });
44+
45+
newET.Context.InstrumentationKey = originalItem.Context.InstrumentationKey;
46+
newET.Timestamp = originalItem.Timestamp;
47+
48+
return newET;
49+
}
50+
51+
/// <summary>
52+
/// Returns true if the feature flag for surfacing user code exceptions was set by the worker,
53+
/// and false if not.
54+
/// </summary>
55+
/// <param name="ex">The <see cref="Exception"/> instance.</param>
56+
private bool EnableUserExceptionFeatureFlag(Exception ex)
57+
{
58+
try
59+
{
60+
string value = (string)ex.Data[RpcWorkerConstants.EnableUserCodeException];
61+
return bool.Parse(value);
62+
}
63+
catch
64+
{
65+
return false;
66+
}
67+
}
68+
}
69+
}

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ internal static void ConfigureApplicationInsights(HostBuilderContext context, IL
387387
{
388388
o.InstrumentationKey = appInsightsInstrumentationKey;
389389
o.ConnectionString = appInsightsConnectionString;
390-
});
390+
}, t => t.TelemetryProcessorChainBuilder.Use(next => new ScriptTelemetryProcessor(next)));
391391

392392
builder.Services.ConfigureOptions<ApplicationInsightsLoggerOptionsSetup>();
393393

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@
4242
<PackageReference Include="Azure.Core" Version="1.19.0" />
4343
<PackageReference Include="Azure.Identity" Version="1.4.1" />
4444
<PackageReference Include="Azure.Storage.Blobs" Version="12.9.0" />
45-
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.34-11937" />
4645
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.20.0" />
4746
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" />
4847
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" Version="2.20.0" />
4948
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer" Version="2.20.0" />
5049
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" Version="2.20.0" />
51-
<PackageReference Include="Microsoft.Azure.WebJobs.Host.Storage" Version="5.0.0-beta.1" />
50+
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.34-11938" />
51+
<PackageReference Include="Microsoft.Azure.WebJobs.Host.Storage" Version="5.0.0-beta.1" />
5252
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.1.1" />
5353
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.2.0">
5454
<NoWarn>NU1701</NoWarn>
@@ -63,7 +63,7 @@
6363
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.2.0" />
6464
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Timers.Storage" Version="1.0.0-beta.1" />
6565
<PackageReference Include="Microsoft.Azure.WebJobs.Script.Abstractions" Version="1.0.3-preview" />
66-
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.31" />
66+
<PackageReference Include="Microsoft.Azure.WebJobs.Logging.ApplicationInsights" Version="3.0.34-11938" />
6767
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.3.1" />
6868
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
6969
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />

src/WebJobs.Script/Workers/Rpc/RpcException.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,24 @@ namespace Microsoft.Azure.WebJobs.Script.Workers.Rpc
77
{
88
public class RpcException : Exception
99
{
10-
public RpcException(string result, string message, string stack)
10+
public RpcException(string result, string message, string stack, string typeName = "", bool isUserException = false)
1111
: base($"Result: {result}\nException: {message}\nStack: {stack}")
1212
{
13+
RemoteStackTrace = stack;
14+
RemoteMessage = message;
15+
if (!string.IsNullOrEmpty(typeName))
16+
{
17+
RemoteTypeName = typeName;
18+
}
19+
IsUserException = isUserException;
1320
}
21+
22+
public bool IsUserException { get; set; }
23+
24+
public string RemoteStackTrace { get; set; }
25+
26+
public string RemoteMessage { get; set; }
27+
28+
public string RemoteTypeName { get; set; }
1429
}
1530
}

src/WebJobs.Script/Workers/Rpc/RpcWorkerConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public static class RpcWorkerConstants
4848
public const string UseNullableValueDictionaryForHttp = "UseNullableValueDictionaryForHttp";
4949
public const string SharedMemoryDataTransfer = "SharedMemoryDataTransfer";
5050
public const string FunctionDataCache = "FunctionDataCache";
51+
public const string AcceptsListOfFunctionLoadRequests = "AcceptsListOfFunctionLoadRequests";
52+
public const string EnableUserCodeException = "EnableUserCodeException";
5153
public const string SupportsLoadResponseCollection = "SupportsLoadResponseCollection";
5254

5355
// Host Capabilities
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.ApplicationInsights;
10+
using Microsoft.ApplicationInsights.Channel;
11+
using Microsoft.ApplicationInsights.DataContracts;
12+
using Microsoft.ApplicationInsights.Extensibility;
13+
using Microsoft.Azure.WebJobs.Script.Config;
14+
using Microsoft.Azure.WebJobs.Script.WebHost;
15+
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
16+
using Microsoft.Extensions.Hosting;
17+
using Microsoft.Extensions.Options;
18+
using Xunit;
19+
20+
namespace Microsoft.Azure.WebJobs.Script.Tests.Configuration
21+
{
22+
public class ScriptTelemetryProcessorTests
23+
{
24+
[Fact]
25+
public async Task Test_TelemetryProcessor_AppInsights()
26+
{
27+
var rpcEx = new RpcException("failed", "user message", "user stack", "user exception type");
28+
rpcEx.IsUserException = true;
29+
30+
TelemetryConfiguration config = new TelemetryConfiguration("instrumentation key");
31+
ExceptionTelemetry oldEt = new ExceptionTelemetry(rpcEx);
32+
config.TelemetryProcessorChainBuilder.Use(next => new MyCustomTelemetryProcessor(next));
33+
TelemetryClient client = new TelemetryClient(config);
34+
client.TrackException(oldEt);
35+
await client.FlushAsync(CancellationToken.None);
36+
}
37+
38+
public class MyCustomTelemetryProcessor : ITelemetryProcessor
39+
{
40+
public MyCustomTelemetryProcessor(ITelemetryProcessor item)
41+
{
42+
this.Next = item;
43+
}
44+
45+
private ITelemetryProcessor Next { get; set; }
46+
47+
public void Process(ITelemetry item)
48+
{
49+
if (item is ExceptionTelemetry exceptionTelemetry
50+
&& exceptionTelemetry.Exception is RpcException rpcException
51+
&& rpcException.IsUserException)
52+
{
53+
item = ToUserException(rpcException, item);
54+
}
55+
this.Next.Process(item);
56+
}
57+
58+
private ITelemetry ToUserException(RpcException rpcException, ITelemetry originalItem)
59+
{
60+
rpcException.RemoteTypeName = "test user exception type";
61+
62+
string typeName = string.IsNullOrEmpty(rpcException.RemoteTypeName) ? rpcException.GetType().ToString() : rpcException.RemoteTypeName;
63+
64+
var userExceptionDetails = new ExceptionDetailsInfo(1, -1, typeName, rpcException.RemoteMessage, true, rpcException.RemoteStackTrace, new StackFrame[] { });
65+
66+
ExceptionTelemetry newET = new ExceptionTelemetry(new[] { userExceptionDetails },
67+
SeverityLevel.Error, "ProblemId",
68+
new Dictionary<string, string>() { },
69+
new Dictionary<string, double>() { });
70+
71+
newET.Context.InstrumentationKey = originalItem.Context.InstrumentationKey;
72+
newET.Timestamp = originalItem.Timestamp;
73+
74+
return newET;
75+
}
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)