Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions run-samples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ SAMPLES=(
"samples/web-app-mysql-flexible-server/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-postgresql-flexible-server/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-postgresql-flexible-server/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-app-configuration/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-app-configuration/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-app-configuration/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh && bash scripts/query-logs-and-metrics.sh"
"samples/web-app-app-configuration/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh && bash scripts/query-logs-and-metrics.sh"
"samples/web-app-custom-image/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/web-app-custom-image/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh"
"samples/aci-blob-storage/python|bash scripts/deploy.sh|bash scripts/validate.sh"
Expand Down Expand Up @@ -78,8 +78,8 @@ TERRAFORM_SAMPLES=(
"samples/web-app-mysql-flexible-server/dotnet/terraform|bash deploy.sh"
"samples/web-app-postgresql-flexible-server/python/terraform|bash deploy.sh"
"samples/web-app-postgresql-flexible-server/dotnet/terraform|bash deploy.sh"
"samples/web-app-app-configuration/python/terraform|bash deploy.sh"
"samples/web-app-app-configuration/dotnet/terraform|bash deploy.sh"
"samples/web-app-app-configuration/python/terraform|bash deploy.sh|bash ../scripts/query-logs-and-metrics.sh"
"samples/web-app-app-configuration/dotnet/terraform|bash deploy.sh|bash ../scripts/query-logs-and-metrics.sh"
"samples/web-app-custom-image/python/terraform|bash deploy.sh|bash ../scripts/validate.sh && bash ../scripts/call-web-app.sh"
"samples/web-app-custom-image/dotnet/terraform|bash deploy.sh|bash ../scripts/validate.sh && bash ../scripts/call-web-app.sh"
"samples/aci-blob-storage/python/terraform|bash deploy.sh"
Expand Down Expand Up @@ -108,8 +108,8 @@ BICEP_SAMPLES=(
"samples/web-app-mysql-flexible-server/dotnet/bicep|bash deploy.sh"
"samples/web-app-postgresql-flexible-server/python/bicep|bash deploy.sh"
"samples/web-app-postgresql-flexible-server/dotnet/bicep|bash deploy.sh"
"samples/web-app-app-configuration/python/bicep|bash deploy.sh"
"samples/web-app-app-configuration/dotnet/bicep|bash deploy.sh"
"samples/web-app-app-configuration/python/bicep|bash deploy.sh|bash ../scripts/query-logs-and-metrics.sh"
"samples/web-app-app-configuration/dotnet/bicep|bash deploy.sh|bash ../scripts/query-logs-and-metrics.sh"
"samples/web-app-custom-image/python/bicep|bash deploy.sh|bash ../scripts/validate.sh && bash ../scripts/call-web-app.sh"
"samples/web-app-custom-image/dotnet/bicep|bash deploy.sh|bash ../scripts/validate.sh && bash ../scripts/call-web-app.sh"
"samples/aci-blob-storage/python/bicep|bash deploy.sh"
Expand Down
114 changes: 114 additions & 0 deletions samples/web-app-app-configuration/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,120 @@ Loaded 5 settings from App Configuration https://local-appconfig-test.azure.loca
PostgreSQL schema initialized
```

## Logs and metrics

The web app, its App Service plan, the App Configuration store, the key vault, the PostgreSQL server, the VNet and the two NSGs each have a diagnostic setting that sends their resource logs, their platform metrics (`AllMetrics`) or both to the `<prefix>-log-analytics-<suffix>` Log Analytics workspace. The four resources the application uses record what it does:

| Resource | Log categories | Tables in the workspace | What the application leaves there |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Web App | `AppServiceHTTPLogs`, `AppServiceConsoleLogs`, `AppServicePlatformLogs`, `AppServiceAppLogs`, `AppServiceAuditLogs`, `AppServiceIPSecAuditLogs`, `AppServiceAuthenticationLogs` | One table per category, named after it | The requests it serves, its console output (a `VacationPlanner.Requests` line per request and the application's log lines) and the start of its container |
| App Configuration store | `HttpRequest`, `Audit` | `AACHttpRequest`, `AACAudit` | The `GET /kv?key=PG_*` with which the application loads its settings, made by its managed identity |
| Key Vault | `AuditEvent`, `AzurePolicyEvaluationDetails` | `AZKVAuditLogs`, `AZKVPolicyEvaluationDetailsLogs` | The `SecretGet` of `pg-user` and `pg-password` that resolves the two Key Vault references |
| PostgreSQL flexible server | `PostgreSQLLogs` | `PGSQLServerLogs` | The connections of the `testuser` role |

Their platform metrics land in the `AzureMetrics` table. The settings of the key vault and of the PostgreSQL server write to the resource-specific tables, which Microsoft recommends over the shared `AzureDiagnostics` table: the Azure CLI script passes `--export-to-resource-specific true`, the Bicep modules set `logAnalyticsDestinationType: 'Dedicated'` and the Terraform modules `log_analytics_destination_type = "Dedicated"`. App Service and App Configuration write to resource-specific tables only. The App Service plan sends its metrics too, and the VNet and the two NSGs their network logs; on the emulator the network categories stay empty, because no traffic crosses an emulated network security group.

### Azure CLI

The query command comes from the `log-analytics` extension of the Azure CLI:

```bash
az extension add --name log-analytics
```

The query API identifies a workspace by its workspace id (`customerId`):

```bash
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--resource-group local-rg \
--workspace-name local-log-analytics-test \
--query customerId \
--output tsv)

az monitor log-analytics query \
--workspace "$WORKSPACE_ID" \
--analytics-query "AppServiceHTTPLogs | project TimeGenerated, CsMethod, CsUriStem, ScStatus, TimeTaken | order by TimeGenerated desc | take 5" \
--output table
```

```text
CsMethod CsUriStem ScStatus TableName TimeGenerated TimeTaken
---------- ----------- ---------- ------------- --------------------------- -----------
GET / 200 PrimaryResult 2026-11-06T16:05:45.368091Z 51
GET / 200 PrimaryResult 2026-11-06T16:05:45.283727Z 51
GET / 200 PrimaryResult 2026-11-06T16:05:45.188826Z 55
...
```

On Azure the table also records the requests of the zip deployment to the app's Kudu site, such as `POST /api/publish` and `GET /api/deployments/latest`. The other tables answer the same way. A few queries to try:

```kusto
// The line the application writes once it has loaded its settings
AppServiceConsoleLogs
| where ResultDescription has "App Configuration"
| project TimeGenerated, ResultDescription

// The requests that loaded the settings, with the principal id of the web app's managed identity (ClientObjectId)
AACHttpRequest
| project TimeGenerated, Method, StatusCode, HitCount, ClientObjectId, RequestURI

// The reads of the two secrets behind the Key Vault references
AZKVAuditLogs
| where OperationName == "SecretGet"
| project TimeGenerated, OperationName, ResultSignature, RequestUri

// The connections of the application role (on Azure the server also logs those of its maintenance role, azuresu)
PGSQLServerLogs
| where Message has "connection authorized" and Message has "testuser"
| project TimeGenerated, Message

// The requests per minute, which the Logs page of the portal draws as a chart
AppServiceHTTPLogs
| summarize Requests = count() by bin(TimeGenerated, 1m)
| render columnchart
```

The platform metrics need no diagnostic setting: the Azure Monitor metrics API serves them directly.

```bash
WEB_APP_ID=$(az webapp show \
--name local-webapp-test \
--resource-group local-rg \
--query id \
--output tsv)

az monitor metrics list \
--resource "$WEB_APP_ID" \
--metrics Requests Http2xx BytesSent \
--interval PT1M \
--aggregation Total \
--query "value[].{Metric:name.value,Total:sum(timeseries[].data[].total)}" \
--output table
```

```text
Metric Total
--------- --------
Requests 8.0
Http2xx 8.0
BytesSent 171720.0
```

[`scripts/query-logs-and-metrics.sh`](./scripts/query-logs-and-metrics.sh) does all of this in one go: it calls the web app, lists the diagnostic settings of the four resources, runs queries like these (plus the platform events of the web app and the exported `AzureMetrics`) until their records arrive, and prints the metrics of the web app, the store, the vault and the server. It exits with a non-zero code when the web app does not answer, or when a query still lacks its records after `RETRY_COUNT` attempts (30 by default) made `SLEEP` seconds apart (10 by default).

### Azure Portal Emulator

The emulator serves an emulated Azure portal at `http://localhost:4566/_localstack/portal/` (see [Azure Portal Emulator](https://docs.localstack.cloud/azure/developer-tools/portal/)):

1. Open the portal and select **Sign in**: the sign-in is a mock and asks for no credentials.
2. Select **Resource groups**, then `local-rg`, the `local-log-analytics-test` workspace and **Logs**. Type a query, for example the `render columnchart` query above, and select **Run**: the **Results** tab lists the records and the **Chart** tab draws them.
3. Open the `local-webapp-test` web app. Under **Monitoring**, **Metrics** charts its platform metrics: select the **Requests** metric, which the chart shows with its default **Sum** aggregation. **Logs** runs queries over the records of the web app only, and **Diagnostic settings** lists its diagnostic setting.
4. The App Configuration store, the key vault and the PostgreSQL server have the same **Metrics**, **Logs** and **Diagnostic settings** pages.

### Delivery times

On the emulator the records are queryable within seconds, and the aggregated ones (`AACHttpRequest` and `AzureMetrics`) once their one-minute window is over. On Azure the records take minutes, and the first records of a new diagnostic setting can take [up to 90 minutes](https://learn.microsoft.com/azure/azure-monitor/data-collection/diagnostic-settings#expected-latency-after-creating-a-diagnostic-setting): run the queries again later, or raise `RETRY_COUNT` for the script. A new setting can also take a while before it delivers anything, and on Azure the records of what the application did while it started, such as its start-up console lines or its secret reads in the key vault, can then be missing for good: once the workspace receives records, `az webapp restart --name local-webapp-test --resource-group local-rg` starts the application again, and the records of the new start arrive.

## Troubleshooting

- **The app logs `App Configuration load failed ... 403` or `Forbidden` and restarts.** The identity's role assignments have not propagated yet (up to 10 minutes on Azure), or one of them is missing. Check `az role assignment list --assignee <principalId> --all` for App Configuration Data Reader on the store and Key Vault Secrets User on the vault; the app keeps retrying and App Service restarts it until the load succeeds.
Expand Down
7 changes: 6 additions & 1 deletion samples/web-app-app-configuration/dotnet/bicep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The [`deploy.sh`](deploy.sh) script creates the resource group while the Bicep m
6. Three [Azure Private DNS Zones](https://learn.microsoft.com/azure/dns/private-dns-privatednszone) (`private-dns-zone.bicep`), `privatelink.postgres.database.azure.com`, `privatelink.azconfig.io` and `privatelink.vaultcore.azure.net`, each linked to the VNet as `link-to-vnet`.
7. Three [Azure Private Endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview) (`private-endpoint.bicep`), groups `postgresqlServer`, `configurationStores` and `vault`, each with a `default` DNS zone group.
8. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview) and one [Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview) per subnet (inside `virtual-network.bicep`).
9. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview) (`log-analytics.bicep`).
9. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview) (`log-analytics.bicep`). The modules of the web app, the plan, the store, the vault, the server and the VNet also deploy the diagnostic settings (named `default`) of their resources; the ones of the Key Vault and of the PostgreSQL server set `logAnalyticsDestinationType: 'Dedicated'`, so their logs land in the resource-specific tables `AZKVAuditLogs`, `AZKVPolicyEvaluationDetailsLogs` and `PGSQLServerLogs`.
10. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans) (`app-service-plan.bicep`).
11. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview) (`web-app.bicep`) with regional VNet integration into *app-subnet*, the user-assigned identity, and the app settings `Endpoints__AppConfiguration` (the store endpoint) and `AZURE_CLIENT_ID` (the identity client id) next to `LOGIN_NAME`, `WEBSITES_PORT` and the Oryx build flags. The template sets **no** `PG_*` app setting: the app loads the five settings from the store.

