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
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 withnode --import ./dist/telemetry.js dist/server.jsfor ES modules; for CommonJS, calluseMicrosoftOpenTelemetrybefore 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: 0withsamplingRatio(0–1) switches to percentage sampling;1keeps everything and costs more. - On App Service codeless monitoring, Entra authentication is set with the app setting
APPLICATIONINSIGHTS_AUTHENTICATION_STRING=Authorization=AAD(system-assigned) orAuthorization=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.