Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure API Management Diagnostic Terraform Module

Configures the service-wide diagnostic on an Azure API Management service β€” which logger receives telemetry, how much is sampled, and how much of each message is captured. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • πŸ”­ Creates one azurerm_api_management_diagnostic β€” the logging configuration that applies to every API in the service.
  • 🎚️ Controls sampling, verbosity, correlation protocol, client-IP recording and the four pipeline stages (frontend/backend, request/response).
  • πŸ›‘οΈ Defaults log_client_ip to false β€” the closed choice for a field that records personal data β€” and leaves body logging off.
  • 🚨 Surfaces the two settings the provider silently discards: sampling_percentage = 0 and always_log_errors = false.
  • 🧯 States what data masking cannot do: it names headers and query parameters, and never reaches a captured body.
  • 🏷️ Carries no tags β€” the resource has none. The universal tail is timeouts only.

πŸ’‘ Why it matters: this one record decides how much of every API call is written to a telemetry store. Two of its settings do nothing when you set them, one of its enums is lower-case while its neighbours are not, and the redaction feature does not cover the field most likely to hold customer data. All four are invisible in a plan.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  apim["terraform-azurerm-api-management"]
  logger["terraform-azurerm-api-management-logger"]
  this["terraform-azurerm-api-management-diagnostic"]
  api["terraform-azurerm-api-management-api"]
  mds["terraform-azurerm-monitor-diagnostic-setting"]
  appi["terraform-azurerm-application-insights"]
  ehns["terraform-azurerm-eventhub-namespace"]

  rg -->|"name"| apim
  apim -->|"name"| logger
  apim -->|"name"| this
  logger -->|"id"| this
  logger -->|"id, per-API override"| api
  api -->|"overrides this record for its own API"| this
  appi -->|"sink behind an applicationinsights logger"| logger
  ehns -->|"sink behind an eventhub logger"| logger
  mds -->|"routes the gateway logs the azuremonitor identifier fills"| apim

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#002b4d,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class apim keystone;
  class rg,logger,api,mds,appi,ehns sib;
Loading

The API Management family is large, so this diagram names only this module's direct neighbours rather than every api-management-* module in the library. Note the edge from the API module back to this one: an API carrying its own diagnostic overrides what is configured here, and nothing in this record lists which APIs those are.


🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs"]
    ident["identifier plus api_management_name plus resource_group_name"]
    lg["api_management_logger_id, not force-new"]
    coll["sampling_percentage, always_log_errors, verbosity, log_client_ip"]
    stages["frontend_request, frontend_response, backend_request, backend_response"]
  end

  this["azurerm_api_management_diagnostic.this"]

  subgraph outputs["Outputs"]
    oid["id, identifier, api_management_logger_id"]
    oset["sampling_percentage, verbosity, log_client_ip, operation_name_format"]
    oexp["logs_message_bodies, headers_logged_by_stage, masked_entries_by_stage"]
    ofact["sampling_percentage_zero_is_silently_ignored, this_is_not_an_audit_trail"]
  end

  ident --> this
  lg --> this
  coll --> this
  stages -->|"data masking reaches headers and query params, never a body"| this
  this --> oid
  this --> oset
  this --> oexp
  this --> ofact

  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b9c7d6,color:#1b2a3a;
  class this me;
  class ident,lg,coll,stages,oid,oset,oexp,ofact sib;
Loading

Resource inventory

Resource Count Notes
azurerm_api_management_diagnostic.this 1 The keystone, and the only resource. Renders up to four pipeline blocks, each with nested data-masking rules.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures provider "azurerm" { features {} }, auth and subscription