Expand Down Expand Up @@ -124,8 +124,13 @@ az keyvault secret list --vault-name local-keyvault-test --query "[].name" --out
az webapp config appsettings list --name local-webapp-test --resource-group local-rg \
--query "[?starts_with(name, 'PG_')].name" --output tsv
bash ../scripts/validate.sh

# read the logs and metrics the diagnostic settings sent to the workspace
bash ../scripts/query-logs-and-metrics.sh
```

See [Logs and metrics](../README.md#logs-and-metrics) for the queries the script runs and for the pages of the Azure Portal Emulator that show the same records.

`<port>` is `5432` in real Azure, or the port suffix of the server's FQDN in LocalStack:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ param metricCategories array = [
'AllMetrics'
]

@description('Specifies whether the retention policy of the diagnostic settings is enabled.')
param retentionPolicyEnabled bool = true

@description('Specifies the retention of the diagnostic settings in days (0 keeps the data as long as the workspace does).')
param retentionPolicyDays int = 0

@description('Specifies the resource tags.')
param tags object

Expand All @@ -88,20 +82,12 @@ var logs = [
for category in logCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]
var metrics = [
for category in metricCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,6 @@ param metricCategories array = [
'AllMetrics'
]

@description('Specifies whether the retention policy of the diagnostic settings is enabled.')
param retentionPolicyEnabled bool = true

@description('Specifies the retention of the diagnostic settings in days (0 keeps the data as long as the workspace does).')
param retentionPolicyDays int = 0

@description('Specifies the resource id of the Log Analytics workspace.')
param workspaceId string

Expand All @@ -116,20 +110,12 @@ var logs = [
for category in logCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]
var metrics = [
for category in metricCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,12 @@ param metricCategories array = [
'AllMetrics'
]

@description('Specifies whether the retention policy of the diagnostic settings is enabled.')
param retentionPolicyEnabled bool = true

@description('Specifies the retention of the diagnostic settings in days (0 keeps the data as long as the workspace does).')
param retentionPolicyDays int = 0
@description('Specifies where the diagnostic settings write the logs in the workspace: Dedicated writes each category to its resource-specific table (AZKVAuditLogs, AZKVPolicyEvaluationDetailsLogs), AzureDiagnostics to the shared AzureDiagnostics table.')
@allowed([
'AzureDiagnostics'
'Dedicated'
])
param logAnalyticsDestinationType string = 'Dedicated'

@description('Specifies the resource tags.')
param tags object
Expand All @@ -134,20 +135,12 @@ var logs = [
for category in logCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]
var metrics = [
for category in metricCategories: {
category: category
enabled: true
retentionPolicy: {
enabled: retentionPolicyEnabled
days: retentionPolicyDays
}
}
]

Expand Down Expand Up @@ -222,6 +215,7 @@ resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr
scope: keyVault
properties: {
workspaceId: workspaceId
logAnalyticsDestinationType: logAnalyticsDestinationType
logs: logs
metrics: metrics
}
Expand Down
Loading
Loading