Wow its nearly a year since I wrote the post on the link below where we looked at using a modified version of an approach Josh Cook had shared from the Power Automate world to get the error message from a Logic App when you implement a try/catch.
https://www.serverless360.com/blog/logic-apps-error-handling-using-serverless360-bam
As an approach it worked ok, but I used to find the following issues in some scenarios:
- The error message you would get was pretty noisy
- If you had nested scopes then you would only get the outer scope shapes info which meant you missed some key error info
I wanted to try and do something a bit more than this to see if I get get more user friendly error info which I want to log to my logging sub-system regardless of if im using Log Analytics Data Collector or BAM. Remember my sessions from Integrate 2021 where we looked at some of the logging approaches. Some links are below:
- https://www.integration-playbook.io/docs/log-analytics-data-collector-api
- https://www.integration-playbook.io/docs/bam-with-logic-apps
- https://www.biztalk360.com/integrate-2021-remote/
Approach
My approach for this problem is that I will implement a try/catch pattern in the usual way but in the catch block I am going to call a function and pass the id for the run of the logic app.
In the function I am then going to call the azure management API and use a filter to get the actions which have a status of failed.
I will then loop through them but I will remove the ones where the message is ” An action failed. No dependent actions succeeded” because these are usually scope shapes with an error inside them and are just noise. I want just the actual errors.
For each action that is a genuine error I will then use the properties to get the link to the message body and I will call it to get the underlying error message from the action.
I will then return an array of the errors within the logic app run.
You can see below that there is an example of my errors from a Logic App which got an error calling a web service.
[
{
"name": "ThrowException_-_SAP_Invoice_Load_Error",
"status": "Failed",
"code": "BadRequest",
"startTime": "2021-09-06T17:57:10.3030265Z",
"endTime": "2021-09-06T17:57:10.365503Z",
"errorMessage": "{\"statusCode\":400,\"headers\":{\"Transfer-Encoding\":\"chunked\",\"Request-Context\":\"appId=cid-v1:d88b415e-d846-444f-8e3d-ab2344382e50\",\"Date\":\"Mon, 06 Sep 2021 17:57:09 GMT\",\"Content-Type\":\"text/plain; charset=utf-8\",\"Content-Length\":\"46\"},\"body\":\"The invoice was not loaded to SAP successfully\"}"
}
]
Implementation
In my catch block you can see below I am calling my function and I am passing in the workflow()[‘run’][‘id’] expression. This will pass the subscription, resource group, workflow name and run id as a string to the function.
In my Function App I have setup the function app with a system assigned managed identity and then in the resource group I have given the managed identity the Logic App Operator role.
The full code for the function is below at the bottom of the article, but I want to call out a couple of bits. After I read the logic app run id from the request body I then run the following code snippet which will call the management API using the managed identity.
var target = "https://management.azure.com";
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync(target);
var url = $"{target}/{id}/actions?api-version=2016-06-01&$filter=status eq 'Failed'";
var logicAppRunRequest = new HttpRequestMessage(HttpMethod.Get, url);
logicAppRunRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var result = await _client.SendAsync(logicAppRunRequest);
I will then loop through the response array.
If an item in the response has a output link uri then this is the path to get the message body with the action result in it. I will call this and download the actual error info.
if(action["properties"]["outputsLink"] != null
&& action["properties"]["outputsLink"]["uri"] != null)
{
var errorMessageUri = action["properties"]["outputsLink"]["uri"].ToString();
var errorMessageRequest = new HttpRequestMessage(HttpMethod.Get, errorMessageUri);
var errorMessageResult = await _client.SendAsync(errorMessageRequest);
var errorMessage = await errorMessageResult.Content.ReadAsStringAsync();
item.Add(new JProperty("errorMessage", errorMessage));
}
Now one point to note here is some of the actions will have slightly different formats for their data in the message we download so to keep things simple I will just add this message to my response item. As I mentioned earlier the full code for the function is below.
Now at this point you have choices depending on how you are logging, but in the implementation I am working on, I want to send a more useful error message to the Serverless360 BAM so that my support users can troubleshoot what happened to the transaction without needing to decipher the entire Logic App. I will do a checkpoint as shown below.
I am just wrapping my error array in an object and then sending it to BAM as the message body.
Over in BAM you can now see I have an error shape flagging up.
If you open the error shape you can see that the archived message now shows just the info from the action that failed. Because it was an HTTP call I am getting the status code, etc but the body is pretty easy to see so the support operator can easily see what the problem was and there isnt a huge error message with lots of noise from nested scope shapes.
The Function Code
For the blog POC I have just used an inline coded function. The code is below. As long as your function app has RBAC access to the Logic App run history for the Logic App you are wanting the error info for then this should work as is, or you can move it to a proper visual studio type function app.
#r "Newtonsoft.Json"
#r "Microsoft.Azure.Services.AppAuthentication"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Azure.Services.AppAuthentication;
using System.Net.Http;
using System.Net.Http.Headers;
private static System.Net.Http.HttpClient _client = new System.Net.Http.HttpClient();
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
var outputActions = new JArray();
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var id = requestBody;
var target = "https://management.azure.com";
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync(target);
var url = $"{target}/{id}/actions?api-version=2016-06-01&$filter=status eq 'Failed'";
var logicAppRunRequest = new HttpRequestMessage(HttpMethod.Get, url);
logicAppRunRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var result = await _client.SendAsync(logicAppRunRequest);
var azureResponseBody = await result.Content.ReadAsStringAsync();
var azureResponse = JObject.Parse(azureResponseBody);
var response = new JArray();
foreach(var action in azureResponse["value"])
{
if(action.ToString().Contains("An action failed. No dependent actions succeeded"))
{
//Skip these actions because they are scope shapes which arent the real error
}
else
{
var item = new JObject();
item.Add(new JProperty("name", action["name"]));
if(action["properties"] != null)
{
item.Add(new JProperty("status", action["properties"]["status"]));
item.Add(new JProperty("code", action["properties"]["code"]));
item.Add(new JProperty("startTime", action["properties"]["startTime"]));
item.Add(new JProperty("endTime", action["properties"]["endTime"]));
if(action["properties"]["outputsLink"] != null
&& action["properties"]["outputsLink"]["uri"] != null)
{
var errorMessageUri = action["properties"]["outputsLink"]["uri"].ToString();
var errorMessageRequest = new HttpRequestMessage(HttpMethod.Get, errorMessageUri);
var errorMessageResult = await _client.SendAsync(errorMessageRequest);
var errorMessage = await errorMessageResult.Content.ReadAsStringAsync();
item.Add(new JProperty("errorMessage", errorMessage));
}
}
response.Add(item);
}
}
return new OkObjectResult(response);
}