Schema notes that bite

  • sampling_percentage = 0 is silently discarded. Microsoft documents 0 as meaningful β€” "0% reflects no logging" β€” and the provider's create-and-update path reads the field with a helper that treats zero as absent, so nothing is sent and Azure keeps what it had. The API-level sibling resource reads the raw configuration and does send zero.
  • always_log_errors = false is never sent. The flag is transmitted only when present and true, and the field is Computed, so a configuration saying false against a resource that has it on shows no diff, indefinitely.
  • verbosity is lower case β€” verbose, information, error β€” while http_correlation_protocol (None, Legacy, W3C) and operation_name_format (Name, Url) are capitalised. Url carries a single capital; URL is rejected.
  • operation_name_format is legal only on the applicationinsights identifier, enforced by the provider in a diff-time check that runs at plan and therefore needs credentials. This module refuses the combination at plan, offline.
  • identifier, resource_group_name and api_management_name are force-new; the last two take that from the provider's shared schema helpers rather than from this resource's own field list. api_management_logger_id is not force-new β€” the sink can be repointed in place.
  • Nine fields are Optional and Computed, including all four pipeline blocks. Leaving one null adopts Azure's current value into state rather than clearing it, which is how an out-of-band change is absorbed instead of reported.
  • data_masking covers headers and query parameters only. There is no body rule, no field path and no pattern matching.
  • No tags, no location. The universal tail is timeouts only.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Microsoft.ApiManagement/service/diagnostics/* β€” write and delete, scoped to the parent API Management service. A custom role at the service is enough; API Management Service Contributor on the service, or Contributor on its resource group, both cover it more broadly than necessary.
  • Microsoft.ApiManagement/service/read on the service, for the existence check the provider performs before creating.
  • Microsoft.ApiManagement/service/loggers/read is not required β€” the logger is referenced by ID and never read, which is also why nothing here validates that the logger suits the identifier.

πŸ”’ This resource holds no credential, so plan access here is not credential access. The credential lives on the logger; see that module's permissions section.


Azure Prerequisites

  • An existing API Management service.
  • An existing logger on that same service, of a kind that matches the identifier. Nothing validates the pairing β€” an applicationinsights diagnostic pointed at an Event Hub logger applies cleanly and delivers nothing.
  • For the azuremonitor identifier, Azure Monitor diagnostic settings on the API Management service that route GatewayLogs somewhere. This record decides what the gateway log contains, not where it goes.
  • The caller configures provider "azurerm" { features {} }, authentication and subscription.

πŸ“ Module Structure

terraform-azurerm-api-management-diagnostic/
β”œβ”€β”€ providers.tf    # required_version + pinned azurerm; no provider block
β”œβ”€β”€ variables.tf    # deeply-typed inputs; every enum and range mirrored from the provider
β”œβ”€β”€ main.tf         # the single keystone resource and its four pipeline blocks
β”œβ”€β”€ outputs.tf      # id first, then settings, exposure posture, and the silent behaviours
β”œβ”€β”€ README.md       # this document
β”œβ”€β”€ SCOPE.md        # the cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore      # the canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "apim_diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id
}

ℹ️ The caller configures the provider, including the mandatory features {} block. This module declares none.

πŸ”’ This call captures no message bodies and records no client IP addresses. Everything else is left to Azure's own values rather than being chosen for you.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
api_management_name string terraform-azurerm-api-management output name
resource_group_name string terraform-azurerm-resource-group output name
api_management_logger_id string terraform-azurerm-api-management-logger output id

Emits

Output Consumed by
id review; a management lock, where destroy protection is wanted
logs_message_bodies, stages_logging_message_bodies data-protection review and alerting
logged_headers_that_commonly_carry_credentials, masked_entries_by_stage control review
sampling_percentage_zero_is_silently_ignored troubleshooting a sampling change that did nothing
verbosity trace policy review β€” a policy below this severity emits nothing

πŸ“š Example Library

1 Β· Minimal Application Insights diagnostic
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id
}

πŸ’‘ Everything except log_client_ip is left to Azure. The module sends log_client_ip = false explicitly, because that field is the one on this resource that distinguishes "set to false" from "not set".

2 Β· Full sampling with errors always logged
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  sampling_percentage = 100
  always_log_errors   = true
  verbosity           = "information"
}

⚠️ Microsoft's published load testing puts full logging at a 40%–50% reduction in throughput above 1,000 requests per second. always_log_errors is the cheap half of this: it exempts errors from sampling without logging everything.

3 Β· Low sampling on a high-volume gateway
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  sampling_percentage = 5
  always_log_errors   = true
  verbosity           = "error"
}

⚠️ Sampling discards requests silently and by design. Microsoft: Application Insights "is not intended to be an audit system". Where an evidential record of API access is required, it does not come from here.

ℹ️ verbosity = "error" also gates trace policies β€” a policy written at information severity emits nothing while this is set, with no error on either side.

4 Β· W3C trace correlation to backends
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  http_correlation_protocol = "W3C"
}

πŸ’‘ W3C is what modern OpenTelemetry SDKs speak. Legacy leaves the trace broken at the gateway boundary β€” both halves work, they simply never correlate, and nothing reports the mismatch.

5 Β· Logging selected headers, with the credential masked
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  frontend_request = {
    headers_to_log = ["Content-Type", "Accept", "Ocp-Apim-Subscription-Key"]

    data_masking = {
      headers = [
        { mode = "Mask", value = "Ocp-Apim-Subscription-Key" },
      ]
    }
  }
}

πŸ”’ The subscription key is logged and masked. Logging it without the masking rule writes the credential into telemetry in clear β€” logged_headers_that_commonly_carry_credentials exists to find exactly that combination.

6 Β· Masking a query parameter
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  frontend_request = {
    data_masking = {
      query_params = [
        { mode = "Hide", value = "access_token" },
        { mode = "Mask", value = "account_id" },
      ]
    }
  }
}

ℹ️ Mask replaces the value; Hide removes the entry entirely. The published documentation restricts Hide to query parameters, while the provider accepts it on headers too β€” this module reports such entries through header_masking_entries_using_hide_mode rather than refusing them.

7 Β· Capturing request and response bodies
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  frontend_request  = { body_bytes = 1024 }
  frontend_response = { body_bytes = 1024 }
}

πŸ”’ Data masking does not reach a body. Whatever these 1024 bytes contain is stored verbatim β€” on a financial API that means account identifiers, balances and personal detail. Microsoft's own portal guidance adds that overriding the default of 0 "might significantly decrease the performance of your APIs". Treat this as a change with a named owner.

8 Β· All four pipeline stages
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  frontend_request  = { headers_to_log = ["Content-Type"] }
  frontend_response = { headers_to_log = ["Content-Type"] }

  backend_request = {
    headers_to_log = ["Content-Type", "Authorization"]
    data_masking   = { headers = [{ mode = "Mask", value = "Authorization" }] }
  }

  backend_response = { headers_to_log = ["Content-Type"] }
}

ℹ️ The backend pair is recorded as dependency telemetry, not as requests β€” a query written over requests will not see it. The backend request also commonly carries a credential the caller never sent, attached by policy, which is why its masking rule differs from the frontend's.

9 Β· The azuremonitor diagnostic
module "gateway_logs" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "azuremonitor"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  sampling_percentage = 100
  always_log_errors   = true
  verbosity           = "information"
}

⚠️ operation_name_format is not legal here β€” it belongs to the applicationinsights identifier only, and this module rejects the combination at plan.

ℹ️ API Management enforces a 32 KB ceiling on an Azure Monitor log entry and, above it, "trims the entry by removing all body and trace content". A body-logging configuration honoured on the Application Insights path can be silently dropped on this one.

10 Β· Both diagnostics on one service
module "logger_appi" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-logger.git?ref=v1.0.0"

  name                = "appinsights"
  resource_group_name = "rg-platform-apim"
  api_management_name = "apim-platform-prod"

  application_insights = {
    connection_string  = data.azurerm_application_insights.platform.connection_string
    identity_client_id = "SystemAssigned"
  }
}

module "logger_stream" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-logger.git?ref=v1.0.0"

  name                = "eventhub-audit"
  resource_group_name = "rg-platform-apim"
  api_management_name = "apim-platform-prod"

  eventhub = {
    name         = "apim-audit"
    endpoint_uri = "contoso-events.servicebus.windows.net"
  }
}

module "diagnostic_appi" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.logger_appi.id
  sampling_percentage      = 10
  operation_name_format    = "Name"
}

module "diagnostic_monitor" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "azuremonitor"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.logger_stream.id
  sampling_percentage      = 100
}

πŸ’‘ A service holds at most two of these records, one per identifier, and they are fully independent β€” different loggers, different sampling, different settings. Running both is two instances of this module.

11 Β· Explicitly recording client IP addresses
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  log_client_ip = true
}

πŸ”’ A client IP address is identifying information in most privacy regimes, and once in a telemetry store it inherits that store's retention, access model and export paths β€” none of which this module can see. Defensible for abuse investigation on an externally exposed gateway; it should be a decision with an owner rather than a default, which is why this module's default is false.

12 Β· Custom timeouts
module "diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = "rg-platform-apim"
  api_management_name      = "apim-platform-prod"
  api_management_logger_id = module.apim_logger.id

  timeouts = {
    create = "45m"
    read   = "10m"
    update = "45m"
    delete = "45m"
  }
}

⚠️ A key that is not create, read, update or delete is silently discarded by Terraform's type conversion β€” no error, and no timeout.

13 Β· Reviewing the exposure posture
output "gateways_logging_bodies" {
  value = [for k, m in module.diagnostics : k if m.logs_message_bodies]
}

output "credential_headers_logged" {
  value = {
    for k, m in module.diagnostics : k => m.logged_headers_that_commonly_carry_credentials
  }
}

output "sampling_changes_that_did_nothing" {
  value = [for k, m in module.diagnostics : k if m.sampling_percentage_zero_is_silently_ignored]
}

πŸ’‘ The third output finds the specific mistake this resource makes easy: someone set sampling to zero to stop telemetry, the plan showed the change, and Azure kept logging at whatever rate it already had.

14 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-platform-apim"
  location = "eastus2"
}

module "logs" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"

  name                = "log-platform-apim"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

module "app_insights" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-application-insights.git?ref=v1.0.0"

  name                = "appi-platform-apim"
  resource_group_name = module.rg.name
  location            = module.rg.location
  workspace_id        = module.logs.id
}

module "apim" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management.git?ref=v1.0.0"

  name                = "apim-platform-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
  publisher_name      = "Platform Engineering"
  publisher_email     = "platform-engineering@example.com"
  sku_name            = "Developer_1"
}

# The application-insights module deliberately does not emit the connection
# string, so the configuration that needs it declares its own data source.
data "azurerm_application_insights" "platform" {
  name                = module.app_insights.name
  resource_group_name = module.app_insights.resource_group_name
}

module "apim_logger" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-logger.git?ref=v1.0.0"

  name                = "appinsights"
  resource_group_name = module.rg.name
  api_management_name = module.apim.name
  resource_id         = module.app_insights.id

  application_insights = {
    connection_string  = data.azurerm_application_insights.platform.connection_string
    identity_client_id = "SystemAssigned"
  }
}

module "apim_diagnostic" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-api-management-diagnostic.git?ref=v1.0.0"

  identifier               = "applicationinsights"
  resource_group_name      = module.rg.name
  api_management_name      = module.apim.name
  api_management_logger_id = module.apim_logger.id

  sampling_percentage       = 10
  always_log_errors         = true
  verbosity                 = "information"
  http_correlation_protocol = "W3C"
  operation_name_format     = "Name"

  frontend_request = {
    headers_to_log = ["Content-Type", "Ocp-Apim-Subscription-Key"]
    data_masking   = { headers = [{ mode = "Mask", value = "Ocp-Apim-Subscription-Key" }] }
  }
}

πŸ”’ No body is captured, no client IP is recorded, and the one credential-bearing header that is logged is masked. operation_name_format = "Name" rather than "Url" also keeps path-embedded identifiers out of the operation name, where no masking rule reaches them.

⚠️ The API Management service must have a system-assigned identity holding Monitoring Metrics Publisher on the Application Insights component for the logger's identity_client_id = "SystemAssigned" to work. That identity is configured on the api-management module, and the role assignment belongs to terraform-azurerm-role-assignments.


πŸ“₯ Inputs

Group Variables
Identity (force-new) identifier, resource_group_name, api_management_name
The sink (mutable) api_management_logger_id
Collection settings sampling_percentage, always_log_errors, verbosity, log_client_ip, http_correlation_protocol, operation_name_format
Pipeline stages frontend_request, frontend_response, backend_request, backend_response
Universal tail timeouts
Full input schemas
identifier               = string   # force-new; "applicationinsights" or "azuremonitor", lower case
resource_group_name      = string   # force-new; the SERVICE's resource group
api_management_name      = string   # force-new; the service NAME, not its id
api_management_logger_id = string   # NOT force-new; the logger's full Resource ID

sampling_percentage       = optional(number)          # 0.0-100.0; 0 is accepted and then discarded
always_log_errors         = optional(bool)            # false is never sent
verbosity                 = optional(string)          # "verbose" | "information" | "error"  (lower case)
log_client_ip             = optional(bool, false)     # closed default; personal data
http_correlation_protocol = optional(string)          # "None" | "Legacy" | "W3C"
operation_name_format     = optional(string)          # "Name" | "Url"; applicationinsights only

# All four stages share this shape:
frontend_request = optional(object({
  body_bytes     = optional(number)             # 0-8192; NOT covered by data_masking
  headers_to_log = optional(set(string), [])    # bare header names, no wildcards
  data_masking = optional(object({
    headers      = optional(list(object({ mode = string, value = string })), [])
    query_params = optional(list(object({ mode = string, value = string })), [])
  }))
}))

timeouts = optional(object({ create, read, update, delete }))

See variables.tf for the full descriptions and every validation, including the exact casing of each enum.


🧾 Outputs

Output Description Notes
id Resource ID of the API Management diagnostic Emitted first; primary cross-module reference
identifier applicationinsights or azuremonitor Also the record's name
api_management_name Name of the parent API Management service Rebuilt from the Resource ID on read
resource_group_name Resource group holding the parent service Rebuilt from the Resource ID on read
api_management_logger_id The logger this diagnostic publishes to Mutable; not force-new
sampling_percentage The sampling rate Azure holds Computed
always_log_errors Whether errors bypass sampling, as Azure holds it Computed; may disagree with the configuration
verbosity verbose, information or error Computed; gates trace policies
log_client_ip Whether the caller's IP is recorded Defaults to false in this module
http_correlation_protocol None, Legacy or W3C Computed
operation_name_format Name, Url, or an empty string when unset Provider sets an empty string, not null
body_bytes_by_stage Payload bytes captured at each of the four stages Derived
stages_logging_message_bodies The stages that capture bodies at all Derived
logs_message_bodies True when any stage captures a body Derived; the flag to alert on
data_masking_does_not_apply_to_bodies Constant true
headers_logged_by_stage The header names captured at each stage Derived
logged_headers_that_commonly_carry_credentials Logged headers that usually hold a credential Derived
masked_entries_by_stage The names redacted at each stage, with the mode Derived
header_masking_entries_using_hide_mode Header rules using Hide Derived; reported, not refused
sampling_percentage_zero_is_silently_ignored True when 0 was configured and discarded Derived
always_log_errors_false_is_never_sent Constant true
an_api_level_diagnostic_overrides_this_one Constant true
this_is_not_an_audit_trail Constant true
full_logging_costs_throughput Constant true
azure_monitor_entries_over_32kb_lose_their_bodies True on the azuremonitor identifier Derived
only_two_diagnostics_can_exist_per_service Constant true
the_logger_kind_is_never_checked_against_the_identifier Constant true
repointing_the_logger_is_an_in_place_update Constant true
force_new_fields The fields whose change destroys and recreates the record
computed_fields_that_mask_drift The Optional+Computed fields that absorb out-of-band change
this_resource_supports_no_azure_resource_tags Constant true

Nothing here is a secret: a diagnostic holds no credential.


🧠 Architecture Notes

Two of these settings do nothing when you set them. sampling_percentage = 0 is read with a helper that treats zero as absent, so no sampling settings are sent and Azure keeps what it had β€” despite Microsoft documenting 0 as "no logging". always_log_errors = false is dropped before the request is built, and because the field is Computed the resulting no-op shows no diff forever. Both are reported through outputs because neither is visible in a plan.

The same field behaves differently on the API-level sibling. azurerm_api_management_api_diagnostic reads sampling_percentage from the raw configuration and does send zero. Two resources that read identically in the documentation disagree on this exact value.

Data masking is narrower than it looks. A masking rule names a header or a query parameter. A body captured by body_bytes is stored verbatim β€” there is no body rule, no field path and no pattern matching. A configuration that masks the subscription key and then logs 8192 bytes of a response has protected the metadata and published the record.

This record is a default, not a floor. Microsoft: "By default, the single API logger (more granular level) overrides the one for all APIs." An API carrying its own diagnostic ignores everything here, and nothing in this resource, its state or the portal lists which APIs those are.

Nine fields are Optional and Computed. Leaving one null adopts Azure's value rather than clearing it, which is convenient until someone changes a sampling rate or a client-IP setting in the portal and no plan ever mentions it.

The logger pairing is unchecked. The ID does not encode the logger's type, so nothing β€” not the provider, not this module β€” can confirm that an applicationinsights diagnostic points at an Application Insights logger. A mismatch applies cleanly and delivers nothing.

Only the sink is mutable. api_management_logger_id updates in place; the identifier, the service name and the resource group all force replacement.


🧱 Design Principles

Concern Default in this module Opt-out
Client IP recording log_client_ip = false β€” the closed choice for a field holding personal data, sent explicitly rather than omitted set log_client_ip = true
Message bodies not captured β€” all four stage blocks are null set body_bytes on a stage
Header logging none β€” headers_to_log is empty on every stage list header names per stage
Sampling and verbosity left null so Azure's own value stands; there is no defensible default rate, and inventing one would be a silent policy decision set sampling_percentage / verbosity
Hide on header masking rules reported through an output, not refused β€” the provider accepts it while the documentation restricts it to query parameters, and a validation failure would block terraform destroy too none
Enum casing every enum is enforced exactly as the provider compares it, with the casing named in the error message none
Tagging no tags variable β€” the resource exposes none tag the parent API Management service

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module by immutable tag β€” ?ref=v1.0.0 β€” never a branch. This module is plan-only from the library's point of view: a human applies from CI against a reviewed plan.


πŸ§ͺ Testing

terraform validate covers every enum with its exact casing, the sampling_percentage and body_bytes ranges, the logger ID shape, the header-name shape, and the operation_name_format / identifier pairing. That last one is worth noting: the provider enforces it in a diff-time check, which runs at plan and therefore needs credentials β€” this module refuses it offline instead.

terraform console fires the same validations against a .tfvars file. Terraform skips a validation whose referenced variable has already failed, so a short error list is not proof a check is missing β€” fix the first failure and re-run.

What only a real plan or apply reaches: whether the service exists, whether the logger ID resolves, and whether the identifier already has a diagnostic. What nothing reaches, at any stage: whether the logger is of a kind that can receive this telemetry, and whether any API has overridden this configuration with its own.


πŸ’¬ Example Output

id                                             = "/subscriptions/.../resourceGroups/rg-platform-apim/providers/Microsoft.ApiManagement/service/apim-platform-prod/diagnostics/applicationinsights"
identifier                                     = "applicationinsights"
api_management_name                            = "apim-platform-prod"
resource_group_name                            = "rg-platform-apim"
api_management_logger_id                       = "/subscriptions/.../service/apim-platform-prod/loggers/appinsights"
sampling_percentage                            = 10
always_log_errors                              = true
verbosity                                      = "information"
log_client_ip                                  = false
http_correlation_protocol                      = "W3C"
operation_name_format                          = "Name"
logs_message_bodies                            = false
stages_logging_message_bodies                  = []
headers_logged_by_stage                        = {
  "backend_request"   = []
  "backend_response"  = []
  "frontend_request"  = ["Content-Type", "Ocp-Apim-Subscription-Key"]
  "frontend_response" = []
}
logged_headers_that_commonly_carry_credentials = {
  "backend_request"   = []
  "backend_response"  = []
  "frontend_request"  = ["Ocp-Apim-Subscription-Key"]
  "frontend_response" = []
}
sampling_percentage_zero_is_silently_ignored   = false
force_new_fields                               = [
  "identifier",
  "resource_group_name",
  "api_management_name",
]
this_is_not_an_audit_trail                     = true

πŸ” Troubleshooting

Symptom Cause Fix
Sampling was set to 0 and telemetry keeps arriving The provider treats a zero here as absent and sends no sampling settings at all, so Azure keeps its previous value Destroy the diagnostic to stop telemetry. sampling_percentage_zero_is_silently_ignored reports the condition
always_log_errors = false produces no diff and no change The flag is only transmitted when present and true, and the field is Computed There is no way to turn the flag off through this field. Recreate the diagnostic without it
Plan rejects verbosity = "Verbose" This enum is lower case, unlike its two neighbours on the same resource Use "verbose", "information" or "error"
Plan rejects operation_name_format = "URL" The SDK constant is Url, with a single capital, and the comparison is case-sensitive Use "Url"
Plan rejects operation_name_format on an azuremonitor diagnostic It is legal only on applicationinsights. The provider raises the same error at plan; this module raises it offline Remove the field, or move it to the applicationinsights diagnostic
Everything applies and no telemetry arrives The logger does not exist, is of the wrong kind for this identifier, or its credential is wrong. None of these is checked here Confirm the logger resolves and matches the identifier; check the logger module's own outputs
Telemetry arrives for some APIs and not others An API carries its own diagnostic, which overrides this record Check each API's own diagnostic. Multiplexing both levels requires different loggers and a Microsoft support request
Bodies are configured but never appear on the azuremonitor path API Management enforces a 32 KB ceiling per entry and trims body and trace content above it Reduce what else is logged, or use the applicationinsights identifier for body capture
A setting changed in the portal never appears as drift The field is Optional and Computed, so Azure's value is adopted into state See computed_fields_that_mask_drift; set the field explicitly to assert a value
A trace policy emits nothing Its severity is below this diagnostic's verbosity Raise the policy's severity, or lower verbosity

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."