Building an Azure Application Insights Dashboard with ASP.NET Core 8 API & React 18
Building an Azure Application Insights Dashboardwith ASP.NET Core 8 API & React 18
This article provides a complete hands-on guide with full source code for every file building a monitoring dashboard that queries Azure Application Insights using KQL, secured with Microsoft Entra ID.
Table of Contents
- Introduction
- What is Azure Application Insights?
- Features In-Depth
- Architecture & Execution Workflow Diagram
- Project Structure, Prerequisites & Packages
- Key Azure SDK Classes Explained (LogsQueryClient, MetricsQueryClient, Serilog)
- NuGet Packages Explained (Azure.Identity, Azure.Monitor.Query, Serilog)
- NPM Packages Explained (@azure/msal-browser, @azure/msal-react)
- API: Configuration (appsettings.json, AzureSettings.cs, EntraIdSettings.cs)
- API: Program.cs
- API: Models (LogEntry.cs, MetricModels.cs, KqlModels.cs)
- API: Service Interfaces
- API: Service Implementations
- API: All 9 Controllers
- React: TypeScript Types
- React: Authentication
- React: API Client & Services
- React: Custom Hooks
- React: Components
- React: Pages
- React: Layout, App, Entry Point & .env
- KQL Deep Dive
- Running the Application
- Summary
1. Introduction
Modern cloud applications generate enormous amounts of telemetry. Azure Application Insights is Microsoft's APM service that collects and analyzes this data. In this article, we build a full-stack monitoring dashboard with an ASP.NET Core 8 API backend and React 18 + TypeScript frontend. Every single source file is included with explanations so you can understand and reproduce the entire project.
2. What is Azure Application Insights?
Azure Application Insights is an extensible APM service that monitors live applications, detects performance anomalies, and diagnoses issues. It stores telemetry in a Log Analytics Workspace queryable via KQL (Kusto Query Language).
| Concept | Description |
|---|---|
| Telemetry | Data from your app: requests, dependencies, exceptions, traces, metrics |
| Log Analytics Workspace | Data store queried via KQL |
| KQL | Read-only query language similar to SQL |
| Smart Detection | AI that auto-detects anomalies |
| Application Map | Visual topology of components |
3. Features In-Depth
Logs (AppTraces)
Log messages with severity, operation ID, and properties.
Requests (AppRequests)
HTTP request tracking: URL, duration, status code, success.
Exceptions (AppExceptions)
Exception type, message, stack trace, severity.
Metrics (AppMetrics)
Numeric measurements with aggregations over time.
Dependencies (AppDependencies)
External calls to SQL, HTTP APIs, Redis with duration.
Availability
Health probes from global locations.
| Feature | Description |
|---|---|
| Live Metrics | Real-time streaming (~1s latency) |
| Transaction Diagnostics | End-to-end trace via Operation ID |
| Profiler | .NET performance traces |
| Snapshot Debugger | Debug snapshot on exception |
| Alerts | Metric, log, and smart detection alerts |
| Workbooks | Custom interactive reports |
4. Architecture & Execution Workflow
learn.microsoft.com/entra/identity-platform/quickstart-register-app and learn.microsoft.com/azure/azure-monitor/app/create-workspace-resource.5. Project Structure, Prerequisites & Packages
API NuGet Packages
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Azure.Monitor.Query" Version="1.7.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.26" />
<PackageReference Include="Microsoft.Identity.Web" Version="4.8.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>
</Project>
React NPM Packages
{
"name": "client-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.2.1",
"@azure/msal-browser": "^5.8.0",
"@azure/msal-react": "^5.3.1",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.100.5",
"antd": "^6.3.7",
"axios": "^1.15.2",
"dayjs": "^1.11.20",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2",
"recharts": "^3.8.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10"
}
}
5a. Key Azure SDK Classes Explained
The ASP.NET Core API uses three critical classes from the Azure SDK and Serilog. Understanding what each class does is essential for working with this project.
LogsQueryClient class From Azure.Monitor.Query Namespace
LogsQueryClient is the primary class from the Azure.Monitor.Query NuGet package used to execute KQL (Kusto Query Language) queries against an Azure Log Analytics Workspace. This is the class that actually talks to Azure Application Insights to fetch your telemetry data.
What it does:
- Sends KQL query strings to the Azure Monitor Logs API over HTTPS
- Authenticates using the provided
TokenCredential(e.g.,ClientSecretCredentialorDefaultAzureCredential) - Returns results as a
LogsQueryResultcontaining aLogsTablewith typed columns and rows - Supports configurable time ranges via
QueryTimeRange - Is thread-safe and designed to be used as a singleton — create one instance and reuse it across all requests
How we use it in this project:
- Registered as a singleton in
Program.cs:builder.Services.AddSingleton(new LogsQueryClient(credential)); - Injected into
CLSLogsService,CLSMetricsService, andCLSKqlServicevia constructor injection - Called via
QueryWorkspaceAsync(workspaceId, kqlQuery, timeRange)in every service method - The returned
LogsQueryResult.Table.Rowsare iterated and mapped to our DTO models (e.g.,CLSLogEntry,CLSTraceItem)
MetricsQueryClient class From Azure.Monitor.Query Namespace
MetricsQueryClient is the companion class from the Azure.Monitor.Query package, designed specifically for querying Azure Monitor Metrics (as opposed to logs). While LogsQueryClient queries log-based tables using KQL, MetricsQueryClient queries the Metrics API which provides pre-aggregated numeric data.
What it does?
- Queries the Azure Monitor Metrics REST API using a resource ID
- Returns pre-aggregated metric values (average, sum, min, max, count) over time intervals
- Supports querying standard platform metrics (CPU, memory, request rate) and custom metrics
- Uses
MetricsQueryOptionsto specify aggregations, granularity, and filters - Also thread-safe and designed for singleton usage
How we use it in this project?
- Registered as a singleton in
Program.csalongsideLogsQueryClient - Injected into
CLSMetricsServicefor potential direct metrics API queries - In our current implementation, we primarily use
LogsQueryClientwith KQL queries against theAppMetricstable, which gives us more flexibility. TheMetricsQueryClientis available for future expansion to query platform-level metrics that are not stored in the Log Analytics Workspace.
Serilog From Serilog.AspNetCore Namespace
Serilog is a popular structured logging library for .NET that replaces the built-in Microsoft.Extensions.Logging with a more powerful, configurable logging pipeline. Unlike traditional text-based logging, Serilog captures log data as structured events with named properties, making logs easier to query and analyze.
What it does?
- Structured Logging: Instead of
log.Info("User logged in: " + userId), you writeLog.Information("User {UserId} logged in", userId)— theUserIdproperty is captured separately and can be filtered/searched independently - Sink-based Architecture: Log events are written to one or more "sinks" (outputs). In our project, we use
Serilog.Sinks.Consoleto write to the terminal. Other sinks include files, Azure Application Insights, Seq, Elasticsearch, and many more. - Configuration from appsettings.json: Minimum log levels, overrides per namespace, and sink settings can all be configured via JSON without code changes
- Request Logging: The
UseSerilogRequestLogging()middleware automatically logs every HTTP request with method, path, status code, and duration - Bootstrap Logger: A lightweight logger is created before the host is built to capture startup errors that would otherwise be lost
How we use it in this project?
- Bootstrap:
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();— captures startup errors - Host integration:
builder.Host.UseSerilog(...)— replaces default logging, reads config from appsettings.json - Request logging:
app.UseSerilogRequestLogging()— logs every HTTP request automatically - In services:
Log.Information("Executing KQL query: {Query}", query)— structured log with query property - Fatal errors:
Log.Fatal(ex, "Application terminated unexpectedly")— captures crash info - Cleanup:
Log.CloseAndFlush()in thefinallyblock ensures all buffered events are written before shutdown
5b. List of NuGet Packages used in ASP.NET Core API
The API project uses the following NuGet packages. Understanding each package's role helps you know why it's included and what it provides.
Azure.Identity v1.21.0
Azure.Identity provides Azure Active Directory (Entra ID) token authentication for Azure SDK client libraries. It is the authentication layer that proves your API's identity to Azure services.
Key classes used in this project:
ClientSecretCredential— Authenticates using a tenant ID, client ID, and client secret from an Entra ID App Registration. This is what we use in production when explicit credentials are configured inappsettings.json. It creates an OAuth 2.0 client credentials flow token.DefaultAzureCredential— A credential chain that automatically tries multiple authentication methods in order: Environment Variables → Managed Identity → Visual Studio → Azure CLI → Azure PowerShell → Interactive Browser. This is the fallback when no explicit credentials are configured, making it perfect for local development (uses your Azure CLI login) and production on Azure (uses Managed Identity).
Why it matters: Without this package, the LogsQueryClient and MetricsQueryClient cannot authenticate with Azure Monitor. Every API call to Log Analytics requires a valid OAuth 2.0 bearer token, and Azure.Identity handles obtaining and refreshing these tokens automatically.
Azure.Monitor.Query v1.7.1
Azure.Monitor.Query is the core Azure SDK package for querying Azure Monitor data. It provides the two main client classes used throughout this project.
What it provides?
LogsQueryClient— Executes KQL queries against Log Analytics Workspaces (see detailed explanation above)MetricsQueryClient— Queries the Azure Monitor Metrics API (see detailed explanation above)LogsQueryResult— The result object containing aLogsTablewithColumns(schema) andRows(data)LogsTableRow— Represents a single row in the result table. OurCLSLogsTableRowExtensionsclass provides safe typed accessors (FNGetString,FNGetInt64,FNGetDouble, etc.) for extracting values from these rows.QueryTimeRange— Specifies the time window for a query. We construct it from aTimeSpanparsed from user-friendly strings like "24h", "7d".
Why it matters: This single package replaces what would otherwise require manual REST API calls to https://api.loganalytics.io/v1/workspaces/{id}/query. The SDK handles authentication, request formatting, response parsing, retry logic, and error handling.
Serilog.AspNetCore v10.0.0 & Serilog.Sinks.Console v6.1.1
Serilog.AspNetCore integrates Serilog with the ASP.NET Core hosting model. It replaces the default ILogger infrastructure so that all logging — from your code, from ASP.NET Core framework, from Entity Framework, etc. — flows through Serilog's pipeline.
What Serilog.AspNetCore provides?
UseSerilog()extension method onIHostBuilderto replace default loggingUseSerilogRequestLogging()middleware that logs HTTP request summaries (method, path, status code, duration in ms)- Configuration binding from
appsettings.jsonviaReadFrom.Configuration() - Bootstrap logger support for capturing startup errors
What Serilog.Sinks.Console provides?
- The
WriteTo.Console()sink that outputs structured log events to the terminal with timestamp, level, and message - Supports colored output, configurable output templates, and theme customization
- In production, you would typically add additional sinks like
Serilog.Sinks.ApplicationInsightsto send logs to Azure, orSerilog.Sinks.Filefor file-based logging
5c. List of NPM Packages Used in Raect @azure/msal-browser, @azure/msal-react
The React application uses two Microsoft Authentication Library (MSAL) packages to implement OAuth 2.0 Authorization Code flow with PKCE against Microsoft Entra ID (Azure AD). These packages handle the entire browser-based authentication lifecycle.
@azure/msal-browser (v5.8.0)
@azure/msal-browser is the core MSAL library for single-page applications. It implements the OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange) in the browser. This is the standard recommended by Microsoft for SPAs authenticating against Entra ID.
Important classes and concepts:
PublicClientApplication— The main MSAL class that manages authentication state. Our project creates a singleton instance (msalInstance) inAuthProvider.tsxand exports it for use by the Axios interceptor. It handles token caching inlocalStorage, silent token renewal, and interactive login flows.acquireTokenSilent()— Attempts to get an access token from the cache without user interaction. This is called by our Axios interceptor before every API request. If the cached token is still valid, no network call is made. If expired but a refresh token exists, it automatically refreshes.acquireTokenRedirect()— When silent acquisition fails (expired session, new consent needed), this redirects the browser to the Entra ID login page. After authentication, the user is redirected back to the app.loginRedirect()— Initiates the initial login flow. Called when the user clicks "Sign in with Microsoft" in ourCLSAuthGuardComponent.handleRedirectPromise()— Must be called on app startup to process the authentication response after returning from the Entra ID login page. OurCLSAuthProviderComponentcalls this during initialization.InteractionRequiredAuthError— An error class thrown when silent token acquisition fails and user interaction is required. Our Axios interceptor catches this to trigger a redirect login.EventType.LOGIN_SUCCESS— Event fired when login completes successfully. We listen for this to set the active account.
Authentication flow in our app:
- User clicks "Sign in with Microsoft" →
loginRedirect(loginRequest) - Browser redirects to
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize - User authenticates → Entra ID redirects back to
http://localhost:5173with an authorization code handleRedirectPromise()exchanges the code for access + refresh tokens (stored in localStorage)- On each API call, Axios interceptor calls
acquireTokenSilent()to get a fresh access token - Token is attached as
Authorization: Bearer {token}header
@azure/msal-react (v5.3.1)
@azure/msal-react is the React-specific wrapper around @azure/msal-browser. It provides React components and hooks that integrate MSAL into the React component lifecycle, making it easy to build authentication-aware UIs.
Important components and hooks used in this project:
MsalProvider— A React context provider that wraps the app and makes the MSAL instance available to all child components. OurCLSAuthProviderComponentrenders<MsalProvider instance={msalInstance}>around the entire app.useIsAuthenticated()— A hook that returnstrueif the user has an active account (is logged in). Used inCLSAuthGuardComponentto decide whether to show the dashboard or the sign-in screen.useMsal()— A hook that returns the MSAL instance, the list of accounts, and the current interaction status. We use it inCLSAuthGuardComponent(to callloginRedirect) andCLSAppLayoutComponent(to show the user's name and handle sign-out).InteractionStatus— An enum indicating whether MSAL is currently processing a login, logout, or token acquisition. We checkinProgress !== InteractionStatus.Noneto show a loading spinner while authentication is in progress.
Why two packages? @azure/msal-browser is the core library that works in any JavaScript environment (vanilla JS, Angular, Vue, etc.). @azure/msal-react adds React-specific bindings (context, hooks, components) so you don't have to manually manage the MSAL lifecycle with useEffect and useState — though our CLSAuthProviderComponent does use useEffect for the initial setup because MSAL v3+ requires explicit initialize() before any operations.
6. API: Configuration
appsettings.json (Note that placeholders MUST be replaced by your Subscription Values)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning"
}
}
},
"Azure": {
"TenantId": "<YOUR_AZURE_TENANT_ID>",
"ClientId": "<YOUR_API_APP_REGISTRATION_CLIENT_ID>",
"ClientSecret": "<YOUR_API_CLIENT_SECRET>",
"WorkspaceId": "<YOUR_LOG_ANALYTICS_WORKSPACE_ID>",
"ResourceId": "<YOUR_APPLICATION_INSIGHTS_RESOURCE_ID>"
},
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<YOUR_AZURE_TENANT_ID>",
"ClientId": "<YOUR_API_APP_REGISTRATION_CLIENT_ID>",
"Audience": "api://<YOUR_API_APP_REGISTRATION_CLIENT_ID>/access_as_user"
},
"AllowedHosts": "*"
}
dotnet user-secrets locally, Azure Key Vault in production.CLSAzureSettings.cs
namespace AppInsights.Api.Configuration;
/// <summary>
/// Configuration settings for connecting to Azure Application Insights
/// via the Log Analytics Workspace. Bound to the "Azure" section in appsettings.json.
/// These settings are used to authenticate with Azure and identify which workspace to query.
/// </summary>
public class CLSAzureSettings
{
/// <summary>
/// The configuration section name in appsettings.json that maps to this class.
/// </summary>
public const string SectionName = "Azure";
/// <summary>
/// The Microsoft Entra ID (Azure AD) tenant identifier.
/// Used to construct the authentication authority URL.
/// </summary>
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// The App Registration client (application) ID used for service-to-service authentication
/// with Azure Monitor APIs via <see cref="Azure.Identity.ClientSecretCredential"/>.
/// </summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>
/// The App Registration client secret. Combined with <see cref="TenantId"/> and
/// <see cref="ClientId"/> to create a <see cref="Azure.Identity.ClientSecretCredential"/>.
/// Should be stored securely using User Secrets or Azure Key Vault.
/// </summary>
public string ClientSecret { get; set; } = string.Empty;
/// <summary>
/// The Log Analytics Workspace ID that backs the Application Insights resource.
/// All KQL queries are executed against this workspace using <see cref="Azure.Monitor.Query.LogsQueryClient"/>.
/// Found in Azure Portal under Log Analytics Workspace > Properties.
/// </summary>
public string WorkspaceId { get; set; } = string.Empty;
/// <summary>
/// Optional full Azure Resource ID of the Application Insights resource.
/// Used by <see cref="Azure.Monitor.Query.MetricsQueryClient"/> for metrics-specific queries.
/// Format: /subscriptions/{sub}/resourceGroups/{rg}/providers/microsoft.insights/components/{name}
/// </summary>
public string? ResourceId { get; set; }
}
CLSEntraIdSettings.cs
namespace AppInsights.Api.Configuration;
/// <summary>
/// Configuration settings for Microsoft Entra ID (formerly Azure AD) JWT bearer authentication.
/// Bound to the "AzureAd" section in appsettings.json.
/// Used by Microsoft.Identity.Web to validate incoming bearer tokens from the React SPA.
/// </summary>
public class CLSEntraIdSettings
{
/// <summary>
/// The configuration section name in appsettings.json that maps to this class.
/// </summary>
public const string SectionName = "AzureAd";
/// <summary>
/// The Entra ID login endpoint. Defaults to the global Azure AD endpoint.
/// Override for sovereign clouds (e.g., Azure Government, Azure China).
/// </summary>
public string Instance { get; set; } = "https://login.microsoftonline.com/";
/// <summary>
/// The Entra ID tenant ID where users authenticate.
/// Combined with <see cref="Instance"/> to form the authority URL.
/// </summary>
public string TenantId { get; set; } = string.Empty;
/// <summary>
/// The Client ID of the API app registration in Entra ID.
/// Tokens must be issued for this application to be accepted.
/// </summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>
/// The expected audience claim in the JWT token.
/// Typically matches the Application ID URI (e.g., "api://appinsights-dashboard").
/// </summary>
public string Audience { get; set; } = string.Empty;
/// <summary>
/// Comma-separated list of API scopes exposed by this API registration
/// (e.g., "access_as_user"). Used for fine-grained authorization.
/// </summary>
public string Scopes { get; set; } = string.Empty;
}
7. API: Program.cs
// =============================================================================
// Program.cs - Application Entry Point
// =============================================================================
// This is the bootstrap file for the ASP.NET Core 8 Web API that serves as the
// backend for the Azure Application Insights Dashboard. It configures:
// - Serilog structured logging
// - Microsoft Entra ID (Azure AD) JWT bearer authentication
// - Azure SDK clients (LogsQueryClient, MetricsQueryClient) for querying App Insights
// - Dependency injection for service layer (CLSLogsService, CLSMetricsService, CLSKqlService)
// - Swagger/OpenAPI documentation
// - CORS policy for the React SPA development server
// - Global error handling with ProblemDetails responses
// =============================================================================
using Azure.Identity;
using Azure.Monitor.Query;
using AppInsights.Api.Configuration;
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
using Serilog;
// Step 1: Create a bootstrap logger for capturing startup errors before the full pipeline is built
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
// Step 2: Replace the default logging with Serilog for structured, configurable logging
builder.Host.UseSerilog((context, config) => config
.ReadFrom.Configuration(context.Configuration)
.WriteTo.Console());
// Step 3: Configure Entra ID (Azure AD) JWT bearer authentication
// Microsoft.Identity.Web reads the "AzureAd" section from appsettings.json
// and sets up token validation automatically
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// Override audience validation to accept both GUID and URI formats of the app ID
// This is necessary because tokens may contain either format depending on the client
builder.Services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidAudiences = new[]
{
"<YOUR_API_APP_REGISTRATION_CLIENT_ID>",
"api://<YOUR_API_APP_REGISTRATION_CLIENT_ID>",
};
});
builder.Services.AddAuthorization();
// Step 4: Bind Azure settings from configuration and create Azure SDK clients
// CLSAzureSettings holds TenantId, ClientId, ClientSecret, and WorkspaceId
builder.Services.Configure<CLSAzureSettings>(builder.Configuration.GetSection(CLSAzureSettings.SectionName));
var azureSettings = builder.Configuration.GetSection(CLSAzureSettings.SectionName).Get<CLSAzureSettings>();
// Create Azure credential and register LogsQueryClient + MetricsQueryClient as singletons
// These clients are thread-safe and should be reused across requests
if (azureSettings != null && !string.IsNullOrEmpty(azureSettings.ClientId))
{
// Use explicit client credentials when ClientId is configured (CI/CD, production)
var credential = new ClientSecretCredential(azureSettings.TenantId, azureSettings.ClientId, azureSettings.ClientSecret);
builder.Services.AddSingleton(new LogsQueryClient(credential));
builder.Services.AddSingleton(new MetricsQueryClient(credential));
}
else
{
// Fallback to DefaultAzureCredential chain: Azure CLI, Managed Identity, Visual Studio, etc.
var credential = new DefaultAzureCredential();
builder.Services.AddSingleton(new LogsQueryClient(credential));
builder.Services.AddSingleton(new MetricsQueryClient(credential));
}
// Step 5: Register application services with dependency injection (scoped = per-request)
builder.Services.AddScoped<CLSILogsService, CLSLogsService>(); // Logs, traces, exceptions
builder.Services.AddScoped<CLSIMetricsService, CLSMetricsService>(); // Metrics, dependencies, availability
builder.Services.AddScoped<CLSIKqlService, CLSKqlService>(); // Raw KQL execution, saved queries
// Step 6: Configure ASP.NET Core features
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "AppInsights Dashboard API", Version = "v1" });
});
// Configure CORS to allow the React Vite dev server to call the API
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5173", "http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// In-memory cache for potential caching of expensive query results
builder.Services.AddMemoryCache();
var app = builder.Build();
// Step 7: Configure the HTTP request pipeline (middleware order matters!)
app.UseSwagger();
app.UseSwaggerUI();
app.UseSerilogRequestLogging(); // Log every HTTP request with Serilog
app.UseCors();
// Global exception handler: catches unhandled exceptions and returns ProblemDetails JSON
// In development, includes the exception message; in production, returns a generic message
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/problem+json";
var error = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
if (error != null)
{
Log.Error(error.Error, "Unhandled exception");
await context.Response.WriteAsJsonAsync(new
{
status = 500,
title = "Internal Server Error",
detail = app.Environment.IsDevelopment() ? error.Error.Message : "An unexpected error occurred."
});
}
});
});
// Authentication must come before Authorization in the pipeline
app.UseAuthentication();
app.UseAuthorization();
// Map attribute-routed controllers (e.g., [Route("api/[controller]")])
app.MapControllers();
Log.Information("AppInsights Dashboard API starting on {Urls}", string.Join(", ", app.Urls));
app.Run();
}
catch (Exception ex)
{
// Log fatal startup errors before the application terminates
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
// Ensure all buffered log events are flushed to sinks before process exit
Log.CloseAndFlush();
}
8. API: Data Models
LogEntry.cs
namespace AppInsights.Api.Models;
/// <summary>
/// Represents a single application trace log entry from the AppTraces table
/// in Azure Application Insights. Maps to diagnostic log messages emitted
/// via ILogger, Serilog, or other logging frameworks.
/// </summary>
public class CLSLogEntry
{
/// <summary>When the log message was emitted (TimeGenerated in KQL).</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The log message text content.</summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// Numeric severity level: 0=Verbose, 1=Information, 2=Warning, 3=Error, 4=Critical.
/// Maps to the SeverityLevel column in the AppTraces table.
/// </summary>
public int SeverityLevel { get; set; }
/// <summary>Human-readable severity name (e.g., "Error", "Warning"). Computed from <see cref="SeverityLevel"/>.</summary>
public string SeverityName { get; set; } = string.Empty;
/// <summary>
/// Unique identifier that correlates this log with other telemetry items
/// (requests, dependencies, exceptions) belonging to the same distributed operation.
/// </summary>
public string? OperationId { get; set; }
/// <summary>The name of the operation that generated this log (e.g., "GET /api/users").</summary>
public string? OperationName { get; set; }
/// <summary>The cloud role name (service name) that emitted this log. Useful in microservice architectures.</summary>
public string? AppRoleName { get; set; }
/// <summary>Additional key-value properties attached to the log entry, serialized as a JSON string.</summary>
public string? CustomDimensions { get; set; }
}
/// <summary>
/// Represents a single span in a distributed trace, which can be either an HTTP request
/// (from AppRequests) or a dependency call (from AppDependencies). Used for trace waterfall views.
/// </summary>
public class CLSTraceItem
{
/// <summary>When the span started executing.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The telemetry item type: "AppRequests", "AppDependencies", "AppTraces", or "AppExceptions".</summary>
public string ItemType { get; set; } = string.Empty;
/// <summary>The name of the request endpoint or dependency target (e.g., "GET /api/orders").</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The execution time in milliseconds. Key metric for identifying slow operations.</summary>
public double DurationMs { get; set; }
/// <summary>Whether the operation completed successfully (HTTP 2xx for requests).</summary>
public bool Success { get; set; }
/// <summary>The HTTP status code for requests, or result code for dependency calls.</summary>
public string? ResultCode { get; set; }
/// <summary>Links this span to its parent distributed trace. Shared across all spans in the same operation.</summary>
public string? OperationId { get; set; }
/// <summary>The ID of the parent span in the trace hierarchy.</summary>
public string? ParentId { get; set; }
/// <summary>Unique identifier for this specific span within the trace.</summary>
public string? Id { get; set; }
/// <summary>The target of a dependency call (e.g., SQL server hostname, external API URL).</summary>
public string? Target { get; set; }
/// <summary>The type of dependency (e.g., "SQL", "HTTP", "Azure blob", "Redis").</summary>
public string? DependencyType { get; set; }
}
/// <summary>
/// Represents a single exception captured by Application Insights from the AppExceptions table.
/// Includes exception details, stack trace, and correlation information.
/// </summary>
public class CLSExceptionEntry
{
/// <summary>When the exception was thrown.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The fully qualified type name of the exception (e.g., "System.NullReferenceException").</summary>
public string ExceptionType { get; set; } = string.Empty;
/// <summary>The exception message text.</summary>
public string Message { get; set; } = string.Empty;
/// <summary>The outermost exception message (may differ from inner exception message in wrapped exceptions).</summary>
public string? OuterMessage { get; set; }
/// <summary>The full stack trace of the exception, useful for debugging.</summary>
public string? StackTrace { get; set; }
/// <summary>Correlates the exception with the request/operation that caused it.</summary>
public string? OperationId { get; set; }
/// <summary>The cloud role name (service) where the exception was thrown.</summary>
public string? AppRoleName { get; set; }
/// <summary>Severity level of the exception: typically 3 (Error) or 4 (Critical).</summary>
public int SeverityLevel { get; set; }
}
/// <summary>
/// Represents a group of exceptions aggregated by type and message.
/// Used for the "Grouped by Type" view to identify the most frequent exceptions.
/// </summary>
public class CLSExceptionGroup
{
/// <summary>The fully qualified exception type name shared by all exceptions in this group.</summary>
public string ExceptionType { get; set; } = string.Empty;
/// <summary>The common outer message for exceptions in this group.</summary>
public string Message { get; set; } = string.Empty;
/// <summary>Total number of times this exception occurred in the selected time range.</summary>
public long Count { get; set; }
/// <summary>When the most recent occurrence of this exception was recorded.</summary>
public DateTimeOffset LastSeen { get; set; }
}
MetricModels.cs
namespace AppInsights.Api.Models;
/// <summary>
/// Describes an available metric in Application Insights.
/// Returned by the metrics definitions endpoint so the UI can populate a metric picker dropdown.
/// </summary>
public class CLSMetricDefinition
{
/// <summary>The internal metric name used in KQL queries (e.g., "requests/duration").</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Optional human-friendly display name for the metric.</summary>
public string? DisplayName { get; set; }
/// <summary>Optional unit of measurement (e.g., "ms", "bytes", "count").</summary>
public string? Unit { get; set; }
}
/// <summary>
/// Contains the time-series result of a metric query, including the metric name,
/// aggregation type, and data points over time. Used to render area/line charts.
/// </summary>
public class CLSMetricResult
{
/// <summary>The name of the queried metric.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Optional unit of measurement for the metric values.</summary>
public string? Unit { get; set; }
/// <summary>The aggregation function applied: "avg", "sum", "min", "max", or "count".</summary>
public string Aggregation { get; set; } = string.Empty;
/// <summary>The time-series data points produced by the metric query.</summary>
public List<CLSMetricDataPoint> Timeseries { get; set; } = new();
}
/// <summary>
/// A single data point in a metric time series, consisting of a timestamp and an aggregated value.
/// </summary>
public class CLSMetricDataPoint
{
/// <summary>The start of the time bucket for this data point.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The aggregated metric value for this time bucket. Null if no data exists for the period.</summary>
public double? Value { get; set; }
}
/// <summary>
/// Aggregated request statistics for the dashboard Overview page.
/// Combines results from multiple KQL queries into a single response:
/// totals, time-series, top failures, and response code distribution.
/// </summary>
public class CLSRequestSummary
{
/// <summary>Total number of HTTP requests received in the selected time range.</summary>
public long TotalRequests { get; set; }
/// <summary>Average response time across all requests, in milliseconds.</summary>
public double AvgDurationMs { get; set; }
/// <summary>Percentage of requests that failed (Success == false), from 0 to 100.</summary>
public double FailureRate { get; set; }
/// <summary>Total number of exceptions thrown in the selected time range.</summary>
public long TotalExceptions { get; set; }
/// <summary>Request count bucketed by time intervals, for the request volume line chart.</summary>
public List<CLSTimeSeriesPoint> RequestsOverTime { get; set; } = new();
/// <summary>Average response time bucketed by time intervals, for the duration trend line chart.</summary>
public List<CLSTimeSeriesPoint> AvgDurationOverTime { get; set; } = new();
/// <summary>Top 5 endpoints with the highest failure counts, for the bar chart.</summary>
public List<CLSNameValuePair> TopFailingEndpoints { get; set; } = new();
/// <summary>HTTP response code distribution grouped by category (2xx, 3xx, 4xx, 5xx), for the pie chart.</summary>
public List<CLSNameValuePair> ResponseCodeDistribution { get; set; } = new();
}
/// <summary>
/// A single point in a time series, consisting of a timestamp and a numeric value.
/// Used for charting request volume and response time over time.
/// </summary>
public class CLSTimeSeriesPoint
{
/// <summary>The start of the time bucket.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The aggregated value for this time bucket.</summary>
public double Value { get; set; }
}
/// <summary>
/// A simple name-value pair used for categorical data like top endpoints or response code distributions.
/// Suitable for bar charts and pie charts.
/// </summary>
public class CLSNameValuePair
{
/// <summary>The category name (e.g., endpoint name, response code group "2xx").</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The numeric value for this category (e.g., request count, failure count).</summary>
public double Value { get; set; }
}
/// <summary>
/// Represents an external dependency call aggregated by name and type.
/// Includes average duration, failure rate, and total call count for dependency analysis.
/// </summary>
public class CLSDependencyEntry
{
/// <summary>The dependency call target name (e.g., SQL server, external API URL).</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The type of dependency (e.g., "SQL", "HTTP", "Azure blob", "Redis").</summary>
public string Type { get; set; } = string.Empty;
/// <summary>Average execution time in milliseconds across all calls to this dependency.</summary>
public double AvgDurationMs { get; set; }
/// <summary>Percentage of calls to this dependency that failed (0–100).</summary>
public double FailureRate { get; set; }
/// <summary>Total number of times this dependency was called in the time range.</summary>
public long CallCount { get; set; }
}
/// <summary>
/// Represents a single availability (web test) result from the AppAvailabilityResults table.
/// Availability tests periodically ping your application from multiple global locations.
/// </summary>
public class CLSAvailabilityResult
{
/// <summary>When the availability test was executed.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>The name of the availability test (configured in Azure Portal).</summary>
public string TestName { get; set; } = string.Empty;
/// <summary>The geographic location from which the test was run (e.g., "East US", "West Europe").</summary>
public string Location { get; set; } = string.Empty;
/// <summary>Whether the availability test passed (true) or failed (false).</summary>
public bool Success { get; set; }
/// <summary>The time taken for the test to complete, in milliseconds.</summary>
public double DurationMs { get; set; }
/// <summary>Optional diagnostic message, typically populated when the test fails.</summary>
public string? Message { get; set; }
}
KqlModels.cs
namespace AppInsights.Api.Models;
/// <summary>
/// Request payload for executing a raw KQL (Kusto Query Language) query
/// against the Log Analytics Workspace via the POST /api/kql endpoint.
/// </summary>
public class CLSKqlQueryRequest
{
/// <summary>The KQL query string to execute (e.g., "AppRequests | take 10").</summary>
public string Query { get; set; } = string.Empty;
/// <summary>
/// Optional time range for the query (e.g., "1h", "24h", "7d").
/// Defaults to 24 hours if not specified. Limits the data scanned by the query engine.
/// </summary>
public string? TimeSpan { get; set; }
}
/// <summary>
/// The result of a KQL query execution, returned in a tabular format.
/// Contains column definitions (schema) and row data, suitable for rendering in a dynamic table.
/// </summary>
public class CLSKqlResult
{
/// <summary>The column definitions (name and data type) for the result table.</summary>
public List<CLSKqlColumn> Columns { get; set; } = new();
/// <summary>The row data as a list of arrays. Each inner list corresponds to one row, with values matching <see cref="Columns"/> by index.</summary>
public List<List<object?>> Rows { get; set; } = new();
/// <summary>The total number of rows returned by the query.</summary>
public int RowCount { get; set; }
}
/// <summary>
/// Describes a single column in a KQL query result, including its name and data type.
/// </summary>
public class CLSKqlColumn
{
/// <summary>The column name as returned by the KQL query.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The data type of the column (e.g., "string", "datetime", "long", "real", "bool").</summary>
public string Type { get; set; } = string.Empty;
}
/// <summary>
/// Represents a saved KQL query that users can name, describe, and reuse.
/// Persisted to a local JSON file on the server.
/// </summary>
public class CLSSavedQuery
{
/// <summary>Unique identifier for the saved query, auto-generated as a GUID.</summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>User-assigned name for the query (e.g., "Top Errors (Last 24h)").</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The KQL query text.</summary>
public string Query { get; set; } = string.Empty;
/// <summary>Optional description explaining what the query does.</summary>
public string? Description { get; set; }
/// <summary>When the query was saved, in UTC.</summary>
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
/// <summary>
/// Represents a paged query request with optional time range, search text, and pagination parameters.
/// Used by endpoints that support server-side pagination.
/// </summary>
public class CLSQueryPagedRequest
{
/// <summary>Optional time range filter (e.g., "24h", "7d").</summary>
public string? TimeRange { get; set; }
/// <summary>Number of items to return per page. Defaults to 50.</summary>
public int PageSize { get; set; } = 50;
/// <summary>Zero-based page index for pagination.</summary>
public int Page { get; set; } = 0;
/// <summary>Optional search text to filter results.</summary>
public string? Search { get; set; }
}
9. API: Service Interfaces
ILogsService.cs
using AppInsights.Api.Models;
namespace AppInsights.Api.Services;
/// <summary>
/// Defines the contract for querying log-based telemetry from Azure Application Insights.
/// Covers data from the AppTraces, AppRequests, and AppExceptions tables in Log Analytics.
/// </summary>
public interface CLSILogsService
{
/// <summary>
/// Retrieves paginated application trace log entries from the AppTraces table.
/// Supports filtering by severity level and free-text search within messages.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d"). Defaults to 24h.</param>
/// <param name="severity">Optional severity level filter (0=Verbose through 4=Critical).</param>
/// <param name="search">Optional text to search for within log messages.</param>
/// <param name="pageSize">Number of log entries per page.</param>
/// <param name="page">Zero-based page index.</param>
/// <returns>A list of <see cref="CLSLogEntry"/> objects for the requested page.</returns>
Task<List<CLSLogEntry>> FNGetLogsAsync(string? timeRange, int? severity, string? search, int pageSize, int page);
/// <summary>
/// Retrieves paginated request traces from the AppRequests table.
/// Supports filtering by operation name, minimum duration, and success status.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <param name="operationName">Optional filter to search within operation names.</param>
/// <param name="minDurationMs">Optional minimum duration threshold in milliseconds.</param>
/// <param name="success">Optional filter for successful (true) or failed (false) requests.</param>
/// <param name="pageSize">Number of trace items per page.</param>
/// <param name="page">Zero-based page index.</param>
/// <returns>A list of <see cref="CLSTraceItem"/> objects for the requested page.</returns>
Task<List<CLSTraceItem>> FNGetTracesAsync(string? timeRange, string? operationName, double? minDurationMs, bool? success, int pageSize, int page);
/// <summary>
/// Retrieves all spans belonging to a specific distributed trace, identified by Operation ID.
/// Queries across AppRequests, AppDependencies, AppTraces, and AppExceptions tables using a union.
/// Used for the trace waterfall visualization in the UI.
/// </summary>
/// <param name="operationId">The Operation ID that links all spans in the distributed trace.</param>
/// <returns>A list of <see cref="CLSTraceItem"/> objects ordered chronologically within the trace.</returns>
Task<List<CLSTraceItem>> FNGetTraceByOperationIdAsync(string operationId);
/// <summary>
/// Retrieves paginated exception entries from the AppExceptions table.
/// Supports filtering by exception type name.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <param name="exceptionType">Optional filter to search within exception type names.</param>
/// <param name="pageSize">Number of exception entries per page.</param>
/// <param name="page">Zero-based page index.</param>
/// <returns>A list of <see cref="CLSExceptionEntry"/> objects for the requested page.</returns>
Task<List<CLSExceptionEntry>> FNGetExceptionsAsync(string? timeRange, string? exceptionType, int pageSize, int page);
/// <summary>
/// Retrieves exceptions grouped (aggregated) by type and message, with occurrence counts.
/// Returns the top 50 exception groups sorted by frequency. Used for the "Grouped by Type" tab.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <returns>A list of <see cref="CLSExceptionGroup"/> objects sorted by count descending.</returns>
Task<List<CLSExceptionGroup>> FNGetExceptionGroupsAsync(string? timeRange);
}
IMetricsService.cs
using AppInsights.Api.Models;
namespace AppInsights.Api.Services;
/// <summary>
/// Defines the contract for querying metric and performance telemetry from Azure Application Insights.
/// Covers request summaries, dependency analysis, availability test results, and custom metrics.
/// </summary>
public interface CLSIMetricsService
{
/// <summary>
/// Builds a comprehensive request summary for the dashboard Overview page.
/// Executes multiple KQL queries to gather: total requests, average duration, failure rate,
/// exception count, request volume over time, duration trends, top failing endpoints,
/// and response code distribution.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d"). Defaults to 24h.</param>
/// <returns>A <see cref="CLSRequestSummary"/> containing all overview statistics and chart data.</returns>
Task<CLSRequestSummary> FNGetRequestSummaryAsync(string? timeRange);
/// <summary>
/// Retrieves paginated dependency call statistics aggregated by name and type.
/// Each entry includes average duration, failure rate, and total call count.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <param name="type">Optional dependency type filter (e.g., "SQL", "HTTP").</param>
/// <param name="pageSize">Number of dependency entries per page.</param>
/// <param name="page">Zero-based page index.</param>
/// <returns>A list of <see cref="CLSDependencyEntry"/> objects sorted by call count descending.</returns>
Task<List<CLSDependencyEntry>> FNGetDependenciesAsync(string? timeRange, string? type, int pageSize, int page);
/// <summary>
/// Retrieves paginated availability (web test) results from the AppAvailabilityResults table.
/// Supports filtering by test name.
/// </summary>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <param name="testName">Optional filter to search within test names.</param>
/// <param name="pageSize">Number of results per page.</param>
/// <param name="page">Zero-based page index.</param>
/// <returns>A list of <see cref="CLSAvailabilityResult"/> objects ordered by time descending.</returns>
Task<List<CLSAvailabilityResult>> FNGetAvailabilityAsync(string? timeRange, string? testName, int pageSize, int page);
/// <summary>
/// Queries a specific metric from the AppMetrics table and returns time-series data.
/// Supports configurable aggregation functions and time bucket intervals.
/// </summary>
/// <param name="metricName">The metric name to query (e.g., "requests/duration").</param>
/// <param name="timeRange">Time range shorthand (e.g., "1h", "24h", "7d").</param>
/// <param name="interval">Optional time bucket interval (e.g., "5m", "1h"). Auto-calculated if null.</param>
/// <param name="aggregation">Aggregation function: "avg", "sum", "min", "max", or "count".</param>
/// <returns>A <see cref="CLSMetricResult"/> containing the time-series data points.</returns>
Task<CLSMetricResult> FNGetMetricAsync(string metricName, string? timeRange, string? interval, string? aggregation);
/// <summary>
/// Retrieves the list of all available metric names from the AppMetrics and AppPerformanceCounters tables.
/// Used to populate the metric picker dropdown in the UI.
/// </summary>
/// <returns>A list of <see cref="CLSMetricDefinition"/> objects sorted alphabetically.</returns>
Task<List<CLSMetricDefinition>> FNGetMetricDefinitionsAsync();
}
IKqlService.cs
using AppInsights.Api.Models;
namespace AppInsights.Api.Services;
/// <summary>
/// Defines the contract for executing raw KQL (Kusto Query Language) queries
/// against the Log Analytics Workspace, and managing saved queries.
/// Queries are validated for safety before execution (no write/delete operations).
/// </summary>
public interface CLSIKqlService
{
/// <summary>
/// Executes a raw KQL query against the Log Analytics Workspace and returns tabular results.
/// The query is validated to ensure it does not contain dangerous commands (e.g., .drop, .delete).
/// </summary>
/// <param name="query">The KQL query string to execute.</param>
/// <param name="timeSpan">Optional time range for the query (e.g., "1h", "24h", "7d"). Defaults to 24h.</param>
/// <returns>A <see cref="CLSKqlResult"/> containing column definitions and row data.</returns>
/// <exception cref="ArgumentException">Thrown when the query is empty or contains disallowed keywords.</exception>
Task<CLSKqlResult> FNExecuteQueryAsync(string query, string? timeSpan);
/// <summary>
/// Retrieves all saved KQL queries from persistent storage.
/// Returns default example queries if no saved queries file exists.
/// </summary>
/// <returns>A list of <see cref="CLSSavedQuery"/> objects.</returns>
Task<List<CLSSavedQuery>> FNGetSavedQueriesAsync();
/// <summary>
/// Saves a new KQL query with a name and optional description.
/// Assigns a new unique ID and sets the creation timestamp.
/// </summary>
/// <param name="query">The <see cref="CLSSavedQuery"/> to save (Id and CreatedAt are overwritten).</param>
/// <returns>The saved query with its assigned ID and timestamp.</returns>
Task<CLSSavedQuery> FNSaveQueryAsync(CLSSavedQuery query);
/// <summary>
/// Deletes a saved KQL query by its unique identifier.
/// </summary>
/// <param name="id">The unique ID of the query to delete.</param>
/// <returns>True if the query was found and deleted; false if not found.</returns>
Task<bool> FNDeleteSavedQueryAsync(string id);
}
10. API: Service Implementations
LogsService.cs
Queries AppTraces, AppRequests, AppExceptions:
using Azure;
using Azure.Monitor.Query;
using Azure.Monitor.Query.Models;
using AppInsights.Api.Configuration;
using AppInsights.Api.Models;
using Microsoft.Extensions.Options;
namespace AppInsights.Api.Services;
/// <summary>
/// Implementation of <see cref="CLSILogsService"/> that queries log-based telemetry
/// from Azure Application Insights using the <see cref="LogsQueryClient"/> and KQL.
/// Handles AppTraces, AppRequests, and AppExceptions tables.
/// </summary>
public class CLSLogsService : CLSILogsService
{
/// <summary>Azure SDK client for executing KQL queries against Log Analytics.</summary>
private readonly LogsQueryClient _logsClient;
/// <summary>The Log Analytics Workspace ID to query.</summary>
private readonly string _workspaceId;
/// <summary>
/// Initializes a new instance of <see cref="CLSLogsService"/> with the required Azure SDK client and settings.
/// </summary>
/// <param name="logsClient">The Azure Monitor <see cref="LogsQueryClient"/> for executing KQL queries.</param>
/// <param name="settings">Azure configuration containing the Workspace ID.</param>
public CLSLogsService(LogsQueryClient logsClient, IOptions<CLSAzureSettings> settings)
{
_logsClient = logsClient;
_workspaceId = settings.Value.WorkspaceId;
}
/// <summary>
/// Retrieves paginated log entries from the AppTraces table.
/// Dynamically builds KQL filter clauses for severity and message search.
/// Uses the row_number() pattern for server-side pagination.
/// </summary>
public async Task<List<CLSLogEntry>> FNGetLogsAsync(string? timeRange, int? severity, string? search, int pageSize, int page)
{
var ts = FNParseTimeSpan(timeRange);
var filters = new List<string>();
if (severity.HasValue)
filters.Add($"| where SeverityLevel == {severity.Value}");
if (!string.IsNullOrWhiteSpace(search))
filters.Add($"| where Message contains \"{FNEscapeKql(search)}\"");
var filterClause = string.Join(" ", filters);
var offset = page * pageSize;
var fetchCount = offset + pageSize;
var query = $"AppTraces {filterClause} | order by TimeGenerated desc | take {fetchCount} | serialize rn=row_number() | where rn > {offset} | project-away rn | project TimeGenerated, Message, SeverityLevel, OperationId, OperationName, AppRoleName, Properties";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
return FNMapToLogEntries(response.Value);
}
/// <summary>
/// Retrieves paginated request traces from the AppRequests table.
/// Supports filtering by operation name, minimum duration, and success/failure status.
/// </summary>
public async Task<List<CLSTraceItem>> FNGetTracesAsync(string? timeRange, string? operationName, double? minDurationMs, bool? success, int pageSize, int page)
{
var ts = FNParseTimeSpan(timeRange);
var filters = new List<string>();
if (!string.IsNullOrWhiteSpace(operationName))
filters.Add($"| where OperationName contains \"{FNEscapeKql(operationName)}\"");
if (minDurationMs.HasValue)
filters.Add($"| where DurationMs > {minDurationMs.Value}");
if (success.HasValue)
filters.Add($"| where Success == {success.Value.ToString().ToLower()}");
var filterClause = string.Join(" ", filters);
var offset = page * pageSize;
var fetchCount = offset + pageSize;
var query = $"AppRequests {filterClause} | order by TimeGenerated desc | take {fetchCount} | serialize rn=row_number() | where rn > {offset} | project-away rn | project TimeGenerated, Type, Name, DurationMs, Success, ResultCode, OperationId, ParentId, Id";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
return FNMapToTraceItems(response.Value);
}
/// <summary>
/// Retrieves all spans for a specific distributed trace by Operation ID.
/// Unions across AppRequests, AppDependencies, AppTraces, and AppExceptions tables
/// to build a complete end-to-end trace waterfall view.
/// </summary>
/// <param name="operationId">The shared Operation ID for the distributed trace.</param>
public async Task<List<CLSTraceItem>> FNGetTraceByOperationIdAsync(string operationId)
{
var query = $"union AppRequests, AppDependencies, AppTraces, AppExceptions | where OperationId == \"{FNEscapeKql(operationId)}\" | order by TimeGenerated asc | take 200 | project TimeGenerated, Type, Name, DurationMs, Success, ResultCode, OperationId, ParentId, Id, Target, DependencyType=Type";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(TimeSpan.FromDays(7)));
return FNMapToTraceItemsFull(response.Value);
}
/// <summary>
/// Retrieves paginated exception entries from the AppExceptions table.
/// Supports filtering by exception type name.
/// </summary>
public async Task<List<CLSExceptionEntry>> FNGetExceptionsAsync(string? timeRange, string? exceptionType, int pageSize, int page)
{
var ts = FNParseTimeSpan(timeRange);
var filters = new List<string>();
if (!string.IsNullOrWhiteSpace(exceptionType))
filters.Add($"| where ExceptionType contains \"{FNEscapeKql(exceptionType)}\"");
var filterClause = string.Join(" ", filters);
var offset = page * pageSize;
var fetchCount = offset + pageSize;
var query = $"AppExceptions {filterClause} | order by TimeGenerated desc | take {fetchCount} | serialize rn=row_number() | where rn > {offset} | project-away rn | project TimeGenerated, ExceptionType, OuterMessage, Details, OperationId, AppRoleName, SeverityLevel";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
return FNMapToExceptions(response.Value);
}
/// <summary>
/// Groups exceptions by type and outer message, returning the top 50 most frequent.
/// Each group includes a count and the timestamp of the most recent occurrence.
/// </summary>
public async Task<List<CLSExceptionGroup>> FNGetExceptionGroupsAsync(string? timeRange)
{
var ts = FNParseTimeSpan(timeRange);
var query = "AppExceptions | summarize count_=count(), lastSeen=max(TimeGenerated) by ExceptionType, OuterMessage | order by count_ desc | take 50";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
var result = new List<CLSExceptionGroup>();
foreach (var row in response.Value.Table.Rows)
{
result.Add(new CLSExceptionGroup
{
ExceptionType = row.FNGetString("ExceptionType") ?? "",
Message = row.FNGetString("OuterMessage") ?? "",
Count = row.FNGetInt64("count_") ?? 0,
LastSeen = row.FNGetDateTimeOffset("lastSeen") ?? DateTimeOffset.MinValue
});
}
return result;
}
/// <summary>
/// Parses a human-readable time range string (e.g., "30m", "24h", "7d") into a <see cref="TimeSpan"/>.
/// Falls back to 24 hours if the input is null, empty, or unrecognized.
/// </summary>
private static TimeSpan FNParseTimeSpan(string? timeRange)
{
if (string.IsNullOrWhiteSpace(timeRange)) return TimeSpan.FromHours(24);
return timeRange.ToLower() switch
{
"30m" => TimeSpan.FromMinutes(30),
"1h" => TimeSpan.FromHours(1),
"6h" => TimeSpan.FromHours(6),
"12h" => TimeSpan.FromHours(12),
"24h" or "1d" => TimeSpan.FromDays(1),
"7d" => TimeSpan.FromDays(7),
"30d" => TimeSpan.FromDays(30),
_ => TimeSpan.TryParse(timeRange, out var ts) ? ts : TimeSpan.FromHours(24)
};
}
/// <summary>
/// Escapes special characters in user input to prevent KQL injection attacks.
/// </summary>
private static string FNEscapeKql(string input) => input.Replace("\"", "\\\"").Replace("'", "\\'");
/// <summary>
/// Maps a KQL query result to a list of <see cref="CLSLogEntry"/> DTOs.
/// Extracts columns: TimeGenerated, Message, SeverityLevel, OperationId, OperationName, AppRoleName, Properties.
/// </summary>
private static List<CLSLogEntry> FNMapToLogEntries(LogsQueryResult result)
{
var entries = new List<CLSLogEntry>();
foreach (var row in result.Table.Rows)
{
entries.Add(new CLSLogEntry
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
Message = row.FNGetString("Message") ?? "",
SeverityLevel = (int)(row.FNGetInt64("SeverityLevel") ?? 0),
SeverityName = FNSeverityToName((int)(row.FNGetInt64("SeverityLevel") ?? 0)),
OperationId = row.FNGetString("OperationId"),
OperationName = row.FNGetString("OperationName"),
AppRoleName = row.FNGetString("AppRoleName"),
CustomDimensions = row.FNGetString("Properties")
});
}
return entries;
}
/// <summary>
/// Maps a KQL query result to a list of <see cref="CLSTraceItem"/> DTOs (basic fields only).
/// Used for the traces list view.
/// </summary>
private static List<CLSTraceItem> FNMapToTraceItems(LogsQueryResult result)
{
var items = new List<CLSTraceItem>();
foreach (var row in result.Table.Rows)
{
items.Add(new CLSTraceItem
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
ItemType = row.FNGetString("Type") ?? "AppRequests",
Name = row.FNGetString("Name") ?? "",
DurationMs = row.FNGetDouble("DurationMs") ?? 0,
Success = row.FNGetBoolean("Success") ?? false,
ResultCode = row.FNGetString("ResultCode"),
OperationId = row.FNGetString("OperationId"),
ParentId = row.FNGetString("ParentId"),
Id = row.FNGetString("Id")
});
}
return items;
}
/// <summary>
/// Maps a KQL query result to a list of <see cref="CLSTraceItem"/> DTOs with all fields
/// including Target and DependencyType. Used for the detailed trace waterfall view.
/// </summary>
private static List<CLSTraceItem> FNMapToTraceItemsFull(LogsQueryResult result)
{
var items = new List<CLSTraceItem>();
foreach (var row in result.Table.Rows)
{
items.Add(new CLSTraceItem
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
ItemType = row.FNGetString("Type") ?? "",
Name = row.FNGetString("Name") ?? "",
DurationMs = row.FNGetDouble("DurationMs") ?? 0,
Success = row.FNGetBoolean("Success") ?? false,
ResultCode = row.FNGetString("ResultCode"),
OperationId = row.FNGetString("OperationId"),
ParentId = row.FNGetString("ParentId"),
Id = row.FNGetString("Id"),
Target = row.FNGetString("Target"),
DependencyType = row.FNGetString("DependencyType")
});
}
return items;
}
/// <summary>
/// Maps a KQL query result to a list of <see cref="CLSExceptionEntry"/> DTOs.
/// </summary>
private static List<CLSExceptionEntry> FNMapToExceptions(LogsQueryResult result)
{
var entries = new List<CLSExceptionEntry>();
foreach (var row in result.Table.Rows)
{
entries.Add(new CLSExceptionEntry
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
ExceptionType = row.FNGetString("ExceptionType") ?? "",
OuterMessage = row.FNGetString("OuterMessage"),
StackTrace = row.FNGetString("Details"),
OperationId = row.FNGetString("OperationId"),
AppRoleName = row.FNGetString("AppRoleName"),
SeverityLevel = (int)(row.FNGetInt64("SeverityLevel") ?? 0)
});
}
return entries;
}
/// <summary>
/// Converts a numeric severity level to its human-readable name.
/// Application Insights uses: 0=Verbose, 1=Information, 2=Warning, 3=Error, 4=Critical.
/// </summary>
private static string FNSeverityToName(int level) => level switch
{
0 => "Verbose",
1 => "Information",
2 => "Warning",
3 => "Error",
4 => "Critical",
_ => "Unknown"
};
}
/// <summary>
/// Extension methods for safely extracting typed values from <see cref="LogsTableRow"/>.
/// Each method handles missing columns and type conversion errors gracefully by returning null.
/// </summary>
internal static class CLSLogsTableRowExtensions
{
/// <summary>Safely extracts a string value from the specified column, returning null on failure.</summary>
public static string? FNGetString(this LogsTableRow row, string column)
{
try { return row[column]?.ToString(); }
catch { return null; }
}
/// <summary>Safely extracts a long (Int64) value from the specified column, returning null on failure.</summary>
public static long? FNGetInt64(this LogsTableRow row, string column)
{
try { var val = row[column]; return val == null ? null : Convert.ToInt64(val); }
catch { return null; }
}
/// <summary>Safely extracts a double value from the specified column, returning null on failure.</summary>
public static double? FNGetDouble(this LogsTableRow row, string column)
{
try { var val = row[column]; return val == null ? null : Convert.ToDouble(val); }
catch { return null; }
}
/// <summary>Safely extracts a boolean value from the specified column, returning null on failure.</summary>
public static bool? FNGetBoolean(this LogsTableRow row, string column)
{
try { var val = row[column]; return val == null ? null : Convert.ToBoolean(val); }
catch { return null; }
}
/// <summary>
/// Safely extracts a DateTimeOffset value from the specified column.
/// Handles both <see cref="DateTimeOffset"/> and <see cref="DateTime"/> raw types,
/// and falls back to string parsing. Returns null on failure.
/// </summary>
public static DateTimeOffset? FNGetDateTimeOffset(this LogsTableRow row, string column)
{
try
{
var val = row[column];
if (val is DateTimeOffset dto) return dto;
if (val is DateTime dt) return new DateTimeOffset(dt, TimeSpan.Zero);
return val == null ? null : DateTimeOffset.Parse(val.ToString()!);
}
catch { return null; }
}
}
MetricsService.cs
Overview dashboard, dependencies, availability, metrics:
using Azure.Monitor.Query;
using AppInsights.Api.Configuration;
using AppInsights.Api.Models;
using Microsoft.Extensions.Options;
namespace AppInsights.Api.Services;
/// <summary>
/// Implementation of <see cref="CLSIMetricsService"/> that queries metric and performance telemetry
/// from Azure Application Insights using KQL. Handles request summaries, dependency analysis,
/// availability test results, and custom metric time-series data.
/// </summary>
public class CLSMetricsService : CLSIMetricsService
{
/// <summary>Azure SDK client for executing KQL queries against Log Analytics.</summary>
private readonly LogsQueryClient _logsClient;
/// <summary>Azure SDK client for querying the Metrics API (used for metric definitions).</summary>
private readonly MetricsQueryClient _metricsClient;
/// <summary>The Log Analytics Workspace ID to query.</summary>
private readonly string _workspaceId;
/// <summary>Optional Application Insights resource ID for Metrics API queries.</summary>
private readonly string? _resourceId;
/// <summary>
/// Initializes a new instance of <see cref="CLSMetricsService"/>.
/// </summary>
/// <param name="logsClient">The Azure Monitor <see cref="LogsQueryClient"/> for KQL queries.</param>
/// <param name="metricsClient">The Azure Monitor <see cref="MetricsQueryClient"/> for metric queries.</param>
/// <param name="settings">Azure configuration containing Workspace ID and optional Resource ID.</param>
public CLSMetricsService(LogsQueryClient logsClient, MetricsQueryClient metricsClient, IOptions<CLSAzureSettings> settings)
{
_logsClient = logsClient;
_metricsClient = metricsClient;
_workspaceId = settings.Value.WorkspaceId;
_resourceId = settings.Value.ResourceId;
}
/// <summary>
/// Builds a comprehensive request summary by executing six KQL queries:
/// 1) Total requests, avg duration, failure rate
/// 2) Total exception count
/// 3) Request volume over time (for line chart)
/// 4) Average duration over time (for line chart)
/// 5) Top 5 failing endpoints (for bar chart)
/// 6) Response code distribution by category (for pie chart)
/// </summary>
public async Task<CLSRequestSummary> FNGetRequestSummaryAsync(string? timeRange)
{
var ts = FNParseTimeSpan(timeRange);
var summary = new CLSRequestSummary();
// Query 1: Aggregate totals - request count, average duration, failure percentage
var totalsQuery = "AppRequests | summarize totalRequests=count(), avgDuration=avg(DurationMs), failureRate=countif(Success==false)*100.0/count()";
var totalsResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, totalsQuery, new QueryTimeRange(ts));
foreach (var row in totalsResponse.Value.Table.Rows)
{
summary.TotalRequests = row.FNGetInt64("totalRequests") ?? 0;
summary.AvgDurationMs = Math.Round(row.FNGetDouble("avgDuration") ?? 0, 2);
summary.FailureRate = Math.Round(row.FNGetDouble("failureRate") ?? 0, 2);
}
// Query 2: Total exceptions count in the time window
var exQuery = "AppExceptions | summarize totalExceptions=count()";
var exResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, exQuery, new QueryTimeRange(ts));
foreach (var row in exResponse.Value.Table.Rows)
{
summary.TotalExceptions = row.FNGetInt64("totalExceptions") ?? 0;
}
// Query 3: Request volume bucketed by dynamic time intervals
var interval = FNGetBinInterval(ts);
var overTimeQuery = $"AppRequests | summarize requestCount=count() by bin(TimeGenerated, {interval}) | order by TimeGenerated asc";
var overTimeResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, overTimeQuery, new QueryTimeRange(ts));
foreach (var row in overTimeResponse.Value.Table.Rows)
{
summary.RequestsOverTime.Add(new CLSTimeSeriesPoint
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
Value = row.FNGetDouble("requestCount") ?? 0
});
}
// Query 4: Average response duration bucketed by time intervals
var durationOverTimeQuery = $"AppRequests | summarize avgDuration=avg(DurationMs) by bin(TimeGenerated, {interval}) | order by TimeGenerated asc";
var durationResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, durationOverTimeQuery, new QueryTimeRange(ts));
foreach (var row in durationResponse.Value.Table.Rows)
{
summary.AvgDurationOverTime.Add(new CLSTimeSeriesPoint
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
Value = Math.Round(row.FNGetDouble("avgDuration") ?? 0, 2)
});
}
// Query 5: Top 5 endpoints with the most failures
var failQuery = "AppRequests | where Success==false | summarize failCount=count() by Name | order by failCount desc | take 5";
var failResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, failQuery, new QueryTimeRange(ts));
foreach (var row in failResponse.Value.Table.Rows)
{
summary.TopFailingEndpoints.Add(new CLSNameValuePair
{
Name = row.FNGetString("Name") ?? "",
Value = row.FNGetDouble("failCount") ?? 0
});
}
// Query 6: Response code distribution grouped into 2xx, 3xx, 4xx, 5xx categories
var codeQuery = "AppRequests | extend codeGroup=case(toint(ResultCode)>=200 and toint(ResultCode)<300, '2xx', toint(ResultCode)>=300 and toint(ResultCode)<400, '3xx', toint(ResultCode)>=400 and toint(ResultCode)<500, '4xx', toint(ResultCode)>=500, '5xx', 'Other') | summarize codeCount=count() by codeGroup | order by codeGroup asc";
var codeResponse = await _logsClient.QueryWorkspaceAsync(_workspaceId, codeQuery, new QueryTimeRange(ts));
foreach (var row in codeResponse.Value.Table.Rows)
{
summary.ResponseCodeDistribution.Add(new CLSNameValuePair
{
Name = row.FNGetString("codeGroup") ?? "",
Value = row.FNGetDouble("codeCount") ?? 0
});
}
return summary;
}
/// <summary>
/// Retrieves paginated dependency call statistics aggregated by name and type.
/// Computes average duration, failure rate, and call count per dependency.
/// </summary>
public async Task<List<CLSDependencyEntry>> FNGetDependenciesAsync(string? timeRange, string? type, int pageSize, int page)
{
var ts = FNParseTimeSpan(timeRange);
var typeFilter = !string.IsNullOrWhiteSpace(type) ? $"| where DependencyType == \"{FNEscapeKql(type)}\"" : "";
var offset = page * pageSize;
var fetchCount = offset + pageSize;
var query = $"AppDependencies {typeFilter} | summarize avgDuration=avg(DurationMs), failureRate=countif(Success==false)*100.0/count(), callCount=count() by Name, DependencyType | order by callCount desc | take {fetchCount} | serialize rn=row_number() | where rn > {offset} | project-away rn";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
var result = new List<CLSDependencyEntry>();
foreach (var row in response.Value.Table.Rows)
{
result.Add(new CLSDependencyEntry
{
Name = row.FNGetString("Name") ?? "",
Type = row.FNGetString("DependencyType") ?? "",
AvgDurationMs = Math.Round(row.FNGetDouble("avgDuration") ?? 0, 2),
FailureRate = Math.Round(row.FNGetDouble("failureRate") ?? 0, 2),
CallCount = row.FNGetInt64("callCount") ?? 0
});
}
return result;
}
/// <summary>
/// Retrieves paginated availability test results from the AppAvailabilityResults table.
/// Supports filtering by test name.
/// </summary>
public async Task<List<CLSAvailabilityResult>> FNGetAvailabilityAsync(string? timeRange, string? testName, int pageSize, int page)
{
var ts = FNParseTimeSpan(timeRange);
var nameFilter = !string.IsNullOrWhiteSpace(testName) ? $"| where Name contains \"{FNEscapeKql(testName)}\"" : "";
var offset = page * pageSize;
var fetchCount = offset + pageSize;
var query = $"AppAvailabilityResults {nameFilter} | order by TimeGenerated desc | take {fetchCount} | serialize rn=row_number() | where rn > {offset} | project-away rn | project TimeGenerated, Name, Location, Success, DurationMs, Message";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
var result = new List<CLSAvailabilityResult>();
foreach (var row in response.Value.Table.Rows)
{
result.Add(new CLSAvailabilityResult
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
TestName = row.FNGetString("Name") ?? "",
Location = row.FNGetString("Location") ?? "",
Success = row.FNGetBoolean("Success") ?? false,
DurationMs = row.FNGetDouble("DurationMs") ?? 0,
Message = row.FNGetString("Message")
});
}
return result;
}
/// <summary>
/// Queries a specific metric from the AppMetrics table with configurable aggregation and time bucketing.
/// Returns time-series data suitable for rendering area/line charts.
/// </summary>
public async Task<CLSMetricResult> FNGetMetricAsync(string metricName, string? timeRange, string? interval, string? aggregation)
{
var ts = FNParseTimeSpan(timeRange);
var agg = aggregation?.ToLower() ?? "avg";
var binInterval = interval ?? FNGetBinInterval(ts);
var aggFunction = agg switch
{
"sum" => "sum",
"min" => "min",
"max" => "max",
"count" => "count",
_ => "avg"
};
var query = $"AppMetrics | where Name == \"{FNEscapeKql(metricName)}\" | summarize metricValue={aggFunction}(Sum) by bin(TimeGenerated, {binInterval}) | order by TimeGenerated asc";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
var result = new CLSMetricResult
{
Name = metricName,
Aggregation = agg
};
foreach (var row in response.Value.Table.Rows)
{
result.Timeseries.Add(new CLSMetricDataPoint
{
Timestamp = row.FNGetDateTimeOffset("TimeGenerated") ?? DateTimeOffset.MinValue,
Value = row.FNGetDouble("metricValue")
});
}
return result;
}
/// <summary>
/// Retrieves all distinct metric names from AppMetrics and AppPerformanceCounters tables.
/// Used to populate the metric picker dropdown in the frontend.
/// </summary>
public async Task<List<CLSMetricDefinition>> FNGetMetricDefinitionsAsync()
{
var query = "union AppMetrics, AppPerformanceCounters | summarize by Name | order by Name asc | take 100";
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(TimeSpan.FromDays(1)));
var result = new List<CLSMetricDefinition>();
foreach (var row in response.Value.Table.Rows)
{
result.Add(new CLSMetricDefinition { Name = row.FNGetString("Name") ?? "" });
}
return result;
}
/// <summary>
/// Parses a human-readable time range string into a <see cref="TimeSpan"/>.
/// Defaults to 24 hours for unrecognized or null inputs.
/// </summary>
private static TimeSpan FNParseTimeSpan(string? timeRange)
{
if (string.IsNullOrWhiteSpace(timeRange)) return TimeSpan.FromHours(24);
return timeRange.ToLower() switch
{
"30m" => TimeSpan.FromMinutes(30),
"1h" => TimeSpan.FromHours(1),
"6h" => TimeSpan.FromHours(6),
"12h" => TimeSpan.FromHours(12),
"24h" or "1d" => TimeSpan.FromDays(1),
"7d" => TimeSpan.FromDays(7),
"30d" => TimeSpan.FromDays(30),
_ => TimeSpan.TryParse(timeRange, out var ts) ? ts : TimeSpan.FromHours(24)
};
}
/// <summary>
/// Determines the appropriate KQL bin() interval based on the total time span.
/// Shorter ranges use finer granularity; longer ranges use coarser buckets to avoid excessive data points.
/// </summary>
private static string FNGetBinInterval(TimeSpan ts)
{
if (ts.TotalMinutes <= 30) return "1m";
if (ts.TotalHours <= 1) return "1m";
if (ts.TotalHours <= 6) return "5m";
if (ts.TotalHours <= 24) return "15m";
if (ts.TotalDays <= 7) return "1h";
return "6h";
}
/// <summary>
/// Escapes special characters in user input to prevent KQL injection.
/// </summary>
private static string FNEscapeKql(string input) => input.Replace("\"", "\\\"").Replace("'", "\\'");
}
KqlService.cs
Raw KQL execution with safety validation:
using System.Text.Json;
using Azure.Monitor.Query;
using AppInsights.Api.Configuration;
using AppInsights.Api.Models;
using Microsoft.Extensions.Options;
using Serilog;
using Log = Serilog.Log;
namespace AppInsights.Api.Services;
/// <summary>
/// Implementation of <see cref="CLSIKqlService"/> that provides raw KQL query execution
/// against the Log Analytics Workspace, with query safety validation and saved query management.
/// Queries are validated to block dangerous operations before execution.
/// </summary>
public class CLSKqlService : CLSIKqlService
{
/// <summary>Azure SDK client for executing KQL queries.</summary>
private readonly LogsQueryClient _logsClient;
/// <summary>The Log Analytics Workspace ID to query.</summary>
private readonly string _workspaceId;
/// <summary>File path for persisting saved queries as JSON.</summary>
private readonly string _savedQueriesPath;
/// <summary>
/// KQL management commands that could modify or delete data.
/// Any query containing these keywords will be rejected by <see cref="FNValidateQuery"/>.
/// </summary>
private static readonly string[] DangerousKeywords = { ".set", ".drop", ".delete", ".purge", ".replace", ".create-or-alter" };
/// <summary>
/// Initializes a new instance of <see cref="CLSKqlService"/>.
/// </summary>
/// <param name="logsClient">The Azure Monitor <see cref="LogsQueryClient"/> for executing KQL.</param>
/// <param name="settings">Azure configuration containing the Workspace ID.</param>
/// <param name="env">Web host environment used to determine the content root for saved queries storage.</param>
public CLSKqlService(LogsQueryClient logsClient, IOptions<CLSAzureSettings> settings, IWebHostEnvironment env)
{
_logsClient = logsClient;
_workspaceId = settings.Value.WorkspaceId;
_savedQueriesPath = Path.Combine(env.ContentRootPath, "saved-queries.json");
}
/// <summary>
/// Executes a raw KQL query against the Log Analytics Workspace after safety validation.
/// Returns the result as a tabular structure with column definitions and row data.
/// </summary>
/// <param name="query">The KQL query to execute. Must be read-only (no management commands).</param>
/// <param name="timeSpan">Optional time range (e.g., "1h", "7d"). Defaults to 24h.</param>
/// <returns>A <see cref="CLSKqlResult"/> with columns and rows from the query.</returns>
/// <exception cref="ArgumentException">Thrown when the query is empty or contains dangerous keywords.</exception>
public async Task<CLSKqlResult> FNExecuteQueryAsync(string query, string? timeSpan)
{
FNValidateQuery(query);
Log.Information("Executing KQL query: {Query}", query);
var ts = string.IsNullOrWhiteSpace(timeSpan) ? TimeSpan.FromHours(24) : FNParseTimeSpan(timeSpan);
var response = await _logsClient.QueryWorkspaceAsync(_workspaceId, query, new QueryTimeRange(ts));
var table = response.Value.Table;
var result = new CLSKqlResult
{
Columns = table.Columns.Select(c => new CLSKqlColumn
{
Name = c.Name,
Type = c.Type.ToString()
}).ToList(),
RowCount = table.Rows.Count
};
foreach (var row in table.Rows)
{
var rowData = new List<object?>();
for (int i = 0; i < table.Columns.Count; i++)
{
rowData.Add(row[i]);
}
result.Rows.Add(rowData);
}
return result;
}
/// <summary>
/// Retrieves all saved queries from the JSON file on disk.
/// Returns a default set of example queries if no saved queries file exists.
/// </summary>
public async Task<List<CLSSavedQuery>> FNGetSavedQueriesAsync()
{
if (!File.Exists(_savedQueriesPath))
return FNGetDefaultQueries();
var json = await File.ReadAllTextAsync(_savedQueriesPath);
return JsonSerializer.Deserialize<List<CLSSavedQuery>>(json) ?? new List<CLSSavedQuery>();
}
/// <summary>
/// Saves a new KQL query to persistent storage with a freshly generated ID and timestamp.
/// </summary>
/// <param name="query">The query to save. ID and CreatedAt will be overwritten.</param>
/// <returns>The saved query with its assigned ID and creation timestamp.</returns>
public async Task<CLSSavedQuery> FNSaveQueryAsync(CLSSavedQuery query)
{
var queries = await FNGetSavedQueriesAsync();
query.Id = Guid.NewGuid().ToString();
query.CreatedAt = DateTimeOffset.UtcNow;
queries.Add(query);
await FNSaveQueriesFileAsync(queries);
return query;
}
/// <summary>
/// Deletes a saved query by its unique identifier.
/// </summary>
/// <param name="id">The ID of the query to remove.</param>
/// <returns>True if the query was found and deleted; false otherwise.</returns>
public async Task<bool> FNDeleteSavedQueryAsync(string id)
{
var queries = await FNGetSavedQueriesAsync();
var removed = queries.RemoveAll(q => q.Id == id);
if (removed > 0)
{
await FNSaveQueriesFileAsync(queries);
return true;
}
return false;
}
/// <summary>
/// Validates that a KQL query is safe to execute. Checks for:
/// 1) Non-empty query text
/// 2) Absence of dangerous management commands (.set, .drop, .delete, etc.)
/// </summary>
/// <exception cref="ArgumentException">Thrown when validation fails.</exception>
private static void FNValidateQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentException("Query cannot be empty.");
var lower = query.ToLower();
foreach (var keyword in DangerousKeywords)
{
if (lower.Contains(keyword))
throw new ArgumentException($"Query contains disallowed keyword: {keyword}");
}
}
/// <summary>
/// Persists the list of saved queries to a JSON file on disk.
/// </summary>
private async Task FNSaveQueriesFileAsync(List<CLSSavedQuery> queries)
{
var json = JsonSerializer.Serialize(queries, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(_savedQueriesPath, json);
}
/// <summary>
/// Returns a default set of example KQL queries for new users.
/// These are shown when no saved-queries.json file exists on disk.
/// </summary>
private static List<CLSSavedQuery> FNGetDefaultQueries() => new()
{
new CLSSavedQuery
{
Id = "default-1",
Name = "Top Errors (Last 24h)",
Query = "AppExceptions\n| summarize count() by ExceptionType, OuterMessage\n| order by count_ desc\n| take 10",
Description = "Top 10 most frequent exceptions"
},
new CLSSavedQuery
{
Id = "default-2",
Name = "Slow Requests (>2s)",
Query = "AppRequests\n| where DurationMs > 2000\n| order by DurationMs desc\n| take 50\n| project TimeGenerated, Name, DurationMs, ResultCode, Success",
Description = "Requests taking longer than 2 seconds"
},
new CLSSavedQuery
{
Id = "default-3",
Name = "Failed Dependencies",
Query = "AppDependencies\n| where Success == false\n| summarize count() by Name, DependencyType, ResultCode\n| order by count_ desc\n| take 20",
Description = "Most failing external dependency calls"
},
new CLSSavedQuery
{
Id = "default-4",
Name = "Request Volume by Endpoint",
Query = "AppRequests\n| summarize requestCount = count(), avgDuration = avg(DurationMs) by Name\n| order by requestCount desc\n| take 20",
Description = "Request count and avg duration per endpoint"
},
new CLSSavedQuery
{
Id = "default-5",
Name = "Trace Logs with Errors",
Query = "AppTraces\n| where SeverityLevel >= 3\n| order by TimeGenerated desc\n| take 100\n| project TimeGenerated, Message, SeverityLevel, OperationId",
Description = "Recent error and critical trace logs"
}
};
/// <summary>
/// Parses a time range string into a <see cref="TimeSpan"/>. Defaults to 24 hours.
/// </summary>
private static TimeSpan FNParseTimeSpan(string timeSpan)
{
return timeSpan.ToLower() switch
{
"1h" => TimeSpan.FromHours(1),
"6h" => TimeSpan.FromHours(6),
"12h" => TimeSpan.FromHours(12),
"24h" or "1d" => TimeSpan.FromDays(1),
"7d" => TimeSpan.FromDays(7),
"30d" => TimeSpan.FromDays(30),
_ => TimeSpan.TryParse(timeSpan, out var ts) ? ts : TimeSpan.FromHours(24)
};
}
}
11. API Controllers
HealthController.cs
Tests Azure connectivity.
using Azure.Monitor.Query;
using AppInsights.Api.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace AppInsights.Api.Controllers;
/// <summary>
/// Health check controller that verifies connectivity to the Azure Log Analytics Workspace.
/// This endpoint is NOT protected by [Authorize] so it can be used by load balancers and monitoring tools.
/// </summary>
[ApiController]
[Route("api/health")]
public class CLSHealthController : ControllerBase
{
private readonly LogsQueryClient _logsClient;
private readonly string _workspaceId;
/// <summary>
/// Initializes a new instance of <see cref="CLSHealthController"/>.
/// </summary>
/// <param name="logsClient">Azure SDK client used to test workspace connectivity.</param>
/// <param name="settings">Azure configuration containing the Workspace ID.</param>
public CLSHealthController(LogsQueryClient logsClient, IOptions<CLSAzureSettings> settings)
{
_logsClient = logsClient;
_workspaceId = settings.Value.WorkspaceId;
}
/// <summary>
/// GET /api/health
/// Executes a minimal KQL query ("print 'health_check'") against the workspace to verify connectivity.
/// Returns 200 with "healthy" status if successful, or 503 with error details if the connection fails.
/// </summary>
/// <returns>JSON object with status, workspace ID, and timestamp.</returns>
[HttpGet]
public async Task<IActionResult> FNGet()
{
try
{
var response = await _logsClient.QueryWorkspaceAsync(
_workspaceId,
"print 'health_check'",
new QueryTimeRange(TimeSpan.FromMinutes(5)));
return Ok(new { status = "healthy", workspace = _workspaceId, timestamp = DateTimeOffset.UtcNow });
}
catch (Exception ex)
{
return StatusCode(503, new { status = "unhealthy", error = ex.Message, timestamp = DateTimeOffset.UtcNow });
}
}
}
LogsController.cs
GET /api/logs with severity and text search.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying application trace logs from Azure Application Insights.
/// Provides paginated access to the AppTraces table with optional severity and text search filters.
/// Requires Entra ID authentication via [Authorize] attribute.
/// </summary>
[Authorize]
[ApiController]
[Route("api/logs")]
public class CLSLogsController : ControllerBase
{
private readonly CLSILogsService _logsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSLogsController"/>.
/// </summary>
/// <param name="logsService">The logs service for querying Application Insights trace data.</param>
public CLSLogsController(CLSILogsService logsService)
{
_logsService = logsService;
}
/// <summary>
/// GET /api/logs?timeRange=24h&severity=3&search=error&pageSize=50&page=0
/// Retrieves paginated application trace logs with optional filtering by severity level and message text.
/// </summary>
/// <param name="timeRange">Time range filter (e.g., "1h", "24h", "7d"). Defaults to "24h".</param>
/// <param name="severity">Optional severity level filter (0=Verbose through 4=Critical).</param>
/// <param name="search">Optional text to search for within log messages.</param>
/// <param name="pageSize">Number of log entries per page. Defaults to 50.</param>
/// <param name="page">Zero-based page index. Defaults to 0.</param>
/// <returns>A JSON array of <see cref="Models.CLSLogEntry"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetLogs(
[FromQuery] string? timeRange = "24h",
[FromQuery] int? severity = null,
[FromQuery] string? search = null,
[FromQuery] int pageSize = 50,
[FromQuery] int page = 0)
{
var logs = await _logsService.FNGetLogsAsync(timeRange, severity, search, pageSize, page);
return Ok(logs);
}
}
TracesController.cs
GET /api/traces and GET /api/traces/{operationId}.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying request traces and distributed trace details
/// from Azure Application Insights. Provides paginated request listing and
/// detailed waterfall view for individual operations.
/// </summary>
[Authorize]
[ApiController]
[Route("api/traces")]
public class CLSTracesController : ControllerBase
{
private readonly CLSILogsService _logsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSTracesController"/>.
/// </summary>
/// <param name="logsService">The logs service for querying trace data.</param>
public CLSTracesController(CLSILogsService logsService)
{
_logsService = logsService;
}
/// <summary>
/// GET /api/traces?timeRange=24h&operationName=GET&minDurationMs=100&success=true&pageSize=50&page=0
/// Retrieves paginated request traces with optional filtering by operation name, duration, and success status.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <param name="operationName">Optional filter to search within operation names.</param>
/// <param name="minDurationMs">Optional minimum duration threshold in milliseconds.</param>
/// <param name="success">Optional filter for successful or failed requests.</param>
/// <param name="pageSize">Number of traces per page. Defaults to 50.</param>
/// <param name="page">Zero-based page index. Defaults to 0.</param>
/// <returns>A JSON array of <see cref="Models.CLSTraceItem"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetTraces(
[FromQuery] string? timeRange = "24h",
[FromQuery] string? operationName = null,
[FromQuery] double? minDurationMs = null,
[FromQuery] bool? success = null,
[FromQuery] int pageSize = 50,
[FromQuery] int page = 0)
{
var traces = await _logsService.FNGetTracesAsync(timeRange, operationName, minDurationMs, success, pageSize, page);
return Ok(traces);
}
/// <summary>
/// GET /api/traces/{operationId}
/// Retrieves all spans (requests, dependencies, traces, exceptions) belonging to a specific
/// distributed operation, identified by its Operation ID. Used for the trace waterfall view.
/// </summary>
/// <param name="operationId">The Operation ID linking all spans in the distributed trace.</param>
/// <returns>A JSON array of <see cref="Models.CLSTraceItem"/> objects ordered chronologically.</returns>
[HttpGet("{operationId}")]
public async Task<IActionResult> FNGetTraceDetail(string operationId)
{
var trace = await _logsService.FNGetTraceByOperationIdAsync(operationId);
return Ok(trace);
}
}
ExceptionsController.cs
GET /api/exceptions and groups.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying exception telemetry from Azure Application Insights.
/// Provides both individual exception listing and grouped exception summaries.
/// </summary>
[Authorize]
[ApiController]
[Route("api/exceptions")]
public class CLSExceptionsController : ControllerBase
{
private readonly CLSILogsService _logsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSExceptionsController"/>.
/// </summary>
/// <param name="logsService">The logs service for querying exception data.</param>
public CLSExceptionsController(CLSILogsService logsService)
{
_logsService = logsService;
}
/// <summary>
/// GET /api/exceptions?timeRange=24h&exceptionType=NullReference&pageSize=50&page=0
/// Retrieves paginated exception entries with optional filtering by exception type.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <param name="exceptionType">Optional filter to search within exception type names.</param>
/// <param name="pageSize">Number of exceptions per page. Defaults to 50.</param>
/// <param name="page">Zero-based page index. Defaults to 0.</param>
/// <returns>A JSON array of <see cref="Models.CLSExceptionEntry"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetExceptions(
[FromQuery] string? timeRange = "24h",
[FromQuery] string? exceptionType = null,
[FromQuery] int pageSize = 50,
[FromQuery] int page = 0)
{
var exceptions = await _logsService.FNGetExceptionsAsync(timeRange, exceptionType, pageSize, page);
return Ok(exceptions);
}
/// <summary>
/// GET /api/exceptions/groups?timeRange=24h
/// Retrieves exceptions aggregated (grouped) by type and message, with counts and last-seen timestamps.
/// Returns the top 50 groups sorted by frequency. Used for the "Grouped by Type" tab in the UI.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <returns>A JSON array of <see cref="Models.CLSExceptionGroup"/> objects.</returns>
[HttpGet("groups")]
public async Task<IActionResult> FNGetExceptionGroups([FromQuery] string? timeRange = "24h")
{
var groups = await _logsService.FNGetExceptionGroupsAsync(timeRange);
return Ok(groups);
}
}
MetricsController.cs
GET /api/metrics definitions and time-series.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying metric data from Azure Application Insights.
/// Provides metric definitions listing and time-series data for individual metrics
/// with configurable aggregation and time bucketing.
/// </summary>
[Authorize]
[ApiController]
[Route("api/metrics")]
public class CLSMetricsController : ControllerBase
{
private readonly CLSIMetricsService _metricsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSMetricsController"/>.
/// </summary>
/// <param name="metricsService">The metrics service for querying Application Insights metrics.</param>
public CLSMetricsController(CLSIMetricsService metricsService)
{
_metricsService = metricsService;
}
/// <summary>
/// GET /api/metrics
/// Retrieves all available metric names from the AppMetrics and AppPerformanceCounters tables.
/// Used to populate the metric picker dropdown in the frontend.
/// </summary>
/// <returns>A JSON array of <see cref="Models.CLSMetricDefinition"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetMetricDefinitions()
{
var definitions = await _metricsService.FNGetMetricDefinitionsAsync();
return Ok(definitions);
}
/// <summary>
/// GET /api/metrics/{metricName}?timeRange=24h&interval=5m&aggregation=avg
/// Retrieves time-series data for a specific metric with configurable aggregation and interval.
/// </summary>
/// <param name="metricName">The metric name to query (URL-encoded if it contains special characters).</param>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <param name="interval">Optional time bucket interval (e.g., "5m", "1h"). Auto-calculated if null.</param>
/// <param name="aggregation">Aggregation function: "avg", "sum", "min", "max", or "count". Defaults to "avg".</param>
/// <returns>A JSON <see cref="Models.CLSMetricResult"/> object with time-series data points.</returns>
[HttpGet("{metricName}")]
public async Task<IActionResult> FNGetMetric(
string metricName,
[FromQuery] string? timeRange = "24h",
[FromQuery] string? interval = null,
[FromQuery] string? aggregation = "avg")
{
var metric = await _metricsService.FNGetMetricAsync(metricName, timeRange, interval, aggregation);
return Ok(metric);
}
}
RequestsController.cs
GET /api/requests/summary for overview.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for the dashboard Overview page.
/// Provides an aggregated request summary combining multiple KQL queries into
/// a single response with totals, time-series, and categorical data.
/// </summary>
[Authorize]
[ApiController]
[Route("api/requests")]
public class CLSRequestsController : ControllerBase
{
private readonly CLSIMetricsService _metricsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSRequestsController"/>.
/// </summary>
/// <param name="metricsService">The metrics service for building request summaries.</param>
public CLSRequestsController(CLSIMetricsService metricsService)
{
_metricsService = metricsService;
}
/// <summary>
/// GET /api/requests/summary?timeRange=24h
/// Returns a comprehensive overview of request performance including:
/// total requests, average duration, failure rate, exception count,
/// request volume over time, duration trends, top failing endpoints,
/// and response code distribution.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <returns>A JSON <see cref="Models.CLSRequestSummary"/> object with all overview data.</returns>
[HttpGet("summary")]
public async Task<IActionResult> FNGetSummary([FromQuery] string? timeRange = "24h")
{
var summary = await _metricsService.FNGetRequestSummaryAsync(timeRange);
return Ok(summary);
}
}
DependenciesController.cs
GET /api/dependencies.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying external dependency call statistics
/// from Azure Application Insights. Dependencies include SQL databases,
/// HTTP APIs, Azure Storage, Redis, and other external services your app calls.
/// </summary>
[Authorize]
[ApiController]
[Route("api/dependencies")]
public class CLSDependenciesController : ControllerBase
{
private readonly CLSIMetricsService _metricsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSDependenciesController"/>.
/// </summary>
/// <param name="metricsService">The metrics service for querying dependency data.</param>
public CLSDependenciesController(CLSIMetricsService metricsService)
{
_metricsService = metricsService;
}
/// <summary>
/// GET /api/dependencies?timeRange=24h&type=SQL&pageSize=50&page=0
/// Retrieves paginated dependency call statistics aggregated by name and type.
/// Each entry includes average duration, failure rate, and total call count.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <param name="type">Optional dependency type filter (e.g., "SQL", "HTTP").</param>
/// <param name="pageSize">Number of entries per page. Defaults to 50.</param>
/// <param name="page">Zero-based page index. Defaults to 0.</param>
/// <returns>A JSON array of <see cref="Models.CLSDependencyEntry"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetDependencies(
[FromQuery] string? timeRange = "24h",
[FromQuery] string? type = null,
[FromQuery] int pageSize = 50,
[FromQuery] int page = 0)
{
var deps = await _metricsService.FNGetDependenciesAsync(timeRange, type, pageSize, page);
return Ok(deps);
}
}
AvailabilityController.cs
GET /api/availability.
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for querying availability (web test) results from Azure Application Insights.
/// Availability tests are periodic probes that check your application's health from multiple
/// geographic locations around the world.
/// </summary>
[Authorize]
[ApiController]
[Route("api/availability")]
public class CLSAvailabilityController : ControllerBase
{
private readonly CLSIMetricsService _metricsService;
/// <summary>
/// Initializes a new instance of <see cref="CLSAvailabilityController"/>.
/// </summary>
/// <param name="metricsService">The metrics service for querying availability data.</param>
public CLSAvailabilityController(CLSIMetricsService metricsService)
{
_metricsService = metricsService;
}
/// <summary>
/// GET /api/availability?timeRange=24h&testName=homepage&pageSize=50&page=0
/// Retrieves paginated availability test results with optional filtering by test name.
/// Each result includes timestamp, test name, location, pass/fail status, and duration.
/// </summary>
/// <param name="timeRange">Time range filter. Defaults to "24h".</param>
/// <param name="testName">Optional filter to search within test names.</param>
/// <param name="pageSize">Number of results per page. Defaults to 50.</param>
/// <param name="page">Zero-based page index. Defaults to 0.</param>
/// <returns>A JSON array of <see cref="Models.CLSAvailabilityResult"/> objects.</returns>
[HttpGet]
public async Task<IActionResult> FNGetAvailability(
[FromQuery] string? timeRange = "24h",
[FromQuery] string? testName = null,
[FromQuery] int pageSize = 50,
[FromQuery] int page = 0)
{
var results = await _metricsService.FNGetAvailabilityAsync(timeRange, testName, pageSize, page);
return Ok(results);
}
}
KqlController.cs
POST /api/kql plus saved queries CRUD.
using AppInsights.Api.Models;
using AppInsights.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace AppInsights.Api.Controllers;
/// <summary>
/// REST API controller for executing raw KQL (Kusto Query Language) queries
/// and managing saved queries. Provides an interactive query editor experience
/// backed by the Log Analytics Workspace.
/// </summary>
[Authorize]
[ApiController]
[Route("api/kql")]
public class CLSKqlController : ControllerBase
{
private readonly CLSIKqlService _kqlService;
/// <summary>
/// Initializes a new instance of <see cref="CLSKqlController"/>.
/// </summary>
/// <param name="kqlService">The KQL service for query execution and saved query management.</param>
public CLSKqlController(CLSIKqlService kqlService)
{
_kqlService = kqlService;
}
/// <summary>
/// POST /api/kql
/// Executes a raw KQL query against the Log Analytics Workspace.
/// The query is validated for safety (no write/delete commands) before execution.
/// Returns tabular results with column definitions and row data.
/// </summary>
/// <param name="request">The query request containing the KQL string and optional time span.</param>
/// <returns>A <see cref="CLSKqlResult"/> on success, or a ProblemDetails 400 response for invalid queries.</returns>
[HttpPost]
public async Task<IActionResult> FNExecuteQuery([FromBody] CLSKqlQueryRequest request)
{
try
{
var result = await _kqlService.FNExecuteQueryAsync(request.Query, request.TimeSpan);
return Ok(result);
}
catch (ArgumentException ex)
{
return BadRequest(new ProblemDetails
{
Title = "Invalid Query",
Detail = ex.Message,
Status = 400
});
}
}
/// <summary>
/// GET /api/kql/saved
/// Retrieves all saved KQL queries. Returns default example queries if none have been saved yet.
/// </summary>
/// <returns>A JSON array of <see cref="CLSSavedQuery"/> objects.</returns>
[HttpGet("saved")]
public async Task<IActionResult> FNGetSavedQueries()
{
var queries = await _kqlService.FNGetSavedQueriesAsync();
return Ok(queries);
}
/// <summary>
/// POST /api/kql/saved
/// Saves a new KQL query with a name and optional description.
/// Returns the saved query with its assigned ID and creation timestamp.
/// </summary>
/// <param name="query">The query to save (name, query text, and optional description).</param>
/// <returns>201 Created with the saved <see cref="CLSSavedQuery"/> object.</returns>
[HttpPost("saved")]
public async Task<IActionResult> SaveQuery([FromBody] CLSSavedQuery query)
{
var saved = await _kqlService.FNSaveQueryAsync(query);
return CreatedAtAction(nameof(FNGetSavedQueries), saved);
}
/// <summary>
/// DELETE /api/kql/saved/{id}
/// Deletes a saved KQL query by its unique identifier.
/// </summary>
/// <param name="id">The unique ID of the query to delete.</param>
/// <returns>204 No Content if deleted, or 404 Not Found if the query doesn't exist.</returns>
[HttpDelete("saved/{id}")]
public async Task<IActionResult> FNDeleteSavedQuery(string id)
{
var deleted = await _kqlService.FNDeleteSavedQueryAsync(id);
return deleted ? NoContent() : NotFound();
}
}
12. React: TypeScript Types
/**
* TypeScript interfaces matching the ASP.NET Core API response DTOs.
* Each interface maps to a corresponding C# model in AppInsights.Api/Models/.
* These types ensure type-safe communication between the React frontend and the .NET backend.
*/
/** Represents a single application trace log entry from the AppTraces table. */
export interface CLSLogEntry {
/** When the log message was emitted (ISO 8601 string). */
timestamp: string;
/** The log message text content. */
message: string;
/** Numeric severity: 0=Verbose, 1=Information, 2=Warning, 3=Error, 4=Critical. */
severityLevel: number;
/** Human-readable severity name (e.g., "Error", "Warning"). */
severityName: string;
/** Correlation ID linking this log to other telemetry in the same distributed operation. */
operationId?: string;
/** The name of the operation (e.g., "GET /api/users"). */
operationName?: string;
/** The cloud role/service name that emitted the log. */
appRoleName?: string;
/** Additional key-value properties as a JSON string. */
customDimensions?: string;
}
/** Represents a single span in a distributed trace (request or dependency call). */
export interface CLSTraceItem {
/** When the span started executing. */
timestamp: string;
/** Telemetry type: "AppRequests", "AppDependencies", "AppTraces", or "AppExceptions". */
itemType: string;
/** The request endpoint or dependency target name. */
name: string;
/** Execution time in milliseconds. */
durationMs: number;
/** Whether the operation completed successfully. */
success: boolean;
/** HTTP status code or dependency result code. */
resultCode?: string;
/** Shared ID linking all spans in the same distributed trace. */
operationId?: string;
/** ID of the parent span in the trace hierarchy. */
parentId?: string;
/** Unique identifier for this specific span. */
id?: string;
/** Target of a dependency call (e.g., SQL server hostname). */
target?: string;
/** Type of dependency (e.g., "SQL", "HTTP"). */
dependencyType?: string;
}
/** Represents a single exception captured by Application Insights. */
export interface CLSExceptionEntry {
/** When the exception was thrown. */
timestamp: string;
/** Fully qualified exception type name (e.g., "System.NullReferenceException"). */
exceptionType: string;
/** The exception message text. */
message: string;
/** The outermost exception message (may differ in wrapped exceptions). */
outerMessage?: string;
/** Full stack trace for debugging. */
stackTrace?: string;
/** Correlation ID for the operation that caused the exception. */
operationId?: string;
/** The service/role where the exception was thrown. */
appRoleName?: string;
/** Severity level, typically 3 (Error) or 4 (Critical). */
severityLevel: number;
}
/** Exceptions aggregated by type and message with occurrence counts. */
export interface CLSExceptionGroup {
/** The shared exception type for all exceptions in this group. */
exceptionType: string;
/** The common outer message. */
message: string;
/** Total number of occurrences. */
count: number;
/** When the most recent occurrence was recorded. */
lastSeen: string;
}
/** Describes an available metric in Application Insights. */
export interface CLSMetricDefinition {
/** Internal metric name used in KQL queries. */
name: string;
/** Human-friendly display name. */
displayName?: string;
/** Unit of measurement (e.g., "ms", "bytes"). */
unit?: string;
}
/** Time-series result of a metric query for chart rendering. */
export interface CLSMetricResult {
/** The queried metric name. */
name: string;
/** Unit of measurement. */
unit?: string;
/** Aggregation function applied: "avg", "sum", "min", "max", or "count". */
aggregation: string;
/** Time-series data points. */
timeseries: CLSMetricDataPoint[];
}
/** A single data point in a metric time series. */
export interface CLSMetricDataPoint {
/** Start of the time bucket. */
timestamp: string;
/** Aggregated value for this bucket. Null if no data. */
value?: number;
}
/** Aggregated request statistics for the dashboard Overview page. */
export interface CLSRequestSummary {
/** Total HTTP requests in the time window. */
totalRequests: number;
/** Average response time in milliseconds. */
avgDurationMs: number;
/** Percentage of failed requests (0-100). */
failureRate: number;
/** Total exceptions thrown. */
totalExceptions: number;
/** Request volume over time for line chart. */
requestsOverTime: CLSTimeSeriesPoint[];
/** Average duration over time for line chart. */
avgDurationOverTime: CLSTimeSeriesPoint[];
/** Top failing endpoints for bar chart. */
topFailingEndpoints: CLSNameValuePair[];
/** Response code distribution for pie chart. */
responseCodeDistribution: CLSNameValuePair[];
}
/** A single point in a time series (timestamp + value). */
export interface CLSTimeSeriesPoint {
/** Start of the time bucket. */
timestamp: string;
/** Aggregated value. */
value: number;
}
/** Simple name-value pair for categorical data (charts, lists). */
export interface CLSNameValuePair {
/** Category name (e.g., endpoint name, response code group). */
name: string;
/** Numeric value (e.g., count, rate). */
value: number;
}
/** External dependency call statistics aggregated by name and type. */
export interface CLSDependencyEntry {
/** Dependency target name. */
name: string;
/** Dependency type (e.g., "SQL", "HTTP", "Redis"). */
type: string;
/** Average execution time in milliseconds. */
avgDurationMs: number;
/** Failure rate as a percentage (0-100). */
failureRate: number;
/** Total number of calls. */
callCount: number;
}
/** A single availability (web test) result. */
export interface CLSAvailabilityResult {
/** When the test was executed. */
timestamp: string;
/** Name of the availability test. */
testName: string;
/** Geographic location of the test probe. */
location: string;
/** Whether the test passed. */
success: boolean;
/** Test completion time in milliseconds. */
durationMs: number;
/** Diagnostic message (typically for failures). */
message?: string;
}
/** Request payload for executing a raw KQL query. */
export interface CLSKqlQueryRequest {
/** The KQL query string. */
query: string;
/** Optional time range (e.g., "1h", "24h", "7d"). */
timeSpan?: string;
}
/** Tabular result of a KQL query execution. */
export interface CLSKqlResult {
/** Column definitions (name and data type). */
columns: CLSKqlColumn[];
/** Row data as arrays of values matching columns by index. */
rows: (string | number | boolean | null)[][];
/** Total number of rows returned. */
rowCount: number;
}
/** A single column in a KQL result table. */
export interface CLSKqlColumn {
/** Column name. */
name: string;
/** Data type (e.g., "string", "datetime", "long"). */
type: string;
}
/** A saved KQL query with name and description for reuse. */
export interface CLSSavedQuery {
/** Unique identifier. */
id: string;
/** User-assigned query name. */
name: string;
/** The KQL query text. */
query: string;
/** Optional description of what the query does. */
description?: string;
/** When the query was saved (ISO 8601). */
createdAt: string;
}
13. React: Authentication
msalConfig.ts
/**
* MSAL (Microsoft Authentication Library) Configuration
*
* Defines the configuration for authenticating users with Microsoft Entra ID (Azure AD).
* Uses MSAL.js v3 browser library for the OAuth 2.0 Authorization Code flow with PKCE.
* Configuration values are read from Vite environment variables (VITE_AZURE_*).
*/
import type { Configuration } from '@azure/msal-browser';
import { LogLevel } from '@azure/msal-browser';
/**
* MSAL configuration object for the PublicClientApplication.
* - clientId: The SPA's App Registration Client ID in Entra ID
* - authority: The Entra ID tenant-specific login endpoint
* - redirectUri: Where Entra ID redirects after authentication (current origin)
* - cacheLocation: Uses localStorage to persist tokens across browser tabs
*/
export const msalConfig: Configuration = {
auth: {
clientId: import.meta.env.VITE_AZURE_CLIENT_ID || '',
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AZURE_TENANT_ID || 'common'}`,
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
},
cache: {
cacheLocation: 'localStorage',
},
system: {
loggerOptions: {
logLevel: LogLevel.Warning,
loggerCallback: (level, message, containsPii) => {
if (containsPii) return;
switch (level) {
case LogLevel.Error:
console.error(message);
break;
case LogLevel.Warning:
console.warn(message);
break;
}
},
},
},
};
/**
* Scopes requested when the React SPA calls the backend API.
* Must match the scope exposed by the API's App Registration in Entra ID.
*/
export const apiScopes = {
scopes: [import.meta.env.VITE_API_SCOPE || 'api://appinsights-dashboard/access_as_user'],
};
/**
* Scopes requested during the initial login.
* Includes OpenID Connect scopes (openid, profile, email) plus the API access scope.
*/
export const loginRequest = {
scopes: ['openid', 'profile', 'email', ...apiScopes.scopes],
};
AuthProvider.tsx
/**
* MSAL Authentication Provider Component
*
* Wraps the entire application in the MSAL React context, enabling authentication
* with Microsoft Entra ID throughout the component tree. This component:
* 1. Initializes the MSAL PublicClientApplication (required for MSAL v3+)
* 2. Handles redirect responses after returning from the Entra ID login page
* 3. Sets the first available account as the active account
* 4. Listens for future login events to update the active account
* 5. Shows a loading spinner until MSAL initialization is complete
*/
import { useState, useEffect } from 'react';
import { MsalProvider } from '@azure/msal-react';
import { PublicClientApplication, EventType } from '@azure/msal-browser';
import { Spin } from 'antd';
import type { ReactNode } from 'react';
import { msalConfig } from './msalConfig';
/**
* Singleton MSAL PublicClientApplication instance.
* Exported so the API client interceptor can acquire tokens silently.
*/
export const msalInstance = new PublicClientApplication(msalConfig);
/** Props for the CLSAuthProviderComponent component. */
interface CLSAuthProviderComponentProps {
/** Child components that will have access to the MSAL authentication context. */
children: ReactNode;
}
/**
* Authentication provider that initializes MSAL and wraps children in MsalProvider.
* Displays a centered spinner while MSAL is initializing and processing any pending redirects.
*/
export default function CLSAuthProviderComponent({ children }: CLSAuthProviderComponentProps) {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const init = async () => {
// MSAL v3+ requires explicit initialization before any other operations
await msalInstance.initialize();
// Process the redirect response if returning from Entra ID login
await msalInstance.handleRedirectPromise();
// Set the first account as active if the user is already logged in
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
// Listen for future login events to update the active account
msalInstance.addEventCallback((event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload as { account: any };
msalInstance.setActiveAccount(payload.account);
}
});
setIsReady(true);
};
init();
}, []);
if (!isReady) {
return <Spin size="large" style={{ display: 'block', margin: '200px auto' }} />;
}
return <MsalProvider instance={msalInstance}>{children}</MsalProvider>;
}
AuthGuard.tsx
/**
* Authentication Guard Component
*
* Protects the dashboard by rendering children only when the user is authenticated.
* If authentication is in progress, shows a loading spinner.
* If the user is not authenticated, shows a sign-in screen with a
* "Sign in with Microsoft" button that triggers an Entra ID redirect login.
*/
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { Button, Result, Spin } from 'antd';
import { LoginOutlined } from '@ant-design/icons';
import type { ReactNode } from 'react';
import { loginRequest } from './msalConfig';
/** Props for the CLSAuthGuardComponent component. */
interface CLSAuthGuardComponentProps {
/** Child components to render when the user is authenticated. */
children: ReactNode;
}
/**
* Renders children only if the user is authenticated via Entra ID.
* Shows a spinner during authentication interactions, or a sign-in page if not authenticated.
*/
export default function CLSAuthGuardComponent({ children }: CLSAuthGuardComponentProps) {
const isAuthenticated = useIsAuthenticated();
const { instance, inProgress } = useMsal();
// Show spinner while MSAL is processing a login/logout/token operation
if (inProgress !== InteractionStatus.None) {
return <Spin size="large" style={{ display: 'block', margin: '200px auto' }} />;
}
// Show sign-in screen for unauthenticated users
if (!isAuthenticated) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<Result
title="Azure Application Insights Dashboard"
subTitle="Sign in with your Microsoft Entra ID account to access the dashboard."
extra={
<Button
type="primary"
size="large"
icon={<LoginOutlined />}
onClick={() => instance.loginRedirect(loginRequest)}
>
Sign in with Microsoft
</Button>
}
/>
</div>
);
}
// User is authenticated — render the protected content
return <>{children}</>;
}
index.ts
export { default as CLSAuthProviderComponent, msalInstance } from './AuthProvider';
export { default as CLSAuthGuardComponent } from './AuthGuard';
export { msalConfig, apiScopes, loginRequest } from './msalConfig';
14. React: API Client & Services
client.ts
/**
* Axios API Client with MSAL Token Interceptor
*
* Creates a pre-configured Axios instance that automatically attaches
* Microsoft Entra ID (Azure AD) bearer tokens to every outgoing request.
* The interceptor silently acquires tokens from MSAL cache, and falls back
* to interactive login redirect if the token is expired or consent is needed.
*/
import axios from 'axios';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
import { msalInstance } from '../auth/AuthProvider';
import { apiScopes } from '../auth/msalConfig';
/**
* Pre-configured Axios instance pointing to the ASP.NET Core backend API.
* Base URL is read from the VITE_API_URL environment variable, defaulting to localhost:5027.
*/
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5027',
headers: { 'Content-Type': 'application/json' },
});
/**
* Request interceptor that attaches a Bearer token to every API call.
* - Acquires the token silently from the MSAL cache (no user interaction).
* - If the token is expired and requires re-authentication, triggers an
* interactive redirect to the Entra ID login page.
* - If no active account exists (user not logged in), the request proceeds
* without a token and the API will return a 401 response.
*/
apiClient.interceptors.request.use(async (config) => {
const account = msalInstance.getActiveAccount();
if (!account) {
return config;
}
try {
const response = await msalInstance.acquireTokenSilent({
...apiScopes,
account,
});
config.headers.Authorization = `Bearer ${response.accessToken}`;
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
// Token expired or consent needed — trigger interactive login
await msalInstance.acquireTokenRedirect(apiScopes);
}
// For other errors, let the request proceed without token (will get 401)
}
return config;
});
export default apiClient;
logsApi.ts
/**
* Logs API Service
* Provides functions for fetching application trace logs from the backend API.
* Calls GET /api/logs with optional filtering and pagination parameters.
*/
import apiClient from './client';
import type { CLSLogEntry } from '../types';
/**
* Fetches paginated application trace logs from the AppTraces table.
* @param params.timeRange - Time range filter (e.g., "1h", "24h", "7d")
* @param params.severity - Optional severity level (0-4) to filter by
* @param params.search - Optional text to search within log messages
* @param params.pageSize - Number of entries per page (default: 50)
* @param params.page - Zero-based page index (default: 0)
* @returns Array of CLSLogEntry objects for the requested page
*/
export const FNfetchLogs = async (params: {
timeRange?: string;
severity?: number;
search?: string;
pageSize?: number;
page?: number;
}): Promise<CLSLogEntry[]> => {
const { data } = await apiClient.get('/api/logs', { params });
return data;
};
tracesApi.ts
/**
* Traces API Service
* Provides functions for fetching request traces and distributed trace details.
* Calls GET /api/traces and GET /api/traces/{operationId}.
*/
import apiClient from './client';
import type { CLSTraceItem } from '../types';
/**
* Fetches paginated request traces from the AppRequests table.
* @param params.timeRange - Time range filter (e.g., "1h", "24h", "7d")
* @param params.operationName - Optional filter to search within operation names
* @param params.minDurationMs - Optional minimum duration threshold in milliseconds
* @param params.success - Optional filter for successful (true) or failed (false) requests
* @param params.pageSize - Number of traces per page
* @param params.page - Zero-based page index
* @returns Array of CLSTraceItem objects for the requested page
*/
export const FNfetchTraces = async (params: {
timeRange?: string;
operationName?: string;
minDurationMs?: number;
success?: boolean;
pageSize?: number;
page?: number;
}): Promise<CLSTraceItem[]> => {
const { data } = await apiClient.get('/api/traces', { params });
return data;
};
/**
* Fetches all spans in a distributed trace by Operation ID.
* Returns requests, dependencies, traces, and exceptions linked by the same operation.
* Used for the trace waterfall visualization in the UI.
* @param operationId - The shared Operation ID for the distributed trace
* @returns Array of CLSTraceItem objects ordered chronologically
*/
export const FNfetchTraceDetail = async (operationId: string): Promise<CLSTraceItem[]> => {
const { data } = await apiClient.get(`/api/traces/${operationId}`);
return data;
};
exceptionsApi.ts
/**
* Exceptions API Service
* Provides functions for fetching individual exceptions and grouped exception summaries.
* Calls GET /api/exceptions and GET /api/exceptions/groups.
*/
import apiClient from './client';
import type { CLSExceptionEntry, CLSExceptionGroup } from '../types';
/**
* Fetches paginated exception entries from the AppExceptions table.
* @param params.timeRange - Time range filter (e.g., "1h", "24h", "7d")
* @param params.exceptionType - Optional filter to search within exception type names
* @param params.pageSize - Number of exceptions per page
* @param params.page - Zero-based page index
* @returns Array of CLSExceptionEntry objects for the requested page
*/
export const FNfetchExceptions = async (params: {
timeRange?: string;
exceptionType?: string;
pageSize?: number;
page?: number;
}): Promise<CLSExceptionEntry[]> => {
const { data } = await apiClient.get('/api/exceptions', { params });
return data;
};
/**
* Fetches exceptions grouped by type and message with occurrence counts.
* Returns the top 50 exception groups sorted by frequency.
* @param timeRange - Optional time range filter (e.g., "24h", "7d")
* @returns Array of CLSExceptionGroup objects sorted by count descending
*/
export const FNfetchExceptionGroups = async (timeRange?: string): Promise<CLSExceptionGroup[]> => {
const { data } = await apiClient.get('/api/exceptions/groups', { params: { timeRange } });
return data;
};
metricsApi.ts
/**
* Metrics API Service
* Provides functions for fetching request summaries, metric definitions,
* metric time-series data, dependency statistics, and availability test results.
*/
import apiClient from './client';
import type { CLSMetricDefinition, CLSMetricResult, CLSRequestSummary, CLSDependencyEntry, CLSAvailabilityResult } from '../types';
/**
* Fetches the aggregated request summary for the dashboard Overview page.
* Includes totals, time-series data, top failures, and response code distribution.
* @param timeRange - Optional time range filter (e.g., "24h", "7d")
* @returns A CLSRequestSummary object with all overview statistics and chart data
*/
export const FNfetchRequestSummary = async (timeRange?: string): Promise<CLSRequestSummary> => {
const { data } = await apiClient.get('/api/requests/summary', { params: { timeRange } });
return data;
};
/**
* Fetches all available metric names from the AppMetrics and AppPerformanceCounters tables.
* Used to populate the metric picker dropdown in the Metrics page.
* @returns Array of CLSMetricDefinition objects sorted alphabetically
*/
export const FNfetchMetricDefinitions = async (): Promise<CLSMetricDefinition[]> => {
const { data } = await apiClient.get('/api/metrics');
return data;
};
/**
* Fetches time-series data for a specific metric with configurable aggregation and interval.
* @param params.metricName - The metric to query (URL-encoded automatically)
* @param params.timeRange - Time range filter (e.g., "1h", "24h")
* @param params.interval - Optional time bucket interval (e.g., "5m", "1h")
* @param params.aggregation - Aggregation function: "avg", "sum", "min", "max", or "count"
* @returns A CLSMetricResult object with time-series data points for chart rendering
*/
export const FNfetchMetric = async (params: {
metricName: string;
timeRange?: string;
interval?: string;
aggregation?: string;
}): Promise<CLSMetricResult> => {
const { metricName, ...rest } = params;
const { data } = await apiClient.get(`/api/metrics/${encodeURIComponent(metricName)}`, { params: rest });
return data;
};
/**
* Fetches paginated dependency call statistics aggregated by name and type.
* @param params.timeRange - Time range filter
* @param params.type - Optional dependency type filter (e.g., "SQL", "HTTP")
* @param params.pageSize - Number of entries per page
* @param params.page - Zero-based page index
* @returns Array of CLSDependencyEntry objects sorted by call count descending
*/
export const FNfetchDependencies = async (params: {
timeRange?: string;
type?: string;
pageSize?: number;
page?: number;
}): Promise<CLSDependencyEntry[]> => {
const { data } = await apiClient.get('/api/dependencies', { params });
return data;
};
/**
* Fetches paginated availability (web test) results.
* @param params.timeRange - Time range filter
* @param params.testName - Optional filter to search within test names
* @param params.pageSize - Number of results per page
* @param params.page - Zero-based page index
* @returns Array of CLSAvailabilityResult objects ordered by time descending
*/
export const FNfetchAvailability = async (params: {
timeRange?: string;
testName?: string;
pageSize?: number;
page?: number;
}): Promise<CLSAvailabilityResult[]> => {
const { data } = await apiClient.get('/api/availability', { params });
return data;
};
kqlApi.ts
/**
* KQL API Service
* Provides functions for executing raw KQL queries and managing saved queries.
* Calls POST /api/kql, GET/POST/DELETE /api/kql/saved.
*/
import apiClient from './client';
import type { CLSKqlResult, CLSSavedQuery } from '../types';
/**
* Executes a raw KQL query against the Log Analytics Workspace.
* The backend validates the query for safety before execution.
* @param query - The KQL query string to execute
* @param timeSpan - Optional time range (e.g., "1h", "24h", "7d")
* @returns A CLSKqlResult with column definitions and row data
* @throws API error with ProblemDetails if the query is invalid or contains dangerous keywords
*/
export const FNexecuteKql = async (query: string, timeSpan?: string): Promise<CLSKqlResult> => {
const { data } = await apiClient.post('/api/kql', { query, timeSpan });
return data;
};
/**
* Fetches all saved KQL queries from the server.
* Returns default example queries if none have been saved yet.
* @returns Array of CLSSavedQuery objects
*/
export const FNfetchSavedQueries = async (): Promise<CLSSavedQuery[]> => {
const { data } = await apiClient.get('/api/kql/saved');
return data;
};
/**
* Saves a new KQL query with a name and optional description.
* The server assigns a unique ID and creation timestamp.
* @param query - Object with name, query text, and optional description
* @returns The saved query with its assigned ID and timestamp
*/
export const FNsaveQuery = async (query: { name: string; query: string; description?: string }): Promise<CLSSavedQuery> => {
const { data } = await apiClient.post('/api/kql/saved', query);
return data;
};
/**
* Deletes a saved KQL query by its unique identifier.
* @param id - The ID of the saved query to delete
*/
export const FNdeleteSavedQuery = async (id: string): Promise<void> => {
await apiClient.delete(`/api/kql/saved/${id}`);
};
15. React: Custom Hooks
/**
* Time Range Context and Hooks
*
* Provides a global time range filter that is shared across all dashboard pages.
* The time range (e.g., "24h", "7d") is stored in React Context and accessible
* from any component via the FNuseTimeRange() hook. When the user changes the time
* range in the header dropdown, all pages automatically refetch data.
*/
import { useState, createContext, useContext } from 'react';
/** Shape of the time range context value. */
interface CLSTimeRangeContextType {
/** Current time range value (e.g., "30m", "1h", "24h", "7d"). */
timeRange: string;
/** Updates the global time range, triggering refetches across all pages. */
setTimeRange: (tr: string) => void;
}
/**
* React Context that holds the global time range state.
* Default value is "24h" (last 24 hours).
*/
export const TimeRangeContext = createContext<CLSTimeRangeContextType>({
timeRange: '24h',
setTimeRange: () => {},
});
/**
* Hook to read and update the global time range from any component.
* Must be used within a TimeRangeContext.Provider.
*/
export const FNuseTimeRange = () => useContext(TimeRangeContext);
/**
* Hook that creates the time range state for the context provider.
* Used in App.tsx to initialize the context value.
* @returns Object with timeRange value and setTimeRange setter.
*/
export const FNuseTimeRangeState = () => {
const [timeRange, setTimeRange] = useState('24h');
return { timeRange, setTimeRange };
};
/**
* Custom React Hooks for Application Insights API Queries
*
* Each hook wraps a TanStack Query useQuery() call that fetches data from the backend API.
* Features provided by TanStack Query:
* - Automatic caching with configurable stale time
* - Background refetching when params change
* - Loading and error state management
* - Conditional fetching via the `enabled` option
* - Automatic polling via `refetchInterval`
*
* Query keys include all parameters, so changing any filter (time range, severity, page)
* automatically triggers a new fetch with proper cache management.
*/
import { useQuery } from '@tanstack/react-query';
import { FNfetchLogs } from '../api/logsApi';
import { FNfetchTraces, FNfetchTraceDetail } from '../api/tracesApi';
import { FNfetchExceptions, FNfetchExceptionGroups } from '../api/exceptionsApi';
import { FNfetchRequestSummary, FNfetchMetricDefinitions, FNfetchMetric, FNfetchDependencies, FNfetchAvailability } from '../api/metricsApi';
import { FNfetchSavedQueries } from '../api/kqlApi';
/** Fetches paginated logs from AppTraces. Refetches when any param changes. */
export const FNuseLogs = (params: { timeRange?: string; severity?: number; search?: string; pageSize?: number; page?: number }) =>
useQuery({ queryKey: ['logs', params], queryFn: () => FNfetchLogs(params) });
/** Fetches paginated request traces from AppRequests. Refetches when any param changes. */
export const FNuseTraces = (params: { timeRange?: string; operationName?: string; minDurationMs?: number; success?: boolean; pageSize?: number; page?: number }) =>
useQuery({ queryKey: ['traces', params], queryFn: () => FNfetchTraces(params) });
/** Fetches all spans for a distributed trace. Only executes when operationId is non-empty. */
export const FNuseTraceDetail = (operationId: string) =>
useQuery({ queryKey: ['trace', operationId], queryFn: () => FNfetchTraceDetail(operationId), enabled: !!operationId });
/** Fetches paginated exceptions from AppExceptions. Refetches when any param changes. */
export const FNuseExceptions = (params: { timeRange?: string; exceptionType?: string; pageSize?: number; page?: number }) =>
useQuery({ queryKey: ['exceptions', params], queryFn: () => FNfetchExceptions(params) });
/** Fetches exception groups (aggregated by type). Refetches when timeRange changes. */
export const FNuseExceptionGroups = (timeRange?: string) =>
useQuery({ queryKey: ['exceptionGroups', timeRange], queryFn: () => FNfetchExceptionGroups(timeRange) });
/** Fetches the overview request summary. Auto-refreshes every 30 seconds for near-real-time data. */
export const FNuseRequestSummary = (timeRange?: string) =>
useQuery({ queryKey: ['requestSummary', timeRange], queryFn: () => FNfetchRequestSummary(timeRange), refetchInterval: 30000 });
/** Fetches all available metric names for the metric picker dropdown. */
export const FNuseMetricDefinitions = () =>
useQuery({ queryKey: ['metricDefinitions'], queryFn: FNfetchMetricDefinitions });
/** Fetches time-series data for a specific metric. Only executes when metricName is non-empty. */
export const FNuseMetric = (params: { metricName: string; timeRange?: string; interval?: string; aggregation?: string }) =>
useQuery({ queryKey: ['metric', params], queryFn: () => FNfetchMetric(params), enabled: !!params.metricName });
/** Fetches paginated dependency statistics from AppDependencies. */
export const FNuseDependencies = (params: { timeRange?: string; type?: string; pageSize?: number; page?: number }) =>
useQuery({ queryKey: ['dependencies', params], queryFn: () => FNfetchDependencies(params) });
/** Fetches paginated availability test results from AppAvailabilityResults. */
export const FNuseAvailability = (params: { timeRange?: string; testName?: string; pageSize?: number; page?: number }) =>
useQuery({ queryKey: ['availability', params], queryFn: () => FNfetchAvailability(params) });
/** Fetches all saved KQL queries for the query editor sidebar. */
export const FNuseSavedQueries = () =>
useQuery({ queryKey: ['savedQueries'], queryFn: FNfetchSavedQueries });
16. React: Components
SeverityTag.tsx
/**
* CLSSeverityTagComponent Component and Utilities
*
* Renders a color-coded Ant Design Tag based on Application Insights severity levels.
* Also exports helper functions and options for use in filters and other components.
*
* Severity Levels:
* 0 = Verbose (gray)
* 1 = Information (blue)
* 2 = Warning (orange)
* 3 = Error (red)
* 4 = Critical (magenta)
*/
import { Tag } from 'antd';
/** Maps severity level numbers to display labels and Ant Design tag colors. */
const severityConfig: Record<number, { label: string; color: string }> = {
0: { label: 'Verbose', color: 'default' },
1: { label: 'Information', color: 'blue' },
2: { label: 'Warning', color: 'orange' },
3: { label: 'Error', color: 'red' },
4: { label: 'Critical', color: 'magenta' },
};
/** Props for the CLSSeverityTagComponent component. */
interface CLSSeverityTagComponentProps {
/** The numeric severity level (0-4). */
level: number;
/** Whether to show the text label (true) or just the number (false). Defaults to true. */
showLabel?: boolean;
}
/**
* Renders a color-coded tag displaying the severity level.
* Used in log tables and detail views to visually indicate log importance.
*/
export default function CLSSeverityTagComponent({ level, showLabel = true }: CLSSeverityTagComponentProps) {
const config = severityConfig[level] || { label: 'Unknown', color: 'default' };
return <Tag color={config.color}>{showLabel ? config.label : level}</Tag>;
}
/**
* Returns the human-readable label for a severity level.
* @param level - Numeric severity (0-4)
* @returns Label string (e.g., "Error", "Warning")
*/
export function FNgetSeverityLabel(level: number): string {
return severityConfig[level]?.label || 'Unknown';
}
/**
* Returns the Ant Design tag color for a severity level.
* @param level - Numeric severity (0-4)
* @returns Color string for the Ant Design Tag component
*/
export function FNgetSeverityColor(level: number): string {
return severityConfig[level]?.color || 'default';
}
/** Array of severity options for use in Select/Filter components. */
export const severityOptions = Object.entries(severityConfig).map(([value, { label }]) => ({
value: Number(value),
label,
}));
StatCard.tsx
/**
* CLSStatCardComponent Component
*
* Renders a single statistic value inside an Ant Design Card.
* Used on the Overview page to display key metrics like total requests,
* average response time, failure rate, and exception count.
*/
import { Card, Statistic } from 'antd';
import type { ReactNode } from 'react';
/** Props for the CLSStatCardComponent component. */
interface CLSStatCardComponentProps {
/** The label displayed above the value (e.g., "Total Requests"). */
title: string;
/** The numeric or string value to display. */
value: number | string;
/** Optional unit suffix displayed after the value (e.g., "ms", "%"). */
suffix?: string;
/** Optional icon or element displayed before the value. */
prefix?: ReactNode;
/** Optional CSS styles for the value text (e.g., color for red/green indicators). */
valueStyle?: React.CSSProperties;
/** Optional number of decimal places for numeric values. */
precision?: number;
}
/**
* Displays a single statistic in a card format.
* Wraps Ant Design's Statistic component inside a Card for consistent dashboard styling.
*/
export default function CLSStatCardComponent({ title, value, suffix, prefix, valueStyle, precision }: CLSStatCardComponentProps) {
return (
<Card>
<Statistic
title={title}
value={value}
suffix={suffix}
prefix={prefix}
valueStyle={valueStyle}
precision={precision}
/>
</Card>
);
}
StatusTag.tsx
/**
* CLSStatusTagComponent Component
*
* Renders a green "Success" or red "Failed" tag based on a boolean value.
* Used in trace and request tables to visually indicate operation outcomes.
* Labels are customizable via props.
*/
import { Tag } from 'antd';
/** Props for the CLSStatusTagComponent component. */
interface CLSStatusTagComponentProps {
/** Whether the operation was successful. */
success: boolean;
/** Label for successful operations. Defaults to "Success". */
successLabel?: string;
/** Label for failed operations. Defaults to "Failed". */
failLabel?: string;
}
/**
* Displays a colored tag indicating success (green) or failure (red).
*/
export default function CLSStatusTagComponent({ success, successLabel = 'Success', failLabel = 'Failed' }: CLSStatusTagComponentProps) {
return <Tag color={success ? 'green' : 'red'}>{success ? successLabel : failLabel}</Tag>;
}
TraceWaterfall.tsx
/**
* CLSTraceWaterfallComponent Component
*
* Renders a waterfall visualization of all spans in a distributed trace.
* Each span displays:
* - A colored type badge (request, dependency, trace, exception)
* - The operation/dependency name and optional target
* - Duration in milliseconds
* - A proportional progress bar relative to the longest span
* - Green (success) or red (failure) background and border colors
*
* Used in the Traces page drawer when clicking on a specific operation.
*/
import { Typography, Tag, Space, Progress } from 'antd';
import type { CLSTraceItem } from '../types';
/** Props for the CLSTraceWaterfallComponent component. */
interface CLSTraceWaterfallComponentProps {
/** Array of spans belonging to the same distributed trace, ordered by time. */
spans: CLSTraceItem[];
}
/**
* Displays a waterfall view of all spans in a distributed trace.
* The progress bar width for each span is proportional to its duration
* relative to the longest span in the trace.
*/
export default function CLSTraceWaterfallComponent({ spans }: CLSTraceWaterfallComponentProps) {
const maxDuration = Math.max(...spans.map(s => s.durationMs), 1);
if (spans.length === 0) {
return <Typography.Text type="secondary">No spans found</Typography.Text>;
}
return (
<div>
<Typography.Text type="secondary" style={{ marginBottom: 16, display: 'block' }}>
{spans.length} span(s) in this trace
</Typography.Text>
{spans.map((item, idx) => (
<div
key={idx}
style={{
padding: '8px 12px',
marginBottom: 4,
background: item.success ? '#f6ffed' : '#fff2f0',
borderLeft: `4px solid ${item.success ? '#52c41a' : '#f5222d'}`,
borderRadius: 4,
}}
>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<div>
<CLSItemTypeTagComponent type={item.itemType} />
<Typography.Text strong>{item.name}</Typography.Text>
{item.target && (
<Typography.Text type="secondary"> → {item.target}</Typography.Text>
)}
</div>
<Typography.Text>{item.durationMs.toFixed(0)} ms</Typography.Text>
</Space>
<Progress
percent={maxDuration > 0 ? (item.durationMs / maxDuration) * 100 : 0}
showInfo={false}
strokeColor={item.success ? '#52c41a' : '#f5222d'}
size="small"
style={{ marginTop: 4 }}
/>
</div>
))}
</div>
);
}
/**
* Renders a colored tag for the telemetry item type.
* Colors: request=blue, dependency=purple, trace=cyan, exception=red.
*/
function CLSItemTypeTagComponent({ type }: { type: string }) {
const colorMap: Record<string, string> = {
request: 'blue',
dependency: 'purple',
trace: 'cyan',
exception: 'red',
};
return <Tag color={colorMap[type] || 'default'}>{type}</Tag>;
}
SeverityFilter.tsx
/**
* CLSSeverityFilterComponent Component
*
* Renders a row of clickable severity level tags that act as toggle filters.
* Clicking a tag selects that severity level; clicking again deselects it.
* Used on the Logs page to filter log entries by severity level.
*/
import { Tag, Space } from 'antd';
import { FNgetSeverityColor, severityOptions } from './SeverityTag';
/** Props for the CLSSeverityFilterComponent component. */
interface CLSSeverityFilterComponentProps {
/** Currently selected severity level, or undefined if no filter is active. */
value?: number;
/** Callback fired when a severity level is selected or deselected. */
onChange: (value: number | undefined) => void;
}
/**
* Toggleable severity filter using colored tags.
* The selected tag is highlighted with its severity color; unselected tags are neutral.
*/
export default function CLSSeverityFilterComponent({ value, onChange }: CLSSeverityFilterComponentProps) {
return (
<Space>
{severityOptions.map((opt) => (
<Tag
key={opt.value}
color={value === opt.value ? FNgetSeverityColor(opt.value) : undefined}
style={{ cursor: 'pointer' }}
onClick={() => onChange(value === opt.value ? undefined : opt.value)}
>
{opt.label}
</Tag>
))}
</Space>
);
}
PageSpin.tsx
/**
* CLSPageSpinComponent Component
*
* A full-page centered loading spinner used as a placeholder
* while dashboard pages are loading data from the API.
*/
import { Spin } from 'antd';
/**
* Renders a large, centered Ant Design spinner.
* Used as the loading state for page-level data fetches.
*/
export default function CLSPageSpinComponent() {
return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
}
TimeSeriesChart.tsx
/**
* CLSTimeSeriesChartComponent Component
*
* A reusable chart component that renders time-series data as either a line chart
* or an area chart using Recharts. Wraps the chart in an Ant Design Card with a title.
* Automatically formats timestamps for the X-axis using dayjs.
*
* Used across dashboard pages for rendering request volume, response time trends,
* and availability duration over time.
*/
import { Card, Empty } from 'antd';
import {
LineChart, Line, AreaChart, Area,
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts';
import dayjs from 'dayjs';
import type { CLSTimeSeriesPoint } from '../types';
/** Props for the CLSTimeSeriesChartComponent component. */
interface CLSTimeSeriesChartComponentProps {
/** Card title displayed above the chart. */
title: string;
/** Array of time-series data points to plot. */
data: CLSTimeSeriesPoint[];
/** Optional Recharts data key (not used externally — value is always "value"). */
dataKey?: string;
/** Chart line/area color. Defaults to "#1890ff" (Ant Design primary blue). */
color?: string;
/** Chart type: "line" or "area". Defaults to "line". */
type?: 'line' | 'area';
/** Chart height in pixels. Defaults to 300. */
height?: number;
/** dayjs format string for X-axis time labels. Defaults to "HH:mm". */
timeFormat?: string;
/** Optional Y-axis label (not currently rendered but available for future use). */
yAxisLabel?: string;
}
/**
* Renders time-series data in a responsive line or area chart.
* Shows an "Empty" placeholder when no data is available.
*/
export default function CLSTimeSeriesChartComponent({
title,
data,
color = '#1890ff',
type = 'line',
height = 300,
timeFormat = 'HH:mm',
}: CLSTimeSeriesChartComponentProps) {
const chartData = data.map(p => ({
time: dayjs(p.timestamp).format(timeFormat),
value: p.value,
}));
if (chartData.length === 0) {
return (
<Card title={title}>
<Empty description="No data available" />
</Card>
);
}
const ChartComponent = type === 'area' ? AreaChart : LineChart;
return (
<Card title={title}>
<ResponsiveContainer width="100%" height={height}>
<ChartComponent data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
{type === 'area' ? (
<Area type="monotone" dataKey="value" stroke={color} fill={color} fillOpacity={0.2} />
) : (
<Line type="monotone" dataKey="value" stroke={color} strokeWidth={2} dot={false} />
)}
</ChartComponent>
</ResponsiveContainer>
</Card>
);
}
index.ts
export { default as CLSSeverityTagComponent, FNgetSeverityLabel, FNgetSeverityColor, severityOptions } from './SeverityTag';
export { default as CLSSeverityFilterComponent } from './SeverityFilter';
export { default as CLSStatCardComponent } from './StatCard';
export { default as CLSStatusTagComponent } from './StatusTag';
export { default as CLSTimeSeriesChartComponent } from './TimeSeriesChart';
export { default as CLSTraceWaterfallComponent } from './TraceWaterfall';
export { default as CLSPageSpinComponent } from './PageSpin';
17. React: Pages
OverviewPage.tsx
/**
* CLSOverviewPageComponent Component
*
* The dashboard home page that displays key Application Insights metrics at a glance.
* Layout consists of three rows:
* Row 1: Four stat cards — Total Requests, Avg Response Time, Failure Rate, Exceptions
* Row 2: Two line charts — Request Volume Over Time, Avg Response Time Over Time
* Row 3: Bar chart (Top Failing Endpoints) + Pie chart (Response Code Distribution)
*
* Data is fetched via the FNuseRequestSummary hook, which auto-refreshes every 30 seconds.
* Uses the global time range from TimeRangeContext to filter the data.
*/
import { Col, Row, Empty, Card } from 'antd';
import {
ArrowUpOutlined,
ArrowDownOutlined,
ClockCircleOutlined,
ApiOutlined,
BugOutlined,
} from '@ant-design/icons';
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { FNuseRequestSummary } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import { CLSStatCardComponent, CLSTimeSeriesChartComponent, CLSPageSpinComponent } from '../components';
/** Color palette for pie chart segments. */
const COLORS = ['#52c41a', '#1890ff', '#faad14', '#f5222d', '#722ed1'];
/**
* Renders the dashboard overview with stat cards, time-series charts,
* a bar chart of top failures, and a pie chart of response code distribution.
*/
export default function CLSOverviewPageComponent() {
const { timeRange } = FNuseTimeRange();
const { data: summary, isLoading } = FNuseRequestSummary(timeRange);
if (isLoading) return <CLSPageSpinComponent />;
if (!summary) return <Empty description="No data available" />;
return (
<div>
{/* Row 1: Key metric stat cards */}
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<CLSStatCardComponent
title="Total Requests"
value={summary.totalRequests}
prefix={<ApiOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<CLSStatCardComponent
title="Avg Response Time"
value={summary.avgDurationMs}
suffix="ms"
prefix={<ClockCircleOutlined />}
valueStyle={{ color: summary.avgDurationMs > 2000 ? '#f5222d' : '#52c41a' }}
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<CLSStatCardComponent
title="Failure Rate"
value={summary.failureRate}
suffix="%"
prefix={summary.failureRate > 5 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: summary.failureRate > 5 ? '#f5222d' : '#52c41a' }}
/>
</Col>
<Col xs={24} sm={12} lg={6}>
<CLSStatCardComponent
title="Exceptions"
value={summary.totalExceptions}
prefix={<BugOutlined />}
valueStyle={{ color: summary.totalExceptions > 0 ? '#faad14' : '#52c41a' }}
/>
</Col>
</Row>
{/* Row 2: Time-series line charts */}
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<CLSTimeSeriesChartComponent title="Request Volume Over Time" data={summary.requestsOverTime} color="#1890ff" />
</Col>
<Col xs={24} lg={12}>
<CLSTimeSeriesChartComponent title="Avg Response Time Over Time" data={summary.avgDurationOverTime} color="#faad14" />
</Col>
</Row>
{/* Row 3: Bar chart (failures) + Pie chart (response codes) */}
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col xs={24} lg={12}>
<Card title="Top Failing Endpoints">
{summary.topFailingEndpoints.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={summary.topFailingEndpoints} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis type="category" dataKey="name" width={200} tick={{ fontSize: 12 }} />
<Tooltip />
<Bar dataKey="value" fill="#f5222d" />
</BarChart>
</ResponsiveContainer>
) : (
<Empty description="No failing endpoints" />
)}
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="Response Code Distribution">
{summary.responseCodeDistribution.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={summary.responseCodeDistribution}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={100}
label={({ name, value }) => `${name}: ${value}`}
>
{summary.responseCodeDistribution.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<Empty description="No data" />
)}
</Card>
</Col>
</Row>
</div>
);
}
LogsPage.tsx
/**
* CLSLogsPageComponent Component
*
* Interactive log explorer for viewing application trace logs from AppTraces.
* Features:
* - Text search within log messages
* - Severity level filter (clickable tag buttons)
* - Paginated table with sortable columns
* - Click-to-open detail drawer showing full log information including custom dimensions
*
* Data is fetched via the FNuseLogs hook with the global time range and local filter state.
*/
import { useState } from 'react';
import { Table, Input, Space, Drawer, Typography, Descriptions } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { FNuseLogs } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import { CLSSeverityTagComponent, CLSSeverityFilterComponent } from '../components';
import type { CLSLogEntry } from '../types';
/**
* Renders a searchable, filterable table of application logs
* with a slide-out detail drawer for viewing individual log entries.
*/
export default function CLSLogsPageComponent() {
const { timeRange } = FNuseTimeRange();
const [search, setSearch] = useState('');
const [severity, setSeverity] = useState<number | undefined>();
const [page, setPage] = useState(0);
const [selectedLog, setSelectedLog] = useState<CLSLogEntry | null>(null);
const { data: logs, isLoading } = FNuseLogs({ timeRange, severity, search: search || undefined, pageSize: 50, page });
/** Table column definitions for the logs table. */
const columns: ColumnsType<CLSLogEntry> = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
width: 180,
render: (ts: string) => dayjs(ts).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: 'Severity',
dataIndex: 'severityLevel',
width: 120,
render: (level: number) => <CLSSeverityTagComponent level={level} />,
},
{
title: 'Message',
dataIndex: 'message',
ellipsis: true,
},
{
title: 'Role',
dataIndex: 'appRoleName',
width: 150,
ellipsis: true,
},
{
title: 'Operation ID',
dataIndex: 'operationId',
width: 200,
ellipsis: true,
},
];
return (
<div>
{/* Search and severity filter controls */}
<Space style={{ marginBottom: 16, width: '100%' }} direction="vertical">
<Space wrap>
<Input.Search
placeholder="Search logs..."
onSearch={setSearch}
allowClear
style={{ width: 400 }}
/>
<CLSSeverityFilterComponent value={severity} onChange={setSeverity} />
</Space>
</Space>
{/* Logs data table with pagination and row click handler */}
<Table<CLSLogEntry>
columns={columns}
dataSource={logs}
loading={isLoading}
rowKey={(r) => `${r.timestamp}-${r.operationId}-${r.message?.substring(0, 20)}`}
pagination={{
current: page + 1,
pageSize: 50,
onChange: (p) => setPage(p - 1),
showSizeChanger: false,
}}
onRow={(record) => ({
onClick: () => setSelectedLog(record),
style: { cursor: 'pointer' },
})}
size="small"
scroll={{ y: 'calc(100vh - 340px)' }}
/>
{/* Detail drawer showing full log entry information */}
<Drawer
title="Log Details"
open={!!selectedLog}
onClose={() => setSelectedLog(null)}
width={600}
>
{selectedLog && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="Timestamp">{dayjs(selectedLog.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')}</Descriptions.Item>
<Descriptions.Item label="Severity"><CLSSeverityTagComponent level={selectedLog.severityLevel} /></Descriptions.Item>
<Descriptions.Item label="Message"><Typography.Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{selectedLog.message}</Typography.Paragraph></Descriptions.Item>
<Descriptions.Item label="Operation ID">{selectedLog.operationId}</Descriptions.Item>
<Descriptions.Item label="Operation Name">{selectedLog.operationName}</Descriptions.Item>
<Descriptions.Item label="Role Name">{selectedLog.appRoleName}</Descriptions.Item>
{selectedLog.customDimensions && (
<Descriptions.Item label="Custom Dimensions">
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, fontFamily: 'monospace', fontSize: 12 }}>
{selectedLog.customDimensions}
</Typography.Paragraph>
</Descriptions.Item>
)}
</Descriptions>
)}
</Drawer>
</div>
);
}
TracesPage.tsx
/**
* CLSTracesPageComponent Component
*
* Displays HTTP request traces from AppRequests with filtering and pagination.
* Features:
* - Search by operation name
* - Paginated table showing timestamp, name, duration, status, response code, and operation ID
* - Sortable duration column for finding slow requests
* - Click on a row to open a trace waterfall drawer showing all spans in the distributed trace
*
* The waterfall view fetches detailed span data via FNuseTraceDetail when an operation ID is selected.
*/
import { useState } from 'react';
import { Table, Input, Space, Drawer } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { FNuseTraces, FNuseTraceDetail } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import { CLSStatusTagComponent, CLSTraceWaterfallComponent } from '../components';
import type { CLSTraceItem } from '../types';
/**
* Renders a paginated table of request traces with a slide-out
* waterfall visualization for distributed trace analysis.
*/
export default function CLSTracesPageComponent() {
const { timeRange } = FNuseTimeRange();
const [operationName, setOperationName] = useState('');
const [page, setPage] = useState(0);
const [selectedOpId, setSelectedOpId] = useState('');
const { data: traces, isLoading } = FNuseTraces({ timeRange, operationName: operationName || undefined, pageSize: 50, page });
const { data: traceDetail, isLoading: detailLoading } = FNuseTraceDetail(selectedOpId);
/** Table column definitions for the traces table. */
const columns: ColumnsType<CLSTraceItem> = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
width: 180,
render: (ts: string) => dayjs(ts).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: 'Name',
dataIndex: 'name',
ellipsis: true,
},
{
title: 'Duration',
dataIndex: 'durationMs',
width: 120,
render: (d: number) => `${d.toFixed(0)} ms`,
sorter: (a, b) => a.durationMs - b.durationMs,
},
{
title: 'Status',
dataIndex: 'success',
width: 100,
render: (s: boolean) => <CLSStatusTagComponent success={s} />,
},
{
title: 'Response Code',
dataIndex: 'resultCode',
width: 120,
},
{
title: 'Operation ID',
dataIndex: 'operationId',
width: 200,
ellipsis: true,
},
];
return (
<div>
{/* Operation name search filter */}
<Space style={{ marginBottom: 16 }}>
<Input.Search
placeholder="Search by operation name..."
onSearch={setOperationName}
allowClear
style={{ width: 400 }}
/>
</Space>
{/* Traces data table */}
<Table<CLSTraceItem>
columns={columns}
dataSource={traces}
loading={isLoading}
rowKey={(r) => `${r.timestamp}-${r.id || r.operationId}`}
pagination={{
current: page + 1,
pageSize: 50,
onChange: (p) => setPage(p - 1),
}}
onRow={(record) => ({
onClick: () => record.operationId && setSelectedOpId(record.operationId),
style: { cursor: 'pointer' },
})}
size="small"
/>
{/* Trace waterfall drawer showing all spans for the selected operation */}
<Drawer
title={`Trace Waterfall: ${selectedOpId}`}
open={!!selectedOpId}
onClose={() => setSelectedOpId('')}
width={800}
loading={detailLoading}
>
{traceDetail && <CLSTraceWaterfallComponent spans={traceDetail} />}
</Drawer>
</div>
);
}
ExceptionsPage.tsx
/**
* CLSExceptionsPageComponent Component
*
* Displays exception telemetry from the AppExceptions table with two views:
* 1. "All Exceptions" tab — Paginated table of individual exception entries
* 2. "Grouped by Type" tab — Aggregated view showing exception types with counts
*
* Features:
* - Exception distribution chart (top 10 types by frequency)
* - Click on an exception to open a detail drawer with stack trace
* - Sortable columns in the grouped view
*/
import { useState } from 'react';
import { Table, Tag, Tabs, Drawer, Typography, Descriptions, Card } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import dayjs from 'dayjs';
import { FNuseExceptions, FNuseExceptionGroups } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import type { CLSExceptionEntry, CLSExceptionGroup } from '../types';
/**
* Renders exception data with distribution chart, tabbed list/grouped views,
* and a detail drawer showing stack traces for individual exceptions.
*/
export default function CLSExceptionsPageComponent() {
const { timeRange } = FNuseTimeRange();
const [page, setPage] = useState(0);
const [selectedEx, setSelectedEx] = useState<CLSExceptionEntry | null>(null);
const { data: exceptions, isLoading } = FNuseExceptions({ timeRange, pageSize: 50, page });
const { data: groups, isLoading: groupsLoading } = FNuseExceptionGroups(timeRange);
/** Column definitions for the individual exceptions table. */
const exColumns: ColumnsType<CLSExceptionEntry> = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
width: 180,
render: (ts: string) => dayjs(ts).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: 'Type',
dataIndex: 'exceptionType',
width: 250,
ellipsis: true,
render: (t: string) => <Tag color="red">{t}</Tag>,
},
{
title: 'Message',
dataIndex: 'outerMessage',
ellipsis: true,
},
{
title: 'Role',
dataIndex: 'appRoleName',
width: 150,
},
];
/** Column definitions for the grouped exceptions table. */
const groupColumns: ColumnsType<CLSExceptionGroup> = [
{
title: 'Exception Type',
dataIndex: 'exceptionType',
ellipsis: true,
render: (t: string) => <Tag color="red">{t}</Tag>,
},
{
title: 'Message',
dataIndex: 'message',
ellipsis: true,
},
{
title: 'Count',
dataIndex: 'count',
width: 100,
sorter: (a, b) => a.count - b.count,
defaultSortOrder: 'descend',
},
{
title: 'Last Seen',
dataIndex: 'lastSeen',
width: 180,
render: (ts: string) => dayjs(ts).format('YYYY-MM-DD HH:mm:ss'),
},
];
// Build chart data from top 10 exception groups (short type names for readability)
const trendData = groups?.slice(0, 10).map(g => ({
type: g.exceptionType.split('.').pop(),
count: g.count,
})) || [];
return (
<div>
{/* Exception distribution chart */}
{trendData.length > 0 && (
<Card title="Exception Distribution" style={{ marginBottom: 16 }}>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={trendData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="type" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="count" stroke="#f5222d" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</Card>
)}
{/* Tabbed view: All Exceptions / Grouped by Type */}
<Tabs defaultActiveKey="list" items={[
{
key: 'list',
label: 'All Exceptions',
children: (
<Table<CLSExceptionEntry>
columns={exColumns}
dataSource={exceptions}
loading={isLoading}
rowKey={(r) => `${r.timestamp}-${r.exceptionType}-${r.operationId}`}
pagination={{
current: page + 1,
pageSize: 50,
onChange: (p) => setPage(p - 1),
}}
onRow={(record) => ({
onClick: () => setSelectedEx(record),
style: { cursor: 'pointer' },
})}
size="small"
/>
),
},
{
key: 'grouped',
label: 'Grouped by Type',
children: (
<Table<CLSExceptionGroup>
columns={groupColumns}
dataSource={groups}
loading={groupsLoading}
rowKey={(r) => `${r.exceptionType}-${r.message}`}
size="small"
/>
),
},
]} />
{/* Exception detail drawer with stack trace */}
<Drawer
title="Exception Details"
open={!!selectedEx}
onClose={() => setSelectedEx(null)}
width={700}
>
{selectedEx && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="Timestamp">{dayjs(selectedEx.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')}</Descriptions.Item>
<Descriptions.Item label="Type"><Tag color="red">{selectedEx.exceptionType}</Tag></Descriptions.Item>
<Descriptions.Item label="Message">{selectedEx.outerMessage || selectedEx.message}</Descriptions.Item>
<Descriptions.Item label="Operation ID">{selectedEx.operationId}</Descriptions.Item>
<Descriptions.Item label="Role Name">{selectedEx.appRoleName}</Descriptions.Item>
{selectedEx.stackTrace && (
<Descriptions.Item label="Stack Trace">
<Typography.Paragraph
style={{
whiteSpace: 'pre-wrap',
fontFamily: 'monospace',
fontSize: 11,
maxHeight: 400,
overflow: 'auto',
margin: 0,
background: '#f5f5f5',
padding: 12,
borderRadius: 4,
}}
>
{selectedEx.stackTrace}
</Typography.Paragraph>
</Descriptions.Item>
)}
</Descriptions>
)}
</Drawer>
</div>
);
}
MetricsPage.tsx
/**
* CLSMetricsPageComponent Component
*
* Interactive metric explorer that allows users to:
* 1. Select a metric from a searchable dropdown (populated from AppMetrics/AppPerformanceCounters)
* 2. Choose an aggregation function (avg, sum, min, max, count)
* 3. Optionally set a custom time bucket interval
* 4. View the resulting time-series data as a responsive area chart
*
* Uses the global time range from TimeRangeContext and fetches data via FNuseMetric hook.
*/
import { useState } from 'react';
import { Card, Select, Space, Spin, Empty } from 'antd';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import dayjs from 'dayjs';
import { FNuseMetricDefinitions, FNuseMetric } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
/** Available aggregation options for the metric query. */
const aggregationOptions = [
{ value: 'avg', label: 'Average' },
{ value: 'sum', label: 'Sum' },
{ value: 'min', label: 'Min' },
{ value: 'max', label: 'Max' },
{ value: 'count', label: 'Count' },
];
/** Available time bucket interval options for the metric query. */
const intervalOptions = [
{ value: '1m', label: '1 minute' },
{ value: '5m', label: '5 minutes' },
{ value: '15m', label: '15 minutes' },
{ value: '1h', label: '1 hour' },
{ value: '6h', label: '6 hours' },
{ value: '1d', label: '1 day' },
];
/**
* Renders a metric explorer with dropdown selectors and an area chart.
* The chart updates automatically when the metric, aggregation, interval, or time range changes.
*/
export default function CLSMetricsPageComponent() {
const { timeRange } = FNuseTimeRange();
const [metricName, setMetricName] = useState('');
const [aggregation, setAggregation] = useState('avg');
const [interval, setInterval] = useState<string | undefined>();
const { data: definitions, isLoading: defsLoading } = FNuseMetricDefinitions();
const { data: metricResult, isLoading: metricLoading } = FNuseMetric({
metricName,
timeRange,
interval,
aggregation,
});
// Transform time-series data for Recharts
const chartData = metricResult?.timeseries.map(p => ({
time: dayjs(p.timestamp).format('MM-DD HH:mm'),
value: p.value ?? 0,
})) || [];
return (
<div>
{/* Metric selection controls */}
<Card style={{ marginBottom: 16 }}>
<Space wrap size="middle">
<Select
showSearch
placeholder="Select a metric"
value={metricName || undefined}
onChange={setMetricName}
loading={defsLoading}
style={{ width: 350 }}
options={definitions?.map(d => ({ value: d.name, label: d.displayName || d.name }))}
filterOption={(input, option) => (option?.label as string)?.toLowerCase().includes(input.toLowerCase())}
/>
<Select
value={aggregation}
onChange={setAggregation}
options={aggregationOptions}
style={{ width: 130 }}
/>
<Select
value={interval}
onChange={setInterval}
options={intervalOptions}
placeholder="Auto interval"
allowClear
style={{ width: 150 }}
/>
</Space>
</Card>
{/* Metric area chart */}
<Card title={metricName ? `${metricName} (${aggregation})` : 'Select a metric to view'}>
{metricLoading ? (
<Spin style={{ display: 'block', margin: '60px auto' }} />
) : chartData.length > 0 ? (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="value" stroke="#1890ff" fill="#1890ff" fillOpacity={0.2} />
</AreaChart>
</ResponsiveContainer>
) : (
<Empty description={metricName ? 'No data for this metric' : 'Choose a metric from the dropdown above'} />
)}
</Card>
</div>
);
}
DependenciesPage.tsx
/**
* CLSDependenciesPageComponent Component
*
* Displays external dependency call statistics from the AppDependencies table.
* Features:
* - Bar chart: Top 10 slowest dependencies (by average duration)
* - Pie chart: Call distribution grouped by dependency type (SQL, HTTP, etc.)
* - Paginated data table with columns: Name, Type, Avg Duration, Failure Rate, Call Count
* - Color-coded failure rate tags (green/orange/red)
* - Sortable columns for duration, failure rate, and call count
*/
import { useState } from 'react';
import { Table, Tag, Card, Row, Col } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { FNuseDependencies } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import type { CLSDependencyEntry } from '../types';
/** Color palette for the dependency type pie chart. */
const COLORS = ['#1890ff', '#722ed1', '#13c2c2', '#faad14', '#f5222d', '#52c41a', '#eb2f96', '#fa8c16'];
/**
* Renders dependency analytics with charts and a detailed data table.
*/
export default function CLSDependenciesPageComponent() {
const { timeRange } = FNuseTimeRange();
const [page, setPage] = useState(0);
const { data: deps, isLoading } = FNuseDependencies({ timeRange, pageSize: 50, page });
/** Table column definitions for the dependencies table. */
const columns: ColumnsType<CLSDependencyEntry> = [
{
title: 'Name',
dataIndex: 'name',
ellipsis: true,
},
{
title: 'Type',
dataIndex: 'type',
width: 120,
render: (t: string) => <Tag color="purple">{t}</Tag>,
},
{
title: 'Avg Duration',
dataIndex: 'avgDurationMs',
width: 130,
render: (d: number) => `${d.toFixed(0)} ms`,
sorter: (a, b) => a.avgDurationMs - b.avgDurationMs,
},
{
title: 'Failure Rate',
dataIndex: 'failureRate',
width: 120,
render: (r: number) => <Tag color={r > 10 ? 'red' : r > 0 ? 'orange' : 'green'}>{r.toFixed(1)}%</Tag>,
sorter: (a, b) => a.failureRate - b.failureRate,
},
{
title: 'Call Count',
dataIndex: 'callCount',
width: 110,
sorter: (a, b) => a.callCount - b.callCount,
defaultSortOrder: 'descend',
},
];
// Top 10 slowest dependencies for bar chart
const topSlowest = [...(deps || [])].sort((a, b) => b.avgDurationMs - a.avgDurationMs).slice(0, 10);
// Group call counts by dependency type for pie chart
const typeGroups: Record<string, number> = {};
deps?.forEach(d => { typeGroups[d.type] = (typeGroups[d.type] || 0) + d.callCount; });
const pieData = Object.entries(typeGroups).map(([name, value]) => ({ name, value }));
return (
<div>
{/* Charts row: Bar chart (slowest) + Pie chart (by type) */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} lg={12}>
<Card title="Top 10 Slowest Dependencies">
<ResponsiveContainer width="100%" height={300}>
<BarChart data={topSlowest} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis type="category" dataKey="name" width={180} tick={{ fontSize: 11 }} />
<Tooltip />
<Bar dataKey="avgDurationMs" fill="#722ed1" name="Avg Duration (ms)" />
</BarChart>
</ResponsiveContainer>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="Calls by Dependency Type">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label>
{pieData.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</Card>
</Col>
</Row>
{/* Dependencies data table */}
<Table<CLSDependencyEntry>
columns={columns}
dataSource={deps}
loading={isLoading}
rowKey={(r) => `${r.name}-${r.type}`}
pagination={{
current: page + 1,
pageSize: 50,
onChange: (p) => setPage(p - 1),
}}
size="small"
/>
</div>
);
}
AvailabilityPage.tsx
/**
* CLSAvailabilityPageComponent Component
*
* Displays availability (web test) results from the AppAvailabilityResults table.
* Features:
* - Duration trend line chart showing test completion times over time
* - Filter by test name
* - Paginated table with: Timestamp, Test Name, Location, Status (Pass/Fail), Duration, Message
* - Color-coded status tags (green for pass, red for fail)
*/
import { useState } from 'react';
import { Table, Tag, Input, Card } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import dayjs from 'dayjs';
import { FNuseAvailability } from '../hooks/useApiQueries';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import type { CLSAvailabilityResult } from '../types';
/**
* Renders availability test results with a duration trend chart
* and a paginated, filterable data table.
*/
export default function CLSAvailabilityPageComponent() {
const { timeRange } = FNuseTimeRange();
const [testName, setTestName] = useState('');
const [page, setPage] = useState(0);
const { data: results, isLoading } = FNuseAvailability({ timeRange, testName: testName || undefined, pageSize: 50, page });
/** Table column definitions for the availability results table. */
const columns: ColumnsType<CLSAvailabilityResult> = [
{
title: 'Timestamp',
dataIndex: 'timestamp',
width: 180,
render: (ts: string) => dayjs(ts).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: 'Test Name',
dataIndex: 'testName',
ellipsis: true,
},
{
title: 'Location',
dataIndex: 'location',
width: 200,
},
{
title: 'Status',
dataIndex: 'success',
width: 100,
render: (s: boolean) => <Tag color={s ? 'green' : 'red'}>{s ? 'Pass' : 'Fail'}</Tag>,
},
{
title: 'Duration',
dataIndex: 'durationMs',
width: 120,
render: (d: number) => `${d.toFixed(0)} ms`,
},
{
title: 'Message',
dataIndex: 'message',
ellipsis: true,
},
];
// Build duration trend data for the chart (reversed to chronological order)
const trendData = results?.map(r => ({
time: dayjs(r.timestamp).format('HH:mm'),
duration: r.durationMs,
success: r.success ? 1 : 0,
})).reverse() || [];
return (
<div>
{/* Duration trend line chart */}
{trendData.length > 0 && (
<Card title="Availability Duration Over Time" style={{ marginBottom: 16 }}>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={trendData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="duration" stroke="#1890ff" strokeWidth={2} dot={false} name="Duration (ms)" />
</LineChart>
</ResponsiveContainer>
</Card>
)}
{/* Test name filter */}
<Input.Search
placeholder="Filter by test name..."
onSearch={setTestName}
allowClear
style={{ width: 400, marginBottom: 16 }}
/>
{/* Availability results data table */}
<Table<CLSAvailabilityResult>
columns={columns}
dataSource={results}
loading={isLoading}
rowKey={(r) => `${r.timestamp}-${r.testName}-${r.location}`}
pagination={{
current: page + 1,
pageSize: 50,
onChange: (p) => setPage(p - 1),
}}
size="small"
/>
</div>
);
}
KqlPage.tsx
/**
* CLSKqlPageComponent Component
*
* Interactive KQL (Kusto Query Language) query editor page.
* Features:
* - Monaco Editor (VS Code editor) for writing KQL queries
* - Ctrl+Enter keyboard shortcut to execute queries
* - Configurable time span for query execution
* - Dynamic result table with columns derived from the query output
* - Saved queries sidebar (load, save, delete queries)
* - CSV export of query results
* - TanStack Query mutations for query execution and saved query management
*
* This page communicates with the POST /api/kql endpoint, which validates
* queries for safety (blocking dangerous management commands) before execution.
*/
import { useState, useCallback } from 'react';
import { Card, Button, Table, Space, Select, message, Modal, Input, List, Typography, Popconfirm, Spin } from 'antd';
import { PlayCircleOutlined, SaveOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
import Editor from '@monaco-editor/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FNexecuteKql, FNsaveQuery, FNdeleteSavedQuery } from '../api/kqlApi';
import { FNuseSavedQueries } from '../hooks/useApiQueries';
import type { CLSKqlResult, CLSSavedQuery } from '../types';
/** Time span options for the query execution dropdown. */
const timeSpanOptions = [
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hours' },
{ value: '24h', label: 'Last 24 hours' },
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
];
/**
* Renders a full-featured KQL query editor with saved queries sidebar,
* Monaco Editor, execution controls, and a dynamic result table.
*/
export default function CLSKqlPageComponent() {
const [query, setQuery] = useState('AppRequests\n| summarize count() by Name\n| order by count_ desc\n| take 10');
const [timeSpan, setTimeSpan] = useState('24h');
const [result, setResult] = useState<CLSKqlResult | null>(null);
const [saveModalOpen, setSaveModalOpen] = useState(false);
const [saveName, setSaveName] = useState('');
const [saveDesc, setSaveDesc] = useState('');
const queryClient = useQueryClient();
const { data: savedQueries } = FNuseSavedQueries();
/** Mutation for executing KQL queries against the backend. */
const executeMutation = useMutation({
mutationFn: () => FNexecuteKql(query, timeSpan),
onSuccess: (data) => setResult(data),
onError: (err: any) => message.error(err.response?.data?.detail || 'Query failed'),
});
/** Mutation for saving a named query to the server. */
const saveMutation = useMutation({
mutationFn: () => FNsaveQuery({ name: saveName, query, description: saveDesc }),
onSuccess: () => {
message.success('Query saved');
setSaveModalOpen(false);
setSaveName('');
setSaveDesc('');
queryClient.invalidateQueries({ queryKey: ['savedQueries'] });
},
});
/** Mutation for deleting a saved query by ID. */
const deleteMutation = useMutation({
mutationFn: (id: string) => FNdeleteSavedQuery(id),
onSuccess: () => {
message.success('Query deleted');
queryClient.invalidateQueries({ queryKey: ['savedQueries'] });
},
});
/** Executes the current query if it's non-empty. */
const FNhandleRunQuery = useCallback(() => {
if (query.trim()) executeMutation.mutate();
}, [query, executeMutation]);
/** Registers the Ctrl+Enter keyboard shortcut in the Monaco Editor. */
const FNhandleEditorMount = useCallback((editor: any) => {
editor.addCommand(2048 | 3, () => { // Ctrl+Enter
FNhandleRunQuery();
});
}, [FNhandleRunQuery]);
/** Exports the current query results as a downloadable CSV file. */
const FNexportCsv = () => {
if (!result) return;
const header = result.columns.map(c => c.name).join(',');
const rows = result.rows.map(r => r.map(v => `"${v ?? ''}"`).join(','));
const csv = [header, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'kql-result.csv';
a.click();
URL.revokeObjectURL(url);
};
// Build dynamic table columns from the KQL result schema
const resultColumns = result?.columns.map(col => ({
title: col.name,
dataIndex: col.name,
key: col.name,
ellipsis: true,
})) || [];
// Transform row arrays into objects keyed by column name for the Ant Design Table
const resultData = result?.rows.map((row, idx) => {
const obj: Record<string, any> = { _key: idx };
result.columns.forEach((col, i) => { obj[col.name] = row[i]; });
return obj;
}) || [];
return (
<div style={{ display: 'flex', gap: 16, height: 'calc(100vh - 160px)' }}>
{/* Saved Queries Sidebar */}
<Card title="Saved Queries" style={{ width: 280, overflow: 'auto', flexShrink: 0 }} size="small">
<List
dataSource={savedQueries}
renderItem={(item: CLSSavedQuery) => (
<List.Item
style={{ cursor: 'pointer', padding: '8px 0' }}
onClick={() => setQuery(item.query)}
actions={[
<Popconfirm
key="del"
title="Delete this query?"
onConfirm={(e) => { e?.stopPropagation(); deleteMutation.mutate(item.id); }}
>
<Button type="text" size="small" icon={<DeleteOutlined />} danger onClick={e => e.stopPropagation()} />
</Popconfirm>
]}
>
<List.Item.Meta
title={<Typography.Text ellipsis>{item.name}</Typography.Text>}
description={<Typography.Text type="secondary" ellipsis style={{ fontSize: 11 }}>{item.description}</Typography.Text>}
/>
</List.Item>
)}
size="small"
/>
</Card>
{/* Main Editor + Results Area */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* Monaco Editor Card */}
<Card size="small" style={{ flexShrink: 0 }}>
<div style={{ border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }}>
<Editor
height="200px"
language="plaintext"
value={query}
onChange={(v) => setQuery(v || '')}
onMount={FNhandleEditorMount}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
wordWrap: 'on',
}}
/>
</div>
<Space style={{ marginTop: 12 }}>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={FNhandleRunQuery}
loading={executeMutation.isPending}
>
Run Query (Ctrl+Enter)
</Button>
<Select value={timeSpan} onChange={setTimeSpan} options={timeSpanOptions} style={{ width: 150 }} />
<Button icon={<SaveOutlined />} onClick={() => setSaveModalOpen(true)}>Save</Button>
{result && <Button icon={<DownloadOutlined />} onClick={FNexportCsv}>Export CSV</Button>}
</Space>
</Card>
{/* Query Results Table */}
<Card
title={result ? `Results (${result.rowCount} rows)` : 'Results'}
size="small"
style={{ flex: 1, overflow: 'auto' }}
>
{executeMutation.isPending ? (
<Spin style={{ display: 'block', margin: '40px auto' }} />
) : result ? (
<Table
columns={resultColumns}
dataSource={resultData}
rowKey="_key"
size="small"
pagination={{ pageSize: 100 }}
scroll={{ x: 'max-content' }}
/>
) : (
<Typography.Text type="secondary">Run a query to see results</Typography.Text>
)}
</Card>
</div>
{/* Save Query Modal */}
<Modal
title="Save Query"
open={saveModalOpen}
onOk={() => saveMutation.mutate()}
onCancel={() => setSaveModalOpen(false)}
confirmLoading={saveMutation.isPending}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Input placeholder="Query name" value={saveName} onChange={e => setSaveName(e.target.value)} />
<Input.TextArea placeholder="Description (optional)" value={saveDesc} onChange={e => setSaveDesc(e.target.value)} rows={2} />
</Space>
</Modal>
</div>
);
}
18. React: Layout, App, Entry Point & .env
AppLayout.tsx
/**
* CLSAppLayoutComponent Component
*
* The main application shell that provides the dashboard's visual structure:
* - Collapsible sidebar with navigation menu (links to all dashboard pages)
* - Header bar with: dashboard title, global time range selector, refresh button,
* dark mode toggle, and user profile dropdown with sign-out
* - Content area that renders the active page via React Router's <Outlet />
*
* This layout wraps all route-level pages and is rendered as a parent route element.
*/
import { useState } from 'react';
import { Layout, Menu, Select, Button, Typography, theme, Dropdown, Avatar } from 'antd';
import {
DashboardOutlined,
FileTextOutlined,
ApartmentOutlined,
BugOutlined,
LineChartOutlined,
ApiOutlined,
CheckCircleOutlined,
CodeOutlined,
ReloadOutlined,
BulbOutlined,
BulbFilled,
UserOutlined,
LogoutOutlined,
} from '@ant-design/icons';
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
import { useMsal } from '@azure/msal-react';
import { FNuseTimeRange } from '../hooks/useTimeRange';
import { useQueryClient } from '@tanstack/react-query';
const { Header, Sider, Content } = Layout;
/** Sidebar navigation menu items mapping routes to icons and labels. */
const menuItems = [
{ key: '/', icon: <DashboardOutlined />, label: 'Overview' },
{ key: '/logs', icon: <FileTextOutlined />, label: 'Logs' },
{ key: '/traces', icon: <ApartmentOutlined />, label: 'Traces' },
{ key: '/exceptions', icon: <BugOutlined />, label: 'Exceptions' },
{ key: '/metrics', icon: <LineChartOutlined />, label: 'Metrics' },
{ key: '/dependencies', icon: <ApiOutlined />, label: 'Dependencies' },
{ key: '/availability', icon: <CheckCircleOutlined />, label: 'Availability' },
{ key: '/kql', icon: <CodeOutlined />, label: 'KQL Query' },
];
/** Time range dropdown options for the global time filter in the header. */
const timeRangeOptions = [
{ value: '30m', label: 'Last 30 minutes' },
{ value: '1h', label: 'Last 1 hour' },
{ value: '6h', label: 'Last 6 hours' },
{ value: '12h', label: 'Last 12 hours' },
{ value: '24h', label: 'Last 24 hours' },
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
];
/** Props for the CLSAppLayoutComponent component. */
interface CLSAppLayoutComponentProps {
/** Whether dark mode is currently active. */
isDarkMode: boolean;
/** Callback to toggle between light and dark mode. */
toggleDarkMode: () => void;
}
/**
* Main application layout with sidebar navigation, header controls, and content area.
* Manages sidebar collapse state, navigation, time range selection, and user profile.
*/
export default function CLSAppLayoutComponent({ isDarkMode, toggleDarkMode }: CLSAppLayoutComponentProps) {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { timeRange, setTimeRange } = FNuseTimeRange();
const queryClient = useQueryClient();
const { token } = theme.useToken();
const { instance, accounts } = useMsal();
// Extract user info from the active MSAL account
const activeAccount = accounts[0];
const userName = activeAccount?.name || activeAccount?.username || 'User';
const userInitial = userName.charAt(0).toUpperCase();
/** Signs the user out via Entra ID redirect logout. */
const FNhandleSignOut = () => {
instance.logoutRedirect({ postLogoutRedirectUri: window.location.origin });
};
/** User profile dropdown menu items. */
const userMenuItems = [
{
key: 'user-info',
label: (
<div style={{ padding: '4px 0' }}>
<Typography.Text strong>{userName}</Typography.Text>
<br />
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{activeAccount?.username}
</Typography.Text>
</div>
),
disabled: true,
},
{ type: 'divider' as const },
{
key: 'sign-out',
icon: <LogoutOutlined />,
label: 'Sign Out',
onClick: FNhandleSignOut,
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
{/* Collapsible sidebar with navigation menu */}
<Sider
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
style={{ background: token.colorBgContainer }}
>
<div style={{ padding: '16px', textAlign: 'center' }}>
<Typography.Title level={collapsed ? 5 : 4} style={{ margin: 0, color: token.colorPrimary }}>
{collapsed ? 'AI' : 'AppInsights'}
</Typography.Title>
</div>
<Menu
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems}
onClick={({ key }) => navigate(key)}
/>
</Sider>
<Layout>
{/* Header with title, time range selector, refresh, dark mode toggle, and user profile */}
<Header style={{
padding: '0 24px',
background: token.colorBgContainer,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
}}>
<Typography.Text strong style={{ fontSize: 16 }}>
Azure Application Insights Dashboard
</Typography.Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Select
value={timeRange}
onChange={setTimeRange}
options={timeRangeOptions}
style={{ width: 160 }}
/>
<Button
icon={<ReloadOutlined />}
onClick={() => queryClient.invalidateQueries()}
>
Refresh
</Button>
<Button
icon={isDarkMode ? <BulbFilled /> : <BulbOutlined />}
onClick={toggleDarkMode}
/>
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight" trigger={['click']}>
<Avatar
style={{ backgroundColor: token.colorPrimary, cursor: 'pointer' }}
icon={<UserOutlined />}
>
{userInitial}
</Avatar>
</Dropdown>
</div>
</Header>
{/* Content area where the active page is rendered via React Router Outlet */}
<Content style={{ margin: 16, overflow: 'auto' }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
}
App.tsx
/**
* Root Application Component
*
* Composes the full application by nesting all required providers and defining routes.
* Provider hierarchy (outermost to innermost):
* 1. CLSAuthProviderComponent — Initializes MSAL and provides authentication context
* 2. QueryClientProvider — Provides TanStack Query caching and state management
* 3. ConfigProvider — Ant Design theme (supports light/dark mode toggle)
* 4. CLSAuthGuardComponent — Blocks unauthenticated access, shows sign-in screen
* 5. TimeRangeContext — Global time range filter shared across all pages
* 6. BrowserRouter + Routes — Client-side routing to dashboard pages
*
* The CLSAppLayoutComponent component provides the sidebar, header, and content area.
* Each route renders inside the layout's <Outlet /> slot.
*/
import { useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ConfigProvider, theme } from 'antd';
import { CLSAuthProviderComponent, CLSAuthGuardComponent } from './auth';
import { TimeRangeContext, FNuseTimeRangeState } from './hooks/useTimeRange';
import CLSAppLayoutComponent from './layouts/AppLayout';
import CLSOverviewPageComponent from './pages/OverviewPage';
import CLSLogsPageComponent from './pages/LogsPage';
import CLSTracesPageComponent from './pages/TracesPage';
import CLSExceptionsPageComponent from './pages/ExceptionsPage';
import CLSMetricsPageComponent from './pages/MetricsPage';
import CLSDependenciesPageComponent from './pages/DependenciesPage';
import CLSAvailabilityPageComponent from './pages/AvailabilityPage';
import CLSKqlPageComponent from './pages/KqlPage';
/**
* TanStack Query client with default options:
* - staleTime: 30s before data is considered stale and eligible for background refetch
* - retry: 1 retry on failure
* - refetchOnWindowFocus: disabled to avoid unnecessary API calls
*/
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});
/**
* Root component that wires together authentication, data fetching,
* theming, and routing for the Application Insights Dashboard.
*/
export default function App() {
const timeRangeState = FNuseTimeRangeState();
const [isDarkMode, setIsDarkMode] = useState(false);
return (
<CLSAuthProviderComponent>
<QueryClientProvider client={queryClient}>
<ConfigProvider
theme={{
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: { colorPrimary: '#1890ff' },
}}
>
<CLSAuthGuardComponent>
<TimeRangeContext.Provider value={timeRangeState}>
<BrowserRouter>
<Routes>
<Route element={<CLSAppLayoutComponent isDarkMode={isDarkMode} toggleDarkMode={() => setIsDarkMode(!isDarkMode)} />}>
<Route path="/" element={<CLSOverviewPageComponent />} />
<Route path="/logs" element={<CLSLogsPageComponent />} />
<Route path="/traces" element={<CLSTracesPageComponent />} />
<Route path="/exceptions" element={<CLSExceptionsPageComponent />} />
<Route path="/metrics" element={<CLSMetricsPageComponent />} />
<Route path="/dependencies" element={<CLSDependenciesPageComponent />} />
<Route path="/availability" element={<CLSAvailabilityPageComponent />} />
<Route path="/kql" element={<CLSKqlPageComponent />} />
</Route>
</Routes>
</BrowserRouter>
</TimeRangeContext.Provider>
</CLSAuthGuardComponent>
</ConfigProvider>
</QueryClientProvider>
</CLSAuthProviderComponent>
);
}
main.tsx
/**
* Application Entry Point
*
* Mounts the root React component into the DOM.
* Uses React.StrictMode for development-time checks (double rendering,
* deprecated API warnings, effect cleanup validation).
*/
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
.env (Environment Variables)
# Entra ID (Azure AD) App Registration for the React SPA
VITE_AZURE_TENANT_ID=<YOUR_AZURE_TENANT_ID>
VITE_AZURE_CLIENT_ID=<YOUR_SPA_APP_REGISTRATION_CLIENT_ID>
# API scope exposed by the backend app registration
VITE_API_SCOPE=api://<YOUR_API_APP_REGISTRATION_CLIENT_ID>/access_as_user
# Backend API URL
VITE_API_URL=http://localhost:5027
.env to .gitignore. Never commit credentials.19. KQL Deep Dive
| Table | Contains |
|---|---|
AppTraces | Log messages |
AppRequests | HTTP requests |
AppExceptions | Exceptions |
AppDependencies | External calls |
AppMetrics | Metrics |
AppAvailabilityResults | Web tests |
// Top 10 exceptions
AppExceptions | summarize count() by ExceptionType, OuterMessage | order by count_ desc | take 10
// Request volume over time
AppRequests | summarize count() by bin(TimeGenerated, 15m) | order by TimeGenerated asc
// Failure rate
AppRequests | summarize total=count(), failRate=countif(Success==false)*100.0/count()
// Distributed trace
union AppRequests, AppDependencies, AppTraces, AppExceptions
| where OperationId == "your-operation-id" | order by TimeGenerated asc
// Dependency analysis
AppDependencies | summarize avg(DurationMs), countif(Success==false)*100.0/count(), count()
by Name, DependencyType | order by count_ desc
20. Running the Application
cd AppInsights.Api
dotnet user-secrets set "Azure:TenantId" "<YOUR_TENANT_ID>"
dotnet user-secrets set "Azure:ClientId" "<YOUR_CLIENT_ID>"
dotnet user-secrets set "Azure:ClientSecret" "<YOUR_SECRET>"
dotnet user-secrets set "Azure:WorkspaceId" "<YOUR_WORKSPACE_ID>"dotnet run --project AppInsights.Apicd client-app && npm install && npm run dev21. Summary
| Area | What We Learned |
|---|---|
| Azure App Insights | All telemetry types, KQL, Log Analytics, smart detection |
| ASP.NET Core 8 | Service architecture, Azure.Monitor.Query SDK, JWT auth, Swagger |
| React 18+TS | TanStack Query, MSAL.js, Ant Design, Recharts, Monaco Editor |
| Security | Entra ID OAuth 2.0, JWT validation, query safety, CORS |
The Complete code for this article can be downloaded from this link.
Conclusion:
ASP.NET Core API In this article, we built a complete backend API that connects to Azure Application Insights using the Azure.Monitor.Query SDK. The API follows a clean three-layer architecture where Controllers handle HTTP routing and authentication, Services construct dynamic KQL queries with server-side pagination and safety validation, and Models define the data contracts shared with the frontend. Key patterns demonstrated include the Options pattern for configuration binding, ClientSecretCredential and DefaultAzureCredential for flexible Azure authentication across environments, Serilog for structured request logging, and JWT bearer token validation with Microsoft.Identity.Web for securing every endpoint with Entra ID. The KQL service validates all user queries against a blocklist of dangerous management commands before execution, ensuring that the interactive query editor cannot be used to modify or delete data. Every service method maps the loosely-typed LogsTableRow results from the Azure SDK into strongly-typed DTOs using null-safe extension methods, producing clean JSON responses that the React frontend can consume with full type safety.
React 18 Frontend The frontend demonstrates how to build an enterprise-grade, data-intensive monitoring dashboard using React 18, TypeScript, and modern libraries. The authentication layer implements the full OAuth 2.0 Authorization Code flow with PKCE using MSAL.js v3, with an Axios interceptor that silently acquires and attaches Bearer tokens to every API call. TanStack Query manages all server state with automatic caching, background refetching, conditional execution, and 30-second polling for the overview dashboard, eliminating the need for manual useState/useEffect data fetching patterns. The UI is composed of reusable components built with Ant Design and Recharts that are shared across eight dashboard pages covering logs, traces, exceptions, metrics, dependencies, availability tests, and an interactive KQL query editor powered by Monaco Editor with saved queries, Ctrl+Enter execution, and CSV export. This full-stack project gives developers and teams complete control over how they monitor, visualize, and analyze their application telemetry from Azure Application Insights, and serves as a foundation that can be extended with SignalR real-time updates, role-based access control, Azure Alerts integration, and MCP support for AI-powered observability.