> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Azure Monitor

<View title="Developer">
  <View title="Federal docs">
    <Danger>
      **This integration is not available in federal environments**

      To learn more, read <a href="/docs/home/infrastructure/federal">LaunchDarkly in federal environments</a>.
    </Danger>
  </View>

  This topic explains how to stream Azure Monitor logs and metrics from your Azure subscription to LaunchDarkly observability features. This integration lets you monitor your Azure infrastructure alongside your feature flag data for unified observability.

  The integration uses Azure Event Hubs and a serverless Azure Function to forward diagnostic data to LaunchDarkly. An Azure Resource Manager (ARM) template automates the entire setup:

  1. Azure Diagnostic Settings stream logs and metrics from your resources to an Event Hub.
  2. The Azure Function triggers on each batch of events, separates logs from metrics, and forwards both as OpenTelemetry Protocol (OTLP) JSON to the LaunchDarkly OpenTelemetry collector.

  ## Prerequisites

  To use the Azure Monitor integration, you need:

  * A LaunchDarkly client-side ID for your target environment
  * An Azure subscription with permissions to create:
    * Event Hub namespaces
    * Function Apps
    * Storage accounts

  To find and copy your LaunchDarkly client-side ID, read [View or copy SDK credentials](/docs/home/account/environment/keys#view-or-copy-sdk-credentials).

  ## Deploy the ARM template

  The ARM template creates all required infrastructure and deploys the forwarding function automatically.

  To deploy the ARM template:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Search for **Deploy a custom template** and select it.
  3. Click **Build your own template in the editor**.
  4. Clear the default contents, then paste the ARM template below into the editor.
  5. Click **Save**.
  6. Select a **Resource group** or create a new one.
  7. Enter your LaunchDarkly environment's client-side ID into the **LaunchDarkly Client Side Id** parameter.
  8. Click **Review + create**, then click **Create**.

  The deployment takes 2-3 minutes. After it completes, the Event Hub and forwarding function are ready to receive data. Copy and save the **Event Hub namespace name** from the deployment outputs to use when you configure Diagnostic Settings.

  ### ARM template

  <Accordion title="Expand ARM template">
    <CodeGroup>
      ```json title="ARM template" expandable lines wrap theme={null}
      {
        "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
          "launchDarklyClientSideId": {
            "type": "string",
            "metadata": {
              "description": "Your LaunchDarkly environment client-side ID"
            }
          },
          "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {
              "description": "Azure region for all resources"
            }
          }
        },
        "variables": {
          "suffix": "[uniqueString(resourceGroup().id)]",
          "eventHubNamespaceName": "[format('ld-obs-{0}', variables('suffix'))]",
          "eventHubName": "launchdarkly-diagnostics",
          "storageAccountName": "[format('ldobs{0}', variables('suffix'))]",
          "functionAppName": "[format('ld-obs-func-{0}', variables('suffix'))]",
          "hostingPlanName": "[format('ld-obs-plan-{0}', variables('suffix'))]"
        },
        "resources": [
          {
            "type": "Microsoft.EventHub/namespaces",
            "apiVersion": "2022-10-01-preview",
            "name": "[variables('eventHubNamespaceName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard",
              "tier": "Standard",
              "capacity": 1
            }
          },
          {
            "type": "Microsoft.EventHub/namespaces/eventhubs",
            "apiVersion": "2022-10-01-preview",
            "name": "[format('{0}/{1}', variables('eventHubNamespaceName'), variables('eventHubName'))]",
            "dependsOn": ["[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"],
            "properties": {
              "messageRetentionInDays": 1,
              "partitionCount": 2
            }
          },
          {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2023-01-01",
            "name": "[variables('storageAccountName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard_LRS"
            },
            "kind": "StorageV2"
          },
          {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2023-01-01",
            "name": "[variables('hostingPlanName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Y1",
              "tier": "Dynamic"
            },
            "properties": {}
          },
          {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2023-01-01",
            "name": "[variables('functionAppName')]",
            "location": "[parameters('location')]",
            "kind": "functionapp",
            "dependsOn": [
              "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
              "[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"
            ],
            "properties": {
              "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "siteConfig": {
                "appSettings": [
                  {
                    "name": "AzureWebJobsStorage",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('functionAppName'))]"
                  },
                  {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~4"
                  },
                  {
                    "name": "FUNCTIONS_WORKER_RUNTIME",
                    "value": "node"
                  },
                  {
                    "name": "WEBSITE_NODE_DEFAULT_VERSION",
                    "value": "~22"
                  },
                  {
                    "name": "EventHubConnection",
                    "value": "[listKeys(resourceId('Microsoft.EventHub/namespaces/authorizationRules', variables('eventHubNamespaceName'), 'RootManageSharedAccessKey'), '2022-10-01-preview').primaryConnectionString]"
                  },
                  {
                    "name": "LAUNCHDARKLY_CLIENT_SIDE_ID",
                    "value": "[parameters('launchDarklyClientSideId')]"
                  },
                  {
                    "name": "LAUNCHDARKLY_SERVICE",
                    "value": "azure"
                  }
                ]
              }
            }
          },
          {
            "type": "Microsoft.Web/sites/functions",
            "apiVersion": "2023-01-01",
            "name": "[format('{0}/ForwardToLaunchDarkly', variables('functionAppName'))]",
            "dependsOn": [
              "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]"
            ],
            "properties": {
              "config": {
                "bindings": [
                  {
                    "type": "eventHubTrigger",
                    "name": "eventHubMessages",
                    "direction": "in",
                    "eventHubName": "[variables('eventHubName')]",
                    "connection": "EventHubConnection",
                    "cardinality": "many",
                    "consumerGroup": "$Default"
                  }
                ]
              },
              "files": {
                "index.js": "const https = require('https')\n\nconst PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID\nconst SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'\nconst OTEL_HOSTNAME = 'otel.observability.app.launchdarkly.com'\nconst MAX_RETRIES = 6\nconst RETRY_INTERVAL = 1000\n\nconst SEVERITY_MAP = {\n  verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',\n  info: 'INFO', warning: 'WARN', warn: 'WARN',\n  error: 'ERROR', critical: 'FATAL',\n}\n\nfunction sendWithRetries(context, path, payload) {\n  const options = {\n    hostname: OTEL_HOSTNAME,\n    path,\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'x-launchdarkly-project': PROJECT_ID,\n    },\n  }\n  let numRetries = MAX_RETRIES\n  let retryInterval = RETRY_INTERVAL\n  return new Promise((resolve, reject) => {\n    const sendRequest = () => {\n      const retryRequest = errMsg => {\n        if (numRetries === 0) return reject(errMsg)\n        context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)\n        numRetries--\n        retryInterval *= 2\n        setTimeout(sendRequest, retryInterval)\n      }\n      const req = https\n        .request(options, resp => {\n          if (resp.statusCode >= 200 && resp.statusCode <= 299) {\n            resolve(true)\n          } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {\n            reject(`HTTP ${resp.statusCode}`)\n          } else {\n            retryRequest(`HTTP ${resp.statusCode}`)\n          }\n        })\n        .on('error', error => retryRequest(error.message))\n        .on('timeout', () => {\n          req.destroy()\n          retryRequest('request timed out')\n        })\n      req.write(JSON.stringify(payload))\n      req.end()\n    }\n    sendRequest()\n  })\n}\n\nconst toNanos = ts => String(new Date(ts).getTime() * 1000000)\nconst toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })\n\nfunction parseRecords(eventHubMessages) {\n  const logs = []\n  const metrics = []\n  for (const message of eventHubMessages) {\n    const items = message.records || [message]\n    for (const item of items) {\n      const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()\n\n      if (item.metricName) {\n        const dimensions = (item.dimensions || []).reduce((acc, d) => {\n          acc[`azure.dimension.${d.name}`] = d.value\n          return acc\n        }, {})\n        metrics.push({\n          name: `azure.${item.metricName}`,\n          timestamp,\n          count: item.count ?? 1,\n          sum: item.total ?? item.average ?? 0,\n          min: item.minimum,\n          max: item.maximum,\n          attributes: {\n            'azure.resource.id': item.resourceId,\n            'azure.namespace': item.namespace,\n            'azure.timeGrain': item.timeGrain,\n            ...dimensions,\n          },\n        })\n      } else {\n        const level = (item.level || item.resultType || 'info').toLowerCase()\n        let props = {}\n        if (item.properties) {\n          try {\n            props = typeof item.properties === 'string'\n              ? JSON.parse(item.properties) : item.properties\n          } catch (_) { /* ignore parse errors */ }\n        }\n        let message = item.resultDescription || item.operationName\n        if (!message && props.CsMethod) {\n          message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()\n        }\n        if (!message) message = JSON.stringify(item)\n        const attrs = {\n          'azure.resourceId': item.resourceId,\n          'azure.category': item.category,\n          'azure.operationName': item.operationName,\n        }\n        for (const [k, v] of Object.entries(props)) {\n          if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)\n        }\n        logs.push({\n          message,\n          timestamp,\n          severityText: SEVERITY_MAP[level] || 'INFO',\n          attributes: attrs,\n        })\n      }\n    }\n  }\n  return { logs, metrics }\n}\n\nfunction buildOtlpLogs(logs) {\n  return {\n    resourceLogs: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeLogs: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        logRecords: logs.map(l => ({\n          timeUnixNano: toNanos(l.timestamp),\n          body: { stringValue: l.message },\n          severityText: l.severityText,\n          attributes: Object.entries(l.attributes)\n            .filter(([, v]) => v != null)\n            .map(([k, v]) => toAttr(k, v)),\n        })),\n      }],\n    }],\n  }\n}\n\nfunction buildOtlpMetrics(metrics) {\n  return {\n    resourceMetrics: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeMetrics: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        metrics: metrics.map(m => {\n          const dp = {\n            timeUnixNano: toNanos(m.timestamp),\n            count: m.count,\n            sum: m.sum,\n            explicitBounds: [],\n            bucketCounts: [m.count],\n            attributes: Object.entries(m.attributes)\n              .filter(([, v]) => v != null)\n              .map(([k, v]) => toAttr(k, v)),\n          }\n          if (m.min != null) dp.min = m.min\n          if (m.max != null) dp.max = m.max\n          return {\n            name: m.name,\n            histogram: {\n              aggregationTemporality: 1,\n              dataPoints: [dp],\n            },\n          }\n        }),\n      }],\n    }],\n  }\n}\n\nmodule.exports = async function (context, eventHubMessages) {\n  if (!PROJECT_ID) {\n    context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')\n    return\n  }\n\n  const { logs, metrics } = parseRecords(eventHubMessages)\n  const promises = []\n\n  if (logs.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  if (metrics.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  const results = await Promise.all(promises)\n  if (!results.every(v => v === true)) {\n    context.log.error('Failed to send some records')\n  }\n}"
              }
            }
          }
        ],
        "outputs": {
          "functionAppName": {
            "type": "string",
            "value": "[variables('functionAppName')]"
          },
          "eventHubNamespaceName": {
            "type": "string",
            "value": "[variables('eventHubNamespaceName')]"
          },
          "eventHubName": {
            "type": "string",
            "value": "[variables('eventHubName')]"
          }
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  The template includes an inline Azure Function that forwards data to LaunchDarkly. To view a readable version of the function source code, read [Function code reference](#function-code-reference) below.

  ## Configure Azure Diagnostic Settings

  After deploying the integration, configure your Azure resources to stream diagnostic data to the Event Hub.

  To configure Diagnostic Settings:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Navigate to the Azure resource you want to monitor, such as an App Service, Virtual Machine, or SQL Database.
  3. Click **Diagnostic settings** under **Monitoring** in the left menu.
  4. Click **Add diagnostic setting**.
  5. Enter a name, such as `launchdarkly-observability`.
  6. Under **Logs**, select the log categories you want to forward.
  7. Under **Metrics**, check **AllMetrics**.
  8. Under **Destination details**, check **Stream to an event hub**.
  9. Select the Event Hub namespace created by the ARM template.
  10. Select `launchdarkly-diagnostics` as the **Event Hub name**.
  11. Select `RootManageSharedAccessKey` as the **Event Hub policy name**.
  12. Click **Save**.

  Repeat these steps for each Azure resource you want to monitor.

  <Tip>
    **Configuring diagnostics at scale**

    For large environments, use Azure Policy to automatically deploy diagnostic settings to resources as you create them. To learn more, read [Create diagnostic settings at scale using Azure Policy](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings-policy) in the Microsoft documentation.
  </Tip>

  ## What the template creates

  The ARM template creates the following Azure resources:

  * **Event Hub Namespace and Event Hub**: Receives diagnostic data streamed from your Azure resources.
  * **Function App with forwarding function (Node.js 22)**: Serverless function that triggers on Event Hub messages, separates logs from metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector, deployed automatically by the template.
  * **App Service Plan (Consumption)**: Serverless hosting for the Function App with pay-per-execution pricing.
  * **Storage Account**: Required for Function App state and trigger checkpointing.

  All resource names are auto-generated using a unique suffix derived from your resource group ID.

  <Tip>
    **Customizing the compute resources**

    The template uses a Consumption (serverless) plan for pay-per-execution pricing, which is sufficient for most workloads. You can modify the template to use a different hosting plan based on your requirements. To learn more, read [Azure Functions hosting options](https://learn.microsoft.com/en-us/azure/azure-functions/functions-scale) in the Microsoft documentation.
  </Tip>

  ## View data in LaunchDarkly observability dashboards

  After deployment, data appears in the LaunchDarkly observability dashboard within 1-2 minutes of configuring Diagnostic Settings.

  To view your data:

  1. Navigate to **Observability** in LaunchDarkly.
  2. Select the environment you configured.
  3. Navigate to the **Logs** tab to view Azure diagnostic logs such as HTTP request logs, console output, and audit events.
  4. Navigate to the **Metrics** tab to view Azure platform metrics such as CPU percentage, memory usage, and request counts.
  5. Use the search and filter controls to query your Azure data.

  Log records include attributes such as `azure.resourceId`, `azure.category`, and `azure.operationName` for filtering. Metric records use names following the pattern `azure.<metricName>`, for example `azure.CpuPercentage` or `azure.Http2xx`, and are sent as OTLP histograms with sum, count, min, and max values. Metrics include attributes such as `azure.resource.id`, `azure.namespace`, and dimension attributes such as `azure.dimension.ActivityName`.

  All records include a `service.name` attribute set to `azure` by default. To distinguish data from multiple Azure subscriptions or environments, change the `LAUNCHDARKLY_SERVICE` application setting in the Function App to a unique value such as `azure-production` or `azure-staging`.

  ## Control data volume (optional)

  By default, only those Azure resources that use Diagnostic Settings send data to LaunchDarkly. To control data volume, enable Diagnostic Settings only on the resources you want to monitor.

  You can also filter within Diagnostic Settings by selecting specific log categories instead of all categories. For example, you might select only **AppServiceHTTPLogs** and **AppServiceConsoleLogs** for an App Service, omitting audit and platform logs.

  ## Function code reference

  The ARM template deploys a Node.js Azure Function that parses Azure Monitor diagnostic records, separates them into logs and metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector at `/v1/logs` and `/v1/metrics`. Expand this section to open or review the function source code.

  <Accordion title="Expand function source code">
    <CodeGroup>
      ```js title="ForwardToLaunchDarkly/index.js" expandable lines wrap theme={null}
      const https = require('https')

      const PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID
      const SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'
      const OTEL_HOSTNAME = 'otel.observability.app.launchdarkly.com'
      const MAX_RETRIES = 6
      const RETRY_INTERVAL = 1000

      const SEVERITY_MAP = {
        verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',
        info: 'INFO', warning: 'WARN', warn: 'WARN',
        error: 'ERROR', critical: 'FATAL',
      }

      function sendWithRetries(context, path, payload) {
        const options = {
          hostname: OTEL_HOSTNAME,
          path,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-launchdarkly-project': PROJECT_ID,
          },
        }
        let numRetries = MAX_RETRIES
        let retryInterval = RETRY_INTERVAL
        return new Promise((resolve, reject) => {
          const sendRequest = () => {
            const retryRequest = errMsg => {
              if (numRetries === 0) return reject(errMsg)
              context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)
              numRetries--
              retryInterval *= 2
              setTimeout(sendRequest, retryInterval)
            }
            const req = https
              .request(options, resp => {
                if (resp.statusCode >= 200 && resp.statusCode <= 299) {
                  resolve(true)
                } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {
                  reject(`HTTP ${resp.statusCode}`)
                } else {
                  retryRequest(`HTTP ${resp.statusCode}`)
                }
              })
              .on('error', error => retryRequest(error.message))
              .on('timeout', () => {
                req.destroy()
                retryRequest('request timed out')
              })
            req.write(JSON.stringify(payload))
            req.end()
          }
          sendRequest()
        })
      }

      const toNanos = ts => String(new Date(ts).getTime() * 1000000)
      const toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })

      function parseRecords(eventHubMessages) {
        const logs = []
        const metrics = []
        for (const message of eventHubMessages) {
          const items = message.records || [message]
          for (const item of items) {
            const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()

            if (item.metricName) {
              const dimensions = (item.dimensions || []).reduce((acc, d) => {
                acc[`azure.dimension.${d.name}`] = d.value
                return acc
              }, {})
              metrics.push({
                name: `azure.${item.metricName}`,
                timestamp,
                count: item.count ?? 1,
                sum: item.total ?? item.average ?? 0,
                min: item.minimum,
                max: item.maximum,
                attributes: {
                  'azure.resource.id': item.resourceId,
                  'azure.namespace': item.namespace,
                  'azure.timeGrain': item.timeGrain,
                  ...dimensions,
                },
              })
            } else {
              const level = (item.level || item.resultType || 'info').toLowerCase()
              let props = {}
              if (item.properties) {
                try {
                  props = typeof item.properties === 'string'
                    ? JSON.parse(item.properties) : item.properties
                } catch (_) { /* ignore parse errors */ }
              }
              let message = item.resultDescription || item.operationName
              if (!message && props.CsMethod) {
                message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()
              }
              if (!message) message = JSON.stringify(item)
              const attrs = {
                'azure.resourceId': item.resourceId,
                'azure.category': item.category,
                'azure.operationName': item.operationName,
              }
              for (const [k, v] of Object.entries(props)) {
                if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)
              }
              logs.push({
                message,
                timestamp,
                severityText: SEVERITY_MAP[level] || 'INFO',
                attributes: attrs,
              })
            }
          }
        }
        return { logs, metrics }
      }

      function buildOtlpLogs(logs) {
        return {
          resourceLogs: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeLogs: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              logRecords: logs.map(l => ({
                timeUnixNano: toNanos(l.timestamp),
                body: { stringValue: l.message },
                severityText: l.severityText,
                attributes: Object.entries(l.attributes)
                  .filter(([, v]) => v != null)
                  .map(([k, v]) => toAttr(k, v)),
              })),
            }],
          }],
        }
      }

      function buildOtlpMetrics(metrics) {
        return {
          resourceMetrics: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeMetrics: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              metrics: metrics.map(m => {
                const dp = {
                  timeUnixNano: toNanos(m.timestamp),
                  count: m.count,
                  sum: m.sum,
                  explicitBounds: [],
                  bucketCounts: [m.count],
                  attributes: Object.entries(m.attributes)
                    .filter(([, v]) => v != null)
                    .map(([k, v]) => toAttr(k, v)),
                }
                if (m.min != null) dp.min = m.min
                if (m.max != null) dp.max = m.max
                return {
                  name: m.name,
                  histogram: {
                    aggregationTemporality: 1,
                    dataPoints: [dp],
                  },
                }
              }),
            }],
          }],
        }
      }

      module.exports = async function (context, eventHubMessages) {
        if (!PROJECT_ID) {
          context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')
          return
        }

        const { logs, metrics } = parseRecords(eventHubMessages)
        const promises = []

        if (logs.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))
              .catch(e => context.log.error(e))
          )
        }

        if (metrics.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))
              .catch(e => context.log.error(e))
          )
        }

        const results = await Promise.all(promises)
        if (!results.every(v => v === true)) {
          context.log.error('Failed to send some records')
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  ## Troubleshooting

  If data does not appear in LaunchDarkly after configuring Diagnostic Settings:

  1. **Verify the Event Hub is receiving data.** Open the Event Hub namespace in the Azure Portal and check the **Messages** chart under **Overview**. If no messages appear, check your Diagnostic Settings configuration.

  2. **Verify the Function App is running.** Open the Function App in the Azure Portal and navigate to **Functions** > **ForwardToLaunchDarkly** > **Monitor** for invocation logs. If invocations fail, check the error messages.

  3. **Verify the client-side ID.** Confirm that the `LAUNCHDARKLY_CLIENT_SIDE_ID` application setting in the Function App matches your LaunchDarkly environment's client-side ID.

  4. **Check Function App logs.** Open the Function App in the Azure Portal and navigate to **Log stream** under **Monitoring** to view real-time logs from the function.
</View>

<View title="Federal docs">
  <View title="Federal docs">
    <Danger>
      **This integration is not available in federal environments**

      To learn more, read <a href="/docs/home/infrastructure/federal">LaunchDarkly in federal environments</a>.
    </Danger>
  </View>

  This topic explains how to stream Azure Monitor logs and metrics from your Azure subscription to LaunchDarkly observability features. This integration lets you monitor your Azure infrastructure alongside your feature flag data for unified observability.

  The integration uses Azure Event Hubs and a serverless Azure Function to forward diagnostic data to LaunchDarkly. An Azure Resource Manager (ARM) template automates the entire setup:

  1. Azure Diagnostic Settings stream logs and metrics from your resources to an Event Hub.
  2. The Azure Function triggers on each batch of events, separates logs from metrics, and forwards both as OpenTelemetry Protocol (OTLP) JSON to the LaunchDarkly OpenTelemetry collector.

  ## Prerequisites

  To use the Azure Monitor integration, you need:

  * A LaunchDarkly client-side ID for your target environment
  * An Azure subscription with permissions to create:
    * Event Hub namespaces
    * Function Apps
    * Storage accounts

  To find and copy your LaunchDarkly client-side ID, read [View or copy SDK credentials](/docs/home/account/environment/keys#view-or-copy-sdk-credentials).

  ## Deploy the ARM template

  The ARM template creates all required infrastructure and deploys the forwarding function automatically.

  To deploy the ARM template:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Search for **Deploy a custom template** and select it.
  3. Click **Build your own template in the editor**.
  4. Clear the default contents, then paste the ARM template below into the editor.
  5. Click **Save**.
  6. Select a **Resource group** or create a new one.
  7. Enter your LaunchDarkly environment's client-side ID into the **LaunchDarkly Client Side Id** parameter.
  8. Click **Review + create**, then click **Create**.

  The deployment takes 2-3 minutes. After it completes, the Event Hub and forwarding function are ready to receive data. Copy and save the **Event Hub namespace name** from the deployment outputs to use when you configure Diagnostic Settings.

  ### ARM template

  <Accordion title="Expand ARM template">
    <CodeGroup>
      ```json title="ARM template" expandable lines wrap theme={null}
      {
        "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
          "launchDarklyClientSideId": {
            "type": "string",
            "metadata": {
              "description": "Your LaunchDarkly environment client-side ID"
            }
          },
          "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {
              "description": "Azure region for all resources"
            }
          }
        },
        "variables": {
          "suffix": "[uniqueString(resourceGroup().id)]",
          "eventHubNamespaceName": "[format('ld-obs-{0}', variables('suffix'))]",
          "eventHubName": "launchdarkly-diagnostics",
          "storageAccountName": "[format('ldobs{0}', variables('suffix'))]",
          "functionAppName": "[format('ld-obs-func-{0}', variables('suffix'))]",
          "hostingPlanName": "[format('ld-obs-plan-{0}', variables('suffix'))]"
        },
        "resources": [
          {
            "type": "Microsoft.EventHub/namespaces",
            "apiVersion": "2022-10-01-preview",
            "name": "[variables('eventHubNamespaceName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard",
              "tier": "Standard",
              "capacity": 1
            }
          },
          {
            "type": "Microsoft.EventHub/namespaces/eventhubs",
            "apiVersion": "2022-10-01-preview",
            "name": "[format('{0}/{1}', variables('eventHubNamespaceName'), variables('eventHubName'))]",
            "dependsOn": ["[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"],
            "properties": {
              "messageRetentionInDays": 1,
              "partitionCount": 2
            }
          },
          {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2023-01-01",
            "name": "[variables('storageAccountName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard_LRS"
            },
            "kind": "StorageV2"
          },
          {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2023-01-01",
            "name": "[variables('hostingPlanName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Y1",
              "tier": "Dynamic"
            },
            "properties": {}
          },
          {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2023-01-01",
            "name": "[variables('functionAppName')]",
            "location": "[parameters('location')]",
            "kind": "functionapp",
            "dependsOn": [
              "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
              "[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"
            ],
            "properties": {
              "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "siteConfig": {
                "appSettings": [
                  {
                    "name": "AzureWebJobsStorage",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('functionAppName'))]"
                  },
                  {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~4"
                  },
                  {
                    "name": "FUNCTIONS_WORKER_RUNTIME",
                    "value": "node"
                  },
                  {
                    "name": "WEBSITE_NODE_DEFAULT_VERSION",
                    "value": "~22"
                  },
                  {
                    "name": "EventHubConnection",
                    "value": "[listKeys(resourceId('Microsoft.EventHub/namespaces/authorizationRules', variables('eventHubNamespaceName'), 'RootManageSharedAccessKey'), '2022-10-01-preview').primaryConnectionString]"
                  },
                  {
                    "name": "LAUNCHDARKLY_CLIENT_SIDE_ID",
                    "value": "[parameters('launchDarklyClientSideId')]"
                  },
                  {
                    "name": "LAUNCHDARKLY_SERVICE",
                    "value": "azure"
                  }
                ]
              }
            }
          },
          {
            "type": "Microsoft.Web/sites/functions",
            "apiVersion": "2023-01-01",
            "name": "[format('{0}/ForwardToLaunchDarkly', variables('functionAppName'))]",
            "dependsOn": [
              "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]"
            ],
            "properties": {
              "config": {
                "bindings": [
                  {
                    "type": "eventHubTrigger",
                    "name": "eventHubMessages",
                    "direction": "in",
                    "eventHubName": "[variables('eventHubName')]",
                    "connection": "EventHubConnection",
                    "cardinality": "many",
                    "consumerGroup": "$Default"
                  }
                ]
              },
              "files": {
                "index.js": "const https = require('https')\n\nconst PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID\nconst SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'\nconst OTEL_HOSTNAME = 'otel.observability.app.launchdarkly.com'\nconst MAX_RETRIES = 6\nconst RETRY_INTERVAL = 1000\n\nconst SEVERITY_MAP = {\n  verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',\n  info: 'INFO', warning: 'WARN', warn: 'WARN',\n  error: 'ERROR', critical: 'FATAL',\n}\n\nfunction sendWithRetries(context, path, payload) {\n  const options = {\n    hostname: OTEL_HOSTNAME,\n    path,\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'x-launchdarkly-project': PROJECT_ID,\n    },\n  }\n  let numRetries = MAX_RETRIES\n  let retryInterval = RETRY_INTERVAL\n  return new Promise((resolve, reject) => {\n    const sendRequest = () => {\n      const retryRequest = errMsg => {\n        if (numRetries === 0) return reject(errMsg)\n        context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)\n        numRetries--\n        retryInterval *= 2\n        setTimeout(sendRequest, retryInterval)\n      }\n      const req = https\n        .request(options, resp => {\n          if (resp.statusCode >= 200 && resp.statusCode <= 299) {\n            resolve(true)\n          } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {\n            reject(`HTTP ${resp.statusCode}`)\n          } else {\n            retryRequest(`HTTP ${resp.statusCode}`)\n          }\n        })\n        .on('error', error => retryRequest(error.message))\n        .on('timeout', () => {\n          req.destroy()\n          retryRequest('request timed out')\n        })\n      req.write(JSON.stringify(payload))\n      req.end()\n    }\n    sendRequest()\n  })\n}\n\nconst toNanos = ts => String(new Date(ts).getTime() * 1000000)\nconst toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })\n\nfunction parseRecords(eventHubMessages) {\n  const logs = []\n  const metrics = []\n  for (const message of eventHubMessages) {\n    const items = message.records || [message]\n    for (const item of items) {\n      const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()\n\n      if (item.metricName) {\n        const dimensions = (item.dimensions || []).reduce((acc, d) => {\n          acc[`azure.dimension.${d.name}`] = d.value\n          return acc\n        }, {})\n        metrics.push({\n          name: `azure.${item.metricName}`,\n          timestamp,\n          count: item.count ?? 1,\n          sum: item.total ?? item.average ?? 0,\n          min: item.minimum,\n          max: item.maximum,\n          attributes: {\n            'azure.resource.id': item.resourceId,\n            'azure.namespace': item.namespace,\n            'azure.timeGrain': item.timeGrain,\n            ...dimensions,\n          },\n        })\n      } else {\n        const level = (item.level || item.resultType || 'info').toLowerCase()\n        let props = {}\n        if (item.properties) {\n          try {\n            props = typeof item.properties === 'string'\n              ? JSON.parse(item.properties) : item.properties\n          } catch (_) { /* ignore parse errors */ }\n        }\n        let message = item.resultDescription || item.operationName\n        if (!message && props.CsMethod) {\n          message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()\n        }\n        if (!message) message = JSON.stringify(item)\n        const attrs = {\n          'azure.resourceId': item.resourceId,\n          'azure.category': item.category,\n          'azure.operationName': item.operationName,\n        }\n        for (const [k, v] of Object.entries(props)) {\n          if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)\n        }\n        logs.push({\n          message,\n          timestamp,\n          severityText: SEVERITY_MAP[level] || 'INFO',\n          attributes: attrs,\n        })\n      }\n    }\n  }\n  return { logs, metrics }\n}\n\nfunction buildOtlpLogs(logs) {\n  return {\n    resourceLogs: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeLogs: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        logRecords: logs.map(l => ({\n          timeUnixNano: toNanos(l.timestamp),\n          body: { stringValue: l.message },\n          severityText: l.severityText,\n          attributes: Object.entries(l.attributes)\n            .filter(([, v]) => v != null)\n            .map(([k, v]) => toAttr(k, v)),\n        })),\n      }],\n    }],\n  }\n}\n\nfunction buildOtlpMetrics(metrics) {\n  return {\n    resourceMetrics: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeMetrics: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        metrics: metrics.map(m => {\n          const dp = {\n            timeUnixNano: toNanos(m.timestamp),\n            count: m.count,\n            sum: m.sum,\n            explicitBounds: [],\n            bucketCounts: [m.count],\n            attributes: Object.entries(m.attributes)\n              .filter(([, v]) => v != null)\n              .map(([k, v]) => toAttr(k, v)),\n          }\n          if (m.min != null) dp.min = m.min\n          if (m.max != null) dp.max = m.max\n          return {\n            name: m.name,\n            histogram: {\n              aggregationTemporality: 1,\n              dataPoints: [dp],\n            },\n          }\n        }),\n      }],\n    }],\n  }\n}\n\nmodule.exports = async function (context, eventHubMessages) {\n  if (!PROJECT_ID) {\n    context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')\n    return\n  }\n\n  const { logs, metrics } = parseRecords(eventHubMessages)\n  const promises = []\n\n  if (logs.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  if (metrics.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  const results = await Promise.all(promises)\n  if (!results.every(v => v === true)) {\n    context.log.error('Failed to send some records')\n  }\n}"
              }
            }
          }
        ],
        "outputs": {
          "functionAppName": {
            "type": "string",
            "value": "[variables('functionAppName')]"
          },
          "eventHubNamespaceName": {
            "type": "string",
            "value": "[variables('eventHubNamespaceName')]"
          },
          "eventHubName": {
            "type": "string",
            "value": "[variables('eventHubName')]"
          }
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  The template includes an inline Azure Function that forwards data to LaunchDarkly. To view a readable version of the function source code, read [Function code reference](#function-code-reference) below.

  ## Configure Azure Diagnostic Settings

  After deploying the integration, configure your Azure resources to stream diagnostic data to the Event Hub.

  To configure Diagnostic Settings:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Navigate to the Azure resource you want to monitor, such as an App Service, Virtual Machine, or SQL Database.
  3. Click **Diagnostic settings** under **Monitoring** in the left menu.
  4. Click **Add diagnostic setting**.
  5. Enter a name, such as `launchdarkly-observability`.
  6. Under **Logs**, select the log categories you want to forward.
  7. Under **Metrics**, check **AllMetrics**.
  8. Under **Destination details**, check **Stream to an event hub**.
  9. Select the Event Hub namespace created by the ARM template.
  10. Select `launchdarkly-diagnostics` as the **Event Hub name**.
  11. Select `RootManageSharedAccessKey` as the **Event Hub policy name**.
  12. Click **Save**.

  Repeat these steps for each Azure resource you want to monitor.

  <Tip>
    **Configuring diagnostics at scale**

    For large environments, use Azure Policy to automatically deploy diagnostic settings to resources as you create them. To learn more, read [Create diagnostic settings at scale using Azure Policy](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings-policy) in the Microsoft documentation.
  </Tip>

  ## What the template creates

  The ARM template creates the following Azure resources:

  * **Event Hub Namespace and Event Hub**: Receives diagnostic data streamed from your Azure resources.
  * **Function App with forwarding function (Node.js 22)**: Serverless function that triggers on Event Hub messages, separates logs from metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector, deployed automatically by the template.
  * **App Service Plan (Consumption)**: Serverless hosting for the Function App with pay-per-execution pricing.
  * **Storage Account**: Required for Function App state and trigger checkpointing.

  All resource names are auto-generated using a unique suffix derived from your resource group ID.

  <Tip>
    **Customizing the compute resources**

    The template uses a Consumption (serverless) plan for pay-per-execution pricing, which is sufficient for most workloads. You can modify the template to use a different hosting plan based on your requirements. To learn more, read [Azure Functions hosting options](https://learn.microsoft.com/en-us/azure/azure-functions/functions-scale) in the Microsoft documentation.
  </Tip>

  ## View data in LaunchDarkly observability dashboards

  After deployment, data appears in the LaunchDarkly observability dashboard within 1-2 minutes of configuring Diagnostic Settings.

  To view your data:

  1. Navigate to **Observability** in LaunchDarkly.
  2. Select the environment you configured.
  3. Navigate to the **Logs** tab to view Azure diagnostic logs such as HTTP request logs, console output, and audit events.
  4. Navigate to the **Metrics** tab to view Azure platform metrics such as CPU percentage, memory usage, and request counts.
  5. Use the search and filter controls to query your Azure data.

  Log records include attributes such as `azure.resourceId`, `azure.category`, and `azure.operationName` for filtering. Metric records use names following the pattern `azure.<metricName>`, for example `azure.CpuPercentage` or `azure.Http2xx`, and are sent as OTLP histograms with sum, count, min, and max values. Metrics include attributes such as `azure.resource.id`, `azure.namespace`, and dimension attributes such as `azure.dimension.ActivityName`.

  All records include a `service.name` attribute set to `azure` by default. To distinguish data from multiple Azure subscriptions or environments, change the `LAUNCHDARKLY_SERVICE` application setting in the Function App to a unique value such as `azure-production` or `azure-staging`.

  ## Control data volume (optional)

  By default, only those Azure resources that use Diagnostic Settings send data to LaunchDarkly. To control data volume, enable Diagnostic Settings only on the resources you want to monitor.

  You can also filter within Diagnostic Settings by selecting specific log categories instead of all categories. For example, you might select only **AppServiceHTTPLogs** and **AppServiceConsoleLogs** for an App Service, omitting audit and platform logs.

  ## Function code reference

  The ARM template deploys a Node.js Azure Function that parses Azure Monitor diagnostic records, separates them into logs and metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector at `/v1/logs` and `/v1/metrics`. Expand this section to open or review the function source code.

  <Accordion title="Expand function source code">
    <CodeGroup>
      ```js title="ForwardToLaunchDarkly/index.js" expandable lines wrap theme={null}
      const https = require('https')

      const PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID
      const SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'
      const OTEL_HOSTNAME = 'otel.observability.app.launchdarkly.com'
      const MAX_RETRIES = 6
      const RETRY_INTERVAL = 1000

      const SEVERITY_MAP = {
        verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',
        info: 'INFO', warning: 'WARN', warn: 'WARN',
        error: 'ERROR', critical: 'FATAL',
      }

      function sendWithRetries(context, path, payload) {
        const options = {
          hostname: OTEL_HOSTNAME,
          path,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-launchdarkly-project': PROJECT_ID,
          },
        }
        let numRetries = MAX_RETRIES
        let retryInterval = RETRY_INTERVAL
        return new Promise((resolve, reject) => {
          const sendRequest = () => {
            const retryRequest = errMsg => {
              if (numRetries === 0) return reject(errMsg)
              context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)
              numRetries--
              retryInterval *= 2
              setTimeout(sendRequest, retryInterval)
            }
            const req = https
              .request(options, resp => {
                if (resp.statusCode >= 200 && resp.statusCode <= 299) {
                  resolve(true)
                } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {
                  reject(`HTTP ${resp.statusCode}`)
                } else {
                  retryRequest(`HTTP ${resp.statusCode}`)
                }
              })
              .on('error', error => retryRequest(error.message))
              .on('timeout', () => {
                req.destroy()
                retryRequest('request timed out')
              })
            req.write(JSON.stringify(payload))
            req.end()
          }
          sendRequest()
        })
      }

      const toNanos = ts => String(new Date(ts).getTime() * 1000000)
      const toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })

      function parseRecords(eventHubMessages) {
        const logs = []
        const metrics = []
        for (const message of eventHubMessages) {
          const items = message.records || [message]
          for (const item of items) {
            const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()

            if (item.metricName) {
              const dimensions = (item.dimensions || []).reduce((acc, d) => {
                acc[`azure.dimension.${d.name}`] = d.value
                return acc
              }, {})
              metrics.push({
                name: `azure.${item.metricName}`,
                timestamp,
                count: item.count ?? 1,
                sum: item.total ?? item.average ?? 0,
                min: item.minimum,
                max: item.maximum,
                attributes: {
                  'azure.resource.id': item.resourceId,
                  'azure.namespace': item.namespace,
                  'azure.timeGrain': item.timeGrain,
                  ...dimensions,
                },
              })
            } else {
              const level = (item.level || item.resultType || 'info').toLowerCase()
              let props = {}
              if (item.properties) {
                try {
                  props = typeof item.properties === 'string'
                    ? JSON.parse(item.properties) : item.properties
                } catch (_) { /* ignore parse errors */ }
              }
              let message = item.resultDescription || item.operationName
              if (!message && props.CsMethod) {
                message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()
              }
              if (!message) message = JSON.stringify(item)
              const attrs = {
                'azure.resourceId': item.resourceId,
                'azure.category': item.category,
                'azure.operationName': item.operationName,
              }
              for (const [k, v] of Object.entries(props)) {
                if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)
              }
              logs.push({
                message,
                timestamp,
                severityText: SEVERITY_MAP[level] || 'INFO',
                attributes: attrs,
              })
            }
          }
        }
        return { logs, metrics }
      }

      function buildOtlpLogs(logs) {
        return {
          resourceLogs: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeLogs: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              logRecords: logs.map(l => ({
                timeUnixNano: toNanos(l.timestamp),
                body: { stringValue: l.message },
                severityText: l.severityText,
                attributes: Object.entries(l.attributes)
                  .filter(([, v]) => v != null)
                  .map(([k, v]) => toAttr(k, v)),
              })),
            }],
          }],
        }
      }

      function buildOtlpMetrics(metrics) {
        return {
          resourceMetrics: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeMetrics: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              metrics: metrics.map(m => {
                const dp = {
                  timeUnixNano: toNanos(m.timestamp),
                  count: m.count,
                  sum: m.sum,
                  explicitBounds: [],
                  bucketCounts: [m.count],
                  attributes: Object.entries(m.attributes)
                    .filter(([, v]) => v != null)
                    .map(([k, v]) => toAttr(k, v)),
                }
                if (m.min != null) dp.min = m.min
                if (m.max != null) dp.max = m.max
                return {
                  name: m.name,
                  histogram: {
                    aggregationTemporality: 1,
                    dataPoints: [dp],
                  },
                }
              }),
            }],
          }],
        }
      }

      module.exports = async function (context, eventHubMessages) {
        if (!PROJECT_ID) {
          context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')
          return
        }

        const { logs, metrics } = parseRecords(eventHubMessages)
        const promises = []

        if (logs.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))
              .catch(e => context.log.error(e))
          )
        }

        if (metrics.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))
              .catch(e => context.log.error(e))
          )
        }

        const results = await Promise.all(promises)
        if (!results.every(v => v === true)) {
          context.log.error('Failed to send some records')
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  ## Troubleshooting

  If data does not appear in LaunchDarkly after configuring Diagnostic Settings:

  1. **Verify the Event Hub is receiving data.** Open the Event Hub namespace in the Azure Portal and check the **Messages** chart under **Overview**. If no messages appear, check your Diagnostic Settings configuration.

  2. **Verify the Function App is running.** Open the Function App in the Azure Portal and navigate to **Functions** > **ForwardToLaunchDarkly** > **Monitor** for invocation logs. If invocations fail, check the error messages.

  3. **Verify the client-side ID.** Confirm that the `LAUNCHDARKLY_CLIENT_SIDE_ID` application setting in the Function App matches your LaunchDarkly environment's client-side ID.

  4. **Check Function App logs.** Open the Function App in the Azure Portal and navigate to **Log stream** under **Monitoring** to view real-time logs from the function.
</View>

<View title="EU docs">
  <View title="Federal docs">
    <Danger>
      **This integration is not available in federal environments**

      To learn more, read <a href="/docs/home/infrastructure/federal">LaunchDarkly in federal environments</a>.
    </Danger>
  </View>

  This topic explains how to stream Azure Monitor logs and metrics from your Azure subscription to LaunchDarkly observability features. This integration lets you monitor your Azure infrastructure alongside your feature flag data for unified observability.

  The integration uses Azure Event Hubs and a serverless Azure Function to forward diagnostic data to LaunchDarkly. An Azure Resource Manager (ARM) template automates the entire setup:

  1. Azure Diagnostic Settings stream logs and metrics from your resources to an Event Hub.
  2. The Azure Function triggers on each batch of events, separates logs from metrics, and forwards both as OpenTelemetry Protocol (OTLP) JSON to the LaunchDarkly OpenTelemetry collector.

  ## Prerequisites

  To use the Azure Monitor integration, you need:

  * A LaunchDarkly client-side ID for your target environment
  * An Azure subscription with permissions to create:
    * Event Hub namespaces
    * Function Apps
    * Storage accounts

  To find and copy your LaunchDarkly client-side ID, read [View or copy SDK credentials](/docs/home/account/environment/keys#view-or-copy-sdk-credentials).

  ## Deploy the ARM template

  The ARM template creates all required infrastructure and deploys the forwarding function automatically.

  To deploy the ARM template:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Search for **Deploy a custom template** and select it.
  3. Click **Build your own template in the editor**.
  4. Clear the default contents, then paste the ARM template below into the editor.
  5. Click **Save**.
  6. Select a **Resource group** or create a new one.
  7. Enter your LaunchDarkly environment's client-side ID into the **LaunchDarkly Client Side Id** parameter.
  8. Click **Review + create**, then click **Create**.

  The deployment takes 2-3 minutes. After it completes, the Event Hub and forwarding function are ready to receive data. Copy and save the **Event Hub namespace name** from the deployment outputs to use when you configure Diagnostic Settings.

  ### ARM template

  <Accordion title="Expand ARM template">
    <CodeGroup>
      ```json title="ARM template" expandable lines wrap theme={null}
      {
        "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
          "launchDarklyClientSideId": {
            "type": "string",
            "metadata": {
              "description": "Your LaunchDarkly environment client-side ID"
            }
          },
          "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {
              "description": "Azure region for all resources"
            }
          }
        },
        "variables": {
          "suffix": "[uniqueString(resourceGroup().id)]",
          "eventHubNamespaceName": "[format('ld-obs-{0}', variables('suffix'))]",
          "eventHubName": "launchdarkly-diagnostics",
          "storageAccountName": "[format('ldobs{0}', variables('suffix'))]",
          "functionAppName": "[format('ld-obs-func-{0}', variables('suffix'))]",
          "hostingPlanName": "[format('ld-obs-plan-{0}', variables('suffix'))]"
        },
        "resources": [
          {
            "type": "Microsoft.EventHub/namespaces",
            "apiVersion": "2022-10-01-preview",
            "name": "[variables('eventHubNamespaceName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard",
              "tier": "Standard",
              "capacity": 1
            }
          },
          {
            "type": "Microsoft.EventHub/namespaces/eventhubs",
            "apiVersion": "2022-10-01-preview",
            "name": "[format('{0}/{1}', variables('eventHubNamespaceName'), variables('eventHubName'))]",
            "dependsOn": ["[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"],
            "properties": {
              "messageRetentionInDays": 1,
              "partitionCount": 2
            }
          },
          {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2023-01-01",
            "name": "[variables('storageAccountName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Standard_LRS"
            },
            "kind": "StorageV2"
          },
          {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2023-01-01",
            "name": "[variables('hostingPlanName')]",
            "location": "[parameters('location')]",
            "sku": {
              "name": "Y1",
              "tier": "Dynamic"
            },
            "properties": {}
          },
          {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2023-01-01",
            "name": "[variables('functionAppName')]",
            "location": "[parameters('location')]",
            "kind": "functionapp",
            "dependsOn": [
              "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
              "[resourceId('Microsoft.EventHub/namespaces', variables('eventHubNamespaceName'))]"
            ],
            "properties": {
              "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
              "siteConfig": {
                "appSettings": [
                  {
                    "name": "AzureWebJobsStorage",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};AccountKey={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-01-01').keys[0].value)]"
                  },
                  {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('functionAppName'))]"
                  },
                  {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~4"
                  },
                  {
                    "name": "FUNCTIONS_WORKER_RUNTIME",
                    "value": "node"
                  },
                  {
                    "name": "WEBSITE_NODE_DEFAULT_VERSION",
                    "value": "~22"
                  },
                  {
                    "name": "EventHubConnection",
                    "value": "[listKeys(resourceId('Microsoft.EventHub/namespaces/authorizationRules', variables('eventHubNamespaceName'), 'RootManageSharedAccessKey'), '2022-10-01-preview').primaryConnectionString]"
                  },
                  {
                    "name": "LAUNCHDARKLY_CLIENT_SIDE_ID",
                    "value": "[parameters('launchDarklyClientSideId')]"
                  },
                  {
                    "name": "LAUNCHDARKLY_SERVICE",
                    "value": "azure"
                  }
                ]
              }
            }
          },
          {
            "type": "Microsoft.Web/sites/functions",
            "apiVersion": "2023-01-01",
            "name": "[format('{0}/ForwardToLaunchDarkly', variables('functionAppName'))]",
            "dependsOn": [
              "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]"
            ],
            "properties": {
              "config": {
                "bindings": [
                  {
                    "type": "eventHubTrigger",
                    "name": "eventHubMessages",
                    "direction": "in",
                    "eventHubName": "[variables('eventHubName')]",
                    "connection": "EventHubConnection",
                    "cardinality": "many",
                    "consumerGroup": "$Default"
                  }
                ]
              },
              "files": {
                "index.js": "const https = require('https')\n\nconst PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID\nconst SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'\nconst OTEL_HOSTNAME = 'otel.observability.app.eu.launchdarkly.com'\nconst MAX_RETRIES = 6\nconst RETRY_INTERVAL = 1000\n\nconst SEVERITY_MAP = {\n  verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',\n  info: 'INFO', warning: 'WARN', warn: 'WARN',\n  error: 'ERROR', critical: 'FATAL',\n}\n\nfunction sendWithRetries(context, path, payload) {\n  const options = {\n    hostname: OTEL_HOSTNAME,\n    path,\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'x-launchdarkly-project': PROJECT_ID,\n    },\n  }\n  let numRetries = MAX_RETRIES\n  let retryInterval = RETRY_INTERVAL\n  return new Promise((resolve, reject) => {\n    const sendRequest = () => {\n      const retryRequest = errMsg => {\n        if (numRetries === 0) return reject(errMsg)\n        context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)\n        numRetries--\n        retryInterval *= 2\n        setTimeout(sendRequest, retryInterval)\n      }\n      const req = https\n        .request(options, resp => {\n          if (resp.statusCode >= 200 && resp.statusCode <= 299) {\n            resolve(true)\n          } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {\n            reject(`HTTP ${resp.statusCode}`)\n          } else {\n            retryRequest(`HTTP ${resp.statusCode}`)\n          }\n        })\n        .on('error', error => retryRequest(error.message))\n        .on('timeout', () => {\n          req.destroy()\n          retryRequest('request timed out')\n        })\n      req.write(JSON.stringify(payload))\n      req.end()\n    }\n    sendRequest()\n  })\n}\n\nconst toNanos = ts => String(new Date(ts).getTime() * 1000000)\nconst toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })\n\nfunction parseRecords(eventHubMessages) {\n  const logs = []\n  const metrics = []\n  for (const message of eventHubMessages) {\n    const items = message.records || [message]\n    for (const item of items) {\n      const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()\n\n      if (item.metricName) {\n        const dimensions = (item.dimensions || []).reduce((acc, d) => {\n          acc[`azure.dimension.${d.name}`] = d.value\n          return acc\n        }, {})\n        metrics.push({\n          name: `azure.${item.metricName}`,\n          timestamp,\n          count: item.count ?? 1,\n          sum: item.total ?? item.average ?? 0,\n          min: item.minimum,\n          max: item.maximum,\n          attributes: {\n            'azure.resource.id': item.resourceId,\n            'azure.namespace': item.namespace,\n            'azure.timeGrain': item.timeGrain,\n            ...dimensions,\n          },\n        })\n      } else {\n        const level = (item.level || item.resultType || 'info').toLowerCase()\n        let props = {}\n        if (item.properties) {\n          try {\n            props = typeof item.properties === 'string'\n              ? JSON.parse(item.properties) : item.properties\n          } catch (_) { /* ignore parse errors */ }\n        }\n        let message = item.resultDescription || item.operationName\n        if (!message && props.CsMethod) {\n          message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()\n        }\n        if (!message) message = JSON.stringify(item)\n        const attrs = {\n          'azure.resourceId': item.resourceId,\n          'azure.category': item.category,\n          'azure.operationName': item.operationName,\n        }\n        for (const [k, v] of Object.entries(props)) {\n          if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)\n        }\n        logs.push({\n          message,\n          timestamp,\n          severityText: SEVERITY_MAP[level] || 'INFO',\n          attributes: attrs,\n        })\n      }\n    }\n  }\n  return { logs, metrics }\n}\n\nfunction buildOtlpLogs(logs) {\n  return {\n    resourceLogs: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeLogs: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        logRecords: logs.map(l => ({\n          timeUnixNano: toNanos(l.timestamp),\n          body: { stringValue: l.message },\n          severityText: l.severityText,\n          attributes: Object.entries(l.attributes)\n            .filter(([, v]) => v != null)\n            .map(([k, v]) => toAttr(k, v)),\n        })),\n      }],\n    }],\n  }\n}\n\nfunction buildOtlpMetrics(metrics) {\n  return {\n    resourceMetrics: [{\n      resource: {\n        attributes: [\n          toAttr('service.name', SERVICE),\n          toAttr('highlight.project_id', PROJECT_ID),\n        ],\n      },\n      scopeMetrics: [{\n        scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },\n        metrics: metrics.map(m => {\n          const dp = {\n            timeUnixNano: toNanos(m.timestamp),\n            count: m.count,\n            sum: m.sum,\n            explicitBounds: [],\n            bucketCounts: [m.count],\n            attributes: Object.entries(m.attributes)\n              .filter(([, v]) => v != null)\n              .map(([k, v]) => toAttr(k, v)),\n          }\n          if (m.min != null) dp.min = m.min\n          if (m.max != null) dp.max = m.max\n          return {\n            name: m.name,\n            histogram: {\n              aggregationTemporality: 1,\n              dataPoints: [dp],\n            },\n          }\n        }),\n      }],\n    }],\n  }\n}\n\nmodule.exports = async function (context, eventHubMessages) {\n  if (!PROJECT_ID) {\n    context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')\n    return\n  }\n\n  const { logs, metrics } = parseRecords(eventHubMessages)\n  const promises = []\n\n  if (logs.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  if (metrics.length > 0) {\n    promises.push(\n      sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))\n        .catch(e => context.log.error(e))\n    )\n  }\n\n  const results = await Promise.all(promises)\n  if (!results.every(v => v === true)) {\n    context.log.error('Failed to send some records')\n  }\n}"
              }
            }
          }
        ],
        "outputs": {
          "functionAppName": {
            "type": "string",
            "value": "[variables('functionAppName')]"
          },
          "eventHubNamespaceName": {
            "type": "string",
            "value": "[variables('eventHubNamespaceName')]"
          },
          "eventHubName": {
            "type": "string",
            "value": "[variables('eventHubName')]"
          }
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  The template includes an inline Azure Function that forwards data to LaunchDarkly. To view a readable version of the function source code, read [Function code reference](#function-code-reference) below.

  ## Configure Azure Diagnostic Settings

  After deploying the integration, configure your Azure resources to stream diagnostic data to the Event Hub.

  To configure Diagnostic Settings:

  1. Open the [Azure Portal](https://portal.azure.com/).
  2. Navigate to the Azure resource you want to monitor, such as an App Service, Virtual Machine, or SQL Database.
  3. Click **Diagnostic settings** under **Monitoring** in the left menu.
  4. Click **Add diagnostic setting**.
  5. Enter a name, such as `launchdarkly-observability`.
  6. Under **Logs**, select the log categories you want to forward.
  7. Under **Metrics**, check **AllMetrics**.
  8. Under **Destination details**, check **Stream to an event hub**.
  9. Select the Event Hub namespace created by the ARM template.
  10. Select `launchdarkly-diagnostics` as the **Event Hub name**.
  11. Select `RootManageSharedAccessKey` as the **Event Hub policy name**.
  12. Click **Save**.

  Repeat these steps for each Azure resource you want to monitor.

  <Tip>
    **Configuring diagnostics at scale**

    For large environments, use Azure Policy to automatically deploy diagnostic settings to resources as you create them. To learn more, read [Create diagnostic settings at scale using Azure Policy](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/diagnostic-settings-policy) in the Microsoft documentation.
  </Tip>

  ## What the template creates

  The ARM template creates the following Azure resources:

  * **Event Hub Namespace and Event Hub**: Receives diagnostic data streamed from your Azure resources.
  * **Function App with forwarding function (Node.js 22)**: Serverless function that triggers on Event Hub messages, separates logs from metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector, deployed automatically by the template.
  * **App Service Plan (Consumption)**: Serverless hosting for the Function App with pay-per-execution pricing.
  * **Storage Account**: Required for Function App state and trigger checkpointing.

  All resource names are auto-generated using a unique suffix derived from your resource group ID.

  <Tip>
    **Customizing the compute resources**

    The template uses a Consumption (serverless) plan for pay-per-execution pricing, which is sufficient for most workloads. You can modify the template to use a different hosting plan based on your requirements. To learn more, read [Azure Functions hosting options](https://learn.microsoft.com/en-us/azure/azure-functions/functions-scale) in the Microsoft documentation.
  </Tip>

  ## View data in LaunchDarkly observability dashboards

  After deployment, data appears in the LaunchDarkly observability dashboard within 1-2 minutes of configuring Diagnostic Settings.

  To view your data:

  1. Navigate to **Observability** in LaunchDarkly.
  2. Select the environment you configured.
  3. Navigate to the **Logs** tab to view Azure diagnostic logs such as HTTP request logs, console output, and audit events.
  4. Navigate to the **Metrics** tab to view Azure platform metrics such as CPU percentage, memory usage, and request counts.
  5. Use the search and filter controls to query your Azure data.

  Log records include attributes such as `azure.resourceId`, `azure.category`, and `azure.operationName` for filtering. Metric records use names following the pattern `azure.<metricName>`, for example `azure.CpuPercentage` or `azure.Http2xx`, and are sent as OTLP histograms with sum, count, min, and max values. Metrics include attributes such as `azure.resource.id`, `azure.namespace`, and dimension attributes such as `azure.dimension.ActivityName`.

  All records include a `service.name` attribute set to `azure` by default. To distinguish data from multiple Azure subscriptions or environments, change the `LAUNCHDARKLY_SERVICE` application setting in the Function App to a unique value such as `azure-production` or `azure-staging`.

  ## Control data volume (optional)

  By default, only those Azure resources that use Diagnostic Settings send data to LaunchDarkly. To control data volume, enable Diagnostic Settings only on the resources you want to monitor.

  You can also filter within Diagnostic Settings by selecting specific log categories instead of all categories. For example, you might select only **AppServiceHTTPLogs** and **AppServiceConsoleLogs** for an App Service, omitting audit and platform logs.

  ## Function code reference

  The ARM template deploys a Node.js Azure Function that parses Azure Monitor diagnostic records, separates them into logs and metrics, and forwards both as OTLP JSON to the LaunchDarkly OpenTelemetry collector at `/v1/logs` and `/v1/metrics`. Expand this section to open or review the function source code.

  <Accordion title="Expand function source code">
    <CodeGroup>
      ```js title="ForwardToLaunchDarkly/index.js" expandable lines wrap theme={null}
      const https = require('https')

      const PROJECT_ID = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID
      const SERVICE = process.env.LAUNCHDARKLY_SERVICE || 'azure'
      const OTEL_HOSTNAME = 'otel.observability.app.eu.launchdarkly.com'
      const MAX_RETRIES = 6
      const RETRY_INTERVAL = 1000

      const SEVERITY_MAP = {
        verbose: 'TRACE', debug: 'DEBUG', informational: 'INFO',
        info: 'INFO', warning: 'WARN', warn: 'WARN',
        error: 'ERROR', critical: 'FATAL',
      }

      function sendWithRetries(context, path, payload) {
        const options = {
          hostname: OTEL_HOSTNAME,
          path,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-launchdarkly-project': PROJECT_ID,
          },
        }
        let numRetries = MAX_RETRIES
        let retryInterval = RETRY_INTERVAL
        return new Promise((resolve, reject) => {
          const sendRequest = () => {
            const retryRequest = errMsg => {
              if (numRetries === 0) return reject(errMsg)
              context.log.warn(`Request failed: ${errMsg}. Retrying ${numRetries} more times`)
              numRetries--
              retryInterval *= 2
              setTimeout(sendRequest, retryInterval)
            }
            const req = https
              .request(options, resp => {
                if (resp.statusCode >= 200 && resp.statusCode <= 299) {
                  resolve(true)
                } else if (resp.statusCode >= 400 && resp.statusCode <= 499) {
                  reject(`HTTP ${resp.statusCode}`)
                } else {
                  retryRequest(`HTTP ${resp.statusCode}`)
                }
              })
              .on('error', error => retryRequest(error.message))
              .on('timeout', () => {
                req.destroy()
                retryRequest('request timed out')
              })
            req.write(JSON.stringify(payload))
            req.end()
          }
          sendRequest()
        })
      }

      const toNanos = ts => String(new Date(ts).getTime() * 1000000)
      const toAttr = (k, v) => ({ key: k, value: { stringValue: String(v) } })

      function parseRecords(eventHubMessages) {
        const logs = []
        const metrics = []
        for (const message of eventHubMessages) {
          const items = message.records || [message]
          for (const item of items) {
            const timestamp = new Date(item.time || item.timeStamp || Date.now()).toISOString()

            if (item.metricName) {
              const dimensions = (item.dimensions || []).reduce((acc, d) => {
                acc[`azure.dimension.${d.name}`] = d.value
                return acc
              }, {})
              metrics.push({
                name: `azure.${item.metricName}`,
                timestamp,
                count: item.count ?? 1,
                sum: item.total ?? item.average ?? 0,
                min: item.minimum,
                max: item.maximum,
                attributes: {
                  'azure.resource.id': item.resourceId,
                  'azure.namespace': item.namespace,
                  'azure.timeGrain': item.timeGrain,
                  ...dimensions,
                },
              })
            } else {
              const level = (item.level || item.resultType || 'info').toLowerCase()
              let props = {}
              if (item.properties) {
                try {
                  props = typeof item.properties === 'string'
                    ? JSON.parse(item.properties) : item.properties
                } catch (_) { /* ignore parse errors */ }
              }
              let message = item.resultDescription || item.operationName
              if (!message && props.CsMethod) {
                message = `${props.CsMethod} ${props.CsUriStem || '/'} ${props.ScStatus || ''} ${props.TimeTaken != null ? props.TimeTaken + 'ms' : ''}`.trim()
              }
              if (!message) message = JSON.stringify(item)
              const attrs = {
                'azure.resourceId': item.resourceId,
                'azure.category': item.category,
                'azure.operationName': item.operationName,
              }
              for (const [k, v] of Object.entries(props)) {
                if (v != null && v !== '') attrs[`azure.properties.${k}`] = String(v)
              }
              logs.push({
                message,
                timestamp,
                severityText: SEVERITY_MAP[level] || 'INFO',
                attributes: attrs,
              })
            }
          }
        }
        return { logs, metrics }
      }

      function buildOtlpLogs(logs) {
        return {
          resourceLogs: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeLogs: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              logRecords: logs.map(l => ({
                timeUnixNano: toNanos(l.timestamp),
                body: { stringValue: l.message },
                severityText: l.severityText,
                attributes: Object.entries(l.attributes)
                  .filter(([, v]) => v != null)
                  .map(([k, v]) => toAttr(k, v)),
              })),
            }],
          }],
        }
      }

      function buildOtlpMetrics(metrics) {
        return {
          resourceMetrics: [{
            resource: {
              attributes: [
                toAttr('service.name', SERVICE),
                toAttr('highlight.project_id', PROJECT_ID),
              ],
            },
            scopeMetrics: [{
              scope: { name: 'azure-monitor-forwarder', version: '1.0.0' },
              metrics: metrics.map(m => {
                const dp = {
                  timeUnixNano: toNanos(m.timestamp),
                  count: m.count,
                  sum: m.sum,
                  explicitBounds: [],
                  bucketCounts: [m.count],
                  attributes: Object.entries(m.attributes)
                    .filter(([, v]) => v != null)
                    .map(([k, v]) => toAttr(k, v)),
                }
                if (m.min != null) dp.min = m.min
                if (m.max != null) dp.max = m.max
                return {
                  name: m.name,
                  histogram: {
                    aggregationTemporality: 1,
                    dataPoints: [dp],
                  },
                }
              }),
            }],
          }],
        }
      }

      module.exports = async function (context, eventHubMessages) {
        if (!PROJECT_ID) {
          context.log.error('LAUNCHDARKLY_CLIENT_SIDE_ID is not configured.')
          return
        }

        const { logs, metrics } = parseRecords(eventHubMessages)
        const promises = []

        if (logs.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/logs', buildOtlpLogs(logs))
              .catch(e => context.log.error(e))
          )
        }

        if (metrics.length > 0) {
          promises.push(
            sendWithRetries(context, '/v1/metrics', buildOtlpMetrics(metrics))
              .catch(e => context.log.error(e))
          )
        }

        const results = await Promise.all(promises)
        if (!results.every(v => v === true)) {
          context.log.error('Failed to send some records')
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  ## Troubleshooting

  If data does not appear in LaunchDarkly after configuring Diagnostic Settings:

  1. **Verify the Event Hub is receiving data.** Open the Event Hub namespace in the Azure Portal and check the **Messages** chart under **Overview**. If no messages appear, check your Diagnostic Settings configuration.

  2. **Verify the Function App is running.** Open the Function App in the Azure Portal and navigate to **Functions** > **ForwardToLaunchDarkly** > **Monitor** for invocation logs. If invocations fail, check the error messages.

  3. **Verify the client-side ID.** Confirm that the `LAUNCHDARKLY_CLIENT_SIDE_ID` application setting in the Function App matches your LaunchDarkly environment's client-side ID.

  4. **Check Function App logs.** Open the Function App in the Azure Portal and navigate to **Log stream** under **Monitoring** to view real-time logs from the function.
</View>
