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

Send traces, metrics and logs from a TypeScript Node.js app to Application Insights without a key — a Bicep workspace and workspace-based Application Insights with local authentication off and the Monitoring Metrics Publisher role, the Microsoft OpenTelemetry Distro loaded before the app with a managed identity credential, the 5-traces-per-second default sampling and how to change it, custom spans and metrics, and how to tell where missing telemetry stops.

markdownguidev15KJZ-RT2Kstage: buildservice: monitorinterface: sdkinterface: iaclanguages: typescriptOpenTelemetry Node.js Azure Monitor@microsoft/opentelemetryuseMicrosoftOpenTelemetry@microsoft/opentelemetry/loadernode --import telemetryAPPLICATIONINSIGHTS_CONNECTION_STRINGApplication Insights Entra authentication Node.jsDisableLocalAuth Application InsightsMonitoring Metrics Publisher3913510d-42f4-4e42-8a64-420c390055ebtracesPerSecond default 5samplingRatiorate-limited samplingcustom spancustom metric counterMicrosoft.Insights/components Bicep WorkspaceResourceIdMicrosoft.OperationalInsights/workspaces@2026-03-01APPLICATIONINSIGHTS_AUTHENTICATION_STRINGApplication Insights no data Node.jsAPPLICATIONINSIGHTS_INSTRUMENTATION_LOGGING_LEVEL

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

1. Workspace, Application Insights and the publish role

// 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

// 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

CheckHow
Telemetry arrivesIn the workspace: `AppRequestswhere TimeGenerated > ago(15m)take 10 (workspace-based tables are AppRequests, AppDependencies, AppTraces, AppExceptions, AppMetrics`)
The exporter's own errorsAPPLICATIONINSIGHTS_INSTRUMENTATION_LOGGING_LEVEL=VERBOSE (with APPLICATIONINSIGHTS_LOG_DESTINATION=file and APPLICATIONINSIGHTS_LOGDIR), or diag.setLogger from @opentelemetry/api
Ingestion rejects itHTTP 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.

Read this document as markdown — the same bytes an agent receives.