Power Platform Application Insights Instrumentation
Model-driven apps and Microsoft Dataverse diagnostics and performance data can be monitored in an Azure Application Insights resource. This enhancement allows developers and admins to gain a deep understanding of what's going on inside the platform. Out of the box, there are form-loads, plug-ins, and platform APIs logging error and performance data.
On a high level, you can report user activity with specific forms and it will allow product owners to understand adoption blockers and drivers.
Setup Application Insights
- Login to Azure Portal
- Create an Application Insights resource
- Activate Data Export to App Insights from Power Platform admin center
Monitoring a Model-driven app
Once you have enabled data export you can immediately start to see monitoring events in your Application Insights resource.
Add instrumentation code to your custom plugins
Knowing about timings in platform-level functionality is handy, but this feature really shines if you can extend it to your own plugins. While there has always been a way of utilizing the ITracingService for Plug-in Trace Logs there is now an additional ILogger service that can be obtained from the service provider.
var logger = (ILogger)serviceProvider.GetService(typeof(ILogger));
logger.LogInformation("log something useful");
The sample plugin below retrieves the share price from marketstack once a tickersymbol
is set on an account and updates the stackexchange
accordingly.
If we create an update on the account we can capture the Request Id from the response headers collection inside F12 developer tools network tab.
This Request Id (req_id
) can be used to trace down the request along with the dependency graph of sub-components e.g. the HTTP Call to the marketstack API by searching for it under transaction search.
When we identified the transaction we see all operations that happened inside that transaction including other plugins as well as connections to external dependencies like in this case the financial data service.
Wrapping up
- Capture telemetry over time for analysis and debugging.
- Easily query events with Kusto Query Language (KQL).
- Analyze if issue was caused by custom code or platform code.
- Alerts based off performance thresholds for form-loads, APIs, and plug-ins
Sample Plugin to receive stock market prices for an account
public class GetStockExchangePlugin : IPlugin
{
// Get an API Key from https://marketstack.com/
private const string ApiKey = "<ENTER YOUR OWN API KEY>";
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var logger = (ILogger)serviceProvider.GetService(typeof(ILogger));
Entity target;
if (!context.InputParameters.TryGetValue("Target", out var targetParameter))
{
throw new InvalidPluginExecutionException("Target not present.");
}
else
{
target = (Entity)targetParameter;
}
var tickersymbol = target.GetAttributeValue<string>("tickersymbol");
if (!string.IsNullOrWhiteSpace(tickersymbol))
{
var stock = GetStockData(logger, tickersymbol);
var symbol = stock?.data?.FirstOrDefault();
if (symbol != null)
{
target["stockexchange"] = $"{symbol.close:c} @{symbol.date:d}";
}
}
}
private static Rootobject GetStockData(ILogger logger, string symbol)
{
using (logger.BeginScope("FetchStockData"))
{
logger.LogInformation("Outbound call to marketstack started.");
using (HttpClient client = new HttpClient())
{
client.Timeout = TimeSpan.FromMilliseconds(15000);
client.DefaultRequestHeaders.ConnectionClose = true;
logger.LogInformation("Getting Stock data for: {0}", symbol);
var response = client
.GetAsync($"http://api.marketstack.com/v1/eod?access_key={ApiKey}&symbols={symbol}&date_to={DateTime.UtcNow:yyyy-MM-dd)}&limit=1")
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
if ((int)response.StatusCode == 422)
{
logger.LogError("'{0}' not found.", symbol);
return null;
}
response.EnsureSuccessStatusCode();
var responseStream = response.Content
.ReadAsStreamAsync()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
return ParseResponse(logger, responseStream);
}
}
}
private static Rootobject ParseResponse(ILogger logger, Stream responseStream)
{
using (responseStream)
using (logger.BeginScope("Parsing Response"))
{
var rootobject = (Rootobject)new DataContractJsonSerializer(typeof(Rootobject), new DataContractJsonSerializerSettings
{
DateTimeFormat = new DateTimeFormat("yyyy-MM-ddTHH:mm:ss+0000")
}).ReadObject(responseStream);
return rootobject;
}
}
[DataContract]
public class Rootobject
{
[DataMember]
public StockData[] data { get; set; }
}
[DataContract]
public class StockData
{
[DataMember]
public decimal close { get; set; }
[DataMember]
public DateTime date { get; set; }
}
}