Skip to content

FinOps

AlertHawk.FinOps is an ASP.NET Core service that analyzes Azure subscriptions for cost and utilization signals, persists results in SQL Server, optionally calls an external AI API for optimization recommendations, and exposes a REST API for dashboards and automation.

The solution is defined in AlertHawk.FinOps.slnx at the repository root of the FinOps folder. It includes:

ProjectRole
AlertHawk.FinOpsWeb API, Azure data collection, analysis orchestration, optional weekly scheduler
AlertHawk.FinOps.TestsUnit and integration tests

All controller routes are prefixed with /finops (see GlobalRoutePrefixConvention in the service). Example: /finops/api/Version. Most endpoints require JWT Bearer or Azure AD; exceptions are called out below.


Overview

  • Azure: Uses a service principal (Azure__*) to read subscriptions listed in Azure__SubscriptionIds (comma-separated). Typical roles include Reader on the subscription and Cost Management Reader where cost APIs are used.
  • SQL Server: Stores analysis runs, resource snapshots, cost details, historical cost rows, AI recommendations, and optional subscription metadata.
  • Analysis: On-demand (sync or async job) or weekly (UTC) via WeeklyAnalysis options for every configured subscription id.
  • AI: Optional; URL, key, and header name come from AI configuration (used when generating recommendations).
  • Observability: Sentry is wired in Program.cs; Swagger UI is enabled only when ASPNETCORE_ENVIRONMENT is Development.

Environment variables

Configuration is read from appsettings.json and environment variables. Nested keys use __ (double underscore), e.g. ConnectionStrings__SqlConnectionString.

General

VariableDescription
ASPNETCORE_ENVIRONMENTDevelopment enables Swagger / Swagger UI; use Production in deployed environments
DOTNET_SYSTEM_GLOBALIZATION_INVARIANTSet to false in Docker images that need full culture data (see service Dockerfile)

Database

VariableDescription
ConnectionStrings__SqlConnectionStringRequired. SQL Server connection string for FinOpsDbContext

On startup the app applies EF Core migrations when pending migrations exist; otherwise it may EnsureCreated when no migrations are present (see Program.cs).

Azure (service principal)

VariableDescription
Azure__TenantIdAzure AD tenant id
Azure__ClientIdApplication (client) id
Azure__ClientSecretClient secret
Azure__SubscriptionIdsComma-separated Azure subscription GUIDs to analyze

Weekly analysis (background)

VariableDescription
WeeklyAnalysis__Enabledtrue to run the hosted scheduler; false to disable
WeeklyAnalysis__DayOfWeekUtcDay name in English, e.g. Sunday
WeeklyAnalysis__HourUtc0–23 (UTC)
WeeklyAnalysis__MinuteUtc0–59 (UTC)

AI recommendations (optional)

VariableDescription
AI__ApiUrlHTTP endpoint for the recommendation agent
AI__ApiKeyAPI key value
AI__ApiKeyHeaderNameHeader name sent with the key (e.g. vendor-specific header)

Authentication

The API uses a combined default policy: an authenticated user via JWT Bearer (JwtBearer scheme) or Azure AD (AzureAd scheme from Microsoft.Identity.Web).

VariableDescription
Jwt__KeySymmetric key for JWT validation (replace insecure defaults in production)
Jwt__IssuersComma-separated valid issuers
Jwt__AudiencesComma-separated valid audiences
AzureAd__Instance, AzureAd__TenantId, AzureAd__ClientId, AzureAd__ClientSecret, …Standard Microsoft.Identity.Web / Azure AD app registration settings (see Authentication patterns in other services)

If Jwt__* or AzureAd__* values are missing, the application falls back to development placeholders in code; production deployments must set real secrets.

Sentry

VariableDescription
Sentry__Dsn, Sentry__Environment, Sentry__SendDefaultPii, …Sentry SDK options (see appsettings.json in the project for the full set used there)

API controllers

Base path: /finops/api. Unless noted, endpoints use [Authorize] and accept JWT Bearer or Azure AD tokens.

Version — /finops/api/Version

MethodRouteAuthDescription
GET/NoneReturns the entry assembly version string

