using Azure.Core;
using Azure.Monitor.Query;
using Azure.Monitor.Query.Models;
using Azure.ResourceManager;
using Azure.ResourceManager.AppContainers;
using Azure.ResourceManager.AppService;
using Azure.ResourceManager.Resources;
namespace Areas.Deploy.Services.Util;
public class AzdAppLogRetriever(TokenCredential credential, string subscriptionId, string azdEnvName)
{
private readonly string _subscriptionId = subscriptionId;
private readonly string _azdEnvName = azdEnvName;
private readonly Dictionary<string, string> _apps = new();
private readonly Dictionary<string, string> _logs = new();
private readonly List<string> _logAnalyticsWorkspaceIds = new();
private string _resourceGroupName = string.Empty;
private ArmClient? _armClient;
private LogsQueryClient? _queryClient;
public async Task InitializeAsync()
{
_armClient = new ArmClient(credential, _subscriptionId);
_queryClient = new LogsQueryClient(credential);
_resourceGroupName = await GetResourceGroupNameAsync();
if (string.IsNullOrEmpty(_resourceGroupName))
{
throw new InvalidOperationException($"No resource group with tag {{\"azd-env-name\": {_azdEnvName}}} found.");
}
}
public async Task GetLogAnalyticsWorkspacesInfoAsync()
{
var subscription = _armClient!.GetSubscriptionResource(new($"/subscriptions/{_subscriptionId}"));
var resourceGroup = await subscription.GetResourceGroupAsync(_resourceGroupName);
var filter = "resourceType eq 'Microsoft.OperationalInsights/workspaces'";
await foreach (var resource in resourceGroup.Value.GetGenericResourcesAsync(filter: filter))
{
_logAnalyticsWorkspaceIds.Add(resource.Id.ToString());
}
if (_logAnalyticsWorkspaceIds.Count == 0)
{
throw new InvalidOperationException($"No log analytics workspaces found for resource group {_resourceGroupName}. Logs cannot be retrieved using this tool.");
}
}
public async Task<GenericResource> RegisterAppAsync(ResourceType resourceType, string serviceName)
{
var subscription = _armClient!.GetSubscriptionResource(new($"/subscriptions/{_subscriptionId}"));
var resourceGroup = await subscription.GetResourceGroupAsync(_resourceGroupName);
var filter = $"tagName eq 'azd-service-name' and tagValue eq '{serviceName}'";
var apps = new List<GenericResource>();
await foreach (var resource in resourceGroup.Value.GetGenericResourcesAsync(filter: filter))
{
var resourceTypeString = resourceType.GetResourceTypeString();
var parts = resourceTypeString.Split('|');
var type = parts[0];
var kind = parts.Length > 1 ? parts[1] : null;
if (resource.Data.ResourceType.ToString() == type &&
(kind == null || resource.Data.Kind?.StartsWith(kind) == true))
{
_logs[resource.Id.ToString()] = string.Empty;
apps.Add(resource);
}
}
return apps.Count switch
{
0 => throw new InvalidOperationException($"No resources found for resource type {resourceType} with tag azd-service-name={serviceName}"),
> 1 => throw new InvalidOperationException($"Multiple resources found for resource type {resourceType} with tag azd-service-name={serviceName}"),
_ => apps[0]
};
}
private static string GetContainerAppLogsQuery(string containerAppName, int limit) =>
$"ContainerAppConsoleLogs_CL | where ContainerAppName_s == '{containerAppName}' | order by _timestamp_d desc | project TimeGenerated, Log_s | take {limit}";
private static string GetAppServiceLogsQuery(string appServiceResourceId, int limit) =>
$"AppServiceConsoleLogs | where _ResourceId == '{appServiceResourceId.ToLowerInvariant()}' | order by TimeGenerated desc | project TimeGenerated, ResultDescription | take {limit}";
private static string GetFunctionAppLogsQuery(string functionAppName, int limit) =>
$"AppTraces | where AppRoleName == '{functionAppName}' | order by TimeGenerated desc | project TimeGenerated, Message | take {limit}";
public async Task<string> QueryAppLogsAsync(ResourceType resourceType, string serviceName, int? limit = null)
{
var app = await RegisterAppAsync(resourceType, serviceName);
var getLogErrors = new List<string>();
var getLogSuccess = false;
var logSearchQuery = string.Empty;
DateTimeOffset? lastDeploymentTime = null;
var actualLimit = limit ?? 200;
DateTimeOffset endTime = DateTime.UtcNow;
DateTimeOffset startTime = endTime.AddHours(-4);
switch (resourceType)
{
case ResourceType.ContainerApps:
logSearchQuery = GetContainerAppLogsQuery(app.Data.Name, actualLimit);
// Get last deployment time for container apps
var containerAppResource = _armClient!.GetContainerAppResource(app.Id);
var containerApp = await containerAppResource.GetAsync();
await foreach (var revision in containerApp.Value.GetContainerAppRevisions())
{
var revisionData = await revision.GetAsync();
if (revisionData.Value.Data.IsActive == true)
{
lastDeploymentTime = revisionData.Value.Data.CreatedOn;
break;
}
}
break;
case ResourceType.AppService:
case ResourceType.FunctionApp:
var webSiteResource = _armClient!.GetWebSiteResource(app.Id);
await foreach (var deployment in webSiteResource.GetSiteDeployments())
{
var deploymentData = await deployment.GetAsync();
if (deploymentData.Value.Data.IsActive == true)
{
lastDeploymentTime = deploymentData.Value.Data.StartOn;
break;
}
}
logSearchQuery = resourceType == ResourceType.AppService
? GetAppServiceLogsQuery(app.Id.ToString(), actualLimit)
: GetFunctionAppLogsQuery(app.Data.Name, actualLimit);
break;
default:
throw new ArgumentException($"Unsupported resource type: {resourceType}");
}
// startTime is now, endTime is 1 hour ago
if (lastDeploymentTime.HasValue && lastDeploymentTime > startTime)
{
startTime = lastDeploymentTime ?? startTime;
}
foreach (var logAnalyticsId in _logAnalyticsWorkspaceIds)
{
try
{
var timeRange = new QueryTimeRange(startTime, endTime);
var response = await _queryClient!.QueryResourceAsync(new(logAnalyticsId), logSearchQuery, timeRange);
if (response.Value.Status == LogsQueryResultStatus.Success)
{
foreach (var table in response.Value.AllTables)
{
foreach (var row in table.Rows)
{
_logs[app.Id.ToString()] += $"[{row[0]}] {row[1]}\n";
}
}
getLogSuccess = true;
break;
}
}
catch (Exception ex)
{
getLogErrors.Add($"Error retrieving logs for {app.Data.Name} from {logAnalyticsId}: {ex.Message}");
}
}
if (!getLogSuccess)
{
throw new InvalidOperationException($"Errors: {string.Join(", ", getLogErrors)}");
}
return $"Console Logs for {serviceName} with resource ID {app.Id} between {startTime} and {endTime}:\n{_logs[app.Id.ToString()]}";
}
private async Task<string> GetResourceGroupNameAsync()
{
var subscription = _armClient!.GetSubscriptionResource(new($"/subscriptions/{_subscriptionId}"));
await foreach (var resourceGroup in subscription.GetResourceGroups())
{
if (resourceGroup.Data.Tags.TryGetValue("azd-env-name", out var envName) && envName == _azdEnvName)
{
return resourceGroup.Data.Name;
}
}
return string.Empty;
}
}
public enum ResourceType
{
AppService,
ContainerApps,
FunctionApp
}
public static class ResourceTypeExtensions
{
private static readonly Dictionary<string, ResourceType> HostToResourceType = new()
{
{ "containerapp", ResourceType.ContainerApps },
{ "appservice", ResourceType.AppService },
{ "function", ResourceType.FunctionApp }
};
private static readonly Dictionary<ResourceType, string> ResourceTypeToString = new()
{
{ ResourceType.AppService, "Microsoft.Web/sites|app" },
{ ResourceType.ContainerApps, "Microsoft.App/containerApps" },
{ ResourceType.FunctionApp, "Microsoft.Web/sites|functionapp" }
};
public static ResourceType GetResourceTypeFromHost(string host)
{
return HostToResourceType.TryGetValue(host, out var resourceType)
? resourceType
: throw new ArgumentException($"Unknown host type: {host}");
}
public static string GetResourceTypeString(this ResourceType resourceType)
{
return ResourceTypeToString[resourceType];
}
}