---
cuppy_hub: azure
cuppy_path: guides/monitor/instrument-node-app-with-opentelemetry.md
cuppy_section: docs
cuppy_doc: 5KJZ-RT2K
cuppy_doc_uuid: 63d4d4e9-62e9-4171-b122-3e430a1408bc
cuppy_permalink: https://cuppy.io/azure/d/5KJZ-RT2K
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/monitor/instrument-node-app-with-opentelemetry
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 2c83a19dbc1e9a25d5afec59e921e4572dfe328aaccb63b5d96831f2a2f62c51
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Instrument a Node.js app with OpenTelemetry and keyless Application Insights

## 1. Workspace, Application Insights and the publish role

```bicep
// Log Analytics workspace + workspace-based Application Insights with local auth off, and the app identity's publish role
param location string = resourceGroup().location
param baseName string
param appPrincipalId string // the app's managed identity (principal ID)

resource workspace 'Microsoft.OperationalInsights/workspaces@2026-03-01' = {
  name: 'log-${baseName}'
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
    features: {
      enableLogAccessUsingOnlyResourcePermissions: true // resource-context permissions apply
    }
    workspaceCapping: {
      dailyQuotaGb: 5 // a budget guard: collection stops when reached
    }
  }
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: 'appi-${baseName}'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: workspace.id
    IngestionMode: 'LogAnalytics'
    DisableLocalAuth: true // only Entra-authenticated telemetry is accepted
  }
}

// Monitoring Metrics Publisher: publishes all telemetry, despite the name
var metricsPublisher = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb')

resource publish 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: appInsights
  name: guid(appInsights.id, appPrincipalId, metricsPublisher)
  properties: {
    roleDefinitionId: metricsPublisher
    principalId: appPrincipalId
    principalType: 'ServicePrincipal'
  }
}

output connectionString string = appInsights.properties.ConnectionString
output workspaceCustomerId string = workspace.properties.customerId
```

`DisableLocalAuth: true` makes the ingestion endpoint accept **only** Entra-authenticated telemetry; the connection string then identifies the resource but grants nothing. Give the app's identity **Monitoring Metrics Publisher** (it publishes all telemetry types, despite the name). The connection string isn't a secret, but keep it in configuration, not code.

## 2. Load the distro before the app

```ts
// telemetry.ts: load first (node --import ./dist/telemetry.js dist/server.js)
import "@microsoft/opentelemetry/loader"; // ESM: registers instrumentation hooks before app modules load
import { useMicrosoftOpenTelemetry } from "@microsoft/opentelemetry";
import { ManagedIdentityCredential } from "@azure/identity";
import { metrics, trace, SpanStatusCode } from "@opentelemetry/api";

useMicrosoftOpenTelemetry({
  samplingRatio: 1,
  tracesPerSecond: 0, // default is 5 traces/s rate-limited; 0 switches to samplingRatio
  azureMonitor: {
    azureMonitorExporterOptions: {
      connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING, // not a secret, but per resource
      // The identity needs Monitoring Metrics Publisher on the Application Insights resource
      credential: process.env.AZURE_CLIENT_ID
        ? new ManagedIdentityCredential({ clientId: process.env.AZURE_CLIENT_ID })
        : new ManagedIdentityCredential(),
    },
  },
});

const tracer = trace.getTracer("orders");
const ordersPlaced = metrics.getMeter("orders").createCounter("orders_placed");

export async function placeOrder(customerId: string): Promise<void> {
  await tracer.startActiveSpan("placeOrder", async (span) => {
    try {
      span.setAttribute("app.customer_id", customerId);
      ordersPlaced.add(1, { channel: "web" });
    } catch (e) {
      span.recordException(e as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw e;
    } finally {
      span.end();
    }
  });
}
```

- Install `@microsoft/opentelemetry` (needs **Node.js 22+**) and `@azure/identity`. Start with `node --import ./dist/telemetry.js dist/server.js` for ES modules; for CommonJS, call `useMicrosoftOpenTelemetry` before requiring anything you want instrumented. **Don't also initialize `@azure/monitor-opentelemetry`.**
- **Sampling:** the distro **defaults to 5 traces per second** (rate-limited). Low-traffic services see everything; busy ones keep a sample, and transaction views look incomplete. `tracesPerSecond: 0` with `samplingRatio` (0–1) switches to percentage sampling; `1` keeps everything and costs more.
- On App Service codeless monitoring, Entra authentication is set with the app setting `APPLICATIONINSIGHTS_AUTHENTICATION_STRING` = `Authorization=AAD` (system-assigned) or `Authorization=AAD;ClientId=<client id>` (user-assigned).

## 3. Check it

| Check | How |
|---|---|
| Telemetry arrives | In the workspace: `AppRequests | where TimeGenerated > ago(15m) | take 10` (workspace-based tables are `AppRequests`, `AppDependencies`, `AppTraces`, `AppExceptions`, `AppMetrics`) |
| The exporter's own errors | `APPLICATIONINSIGHTS_INSTRUMENTATION_LOGGING_LEVEL=VERBOSE` (with `APPLICATIONINSIGHTS_LOG_DESTINATION=file` and `APPLICATIONINSIGHTS_LOGDIR`), or `diag.setLogger` from `@opentelemetry/api` |
| Ingestion rejects it | HTTP 400 `Authentication not supported`, 401 or 403 in those logs: `reference/monitor/application-insights-errors.md` |

## Next

Errors and limits: `reference/monitor/application-insights-errors.md`. Querying the data from code: `reference/monitor/log-analytics-workspaces.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