Analysis — /finops/api/Analysis/*

MethodRouteDescription
POST/startBody: JSON string (Azure subscription id). Runs analysis synchronously; returns summary including AnalysisRunId
POST/start-asyncBody: JSON string (subscription id). Returns 202 Accepted with JobId; poll GET jobs/{jobId}
GET/jobs/{jobId}Job status for async analysis
POST/cleanupDeletes old analysis runs, keeping the latest run per subscription (and related rows)

Analysis runs — /finops/api/AnalysisRuns/*

MethodRouteDescription
GET/List analysis runs
GET/{id}Run by id
GET/latestLatest run
GET/latest-per-subscriptionLatest run per subscription
GET/subscription/{subscriptionId}Runs for a subscription
DELETE/{id}Delete a run

Subscription summaries (from runs) — /finops/api/Subscription/*

MethodRouteDescription
GET/Distinct subscriptions derived from stored analysis runs (name from latest run)

Subscriptions (CRUD metadata) — /finops/api/Subscriptions/*

MethodRouteDescription
GET/All Subscription rows
GET/{id}By primary key
GET/by-subscription-id/{subscriptionId}By Azure subscription id
POST/Create or update description metadata
PUT/{id}Update
DELETE/{id}Delete

Resources — /finops/api/Resources/*

MethodRouteDescription
GET/analysis/{analysisRunId}Resources for a run
GET/analysis/{analysisRunId}/type/{resourceType}Filter by resource type
GET/analysis/{analysisRunId}/resourcegroup/{resourceGroup}Filter by resource group
GET/analysis/{analysisRunId}/flagsResources with flags
GET/analysis/{analysisRunId}/summary/typesSummary by type
GET/analysis/{analysisRunId}/summary/resourcegroupsSummary by resource group
GET/analysis/{analysisRunId}/searchQuery: searchTerm — search name, group, or type

Cost details — /finops/api/CostDetails/*

MethodRouteDescription
GET/analysis/{analysisRunId}Cost lines for a run
GET/analysis/{analysisRunId}/type/{costType}Filter by cost type
GET/analysis/{analysisRunId}/top/{count}Top contributors
GET/analysis/{analysisRunId}/summary/resourcegroupsAggregated by resource group
GET/analysis/{analysisRunId}/summary/servicesAggregated by service

Historical costs — /finops/api/HistoricalCosts/*

MethodRouteDescription
GET/analysis/{analysisRunId}Historical cost rows for a run
GET/subscription/{subscriptionId}By subscription
GET/analysis/{analysisRunId}/daily-totalsDaily totals
GET/analysis/{analysisRunId}/by-resourcegroupBy resource group
GET/analysis/{analysisRunId}/by-serviceBy service
GET/analysis/{analysisRunId}/trendTrend payload for charts

Recommendations — /finops/api/Recommendations/*

MethodRouteDescription
GET/analysis/{analysisRunId}Recommendations for a run
GET/analysis/{analysisRunId}/latestLatest recommendations for that run context
GET/{id}/formattedSingle recommendation, formatted (e.g. markdown-friendly)
GET/List recommendations

Dashboard — /finops/api/Dashboard/*

MethodRouteDescription
GET/summaryAggregated dashboard summary
GET/cost-trendsCost trend data
GET/resource-distributionResource distribution
GET/optimization-opportunitiesOptimization-oriented summary

Helm chart reference

The main Helm chart deploys FinOps as finops-api: set finops-api.replicas and finops-api.env in values.yaml, and the container image under image.finops-api.

Configure SQL, JWT, Azure AD, Sentry, and Swagger credentials the same way as other AlertHawk APIs. Add Azure data collection (Azure__*), optional AI (AI__ApiUrl, AI__ApiKey, AI__ApiKeyHeaderName), and optional weekly runs (WeeklyAnalysis__*). Full variable list: Environment variables — finops-api.


Local development

From the repository folder that contains AlertHawk.FinOps.slnx:

bash
dotnet restore AlertHawk.FinOps/AlertHawk.FinOps.slnx
dotnet run --project AlertHawk.FinOps/AlertHawk.FinOps/AlertHawk.FinOps.csproj

With ASPNETCORE_ENVIRONMENT=Development, open Swagger at the URL shown in Properties/launchSettings.json (e.g. https://localhost:5001/swagger).

Run tests:

bash
dotnet test AlertHawk.FinOps/AlertHawk.FinOps.slnx

Further reading

AlertHawk - Self-hosted monitoring solution.