Deep-linking Azure Log Analytics and Application Insights queries
For a while, I've wanted to craft deep links (direct links) to queries in Azure Monitor. Either directly to Log Analytics workspaces or to Application Insights resources I have.
I have plenty of use cases for this, but most notably is the fact that we want to easily share queries with peers and users of our administrative tools, which can directly link to the resources with pre-defined queries. This way, the queries always can cater to the dynamic nature of our workloads, and give the admin user a one-click solution to land in Azure to further troubleshoot.
In this post, I'm just laying out the method to encode the links properly. If you've landed here and read about this topic, I'm sure you already have your use cases figured out.
- Get the code (GitHub)
It's all in the URL
The general format of the links to the queries wasn't hard to figure out. In either Application Insights or Log Analytics, click to view the "Logs" and enter any query.
From the menu, we can copy the link to the URL.
This will give us the format of the URL, which looks like this:
The Log Analytics URL format
The format for a direct/deep-link to a query in Log Analytics looks like this.
https://portal.azure.com/#blade/Microsoft_OperationsManagementSuite_Workspace/AnalyticsBlade/initiator/AnalyticsShareLinkToQuery/isQueryEditorVisible/true/scope/{"resources"%3A[{"resourceId"%3A"%2Fsubscriptions%2FSUBSCRIPTION_GUID
%2Fresourcegroups%2FRESOURCE_GROUP_NAME
%2Fproviders%2Fmicrosoft.operationalinsights
%2Fworkspaces%2FWORKSPACE_ID"}]}/query/ENCODED_QUERY/isQueryBase64Compressed/true/timespanInIsoFormat/TIMESPAN
The Application Insights URL format
The format for a direct/deep-link to a query in Application Insights looks like this.
https://portal.azure.com#@TENANT_ID/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2FSUBSCRIPTION_ID%2FresourceGroups%2FRESOURCE_GROUP_NAME
%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2FAPP_INSIGHTS_NAME/source/LogsBlade.AnalyticsShareLinkToQuery/q/ENCODED_QUERY/timespan/TIMESPAN
We can make a few notes on the differences in the Log Analytics and Application Insights URLs. The latter requires the Tenant ID and the App Insights Name, whereas the former requires the Log Analytics Workspace name.
So what are these values in the URL?
- Tenant ID. This is the ID of your Directory (Tenant).
- Subscription ID. This is the ID of your Azure Subscription where the resource exists.
- Resource Group Name. This is the name of the resource group where your App Insights or Log Analytics resource is deployed.
- WorkspaceID / App Insights Name. Respectively, this is the Log Analytics Workspace ID (GUID) or the Application Insights Name.
- Encoded Query. This is a Base64 encoded, Url encoded compressed query. More on that below.
- Timespan. This defines the timespan to set for the query, if desirable.
Most of these values are obvious, and we can easily inject them into our URLs. However, the Encoded Query part isn't entirely obvious, so let's dive into that one.
Encoding the queries
I have simplified the code samples for this blog post, to make it obvious how the formatting and encoding of the queries work.
First, the query is compressed (zipped), then it's converted to Base64, and then URL Encoded.
Plain-text query:
AppExceptions
| where ProblemId != 'System.InvalidOperationException'
| summarize count() by ProblemId
| order by count_ desc
| render columnchart
When encoded, the above query will look something like this:
H4sIAAAAAAAACnXMMQ7CMAyF4R2JO5ipsHADBgaGTiBxAJQ6lhopjiMnKbTq4VEYYCFv%2ft93jvHyQorZSUjbDfzZCs%2bRlOCmMnji3sLuBN19Tpn42IfJeGevkdRU44t1LSwVZqNuIUApIe8PMMw%2fu%2fUStaS1%2fJweYClhq1UKNUbxhQOORjO8Ad%2fYR6znAAAA
Encoding the queries in .NET
I build a small sample to help with converting the plain-text KQL queries to the correct encoding, and plug that into the correct URL format so you can easily produce direct links to your queries.
Helper for encoding the queries:
public class LinkGenerator
{
/// <summary>
/// Gets a direct/deep-link URL to a specific KQL query in an Azure Log Analytics workspace.
/// </summary>
/// <param name="kqlQuery">The plain-text query.</param>
/// <param name="subscriptionId">The Azure Subscription ID.</param>
/// <param name="resourceGroup">The Resource Group Nme.</param>
/// <param name="workspaceId">The Log Analytics Workspace ID.</param>
/// <param name="timeFrame">The Timeframe for the query.</param>
/// <returns></returns>
public static string GetLogAnalyticsDeepLink(string kqlQuery, string subscriptionId, string resourceGroup, string workspaceId, QueryTimeframe timeFrame)
{
// Encode the query with our params.
var encodedQuery = GetEncodedQuery(kqlQuery);
// Return the URL with the query embedded, and put the timespan at the end.
return $"https://portal.azure.com/#blade/Microsoft_OperationsManagementSuite_Workspace/AnalyticsBlade/initiator/AnalyticsShareLinkToQuery/isQueryEditorVisible/true/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2F{subscriptionId}%2Fresourcegroups%2F{resourceGroup}%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F{workspaceId}%22%7D%5D%7D/query/{encodedQuery}/isQueryBase64Compressed/true/timespanInIsoFormat/{timeFrame}";
}
/// <summary>
/// Gets a direct/deep-link URL to a specific KQL query in an Azure Application Insights resource.
/// </summary>
/// <param name="kqlQuery">The plain-text query.</param>
/// <param name="tenantId">The Azure directory/tenant ID.</param>
/// <param name="subscriptionId">The Azure Subscription ID.</param>
/// <param name="resourceGroup">The Resource Group Nme.</param>
/// <param name="appInsightsName">The Azure Application Insights resource name.</param>
/// <param name="timeFrame">The Timeframe for the query.</param>
/// <returns></returns>
public static string GetApplicationInsightsDeepLink(string kqlQuery, string tenantId, string subscriptionId, string resourceGroup, string appInsightsName, QueryTimeframe timeFrame)
{
// Encode the query with our params.
var encodedQuery = GetEncodedQuery(kqlQuery);
// Return the URL with the query embedded, and put the timespan at the end.
return $"https://portal.azure.com#@{tenantId}/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F{subscriptionId}%2FresourceGroups%2F{resourceGroup}%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2F{appInsightsName}/source/LogsBlade.AnalyticsShareLinkToQuery/q/{encodedQuery}/timespan/{timeFrame}";
}
private static string GetEncodedQuery(string kqlQuery)
{
string encodedQuery;
// Encode the query.
var encodedQueryBytes = Encoding.UTF8.GetBytes(kqlQuery);
// Compress.
using (MemoryStream memoryStream = new MemoryStream())
{
using (GZipStream zipStream = new GZipStream(memoryStream, CompressionMode.Compress))
{
// 1. Compress/Zip the string.
zipStream.Write(encodedQueryBytes, 0, encodedQueryBytes.Length);
zipStream.Close();
var compressedQuery = memoryStream.ToArray();
// 2. Base64 encode the string.
// 3. UrlEncode the string.
encodedQuery = HttpUtility.UrlEncode(Convert.ToBase64String(compressedQuery));
}
}
// 4. Ready. The encoded query that can be passed on to the URL.
return encodedQuery;
}
}
/// <summary>
/// Defines the timeframe to include in the query.
/// Based on the ISO standard.
/// In this demo, I am just setting a few pre-defined selections to make the query building easy. Adjust as needed.
/// </summary>
public enum QueryTimeframe
{
// Minute intervals.
PT5M,
PT30M,
PT60M,
// One Day.
P1D,
// One Week.
P1W,
// One Month.
P1M
}
Calling the helper is straightforward:
string lawQuery = @"AppExceptions
| where ProblemId != 'System.InvalidOperationException'
| summarize count() by ProblemId
| order by count_ desc
| render columnchart ";
// Simple call to demonstrate.
var lawUrl = LinkGenerator.GetLogAnalyticsDeepLink(
lawQuery,
"SUBSCRIPTION_ID",
"RESOURCE_GROU_NAME",
"LOG_ANALYTICS_WORKSPACE_NAME",
QueryTimeframe.P1W);
Console.WriteLine("LOG ANALYTICS LINK:");
Console.WriteLine(lawUrl);
var appiQuery = @"exceptions
| where problemId != 'System.InvalidOperationException'
| summarize count() by problemId
| order by count_ desc
| render columnchart";
// Simple call to demonstrate.
var appiUrl = LinkGenerator.GetApplicationInsightsDeepLink(
appiQuery,
"TENANT_ID",
"SUBSCRIPTION_ID",
"RESOURCE_GROUP_NAME",
"APPLICATION_INSIGHTS_RESOURCE_NAME",
QueryTimeframe.P1W);
Console.WriteLine();
Console.WriteLine("APP INSIGHTS LINK:");
Console.WriteLine(appiUrl);```
The results
Clicking the new links will take you directly to the query in the given resource (Application Insights or Log Analytics, depending on which type of URL you chose).
That's it for now. Direct links to Application Insights and Log Analytics both work, and we can now construct complex queries dynamically in our application(s) and with the click of a button we're landing in the correct resource in Azure to dive deeper into the logs.
Enjoy.